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;
|
export type XastNode = XastRoot | XastChild;
|
||||||
|
|
||||||
type VisitorNode<Node> = {
|
type VisitorNode<Node> = {
|
||||||
enter?: (node: Node, parentNode: XastParent) => void;
|
enter?: (node: Node, parentNode: XastParent) => void | symbol;
|
||||||
exit?: (node: Node, parentNode: XastParent) => void;
|
exit?: (node: Node, parentNode: XastParent) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -75,6 +75,9 @@ const traverse = (node, fn) => {
|
|||||||
};
|
};
|
||||||
exports.traverse = traverse;
|
exports.traverse = traverse;
|
||||||
|
|
||||||
|
const visitSkip = Symbol();
|
||||||
|
exports.visitSkip = visitSkip;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {(node: XastNode, visitor: Visitor, parentNode?: any) => void}
|
* @type {(node: XastNode, visitor: Visitor, parentNode?: any) => void}
|
||||||
*/
|
*/
|
||||||
@@ -82,7 +85,10 @@ const visit = (node, visitor, parentNode) => {
|
|||||||
const callbacks = visitor[node.type];
|
const callbacks = visitor[node.type];
|
||||||
if (callbacks && callbacks.enter) {
|
if (callbacks && callbacks.enter) {
|
||||||
// @ts-ignore hard to infer
|
// @ts-ignore hard to infer
|
||||||
callbacks.enter(node, parentNode);
|
const symbol = callbacks.enter(node, parentNode);
|
||||||
|
if (symbol === visitSkip) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// visit root children
|
// visit root children
|
||||||
if (node.type === 'root') {
|
if (node.type === 'root') {
|
||||||
|
209
lib/xast.test.js
209
lib/xast.test.js
@@ -1,107 +1,122 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { visit, detachNodeFromParent } = require('./xast.js');
|
/**
|
||||||
|
* @typedef {import('./types').XastRoot} XastRoot
|
||||||
|
* @typedef {import('./types').XastElement} XastElement
|
||||||
|
*/
|
||||||
|
|
||||||
const getAst = () => {
|
const { visit, visitSkip, detachNodeFromParent } = require('./xast.js');
|
||||||
const ast = {
|
|
||||||
type: 'root',
|
/**
|
||||||
children: [
|
* @type {(children: Array<XastElement>) => XastRoot}
|
||||||
{
|
*/
|
||||||
type: 'element',
|
const root = (children) => {
|
||||||
name: 'g',
|
return { type: 'root', children };
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('xast', () => {
|
/**
|
||||||
it('enter into nodes', () => {
|
* @type {(
|
||||||
const root = getAst();
|
* name: string,
|
||||||
const entered = [];
|
* attrs?: null | Record<string, string>,
|
||||||
visit(root, {
|
* children?: Array<XastElement>
|
||||||
root: {
|
* ) => XastElement}
|
||||||
enter: (node) => {
|
*/
|
||||||
entered.push(node.type);
|
const x = (name, attrs = null, children = []) => {
|
||||||
},
|
return { type: 'element', name, attributes: attrs || {}, children };
|
||||||
},
|
};
|
||||||
element: {
|
|
||||||
enter: (node) => {
|
|
||||||
entered.push(`${node.type}:${node.name}`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(entered).toEqual([
|
|
||||||
'root',
|
|
||||||
'element:g',
|
|
||||||
'element:rect',
|
|
||||||
'element:circle',
|
|
||||||
'element:ellipse',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('exit from nodes', () => {
|
test('visit enters into nodes', () => {
|
||||||
const root = getAst();
|
const ast = root([x('g', null, [x('rect'), x('circle')]), x('ellipse')]);
|
||||||
const exited = [];
|
/**
|
||||||
visit(root, {
|
* @type {Array<string>}
|
||||||
root: {
|
*/
|
||||||
exit: (node) => {
|
const entered = [];
|
||||||
exited.push(node.type);
|
visit(ast, {
|
||||||
},
|
root: {
|
||||||
|
enter: (node) => {
|
||||||
|
entered.push(node.type);
|
||||||
},
|
},
|
||||||
element: {
|
},
|
||||||
exit: (node) => {
|
element: {
|
||||||
exited.push(`${node.type}:${node.name}`);
|
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,
|
"resolveJsonModule": true,
|
||||||
"noImplicitAny": true
|
"noImplicitAny": true
|
||||||
},
|
},
|
||||||
"include": ["plugins/**/*"],
|
"include": ["plugins/**/*", "lib/xast.test.js"],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"plugins/_applyTransforms.js",
|
"plugins/_applyTransforms.js",
|
||||||
"plugins/cleanupIDs.js",
|
"plugins/cleanupIDs.js",
|
||||||
|
Reference in New Issue
Block a user