mirror of
https://github.com/svg/svgo.git
synced 2025-08-09 02:22:08 +03:00
Add visitSkip symbol (#1547)
This should help to avoid node.parentNode and closestByName in some cases by skiping visiting children of current node. Works only in `enter` listener.
This commit is contained in:
@@ -52,7 +52,7 @@ export type XastParent = XastRoot | XastElement;
|
||||
export type XastNode = XastRoot | XastChild;
|
||||
|
||||
type VisitorNode<Node> = {
|
||||
enter?: (node: Node, parentNode: XastParent) => void;
|
||||
enter?: (node: Node, parentNode: XastParent) => void | symbol;
|
||||
exit?: (node: Node, parentNode: XastParent) => void;
|
||||
};
|
||||
|
||||
|
@@ -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') {
|
||||
|
209
lib/xast.test.js
209
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<XastElement>) => 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<string, string>,
|
||||
* children?: Array<XastElement>
|
||||
* ) => 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<string>}
|
||||
*/
|
||||
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<string>}
|
||||
*/
|
||||
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<string>}
|
||||
*/
|
||||
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<string>}
|
||||
*/
|
||||
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')])
|
||||
);
|
||||
});
|
||||
|
@@ -10,7 +10,7 @@
|
||||
"resolveJsonModule": true,
|
||||
"noImplicitAny": true
|
||||
},
|
||||
"include": ["plugins/**/*"],
|
||||
"include": ["plugins/**/*", "lib/xast.test.js"],
|
||||
"exclude": [
|
||||
"plugins/_applyTransforms.js",
|
||||
"plugins/cleanupIDs.js",
|
||||
|
Reference in New Issue
Block a user