From cc53bb68ae6679b04e1ac63d67d0989acacbf6e4 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Sun, 13 Aug 2023 18:19:16 +0200 Subject: [PATCH] Refactor to use JSDoc, remove `FrozenProcessor` type --- .gitignore | 1 + index.d.ts | 1181 +------------------------------ index.js | 1 + index.test-d.ts | 37 +- lib/callable-instance.d.ts | 7 + lib/callable-instance.js | 31 + lib/index.js | 1368 ++++++++++++++++++++++++++++-------- package.json | 2 +- script/fix-types.js | 38 + test/core.js | 1 - test/parse.js | 15 +- test/process-compilers.js | 2 + test/stringify.js | 13 +- tsconfig.json | 2 +- 14 files changed, 1236 insertions(+), 1463 deletions(-) create mode 100644 lib/callable-instance.d.ts create mode 100644 lib/callable-instance.js create mode 100644 script/fix-types.js diff --git a/.gitignore b/.gitignore index fcb26070..66103597 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules/ *.log yarn.lock !/index.d.ts +!/lib/callable-instance.d.ts diff --git a/index.d.ts b/index.d.ts index cf53d2af..6a4f5855 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,20 +1,30 @@ -// TypeScript Version: 4.0 - -// Note: this is a `.d.ts` file because it is not possible to have default type -// parameters in JSDoc-based TypeScript, which is a feature we use to type that: -// -// ```js -// .use(somePlugin, theOptions) -// ``` -// -// `theOptions` matches the options that `somePlugin` expects and thus is very -// important for making unified usable in TypeScript. -// -// Furthermore, this is places in the root of the project because types that -// accept type parameters cannot be re-exported as such easily. - -import type {Node} from 'unist' -import type {VFile, VFileCompatible, VFileValue} from 'vfile' +import type {VFileValue} from 'vfile' +import type {CompileResults} from './lib/index.js' + +export type { + // `CompileResultMap` is typed and exposed below. + CompileResults, + Compiler, + CompilerClass, + CompilerFunction, + Pluggable, + PluggableList, + Plugin, + // To do: remove next major. + Plugin as Attacher, + PluginTuple, + Parser, + ParserClass, + ParserFunction, + Preset, + ProcessCallback, + Processor, + RunCallback, + TransformCallback, + Transformer +} from './lib/index.js' + +export {unified} from './lib/index.js' /** * Interface of known results from compilers. @@ -36,1141 +46,10 @@ import type {VFile, VFileCompatible, VFileValue} from 'vfile' * * Use {@link CompileResults `CompileResults`} to access the values. */ -// Note: if `Value` from `VFile` is changed, this should too. export interface CompileResultMap { + // Note: if `Value` from `VFile` is changed, this should too. Uint8Array: Uint8Array string: string + // Empties. + null: null } - -/** - * Acceptable results from compilers. - * - * To register custom results, add them to - * {@link CompileResultMap `CompileResultMap`}. - */ -type CompileResults = CompileResultMap[keyof CompileResultMap] - -/** - * 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`}. - * - * @typeParam Result - * Compile result. - */ -type VFileWithOutput = - Result extends VFileValue | undefined ? VFile : VFile & {result: Result} - -/** - * Create a processor based on the input/output of a {@link Plugin plugin}. - * - * @typeParam ParseTree - * Output of `parse`. - * @typeParam HeadTree - * Input for `run`. - * @typeParam TailTree - * Output for `run`. - * @typeParam CompileTree - * Input of `stringify`. - * @typeParam CompileResult - * Output of `stringify`. - * @typeParam Input - * Input of plugin. - * @typeParam Output - * Output of plugin. - */ -type UsePlugin< - ParseTree extends Node | undefined, - HeadTree extends Node | undefined, - TailTree extends Node | undefined, - CompileTree extends Node | undefined, - CompileResult extends CompileResults | undefined, - Input extends Node | string | undefined, - Output -> = Input extends string - ? Output extends Node | undefined - ? // Parser. - Processor< - Output extends undefined ? ParseTree : Output, - HeadTree, - TailTree, - CompileTree, - CompileResult - > - : // Unknown. - Processor - : Output extends CompileResults - ? Input extends Node | undefined - ? // Compiler. - Processor< - ParseTree, - HeadTree, - TailTree, - Input extends undefined ? CompileTree : Input, - Output extends undefined ? CompileResult : Output - > - : // Unknown. - Processor - : Input extends Node | undefined - ? Output extends Node | undefined - ? // Transform. - Processor< - ParseTree, - // No `HeadTree` yet? Set `Input`. - HeadTree extends undefined ? Input : HeadTree, - Output extends undefined ? TailTree : Output, - CompileTree, - CompileResult - > - : // Unknown. - Processor - : // Unknown. - Processor - -/** - * Processor. - * - * @typeParam ParseTree - * Output of `parse`. - * @typeParam HeadTree - * Input for `run`. - * @typeParam TailTree - * Output for `run`. - * @typeParam CompileTree - * Input of `stringify`. - * @typeParam CompileResult - * Output of `stringify`. - */ -export type Processor< - ParseTree extends Node | undefined = undefined, - HeadTree extends Node | undefined = undefined, - TailTree extends Node | undefined = undefined, - CompileTree extends Node | undefined = undefined, - CompileResult extends CompileResults | undefined = undefined -> = { - /** - * Configure the processor with 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. - * - * @example - * ```js - * import {unified} from 'unified' - * - * unified() - * // Preset with plugins and settings: - * .use({plugins: [pluginA, [pluginB, {}]], settings: {position: false}}) - * // Settings only: - * .use({settings: {position: false}}) - * ``` - * - * @param preset - * Single preset ({@link Preset `Preset`}): an object with a `plugins` - * and/or `settings`. - * @returns - * Current processor. - */ - use( - preset?: Preset | null | undefined - ): Processor - - /** - * Configure the processor with a list of usable values. - * - * 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. - * - * @example - * ```js - * import {unified} from 'unified' - * - * unified() - * // Plugins: - * .use([pluginA, pluginB]) - * // Two plugins, the second with options: - * .use([pluginC, [pluginD, {}]]) - * ``` - * - * @param list - * List of plugins plugins, presets, and tuples - * ({@link PluggableList `PluggableList`}). - * @returns - * Current processor. - */ - use( - list: PluggableList - ): Processor - - /** - * Configure the processor to use a {@link Plugin `Plugin`}. - * - * 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. - * - * @example - * ```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}) - * ``` - * - * @typeParam Parameters - * Arguments passed to the plugin. - * @typeParam Input - * Value that is expected as input. - * - * * 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. - * @typeParam Output - * 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. - * @param plugin - * {@link Plugin `Plugin`} to use. - * @param parameters - * Arguments passed to the {@link Plugin plugin}. - * - * Plugins typically receive one options object, but could receive other and - * more values. - * Itโ€™s also possible to pass a boolean: `true` (to turn a plugin on), - * `false` (to turn a plugin off). - * @returns - * Current processor. - */ - use< - Parameters extends unknown[] = [], - Input extends Node | string | undefined = undefined, - Output = Input - >( - plugin: Plugin, - ...parameters: Parameters | [boolean] - ): UsePlugin< - ParseTree, - HeadTree, - TailTree, - CompileTree, - CompileResult, - Input, - Output - > - - /** - * Configure the processor to use a tuple of a {@link Plugin `Plugin`} with - * its parameters. - * - * 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. - * - * @example - * ```js - * import {unified} from 'unified' - * - * unified() - * // Plugin with options: - * .use([pluginA, {x: true, y: true}]) - * ``` - * - * @typeParam Parameters - * Arguments passed to the plugin. - * @typeParam Input - * Value that is expected as input. - * - * * 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. - * @typeParam Output - * 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. - * @param tuple - * {@link Plugin `Plugin`} with arguments to use. - * - * Plugins typically receive one options object, but could receive other and - * more values. - * Itโ€™s also possible to pass a boolean: `true` (to turn a plugin on), - * `false` (to turn a plugin off). - * @returns - * Current processor. - */ - use< - Parameters extends unknown[] = [], - Input extends Node | string | undefined = undefined, - Output = Input - >( - tuple: - | [plugin: Plugin, enable: boolean] // Enable or disable the plugin. - | [plugin: Plugin, ...parameters: Parameters] // Configure the plugin. - ): UsePlugin< - ParseTree, - HeadTree, - TailTree, - CompileTree, - CompileResult, - Input, - Output - > -} & FrozenProcessor - -/** - * Frozen processor. - * - * @typeParam ParseTree - * Output of `parse`. - * @typeParam HeadTree - * Input for `run`. - * @typeParam TailTree - * Output for `run`. - * @typeParam CompileTree - * Input of `stringify`. - * @typeParam CompileResult - * Output of `stringify`. - */ -export type FrozenProcessor< - ParseTree extends Node | undefined = undefined, - HeadTree extends Node | undefined = undefined, - TailTree extends Node | undefined = undefined, - CompileTree extends Node | undefined = undefined, - CompileResult extends CompileResults | undefined = undefined -> = { - /** - * Create a processor. - * - * @returns - * 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. - */ - (): Processor - - /** - * Internal list of configured plugins. - * - * @private - */ - attachers: Array> - - /** - * 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. - * - * `Parser` can be a normal function, in which case it must return the syntax - * tree representation of the given file ({@link Node `Node`}). - * - * `Parser` can also be a constructor function (a function with a `parse` - * field in its `prototype`), in which case it is constructed with `new`. - * Instances must have a `parse` method that is called without arguments and must - * return a {@link Node `Node`}. - */ - Parser?: Parser | undefined - - /** - * A **compiler** handles the compiling of a syntax tree to something else (in - * most cases, text). - * - * It is used in the stringify phase and called with a {@link Node `Node`} - * and {@link VFile `VFile`} representation of the document to compile. - * - * `Compiler` can be a normal function, in which case it should return the - * textual representation of the given tree (`string`). - * - * `Compiler` can also be a constructor function (a function with a `compile` - * field in its `prototype`), in which case it is constructed with `new`. - * Instances must have a `compile` method that is called without arguments and - * should return a `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 - */ - Compiler?: - | Compiler< - CompileTree extends undefined ? Node : CompileTree, - CompileResult extends undefined ? unknown : CompileResult - > - | undefined - - /** - * 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 file - * file to parse; typically `string`; any value accepted as `x` in - * `new VFile(x)`. - * @returns - * Syntax tree representing `file`. - */ - parse( - file?: VFileCompatible | undefined - ): ParseTree extends undefined ? Node : ParseTree - - /** - * 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 tree - * Tree to compile - * @param file - * File associated with `node` (optional); any value accepted as `x` in - * `new VFile(x)`. - * @returns - * 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: CompileTree extends undefined ? Node : CompileTree, - file?: VFileCompatible | undefined - ): CompileResult extends undefined ? VFileValue : CompileResult - - /** - * Run *transformers* on a syntax tree. - * - * > ๐Ÿ‘‰ **Note**: `run` freezes the processor if not already *frozen*. - * - * > ๐Ÿ‘‰ **Note**: `run` performs the run phase, not other phases. - * - * @param tree - * Tree to transform and inspect. - * @param done - * Callback. - * @returns - * Nothing. - */ - run( - tree: HeadTree extends undefined ? Node : HeadTree, - done: RunCallback - ): undefined - - /** - * Run *transformers* on a syntax tree. - * - * > ๐Ÿ‘‰ **Note**: `run` freezes the processor if not already *frozen*. - * - * > ๐Ÿ‘‰ **Note**: `run` performs the run phase, not other phases. - * - * @param tree - * Tree to transform and inspect. - * @param file - * File associated with `node` (optional); any value accepted as `x` in - * `new VFile(x)`. - * @param done - * Callback. - * @returns - * Nothing. - */ - run( - tree: HeadTree extends undefined ? Node : HeadTree, - file: VFileCompatible | undefined, - done: RunCallback - ): undefined - - /** - * Run *transformers* on a syntax tree. - * - * > ๐Ÿ‘‰ **Note**: `run` freezes the processor if not already *frozen*. - * - * > ๐Ÿ‘‰ **Note**: `run` performs the run phase, not other phases. - * - * @param tree - * Tree to transform and inspect. - * @param file - * File associated with `node` (optional); any value accepted as `x` in - * `new VFile(x)`. - * @returns - * A `Promise` rejected with a fatal error or resolved with the transformed - * tree. - */ - run( - tree: HeadTree extends undefined ? Node : HeadTree, - file?: VFileCompatible | undefined - ): Promise - - /** - * 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 tree - * Tree to transform and inspect. - * @param file - * File associated with `node` (optional); any value accepted as `x` in - * `new VFile(x)`. - * @returns - * Transformed tree. - */ - runSync( - tree: HeadTree extends undefined ? Node : HeadTree, - file?: VFileCompatible | undefined - ): TailTree extends undefined ? Node : TailTree - - /** - * 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. - * - * @param file - * File; any value accepted as `x` in `new VFile(x)`. - * @param done - * Callback. - * @returns - * Nothing. - */ - process( - file: VFileCompatible | undefined, - done: ProcessCallback> - ): undefined - - /** - * 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. - * - * @param file - * File; any value accepted as `x` in `new VFile(x)`. - * @returns - * `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?: VFileCompatible | undefined - ): Promise> - - /** - * 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 file - * File; any value accepted as `x` in `new VFile(x)`. - * @returns - * 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?: VFileCompatible | undefined - ): VFileWithOutput - - /** - * 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. - * - * @returns - * The key-value store. - */ - data(): Record - - /** - * 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. - * - * @param data - * Values to set. - * @returns - * The processor that `data` is called on. - */ - data( - data: Record - ): Processor - - /** - * 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. - * - * @param key - * Key to get. - * @returns - * The value at `key`. - */ - data(key: string): unknown - - /** - * 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. - * - * @param key - * Key to set. - * @param value - * Value to set. - * @returns - * The processor that `data` is called on. - */ - data( - key: string, - value: unknown - ): Processor - - /** - * 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 - * The processor that `freeze` was called on. - */ - freeze(): FrozenProcessor< - ParseTree, - HeadTree, - TailTree, - CompileTree, - CompileResult - > -} - -/** - * **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 - * - * Plugins are a concept. - * They materialize as `Attacher`s. - * - * Attachers are materialized plugins. - * They are functions that can receive options and configure the processor. - * - * Attachers change the processor, such as the parser, the compiler, by - * configuring data, or by specifying how the tree and file are handled. - * - * > ๐Ÿ‘‰ **Note**: attachers are called when the processor is *frozen*, - * > not when they are applied. - * - * @typeParam Parameters - * Arguments passed to the plugin. - * @typeParam Input - * Value that is expected as input. - * - * * 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. - * @typeParam Output - * 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. - * @this - * Processor the attacher is applied to. - * @param parameters - * Arguments passed to the plugin. - * - * Plugins typically receive one options object, but could receive other and - * more values. - * @returns - * Optional transform. - */ -export type Plugin< - Parameters extends unknown[] = [], - Input extends Node | string | undefined = undefined, - Output = Input -> = ( - this: Processor, - ...parameters: Parameters -) => 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 - -/** - * Presets are sharable configuration. - * - * They can contain plugins and settings. - */ -export type Preset = { - /** - * List of plugins and presets. - */ - plugins?: PluggableList - - /** - * Shared settings for parsers and compilers. - */ - settings?: Record -} - -/** - * Tuple of a plugin and its setting(s). - * The first item is a plugin, the rest are its parameters. - * - * @typeParam Parameters - * Arguments passed to the plugin. - * @typeParam Input - * Value that is expected as input. - * - * * 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. - * @typeParam Output - * 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. - */ -export type PluginTuple< - Parameters extends unknown[] = [], - Input extends Node | string | undefined = undefined, - Output = undefined -> = [Plugin, ...Parameters] - -/** - * A union of the different ways to add plugins and settings. - */ -export type Pluggable = - | Plugin - | PluginTuple - | Preset - -/** - * A list of plugins and presets. - */ -export type PluggableList = Pluggable[] - -// To do: remove? -/** - * Attacher. - * - * @deprecated - * Please use `Plugin`. - */ -export type Attacher< - Parameters extends unknown[] = unknown[], - Input extends Node | string = Node, - Output extends CompileResults | Node = Input -> = Plugin - -/** - * 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. - * - * [trough]: https://github.com/wooorm/trough#function-fninput-next - * - * @typeParam Input - * Node type that the transformer expects. - * @typeParam Output - * Node type that the transformer yields. - * @param tree - * Tree to handle. - * @param file - * File to handle. - * @param next - * Callback. - * @returns - * If you accept `next`, nothing. - * Otherwise: - * - * * `Error` โ€” fatal error to stop the process - * * `Promise` or `undefined` โ€” the next transformer keeps using - * same tree - * * `Promise` or `Node` โ€” new, changed, tree - */ -export type Transformer< - Input extends Node = Node, - Output extends Node = Input -> = ( - tree: Input, - file: VFile, - next: TransformCallback -) => - | Promise - | Promise // For some reason this is needed separately. - | Output - | Error - | undefined - | void - -/** - * If the signature of a `transformer` accepts a third argument, the - * transformer may perform asynchronous operations, and must call `next()`. - * - * @typeParam Tree - * Node type that the transformer yields. - * @param error - * Fatal error to stop the process (optional). - * @param tree - * New, changed, tree (optional). - * @param file - * New, changed, file (optional). - * @returns - * Nothing. - */ -export type TransformCallback = ( - error?: Error | undefined, - tree?: Output, - file?: VFile | undefined -) => undefined - -/** - * 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. - * - * `Parser` can be a normal function, in which case it must return the syntax - * tree representation of the given file ({@link Node `Node`}). - * - * `Parser` can also be a constructor function (a function with a `parse` - * field in its `prototype`), in which case it is constructed with `new`. - * Instances must have a `parse` method that is called without arguments and must - * return a {@link Node `Node`}. - * - * @typeParam Tree - * The node that the parser yields. - */ -export type Parser = - | ParserClass - | ParserFunction - -/** - * A class to parse files. - * - * @typeParam Tree - * The node that the parser yields. - */ -export class ParserClass { - prototype: { - /** - * Parse a file. - * - * @returns - * Parsed tree. - */ - parse(): Tree - } - - /** - * Constructor. - * - * @param document - * Document to parse. - * @param file - * File associated with `document`. - * @returns - * Instance. - */ - constructor(document: string, file: VFile) -} - -/** - * Regular function to parse a file. - * - * @typeParam Tree - * The node that the parser yields. - * @param document - * Document to parse. - * @param file - * File associated with `document`. - * @returns - * Node representing the given file. - */ -export type ParserFunction = ( - document: string, - file: VFile -) => Tree - -/** - * A **compiler** handles the compiling of a syntax tree to something else (in - * most cases, text). - * - * It is used in the stringify phase and called with a {@link Node `Node`} - * and {@link VFile `VFile`} representation of the document to compile. - * - * `Compiler` can be a normal function, in which case it should return the - * textual representation of the given tree (`string`). - * - * `Compiler` can also be a constructor function (a function with a `compile` - * field in its `prototype`), in which case it is constructed with `new`. - * Instances must have a `compile` method that is called without arguments and - * should return a `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 - * - * @typeParam Tree - * The node that the compiler receives. - * @typeParam Result - * The thing that the compiler yields. - */ -export type Compiler = - | CompilerClass - | CompilerFunction - -/** - * A class to compile trees. - * - * @typeParam Tree - * The node that the compiler receives. - * @typeParam Result - * The thing that the compiler yields. - */ -export class CompilerClass { - prototype: { - /** - * Compile a tree. - * - * @returns - * New content: compiled text (`string` or `Uint8Array`, for - * `file.value`) or something else (for `file.result`). - */ - compile(): Result - } - - /** - * Constructor. - * - * @param tree - * Tree to compile. - * @param file - * File associated with `tree`. - * @returns - * Instance. - */ - constructor(tree: Tree, file: VFile) -} - -/** - * Regular function to compile a tree. - * - * @typeParam Tree - * The node that the compiler receives. - * @typeParam Result - * The thing that the compiler yields. - * @param tree - * Tree to compile. - * @param file - * File associated with `tree`. - * @returns - * New content: compiled text (`string` or `Uint8Array`, for `file.value`) or - * something else (for `file.result`). - */ -export type CompilerFunction< - Tree extends Node = Node, - Result = CompileResults -> = (tree: Tree, file: VFile) => Result - -/** - * Callback called when transformers are done. - * - * Called with either an error or results. - - * - * @typeParam Tree - * The tree that the callback receives. - * @param error - * Fatal error. - * @param tree - * Transformed tree. - * @param file - * File. - * @returns - * Nothing. - */ -export type RunCallback = ( - error?: Error | undefined, - tree?: Tree | undefined, - file?: VFile | undefined -) => undefined - -/** - * Callback called when the process is done. - * - * Called with either an error or a result. - * - * @typeParam File - * The file that the callback receives. - * @param error - * Fatal error. - * @param file - * Processed file. - * @returns - * Nothing. - */ -export type ProcessCallback = ( - error?: Error | undefined, - file?: File | undefined -) => undefined - -/** - * A frozen processor. - */ -export function unified(): Processor diff --git a/index.js b/index.js index 024230ea..43197543 100644 --- a/index.js +++ b/index.js @@ -1 +1,2 @@ +// Note: types exposed from `index.d.ts`. export {unified} from './lib/index.js' diff --git a/index.test-d.ts b/index.test-d.ts index bae224c9..2523938e 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -3,16 +3,11 @@ import type {Root as MdastRoot} from 'mdast' import {expectType} from 'tsd' import type {Node as UnistNode} from 'unist' import type {VFile} from 'vfile' -import type { - FrozenProcessor, - Plugin, - Processor, - TransformCallback -} from './index.js' +import type {Plugin, Processor, TransformCallback} from './index.js' import {unified} from './index.js' expectType(unified()) -expectType(unified().freeze()) +expectType(unified().freeze()) type ReactNode = { kind: string @@ -351,7 +346,9 @@ expectType>(processorWithRemarkParse) expectType(processorWithRemarkParse.parse('')) expectType(processorWithRemarkParse.runSync(mdastRoot)) expectType(processorWithRemarkParse.runSync(hastRoot)) -expectType(processorWithRemarkParse.stringify(mdastRoot)) +expectType( + processorWithRemarkParse.stringify(mdastRoot) +) processorWithRemarkParse.stringify(hastRoot) expectType(processorWithRemarkParse.processSync('')) @@ -367,8 +364,12 @@ expectType(processorWithRemarkLint.parse('')) expectType(processorWithRemarkLint.runSync(mdastRoot)) // @ts-expect-error: not the correct node type. processorWithRemarkLint.runSync(hastRoot) -expectType(processorWithRemarkLint.stringify(mdastRoot)) -expectType(processorWithRemarkLint.stringify(hastRoot)) +expectType( + processorWithRemarkLint.stringify(mdastRoot) +) +expectType( + processorWithRemarkLint.stringify(hastRoot) +) expectType(processorWithRemarkLint.processSync('')) // Inspect/transform plugin (implicit). @@ -388,10 +389,10 @@ expectType(processorWithRemarkLintImplicit.parse('')) expectType(processorWithRemarkLintImplicit.runSync(mdastRoot)) // @ts-expect-error: not the correct node type. processorWithRemarkLintImplicit.runSync(hastRoot) -expectType( +expectType( processorWithRemarkLintImplicit.stringify(mdastRoot) ) -expectType( +expectType( processorWithRemarkLintImplicit.stringify(hastRoot) ) expectType(processorWithRemarkLintImplicit.processSync('')) @@ -408,8 +409,12 @@ expectType(processorWithRemarkRehype.parse('')) expectType(processorWithRemarkRehype.runSync(mdastRoot)) // @ts-expect-error: not the correct node type. processorWithRemarkRehype.runSync(hastRoot) -expectType(processorWithRemarkRehype.stringify(hastRoot)) -expectType(processorWithRemarkRehype.stringify(mdastRoot)) +expectType( + processorWithRemarkRehype.stringify(hastRoot) +) +expectType( + processorWithRemarkRehype.stringify(mdastRoot) +) expectType(processorWithRemarkRehype.processSync('')) // Mutate plugin (implicit). @@ -429,10 +434,10 @@ expectType(processorWithRemarkRehypeImplicit.parse('')) expectType(processorWithRemarkRehypeImplicit.runSync(mdastRoot)) // @ts-expect-error: not the correct node type. processorWithRemarkRehypeImplicit.runSync(hastRoot) -expectType( +expectType( processorWithRemarkRehypeImplicit.stringify(hastRoot) ) -expectType( +expectType( processorWithRemarkRehypeImplicit.stringify(mdastRoot) ) expectType(processorWithRemarkRehypeImplicit.processSync('')) diff --git a/lib/callable-instance.d.ts b/lib/callable-instance.d.ts new file mode 100644 index 00000000..0fbc88f7 --- /dev/null +++ b/lib/callable-instance.d.ts @@ -0,0 +1,7 @@ +type Func = (...argv: Args) => Return + +export type ICallableInstance = new ( + property: string | symbol +) => Func + +export const CallableInstance: ICallableInstance diff --git a/lib/callable-instance.js b/lib/callable-instance.js new file mode 100644 index 00000000..6c092d88 --- /dev/null +++ b/lib/callable-instance.js @@ -0,0 +1,31 @@ +/* eslint-disable unicorn/no-this-assignment */ +/** + * @param {string} property + */ +export function CallableInstance(property) { + /** @type {Function} */ + const self = this + const constr = self.constructor + // Prototypes do exist. + // type-coverage:ignore-next-line + const proto = /** @type {Record} */ (constr.prototype) + const func = proto[property] + const apply = function () { + return func.apply(apply, arguments) + } + + Object.setPrototypeOf(apply, proto) + + const names = Object.getOwnPropertyNames(func) + + for (const p of names) { + const descriptor = Object.getOwnPropertyDescriptor(func, p) + if (descriptor) Object.defineProperty(apply, p, descriptor) + } + + return apply +} + +// Prototypes do exist. +// type-coverage:ignore-next-line +CallableInstance.prototype = Object.create(Function.prototype) diff --git a/lib/index.js b/lib/index.js index daa54c6c..f81bc3bd 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,19 +1,392 @@ +/* eslint-disable unicorn/no-this-assignment */ /** + * @typedef {import('trough').Pipeline} Pipeline + * * @typedef {import('unist').Node} Node * * @typedef {import('vfile').VFileCompatible} VFileCompatible * @typedef {import('vfile').VFileValue} VFileValue * - * @typedef {import('../index.js').Compiler} Compiler - * @typedef {import('../index.js').Parser} Parser - * @typedef {import('../index.js').Pluggable} Pluggable - * @typedef {import('../index.js').PluggableList} PluggableList - * @typedef {import('../index.js').PluginTuple} PluginTuple - * @typedef {import('../index.js').Plugin} Plugin - * @typedef {import('../index.js').Preset} Preset - * @typedef {import('../index.js').ProcessCallback} ProcessCallback - * @typedef {import('../index.js').Processor} Processor - * @typedef {import('../index.js').RunCallback} RunCallback + * @typedef {import('../index.js').CompileResultMap} CompileResultMap + */ + +/** + * @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. + * @template {CompileResults} [Result=CompileResults] + * The thing that the compiler yields. + * @typedef {CompilerClass | CompilerFunction} Compiler + * A **compiler** handles the compiling of a syntax tree to something else + * (in most cases, text). + * + * It is used in the stringify phase and called with a {@link Node `Node`} + * and {@link VFile `VFile`} representation of the document to compile. + * + * `Compiler` can be a normal function, in which case it should return the + * textual representation of the given tree (`string`). + * + * `Compiler` can also be a constructor function (a function with a `compile` + * field in its `prototype`), in which case it is constructed with `new`. + * Instances must have a `compile` method that is called without arguments + * and should return a `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 + */ + +/** + * @template {Node} [Tree=Node] + * @template {CompileResults} [Result=CompileResults] + * @typedef {({ + * prototype: {compile(): Result} + * new (tree: Tree, file: VFile): CompilerClass['prototype'] + * })} CompilerClass + * Class to compile trees. + */ + +/** + * @template {Node} [Tree=Node] + * The node that the compiler receives. + * @template {CompileResults} [Result=CompileResults] + * The thing that the compiler yields. + * @callback CompilerFunction + * Regular function to compile a tree. + * @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. + * @typedef {ParserClass | ParserFunction} 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. + * + * `Parser` can be a normal function, in which case it must return the syntax + * tree representation of the given file ({@link Node `Node`}). + * + * `Parser` can also be a constructor function (a function with a `parse` + * field in its `prototype`), in which case it is constructed with `new`. + * Instances must have a `parse` method that is called without arguments and + * must return a {@link Node `Node`}. + */ + +/** + * @template {Node} [Tree=Node] + * @typedef {({ + * prototype: {parse(): Tree} + * new (document: string, file: VFile): ParserClass['prototype'] + * })} ParserClass + * Class to parse files. + */ + +/** + * @template {Node} [Tree=Node] + * The node that the parser yields. + * @callback ParserFunction + * Regular function to parse a file. + * @param {string} document + * Document to parse. + * @param {VFile} file + * File associated with `document`. + * @returns {Tree} + * Node representing the given file. + */ + +/** + * @typedef {( + * Plugin, any, any> | + * PluginTuple, any, any> | + * Preset + * )} Pluggable + * Union of the different ways to add plugins and settings. + */ + +/** + * @typedef {Array} PluggableList + * List of plugins and presets. + */ + +// Note: we canโ€™t use `callback` yet as it messes up `this`: +// . +/** + * @template {Array} [PluginParameters=[]] + * Arguments passed to the plugin. + * @template {Node | string | undefined} [Input=Node] + * Value that is expected as input. + * + * * 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. + * + * * 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 + * **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 + * + * Plugins are a concept. + * They materialize as `Attacher`s. + * + * Attachers are materialized plugins. + * They are functions that can receive options and configure the processor. + * + * Attachers change the processor, such as the parser, the compiler, by + * configuring data, or by specifying how the tree and file are handled. + * + * > ๐Ÿ‘‰ **Note**: attachers are called when the processor is *frozen*, + * > not when they are applied. + */ + +/** + * Tuple of a plugin and its setting(s). + * The first item is a plugin, the rest are its parameters. + * + * @template {Array} [TupleParameters=[]] + * Arguments passed to the plugin. + * @template {Node | string | undefined} [Input=undefined] + * Value that is expected as input. + * + * * 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] + * @typedef {( + * [ + * plugin: Plugin, + * ...parameters: TupleParameters + * ] + * )} PluginTuple + * 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 Preset + * Presets are sharable configuration. + * + * They can contain plugins and settings. + * @property {PluggableList | undefined} [plugins] + * List of plugins and presets. + * @property {Record | undefined} [settings] + * Shared settings for parsers and compilers. + */ + +/** + * @template {VFile} [File=VFile] + * The file that the callback receives. + * @callback ProcessCallback + * Callback called when the process is done. + * + * Called with either an error or a result. + * @param {Error | undefined} [error] + * Fatal error. + * @param {File | undefined} [file] + * Processed file. + * @returns {undefined} + * Nothing. + */ + +/** + * @template {Node} [Tree=Node] + * The tree that the callback receives. + * @callback RunCallback + * Callback called when transformers are done. + * + * Called with either an error or results. + * @param {Error | undefined} [error] + * Fatal error. + * @param {Tree | undefined} [tree] + * Transformed tree. + * @param {VFile | undefined} [file] + * File. + * @returns {undefined} + * Nothing. + */ + +/** + * @template {Node} [Input=Node] + * Node type that the transformer expects. + * @template {Node} [Output=Input] + * Node type that the transformer yields. + * @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. + * + * [trough]: https://github.com/wooorm/trough#function-fninput-next + * @param {Input} tree + * Tree to handle. + * @param {VFile} file + * File to handle. + * @param {TransformCallback} next + * Callback. + * @returns {( + * Promise | + * Promise | // 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` or `undefined` โ€” the next transformer keeps using + * same tree + * * `Promise` or `Node` โ€” new, changed, tree + */ + +/** + * @template {Node} [Output=Node] + * Node type that the transformer yields. + * @callback TransformCallback + * If the signature of a `transformer` accepts a third argument, the + * transformer may perform asynchronous operations, and must call `next()`. + * @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 | 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. + * @typedef {( + * Input extends string + * ? Output extends Node | undefined + * ? // Parser. + * Processor< + * Output extends undefined ? ParseTree : Output, + * HeadTree, + * TailTree, + * CompileTree, + * CompileResult + * > + * : // Unknown. + * Processor + * : Output extends CompileResults + * ? Input extends Node | undefined + * ? // Compiler. + * Processor< + * ParseTree, + * HeadTree, + * TailTree, + * Input extends undefined ? CompileTree : Input, + * Output extends undefined ? CompileResult : Output + * > + * : // Unknown. + * Processor + * : 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 + * : // Unknown. + * Processor + * )} 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 VFileValue | 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 structuredClone from '@ungap/structured-clone' @@ -21,105 +394,260 @@ import {bail} from 'bail' import isPlainObj from 'is-plain-obj' import {trough} from 'trough' import {VFile} from 'vfile' - -// Expose a frozen processor. -export const unified = base().freeze() +import {CallableInstance} from './callable-instance.js' const own = {}.hasOwnProperty -// Function to create the first processor. -/** - * @returns {Processor} - */ -function base() { - const transformers = trough() - /** @type {Processor['attachers']} */ - const attachers = [] - /** @type {{settings?: Record} & Record} */ - let namespace = {} - /** @type {boolean | undefined} */ - let frozen - let freezeIndex = -1 - - // Data management. - // @ts-expect-error: overloads are handled. - processor.data = data - processor.Parser = undefined - processor.Compiler = undefined - - // Lock. - processor.freeze = freeze - - // Plugins. - processor.attachers = attachers - processor.use = use - - // API. - processor.parse = parse - processor.stringify = stringify - // @ts-expect-error: overloads are handled. - processor.run = run - processor.runSync = runSync - // @ts-expect-error: overloads are handled. - processor.process = process - processor.processSync = processSync - - // Expose. - return processor - - // Create a new processor based on the processor in the current scope. - /** @type {Processor} */ - function processor() { - const destination = base() +/** + * @template {Node | undefined} [ParseTree=undefined] + * Output of `parse`. + * @template {Node | undefined} [HeadTree=undefined] + * Input for `run`. + * @template {Node | undefined} [TailTree=undefined] + * Output for `run`. + * @template {Node | undefined} [CompileTree=undefined] + * Input of `stringify`. + * @template {CompileResults | undefined} [CompileResult=undefined] + * Output of `stringify`. + * @extends {CallableInstance<[], Processor>} + */ +export class Processor extends CallableInstance { + /** + * Create a processor. + */ + constructor() { + // If `Processor()` is called (w/o new), `copy` is called instead. + super('copy') + + /** + * A **compiler** handles the compiling of a syntax tree to something else + * (in most cases, text). + * + * It is used in the stringify phase and called with a {@link Node `Node`} + * and {@link VFile `VFile`} representation of the document to compile. + * + * `Compiler` can be a normal function, in which case it should return the + * textual representation of the given tree (`string`). + * + * `Compiler` can also be a constructor function (a function with a + * `compile` field in its `prototype`), in which case it is constructed with + * `new`. + * Instances must have a `compile` method that is called without arguments + * and should return a `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 + * + * @type {( + * Compiler< + * CompileTree extends undefined ? Node : CompileTree, + * CompileResult extends undefined ? CompileResults : CompileResult + * > | + * undefined + * )} + */ + this.Compiler = undefined + + /** + * 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. + * + * `Parser` can be a normal function, in which case it must return the + * syntax tree representation of the given file ({@link Node `Node`}). + * + * `Parser` can also be a constructor function (a function with a `parse` + * field in its `prototype`), in which case it is constructed with `new`. + * Instances must have a `parse` method that is called without arguments + * and must return a {@link Node `Node`}. + * + * @type {( + * Parser | + * 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>>} + */ + this.attachers = [] + + /** + * 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 {{settings?: Record} & Record} + */ + this.namespace = {} + + /** + * 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} + * 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} */ ( + new Processor() + ) let index = -1 - while (++index < attachers.length) { - const attacher = attachers[index] + while (++index < this.attachers.length) { + const attacher = this.attachers[index] destination.use(...attacher) } - destination.data(structuredClone(namespace)) + destination.data(structuredClone(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. + * + * @overload + * @returns {Record} + * + * @overload + * @param {Record} dataset + * @returns {Processor} + * + * @overload + * @param {string} key + * @returns {unknown} + * + * @overload + * @param {string} key + * @param {unknown} value + * @returns {Processor} + * * @param {Record | string} [key] + * Key to get or set, or entire dataset to set, or nothing to get the + * entire dataset. * @param {unknown} [value] + * Value to set. * @returns {unknown} + * The processor that `data` is called on when settings, the value at `key` + * when getting, or the entire dataset when getting w/o key. */ - function data(key, value) { + data(key, value) { if (typeof key === 'string') { // Set `key`. if (arguments.length === 2) { - assertUnfrozen('data', frozen) - namespace[key] = value - return processor + assertUnfrozen('data', this.frozen) + this.namespace[key] = value + return this } // Get `key`. - return (own.call(namespace, key) && namespace[key]) || undefined + return (own.call(this.namespace, key) && this.namespace[key]) || undefined } // Set space. if (key) { - assertUnfrozen('data', frozen) - namespace = key - return processor + assertUnfrozen('data', this.frozen) + this.namespace = key + return this } // Get space. - return namespace + return this.namespace } - /** @type {Processor['freeze']} */ - function freeze() { - if (frozen) { - return processor + /** + * 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} + * The processor that `freeze` was called on. + */ + freeze() { + if (this.frozen) { + return this } - while (++freezeIndex < attachers.length) { - const [attacher, ...options] = attachers[freezeIndex] + // 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 @@ -129,29 +657,485 @@ function base() { options[0] = undefined } - const transformer = attacher.call(processor, ...options) + const transformer = attacher.call(self, ...options) if (typeof transformer === 'function') { - transformers.use(transformer) + this.transformers.use(transformer) } } - frozen = true - freezeIndex = Number.POSITIVE_INFINITY + 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 {VFileCompatible | undefined} [file] + * file to parse; typically `string`; 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 + assertParser('parse', Parser) + + if (newable(Parser, 'parse')) { + const ParserClass = + /** @type {ParserClass} */ ( + Parser + ) + + const parserInstace = new ParserClass(String(realFile), realFile) + + return parserInstace.parse() + } + + const parserFunction = + /** @type {ParserFunction} */ ( + Parser + ) - return processor + return parserFunction(String(realFile), realFile) } /** - * @param {Exclude | PluggableList | null | undefined} [value] + * 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 {VFileCompatible | undefined} file + * @param {ProcessCallback>} done + * @returns {undefined} + * + * @overload + * @param {VFileCompatible | undefined} [file] + * @returns {Promise>} + * + * @param {VFileCompatible | undefined} [file] + * File; any value accepted as `x` in `new VFile(x)`. + * @param {ProcessCallback> | undefined} [done] + * Callback. + * @returns {Promise | undefined} + * Nothing if `done` is given. + * Otherwise `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) + assertCompiler('process', this.Compiler) + + if (!done) { + return new Promise(executor) + } + + executor(undefined, done) + + // Note: `void`s needed for TS. + /** + * @param {((file: VFile) => 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) { + realDone(error) + } else { + // 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 result = self.stringify(compileTree, file) + + if (result === null || result === undefined) { + // Empty. + } else if (looksLikeAVFileValue(result)) { + file.value = result + } else { + file.result = result + } + + realDone(error, file) + } + }) + + /** + * @param {Error | undefined} [error] + * @param {VFile | undefined} [file] + * @returns {undefined} + */ + function realDone(error, file) { + if (error || !file) { + reject(error) + } else if (resolve) { + resolve(file) + } else { + // @ts-expect-error: `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 {VFileCompatible | undefined} [file] + * File; any value accepted as `x` in `new VFile(x)`. + * @returns {VFileWithOutput} + * 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 | undefined} */ + let complete + + this.freeze() + assertParser('processSync', this.Parser) + assertCompiler('processSync', this.Compiler) + + // The result will be set by `this.process` on this file. + const realFile = /** @type {VFileWithOutput} */ (vfile(file)) + + this.process(realFile, realDone) + + assertDone('processSync', 'process', complete) + + return realFile + + /** + * @param {Error | undefined} [error] + * @returns {undefined} + */ + function realDone(error) { + complete = true + bail(error) + } + } + + /** + * 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} done + * @returns {undefined} + * + * @overload + * @param {HeadTree extends undefined ? Node : HeadTree} tree + * @param {VFileCompatible | undefined} file + * @param {RunCallback} done + * @returns {undefined} + * + * @overload + * @param {HeadTree extends undefined ? Node : HeadTree} tree + * @param {VFileCompatible | undefined} [file] + * @returns {Promise} + * + * @param {HeadTree extends undefined ? Node : HeadTree} tree + * Tree to transform and inspect. + * @param {( + * RunCallback | + * VFileCompatible + * )} [file] + * File associated with `node` (optional); any value accepted as `x` in + * `new VFile(x)`. + * @param {RunCallback} [done] + * @returns {Promise | undefined} + * Nothing if `done` is given. + * Otherwise, `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 + } + + if (!done) { + return new Promise(executor) + } + + executor(undefined, done) + + // 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) { + // @ts-expect-error: `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 { + // @ts-expect-error: `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 {VFileCompatible | 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 {Node | undefined} */ + let result + /** @type {boolean | undefined} */ + let complete + + this.run(tree, file, realDone) + + assertDone('runSync', 'run', complete) + + // @ts-expect-error: we either bailed on an error or have a tree. + return result + + /** + * @param {Error | undefined} [error] + * @param {Node} [tree] + * @returns {undefined} + */ + 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 {VFileCompatible | undefined} [file] + * File associated with `node` (optional); any value accepted as `x` in + * `new VFile(x)`. + * @returns {CompileResult extends undefined ? VFileValue | null : 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 + assertCompiler('stringify', Compiler) + assertNode(tree) + + if (newable(Compiler, 'compile')) { + const CompilerClass = /** + * @type {( + * CompilerClass< + * CompileTree extends undefined ? Node : CompileTree, + * CompileResult extends undefined ? CompileResults : CompileResult + * > + * )} + */ (Compiler) + + const compilerInstace = new CompilerClass(tree, realFile) + + return compilerInstace.compile() + } + + const compilerFunction = /** + * @type {( + * CompilerFunction< + * CompileTree extends undefined ? Node : CompileTree, + * CompileResult extends undefined ? CompileResults : CompileResult + * > + * )} + */ (Compiler) + + return compilerFunction(tree, realFile) + } + + /** + * Configure the processor to use a plugin, a list of usable + * values, or with 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. + * + * @example + * ```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} [Parameters=[]] + * @template {Node | string | undefined} [Input=undefined] + * @template [Output=Input] + * + * @overload + * @param {Preset | null | undefined} [preset] + * @returns {Processor} + * + * @overload + * @param {PluggableList} list + * @returns {Processor} + * + * @overload + * @param {Plugin} plugin + * @param {...(Parameters | [boolean])} parameters + * @returns {UsePlugin} + * + * @overload + * @param {[plugin: Plugin, enable: boolean] | [plugin: Plugin, ...parameters: Parameters]} tuple + * @returns {UsePlugin} + * + * @param {[plugin: Plugin, ...parameters: Array] | PluggableList | Plugin | Preset | null | undefined} value + * Usable value. * @param {...unknown} options - * @returns {Processor} + * Parameters, when a plugin is given as a usable value. + * @returns {Processor} + * Current processor. */ - function use(value, ...options) { + use(value, ...options) { + const attachers = this.attachers + const namespace = this.namespace /** @type {Record | undefined} */ let settings - assertUnfrozen('use', frozen) + assertUnfrozen('use', this.frozen) if (value === null || value === undefined) { // Empty. @@ -159,6 +1143,7 @@ function base() { addPlugin(value, ...options) } else if (typeof value === 'object') { if (Array.isArray(value)) { + // @ts-expect-error: look at tuples? addList(value) } else { addPreset(value) @@ -172,10 +1157,10 @@ function base() { namespace.settings = Object.assign(namespace.settings || {}, settings) } - return processor + return this /** - * @param {import('../index.js').Pluggable} value + * @param {Pluggable} value * @returns {undefined} */ function add(value) { @@ -239,7 +1224,7 @@ function base() { */ function addPlugin(plugin, value) { let index = -1 - /** @type {Processor['attachers'][number] | undefined} */ + /** @type {PluginTuple> | undefined} */ let entry while (++index < attachers.length) { @@ -261,211 +1246,18 @@ function base() { } } } - - /** @type {Processor['parse']} */ - function parse(doc) { - processor.freeze() - const file = vfile(doc) - const Parser = processor.Parser - assertParser('parse', Parser) - - if (newable(Parser, 'parse')) { - // @ts-expect-error: `newable` checks this. - return new Parser(String(file), file).parse() - } - - // @ts-expect-error: `newable` checks this. - return Parser(String(file), file) // eslint-disable-line new-cap - } - - /** @type {Processor['stringify']} */ - function stringify(node, doc) { - processor.freeze() - const file = vfile(doc) - const Compiler = processor.Compiler - assertCompiler('stringify', Compiler) - assertNode(node) - - if (newable(Compiler, 'compile')) { - // @ts-expect-error: `newable` checks this. - return new Compiler(node, file).compile() - } - - // @ts-expect-error: `newable` checks this. - return Compiler(node, file) // eslint-disable-line new-cap - } - - /** - * @param {Node} node - * @param {RunCallback | VFileCompatible} [doc] - * @param {RunCallback} [callback] - * @returns {Promise | undefined} - */ - function run(node, doc, callback) { - assertNode(node) - processor.freeze() - - if (!callback && typeof doc === 'function') { - callback = doc - doc = undefined - } - - if (!callback) { - return new Promise(executor) - } - - executor(undefined, callback) - - // Note: `void`s needed for TS. - /** - * @param {((node: Node) => undefined | void) | undefined} resolve - * @param {(error: Error) => undefined | void} reject - * @returns {undefined} - */ - function executor(resolve, reject) { - // @ts-expect-error: `doc` canโ€™t be a callback anymore, we checked. - transformers.run(node, vfile(doc), done) - - /** - * @param {Error | undefined} error - * @param {Node} tree - * @param {VFile} file - * @returns {undefined} - */ - function done(error, tree, file) { - tree = tree || node - if (error) { - reject(error) - } else if (resolve) { - resolve(tree) - } else { - // @ts-expect-error: `callback` is defined if `resolve` is not. - callback(undefined, tree, file) - } - } - } - } - - /** @type {Processor['runSync']} */ - function runSync(node, file) { - /** @type {Node | undefined} */ - let result - /** @type {boolean | undefined} */ - let complete - - processor.run(node, file, done) - - assertDone('runSync', 'run', complete) - - // @ts-expect-error: we either bailed on an error or have a tree. - return result - - /** - * @param {Error | undefined} [error] - * @param {Node} [tree] - * @returns {undefined} - */ - function done(error, tree) { - bail(error) - result = tree - complete = true - } - } - - /** - * @param {VFileCompatible} doc - * @param {ProcessCallback} [callback] - * @returns {Promise | undefined} - */ - function process(doc, callback) { - processor.freeze() - assertParser('process', processor.Parser) - assertCompiler('process', processor.Compiler) - - if (!callback) { - return new Promise(executor) - } - - executor(undefined, callback) - - // Note: `void`s needed for TS. - /** - * @param {((file: VFile) => undefined | void) | undefined} resolve - * @param {(error?: Error | undefined) => undefined | void} reject - * @returns {undefined} - */ - function executor(resolve, reject) { - const file = vfile(doc) - - processor.run(processor.parse(file), file, function (error, tree, file) { - if (error || !tree || !file) { - done(error) - } else { - /** @type {unknown} */ - const result = processor.stringify(tree, file) - - if (result === null || result === undefined) { - // Empty. - } else if (looksLikeAVFileValue(result)) { - file.value = result - } else { - file.result = result - } - - done(error, file) - } - }) - - /** - * @param {Error | undefined} [error] - * @param {VFile | undefined} [file] - * @returns {undefined} - */ - function done(error, file) { - if (error || !file) { - reject(error) - } else if (resolve) { - resolve(file) - } else { - // @ts-expect-error: `callback` is defined if `resolve` is not. - callback(undefined, file) - } - } - } - } - - /** @type {import('../index.js').Processor['processSync']} */ - function processSync(doc) { - /** @type {boolean | undefined} */ - let complete - - processor.freeze() - assertParser('processSync', processor.Parser) - assertCompiler('processSync', processor.Compiler) - - const file = vfile(doc) - - processor.process(file, done) - - assertDone('processSync', 'process', complete) - - return file - - /** - * @param {Error | undefined} [error] - * @returns {undefined} - */ - function done(error) { - complete = true - bail(error) - } - } } +/** + * Base processor. + */ +export const unified = new Processor().freeze() + /** * Check if `value` is a constructor. * - * @param {unknown} value + * @template {unknown} Value + * @param {Value} value * @param {string} name * @returns {boolean} */ @@ -520,7 +1312,7 @@ function assertParser(name, value) { * * @param {string} name * @param {unknown} value - * @returns {asserts value is Compiler} + * @returns {asserts value is Compiler} */ function assertCompiler(name, value) { if (typeof value !== 'function') { diff --git a/package.json b/package.json index e135f064..a51d02db 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "xo": "^0.56.0" }, "scripts": { - "build": "tsc --build --clean && tsc --build && type-coverage && tsd", + "build": "tsc --build --clean && tsc --build && node script/fix-types.js && type-coverage && tsd", "format": "remark . --frail --output --quiet && prettier . --log-level warn --write && xo --fix", "prepack": "npm run build && npm run format", "test": "npm run build && npm run format && npm run test-coverage", diff --git a/script/fix-types.js b/script/fix-types.js new file mode 100644 index 00000000..a17b1fcf --- /dev/null +++ b/script/fix-types.js @@ -0,0 +1,38 @@ +import fs from 'node:fs/promises' + +const url = new URL('../lib/index.d.ts', import.meta.url) + +let file = '' + +try { + file = String(await fs.readFile(url)) +} catch { + console.error('Could not read `lib/index.d.ts`, did `tsc` run already?') +} + +const result = file + .replace(/declare const Processor_base: [^\n]+/, function () { + console.log('Fixed `CallableInstance` import') + return "declare const CallableInstance: import('./callable-instance.js').ICallableInstance" + }) + .replace(/extends Processor_base/, function () { + console.log('Fixed `CallableInstance` use') + return 'extends CallableInstance<[], Processor>' + }) + .replace( + /\.\.\.parameters: Parameters_1 \| \[boolean] \| undefined/, + function () { + console.log( + 'Fixed `use` overload with plugin, and *non-optional* parameters' + ) + return '...parameters: Parameters_1 | [boolean]' + } + ) + +if (file === result) { + console.error( + 'Could not fix `lib/index.d.ts`, was `tsc` fixed somewhow? Or were changes already applied?' + ) +} else { + await fs.writeFile(url, result) +} diff --git a/test/core.js b/test/core.js index 5fc9ffda..b9e056ba 100644 --- a/test/core.js +++ b/test/core.js @@ -9,7 +9,6 @@ test('core', async function (t) { await t.test('should expose a frozen processor', async function () { assert.throws(function () { - // @ts-expect-error: check that `use` cannot be used on frozen processors. unified.use(function () {}) }, /Cannot call `use` on a frozen processor/) }) diff --git a/test/parse.js b/test/parse.js index 3fa472f2..1d4dc5e6 100644 --- a/test/parse.js +++ b/test/parse.js @@ -73,17 +73,28 @@ test('`parse`', async function (t) { async function () { const processor = unified() - processor.Parser = function (doc, file) { + /** + * @constructor + * @param {string} doc + * @param {VFile} file + */ + function Parser(doc, file) { assert.equal(typeof doc, 'string') assert.ok(file instanceof VFile) assert.equal(arguments.length, 2) } - processor.Parser.prototype.parse = function () { + /** + * @returns {Node} + */ + // type-coverage:ignore-next-line -- for some reason TS does understand `Parser.prototype`, but not `Compiler.prototype`. + Parser.prototype.parse = function () { assert.equal(arguments.length, 0) return givenNode } + processor.Parser = Parser + assert.equal(processor.parse('charlie'), givenNode) } ) diff --git a/test/process-compilers.js b/test/process-compilers.js index dad6c4c1..88c89d56 100644 --- a/test/process-compilers.js +++ b/test/process-compilers.js @@ -59,6 +59,8 @@ test('process (compilers)', async function (t) { } processor.Parser = simpleParser + + // @ts-expect-error: custom node, which should be registered!. processor.Compiler = function () { return result } diff --git a/test/stringify.js b/test/stringify.js index ad86b19d..bc55efee 100644 --- a/test/stringify.js +++ b/test/stringify.js @@ -70,18 +70,25 @@ test('`stringify`', async function (t) { async function () { const processor = unified() - processor.Compiler = function (node, file) { + /** + * @constructor + * @param {Node} node + * @param {VFile} file + */ + function Compiler(node, file) { assert.equal(node, givenNode, 'should pass a node') assert.ok(file instanceof VFile, 'should pass a file') assert.equal(arguments.length, 2) } - // type-coverage:ignore-next-line -- for some reason TS does understand `Parser.prototype`, but not `Compiler.prototype`. - processor.Compiler.prototype.compile = function () { + // type-coverage:ignore-next-line -- for some reason TS does understand `Compiler.prototype`, but not `Compiler.prototype`. + Compiler.prototype.compile = function () { assert.equal(arguments.length, 0) return 'echo' } + processor.Compiler = Compiler + assert.equal(processor.stringify(givenNode, givenFile), 'echo') } ) diff --git a/tsconfig.json b/tsconfig.json index ad1496e9..dd290ca4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,5 @@ "target": "es2020" }, "exclude": ["coverage/", "node_modules/"], - "include": ["**/*.js", "index.d.ts"] + "include": ["**/*.js", "lib/callable-instance.d.ts", "index.d.ts"] }