diff --git a/.gitignore b/.gitignore index 39233a5..242286d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .DS_Store -color.browser.d.ts -color.d.ts -index.d.ts +lib/color.browser.d.ts +lib/color.d.ts +lib/index.d.ts test.d.ts *.log coverage/ diff --git a/complex-types.d.ts b/complex-types.d.ts index 9e8275e..b9cae93 100644 --- a/complex-types.d.ts +++ b/complex-types.d.ts @@ -1,112 +1,2 @@ -/* eslint-disable @typescript-eslint/ban-types */ - -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 -> = Tree extends Parent - ? - | Tree - | InclusiveDescendant< - Exclude, - Found | Tree - > - : Tree - -type Predicate = Fn extends ( - value: any -) => value is infer Thing - ? Thing - : Fallback - -type MatchesOne = - // Is this a node? - Value extends Node - ? // No test. - Check extends null - ? Value - : // No test. - Check extends undefined - ? Value - : // Function test. - Check extends Function - ? Extract> - : // String (type) test. - Value['type'] extends Check - ? Value - : // Partial test. - Value extends Check - ? Value - : never - : never - -export type Matches = - // Is this a list? - Check extends Array - ? MatchesOne - : MatchesOne - -/** - * 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, ancestors: Array) => VisitorResult - -export type BuildVisitor< - Tree extends Node = Node, - Check extends Test = string -> = Visitor< - Matches, Check>, - Extract, Parent> -> - -/* eslint-enable @typescript-eslint/ban-types */ +// To do: next major: remove this file. +export type {Visitor, BuildVisitor} from './lib/complex-types.js' diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..0f56ed8 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,10 @@ +export type {Test} from 'unist-util-is' +export type { + Action, + ActionTuple, + Index, + VisitorResult, + Visitor, + BuildVisitor +} from './lib/complex-types.js' +export {CONTINUE, EXIT, SKIP, visitParents} from './lib/index.js' diff --git a/index.js b/index.js index 427c213..995e97b 100644 --- a/index.js +++ b/index.js @@ -1,156 +1,2 @@ -/** - * @typedef {import('unist').Node} Node - * @typedef {import('unist').Parent} Parent - * @typedef {import('unist-util-is').Test} Test - * @typedef {import('./complex-types.js').Action} Action - * @typedef {import('./complex-types.js').Index} Index - * @typedef {import('./complex-types.js').ActionTuple} ActionTuple - * @typedef {import('./complex-types.js').VisitorResult} VisitorResult - * @typedef {import('./complex-types.js').Visitor} Visitor - */ - -import {convert} from 'unist-util-is' -import {color} from './color.js' - -/** - * Continue traversing as normal - */ -export const CONTINUE = true -/** - * Do not traverse this node’s children - */ -export const SKIP = 'skip' -/** - * Stop traversing immediately - */ -export const EXIT = false - -/** - * Visit children of tree which pass test. - * - * @param tree - * Tree to walk - * @param [test] - * `unist-util-is`-compatible test - * @param visitor - * Function called for nodes that pass `test`. - * @param [reverse=false] - * Traverse in reverse preorder (NRL) instead of preorder (NLR) (default). - */ -export const visitParents = - /** - * @type {( - * ((tree: Tree, test: Check, visitor: import('./complex-types.js').BuildVisitor, reverse?: boolean) => void) & - * ((tree: Tree, visitor: import('./complex-types.js').BuildVisitor, reverse?: boolean) => void) - * )} - */ - ( - /** - * @param {Node} tree - * @param {Test} test - * @param {import('./complex-types.js').Visitor} visitor - * @param {boolean} [reverse=false] - */ - 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, null, [])() - - /** - * @param {Node} node - * @param {number?} index - * @param {Array} parents - */ - function factory(node, index, parents) { - /** @type {Record} */ - // @ts-expect-error: hush - const value = typeof node === 'object' && node !== null ? node : {} - /** @type {string|undefined} */ - let name - - if (typeof value.type === 'string') { - name = - typeof value.tagName === 'string' - ? value.tagName - : typeof value.name === 'string' - ? value.name - : undefined - - Object.defineProperty(visit, 'name', { - value: - 'node (' + - color(value.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 - } - } - } - ) - -/** - * @param {VisitorResult} value - * @returns {ActionTuple} - */ -function toResult(value) { - if (Array.isArray(value)) { - return value - } - - if (typeof value === 'number') { - return [CONTINUE, value] - } - - return [value] -} +// Note: types exported from `index.d.ts` +export {CONTINUE, EXIT, SKIP, visitParents} from './lib/index.js' diff --git a/color.browser.js b/lib/color.browser.js similarity index 100% rename from color.browser.js rename to lib/color.browser.js diff --git a/color.js b/lib/color.js similarity index 100% rename from color.js rename to lib/color.js diff --git a/lib/complex-types.d.ts b/lib/complex-types.d.ts new file mode 100644 index 0000000..9e8275e --- /dev/null +++ b/lib/complex-types.d.ts @@ -0,0 +1,112 @@ +/* eslint-disable @typescript-eslint/ban-types */ + +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 +> = Tree extends Parent + ? + | Tree + | InclusiveDescendant< + Exclude, + Found | Tree + > + : Tree + +type Predicate = Fn extends ( + value: any +) => value is infer Thing + ? Thing + : Fallback + +type MatchesOne = + // Is this a node? + Value extends Node + ? // No test. + Check extends null + ? Value + : // No test. + Check extends undefined + ? Value + : // Function test. + Check extends Function + ? Extract> + : // String (type) test. + Value['type'] extends Check + ? Value + : // Partial test. + Value extends Check + ? Value + : never + : never + +export type Matches = + // Is this a list? + Check extends Array + ? MatchesOne + : MatchesOne + +/** + * 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, ancestors: Array) => VisitorResult + +export type BuildVisitor< + Tree extends Node = Node, + Check extends Test = string +> = Visitor< + Matches, Check>, + Extract, Parent> +> + +/* eslint-enable @typescript-eslint/ban-types */ diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..fc0e1e3 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,153 @@ +/** + * @typedef {import('unist').Node} Node + * @typedef {import('unist').Parent} Parent + * @typedef {import('unist-util-is').Test} Test + * @typedef {import('./complex-types.js').ActionTuple} ActionTuple + * @typedef {import('./complex-types.js').VisitorResult} VisitorResult + */ + +import {convert} from 'unist-util-is' +import {color} from './color.js' + +/** + * Continue traversing as normal + */ +export const CONTINUE = true +/** + * Do not traverse this node’s children + */ +export const SKIP = 'skip' +/** + * Stop traversing immediately + */ +export const EXIT = false + +/** + * Visit children of tree which pass test. + * + * @param tree + * Tree to walk + * @param [test] + * `unist-util-is`-compatible test + * @param visitor + * Function called for nodes that pass `test`. + * @param [reverse=false] + * Traverse in reverse preorder (NRL) instead of preorder (NLR) (default). + */ +export const visitParents = + /** + * @type {( + * ((tree: Tree, test: Check, visitor: import('./complex-types.js').BuildVisitor, reverse?: boolean) => void) & + * ((tree: Tree, visitor: import('./complex-types.js').BuildVisitor, reverse?: boolean) => void) + * )} + */ + ( + /** + * @param {Node} tree + * @param {Test} test + * @param {import('./complex-types.js').Visitor} visitor + * @param {boolean} [reverse=false] + */ + 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, null, [])() + + /** + * @param {Node} node + * @param {number?} index + * @param {Array} parents + */ + function factory(node, index, parents) { + /** @type {Record} */ + // @ts-expect-error: hush + const value = typeof node === 'object' && node !== null ? node : {} + /** @type {string|undefined} */ + let name + + if (typeof value.type === 'string') { + name = + typeof value.tagName === 'string' + ? value.tagName + : typeof value.name === 'string' + ? value.name + : undefined + + Object.defineProperty(visit, 'name', { + value: + 'node (' + + color(value.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 + } + } + } + ) + +/** + * @param {VisitorResult} value + * @returns {ActionTuple} + */ +function toResult(value) { + if (Array.isArray(value)) { + return value + } + + if (typeof value === 'number') { + return [CONTINUE, value] + } + + return [value] +} diff --git a/package.json b/package.json index f4cd075..e7a65d3 100644 --- a/package.json +++ b/package.json @@ -31,17 +31,14 @@ "type": "module", "main": "index.js", "browser": { - "./color.js": "./color.browser.js" + "./lib/color.js": "./lib/color.browser.js" }, "react-native": { - "./color.js": "./color.browser.js" + "./lib/color.js": "./lib/color.browser.js" }, "types": "index.d.ts", "files": [ - "color.d.ts", - "color.js", - "color.browser.d.ts", - "color.browser.js", + "lib/", "complex-types.d.ts", "index.d.ts", "index.js" @@ -102,7 +99,7 @@ "ignoreCatch": true, "#": "needed `any`s", "ignoreFiles": [ - "complex-types.d.ts" + "lib/complex-types.d.ts" ] } } diff --git a/tsconfig.json b/tsconfig.json index da782de..b7fbe6a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,10 @@ { - "include": ["**/*.js", "complex-types.d.ts"], + "include": [ + "**/*.js", + "lib/complex-types.d.ts", + "complex-types.d.ts", + "index.d.ts" + ], "exclude": ["coverage/", "node_modules/"], "compilerOptions": { "checkJs": true,