Skip to content

Commit

Permalink
Add improved parent type
Browse files Browse the repository at this point in the history
Previously, a basic `Parent` from `@types/unist` was used for the third parameter of a visitor (`parent`). This changes that to instead use an array of descendants in `tree` which implement the abstract `Parent` interface and can have `node` as a child.

Closes GH-30.
Closes GH-31.
Related-to: syntax-tree/unist-util-visit-parents#11.
  • Loading branch information
wooorm committed Sep 23, 2021
1 parent 6ca3b63 commit a76200b
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 32 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.DS_Store
*.d.ts
index.d.ts
test.d.ts
*.log
coverage/
node_modules/
Expand Down
54 changes: 54 additions & 0 deletions complex-types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type {Node, Parent} from 'unist'
import type {Test} from 'unist-util-is'
import type {
VisitorResult,
Matches,
InclusiveDescendant
} from 'unist-util-visit-parents/complex-types'

/**
* Called when a node (matching test, if given) is found.
* 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 visited.
* If adding or removing previous siblings (or next siblings, in case of reverse) of node,
* visitor should return a new index (number) 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.
*/
export type Visitor<
Visited extends Node = Node,
Ancestor extends Parent = Parent
> = (
node: Visited,
index: Visited extends Node ? number | null : never,
parent: Ancestor extends Node ? Ancestor | null : Ancestor
) => VisitorResult

type ParentsOf<
Ancestor extends Node,
Child extends Node
> = Ancestor extends Parent
? Child extends Ancestor['children'][number]
? Ancestor
: never
: never

type BuildVisitorFromMatch<
Visited extends Node,
Ancestor extends Parent
> = Visitor<Visited, ParentsOf<Ancestor, Visited>>

type BuildVisitorFromDescendants<
Descendant extends Node,
Check extends Test
> = BuildVisitorFromMatch<
Matches<Descendant, Check>,
Extract<Descendant, Parent>
>

export type BuildVisitor<
Tree extends Node = Node,
Check extends Test = string
> = BuildVisitorFromDescendants<InclusiveDescendant<Tree>, Check>
26 changes: 4 additions & 22 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,7 @@
* @typedef {import('unist').Parent} Parent
* @typedef {import('unist-util-is').Test} Test
* @typedef {import('unist-util-visit-parents').VisitorResult} VisitorResult
*/

/**
* Called when a node (matching test, if given) is found.
* 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 visited.
* If adding or removing previous siblings (or next siblings, in case of reverse) of node,
* visitor should return a new index (number) 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.
*
* @template {Node} V
* @callback Visitor
* @param {V} node Found node
* @param {number|null} index Position of `node` in `parent`
* @param {Parent|null} parent Parent of `node`
* @returns {VisitorResult}
* @typedef {import('./complex-types').Visitor} Visitor
*/

import {visitParents, CONTINUE, SKIP, EXIT} from 'unist-util-visit-parents'
Expand All @@ -39,15 +21,15 @@ export {CONTINUE, SKIP, EXIT}
export const visit =
/**
* @type {(
* (<Tree extends Node, Check extends Test>(tree: Tree, test: Check, visitor: Visitor<import('unist-util-visit-parents/complex-types').Matches<import('unist-util-visit-parents/complex-types').InclusiveDescendant<Tree>, Check>>, reverse?: boolean) => void) &
* (<Tree extends Node>(tree: Tree, visitor: Visitor<import('unist-util-visit-parents/complex-types').InclusiveDescendant<Tree>>, reverse?: boolean) => void)
* (<Tree extends Node, Check extends Test>(tree: Tree, test: Check, visitor: import('./complex-types').BuildVisitor<Tree, Check>, reverse?: boolean) => void) &
* (<Tree extends Node>(tree: Tree, visitor: import('./complex-types').BuildVisitor<Tree>, reverse?: boolean) => void)
* )}
*/
(
/**
* @param {Node} tree
* @param {Test} test
* @param {Visitor<Node>} visitor
* @param {import('./complex-types').Visitor} visitor
* @param {boolean} [reverse]
*/
function (tree, test, visitor, reverse) {
Expand Down
23 changes: 16 additions & 7 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,21 @@ expectError(visit())
expectError(visit(sampleTree))

/* Visit without test. */
visit(sampleTree, (node) => {
visit(sampleTree, (node, _, parent) => {
expectType<Root | Content>(node)
expectType<Extract<Root | Content, Parent> | null>(parent)
})

/* Visit with type test. */
visit(sampleTree, 'heading', (node) => {
visit(sampleTree, 'heading', (node, _, parent) => {
expectType<Heading>(node)
expectType<Root | Blockquote | null>(parent)
})
visit(sampleTree, 'element', (node) => {
visit(sampleTree, 'element', (node, index, parent) => {
// Not in tree.
expectType<never>(node)
expectType<never>(index)
expectType<never>(parent)
})
expectError(visit(sampleTree, 'heading', (_: Element) => {}))

Expand Down Expand Up @@ -157,29 +161,34 @@ expectError(visit(sampleTree, () => [1]))
expectError(visit(sampleTree, () => ['random', 1]))

/* Should infer children from the given tree. */
visit(complexTree, (node) => {
visit(complexTree, (node, _, parent) => {
expectType<Root | Content>(node)
expectType<Extract<Root | Content, Parent> | null>(parent)
})

const blockquote = complexTree.children[0]
if (is<Blockquote>(blockquote, 'blockquote')) {
visit(blockquote, (node) => {
visit(blockquote, (node, _, parent) => {
expectType<Content>(node)
expectType<Extract<Content, Parent> | null>(parent)
})
}

const paragraph = complexTree.children[1]
if (is<Paragraph>(paragraph, 'paragraph')) {
visit(paragraph, (node) => {
visit(paragraph, (node, _, parent) => {
expectType<Paragraph | Phrasing>(node)
expectType<Paragraph | Emphasis | null>(parent)
})

const child = paragraph.children[1]

if (is<Emphasis>(child, 'emphasis')) {
visit(child, 'blockquote', (node) => {
visit(child, 'blockquote', (node, index, parent) => {
// `blockquote` does not exist in phrasing.
expectType<never>(node)
expectType<never>(index)
expectType<never>(parent)
})
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"main": "index.js",
"types": "index.d.ts",
"files": [
"complex-types.d.ts",
"index.d.ts",
"index.js"
],
Expand All @@ -68,7 +69,7 @@
},
"scripts": {
"prepack": "npm run build && npm run format",
"build": "rimraf \"*.d.ts\" && tsc && tsd && type-coverage",
"build": "rimraf \"{index,test}.d.ts\" && tsc && tsd && type-coverage",
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix",
"test-api": "node test.js",
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node test.js",
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"include": ["*.js"],
"include": ["index.js", "test.js"],
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020"],
Expand Down

0 comments on commit a76200b

Please sign in to comment.