diff --git a/lib/complex-types.d.ts b/lib/complex-types.d.ts deleted file mode 100644 index 9a4e26e..0000000 --- a/lib/complex-types.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type {Node, Parent} from 'unist' - -/** - * Internal utility to collect all descendants of in `Tree`. - * @see https://github.com/syntax-tree/unist-util-visit-parents/blob/18d36ad/complex-types.d.ts#L43 - */ -export type InclusiveDescendant< - Tree extends Node = never, - Found = void -> = Tree extends Parent - ? - | Tree - | InclusiveDescendant< - Exclude, - Found | Tree - > - : Tree - -export type MapFunction = ( - node: InclusiveDescendant, - index: number | undefined, - parent: InclusiveDescendant | undefined -) => InclusiveDescendant diff --git a/lib/index.js b/lib/index.js index a68ef75..a6995f8 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,11 +1,59 @@ /** - * @typedef {import('unist').Node} Node + * @typedef {import('unist').Node} UnistNode + * @typedef {import('unist').Parent} UnistParent */ /** - * @template {Node} [Kind=Node] + * @typedef {0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10} Uint + * Number; capped reasonably. + * @see https://github.com/syntax-tree/unist-util-visit-parents/blob/main/lib/index.js + */ + +/** + * @typedef {I extends 0 ? 1 : I extends 1 ? 2 : I extends 2 ? 3 : I extends 3 ? 4 : I extends 4 ? 5 : I extends 5 ? 6 : I extends 6 ? 7 : I extends 7 ? 8 : I extends 8 ? 9 : 10} Increment + * Increment a number in the type system. + * @template {Uint} [I=0] + * Index. + * @see https://github.com/syntax-tree/unist-util-visit-parents/blob/main/lib/index.js + */ + +/** + * @typedef {( + * Tree extends UnistParent + * ? Depth extends Max + * ? Tree + * : Tree | InclusiveDescendant> + * : Tree + * )} InclusiveDescendant + * Collect all (inclusive) descendants of `Tree`. + * + * > 👉 **Note**: for performance reasons, this seems to be the fastest way to + * > recurse without actually running into an infinite loop, which the + * > previous version did. + * > + * > Practically, a max of `2` is typically enough assuming a `Root` is + * > passed, but it doesn’t improve performance. + * > It gets higher with `List > ListItem > Table > TableRow > TableCell`. + * > Using up to `10` doesn’t hurt or help either. + * @template {UnistNode} Tree + * Tree type. + * @template {Uint} [Max=10] + * Max; searches up to this depth. + * @template {Uint} [Depth=0] + * Current depth. + * @see https://github.com/syntax-tree/unist-util-visit-parents/blob/main/lib/index.js + */ + +/** + * @template {UnistNode} Tree * Node type. - * @typedef {import('./complex-types.js').MapFunction} MapFunction + * @typedef {( + * ( + * node: Readonly>, + * index: number | undefined, + * parent: Readonly, UnistParent>> | undefined + * ) => Tree | InclusiveDescendant + * )} MapFunction * Function called with a node, its index, and its parent to produce a new * node. */ @@ -13,32 +61,42 @@ /** * Create a new tree by mapping all nodes with the given function. * - * @template {Node} Kind + * @template {UnistNode} Tree * Type of input tree. - * @param {Kind} tree + * @param {Tree} tree * Tree to map. - * @param {MapFunction} mapFunction + * @param {MapFunction} mapFunction * Function called with a node, its index, and its parent to produce a new * node. - * @returns {Kind} + * @returns {InclusiveDescendant} * New mapped tree. */ export function map(tree, mapFunction) { - // @ts-expect-error Looks like a children. - return preorder(tree, null, null) - - /** @type {import('./complex-types.js').MapFunction} */ - function preorder(node, index, parent) { - const newNode = Object.assign({}, mapFunction(node, index, parent)) - - if ('children' in node) { - // @ts-expect-error Looks like a parent. - newNode.children = node.children.map(function ( - /** @type {import('./complex-types.js').InclusiveDescendant} */ child, - /** @type {number} */ index - ) { - return preorder(child, index, node) + const result = preorder(tree, undefined, undefined) + + // @ts-expect-error: the new node is expected. + return result + + /** @type {MapFunction} */ + function preorder(oldNode, index, parent) { + /** @type {UnistNode} */ + const newNode = { + ...mapFunction( + // @ts-expect-error: the old node is expected. + oldNode, + index, + parent + ) + } + + if ('children' in oldNode) { + const newNodeAstParent = /** @type {UnistParent} */ (newNode) + + const nextChildren = oldNode.children.map(function (child, index) { + return preorder(child, index, oldNode) }) + + newNodeAstParent.children = nextChildren } return newNode diff --git a/readme.md b/readme.md index bfbb89c..e3a1cea 100644 --- a/readme.md +++ b/readme.md @@ -80,13 +80,13 @@ const tree = u('tree', [ u('leaf', 'leaf 3') ]) -const next = map(tree, (node) => { +const next = map(tree, function (node) { return node.type === 'leaf' ? Object.assign({}, node, {value: 'CHANGED'}) : node }) -console.dir(next, {depth: null}) +console.dir(next, {depth: undefined}) ``` Yields: diff --git a/test.js b/test.js index d09efd0..2fee133 100644 --- a/test.js +++ b/test.js @@ -1,97 +1,52 @@ /** - * @typedef {{type: 'leaf', value: string}} Leaf - * @typedef {{type: 'node', children: Array}} Node - * @typedef {{type: 'root', children: Array}} Root - * @typedef {Root | Node | Leaf} AnyNode + * @typedef {import('mdast').Content} Content + * @typedef {import('mdast').Root} Root */ import assert from 'node:assert/strict' import test from 'node:test' import {u} from 'unist-builder' import {map} from './index.js' -import * as mod from './index.js' -test('map', function () { - assert.deepEqual( - Object.keys(mod).sort(), - ['map'], - 'should expose the public api' - ) - - /** @type {Root} */ - const rootA = u('root', [u('node', [u('leaf', '1')]), u('leaf', '2')]) - assert.deepEqual( - map(rootA, changeLeaf), - u('root', [u('node', [u('leaf', 'CHANGED')]), u('leaf', 'CHANGED')]), - 'should map the specified node' - ) - - /** @type {Root} */ - const rootB = { - type: 'root', - children: [ - {type: 'node', children: [{type: 'leaf', value: '1'}]}, - {type: 'leaf', value: '2'} - ] - } - assert.deepEqual( - map(rootB, changeLeaf), - u('root', [u('node', [u('leaf', 'CHANGED')]), u('leaf', 'CHANGED')]), - 'should map the specified node' - ) - - /** @type {Root} */ - const rootC = u('root', [u('node', [u('leaf', '1')]), u('leaf', '2')]) - assert.deepEqual( - // @ts-expect-error: invalid: - map(rootC, nullLeaf), - // @ts-expect-error: not valid but tested anyway. - u('root', [u('node', [{}]), {}]), - 'should work when retuning an empty object' - ) - - assert.deepEqual( - // @ts-expect-error runtime. - map({}, addValue), - {value: 'test'}, - 'should work when passing an empty object' - ) - - /** @type {Root} */ - const tree = u('root', [u('node', [u('leaf', '1')]), u('leaf', '2')]) - - assert.deepEqual( - map(tree, asIs), - tree, - 'should support an explicitly typed `MapFunction`' - ) +test('map', async function (t) { + await t.test('should expose the public api', async function () { + assert.deepEqual(Object.keys(await import('./index.js')).sort(), ['map']) + }) + + await t.test('should map the specified node', async function () { + /** @type {Root} */ + const tree = u('root', [u('paragraph', [u('text', '1')]), u('text', '2')]) + + assert.deepEqual( + map(tree, changeLeaf), + u('root', [u('paragraph', [u('text', 'CHANGED')]), u('text', 'CHANGED')]) + ) + }) + + await t.test('should map the specified node', async function () { + /** @type {Root} */ + const tree = u('root', [u('paragraph', [u('text', '1')]), u('text', '2')]) + + assert.deepEqual( + map(tree, changeLeaf), + u('root', [u('paragraph', [u('text', 'CHANGED')]), u('text', 'CHANGED')]) + ) + }) + + await t.test('should work when passing an empty object', async function () { + assert.deepEqual( + // @ts-expect-error: check how not-a-node is handled. + map({}, function () { + return {value: 'test'} + }), + {value: 'test'} + ) + }) }) -/** - * @param {AnyNode} node - * @returns {AnyNode} - */ -function changeLeaf(node) { - return node.type === 'leaf' - ? Object.assign({}, node, {value: 'CHANGED'}) - : node -} - -/** - * @param {AnyNode} node - * @returns {Root | Node | null} - */ -function nullLeaf(node) { - return node.type === 'leaf' ? null : node -} - -function addValue() { - return {value: 'test'} -} - /** * @type {import('./index.js').MapFunction} */ -function asIs(node) { - return node +function changeLeaf(node) { + return node.type === 'text' ? {...node, value: 'CHANGED'} : node } diff --git a/tsconfig.json b/tsconfig.json index 81b0746..870d82c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,5 @@ "target": "es2020" }, "exclude": ["coverage/", "node_modules/"], - "include": ["**/*.js", "lib/complex-types.d.ts"] + "include": ["**/*.js"] }