diff --git a/plugins/reusePaths.js b/plugins/reusePaths.js index 4d91d6f9d..d1cb53b14 100644 --- a/plugins/reusePaths.js +++ b/plugins/reusePaths.js @@ -1,5 +1,7 @@ 'use strict'; +const { detachNodeFromParent, querySelectorAll } = require('../lib/xast'); + /** * @typedef {import('../lib/types').XastElement} XastElement * @typedef {import('../lib/types').XastParent} XastParent @@ -26,9 +28,18 @@ exports.fn = () => { */ const paths = new Map(); + /** + * Reference to the first defs element that is a direct child of the svg + * element if one exists. + * + * @type {XastElement} + * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs + */ + let svgDefs; + return { element: { - enter: (node) => { + enter: (node, parentNode) => { if (node.name === 'path' && node.attributes.d != null) { const d = node.attributes.d; const fill = node.attributes.fill || ''; @@ -41,6 +52,15 @@ exports.fn = () => { } list.push(node); } + + if ( + svgDefs == null && + node.name === 'defs' && + parentNode.type === 'element' && + parentNode.name === 'svg' + ) { + svgDefs = node; + } }, exit: (node, parentNode) => { @@ -48,17 +68,22 @@ exports.fn = () => { /** * @type {XastElement} */ - const defsTag = { - type: 'element', - name: 'defs', - attributes: {}, - children: [], - }; - // TODO remove legacy parentNode in v4 - Object.defineProperty(defsTag, 'parentNode', { - writable: true, - value: node, - }); + let defsTag = svgDefs; + + if (defsTag == null) { + defsTag = { + type: 'element', + name: 'defs', + attributes: {}, + children: [], + }; + // TODO remove legacy parentNode in v4 + Object.defineProperty(defsTag, 'parentNode', { + writable: true, + value: node, + }); + } + let index = 0; for (const list of paths.values()) { if (list.length > 1) { @@ -90,11 +115,41 @@ exports.fn = () => { defsTag.children.push(reusablePath); // convert paths to for (const pathNode of list) { - pathNode.name = 'use'; - pathNode.attributes['xlink:href'] = '#' + id; delete pathNode.attributes.d; delete pathNode.attributes.stroke; delete pathNode.attributes.fill; + + if ( + defsTag.children.includes(pathNode) && + pathNode.children.length === 0 + ) { + if (Object.keys(pathNode.attributes).length === 0) { + detachNodeFromParent(pathNode, defsTag); + continue; + } + + if ( + Object.keys(pathNode.attributes).length === 1 && + pathNode.attributes.id != null + ) { + detachNodeFromParent(pathNode, defsTag); + const selector = `[xlink\\:href=#${pathNode.attributes.id}], [href=#${pathNode.attributes.id}]`; + for (const child of querySelectorAll(node, selector)) { + if (child.type !== 'element') { + continue; + } + for (const name of ['href', 'xlink:href']) { + if (child.attributes[name] != null) { + child.attributes[name] = '#' + id; + } + } + } + continue; + } + } + + pathNode.name = 'use'; + pathNode.attributes['xlink:href'] = '#' + id; } } } @@ -102,7 +157,10 @@ exports.fn = () => { if (node.attributes['xmlns:xlink'] == null) { node.attributes['xmlns:xlink'] = 'http://www.w3.org/1999/xlink'; } - node.children.unshift(defsTag); + + if (svgDefs == null) { + node.children.unshift(defsTag); + } } } }, diff --git a/test/plugins/reusePaths.04.svg b/test/plugins/reusePaths.04.svg new file mode 100644 index 000000000..5d5b57425 --- /dev/null +++ b/test/plugins/reusePaths.04.svg @@ -0,0 +1,23 @@ +Don't create a new defs tag if one already exists as a direct child of the svg +element. + +=== + + + + + + + + +@@@ + + + + + + + + + diff --git a/test/plugins/reusePaths.05.svg b/test/plugins/reusePaths.05.svg new file mode 100644 index 000000000..7cfd410c5 --- /dev/null +++ b/test/plugins/reusePaths.05.svg @@ -0,0 +1,33 @@ +When merging existing defs, remove redundant paths with no attributes or only +an ID attribute. + +=== + + + + + + + + + + + + + + + +@@@ + + + + + + + + + + + + +