Skip to content

Commit

Permalink
Add improved parents type
Browse files Browse the repository at this point in the history
Previously, a basic `Parent` from `@types/unist` was used, as an array, for the second parameter of a visitor (`parents`). This changes that to instead use an array of descendants in `tree` which implement the abstract `Parent` interface. This is not perfect, because several parents can’t be found in certain nodes practically, but it will at least help folks narrow.

Related to syntax-tree/unist-util-visit#30.
Closes GH-11.

Reviewed-by: Christian Murphy <christian.murphy.42@gmail.com>
  • Loading branch information
wooorm authored Sep 21, 2021
1 parent bd56c69 commit 40f2917
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 30 deletions.
61 changes: 61 additions & 0 deletions complex-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,43 @@
import type {Node, Parent} from 'unist'
import type {Test} from 'unist-util-is'

/**
* Union of the action types
*/
export type Action = boolean | 'skip'

/**
* 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 (or next siblings, in case of
* reverse) Results less than 0 or greater than or equal to `children.length`
* stop traversing the parent.
*/
export type Index = number

/**
* List with one or two values, the first an action, the second an index.
*/
export type ActionTuple = [
(Action | null | undefined | void)?,
(Index | null | undefined)?
]

/**
* Any value that can be returned from a visitor
*/
export type VisitorResult =
| null
| undefined
| Action
| Index
| ActionTuple
| void

/**
* Internal utility to collect all descendants of in `Tree`.
*/
export type InclusiveDescendant<
Tree extends Node = never,
Found = void
Expand Down Expand Up @@ -48,4 +85,28 @@ export type Matches<Value, Check> =
? MatchesOne<Value, Check[keyof Check]>
: MatchesOne<Value, Check>

/**
* Invoked 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, ancestors: Ancestor[]) => VisitorResult

export type BuildVisitor<
Tree extends Node = Node,
Check extends Test = string
> = Visitor<
Matches<InclusiveDescendant<Tree>, Check>,
Extract<InclusiveDescendant<Tree>, Parent>
>

/* eslint-enable @typescript-eslint/ban-types */
36 changes: 8 additions & 28 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,11 @@
* @typedef {import('unist').Node} Node
* @typedef {import('unist').Parent} Parent
* @typedef {import('unist-util-is').Test} Test
*/

/**
* @typedef {CONTINUE|SKIP|EXIT} 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 (or next siblings, in case of reverse) 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 {null|undefined|Action|Index|ActionTuple|void} VisitorResult Any value that can be returned from a visitor
*/

/**
* Invoked 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 {Array.<Parent>} ancestors Ancestors of node
* @returns {VisitorResult}
* @typedef {import('./complex-types').Action} Action
* @typedef {import('./complex-types').Index} Index
* @typedef {import('./complex-types').ActionTuple} ActionTuple
* @typedef {import('./complex-types').VisitorResult} VisitorResult
* @typedef {import('./complex-types').Visitor} Visitor
*/

import {convert} from 'unist-util-is'
Expand Down Expand Up @@ -56,15 +36,15 @@ export const EXIT = false
export const visitParents =
/**
* @type {(
* (<Tree extends Node, Check extends Test>(tree: Tree, test: Check, visitor: Visitor<import('./complex-types').Matches<import('./complex-types').InclusiveDescendant<Tree>, Check>>, reverse?: boolean) => void) &
* (<Tree extends Node>(tree: Tree, visitor: Visitor<import('./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<Node>} visitor
* @param {boolean} [reverse]
*/
function (tree, test, visitor, reverse) {
Expand Down
7 changes: 5 additions & 2 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,17 +93,20 @@ expectError(visitParents())
expectError(visitParents(sampleTree))

/* Visit without test. */
visitParents(sampleTree, (node) => {
visitParents(sampleTree, (node, parents) => {
expectType<Root | Content>(node)
expectType<Array<Root | Blockquote | Heading | Paragraph | Emphasis>>(parents)
})
visitParents(implicitTree, (node) => {
expectAssignable<Node>(node)
expectNotType<Node>(node) // Objects are too loose.
})

/* Visit with type test. */
visitParents(sampleTree, 'heading', (node) => {
visitParents(sampleTree, 'heading', (node, parents) => {
expectType<Heading>(node)
// Note that most of these can’t be a parent of `Heading`, but still.
expectType<Array<Root | Blockquote | Heading | Paragraph | Emphasis>>(parents)
})
visitParents(sampleTree, 'element', (node) => {
// Not in tree.
Expand Down

0 comments on commit 40f2917

Please sign in to comment.