diff --git a/lib/types.ts b/lib/types.ts index 65116fa7..396bd790 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -52,7 +52,7 @@ export type XastParent = XastRoot | XastElement; export type XastNode = XastRoot | XastChild; type VisitorNode = { - enter?: (node: Node, parentNode: XastParent) => void; + enter?: (node: Node, parentNode: XastParent) => void | symbol; exit?: (node: Node, parentNode: XastParent) => void; }; diff --git a/lib/xast.js b/lib/xast.js index 1796dcca..ffa5d4ec 100644 --- a/lib/xast.js +++ b/lib/xast.js @@ -75,6 +75,9 @@ const traverse = (node, fn) => { }; exports.traverse = traverse; +const visitSkip = Symbol(); +exports.visitSkip = visitSkip; + /** * @type {(node: XastNode, visitor: Visitor, parentNode?: any) => void} */ @@ -82,7 +85,10 @@ const visit = (node, visitor, parentNode) => { const callbacks = visitor[node.type]; if (callbacks && callbacks.enter) { // @ts-ignore hard to infer - callbacks.enter(node, parentNode); + const symbol = callbacks.enter(node, parentNode); + if (symbol === visitSkip) { + return; + } } // visit root children if (node.type === 'root') { diff --git a/lib/xast.test.js b/lib/xast.test.js index 22955262..18019aad 100644 --- a/lib/xast.test.js +++ b/lib/xast.test.js @@ -1,107 +1,122 @@ 'use strict'; -const { visit, detachNodeFromParent } = require('./xast.js'); +/** + * @typedef {import('./types').XastRoot} XastRoot + * @typedef {import('./types').XastElement} XastElement + */ -const getAst = () => { - const ast = { - type: 'root', - children: [ - { - type: 'element', - name: 'g', - attributes: {}, - children: [ - { - type: 'element', - name: 'rect', - attributes: {}, - children: [], - }, - { - type: 'element', - name: 'circle', - attributes: {}, - children: [], - }, - ], - }, - { - type: 'element', - name: 'ellipse', - attributes: {}, - children: [], - }, - ], - }; - ast.children[0].parentNode = ast; - ast.children[0].children[0].parentNode = ast.children[0]; - ast.children[0].children[1].parentNode = ast.children[0]; - ast.children[1].parentNode = ast; - return ast; +const { visit, visitSkip, detachNodeFromParent } = require('./xast.js'); + +/** + * @type {(children: Array) => XastRoot} + */ +const root = (children) => { + return { type: 'root', children }; }; -describe('xast', () => { - it('enter into nodes', () => { - const root = getAst(); - const entered = []; - visit(root, { - root: { - enter: (node) => { - entered.push(node.type); - }, - }, - element: { - enter: (node) => { - entered.push(`${node.type}:${node.name}`); - }, - }, - }); - expect(entered).toEqual([ - 'root', - 'element:g', - 'element:rect', - 'element:circle', - 'element:ellipse', - ]); - }); +/** + * @type {( + * name: string, + * attrs?: null | Record, + * children?: Array + * ) => XastElement} + */ +const x = (name, attrs = null, children = []) => { + return { type: 'element', name, attributes: attrs || {}, children }; +}; - it('exit from nodes', () => { - const root = getAst(); - const exited = []; - visit(root, { - root: { - exit: (node) => { - exited.push(node.type); - }, +test('visit enters into nodes', () => { + const ast = root([x('g', null, [x('rect'), x('circle')]), x('ellipse')]); + /** + * @type {Array} + */ + const entered = []; + visit(ast, { + root: { + enter: (node) => { + entered.push(node.type); }, - element: { - exit: (node) => { - exited.push(`${node.type}:${node.name}`); - }, + }, + element: { + enter: (node) => { + entered.push(`${node.type}:${node.name}`); }, - }); - expect(exited).toEqual([ - 'element:rect', - 'element:circle', - 'element:g', - 'element:ellipse', - 'root', - ]); - }); - - it('skip entering children if node is detached', () => { - const root = getAst(); - const entered = []; - visit(root, { - element: { - enter: (node, parentNode) => { - entered.push(node.name); - if (node.name === 'g') { - detachNodeFromParent(node, parentNode); - } - }, - }, - }); - expect(entered).toEqual(['g', 'ellipse']); + }, }); + expect(entered).toEqual([ + 'root', + 'element:g', + 'element:rect', + 'element:circle', + 'element:ellipse', + ]); +}); + +test('visit exits from nodes', () => { + const ast = root([x('g', null, [x('rect'), x('circle')]), x('ellipse')]); + /** + * @type {Array} + */ + const exited = []; + visit(ast, { + root: { + exit: (node) => { + exited.push(node.type); + }, + }, + element: { + exit: (node) => { + exited.push(`${node.type}:${node.name}`); + }, + }, + }); + expect(exited).toEqual([ + 'element:rect', + 'element:circle', + 'element:g', + 'element:ellipse', + 'root', + ]); +}); + +test('visit skips entering children if node is detached', () => { + const ast = root([x('g', null, [x('rect'), x('circle')]), x('ellipse')]); + /** + * @type {Array} + */ + const entered = []; + visit(ast, { + element: { + enter: (node, parentNode) => { + entered.push(node.name); + if (node.name === 'g') { + detachNodeFromParent(node, parentNode); + } + }, + }, + }); + expect(entered).toEqual(['g', 'ellipse']); + expect(ast).toEqual(root([x('ellipse')])); +}); + +test('visit skips entering children when symbol is passed', () => { + const ast = root([x('g', null, [x('rect'), x('circle')]), x('ellipse')]); + /** + * @type {Array} + */ + const entered = []; + visit(ast, { + element: { + enter: (node) => { + entered.push(node.name); + if (node.name === 'g') { + return visitSkip; + } + }, + }, + }); + expect(entered).toEqual(['g', 'ellipse']); + expect(ast).toEqual( + root([x('g', null, [x('rect'), x('circle')]), x('ellipse')]) + ); }); diff --git a/tsconfig.json b/tsconfig.json index 450d1273..e5779f22 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "resolveJsonModule": true, "noImplicitAny": true }, - "include": ["plugins/**/*"], + "include": ["plugins/**/*", "lib/xast.test.js"], "exclude": [ "plugins/_applyTransforms.js", "plugins/cleanupIDs.js",