/** * @typedef {import('unist').Node} Node * @typedef {import('vfile').VFileCompatible} VFileCompatible * @typedef {import('vfile').VFileValue} VFileValue * @typedef {import('..').Processor} Processor * @typedef {import('..').Plugin} Plugin * @typedef {import('..').Preset} Preset * @typedef {import('..').Pluggable} Pluggable * @typedef {import('..').PluggableList} PluggableList * @typedef {import('..').Transformer} Transformer * @typedef {import('..').Parser} Parser * @typedef {import('..').Compiler} Compiler * @typedef {import('..').RunCallback} RunCallback * @typedef {import('..').ProcessCallback} ProcessCallback * * @typedef Context * @property {Node} tree * @property {VFile} file */ import {bail} from 'bail' import isBuffer from 'is-buffer' import extend from 'extend' import isPlainObj from 'is-plain-obj' import {trough} from 'trough' import {VFile} from 'vfile' // Expose a frozen processor. export const unified = base().freeze() const own = {}.hasOwnProperty // Function to create the first processor. /** * @returns {Processor} */ function base() { const transformers = trough() /** @type {Processor['attachers']} */ const attachers = [] /** @type {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 // @ts-expect-error: overloads are handled. 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() let index = -1 while (++index < attachers.length) { destination.use(...attachers[index]) } destination.data(extend(true, {}, namespace)) return destination } /** * @param {string|Record} [key] * @param {unknown} [value] * @returns {unknown} */ function data(key, value) { if (typeof key === 'string') { // Set `key`. if (arguments.length === 2) { assertUnfrozen('data', frozen) namespace[key] = value return processor } // Get `key`. return (own.call(namespace, key) && namespace[key]) || null } // Set space. if (key) { assertUnfrozen('data', frozen) namespace = key return processor } // Get space. return namespace } /** @type {Processor['freeze']} */ function freeze() { if (frozen) { return processor } while (++freezeIndex < attachers.length) { const [attacher, ...options] = attachers[freezeIndex] if (options[0] === false) { continue } if (options[0] === true) { options[0] = undefined } /** @type {Transformer|void} */ const transformer = attacher.call(processor, ...options) if (typeof transformer === 'function') { transformers.use(transformer) } } frozen = true freezeIndex = Number.POSITIVE_INFINITY return processor } /** * @param {Pluggable|null|undefined} [value] * @param {...unknown} options * @returns {Processor} */ function use(value, ...options) { /** @type {Record|undefined} */ let settings assertUnfrozen('use', frozen) if (value === null || value === undefined) { // Empty. } else if (typeof value === 'function') { addPlugin(value, ...options) } else if (typeof value === 'object') { if (Array.isArray(value)) { addList(value) } else { addPreset(value) } } else { throw new TypeError('Expected usable value, not `' + value + '`') } if (settings) { namespace.settings = Object.assign(namespace.settings || {}, settings) } return processor /** * @param {import('..').Pluggable} value * @returns {void} */ function add(value) { if (typeof value === 'function') { addPlugin(value) } else if (typeof value === 'object') { if (Array.isArray(value)) { const [plugin, ...options] = value addPlugin(plugin, ...options) } else { addPreset(value) } } else { throw new TypeError('Expected usable value, not `' + value + '`') } } /** * @param {Preset} result * @returns {void} */ function addPreset(result) { addList(result.plugins) if (result.settings) { settings = Object.assign(settings || {}, result.settings) } } /** * @param {PluggableList|null|undefined} [plugins] * @returns {void} */ 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 {...unknown} [value] * @returns {void} */ function addPlugin(plugin, value) { let index = -1 /** @type {Processor['attachers'][number]|undefined} */ let entry while (++index < attachers.length) { if (attachers[index][0] === plugin) { entry = attachers[index] break } } if (entry) { if (isPlainObj(entry[1]) && isPlainObj(value)) { value = extend(true, entry[1], value) } entry[1] = value } else { // @ts-expect-error: fine. attachers.push([...arguments]) } } } /** @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 {VFileCompatible|RunCallback} [doc] * @param {RunCallback} [callback] * @returns {Promise|void} */ 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(null, callback) /** * @param {null|((node: Node) => void)} resolve * @param {(error: Error) => void} reject * @returns {void} */ function executor(resolve, reject) { // @ts-expect-error: `doc` can’t be a callback anymore, we checked. transformers.run(node, vfile(doc), done) /** * @param {Error|null} error * @param {Node} tree * @param {VFile} file * @returns {void} */ 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(null, 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|null} [error] * @param {Node} [tree] * @returns {void} */ 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(null, callback) /** * @param {null|((file: VFile) => void)} resolve * @param {(error?: Error|null|undefined) => void} reject * @returns {void} */ function executor(resolve, reject) { const file = vfile(doc) processor.run(processor.parse(file), file, (error, tree, file) => { if (error || !tree || !file) { done(error) } else { /** @type {unknown} */ const result = processor.stringify(tree, file) if (result === undefined || result === null) { // Empty. } else if (looksLikeAVFileValue(result)) { file.value = result } else { file.result = result } done(error, file) } }) /** * @param {Error|null|undefined} [error] * @param {VFile|undefined} [file] * @returns {void} */ 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(null, file) } } } } /** @type {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|null|undefined} [error] * @returns {void} */ function done(error) { complete = true bail(error) } } } /** * Check if `value` is a constructor. * * @param {unknown} value * @param {string} name * @returns {boolean} */ function newable(value, name) { return ( typeof value === 'function' && // Prototypes do exist. // type-coverage:ignore-next-line value.prototype && // A function with keys in its prototype is probably a constructor. // Classes’ prototype methods are not enumerable, so we check if some value // exists in the prototype. // type-coverage:ignore-next-line (keys(value.prototype) || name in value.prototype) ) } /** * Check if `value` is an object with keys. * * @param {Record} value * @returns {boolean} */ function keys(value) { /** @type {string} */ let key for (key in value) { if (own.call(value, key)) { return true } } return false } /** * 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 {VFileCompatible} [value] * @returns {VFile} */ function vfile(value) { return looksLikeAVFile(value) ? value : new VFile(value) } /** * @param {VFileCompatible} [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 VFileValue} */ function looksLikeAVFileValue(value) { return typeof value === 'string' || isBuffer(value) }