/** * @typedef {import('unist').Node} Node * @typedef {import('unist').Parent} Parent * @typedef {import('unist-util-is').Test} Test */ /** * @typedef {boolean | 'skip'} Action * Union of the action types. * * @typedef {number} Index * Move to the sibling at `index` next (after node itself is completely * traversed). * * Useful if mutating the tree, such as removing the node the visitor is * currently on, or any of its previous siblings. * Results less than 0 or greater than or equal to `children.length` stop * traversing the parent. * * @typedef {[(Action | null | undefined | void)?, (Index | null | undefined)?]} ActionTuple * List with one or two values, the first an action, the second an index. * * @typedef {Action | ActionTuple | Index | null | undefined | void} VisitorResult * Any value that can be returned from a visitor. */ /** * @template {Node} [Visited=Node] * Visited node type. * @template {Parent} [Ancestor=Parent] * Ancestor type. * @callback Visitor * Handle a node (matching `test`, if given). * * Visitors are free to transform `node`. * They can also transform the parent of node (the last of `ancestors`). * * Replacing `node` itself, if `SKIP` is not returned, still causes its * descendants to be walked (which is a bug). * * When adding or removing previous siblings of `node` (or next siblings, in * case of reverse), the `Visitor` should return a new `Index` to specify the * sibling to traverse after `node` is traversed. * Adding or removing next siblings of `node` (or previous siblings, in case * of reverse) is handled as expected without needing to return a new `Index`. * * Removing the children property of an ancestor still results in them being * traversed. * @param {Visited} node * Found node. * @param {Array} ancestors * Ancestors of `node`. * @returns {VisitorResult} * What to do next. * * An `Index` is treated as a tuple of `[CONTINUE, Index]`. * An `Action` is treated as a tuple of `[Action]`. * * Passing a tuple back only makes sense if the `Action` is `SKIP`. * When the `Action` is `EXIT`, that action can be returned. * When the `Action` is `CONTINUE`, `Index` can be returned. */ /** * @template {Node} [Tree=Node] * Tree type. * @template {Test} [Check=string] * Test type. * @typedef {Visitor, Check>, Extract, Parent>>} BuildVisitor * Build a typed `Visitor` function from a tree and a test. * * It will infer which values are passed as `node` and which as `parents`. */ import {convert} from 'unist-util-is' import {color} from './color.js' /** * Continue traversing as normal. */ export const CONTINUE = true /** * Stop traversing immediately. */ export const EXIT = false /** * Do not traverse this node’s children. */ export const SKIP = 'skip' /** * Visit nodes, with ancestral information. * * This algorithm performs *depth-first* *tree traversal* in *preorder* * (**NLR**) or if `reverse` is given, in *reverse preorder* (**NRL**). * * You can choose for which nodes `visitor` is called by passing a `test`. * For complex tests, you should test yourself in `visitor`, as it will be * faster and will have improved type information. * * Walking the tree is an intensive task. * Make use of the return values of the visitor when possible. * Instead of walking a tree multiple times, walk it once, use `unist-util-is` * to check if a node matches, and then perform different operations. * * You can change the tree. * See `Visitor` for more info. * * @param tree * Tree to traverse. * @param test * `unist-util-is`-compatible test * @param visitor * Handle each node. * @param reverse * Traverse in reverse preorder (NRL) instead of the default preorder (NLR). * @returns * Nothing. */ export const visitParents = /** * @type {( * ((tree: Tree, test: Check, visitor: BuildVisitor, reverse?: boolean | null | undefined) => void) & * ((tree: Tree, visitor: BuildVisitor, reverse?: boolean | null | undefined) => void) * )} */ ( /** * @param {Node} tree * @param {Test} test * @param {Visitor} visitor * @param {boolean | null | undefined} [reverse] * @returns {void} */ function (tree, test, visitor, reverse) { if (typeof test === 'function' && typeof visitor !== 'function') { reverse = visitor // @ts-expect-error no visitor given, so `visitor` is test. visitor = test test = null } const is = convert(test) const step = reverse ? -1 : 1 factory(tree, undefined, [])() /** * @param {Node} node * @param {number | undefined} index * @param {Array} parents */ function factory(node, index, parents) { /** @type {Record} */ // @ts-expect-error: hush const value = node && typeof node === 'object' ? node : {} if (typeof value.type === 'string') { const name = // `hast` typeof value.tagName === 'string' ? value.tagName : // `xast` typeof value.name === 'string' ? value.name : undefined Object.defineProperty(visit, 'name', { value: 'node (' + color(node.type + (name ? '<' + name + '>' : '')) + ')' }) } return visit function visit() { /** @type {ActionTuple} */ let result = [] /** @type {ActionTuple} */ let subresult /** @type {number} */ let offset /** @type {Array} */ let grandparents if (!test || is(node, index, parents[parents.length - 1] || null)) { result = toResult(visitor(node, parents)) if (result[0] === EXIT) { return result } } // @ts-expect-error looks like a parent. if (node.children && result[0] !== SKIP) { // @ts-expect-error looks like a parent. offset = (reverse ? node.children.length : -1) + step // @ts-expect-error looks like a parent. grandparents = parents.concat(node) // @ts-expect-error looks like a parent. while (offset > -1 && offset < node.children.length) { // @ts-expect-error looks like a parent. subresult = factory(node.children[offset], offset, grandparents)() if (subresult[0] === EXIT) { return subresult } offset = typeof subresult[1] === 'number' ? subresult[1] : offset + step } } return result } } } ) /** * Turn a return value into a clean result. * * @param {VisitorResult} value * Valid return values from visitors. * @returns {ActionTuple} * Clean result. */ function toResult(value) { if (Array.isArray(value)) { return value } if (typeof value === 'number') { return [CONTINUE, value] } return [value] }