1324 lines
40 KiB
JavaScript
1324 lines
40 KiB
JavaScript
/**
|
||
* @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 you’re using a compiler that doesn’t 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 can’t 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 it’s 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`: don’t 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 shouldn’t 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 we’re 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.
|
||
*
|
||
* It’s 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 you’re using a compiler that doesn’t 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 you’re using a compiler that doesn’t 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` can’t 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 you’re using a compiler that doesn’t 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 we’d 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.
|
||
// That’s why it’s 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
|
||
)
|
||
}
|