site/node_modules/unified/lib/index.js

1324 lines
40 KiB
JavaScript
Raw Normal View History

2024-10-14 06:09:33 +00:00
/**
* @typedef {import('trough').Pipeline} Pipeline
*
* @typedef {import('unist').Node} Node
*
* @typedef {import('vfile').Compatible} Compatible
* @typedef {import('vfile').Value} Value
*
* @typedef {import('../index.js').CompileResultMap} CompileResultMap
* @typedef {import('../index.js').Data} Data
* @typedef {import('../index.js').Settings} Settings
*/
/**
* @typedef {CompileResultMap[keyof CompileResultMap]} CompileResults
* Acceptable results from compilers.
*
* To register custom results, add them to
* {@link CompileResultMap `CompileResultMap`}.
*/
/**
* @template {Node} [Tree=Node]
* The node that the compiler receives (default: `Node`).
* @template {CompileResults} [Result=CompileResults]
* The thing that the compiler yields (default: `CompileResults`).
* @callback Compiler
* A **compiler** handles the compiling of a syntax tree to something else
* (in most cases, text) (TypeScript type).
*
* It is used in the stringify phase and called with a {@link Node `Node`}
* and {@link VFile `VFile`} representation of the document to compile.
* It should return the textual representation of the given tree (typically
* `string`).
*
* > 👉 **Note**: unified typically compiles by serializing: most compilers
* > return `string` (or `Uint8Array`).
* > Some compilers, such as the one configured with
* > [`rehype-react`][rehype-react], return other values (in this case, a
* > React tree).
* > If youre using a compiler that doesnt serialize, expect different
* > result values.
* >
* > To register custom results in TypeScript, add them to
* > {@link CompileResultMap `CompileResultMap`}.
*
* [rehype-react]: https://github.com/rehypejs/rehype-react
* @param {Tree} tree
* Tree to compile.
* @param {VFile} file
* File associated with `tree`.
* @returns {Result}
* New content: compiled text (`string` or `Uint8Array`, for `file.value`) or
* something else (for `file.result`).
*/
/**
* @template {Node} [Tree=Node]
* The node that the parser yields (default: `Node`)
* @callback Parser
* A **parser** handles the parsing of text to a syntax tree.
*
* It is used in the parse phase and is called with a `string` and
* {@link VFile `VFile`} of the document to parse.
* It must return the syntax tree representation of the given file
* ({@link Node `Node`}).
* @param {string} document
* Document to parse.
* @param {VFile} file
* File associated with `document`.
* @returns {Tree}
* Node representing the given file.
*/
/**
* @typedef {(
* Plugin<Array<any>, any, any> |
* PluginTuple<Array<any>, any, any> |
* Preset
* )} Pluggable
* Union of the different ways to add plugins and settings.
*/
/**
* @typedef {Array<Pluggable>} PluggableList
* List of plugins and presets.
*/
// Note: we cant use `callback` yet as it messes up `this`:
// <https://github.com/microsoft/TypeScript/issues/55197>.
/**
* @template {Array<unknown>} [PluginParameters=[]]
* Arguments passed to the plugin (default: `[]`, the empty tuple).
* @template {Node | string | undefined} [Input=Node]
* Value that is expected as input (default: `Node`).
*
* * If the plugin returns a {@link Transformer `Transformer`}, this
* should be the node it expects.
* * If the plugin sets a {@link Parser `Parser`}, this should be
* `string`.
* * If the plugin sets a {@link Compiler `Compiler`}, this should be the
* node it expects.
* @template [Output=Input]
* Value that is yielded as output (default: `Input`).
*
* * If the plugin returns a {@link Transformer `Transformer`}, this
* should be the node that that yields.
* * If the plugin sets a {@link Parser `Parser`}, this should be the
* node that it yields.
* * If the plugin sets a {@link Compiler `Compiler`}, this should be
* result it yields.
* @typedef {(
* (this: Processor, ...parameters: PluginParameters) =>
* Input extends string ? // Parser.
* Output extends Node | undefined ? undefined | void : never :
* Output extends CompileResults ? // Compiler.
* Input extends Node | undefined ? undefined | void : never :
* Transformer<
* Input extends Node ? Input : Node,
* Output extends Node ? Output : Node
* > | undefined | void
* )} Plugin
* Single plugin.
*
* Plugins configure the processors they are applied on in the following
* ways:
*
* * they change the processor, such as the parser, the compiler, or by
* configuring data
* * they specify how to handle trees and files
*
* In practice, they are functions that can receive options and configure the
* processor (`this`).
*
* > 👉 **Note**: plugins are called when the processor is *frozen*, not when
* > they are applied.
*/
/**
* Tuple of a plugin and its configuration.
*
* The first item is a plugin, the rest are its parameters.
*
* @template {Array<unknown>} [TupleParameters=[]]
* Arguments passed to the plugin (default: `[]`, the empty tuple).
* @template {Node | string | undefined} [Input=undefined]
* Value that is expected as input (optional).
*
* * If the plugin returns a {@link Transformer `Transformer`}, this
* should be the node it expects.
* * If the plugin sets a {@link Parser `Parser`}, this should be
* `string`.
* * If the plugin sets a {@link Compiler `Compiler`}, this should be the
* node it expects.
* @template [Output=undefined] (optional).
* Value that is yielded as output.
*
* * If the plugin returns a {@link Transformer `Transformer`}, this
* should be the node that that yields.
* * If the plugin sets a {@link Parser `Parser`}, this should be the
* node that it yields.
* * If the plugin sets a {@link Compiler `Compiler`}, this should be
* result it yields.
* @typedef {(
* [
* plugin: Plugin<TupleParameters, Input, Output>,
* ...parameters: TupleParameters
* ]
* )} PluginTuple
*/
/**
* @typedef Preset
* Sharable configuration.
*
* They can contain plugins and settings.
* @property {PluggableList | undefined} [plugins]
* List of plugins and presets (optional).
* @property {Settings | undefined} [settings]
* Shared settings for parsers and compilers (optional).
*/
/**
* @template {VFile} [File=VFile]
* The file that the callback receives (default: `VFile`).
* @callback ProcessCallback
* Callback called when the process is done.
*
* Called with either an error or a result.
* @param {Error | undefined} [error]
* Fatal error (optional).
* @param {File | undefined} [file]
* Processed file (optional).
* @returns {undefined}
* Nothing.
*/
/**
* @template {Node} [Tree=Node]
* The tree that the callback receives (default: `Node`).
* @callback RunCallback
* Callback called when transformers are done.
*
* Called with either an error or results.
* @param {Error | undefined} [error]
* Fatal error (optional).
* @param {Tree | undefined} [tree]
* Transformed tree (optional).
* @param {VFile | undefined} [file]
* File (optional).
* @returns {undefined}
* Nothing.
*/
/**
* @template {Node} [Output=Node]
* Node type that the transformer yields (default: `Node`).
* @callback TransformCallback
* Callback passed to transforms.
*
* If the signature of a `transformer` accepts a third argument, the
* transformer may perform asynchronous operations, and must call it.
* @param {Error | undefined} [error]
* Fatal error to stop the process (optional).
* @param {Output | undefined} [tree]
* New, changed, tree (optional).
* @param {VFile | undefined} [file]
* New, changed, file (optional).
* @returns {undefined}
* Nothing.
*/
/**
* @template {Node} [Input=Node]
* Node type that the transformer expects (default: `Node`).
* @template {Node} [Output=Input]
* Node type that the transformer yields (default: `Input`).
* @callback Transformer
* Transformers handle syntax trees and files.
*
* They are functions that are called each time a syntax tree and file are
* passed through the run phase.
* When an error occurs in them (either because its thrown, returned,
* rejected, or passed to `next`), the process stops.
*
* The run phase is handled by [`trough`][trough], see its documentation for
* the exact semantics of these functions.
*
* > 👉 **Note**: you should likely ignore `next`: dont accept it.
* > it supports callback-style async work.
* > But promises are likely easier to reason about.
*
* [trough]: https://github.com/wooorm/trough#function-fninput-next
* @param {Input} tree
* Tree to handle.
* @param {VFile} file
* File to handle.
* @param {TransformCallback<Output>} next
* Callback.
* @returns {(
* Promise<Output | undefined | void> |
* Promise<never> | // For some reason this is needed separately.
* Output |
* Error |
* undefined |
* void
* )}
* If you accept `next`, nothing.
* Otherwise:
*
* * `Error` fatal error to stop the process
* * `Promise<undefined>` or `undefined` the next transformer keeps using
* same tree
* * `Promise<Node>` or `Node` new, changed, tree
*/
/**
* @template {Node | undefined} ParseTree
* Output of `parse`.
* @template {Node | undefined} HeadTree
* Input for `run`.
* @template {Node | undefined} TailTree
* Output for `run`.
* @template {Node | undefined} CompileTree
* Input of `stringify`.
* @template {CompileResults | undefined} CompileResult
* Output of `stringify`.
* @template {Node | string | undefined} Input
* Input of plugin.
* @template Output
* Output of plugin (optional).
* @typedef {(
* Input extends string
* ? Output extends Node | undefined
* ? // Parser.
* Processor<
* Output extends undefined ? ParseTree : Output,
* HeadTree,
* TailTree,
* CompileTree,
* CompileResult
* >
* : // Unknown.
* Processor<ParseTree, HeadTree, TailTree, CompileTree, CompileResult>
* : Output extends CompileResults
* ? Input extends Node | undefined
* ? // Compiler.
* Processor<
* ParseTree,
* HeadTree,
* TailTree,
* Input extends undefined ? CompileTree : Input,
* Output extends undefined ? CompileResult : Output
* >
* : // Unknown.
* Processor<ParseTree, HeadTree, TailTree, CompileTree, CompileResult>
* : Input extends Node | undefined
* ? Output extends Node | undefined
* ? // Transform.
* Processor<
* ParseTree,
* HeadTree extends undefined ? Input : HeadTree,
* Output extends undefined ? TailTree : Output,
* CompileTree,
* CompileResult
* >
* : // Unknown.
* Processor<ParseTree, HeadTree, TailTree, CompileTree, CompileResult>
* : // Unknown.
* Processor<ParseTree, HeadTree, TailTree, CompileTree, CompileResult>
* )} UsePlugin
* Create a processor based on the input/output of a {@link Plugin plugin}.
*/
/**
* @template {CompileResults | undefined} Result
* Node type that the transformer yields.
* @typedef {(
* Result extends Value | undefined ?
* VFile :
* VFile & {result: Result}
* )} VFileWithOutput
* Type to generate a {@link VFile `VFile`} corresponding to a compiler result.
*
* If a result that is not acceptable on a `VFile` is used, that will
* be stored on the `result` field of {@link VFile `VFile`}.
*/
import {bail} from 'bail'
import extend from 'extend'
import {ok as assert} from 'devlop'
import isPlainObj from 'is-plain-obj'
import {trough} from 'trough'
import {VFile} from 'vfile'
import {CallableInstance} from './callable-instance.js'
// To do: next major: drop `Compiler`, `Parser`: prefer lowercase.
// To do: we could start yielding `never` in TS when a parser is missing and
// `parse` is called.
// Currently, we allow directly setting `processor.parser`, which is untyped.
const own = {}.hasOwnProperty
/**
* @template {Node | undefined} [ParseTree=undefined]
* Output of `parse` (optional).
* @template {Node | undefined} [HeadTree=undefined]
* Input for `run` (optional).
* @template {Node | undefined} [TailTree=undefined]
* Output for `run` (optional).
* @template {Node | undefined} [CompileTree=undefined]
* Input of `stringify` (optional).
* @template {CompileResults | undefined} [CompileResult=undefined]
* Output of `stringify` (optional).
* @extends {CallableInstance<[], Processor<ParseTree, HeadTree, TailTree, CompileTree, CompileResult>>}
*/
export class Processor extends CallableInstance {
/**
* Create a processor.
*/
constructor() {
// If `Processor()` is called (w/o new), `copy` is called instead.
super('copy')
/**
* Compiler to use (deprecated).
*
* @deprecated
* Use `compiler` instead.
* @type {(
* Compiler<
* CompileTree extends undefined ? Node : CompileTree,
* CompileResult extends undefined ? CompileResults : CompileResult
* > |
* undefined
* )}
*/
this.Compiler = undefined
/**
* Parser to use (deprecated).
*
* @deprecated
* Use `parser` instead.
* @type {(
* Parser<ParseTree extends undefined ? Node : ParseTree> |
* undefined
* )}
*/
this.Parser = undefined
// Note: the following fields are considered private.
// However, they are needed for tests, and TSC generates an untyped
// `private freezeIndex` field for, which trips `type-coverage` up.
// Instead, we use `@deprecated` to visualize that they shouldnt be used.
/**
* Internal list of configured plugins.
*
* @deprecated
* This is a private internal property and should not be used.
* @type {Array<PluginTuple<Array<unknown>>>}
*/
this.attachers = []
/**
* Compiler to use.
*
* @type {(
* Compiler<
* CompileTree extends undefined ? Node : CompileTree,
* CompileResult extends undefined ? CompileResults : CompileResult
* > |
* undefined
* )}
*/
this.compiler = undefined
/**
* Internal state to track where we are while freezing.
*
* @deprecated
* This is a private internal property and should not be used.
* @type {number}
*/
this.freezeIndex = -1
/**
* Internal state to track whether were frozen.
*
* @deprecated
* This is a private internal property and should not be used.
* @type {boolean | undefined}
*/
this.frozen = undefined
/**
* Internal state.
*
* @deprecated
* This is a private internal property and should not be used.
* @type {Data}
*/
this.namespace = {}
/**
* Parser to use.
*
* @type {(
* Parser<ParseTree extends undefined ? Node : ParseTree> |
* undefined
* )}
*/
this.parser = undefined
/**
* Internal list of configured transformers.
*
* @deprecated
* This is a private internal property and should not be used.
* @type {Pipeline}
*/
this.transformers = trough()
}
/**
* Copy a processor.
*
* @deprecated
* This is a private internal method and should not be used.
* @returns {Processor<ParseTree, HeadTree, TailTree, CompileTree, CompileResult>}
* New *unfrozen* processor ({@link Processor `Processor`}) that is
* configured to work the same as its ancestor.
* When the descendant processor is configured in the future it does not
* affect the ancestral processor.
*/
copy() {
// Cast as the type parameters will be the same after attaching.
const destination =
/** @type {Processor<ParseTree, HeadTree, TailTree, CompileTree, CompileResult>} */ (
new Processor()
)
let index = -1
while (++index < this.attachers.length) {
const attacher = this.attachers[index]
destination.use(...attacher)
}
destination.data(extend(true, {}, this.namespace))
return destination
}
/**
* Configure the processor with info available to all plugins.
* Information is stored in an object.
*
* Typically, options can be given to a specific plugin, but sometimes it
* makes sense to have information shared with several plugins.
* For example, a list of HTML elements that are self-closing, which is
* needed during all phases.
*
* > 👉 **Note**: setting information cannot occur on *frozen* processors.
* > Call the processor first to create a new unfrozen processor.
*
* > 👉 **Note**: to register custom data in TypeScript, augment the
* > {@link Data `Data`} interface.
*
* @example
* This example show how to get and set info:
*
* ```js
* import {unified} from 'unified'
*
* const processor = unified().data('alpha', 'bravo')
*
* processor.data('alpha') // => 'bravo'
*
* processor.data() // => {alpha: 'bravo'}
*
* processor.data({charlie: 'delta'})
*
* processor.data() // => {charlie: 'delta'}
* ```
*
* @template {keyof Data} Key
*
* @overload
* @returns {Data}
*
* @overload
* @param {Data} dataset
* @returns {Processor<ParseTree, HeadTree, TailTree, CompileTree, CompileResult>}
*
* @overload
* @param {Key} key
* @returns {Data[Key]}
*
* @overload
* @param {Key} key
* @param {Data[Key]} value
* @returns {Processor<ParseTree, HeadTree, TailTree, CompileTree, CompileResult>}
*
* @param {Data | Key} [key]
* Key to get or set, or entire dataset to set, or nothing to get the
* entire dataset (optional).
* @param {Data[Key]} [value]
* Value to set (optional).
* @returns {unknown}
* The current processor when setting, the value at `key` when getting, or
* the entire dataset when getting without key.
*/
data(key, value) {
if (typeof key === 'string') {
// Set `key`.
if (arguments.length === 2) {
assertUnfrozen('data', this.frozen)
this.namespace[key] = value
return this
}
// Get `key`.
return (own.call(this.namespace, key) && this.namespace[key]) || undefined
}
// Set space.
if (key) {
assertUnfrozen('data', this.frozen)
this.namespace = key
return this
}
// Get space.
return this.namespace
}
/**
* Freeze a processor.
*
* Frozen processors are meant to be extended and not to be configured
* directly.
*
* When a processor is frozen it cannot be unfrozen.
* New processors working the same way can be created by calling the
* processor.
*
* Its possible to freeze processors explicitly by calling `.freeze()`.
* Processors freeze automatically when `.parse()`, `.run()`, `.runSync()`,
* `.stringify()`, `.process()`, or `.processSync()` are called.
*
* @returns {Processor<ParseTree, HeadTree, TailTree, CompileTree, CompileResult>}
* The current processor.
*/
freeze() {
if (this.frozen) {
return this
}
// Cast so that we can type plugins easier.
// Plugins are supposed to be usable on different processors, not just on
// this exact processor.
const self = /** @type {Processor} */ (/** @type {unknown} */ (this))
while (++this.freezeIndex < this.attachers.length) {
const [attacher, ...options] = this.attachers[this.freezeIndex]
if (options[0] === false) {
continue
}
if (options[0] === true) {
options[0] = undefined
}
const transformer = attacher.call(self, ...options)
if (typeof transformer === 'function') {
this.transformers.use(transformer)
}
}
this.frozen = true
this.freezeIndex = Number.POSITIVE_INFINITY
return this
}
/**
* Parse text to a syntax tree.
*
* > 👉 **Note**: `parse` freezes the processor if not already *frozen*.
*
* > 👉 **Note**: `parse` performs the parse phase, not the run phase or other
* > phases.
*
* @param {Compatible | undefined} [file]
* file to parse (optional); typically `string` or `VFile`; any value
* accepted as `x` in `new VFile(x)`.
* @returns {ParseTree extends undefined ? Node : ParseTree}
* Syntax tree representing `file`.
*/
parse(file) {
this.freeze()
const realFile = vfile(file)
const parser = this.parser || this.Parser
assertParser('parse', parser)
return parser(String(realFile), realFile)
}
/**
* Process the given file as configured on the processor.
*
* > 👉 **Note**: `process` freezes the processor if not already *frozen*.
*
* > 👉 **Note**: `process` performs the parse, run, and stringify phases.
*
* @overload
* @param {Compatible | undefined} file
* @param {ProcessCallback<VFileWithOutput<CompileResult>>} done
* @returns {undefined}
*
* @overload
* @param {Compatible | undefined} [file]
* @returns {Promise<VFileWithOutput<CompileResult>>}
*
* @param {Compatible | undefined} [file]
* File (optional); typically `string` or `VFile`]; any value accepted as
* `x` in `new VFile(x)`.
* @param {ProcessCallback<VFileWithOutput<CompileResult>> | undefined} [done]
* Callback (optional).
* @returns {Promise<VFile> | undefined}
* Nothing if `done` is given.
* Otherwise a promise, rejected with a fatal error or resolved with the
* processed file.
*
* The parsed, transformed, and compiled value is available at
* `file.value` (see note).
*
* > 👉 **Note**: unified typically compiles by serializing: most
* > compilers return `string` (or `Uint8Array`).
* > Some compilers, such as the one configured with
* > [`rehype-react`][rehype-react], return other values (in this case, a
* > React tree).
* > If youre using a compiler that doesnt serialize, expect different
* > result values.
* >
* > To register custom results in TypeScript, add them to
* > {@link CompileResultMap `CompileResultMap`}.
*
* [rehype-react]: https://github.com/rehypejs/rehype-react
*/
process(file, done) {
const self = this
this.freeze()
assertParser('process', this.parser || this.Parser)
assertCompiler('process', this.compiler || this.Compiler)
return done ? executor(undefined, done) : new Promise(executor)
// Note: `void`s needed for TS.
/**
* @param {((file: VFileWithOutput<CompileResult>) => undefined | void) | undefined} resolve
* @param {(error: Error | undefined) => undefined | void} reject
* @returns {undefined}
*/
function executor(resolve, reject) {
const realFile = vfile(file)
// Assume `ParseTree` (the result of the parser) matches `HeadTree` (the
// input of the first transform).
const parseTree =
/** @type {HeadTree extends undefined ? Node : HeadTree} */ (
/** @type {unknown} */ (self.parse(realFile))
)
self.run(parseTree, realFile, function (error, tree, file) {
if (error || !tree || !file) {
return realDone(error)
}
// Assume `TailTree` (the output of the last transform) matches
// `CompileTree` (the input of the compiler).
const compileTree =
/** @type {CompileTree extends undefined ? Node : CompileTree} */ (
/** @type {unknown} */ (tree)
)
const compileResult = self.stringify(compileTree, file)
if (looksLikeAValue(compileResult)) {
file.value = compileResult
} else {
file.result = compileResult
}
realDone(error, /** @type {VFileWithOutput<CompileResult>} */ (file))
})
/**
* @param {Error | undefined} error
* @param {VFileWithOutput<CompileResult> | undefined} [file]
* @returns {undefined}
*/
function realDone(error, file) {
if (error || !file) {
reject(error)
} else if (resolve) {
resolve(file)
} else {
assert(done, '`done` is defined if `resolve` is not')
done(undefined, file)
}
}
}
}
/**
* Process the given file as configured on the processor.
*
* An error is thrown if asynchronous transforms are configured.
*
* > 👉 **Note**: `processSync` freezes the processor if not already *frozen*.
*
* > 👉 **Note**: `processSync` performs the parse, run, and stringify phases.
*
* @param {Compatible | undefined} [file]
* File (optional); typically `string` or `VFile`; any value accepted as
* `x` in `new VFile(x)`.
* @returns {VFileWithOutput<CompileResult>}
* The processed file.
*
* The parsed, transformed, and compiled value is available at
* `file.value` (see note).
*
* > 👉 **Note**: unified typically compiles by serializing: most
* > compilers return `string` (or `Uint8Array`).
* > Some compilers, such as the one configured with
* > [`rehype-react`][rehype-react], return other values (in this case, a
* > React tree).
* > If youre using a compiler that doesnt serialize, expect different
* > result values.
* >
* > To register custom results in TypeScript, add them to
* > {@link CompileResultMap `CompileResultMap`}.
*
* [rehype-react]: https://github.com/rehypejs/rehype-react
*/
processSync(file) {
/** @type {boolean} */
let complete = false
/** @type {VFileWithOutput<CompileResult> | undefined} */
let result
this.freeze()
assertParser('processSync', this.parser || this.Parser)
assertCompiler('processSync', this.compiler || this.Compiler)
this.process(file, realDone)
assertDone('processSync', 'process', complete)
assert(result, 'we either bailed on an error or have a tree')
return result
/**
* @type {ProcessCallback<VFileWithOutput<CompileResult>>}
*/
function realDone(error, file) {
complete = true
bail(error)
result = file
}
}
/**
* Run *transformers* on a syntax tree.
*
* > 👉 **Note**: `run` freezes the processor if not already *frozen*.
*
* > 👉 **Note**: `run` performs the run phase, not other phases.
*
* @overload
* @param {HeadTree extends undefined ? Node : HeadTree} tree
* @param {RunCallback<TailTree extends undefined ? Node : TailTree>} done
* @returns {undefined}
*
* @overload
* @param {HeadTree extends undefined ? Node : HeadTree} tree
* @param {Compatible | undefined} file
* @param {RunCallback<TailTree extends undefined ? Node : TailTree>} done
* @returns {undefined}
*
* @overload
* @param {HeadTree extends undefined ? Node : HeadTree} tree
* @param {Compatible | undefined} [file]
* @returns {Promise<TailTree extends undefined ? Node : TailTree>}
*
* @param {HeadTree extends undefined ? Node : HeadTree} tree
* Tree to transform and inspect.
* @param {(
* RunCallback<TailTree extends undefined ? Node : TailTree> |
* Compatible
* )} [file]
* File associated with `node` (optional); any value accepted as `x` in
* `new VFile(x)`.
* @param {RunCallback<TailTree extends undefined ? Node : TailTree>} [done]
* Callback (optional).
* @returns {Promise<TailTree extends undefined ? Node : TailTree> | undefined}
* Nothing if `done` is given.
* Otherwise, a promise rejected with a fatal error or resolved with the
* transformed tree.
*/
run(tree, file, done) {
assertNode(tree)
this.freeze()
const transformers = this.transformers
if (!done && typeof file === 'function') {
done = file
file = undefined
}
return done ? executor(undefined, done) : new Promise(executor)
// Note: `void`s needed for TS.
/**
* @param {(
* ((tree: TailTree extends undefined ? Node : TailTree) => undefined | void) |
* undefined
* )} resolve
* @param {(error: Error) => undefined | void} reject
* @returns {undefined}
*/
function executor(resolve, reject) {
assert(
typeof file !== 'function',
'`file` cant be a `done` anymore, we checked'
)
const realFile = vfile(file)
transformers.run(tree, realFile, realDone)
/**
* @param {Error | undefined} error
* @param {Node} outputTree
* @param {VFile} file
* @returns {undefined}
*/
function realDone(error, outputTree, file) {
const resultingTree =
/** @type {TailTree extends undefined ? Node : TailTree} */ (
outputTree || tree
)
if (error) {
reject(error)
} else if (resolve) {
resolve(resultingTree)
} else {
assert(done, '`done` is defined if `resolve` is not')
done(undefined, resultingTree, file)
}
}
}
}
/**
* Run *transformers* on a syntax tree.
*
* An error is thrown if asynchronous transforms are configured.
*
* > 👉 **Note**: `runSync` freezes the processor if not already *frozen*.
*
* > 👉 **Note**: `runSync` performs the run phase, not other phases.
*
* @param {HeadTree extends undefined ? Node : HeadTree} tree
* Tree to transform and inspect.
* @param {Compatible | undefined} [file]
* File associated with `node` (optional); any value accepted as `x` in
* `new VFile(x)`.
* @returns {TailTree extends undefined ? Node : TailTree}
* Transformed tree.
*/
runSync(tree, file) {
/** @type {boolean} */
let complete = false
/** @type {(TailTree extends undefined ? Node : TailTree) | undefined} */
let result
this.run(tree, file, realDone)
assertDone('runSync', 'run', complete)
assert(result, 'we either bailed on an error or have a tree')
return result
/**
* @type {RunCallback<TailTree extends undefined ? Node : TailTree>}
*/
function realDone(error, tree) {
bail(error)
result = tree
complete = true
}
}
/**
* Compile a syntax tree.
*
* > 👉 **Note**: `stringify` freezes the processor if not already *frozen*.
*
* > 👉 **Note**: `stringify` performs the stringify phase, not the run phase
* > or other phases.
*
* @param {CompileTree extends undefined ? Node : CompileTree} tree
* Tree to compile.
* @param {Compatible | undefined} [file]
* File associated with `node` (optional); any value accepted as `x` in
* `new VFile(x)`.
* @returns {CompileResult extends undefined ? Value : CompileResult}
* Textual representation of the tree (see note).
*
* > 👉 **Note**: unified typically compiles by serializing: most compilers
* > return `string` (or `Uint8Array`).
* > Some compilers, such as the one configured with
* > [`rehype-react`][rehype-react], return other values (in this case, a
* > React tree).
* > If youre using a compiler that doesnt serialize, expect different
* > result values.
* >
* > To register custom results in TypeScript, add them to
* > {@link CompileResultMap `CompileResultMap`}.
*
* [rehype-react]: https://github.com/rehypejs/rehype-react
*/
stringify(tree, file) {
this.freeze()
const realFile = vfile(file)
const compiler = this.compiler || this.Compiler
assertCompiler('stringify', compiler)
assertNode(tree)
return compiler(tree, realFile)
}
/**
* Configure the processor to use a plugin, a list of usable values, or a
* preset.
*
* If the processor is already using a plugin, the previous plugin
* configuration is changed based on the options that are passed in.
* In other words, the plugin is not added a second time.
*
* > 👉 **Note**: `use` cannot be called on *frozen* processors.
* > Call the processor first to create a new unfrozen processor.
*
* @example
* There are many ways to pass plugins to `.use()`.
* This example gives an overview:
*
* ```js
* import {unified} from 'unified'
*
* unified()
* // Plugin with options:
* .use(pluginA, {x: true, y: true})
* // Passing the same plugin again merges configuration (to `{x: true, y: false, z: true}`):
* .use(pluginA, {y: false, z: true})
* // Plugins:
* .use([pluginB, pluginC])
* // Two plugins, the second with options:
* .use([pluginD, [pluginE, {}]])
* // Preset with plugins and settings:
* .use({plugins: [pluginF, [pluginG, {}]], settings: {position: false}})
* // Settings only:
* .use({settings: {position: false}})
* ```
*
* @template {Array<unknown>} [Parameters=[]]
* @template {Node | string | undefined} [Input=undefined]
* @template [Output=Input]
*
* @overload
* @param {Preset | null | undefined} [preset]
* @returns {Processor<ParseTree, HeadTree, TailTree, CompileTree, CompileResult>}
*
* @overload
* @param {PluggableList} list
* @returns {Processor<ParseTree, HeadTree, TailTree, CompileTree, CompileResult>}
*
* @overload
* @param {Plugin<Parameters, Input, Output>} plugin
* @param {...(Parameters | [boolean])} parameters
* @returns {UsePlugin<ParseTree, HeadTree, TailTree, CompileTree, CompileResult, Input, Output>}
*
* @param {PluggableList | Plugin | Preset | null | undefined} value
* Usable value.
* @param {...unknown} parameters
* Parameters, when a plugin is given as a usable value.
* @returns {Processor<ParseTree, HeadTree, TailTree, CompileTree, CompileResult>}
* Current processor.
*/
use(value, ...parameters) {
const attachers = this.attachers
const namespace = this.namespace
assertUnfrozen('use', this.frozen)
if (value === null || value === undefined) {
// Empty.
} else if (typeof value === 'function') {
addPlugin(value, parameters)
} else if (typeof value === 'object') {
if (Array.isArray(value)) {
addList(value)
} else {
addPreset(value)
}
} else {
throw new TypeError('Expected usable value, not `' + value + '`')
}
return this
/**
* @param {Pluggable} value
* @returns {undefined}
*/
function add(value) {
if (typeof value === 'function') {
addPlugin(value, [])
} else if (typeof value === 'object') {
if (Array.isArray(value)) {
const [plugin, ...parameters] =
/** @type {PluginTuple<Array<unknown>>} */ (value)
addPlugin(plugin, parameters)
} else {
addPreset(value)
}
} else {
throw new TypeError('Expected usable value, not `' + value + '`')
}
}
/**
* @param {Preset} result
* @returns {undefined}
*/
function addPreset(result) {
if (!('plugins' in result) && !('settings' in result)) {
throw new Error(
'Expected usable value but received an empty preset, which is probably a mistake: presets typically come with `plugins` and sometimes with `settings`, but this has neither'
)
}
addList(result.plugins)
if (result.settings) {
namespace.settings = extend(true, namespace.settings, result.settings)
}
}
/**
* @param {PluggableList | null | undefined} plugins
* @returns {undefined}
*/
function addList(plugins) {
let index = -1
if (plugins === null || plugins === undefined) {
// Empty.
} else if (Array.isArray(plugins)) {
while (++index < plugins.length) {
const thing = plugins[index]
add(thing)
}
} else {
throw new TypeError('Expected a list of plugins, not `' + plugins + '`')
}
}
/**
* @param {Plugin} plugin
* @param {Array<unknown>} parameters
* @returns {undefined}
*/
function addPlugin(plugin, parameters) {
let index = -1
let entryIndex = -1
while (++index < attachers.length) {
if (attachers[index][0] === plugin) {
entryIndex = index
break
}
}
if (entryIndex === -1) {
attachers.push([plugin, ...parameters])
}
// Only set if there was at least a `primary` value, otherwise wed change
// `arguments.length`.
else if (parameters.length > 0) {
let [primary, ...rest] = parameters
const currentPrimary = attachers[entryIndex][1]
if (isPlainObj(currentPrimary) && isPlainObj(primary)) {
primary = extend(true, currentPrimary, primary)
}
attachers[entryIndex] = [plugin, primary, ...rest]
}
}
}
}
// Note: this returns a *callable* instance.
// Thats why its documented as a function.
/**
* Create a new processor.
*
* @example
* This example shows how a new processor can be created (from `remark`) and linked
* to **stdin**(4) and **stdout**(4).
*
* ```js
* import process from 'node:process'
* import concatStream from 'concat-stream'
* import {remark} from 'remark'
*
* process.stdin.pipe(
* concatStream(function (buf) {
* process.stdout.write(String(remark().processSync(buf)))
* })
* )
* ```
*
* @returns
* New *unfrozen* processor (`processor`).
*
* This processor is configured to work the same as its ancestor.
* When the descendant processor is configured in the future it does not
* affect the ancestral processor.
*/
export const unified = new Processor().freeze()
/**
* Assert a parser is available.
*
* @param {string} name
* @param {unknown} value
* @returns {asserts value is Parser}
*/
function assertParser(name, value) {
if (typeof value !== 'function') {
throw new TypeError('Cannot `' + name + '` without `parser`')
}
}
/**
* Assert a compiler is available.
*
* @param {string} name
* @param {unknown} value
* @returns {asserts value is Compiler}
*/
function assertCompiler(name, value) {
if (typeof value !== 'function') {
throw new TypeError('Cannot `' + name + '` without `compiler`')
}
}
/**
* Assert the processor is not frozen.
*
* @param {string} name
* @param {unknown} frozen
* @returns {asserts frozen is false}
*/
function assertUnfrozen(name, frozen) {
if (frozen) {
throw new Error(
'Cannot call `' +
name +
'` on a frozen processor.\nCreate a new processor first, by calling it: use `processor()` instead of `processor`.'
)
}
}
/**
* Assert `node` is a unist node.
*
* @param {unknown} node
* @returns {asserts node is Node}
*/
function assertNode(node) {
// `isPlainObj` unfortunately uses `any` instead of `unknown`.
// type-coverage:ignore-next-line
if (!isPlainObj(node) || typeof node.type !== 'string') {
throw new TypeError('Expected node, got `' + node + '`')
// Fine.
}
}
/**
* Assert that `complete` is `true`.
*
* @param {string} name
* @param {string} asyncName
* @param {unknown} complete
* @returns {asserts complete is true}
*/
function assertDone(name, asyncName, complete) {
if (!complete) {
throw new Error(
'`' + name + '` finished async. Use `' + asyncName + '` instead'
)
}
}
/**
* @param {Compatible | undefined} [value]
* @returns {VFile}
*/
function vfile(value) {
return looksLikeAVFile(value) ? value : new VFile(value)
}
/**
* @param {Compatible | undefined} [value]
* @returns {value is VFile}
*/
function looksLikeAVFile(value) {
return Boolean(
value &&
typeof value === 'object' &&
'message' in value &&
'messages' in value
)
}
/**
* @param {unknown} [value]
* @returns {value is Value}
*/
function looksLikeAValue(value) {
return typeof value === 'string' || isUint8Array(value)
}
/**
* Assert `value` is an `Uint8Array`.
*
* @param {unknown} value
* thing.
* @returns {value is Uint8Array}
* Whether `value` is an `Uint8Array`.
*/
function isUint8Array(value) {
return Boolean(
value &&
typeof value === 'object' &&
'byteLength' in value &&
'byteOffset' in value
)
}