599 lines
14 KiB
JavaScript
599 lines
14 KiB
JavaScript
/**
|
||
* @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<string, unknown>} */
|
||
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<string, unknown>} [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<string, unknown>|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<unknown[]>} 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<Node>|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<VFile>|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<string, unknown>} 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)
|
||
}
|