Skip to content

Commit

Permalink
Change to improve type inferral
Browse files Browse the repository at this point in the history
  • Loading branch information
wooorm committed Jul 5, 2023
1 parent 402d98b commit 66e15b8
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 130 deletions.
23 changes: 0 additions & 23 deletions lib/complex-types.d.ts

This file was deleted.

100 changes: 79 additions & 21 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,102 @@
/**
* @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['children'][number], Max, Increment<Depth>>
* : 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<Kind>} MapFunction
* @typedef {(
* (
* node: Readonly<InclusiveDescendant<Tree>>,
* index: number | undefined,
* parent: Readonly<Extract<InclusiveDescendant<Tree>, UnistParent>> | undefined
* ) => Tree | InclusiveDescendant<Tree>
* )} MapFunction
* Function called with a node, its index, and its parent to produce a new
* node.
*/

/**
* 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<Kind>} mapFunction
* @param {MapFunction<Tree>} mapFunction
* Function called with a node, its index, and its parent to produce a new
* node.
* @returns {Kind}
* @returns {InclusiveDescendant<Tree>}
* 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<Kind>} */
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<Kind>} */ 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<UnistNode | UnistParent>} */
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
Expand Down
4 changes: 2 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
121 changes: 38 additions & 83 deletions test.js
Original file line number Diff line number Diff line change
@@ -1,97 +1,52 @@
/**
* @typedef {{type: 'leaf', value: string}} Leaf
* @typedef {{type: 'node', children: Array<Node | Leaf>}} Node
* @typedef {{type: 'root', children: Array<Node | Leaf>}} 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<Root>}
*/
function asIs(node) {
return node
function changeLeaf(node) {
return node.type === 'text' ? {...node, value: 'CHANGED'} : node
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@
"target": "es2020"
},
"exclude": ["coverage/", "node_modules/"],
"include": ["**/*.js", "lib/complex-types.d.ts"]
"include": ["**/*.js"]
}

0 comments on commit 66e15b8

Please sign in to comment.