diff --git a/lib/parser.js b/lib/parser.js index 997f3d32..0ee21aef 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -107,11 +107,6 @@ export const parseSvg = (data, from) => { * @type {(node: XastChild) => void} */ const pushToContent = (node) => { - // TODO remove legacy parentNode in v4 - Object.defineProperty(node, 'parentNode', { - writable: true, - value: current, - }); current.children.push(node); }; diff --git a/lib/style.js b/lib/style.js index a30b5628..2e064887 100644 --- a/lib/style.js +++ b/lib/style.js @@ -10,15 +10,16 @@ import { /** * @typedef {import('css-tree').Rule} CsstreeRule + * @typedef {import('./types.js').ComputedStyles} ComputedStyles * @typedef {import('./types.js').Specificity} Specificity * @typedef {import('./types.js').Stylesheet} Stylesheet - * @typedef {import('./types.js').StylesheetRule} StylesheetRule * @typedef {import('./types.js').StylesheetDeclaration} StylesheetDeclaration - * @typedef {import('./types.js').ComputedStyles} ComputedStyles - * @typedef {import('./types.js').XastRoot} XastRoot - * @typedef {import('./types.js').XastElement} XastElement - * @typedef {import('./types.js').XastParent} XastParent + * @typedef {import('./types.js').StylesheetRule} StylesheetRule * @typedef {import('./types.js').XastChild} XastChild + * @typedef {import('./types.js').XastElement} XastElement + * @typedef {import('./types.js').XastNode} XastNode + * @typedef {import('./types.js').XastParent} XastParent + * @typedef {import('./types.js').XastRoot} XastRoot */ const csstreeWalkSkip = csstree.walk.skip; @@ -130,9 +131,10 @@ const parseStyleDeclarations = (css) => { /** * @param {Stylesheet} stylesheet * @param {XastElement} node + * @param {Map=} parents * @returns {ComputedStyles} */ -const computeOwnStyle = (stylesheet, node) => { +const computeOwnStyle = (stylesheet, node, parents = undefined) => { /** @type {ComputedStyles} */ const computedStyle = {}; const importantStyles = new Map(); @@ -147,7 +149,7 @@ const computeOwnStyle = (stylesheet, node) => { // collect matching rules for (const { selector, declarations, dynamic } of stylesheet.rules) { - if (matches(node, selector)) { + if (matches(node, selector, parents)) { for (const { name, value, important } of declarations) { const computed = computedStyle[name]; if (computed && computed.type === 'dynamic') { @@ -259,10 +261,10 @@ export const collectStylesheet = (root) => { */ export const computeStyle = (stylesheet, node) => { const { parents } = stylesheet; - const computedStyles = computeOwnStyle(stylesheet, node); + const computedStyles = computeOwnStyle(stylesheet, node, parents); let parent = parents.get(node); while (parent != null && parent.type !== 'root') { - const inheritedStyles = computeOwnStyle(stylesheet, parent); + const inheritedStyles = computeOwnStyle(stylesheet, parent, parents); for (const [name, computed] of Object.entries(inheritedStyles)) { if ( computedStyles[name] == null && diff --git a/lib/style.test.js b/lib/style.test.js index f904ede2..3629f97d 100644 --- a/lib/style.test.js +++ b/lib/style.test.js @@ -11,9 +11,7 @@ import { parseSvg } from './parser.js'; * @type {(node: XastParent, id: string) => XastElement} */ const getElementById = (node, id) => { - /** - * @type {?XastElement} - */ + /** @type {?XastElement} */ let matched = null; visit(node, { element: { diff --git a/lib/svgo-node.js b/lib/svgo-node.js index 6769f771..efe67ab9 100644 --- a/lib/svgo-node.js +++ b/lib/svgo-node.js @@ -5,6 +5,7 @@ import { VERSION, optimize as optimizeAgnostic, builtinPlugins, + mapNodesToParents, querySelector, querySelectorAll, _collections, @@ -113,6 +114,7 @@ export default { builtinPlugins, loadConfig, optimize, + mapNodesToParents, querySelector, querySelectorAll, _collections, diff --git a/lib/svgo.d.ts b/lib/svgo.d.ts index 76395f81..869511ae 100644 --- a/lib/svgo.d.ts +++ b/lib/svgo.d.ts @@ -162,4 +162,4 @@ export declare const VERSION: string; /** The core of SVGO */ export declare function optimize(input: string, config?: Config): Output; -export { querySelector, querySelectorAll } from './xast.js'; +export { mapNodesToParents, querySelector, querySelectorAll } from './xast.js'; diff --git a/lib/svgo.js b/lib/svgo.js index 529fe404..b854e778 100644 --- a/lib/svgo.js +++ b/lib/svgo.js @@ -4,7 +4,7 @@ import { builtin } from './builtin.js'; import { invokePlugins } from './svgo/plugins.js'; import { encodeSVGDatauri } from './svgo/tools.js'; import { VERSION } from './version.js'; -import { querySelector, querySelectorAll } from './xast.js'; +import { mapNodesToParents, querySelector, querySelectorAll } from './xast.js'; import _collections from '../plugins/_collections.js'; /** @@ -78,6 +78,7 @@ const resolvePluginConfig = (plugin) => { export { VERSION, builtin as builtinPlugins, + mapNodesToParents, querySelector, querySelectorAll, _collections, @@ -147,6 +148,7 @@ export default { VERSION, optimize, builtinPlugins: builtin, + mapNodesToParents, querySelector, querySelectorAll, _collections, diff --git a/lib/svgo/css-select-adapter.js b/lib/svgo/css-select-adapter.js index d9e73059..fb15c57d 100644 --- a/lib/svgo/css-select-adapter.js +++ b/lib/svgo/css-select-adapter.js @@ -1,5 +1,5 @@ /** - * @typedef {Required>['adapter']} Adapter + * @typedef {Required>['adapter']} Adapter * @typedef {import('../types.js').XastChild} XastChild * @typedef {import('../types.js').XastElement} XastElement * @typedef {import('../types.js').XastNode} XastNode @@ -33,20 +33,6 @@ const getName = (elemAst) => { return elemAst.name; }; -/** @type {Adapter['getParent']} */ -const getParent = (node) => { - return node.parentNode || null; -}; - -/** - * @param {any} elem - * @returns {any} - */ -const getSiblings = (elem) => { - const parent = getParent(elem); - return parent ? getChildren(parent) : []; -}; - /** @type {Adapter['getText']} */ const getText = (node) => { if (node.children[0].type === 'text' || node.children[0].type === 'cdata') { @@ -60,38 +46,6 @@ const hasAttrib = (elem, name) => { return elem.attributes[name] !== undefined; }; -/** - * @param {any} nodes - * @returns {any} - */ -const removeSubsets = (nodes) => { - let idx = nodes.length; - let node; - let ancestor; - let replace; - // Check if each node (or one of its ancestors) is already contained in the - // array. - while (--idx > -1) { - node = ancestor = nodes[idx]; - // Temporarily remove the node under consideration - nodes[idx] = null; - replace = true; - while (ancestor) { - if (nodes.includes(ancestor)) { - replace = false; - nodes.splice(idx, 1); - break; - } - ancestor = getParent(ancestor); - } - // If the node has been found to be unique, re-insert it. - if (replace) { - nodes[idx] = node; - } - } - return nodes; -}; - /** @type {Adapter['findAll']} */ const findAll = (test, elems) => { const result = []; @@ -123,19 +77,68 @@ const findOne = (test, elems) => { }; /** - * @type {Adapter} + * @param {Map} parents + * @returns {Adapter} */ -export default { - isTag, - existsOne, - getAttributeValue, - getChildren, - getName, - getParent, - getSiblings, - getText, - hasAttrib, - removeSubsets, - findAll, - findOne, -}; +export function createAdapter(parents) { + /** @type {Adapter['getParent']} */ + const getParent = (node) => { + return parents.get(node) || null; + }; + + /** + * @param {any} elem + * @returns {any} + */ + const getSiblings = (elem) => { + const parent = getParent(elem); + return parent ? getChildren(parent) : []; + }; + + /** + * @param {any} nodes + * @returns {any} + */ + const removeSubsets = (nodes) => { + let idx = nodes.length; + let node; + let ancestor; + let replace; + // Check if each node (or one of its ancestors) is already contained in the + // array. + while (--idx > -1) { + node = ancestor = nodes[idx]; + // Temporarily remove the node under consideration + nodes[idx] = null; + replace = true; + while (ancestor) { + if (nodes.includes(ancestor)) { + replace = false; + nodes.splice(idx, 1); + break; + } + ancestor = getParent(ancestor); + } + // If the node has been found to be unique, re-insert it. + if (replace) { + nodes[idx] = node; + } + } + return nodes; + }; + + return { + isTag, + existsOne, + getAttributeValue, + getChildren, + getName, + getParent, + getSiblings, + getText, + hasAttrib, + removeSubsets, + findAll, + findOne, + }; +} diff --git a/lib/xast.js b/lib/xast.js index 5f8b4c79..213da728 100644 --- a/lib/xast.js +++ b/lib/xast.js @@ -1,8 +1,8 @@ import { selectAll, selectOne, is } from 'css-select'; -import adapter from './svgo/css-select-adapter.js'; +import { createAdapter } from './svgo/css-select-adapter.js'; /** - * @typedef {import('css-select').Options} Options + * @typedef {import('css-select').Options} Options * @typedef {import('./types.js').XastElement} XastElement * @typedef {import('./types.js').XastNode} XastNode * @typedef {import('./types.js').XastChild} XastChild @@ -10,37 +10,81 @@ import adapter from './svgo/css-select-adapter.js'; * @typedef {import('./types.js').Visitor} Visitor */ -/** @type {Options} */ -const cssSelectOptions = { - xmlMode: true, - adapter, -}; +/** + * @param {Map} parents + * @returns {Options} + */ +function createCssSelectOptions(parents) { + return { + xmlMode: true, + adapter: createAdapter(parents), + }; +} /** - * @param {XastNode} node Element to query the children of. + * Maps all nodes to their parent node recursively. + * + * @param {XastParent} node + * @returns {Map} + */ +export function mapNodesToParents(node) { + /** @type {Map} */ + const parents = new Map(); + + for (const child of node.children) { + parents.set(child, node); + visit( + child, + { + element: { + enter: (child, parent) => { + parents.set(child, parent); + }, + }, + }, + node, + ); + } + + return parents; +} + +/** + * @param {XastParent} node Element to query the children of. * @param {string} selector CSS selector string. + * @param {Map=} parents * @returns {XastChild[]} All matching elements. */ -export const querySelectorAll = (node, selector) => { - return selectAll(selector, node, cssSelectOptions); +export const querySelectorAll = ( + node, + selector, + parents = mapNodesToParents(node), +) => { + return selectAll(selector, node, createCssSelectOptions(parents)); }; /** - * @param {XastNode} node Element to query the children of. + * @param {XastParent} node Element to query the children of. * @param {string} selector CSS selector string. + * @param {Map=} parents * @returns {?XastChild} First match, or null if there was no match. */ -export const querySelector = (node, selector) => { - return selectOne(selector, node, cssSelectOptions); +export const querySelector = ( + node, + selector, + parents = mapNodesToParents(node), +) => { + return selectOne(selector, node, createCssSelectOptions(parents)); }; /** * @param {XastElement} node * @param {string} selector + * @param {Map=} parents * @returns {boolean} */ -export const matches = (node, selector) => { - return is(node, selector, cssSelectOptions); +export const matches = (node, selector, parents = mapNodesToParents(node)) => { + return is(node, selector, createCssSelectOptions(parents)); }; export const visitSkip = Symbol(); @@ -48,7 +92,7 @@ export const visitSkip = Symbol(); /** * @param {XastNode} node * @param {Visitor} visitor - * @param {?any} parentNode + * @param {any=} parentNode */ export const visit = (node, visitor, parentNode = undefined) => { const callbacks = visitor[node.type]; diff --git a/plugins/collapseGroups.js b/plugins/collapseGroups.js index fd3fc12d..bacf78b4 100644 --- a/plugins/collapseGroups.js +++ b/plugins/collapseGroups.js @@ -129,13 +129,6 @@ export const fn = (root) => { // replace current node with all its children const index = parentNode.children.indexOf(node); parentNode.children.splice(index, 1, ...node.children); - // TODO remove legacy parentNode in v4 - for (const child of node.children) { - Object.defineProperty(child, 'parentNode', { - writable: true, - value: parentNode, - }); - } } }, }, diff --git a/plugins/mergeStyles.js b/plugins/mergeStyles.js index 2b59f3d2..5f7e597b 100644 --- a/plugins/mergeStyles.js +++ b/plugins/mergeStyles.js @@ -83,11 +83,6 @@ export const fn = () => { * @type {XastChild} */ const child = { type: styleContentType, value: collectedStyles }; - // TODO remove legacy parentNode in v4 - Object.defineProperty(child, 'parentNode', { - writable: true, - value: firstStyleElement, - }); firstStyleElement.children = [child]; } }, diff --git a/plugins/removeScripts.js b/plugins/removeScripts.js index 6ad972c9..886daf43 100644 --- a/plugins/removeScripts.js +++ b/plugins/removeScripts.js @@ -55,14 +55,6 @@ export const fn = () => { (child) => child.type !== 'text', ); parentNode.children.splice(index, 1, ...usefulChildren); - - // TODO remove legacy parentNode in v4 - for (const child of node.children) { - Object.defineProperty(child, 'parentNode', { - writable: true, - value: parentNode, - }); - } } } }, diff --git a/plugins/removeUselessDefs.js b/plugins/removeUselessDefs.js index 4ae74d12..277e3069 100644 --- a/plugins/removeUselessDefs.js +++ b/plugins/removeUselessDefs.js @@ -32,13 +32,6 @@ export const fn = () => { if (usefulNodes.length === 0) { detachNodeFromParent(node, parentNode); } - // TODO remove legacy parentNode in v4 - for (const usefulNode of usefulNodes) { - Object.defineProperty(usefulNode, 'parentNode', { - writable: true, - value: node, - }); - } node.children = usefulNodes; } }, diff --git a/plugins/reusePaths.js b/plugins/reusePaths.js index 2e5dd958..fdf53fcc 100644 --- a/plugins/reusePaths.js +++ b/plugins/reusePaths.js @@ -92,11 +92,6 @@ export const fn = (root) => { attributes: {}, children: [], }; - // TODO remove legacy parentNode in v4 - Object.defineProperty(defsTag, 'parentNode', { - writable: true, - value: node, - }); } let index = 0; @@ -129,11 +124,6 @@ export const fn = (root) => { reusablePath.attributes.id = originalId; delete list[0].attributes.id; } - // TODO remove legacy parentNode in v4 - Object.defineProperty(reusablePath, 'parentNode', { - writable: true, - value: defsTag, - }); defsTag.children.push(reusablePath); // convert paths to for (const pathNode of list) {