summaryrefslogtreecommitdiff
path: root/node_modules/svgo/plugins
diff options
context:
space:
mode:
authorlittleblack111 <littleblack11111@gmail.com>2023-12-21 22:46:53 +0800
committerlittleblack111 <littleblack11111@gmail.com>2023-12-21 22:46:53 +0800
commitf06956efd2dc4bf77244a9fcce2b883a00b925af (patch)
tree0123cb73ce67f1f4b17e1d6cbabff076ee44e31b /node_modules/svgo/plugins
parent8d980c13970c15b6d2312b6fc4ac51698dd0d220 (diff)
updated node modules
Diffstat (limited to 'node_modules/svgo/plugins')
-rw-r--r--node_modules/svgo/plugins/_collections.js137
-rw-r--r--node_modules/svgo/plugins/_transforms.js2
-rw-r--r--node_modules/svgo/plugins/applyTransforms.js37
-rw-r--r--node_modules/svgo/plugins/cleanupAttrs.js2
-rw-r--r--node_modules/svgo/plugins/cleanupEnableBackground.js148
-rw-r--r--node_modules/svgo/plugins/cleanupIds.js70
-rw-r--r--node_modules/svgo/plugins/collapseGroups.js2
-rw-r--r--node_modules/svgo/plugins/convertOneStopGradients.js168
-rw-r--r--node_modules/svgo/plugins/convertPathData.js80
-rw-r--r--node_modules/svgo/plugins/convertShapeToPath.js2
-rw-r--r--node_modules/svgo/plugins/convertStyleToAttrs.js2
-rw-r--r--node_modules/svgo/plugins/convertTransform.js22
-rw-r--r--node_modules/svgo/plugins/inlineStyles.js270
-rw-r--r--node_modules/svgo/plugins/mergeStyles.js2
-rw-r--r--node_modules/svgo/plugins/minifyStyles.js98
-rw-r--r--node_modules/svgo/plugins/moveGroupAttrsToElems.js3
-rw-r--r--node_modules/svgo/plugins/plugins-types.d.ts (renamed from node_modules/svgo/plugins/plugins-types.ts)68
-rw-r--r--node_modules/svgo/plugins/prefixIds.js204
-rw-r--r--node_modules/svgo/plugins/removeAttributesBySelector.js2
-rw-r--r--node_modules/svgo/plugins/removeAttrs.js8
-rw-r--r--node_modules/svgo/plugins/removeComments.js28
-rw-r--r--node_modules/svgo/plugins/removeDesc.js4
-rw-r--r--node_modules/svgo/plugins/removeEditorsNSData.js2
-rw-r--r--node_modules/svgo/plugins/removeEmptyContainers.js3
-rw-r--r--node_modules/svgo/plugins/removeHiddenElems.js172
-rw-r--r--node_modules/svgo/plugins/removeOffCanvasPaths.js2
-rw-r--r--node_modules/svgo/plugins/removeRasterImages.js2
-rw-r--r--node_modules/svgo/plugins/removeScriptElement.js49
-rw-r--r--node_modules/svgo/plugins/removeTitle.js2
-rw-r--r--node_modules/svgo/plugins/removeUnknownsAndDefaults.js16
-rw-r--r--node_modules/svgo/plugins/removeUselessStrokeAndFill.js3
-rw-r--r--node_modules/svgo/plugins/removeXMLNS.js1
-rw-r--r--node_modules/svgo/plugins/removeXlink.js226
-rw-r--r--node_modules/svgo/plugins/reusePaths.js146
34 files changed, 1494 insertions, 489 deletions
diff --git a/node_modules/svgo/plugins/_collections.js b/node_modules/svgo/plugins/_collections.js
index dfa8bff6..b9e67cb7 100644
--- a/node_modules/svgo/plugins/_collections.js
+++ b/node_modules/svgo/plugins/_collections.js
@@ -93,13 +93,19 @@ exports.elemsGroups = {
],
};
-exports.textElems = exports.elemsGroups.textContent.concat('title');
+/**
+ * Elements where adding or removing whitespace may effect rendering, metadata,
+ * or semantic meaning.
+ *
+ * @see https://developer.mozilla.org/docs/Web/HTML/Element/pre
+ */
+exports.textElems = [...exports.elemsGroups.textContent, 'title', 'pre'];
exports.pathElems = ['path', 'glyph', 'missing-glyph'];
-// https://www.w3.org/TR/SVG11/intro.html#Definitions
/**
* @type {Record<string, Array<string>>}
+ * @see https://www.w3.org/TR/SVG11/intro.html#Definitions
*/
exports.attrsGroups = {
animationAddition: ['additive', 'accumulate'],
@@ -218,13 +224,72 @@ exports.attrsGroups = {
'xlink:title',
],
documentEvent: [
- 'onunload',
'onabort',
'onerror',
'onresize',
'onscroll',
+ 'onunload',
'onzoom',
],
+ documentElementEvent: ['oncopy', 'oncut', 'onpaste'],
+ globalEvent: [
+ 'oncancel',
+ 'oncanplay',
+ 'oncanplaythrough',
+ 'onchange',
+ 'onclick',
+ 'onclose',
+ 'oncuechange',
+ 'ondblclick',
+ 'ondrag',
+ 'ondragend',
+ 'ondragenter',
+ 'ondragleave',
+ 'ondragover',
+ 'ondragstart',
+ 'ondrop',
+ 'ondurationchange',
+ 'onemptied',
+ 'onended',
+ 'onerror',
+ 'onfocus',
+ 'oninput',
+ 'oninvalid',
+ 'onkeydown',
+ 'onkeypress',
+ 'onkeyup',
+ 'onload',
+ 'onloadeddata',
+ 'onloadedmetadata',
+ 'onloadstart',
+ 'onmousedown',
+ 'onmouseenter',
+ 'onmouseleave',
+ 'onmousemove',
+ 'onmouseout',
+ 'onmouseover',
+ 'onmouseup',
+ 'onmousewheel',
+ 'onpause',
+ 'onplay',
+ 'onplaying',
+ 'onprogress',
+ 'onratechange',
+ 'onreset',
+ 'onresize',
+ 'onscroll',
+ 'onseeked',
+ 'onseeking',
+ 'onselect',
+ 'onshow',
+ 'onstalled',
+ 'onsubmit',
+ 'onsuspend',
+ 'ontimeupdate',
+ 'ontoggle',
+ 'onvolumechange',
+ 'onwaiting',
+ ],
filterPrimitive: ['x', 'y', 'width', 'height', 'result'],
transferFunction: [
'type',
@@ -304,7 +369,6 @@ exports.attrsGroupsDefaults = {
},
};
-// https://www.w3.org/TR/SVG11/eltindex.html
/**
* @type {Record<string, {
* attrsGroups: Array<string>,
@@ -313,6 +377,7 @@ exports.attrsGroupsDefaults = {
* contentGroups?: Array<string>,
* content?: Array<string>,
* }>}
+ * @see https://www.w3.org/TR/SVG11/eltindex.html
*/
exports.elems = {
a: {
@@ -1889,7 +1954,9 @@ exports.editorNamespaces = [
'http://www.vector.evaxdesign.sk',
];
-// https://www.w3.org/TR/SVG11/linking.html#processingIRI
+/**
+ * @see https://www.w3.org/TR/SVG11/linking.html#processingIRI
+ */
exports.referencesProps = [
'clip-path',
'color-profile',
@@ -1903,7 +1970,9 @@ exports.referencesProps = [
'style',
];
-// https://www.w3.org/TR/SVG11/propidx.html
+/**
+ * @see https://www.w3.org/TR/SVG11/propidx.html
+ */
exports.inheritableAttrs = [
'clip-rule',
'color',
@@ -2157,7 +2226,9 @@ exports.colorsShortNames = {
'#f5deb3': 'wheat',
};
-// https://www.w3.org/TR/SVG11/single-page.html#types-DataTypeColor
+/**
+ * @see https://www.w3.org/TR/SVG11/single-page.html#types-DataTypeColor
+ */
exports.colorsProps = [
'color',
'fill',
@@ -2166,3 +2237,55 @@ exports.colorsProps = [
'flood-color',
'lighting-color',
];
+
+/** @see https://developer.mozilla.org/docs/Web/CSS/Pseudo-classes */
+exports.pseudoClasses = {
+ displayState: ['fullscreen', 'modal', 'picture-in-picture'],
+ input: [
+ 'autofill',
+ 'enabled',
+ 'disabled',
+ 'read-only',
+ 'read-write',
+ 'placeholder-shown',
+ 'default',
+ 'checked',
+ 'indetermined',
+ 'blank',
+ 'valid',
+ 'invalid',
+ 'in-range',
+ 'out-of-range',
+ 'required',
+ 'optional',
+ 'user-invalid',
+ ],
+ linguistic: ['dir', 'lang'],
+ location: [
+ 'any-link',
+ 'link',
+ 'visited',
+ 'local-link',
+ 'target',
+ 'target-within',
+ 'scope',
+ ],
+ resourceState: ['playing', 'paused'],
+ timeDimensional: ['current', 'past', 'future'],
+ treeStructural: [
+ 'root',
+ 'empty',
+ 'nth-child',
+ 'nth-last-child',
+ 'first-child',
+ 'last-child',
+ 'only-child',
+ 'nth-of-type',
+ 'nth-last-of-type',
+ 'first-of-type',
+ 'last-of-type',
+ 'only-of-type',
+ ],
+ userAction: ['hover', 'active', 'focus', 'focus-visible', 'focus-within'],
+ functional: ['is', 'not', 'where', 'has'],
+};
diff --git a/node_modules/svgo/plugins/_transforms.js b/node_modules/svgo/plugins/_transforms.js
index 159f1a3b..efc141c7 100644
--- a/node_modules/svgo/plugins/_transforms.js
+++ b/node_modules/svgo/plugins/_transforms.js
@@ -22,7 +22,7 @@ exports.transform2js = (transformString) => {
const transforms = [];
// current transform context
/**
- * @type {null | TransformItem}
+ * @type {?TransformItem}
*/
let current = null;
// split value into ['', 'translate', '10 50', '', 'scale', '2', '', 'rotate', '-45', '']
diff --git a/node_modules/svgo/plugins/applyTransforms.js b/node_modules/svgo/plugins/applyTransforms.js
index bf60ac89..c0b848c9 100644
--- a/node_modules/svgo/plugins/applyTransforms.js
+++ b/node_modules/svgo/plugins/applyTransforms.js
@@ -12,7 +12,10 @@ const {
transformArc,
} = require('./_transforms.js');
const { path2js } = require('./_path.js');
-const { removeLeadingZero } = require('../lib/svgo/tools.js');
+const {
+ removeLeadingZero,
+ includesUrlReference,
+} = require('../lib/svgo/tools.js');
const { referencesProps, attrsGroupsDefaults } = require('./_collections.js');
/**
@@ -35,9 +38,6 @@ const applyTransforms = (root, params) => {
return {
element: {
enter: (node) => {
- const computedStyle = computeStyle(stylesheet, node);
-
- // used only for paths for now
if (node.attributes.d == null) {
return;
}
@@ -48,7 +48,7 @@ const applyTransforms = (root, params) => {
}
// if there are no 'stroke' attr and references to other objects such as
- // gradiends or clip-path which are also subjects to transform.
+ // gradients or clip-path which are also subjects to transform.
if (
node.attributes.transform == null ||
node.attributes.transform === '' ||
@@ -57,32 +57,41 @@ const applyTransforms = (root, params) => {
node.attributes.style != null ||
Object.entries(node.attributes).some(
([name, value]) =>
- referencesProps.includes(name) && value.includes('url(')
+ referencesProps.includes(name) && includesUrlReference(value)
)
) {
return;
}
+ const computedStyle = computeStyle(stylesheet, node);
+ const transformStyle = computedStyle.transform;
+
+ // Transform overridden in <style> tag which is not considered
+ if (
+ transformStyle.type === 'static' &&
+ transformStyle.value !== node.attributes.transform
+ ) {
+ return;
+ }
+
const matrix = transformsMultiply(
transform2js(node.attributes.transform)
);
+
const stroke =
- computedStyle.stroke != null && computedStyle.stroke.type === 'static'
+ computedStyle.stroke?.type === 'static'
? computedStyle.stroke.value
: null;
const strokeWidth =
- computedStyle['stroke-width'] != null &&
- computedStyle['stroke-width'].type === 'static'
+ computedStyle['stroke-width']?.type === 'static'
? computedStyle['stroke-width'].value
: null;
const transformPrecision = params.transformPrecision;
if (
- (computedStyle.stroke != null &&
- computedStyle.stroke.type === 'dynamic') ||
- (computedStyle.strokeWidth != null &&
- computedStyle['stroke-width'].type === 'dynamic')
+ computedStyle.stroke?.type === 'dynamic' ||
+ computedStyle['stroke-width']?.type === 'dynamic'
) {
return;
}
@@ -94,7 +103,7 @@ const applyTransforms = (root, params) => {
);
if (stroke && stroke != 'none') {
- if (params.applyTransformsStroked === false) {
+ if (!params.applyTransformsStroked) {
return;
}
diff --git a/node_modules/svgo/plugins/cleanupAttrs.js b/node_modules/svgo/plugins/cleanupAttrs.js
index 399de194..af66193a 100644
--- a/node_modules/svgo/plugins/cleanupAttrs.js
+++ b/node_modules/svgo/plugins/cleanupAttrs.js
@@ -22,7 +22,7 @@ exports.fn = (root, params) => {
enter: (node) => {
for (const name of Object.keys(node.attributes)) {
if (newlines) {
- // new line which requires a space instead of themselve
+ // new line which requires a space instead of themself
node.attributes[name] = node.attributes[name].replace(
regNewlinesNeedSpace,
(match, p1, p2) => p1 + ' ' + p2
diff --git a/node_modules/svgo/plugins/cleanupEnableBackground.js b/node_modules/svgo/plugins/cleanupEnableBackground.js
index 80be32be..1d2690d8 100644
--- a/node_modules/svgo/plugins/cleanupEnableBackground.js
+++ b/node_modules/svgo/plugins/cleanupEnableBackground.js
@@ -1,30 +1,29 @@
'use strict';
+const csstree = require('css-tree');
const { visit } = require('../lib/xast.js');
exports.name = 'cleanupEnableBackground';
exports.description =
'remove or cleanup enable-background attribute when possible';
+const regEnableBackground =
+ /^new\s0\s0\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)$/;
+
/**
* Remove or cleanup enable-background attr which coincides with a width/height box.
*
* @see https://www.w3.org/TR/SVG11/filters.html#EnableBackgroundProperty
- *
* @example
* <svg width="100" height="50" enable-background="new 0 0 100 50">
* ⬇
* <svg width="100" height="50">
- *
* @author Kir Belevich
- *
* @type {import('./plugins-types').Plugin<'cleanupEnableBackground'>}
*/
exports.fn = (root) => {
- const regEnableBackground =
- /^new\s0\s0\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)$/;
-
let hasFilter = false;
+
visit(root, {
element: {
enter: (node) => {
@@ -38,36 +37,129 @@ exports.fn = (root) => {
return {
element: {
enter: (node) => {
- if (node.attributes['enable-background'] == null) {
+ /** @type {?csstree.CssNode} */
+ let newStyle = null;
+ /** @type {?csstree.ListItem<csstree.CssNode>} */
+ let enableBackgroundDeclaration = null;
+
+ if (node.attributes.style != null) {
+ newStyle = csstree.parse(node.attributes.style, {
+ context: 'declarationList',
+ });
+
+ if (newStyle.type === 'DeclarationList') {
+ /** @type {csstree.ListItem<csstree.CssNode>[]} */
+ const enableBackgroundDeclarations = [];
+
+ csstree.walk(newStyle, (node, nodeItem) => {
+ if (
+ node.type === 'Declaration' &&
+ node.property === 'enable-background'
+ ) {
+ enableBackgroundDeclarations.push(nodeItem);
+ enableBackgroundDeclaration = nodeItem;
+ }
+ });
+
+ for (let i = 0; i < enableBackgroundDeclarations.length - 1; i++) {
+ newStyle.children.remove(enableBackgroundDeclarations[i]);
+ }
+ }
+ }
+
+ if (!hasFilter) {
+ delete node.attributes['enable-background'];
+
+ if (newStyle?.type === 'DeclarationList') {
+ if (enableBackgroundDeclaration) {
+ newStyle.children.remove(enableBackgroundDeclaration);
+ }
+
+ if (newStyle.children.isEmpty) {
+ delete node.attributes.style;
+ } else {
+ node.attributes.style = csstree.generate(newStyle);
+ }
+ }
+
return;
}
- if (hasFilter) {
+
+ const hasDimensions =
+ node.attributes.width != null && node.attributes.height != null;
+
+ if (
+ (node.name === 'svg' ||
+ node.name === 'mask' ||
+ node.name === 'pattern') &&
+ hasDimensions
+ ) {
+ const attrValue = node.attributes['enable-background'];
+ const attrCleaned = cleanupValue(
+ attrValue,
+ node.name,
+ node.attributes.width,
+ node.attributes.height
+ );
+
+ if (attrCleaned) {
+ node.attributes['enable-background'] = attrCleaned;
+ } else {
+ delete node.attributes['enable-background'];
+ }
+
if (
- (node.name === 'svg' ||
- node.name === 'mask' ||
- node.name === 'pattern') &&
- node.attributes.width != null &&
- node.attributes.height != null
+ newStyle?.type === 'DeclarationList' &&
+ enableBackgroundDeclaration
) {
- const match =
- node.attributes['enable-background'].match(regEnableBackground);
- if (
- match != null &&
- node.attributes.width === match[1] &&
- node.attributes.height === match[3]
- ) {
- if (node.name === 'svg') {
- delete node.attributes['enable-background'];
- } else {
- node.attributes['enable-background'] = 'new';
- }
+ const styleValue = csstree.generate(
+ // @ts-ignore
+ enableBackgroundDeclaration.data.value
+ );
+ const styleCleaned = cleanupValue(
+ styleValue,
+ node.name,
+ node.attributes.width,
+ node.attributes.height
+ );
+
+ if (styleCleaned) {
+ // @ts-ignore
+ enableBackgroundDeclaration.data.value = {
+ type: 'Raw',
+ value: styleCleaned,
+ };
+ } else {
+ newStyle.children.remove(enableBackgroundDeclaration);
}
}
- } else {
- //we don't need 'enable-background' if we have no filters
- delete node.attributes['enable-background'];
+ }
+
+ if (newStyle?.type === 'DeclarationList') {
+ if (newStyle.children.isEmpty) {
+ delete node.attributes.style;
+ } else {
+ node.attributes.style = csstree.generate(newStyle);
+ }
}
},
},
};
};
+
+/**
+ * @param {string} value Value of a enable-background attribute or style declaration.
+ * @param {string} nodeName Name of the node the value was assigned to.
+ * @param {string} width Width of the node the value was assigned to.
+ * @param {string} height Height of the node the value was assigned to.
+ * @returns {string | undefined} Cleaned up value, or undefined if it's redundant.
+ */
+const cleanupValue = (value, nodeName, width, height) => {
+ const match = regEnableBackground.exec(value);
+
+ if (match != null && width === match[1] && height === match[3]) {
+ return nodeName === 'svg' ? undefined : 'new';
+ }
+
+ return value;
+};
diff --git a/node_modules/svgo/plugins/cleanupIds.js b/node_modules/svgo/plugins/cleanupIds.js
index 1d2532b2..65b38d16 100644
--- a/node_modules/svgo/plugins/cleanupIds.js
+++ b/node_modules/svgo/plugins/cleanupIds.js
@@ -5,14 +5,11 @@
*/
const { visitSkip } = require('../lib/xast.js');
-const { referencesProps } = require('./_collections.js');
+const { hasScripts, findReferences } = require('../lib/svgo/tools');
exports.name = 'cleanupIds';
exports.description = 'removes unused IDs and minifies used';
-const regReferencesUrl = /\burl\((["'])?#(.+?)\1\)/;
-const regReferencesHref = /^#(.+?)$/;
-const regReferencesBegin = /(\D+)\./;
const generateIdChars = [
'a',
'b',
@@ -86,7 +83,8 @@ const hasStringPrefix = (string, prefixes) => {
/**
* Generate unique minimal ID.
*
- * @type {(currentId: null | Array<number>) => Array<number>}
+ * @param {?number[]} currentId
+ * @returns {number[]}
*/
const generateId = (currentId) => {
if (currentId == null) {
@@ -146,7 +144,7 @@ exports.fn = (_root, params) => {
*/
const nodeById = new Map();
/**
- * @type {Map<string, Array<{element: XastElement, name: string, value: string }>>}
+ * @type {Map<string, Array<{element: XastElement, name: string }>>}
*/
const referencesById = new Map();
let deoptimized = false;
@@ -154,11 +152,11 @@ exports.fn = (_root, params) => {
return {
element: {
enter: (node) => {
- if (force == false) {
- // deoptimize if style or script elements are present
+ if (!force) {
+ // deoptimize if style or scripts are present
if (
- (node.name === 'style' || node.name === 'script') &&
- node.children.length !== 0
+ (node.name === 'style' && node.children.length !== 0) ||
+ hasScripts(node)
) {
deoptimized = true;
return;
@@ -189,36 +187,14 @@ exports.fn = (_root, params) => {
nodeById.set(id, node);
}
} else {
- // collect all references
- /**
- * @type {null | string}
- */
- let id = null;
- if (referencesProps.includes(name)) {
- const match = value.match(regReferencesUrl);
- if (match != null) {
- id = match[2]; // url() reference
- }
- }
- if (name === 'href' || name.endsWith(':href')) {
- const match = value.match(regReferencesHref);
- if (match != null) {
- id = match[1]; // href reference
- }
- }
- if (name === 'begin') {
- const match = value.match(regReferencesBegin);
- if (match != null) {
- id = match[1]; // href reference
- }
- }
- if (id != null) {
+ const ids = findReferences(name, value);
+ for (const id of ids) {
let refs = referencesById.get(id);
if (refs == null) {
refs = [];
referencesById.set(id, refs);
}
- refs.push({ element: node, name, value });
+ refs.push({ element: node, name });
}
}
}
@@ -231,33 +207,35 @@ exports.fn = (_root, params) => {
return;
}
/**
- * @type {(id: string) => boolean}
- **/
+ * @param {string} id
+ * @returns {boolean}
+ */
const isIdPreserved = (id) =>
preserveIds.has(id) || hasStringPrefix(id, preserveIdPrefixes);
- /**
- * @type {null | Array<number>}
- */
+ /** @type {?number[]} */
let currentId = null;
for (const [id, refs] of referencesById) {
const node = nodeById.get(id);
if (node != null) {
// replace referenced IDs with the minified ones
if (minify && isIdPreserved(id) === false) {
- /**
- * @type {null | string}
- */
+ /** @type {?string} */
let currentIdString = null;
do {
currentId = generateId(currentId);
currentIdString = getIdString(currentId);
- } while (isIdPreserved(currentIdString));
+ } while (
+ isIdPreserved(currentIdString) ||
+ (referencesById.has(currentIdString) &&
+ nodeById.get(currentIdString) == null)
+ );
node.attributes.id = currentIdString;
- for (const { element, name, value } of refs) {
+ for (const { element, name } of refs) {
+ const value = element.attributes[name];
if (value.includes('#')) {
// replace id in href and url()
element.attributes[name] = value.replace(
- `#${id}`,
+ `#${encodeURI(id)}`,
`#${currentIdString}`
);
} else {
diff --git a/node_modules/svgo/plugins/collapseGroups.js b/node_modules/svgo/plugins/collapseGroups.js
index ff38caff..6042a01c 100644
--- a/node_modules/svgo/plugins/collapseGroups.js
+++ b/node_modules/svgo/plugins/collapseGroups.js
@@ -63,7 +63,7 @@ exports.fn = () => {
return;
}
- // move group attibutes to the single child element
+ // move group attributes to the single child element
if (
Object.keys(node.attributes).length !== 0 &&
node.children.length === 1
diff --git a/node_modules/svgo/plugins/convertOneStopGradients.js b/node_modules/svgo/plugins/convertOneStopGradients.js
new file mode 100644
index 00000000..e5f4625d
--- /dev/null
+++ b/node_modules/svgo/plugins/convertOneStopGradients.js
@@ -0,0 +1,168 @@
+'use strict';
+
+/**
+ * @typedef {import('../lib/types').XastElement} XastElement
+ * @typedef {import('../lib/types').XastParent} XastParent
+ */
+
+const { attrsGroupsDefaults, colorsProps } = require('./_collections');
+const {
+ detachNodeFromParent,
+ querySelectorAll,
+ querySelector,
+} = require('../lib/xast');
+const { computeStyle, collectStylesheet } = require('../lib/style');
+
+exports.name = 'convertOneStopGradients';
+exports.description =
+ 'converts one-stop (single color) gradients to a plain color';
+
+/**
+ * Converts one-stop (single color) gradients to a plain color.
+ *
+ * @author Seth Falco <seth@falco.fun>
+ * @type {import('./plugins-types').Plugin<'convertOneStopGradients'>}
+ * @see https://developer.mozilla.org/docs/Web/SVG/Element/linearGradient
+ * @see https://developer.mozilla.org/docs/Web/SVG/Element/radialGradient
+ */
+exports.fn = (root) => {
+ const stylesheet = collectStylesheet(root);
+
+ /**
+ * Parent defs that had gradients elements removed from them.
+ *
+ * @type {Set<XastElement>}
+ */
+ const effectedDefs = new Set();
+
+ /**
+ * @type {Map<XastElement, XastParent>}
+ */
+ const allDefs = new Map();
+
+ /**
+ * @type {Map<XastElement, XastParent>}
+ */
+ const gradientsToDetach = new Map();
+
+ /** Number of references to the xlink:href attribute. */
+ let xlinkHrefCount = 0;
+
+ return {
+ element: {
+ enter: (node, parentNode) => {
+ if (node.attributes['xlink:href'] != null) {
+ xlinkHrefCount++;
+ }
+
+ if (node.name === 'defs') {
+ allDefs.set(node, parentNode);
+ return;
+ }
+
+ if (node.name !== 'linearGradient' && node.name !== 'radialGradient') {
+ return;
+ }
+
+ const stops = node.children.filter((child) => {
+ return child.type === 'element' && child.name === 'stop';
+ });
+
+ const href = node.attributes['xlink:href'] || node.attributes['href'];
+ let effectiveNode =
+ stops.length === 0 && href != null && href.startsWith('#')
+ ? querySelector(root, href)
+ : node;
+
+ if (effectiveNode == null || effectiveNode.type !== 'element') {
+ gradientsToDetach.set(node, parentNode);
+ return;
+ }
+
+ const effectiveStops = effectiveNode.children.filter((child) => {
+ return child.type === 'element' && child.name === 'stop';
+ });
+
+ if (
+ effectiveStops.length !== 1 ||
+ effectiveStops[0].type !== 'element'
+ ) {
+ return;
+ }
+
+ if (parentNode.type === 'element' && parentNode.name === 'defs') {
+ effectedDefs.add(parentNode);
+ }
+
+ gradientsToDetach.set(node, parentNode);
+
+ let color;
+ const style = computeStyle(stylesheet, effectiveStops[0])['stop-color'];
+ if (style != null && style.type === 'static') {
+ color = style.value;
+ }
+
+ const selectorVal = `url(#${node.attributes.id})`;
+
+ const selector = colorsProps
+ .map((attr) => `[${attr}="${selectorVal}"]`)
+ .join(',');
+ const elements = querySelectorAll(root, selector);
+ for (const element of elements) {
+ if (element.type !== 'element') {
+ continue;
+ }
+
+ for (const attr of colorsProps) {
+ if (element.attributes[attr] !== selectorVal) {
+ continue;
+ }
+
+ if (color != null) {
+ element.attributes[attr] = color;
+ } else {
+ delete element.attributes[attr];
+ }
+ }
+ }
+
+ const styledElements = querySelectorAll(
+ root,
+ `[style*=${selectorVal}]`
+ );
+ for (const element of styledElements) {
+ if (element.type !== 'element') {
+ continue;
+ }
+
+ element.attributes.style = element.attributes.style.replace(
+ selectorVal,
+ color || attrsGroupsDefaults.presentation['stop-color']
+ );
+ }
+ },
+
+ exit: (node) => {
+ if (node.name === 'svg') {
+ for (const [gradient, parent] of gradientsToDetach.entries()) {
+ if (gradient.attributes['xlink:href'] != null) {
+ xlinkHrefCount--;
+ }
+
+ detachNodeFromParent(gradient, parent);
+ }
+
+ if (xlinkHrefCount === 0) {
+ delete node.attributes['xmlns:xlink'];
+ }
+
+ for (const [defs, parent] of allDefs.entries()) {
+ if (effectedDefs.has(defs) && defs.children.length === 0) {
+ detachNodeFromParent(defs, parent);
+ }
+ }
+ }
+ },
+ },
+ };
+};
diff --git a/node_modules/svgo/plugins/convertPathData.js b/node_modules/svgo/plugins/convertPathData.js
index a5f375ba..6229d9f7 100644
--- a/node_modules/svgo/plugins/convertPathData.js
+++ b/node_modules/svgo/plugins/convertPathData.js
@@ -46,6 +46,7 @@ let arcTolerance;
* },
* straightCurves: boolean,
* lineShorthands: boolean,
+ * convertToZ: boolean,
* curveSmoothShorthands: boolean,
* floatPrecision: number | false,
* transformPrecision: number,
@@ -95,6 +96,7 @@ exports.fn = (root, params) => {
},
straightCurves = true,
lineShorthands = true,
+ convertToZ = true,
curveSmoothShorthands = true,
floatPrecision = 3,
transformPrecision = 5,
@@ -116,6 +118,7 @@ exports.fn = (root, params) => {
makeArcs,
straightCurves,
lineShorthands,
+ convertToZ,
curveSmoothShorthands,
floatPrecision,
transformPrecision,
@@ -167,6 +170,12 @@ exports.fn = (root, params) => {
(computedStyle['stroke-linecap'].type === 'dynamic' ||
computedStyle['stroke-linecap'].value !== 'butt');
const maybeHasStrokeAndLinecap = maybeHasStroke && maybeHasLinecap;
+ const isSafeToUseZ = maybeHasStroke
+ ? computedStyle['stroke-linecap']?.type === 'static' &&
+ computedStyle['stroke-linecap'].value === 'round' &&
+ computedStyle['stroke-linejoin']?.type === 'static' &&
+ computedStyle['stroke-linejoin'].value === 'round'
+ : true;
var data = path2js(node);
@@ -175,6 +184,7 @@ exports.fn = (root, params) => {
convertToRelative(data);
data = filters(data, newParams, {
+ isSafeToUseZ,
maybeHasStrokeAndLinecap,
hasMarkerMid,
});
@@ -371,10 +381,14 @@ const convertToRelative = (pathData) => {
* @type {(
* path: PathDataItem[],
* params: InternalParams,
- * aux: { maybeHasStrokeAndLinecap: boolean, hasMarkerMid: boolean }
+ * aux: { isSafeToUseZ: boolean, maybeHasStrokeAndLinecap: boolean, hasMarkerMid: boolean }
* ) => PathDataItem[]}
*/
-function filters(path, params, { maybeHasStrokeAndLinecap, hasMarkerMid }) {
+function filters(
+ path,
+ params,
+ { isSafeToUseZ, maybeHasStrokeAndLinecap, hasMarkerMid }
+) {
var stringify = data2Path.bind(null, params),
relSubpoint = [0, 0],
pathBase = [0, 0],
@@ -579,7 +593,7 @@ function filters(path, params, { maybeHasStrokeAndLinecap, hasMarkerMid }) {
}
}
- // Rounding relative coordinates, taking in account accummulating error
+ // Rounding relative coordinates, taking in account accumulating error
// to get closer to absolute coordinates. Sum of rounded value remains same:
// l .25 3 .25 2 .25 3 .25 2 -> l .3 3 .2 2 .3 3 .2 2
if (precision !== false) {
@@ -664,6 +678,24 @@ function filters(path, params, { maybeHasStrokeAndLinecap, hasMarkerMid }) {
}
}
+ // convert going home to z
+ // m 0 0 h 5 v 5 l -5 -5 -> m 0 0 h 5 v 5 z
+ if (
+ params.convertToZ &&
+ (isSafeToUseZ || next?.command === 'Z' || next?.command === 'z') &&
+ (command === 'l' || command === 'h' || command === 'v')
+ ) {
+ if (
+ // @ts-ignore
+ Math.abs(pathBase[0] - item.coords[0]) < error &&
+ // @ts-ignore
+ Math.abs(pathBase[1] - item.coords[1]) < error
+ ) {
+ command = 'z';
+ data = [];
+ }
+ }
+
// collapse repeated commands
// h 20 h 30 -> h 50
if (
@@ -796,17 +828,25 @@ function filters(path, params, { maybeHasStrokeAndLinecap, hasMarkerMid }) {
item.command = command;
item.args = data;
-
- prev = item;
} else {
// z resets coordinates
relSubpoint[0] = pathBase[0];
relSubpoint[1] = pathBase[1];
// @ts-ignore
if (prev.command === 'Z' || prev.command === 'z') return false;
- prev = item;
}
+ if (
+ (command === 'Z' || command === 'z') &&
+ params.removeUseless &&
+ isSafeToUseZ &&
+ // @ts-ignore
+ Math.abs(item.base[0] - item.coords[0]) < error / 10 &&
+ // @ts-ignore
+ Math.abs(item.base[1] - item.coords[1]) < error / 10
+ )
+ return false;
+ prev = item;
return true;
});
@@ -935,7 +975,7 @@ function getIntersection(coords) {
c2 = coords[4] * coords[7] - coords[5] * coords[6], // x1 * y2 - x2 * y1
denom = a1 * b2 - a2 * b1;
- if (!denom) return; // parallel lines havn't an intersection
+ if (!denom) return; // parallel lines haven't an intersection
/**
* @type {Point}
@@ -952,6 +992,16 @@ function getIntersection(coords) {
}
/**
+ * Does the same as `Number.prototype.toFixed` but without casting
+ * the return value to a string.
+ * @type {(num: number, precision: number) => number}
+ */
+function toFixed(num, precision) {
+ const pow = 10 ** precision;
+ return Math.round(num * pow) / pow;
+}
+
+/**
* Decrease accuracy of floating-point numbers
* in path data keeping a specified number of decimals.
* Smart rounds values like 2.3491 to 2.35 instead of 2.349.
@@ -960,16 +1010,14 @@ function getIntersection(coords) {
* @type {(data: number[]) => number[]}
*/
function strongRound(data) {
- for (var i = data.length; i-- > 0; ) {
- // @ts-ignore
- if (data[i].toFixed(precision) != data[i]) {
- // @ts-ignore
- var rounded = +data[i].toFixed(precision - 1);
+ const precisionNum = precision || 0;
+ for (let i = data.length; i-- > 0; ) {
+ const fixed = toFixed(data[i], precisionNum);
+ if (fixed !== data[i]) {
+ const rounded = toFixed(data[i], precisionNum - 1);
data[i] =
- // @ts-ignore
- +Math.abs(rounded - data[i]).toFixed(precision + 1) >= error
- ? // @ts-ignore
- +data[i].toFixed(precision)
+ toFixed(Math.abs(rounded - data[i]), precisionNum + 1) >= error
+ ? fixed
: rounded;
}
}
diff --git a/node_modules/svgo/plugins/convertShapeToPath.js b/node_modules/svgo/plugins/convertShapeToPath.js
index 7705fa97..8a3b5a1f 100644
--- a/node_modules/svgo/plugins/convertShapeToPath.js
+++ b/node_modules/svgo/plugins/convertShapeToPath.js
@@ -139,7 +139,7 @@ exports.fn = (root, params) => {
delete node.attributes.r;
}
- // optionally covert ellipse
+ // optionally convert ellipse
if (node.name === 'ellipse' && convertArcs) {
const ecx = Number(node.attributes.cx || '0');
const ecy = Number(node.attributes.cy || '0');
diff --git a/node_modules/svgo/plugins/convertStyleToAttrs.js b/node_modules/svgo/plugins/convertStyleToAttrs.js
index 5d5107d6..662119e4 100644
--- a/node_modules/svgo/plugins/convertStyleToAttrs.js
+++ b/node_modules/svgo/plugins/convertStyleToAttrs.js
@@ -19,7 +19,7 @@ const rSingleQuotes = "'(?:[^'\\n\\r\\\\]|" + rEscape + ")*?(?:'|$)"; // string
const rQuotes = '"(?:[^"\\n\\r\\\\]|' + rEscape + ')*?(?:"|$)'; // string in double quotes: "smth"
const rQuotedString = new RegExp('^' + g(rSingleQuotes, rQuotes) + '$');
// Parentheses, E.g.: url(data:image/png;base64,iVBO...).
-// ':' and ';' inside of it should be threated as is. (Just like in strings.)
+// ':' and ';' inside of it should be treated as is. (Just like in strings.)
const rParenthesis =
'\\(' + g('[^\'"()\\\\]+', rEscape, rSingleQuotes, rQuotes) + '*?' + '\\)';
// The value. It can have strings and parentheses (see above). Fallbacks to anything in case of unexpected input.
diff --git a/node_modules/svgo/plugins/convertTransform.js b/node_modules/svgo/plugins/convertTransform.js
index d90c3efd..b69e17a3 100644
--- a/node_modules/svgo/plugins/convertTransform.js
+++ b/node_modules/svgo/plugins/convertTransform.js
@@ -131,7 +131,7 @@ const convertTransform = (item, attrName, params) => {
* Defines precision to work with certain parts.
* transformPrecision - for scale and four first matrix parameters (needs a better precision due to multiplying),
* floatPrecision - for translate including two last matrix and rotate parameters,
- * degPrecision - for rotate and skew. By default it's equal to (rougly)
+ * degPrecision - for rotate and skew. By default it's equal to (roughly)
* transformPrecision - 2 or floatPrecision whichever is lower. Can be set in params.
*
* @type {(data: Array<TransformItem>, params: TransformParams) => TransformParams}
@@ -294,7 +294,7 @@ const convertToShorts = (transforms, params) => {
/**
* Remove useless transforms.
*
- * @type {(trasforms: Array<TransformItem>) => Array<TransformItem>}
+ * @type {(transforms: Array<TransformItem>) => Array<TransformItem>}
*/
const removeUseless = (transforms) => {
return transforms.filter((transform) => {
@@ -335,18 +335,12 @@ const removeUseless = (transforms) => {
* @type {(transformJS: Array<TransformItem>, params: TransformParams) => string}
*/
const js2transform = (transformJS, params) => {
- var transformString = '';
-
- // collect output value string
- transformJS.forEach((transform) => {
- roundTransform(transform, params);
- transformString +=
- (transformString && ' ') +
- transform.name +
- '(' +
- cleanupOutData(transform.data, params) +
- ')';
- });
+ const transformString = transformJS
+ .map((transform) => {
+ roundTransform(transform, params);
+ return `${transform.name}(${cleanupOutData(transform.data, params)})`;
+ })
+ .join('');
return transformString;
};
diff --git a/node_modules/svgo/plugins/inlineStyles.js b/node_modules/svgo/plugins/inlineStyles.js
index e931b5f6..1cdcebe7 100644
--- a/node_modules/svgo/plugins/inlineStyles.js
+++ b/node_modules/svgo/plugins/inlineStyles.js
@@ -1,14 +1,13 @@
'use strict';
/**
- * @typedef {import('../lib/types').Specificity} Specificity
* @typedef {import('../lib/types').XastElement} XastElement
* @typedef {import('../lib/types').XastParent} XastParent
*/
const csstree = require('css-tree');
const {
- // @ts-ignore not defined in @types/csso
+ // @ts-ignore internal api
syntax: { specificity },
} = require('csso');
const {
@@ -16,54 +15,32 @@ const {
querySelectorAll,
detachNodeFromParent,
} = require('../lib/xast.js');
+const { compareSpecificity, includesAttrSelector } = require('../lib/style');
+const { attrsGroups, pseudoClasses } = require('./_collections');
exports.name = 'inlineStyles';
exports.description = 'inline styles (additional options)';
/**
- * Compares two selector specificities.
- * extracted from https://github.com/keeganstreet/specificity/blob/main/specificity.js#L211
+ * Some pseudo-classes can only be calculated by clients, like :visited,
+ * :future, or :hover, but there are other pseudo-classes that we can evaluate
+ * during optimization.
*
- * @type {(a: Specificity, b: Specificity) => number}
- */
-const compareSpecificity = (a, b) => {
- for (var i = 0; i < 4; i += 1) {
- if (a[i] < b[i]) {
- return -1;
- } else if (a[i] > b[i]) {
- return 1;
- }
- }
- return 0;
-};
-
-/**
- * @type {(value: any) => any}
+ * The list of pseudo-classes that we can evaluate during optimization, and so
+ * shouldn't be toggled conditionally through the `usePseudos` parameter.
+ *
+ * @see https://developer.mozilla.org/docs/Web/CSS/Pseudo-classes
*/
-const toAny = (value) => value;
+const preservedPseudos = [
+ ...pseudoClasses.functional,
+ ...pseudoClasses.treeStructural,
+];
/**
- * Moves + merges styles from style elements to element styles
- *
- * Options
- * onlyMatchedOnce (default: true)
- * inline only selectors that match once
- *
- * removeMatchedSelectors (default: true)
- * clean up matched selectors,
- * leave selectors that hadn't matched
- *
- * useMqs (default: ['', 'screen'])
- * what media queries to be used
- * empty string element for styles outside media queries
- *
- * usePseudos (default: [''])
- * what pseudo-classes/-elements to be used
- * empty string element for all non-pseudo-classes and/or -elements
- *
- * @author strarsis <strarsis@gmail.com>
+ * Merges styles from style nodes into inline styles.
*
* @type {import('./plugins-types').Plugin<'inlineStyles'>}
+ * @author strarsis <strarsis@gmail.com>
*/
exports.fn = (root, params) => {
const {
@@ -90,15 +67,12 @@ exports.fn = (root, params) => {
return {
element: {
enter: (node, parentNode) => {
- // skip <foreignObject /> content
if (node.name === 'foreignObject') {
return visitSkip;
}
- // collect only non-empty <style /> elements
if (node.name !== 'style' || node.children.length === 0) {
return;
}
- // values other than the empty string or text/css are not used
if (
node.attributes.type != null &&
node.attributes.type !== '' &&
@@ -106,16 +80,14 @@ exports.fn = (root, params) => {
) {
return;
}
- // parse css in style element
- let cssText = '';
- for (const child of node.children) {
- if (child.type === 'text' || child.type === 'cdata') {
- cssText += child.value;
- }
- }
- /**
- * @type {null | csstree.CssNode}
- */
+
+ const cssText = node.children
+ .filter((child) => child.type === 'text' || child.type === 'cdata')
+ // @ts-ignore
+ .map((child) => child.value)
+ .join('');
+
+ /** @type {?csstree.CssNode} */
let cssAst = null;
try {
cssAst = csstree.parse(cssText, {
@@ -131,63 +103,68 @@ exports.fn = (root, params) => {
// collect selectors
csstree.walk(cssAst, {
- visit: 'Selector',
- enter(node, item) {
+ visit: 'Rule',
+ enter(node) {
const atrule = this.atrule;
- const rule = this.rule;
- if (rule == null) {
- return;
- }
// skip media queries not included into useMqs param
- let mq = '';
+ let mediaQuery = '';
if (atrule != null) {
- mq = atrule.name;
+ mediaQuery = atrule.name;
if (atrule.prelude != null) {
- mq += ` ${csstree.generate(atrule.prelude)}`;
+ mediaQuery += ` ${csstree.generate(atrule.prelude)}`;
}
}
- if (useMqs.includes(mq) === false) {
+ if (!useMqs.includes(mediaQuery)) {
return;
}
- /**
- * @type {Array<{
- * item: csstree.ListItem<csstree.CssNode>,
- * list: csstree.List<csstree.CssNode>
- * }>}
- */
- const pseudos = [];
- if (node.type === 'Selector') {
- node.children.forEach((childNode, childItem, childList) => {
- if (
- childNode.type === 'PseudoClassSelector' ||
- childNode.type === 'PseudoElementSelector'
- ) {
- pseudos.push({ item: childItem, list: childList });
- }
- });
- }
+ if (node.prelude.type === 'SelectorList') {
+ node.prelude.children.forEach((childNode, item) => {
+ if (childNode.type === 'Selector') {
+ /**
+ * @type {Array<{
+ * item: csstree.ListItem<csstree.CssNode>,
+ * list: csstree.List<csstree.CssNode>
+ * }>}
+ */
+ const pseudos = [];
- // skip pseudo classes and pseudo elements not includes into usePseudos param
- const pseudoSelectors = csstree.generate({
- type: 'Selector',
- children: new csstree.List().fromArray(
- pseudos.map((pseudo) => pseudo.item.data)
- ),
- });
- if (usePseudos.includes(pseudoSelectors) === false) {
- return;
- }
+ childNode.children.forEach(
+ (grandchildNode, grandchildItem, grandchildList) => {
+ const isPseudo =
+ grandchildNode.type === 'PseudoClassSelector' ||
+ grandchildNode.type === 'PseudoElementSelector';
- // remove pseudo classes and elements to allow querySelector match elements
- // TODO this is not very accurate since some pseudo classes like first-child
- // are used for selection
- for (const pseudo of pseudos) {
- pseudo.list.remove(pseudo.item);
- }
+ if (
+ isPseudo &&
+ !preservedPseudos.includes(grandchildNode.name)
+ ) {
+ pseudos.push({
+ item: grandchildItem,
+ list: grandchildList,
+ });
+ }
+ }
+ );
- selectors.push({ node, item, rule });
+ const pseudoSelectors = csstree.generate({
+ type: 'Selector',
+ children: new csstree.List().fromArray(
+ pseudos.map((pseudo) => pseudo.item.data)
+ ),
+ });
+
+ if (usePseudos.includes(pseudoSelectors)) {
+ for (const pseudo of pseudos) {
+ pseudo.list.remove(pseudo.item);
+ }
+ }
+
+ selectors.push({ node: childNode, rule: node, item: item });
+ }
+ });
+ }
},
});
},
@@ -198,8 +175,8 @@ exports.fn = (root, params) => {
if (styles.length === 0) {
return;
}
- // stable sort selectors
- const sortedSelectors = [...selectors]
+ const sortedSelectors = selectors
+ .slice()
.sort((a, b) => {
const aSpecificity = specificity(a.item.data);
const bSpecificity = specificity(b.item.data);
@@ -210,9 +187,7 @@ exports.fn = (root, params) => {
for (const selector of sortedSelectors) {
// match selectors
const selectorText = csstree.generate(selector.item.data);
- /**
- * @type {Array<XastElement>}
- */
+ /** @type {Array<XastElement>} */
const matchedElements = [];
try {
for (const node of querySelectorAll(root, selectorText)) {
@@ -237,9 +212,7 @@ exports.fn = (root, params) => {
// apply <style/> to matched elements
for (const selectedEl of matchedElements) {
const styleDeclarationList = csstree.parse(
- selectedEl.attributes.style == null
- ? ''
- : selectedEl.attributes.style,
+ selectedEl.attributes.style ?? '',
{
context: 'declarationList',
parseValue: false,
@@ -249,10 +222,18 @@ exports.fn = (root, params) => {
continue;
}
const styleDeclarationItems = new Map();
+
+ /** @type {csstree.ListItem<csstree.CssNode>} */
+ let firstListItem;
+
csstree.walk(styleDeclarationList, {
visit: 'Declaration',
enter(node, item) {
- styleDeclarationItems.set(node.property, item);
+ if (firstListItem == null) {
+ firstListItem = item;
+ }
+
+ styleDeclarationItems.set(node.property.toLowerCase(), item);
},
});
// merge declarations
@@ -263,13 +244,25 @@ exports.fn = (root, params) => {
// no inline styles, external styles, external styles used
// inline styles, external styles same priority as inline styles, inline styles used
// inline styles, external styles higher priority than inline styles, external styles used
- const matchedItem = styleDeclarationItems.get(
- ruleDeclaration.property
- );
+ const property = ruleDeclaration.property;
+
+ if (
+ attrsGroups.presentation.includes(property) &&
+ !selectors.some((selector) =>
+ includesAttrSelector(selector.item, property)
+ )
+ ) {
+ delete selectedEl.attributes[property];
+ }
+
+ const matchedItem = styleDeclarationItems.get(property);
const ruleDeclarationItem =
styleDeclarationList.children.createItem(ruleDeclaration);
if (matchedItem == null) {
- styleDeclarationList.children.append(ruleDeclarationItem);
+ styleDeclarationList.children.insert(
+ ruleDeclarationItem,
+ firstListItem
+ );
} else if (
matchedItem.data.important !== true &&
ruleDeclaration.important === true
@@ -278,15 +271,15 @@ exports.fn = (root, params) => {
matchedItem,
ruleDeclarationItem
);
- styleDeclarationItems.set(
- ruleDeclaration.property,
- ruleDeclarationItem
- );
+ styleDeclarationItems.set(property, ruleDeclarationItem);
}
},
});
- selectedEl.attributes.style =
- csstree.generate(styleDeclarationList);
+
+ const newStyles = csstree.generate(styleDeclarationList);
+ if (newStyles.length !== 0) {
+ selectedEl.attributes.style = newStyles;
+ }
}
if (
@@ -301,7 +294,7 @@ exports.fn = (root, params) => {
}
// no further processing required
- if (removeMatchedSelectors === false) {
+ if (!removeMatchedSelectors) {
return;
}
@@ -323,17 +316,18 @@ exports.fn = (root, params) => {
? null
: selectedEl.attributes.class.split(' ')
);
- /**
- * csstree v2 changed this type
- * @type {csstree.CssNode}
- */
- const firstSubSelector = toAny(selector.node.children.first);
- if (
- firstSubSelector != null &&
- firstSubSelector.type === 'ClassSelector'
- ) {
- classList.delete(firstSubSelector.name);
+
+ for (const child of selector.node.children) {
+ if (
+ child.type === 'ClassSelector' &&
+ !selectors.some((selector) =>
+ includesAttrSelector(selector.item, 'class', child.name, true)
+ )
+ ) {
+ classList.delete(child.name);
+ }
}
+
if (classList.size === 0) {
delete selectedEl.attributes.class;
} else {
@@ -341,13 +335,20 @@ exports.fn = (root, params) => {
}
// ID
+ const firstSubSelector = selector.node.children.first;
if (
- firstSubSelector != null &&
- firstSubSelector.type === 'IdSelector'
+ firstSubSelector?.type === 'IdSelector' &&
+ selectedEl.attributes.id === firstSubSelector.name &&
+ !selectors.some((selector) =>
+ includesAttrSelector(
+ selector.item,
+ 'id',
+ firstSubSelector.name,
+ true
+ )
+ )
) {
- if (selectedEl.attributes.id === firstSubSelector.name) {
- delete selectedEl.attributes.id;
- }
+ delete selectedEl.attributes.id;
}
}
}
@@ -360,8 +361,7 @@ exports.fn = (root, params) => {
if (
node.type === 'Rule' &&
node.prelude.type === 'SelectorList' &&
- // csstree v2 changed this type
- toAny(node.prelude.children.isEmpty)
+ node.prelude.children.isEmpty
) {
list.remove(item);
}
@@ -369,8 +369,8 @@ exports.fn = (root, params) => {
});
// csstree v2 changed this type
- if (toAny(style.cssAst.children.isEmpty)) {
- // remove emtpy style element
+ if (style.cssAst.children.isEmpty) {
+ // remove empty style element
detachNodeFromParent(style.node, style.parentNode);
} else {
// update style element if any styles left
diff --git a/node_modules/svgo/plugins/mergeStyles.js b/node_modules/svgo/plugins/mergeStyles.js
index 1cc32015..764d0194 100644
--- a/node_modules/svgo/plugins/mergeStyles.js
+++ b/node_modules/svgo/plugins/mergeStyles.js
@@ -19,7 +19,7 @@ exports.description = 'merge multiple style elements into one';
*/
exports.fn = () => {
/**
- * @type {null | XastElement}
+ * @type {?XastElement}
*/
let firstStyleElement = null;
let collectedStyles = '';
diff --git a/node_modules/svgo/plugins/minifyStyles.js b/node_modules/svgo/plugins/minifyStyles.js
index a10b8d20..598b6db3 100644
--- a/node_modules/svgo/plugins/minifyStyles.js
+++ b/node_modules/svgo/plugins/minifyStyles.js
@@ -2,27 +2,48 @@
/**
* @typedef {import('../lib/types').XastElement} XastElement
+ * @typedef {import('../lib/types').XastParent} XastParent
*/
const csso = require('csso');
+const { detachNodeFromParent } = require('../lib/xast');
+const { hasScripts } = require('../lib/svgo/tools');
exports.name = 'minifyStyles';
-exports.description =
- 'minifies styles and removes unused styles based on usage data';
+exports.description = 'minifies styles and removes unused styles';
/**
- * Minifies styles (<style> element + style attribute) using CSSO
+ * Minifies styles (<style> element + style attribute) using CSSO.
*
* @author strarsis <strarsis@gmail.com>
- *
* @type {import('./plugins-types').Plugin<'minifyStyles'>}
*/
exports.fn = (_root, { usage, ...params }) => {
+ /** @type {Map<XastElement, XastParent>} */
+ const styleElements = new Map();
+
+ /** @type {Array<XastElement>} */
+ const elementsWithStyleAttributes = [];
+
+ /** @type {Set<string>} */
+ const tagsUsage = new Set();
+
+ /** @type {Set<string>} */
+ const idsUsage = new Set();
+
+ /** @type {Set<string>} */
+ const classesUsage = new Set();
+
let enableTagsUsage = true;
let enableIdsUsage = true;
let enableClassesUsage = true;
- // force to use usage data even if it unsafe (document contains <script> or on* attributes)
+
+ /**
+ * Force to use usage data even if it unsafe. For example, the document
+ * contains scripts or in attributes..
+ */
let forceUsageDeoptimized = false;
+
if (typeof usage === 'boolean') {
enableTagsUsage = usage;
enableIdsUsage = usage;
@@ -33,40 +54,17 @@ exports.fn = (_root, { usage, ...params }) => {
enableClassesUsage = usage.classes == null ? true : usage.classes;
forceUsageDeoptimized = usage.force == null ? false : usage.force;
}
- /**
- * @type {Array<XastElement>}
- */
- const styleElements = [];
- /**
- * @type {Array<XastElement>}
- */
- const elementsWithStyleAttributes = [];
+
let deoptimized = false;
- /**
- * @type {Set<string>}
- */
- const tagsUsage = new Set();
- /**
- * @type {Set<string>}
- */
- const idsUsage = new Set();
- /**
- * @type {Set<string>}
- */
- const classesUsage = new Set();
return {
element: {
- enter: (node) => {
+ enter: (node, parentNode) => {
// detect deoptimisations
- if (node.name === 'script') {
+ if (hasScripts(node)) {
deoptimized = true;
}
- for (const name of Object.keys(node.attributes)) {
- if (name.startsWith('on')) {
- deoptimized = true;
- }
- }
+
// collect tags, ids and classes usage
tagsUsage.add(node.name);
if (node.attributes.id != null) {
@@ -79,7 +77,7 @@ exports.fn = (_root, { usage, ...params }) => {
}
// collect style elements or elements with style attribute
if (node.name === 'style' && node.children.length !== 0) {
- styleElements.push(node);
+ styleElements.set(node, parentNode);
} else if (node.attributes.style != null) {
elementsWithStyleAttributes.push(node);
}
@@ -88,40 +86,44 @@ exports.fn = (_root, { usage, ...params }) => {
root: {
exit: () => {
- /**
- * @type {csso.Usage}
- */
+ /** @type {csso.Usage} */
const cssoUsage = {};
- if (deoptimized === false || forceUsageDeoptimized === true) {
- if (enableTagsUsage && tagsUsage.size !== 0) {
+ if (!deoptimized || forceUsageDeoptimized) {
+ if (enableTagsUsage) {
cssoUsage.tags = Array.from(tagsUsage);
}
- if (enableIdsUsage && idsUsage.size !== 0) {
+ if (enableIdsUsage) {
cssoUsage.ids = Array.from(idsUsage);
}
- if (enableClassesUsage && classesUsage.size !== 0) {
+ if (enableClassesUsage) {
cssoUsage.classes = Array.from(classesUsage);
}
}
// minify style elements
- for (const node of styleElements) {
+ for (const [styleNode, styleNodeParent] of styleElements.entries()) {
if (
- node.children[0].type === 'text' ||
- node.children[0].type === 'cdata'
+ styleNode.children[0].type === 'text' ||
+ styleNode.children[0].type === 'cdata'
) {
- const cssText = node.children[0].value;
+ const cssText = styleNode.children[0].value;
const minified = csso.minify(cssText, {
...params,
usage: cssoUsage,
}).css;
+
+ if (minified.length === 0) {
+ detachNodeFromParent(styleNode, styleNodeParent);
+ continue;
+ }
+
// preserve cdata if necessary
// TODO split cdata -> text optimisation into separate plugin
if (cssText.indexOf('>') >= 0 || cssText.indexOf('<') >= 0) {
- node.children[0].type = 'cdata';
- node.children[0].value = minified;
+ styleNode.children[0].type = 'cdata';
+ styleNode.children[0].value = minified;
} else {
- node.children[0].type = 'text';
- node.children[0].value = minified;
+ styleNode.children[0].type = 'text';
+ styleNode.children[0].value = minified;
}
}
}
diff --git a/node_modules/svgo/plugins/moveGroupAttrsToElems.js b/node_modules/svgo/plugins/moveGroupAttrsToElems.js
index 60b55676..acadd894 100644
--- a/node_modules/svgo/plugins/moveGroupAttrsToElems.js
+++ b/node_modules/svgo/plugins/moveGroupAttrsToElems.js
@@ -1,6 +1,7 @@
'use strict';
const { pathElems, referencesProps } = require('./_collections.js');
+const { includesUrlReference } = require('../lib/svgo/tools.js');
exports.name = 'moveGroupAttrsToElems';
exports.description = 'moves some group attributes to the content elements';
@@ -36,7 +37,7 @@ exports.fn = () => {
node.attributes.transform != null &&
Object.entries(node.attributes).some(
([name, value]) =>
- referencesProps.includes(name) && value.includes('url(')
+ referencesProps.includes(name) && includesUrlReference(value)
) === false &&
node.children.every(
(child) =>
diff --git a/node_modules/svgo/plugins/plugins-types.ts b/node_modules/svgo/plugins/plugins-types.d.ts
index a4cca210..7cd56706 100644
--- a/node_modules/svgo/plugins/plugins-types.ts
+++ b/node_modules/svgo/plugins/plugins-types.d.ts
@@ -14,8 +14,8 @@ type DefaultPlugins = {
cleanupIds: {
remove?: boolean;
minify?: boolean;
- preserve?: Array<string>;
- preservePrefixes?: Array<string>;
+ preserve?: string[];
+ preservePrefixes?: string[];
force?: boolean;
};
cleanupNumericValues: {
@@ -42,6 +42,7 @@ type DefaultPlugins = {
};
straightCurves?: boolean;
lineShorthands?: boolean;
+ convertToZ?: boolean;
curveSmoothShorthands?: boolean;
floatPrecision?: number | false;
transformPrecision?: number;
@@ -73,10 +74,28 @@ type DefaultPlugins = {
};
mergeStyles: void;
inlineStyles: {
+ /**
+ * Inlines selectors that match once only.
+ *
+ * @default true
+ */
onlyMatchedOnce?: boolean;
+ /**
+ * Clean up matched selectors. Unused selects are left as-is.
+ *
+ * @default true
+ */
removeMatchedSelectors?: boolean;
- useMqs?: Array<string>;
- usePseudos?: Array<string>;
+ /**
+ * Media queries to use. An empty string indicates all selectors outside of
+ * media queries.
+ */
+ useMqs?: string[];
+ /**
+ * Pseudo-classes and elements to use. An empty string indicates all
+ * all non-pseudo-classes and elements.
+ */
+ usePseudos?: string[];
};
mergePaths: {
force?: boolean;
@@ -89,13 +108,13 @@ type DefaultPlugins = {
* Disable or enable a structure optimisations.
* @default true
*/
- restructure?: boolean | undefined;
+ restructure?: boolean;
/**
- * Enables merging of @media rules with the same media query by splitted by other rules.
+ * Enables merging of @media rules with the same media query split by other rules.
* The optimisation is unsafe in general, but should work fine in most cases. Use it on your own risk.
* @default false
*/
- forceMediaMerge?: boolean | undefined;
+ forceMediaMerge?: boolean;
/**
* Specify what comments to leave:
* - 'exclamation' or true – leave all exclamation comments
@@ -103,7 +122,7 @@ type DefaultPlugins = {
* - false – remove all comments
* @default true
*/
- comments?: string | boolean | undefined;
+ comments?: string | boolean;
/**
* Advanced optimizations
*/
@@ -119,13 +138,15 @@ type DefaultPlugins = {
moveElemsAttrsToGroup: void;
moveGroupAttrsToElems: void;
- removeComments: void;
+ removeComments: {
+ preservePatterns: Array<RegExp | string> | false;
+ };
removeDesc: {
removeAny?: boolean;
};
removeDoctype: void;
removeEditorsNSData: {
- additionalNamespaces?: Array<string>;
+ additionalNamespaces?: string[];
};
removeEmptyAttrs: void;
removeEmptyContainers: void;
@@ -158,6 +179,12 @@ type DefaultPlugins = {
unknownContent?: boolean;
unknownAttrs?: boolean;
defaultAttrs?: boolean;
+ /**
+ * If to remove XML declarations that are assigned their default value. XML
+ * declarations are the properties in the `<?xml … ?>` block at the top of
+ * the document.
+ */
+ defaultMarkupDeclarations?: boolean;
uselessOverrides?: boolean;
keepDataAttrs?: boolean;
keepAriaAttrs?: boolean;
@@ -173,7 +200,7 @@ type DefaultPlugins = {
removeViewBox: void;
removeXMLProcInst: void;
sortAttrs: {
- order?: Array<string>;
+ order?: string[];
xmlnsOrder?: 'front' | 'alphabetical';
};
sortDefsChildren: void;
@@ -202,6 +229,7 @@ export type BuiltinsWithOptionalParams = DefaultPlugins & {
defaultPx?: boolean;
convertToPx?: boolean;
};
+ convertOneStopGradients: void;
convertStyleToAttrs: {
keepImportant?: boolean;
};
@@ -219,6 +247,16 @@ export type BuiltinsWithOptionalParams = DefaultPlugins & {
removeRasterImages: void;
removeScriptElement: void;
removeStyleElement: void;
+ removeXlink: {
+ /**
+ * By default this plugin ignores legacy elements that were deprecated or
+ * removed in SVG 2. Set to true to force performing operations on those
+ * too.
+ *
+ * @default false
+ */
+ includeLegacy: boolean
+ };
removeXMLNS: void;
reusePaths: void;
};
@@ -230,17 +268,17 @@ export type BuiltinsWithRequiredParams = {
};
addClassesToSVGElement: {
className?: string;
- classNames?: Array<string>;
+ classNames?: string[];
};
removeAttributesBySelector: any;
removeAttrs: {
elemSeparator?: string;
preserveCurrentColor?: boolean;
- attrs: string | Array<string>;
+ attrs: string | string[];
};
removeElementsByAttr: {
- id?: string | Array<string>;
- class?: string | Array<string>;
+ id?: string | string[];
+ class?: string | string[];
};
};
diff --git a/node_modules/svgo/plugins/prefixIds.js b/node_modules/svgo/plugins/prefixIds.js
index 66762936..2f23971e 100644
--- a/node_modules/svgo/plugins/prefixIds.js
+++ b/node_modules/svgo/plugins/prefixIds.js
@@ -1,5 +1,10 @@
'use strict';
+/**
+ * @typedef {import('../lib/types.js').PluginInfo} PluginInfo
+ * @typedef {import('../lib/types').XastElement} XastElement
+ */
+
const csstree = require('css-tree');
const { referencesProps } = require('./_collections.js');
@@ -12,7 +17,7 @@ exports.description = 'prefix IDs';
*/
const getBasename = (path) => {
// extract everything after latest slash or backslash
- const matched = path.match(/[/\\]?([^/\\]+)$/);
+ const matched = /[/\\]?([^/\\]+)$/.exec(path);
if (matched) {
return matched[1];
}
@@ -41,57 +46,100 @@ const unquote = (string) => {
};
/**
- * prefix an ID
- * @type {(prefix: string, name: string) => string}
+ * Prefix the given string, unless it already starts with the generated prefix.
+ *
+ * @param {(id: string) => string} prefixGenerator Function to generate a prefix.
+ * @param {string} body An arbitrary string.
+ * @returns {string} The given string with a prefix prepended to it.
*/
-const prefixId = (prefix, value) => {
- if (value.startsWith(prefix)) {
- return value;
+const prefixId = (prefixGenerator, body) => {
+ const prefix = prefixGenerator(body);
+ if (body.startsWith(prefix)) {
+ return body;
}
- return prefix + value;
+ return prefix + body;
};
/**
- * prefix an #ID
- * @type {(prefix: string, name: string) => string | null}
+ * Insert the prefix in a reference string. A reference string is already
+ * prefixed with #, so the prefix is inserted after the first character.
+ *
+ * @param {(id: string) => string} prefixGenerator Function to generate a prefix.
+ * @param {string} reference An arbitrary string, should start with "#".
+ * @returns {?string} The given string with a prefix inserted, or null if the string did not start with "#".
*/
-const prefixReference = (prefix, value) => {
- if (value.startsWith('#')) {
- return '#' + prefixId(prefix, value.slice(1));
+const prefixReference = (prefixGenerator, reference) => {
+ if (reference.startsWith('#')) {
+ return '#' + prefixId(prefixGenerator, reference.slice(1));
}
return null;
};
-/** @type {(value: any) => any} */
-const toAny = (value) => value;
+/**
+ * Generates a prefix for the given string.
+ *
+ * @param {string} body An arbitrary string.
+ * @param {XastElement} node XML node that the identifier belongs to.
+ * @param {PluginInfo} info
+ * @param {((node: XastElement, info: PluginInfo) => string)|string|boolean|undefined} prefixGenerator Some way of obtaining a prefix.
+ * @param {string} delim Content to insert between the prefix and original value.
+ * @param {Map<string, string>} history Map of previously generated prefixes to IDs.
+ * @returns {string} A generated prefix.
+ */
+const generatePrefix = (body, node, info, prefixGenerator, delim, history) => {
+ if (typeof prefixGenerator === 'function') {
+ let prefix = history.get(body);
+
+ if (prefix != null) {
+ return prefix;
+ }
+
+ prefix = prefixGenerator(node, info) + delim;
+ history.set(body, prefix);
+ return prefix;
+ }
+
+ if (typeof prefixGenerator === 'string') {
+ return prefixGenerator + delim;
+ }
+
+ if (prefixGenerator === false) {
+ return '';
+ }
+
+ if (info.path != null && info.path.length > 0) {
+ return escapeIdentifierName(getBasename(info.path)) + delim;
+ }
+
+ return 'prefix' + delim;
+};
/**
* Prefixes identifiers
*
* @author strarsis <strarsis@gmail.com>
- *
* @type {import('./plugins-types').Plugin<'prefixIds'>}
*/
exports.fn = (_root, params, info) => {
- const { delim = '__', prefixIds = true, prefixClassNames = true } = params;
+ const {
+ delim = '__',
+ prefix,
+ prefixIds = true,
+ prefixClassNames = true,
+ } = params;
+
+ /** @type {Map<string, string>} */
+ const prefixMap = new Map();
return {
element: {
enter: (node) => {
/**
- * prefix, from file name or option
- * @type {string}
+ * @param {string} id A node identifier or class.
+ * @returns {string} Given string with a prefix inserted, or null if the string did not start with "#".
*/
- let prefix = 'prefix' + delim;
- if (typeof params.prefix === 'function') {
- prefix = params.prefix(node, info) + delim;
- } else if (typeof params.prefix === 'string') {
- prefix = params.prefix + delim;
- } else if (params.prefix === false) {
- prefix = '';
- } else if (info.path != null && info.path.length > 0) {
- prefix = escapeIdentifierName(getBasename(info.path)) + delim;
- }
+ const prefixGenerator = (id) =>
+ generatePrefix(id, node, info, prefix, delim, prefixMap);
// prefix id/class selectors and url() references in styles
if (node.name === 'style') {
@@ -100,57 +148,48 @@ exports.fn = (_root, params, info) => {
return;
}
- // parse styles
- let cssText = '';
- if (
- node.children[0].type === 'text' ||
- node.children[0].type === 'cdata'
- ) {
- cssText = node.children[0].value;
- }
- /**
- * @type {null | csstree.CssNode}
- */
- let cssAst = null;
- try {
- cssAst = csstree.parse(cssText, {
- parseValue: true,
- parseCustomProperty: false,
- });
- } catch {
- return;
- }
+ for (const child of node.children) {
+ if (child.type !== 'text' && child.type !== 'cdata') {
+ continue;
+ }
- csstree.walk(cssAst, (node) => {
- // #ID, .class selectors
- if (
- (prefixIds && node.type === 'IdSelector') ||
- (prefixClassNames && node.type === 'ClassSelector')
- ) {
- node.name = prefixId(prefix, node.name);
+ const cssText = child.value;
+ /** @type {?csstree.CssNode} */
+ let cssAst = null;
+ try {
+ cssAst = csstree.parse(cssText, {
+ parseValue: true,
+ parseCustomProperty: false,
+ });
+ } catch {
return;
}
- // url(...) references
- // csstree v2 changed this type
- if (node.type === 'Url' && toAny(node.value).length > 0) {
- const prefixed = prefixReference(
- prefix,
- unquote(toAny(node.value))
- );
- if (prefixed != null) {
- toAny(node).value = prefixed;
+
+ csstree.walk(cssAst, (node) => {
+ if (
+ (prefixIds && node.type === 'IdSelector') ||
+ (prefixClassNames && node.type === 'ClassSelector')
+ ) {
+ node.name = prefixId(prefixGenerator, node.name);
+ return;
}
- }
- });
+ // @ts-ignore csstree v2 changed this type
+ if (node.type === 'Url' && node.value.length > 0) {
+ const prefixed = prefixReference(
+ prefixGenerator,
+ // @ts-ignore
+ unquote(node.value)
+ );
+ if (prefixed != null) {
+ // @ts-ignore
+ node.value = prefixed;
+ }
+ }
+ });
- // update styles
- if (
- node.children[0].type === 'text' ||
- node.children[0].type === 'cdata'
- ) {
- node.children[0].value = csstree.generate(cssAst);
+ child.value = csstree.generate(cssAst);
+ return;
}
- return;
}
// prefix an ID attribute value
@@ -159,7 +198,7 @@ exports.fn = (_root, params, info) => {
node.attributes.id != null &&
node.attributes.id.length !== 0
) {
- node.attributes.id = prefixId(prefix, node.attributes.id);
+ node.attributes.id = prefixId(prefixGenerator, node.attributes.id);
}
// prefix a class attribute value
@@ -170,7 +209,7 @@ exports.fn = (_root, params, info) => {
) {
node.attributes.class = node.attributes.class
.split(/\s+/)
- .map((name) => prefixId(prefix, name))
+ .map((name) => prefixId(prefixGenerator, name))
.join(' ');
}
@@ -181,23 +220,26 @@ exports.fn = (_root, params, info) => {
node.attributes[name] != null &&
node.attributes[name].length !== 0
) {
- const prefixed = prefixReference(prefix, node.attributes[name]);
+ const prefixed = prefixReference(
+ prefixGenerator,
+ node.attributes[name]
+ );
if (prefixed != null) {
node.attributes[name] = prefixed;
}
}
}
- // prefix an URL attribute value
+ // prefix a URL attribute value
for (const name of referencesProps) {
if (
node.attributes[name] != null &&
node.attributes[name].length !== 0
) {
node.attributes[name] = node.attributes[name].replace(
- /url\((.*?)\)/gi,
- (match, url) => {
- const prefixed = prefixReference(prefix, url);
+ /\burl\((["'])?(#.+?)\1\)/gi,
+ (match, _, url) => {
+ const prefixed = prefixReference(prefixGenerator, url);
if (prefixed == null) {
return match;
}
@@ -216,7 +258,7 @@ exports.fn = (_root, params, info) => {
const parts = node.attributes[name].split(/\s*;\s+/).map((val) => {
if (val.endsWith('.end') || val.endsWith('.start')) {
const [id, postfix] = val.split('.');
- return `${prefixId(prefix, id)}.${postfix}`;
+ return `${prefixId(prefixGenerator, id)}.${postfix}`;
}
return val;
});
diff --git a/node_modules/svgo/plugins/removeAttributesBySelector.js b/node_modules/svgo/plugins/removeAttributesBySelector.js
index 09db8a8d..12630c2a 100644
--- a/node_modules/svgo/plugins/removeAttributesBySelector.js
+++ b/node_modules/svgo/plugins/removeAttributesBySelector.js
@@ -69,7 +69,7 @@ exports.description =
* ↓
* <rect x="0" y="0" width="100" height="100"/>
*
- * @link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|MDN CSS Selectors
+ * @link https://developer.mozilla.org/docs/Web/CSS/CSS_Selectors|MDN CSS Selectors
*
* @author Bradley Mease
*
diff --git a/node_modules/svgo/plugins/removeAttrs.js b/node_modules/svgo/plugins/removeAttrs.js
index c289d8ca..dc1224bd 100644
--- a/node_modules/svgo/plugins/removeAttrs.js
+++ b/node_modules/svgo/plugins/removeAttrs.js
@@ -104,13 +104,11 @@ exports.fn = (root, params) => {
enter: (node) => {
for (let pattern of attrs) {
// if no element separators (:), assume it's attribute name, and apply to all elements *regardless of value*
- if (pattern.includes(elemSeparator) === false) {
- pattern = ['.*', elemSeparator, pattern, elemSeparator, '.*'].join(
- ''
- );
+ if (!pattern.includes(elemSeparator)) {
+ pattern = ['.*', pattern, '.*'].join(elemSeparator);
// if only 1 separator, assume it's element and attribute name, and apply regardless of attribute value
} else if (pattern.split(elemSeparator).length < 3) {
- pattern = [pattern, elemSeparator, '.*'].join('');
+ pattern = [pattern, '.*'].join(elemSeparator);
}
// create regexps for element, attribute name, and attribute value
diff --git a/node_modules/svgo/plugins/removeComments.js b/node_modules/svgo/plugins/removeComments.js
index 70e46271..67f8efd1 100644
--- a/node_modules/svgo/plugins/removeComments.js
+++ b/node_modules/svgo/plugins/removeComments.js
@@ -6,6 +6,12 @@ exports.name = 'removeComments';
exports.description = 'removes comments';
/**
+ * If a comment matches one of the following patterns, it will be
+ * preserved by default. Particularly for copyright/license information.
+ */
+const DEFAULT_PRESERVE_PATTERNS = [/^!/];
+
+/**
* Remove comments.
*
* @example
@@ -16,13 +22,29 @@ exports.description = 'removes comments';
*
* @type {import('./plugins-types').Plugin<'removeComments'>}
*/
-exports.fn = () => {
+exports.fn = (_root, params) => {
+ const { preservePatterns = DEFAULT_PRESERVE_PATTERNS } = params;
+
return {
comment: {
enter: (node, parentNode) => {
- if (node.value.charAt(0) !== '!') {
- detachNodeFromParent(node, parentNode);
+ if (preservePatterns) {
+ if (!Array.isArray(preservePatterns)) {
+ throw Error(
+ `Expected array in removeComments preservePatterns parameter but received ${preservePatterns}`
+ );
+ }
+
+ const matches = preservePatterns.some((pattern) => {
+ return new RegExp(pattern).test(node.value);
+ });
+
+ if (matches) {
+ return;
+ }
}
+
+ detachNodeFromParent(node, parentNode);
},
},
};
diff --git a/node_modules/svgo/plugins/removeDesc.js b/node_modules/svgo/plugins/removeDesc.js
index 5d9e57ae..f766350e 100644
--- a/node_modules/svgo/plugins/removeDesc.js
+++ b/node_modules/svgo/plugins/removeDesc.js
@@ -12,14 +12,14 @@ const standardDescs = /^(Created with|Created using)/;
* Removes only standard editors content or empty elements 'cause it can be used for accessibility.
* Enable parameter 'removeAny' to remove any description.
*
- * https://developer.mozilla.org/en-US/docs/Web/SVG/Element/desc
+ * https://developer.mozilla.org/docs/Web/SVG/Element/desc
*
* @author Daniel Wabyick
*
* @type {import('./plugins-types').Plugin<'removeDesc'>}
*/
exports.fn = (root, params) => {
- const { removeAny = true } = params;
+ const { removeAny = false } = params;
return {
element: {
enter: (node, parentNode) => {
diff --git a/node_modules/svgo/plugins/removeEditorsNSData.js b/node_modules/svgo/plugins/removeEditorsNSData.js
index cf8016cd..e85bef89 100644
--- a/node_modules/svgo/plugins/removeEditorsNSData.js
+++ b/node_modules/svgo/plugins/removeEditorsNSData.js
@@ -30,7 +30,7 @@ exports.fn = (_root, params) => {
return {
element: {
enter: (node, parentNode) => {
- // collect namespace aliases from svg element
+ // collect namespace prefixes from svg element
if (node.name === 'svg') {
for (const [name, value] of Object.entries(node.attributes)) {
if (name.startsWith('xmlns:') && namespaces.includes(value)) {
diff --git a/node_modules/svgo/plugins/removeEmptyContainers.js b/node_modules/svgo/plugins/removeEmptyContainers.js
index d803fc9c..bff3ab9c 100644
--- a/node_modules/svgo/plugins/removeEmptyContainers.js
+++ b/node_modules/svgo/plugins/removeEmptyContainers.js
@@ -49,6 +49,9 @@ exports.fn = () => {
if (node.name === 'mask' && node.attributes.id != null) {
return;
}
+ if (parentNode.type === 'element' && parentNode.name === 'switch') {
+ return;
+ }
detachNodeFromParent(node, parentNode);
},
},
diff --git a/node_modules/svgo/plugins/removeHiddenElems.js b/node_modules/svgo/plugins/removeHiddenElems.js
index 5c1a0135..6ed2750b 100644
--- a/node_modules/svgo/plugins/removeHiddenElems.js
+++ b/node_modules/svgo/plugins/removeHiddenElems.js
@@ -1,5 +1,12 @@
'use strict';
+/**
+ * @typedef {import('../lib/types').XastChild} XastChild
+ * @typedef {import('../lib/types').XastElement} XastElement
+ * @typedef {import('../lib/types').XastParent} XastParent
+ */
+
+const { elemsGroups } = require('./_collections.js');
const {
visit,
visitSkip,
@@ -8,6 +15,9 @@ const {
} = require('../lib/xast.js');
const { collectStylesheet, computeStyle } = require('../lib/style.js');
const { parsePathData } = require('../lib/path.js');
+const { hasScripts, findReferences } = require('../lib/svgo/tools.js');
+
+const nonRendering = elemsGroups.nonRendering;
exports.name = 'removeHiddenElems';
exports.description =
@@ -50,11 +60,67 @@ exports.fn = (root, params) => {
} = params;
const stylesheet = collectStylesheet(root);
+ /**
+ * Skip non-rendered nodes initially, and only detach if they have no ID, or
+ * their ID is not referenced by another node.
+ *
+ * @type {Map<XastElement, XastParent>}
+ */
+ const nonRenderedNodes = new Map();
+
+ /**
+ * IDs for removed hidden definitions.
+ *
+ * @type {Set<string>}
+ */
+ const removedDefIds = new Set();
+
+ /**
+ * @type {Map<XastElement, XastParent>}
+ */
+ const allDefs = new Map();
+
+ /** @type {Set<string>} */
+ const allReferences = new Set();
+
+ /**
+ * @type {Map<string, Array<{ node: XastElement, parentNode: XastParent }>>}
+ */
+ const referencesById = new Map();
+
+ /**
+ * If styles are present, we can't be sure if a definition is unused or not
+ */
+ let deoptimized = false;
+
+ /**
+ * @param {XastChild} node
+ * @param {XastParent} parentNode
+ */
+ function removeElement(node, parentNode) {
+ if (
+ node.type === 'element' &&
+ node.attributes.id != null &&
+ parentNode.type === 'element' &&
+ parentNode.name === 'defs'
+ ) {
+ removedDefIds.add(node.attributes.id);
+ }
+
+ detachNodeFromParent(node, parentNode);
+ }
+
visit(root, {
element: {
enter: (node, parentNode) => {
- // transparent element inside clipPath still affect clipped elements
- if (node.name === 'clipPath') {
+ // transparent non-rendering elements still apply where referenced
+ if (nonRendering.includes(node.name)) {
+ if (node.attributes.id == null) {
+ detachNodeFromParent(node, parentNode);
+ return visitSkip;
+ }
+
+ nonRenderedNodes.set(node, parentNode);
return visitSkip;
}
const computedStyle = computeStyle(stylesheet, node);
@@ -67,8 +133,7 @@ exports.fn = (root, params) => {
computedStyle.opacity.type === 'static' &&
computedStyle.opacity.value === '0'
) {
- detachNodeFromParent(node, parentNode);
- return;
+ removeElement(node, parentNode);
}
},
},
@@ -77,6 +142,33 @@ exports.fn = (root, params) => {
return {
element: {
enter: (node, parentNode) => {
+ if (
+ (node.name === 'style' && node.children.length !== 0) ||
+ hasScripts(node)
+ ) {
+ deoptimized = true;
+ return;
+ }
+
+ if (node.name === 'defs') {
+ allDefs.set(node, parentNode);
+ }
+
+ if (node.name === 'use') {
+ for (const attr of Object.keys(node.attributes)) {
+ if (attr !== 'href' && !attr.endsWith(':href')) continue;
+ const value = node.attributes[attr];
+ const id = value.slice(1);
+
+ let refs = referencesById.get(id);
+ if (!refs) {
+ refs = [];
+ referencesById.set(id, refs);
+ }
+ refs.push({ node, parentNode });
+ }
+ }
+
// Removes hidden elements
// https://www.w3schools.com/cssref/pr_class_visibility.asp
const computedStyle = computeStyle(stylesheet, node);
@@ -88,7 +180,7 @@ exports.fn = (root, params) => {
// keep if any descendant enables visibility
querySelector(node, '[visibility=visible]') == null
) {
- detachNodeFromParent(node, parentNode);
+ removeElement(node, parentNode);
return;
}
@@ -105,7 +197,7 @@ exports.fn = (root, params) => {
// markers with display: none still rendered
node.name !== 'marker'
) {
- detachNodeFromParent(node, parentNode);
+ removeElement(node, parentNode);
return;
}
@@ -121,7 +213,7 @@ exports.fn = (root, params) => {
node.children.length === 0 &&
node.attributes.r === '0'
) {
- detachNodeFromParent(node, parentNode);
+ removeElement(node, parentNode);
return;
}
@@ -137,7 +229,7 @@ exports.fn = (root, params) => {
node.children.length === 0 &&
node.attributes.rx === '0'
) {
- detachNodeFromParent(node, parentNode);
+ removeElement(node, parentNode);
return;
}
@@ -153,7 +245,7 @@ exports.fn = (root, params) => {
node.children.length === 0 &&
node.attributes.ry === '0'
) {
- detachNodeFromParent(node, parentNode);
+ removeElement(node, parentNode);
return;
}
@@ -169,7 +261,7 @@ exports.fn = (root, params) => {
node.children.length === 0 &&
node.attributes.width === '0'
) {
- detachNodeFromParent(node, parentNode);
+ removeElement(node, parentNode);
return;
}
@@ -186,7 +278,7 @@ exports.fn = (root, params) => {
node.children.length === 0 &&
node.attributes.height === '0'
) {
- detachNodeFromParent(node, parentNode);
+ removeElement(node, parentNode);
return;
}
@@ -201,7 +293,7 @@ exports.fn = (root, params) => {
node.name === 'pattern' &&
node.attributes.width === '0'
) {
- detachNodeFromParent(node, parentNode);
+ removeElement(node, parentNode);
return;
}
@@ -216,7 +308,7 @@ exports.fn = (root, params) => {
node.name === 'pattern' &&
node.attributes.height === '0'
) {
- detachNodeFromParent(node, parentNode);
+ removeElement(node, parentNode);
return;
}
@@ -231,7 +323,7 @@ exports.fn = (root, params) => {
node.name === 'image' &&
node.attributes.width === '0'
) {
- detachNodeFromParent(node, parentNode);
+ removeElement(node, parentNode);
return;
}
@@ -246,7 +338,7 @@ exports.fn = (root, params) => {
node.name === 'image' &&
node.attributes.height === '0'
) {
- detachNodeFromParent(node, parentNode);
+ removeElement(node, parentNode);
return;
}
@@ -257,12 +349,12 @@ exports.fn = (root, params) => {
// <path d=""/>
if (pathEmptyD && node.name === 'path') {
if (node.attributes.d == null) {
- detachNodeFromParent(node, parentNode);
+ removeElement(node, parentNode);
return;
}
const pathData = parsePathData(node.attributes.d);
if (pathData.length === 0) {
- detachNodeFromParent(node, parentNode);
+ removeElement(node, parentNode);
return;
}
// keep single point paths for markers
@@ -271,10 +363,9 @@ exports.fn = (root, params) => {
computedStyle['marker-start'] == null &&
computedStyle['marker-end'] == null
) {
- detachNodeFromParent(node, parentNode);
+ removeElement(node, parentNode);
return;
}
- return;
}
// Polyline with empty points
@@ -287,7 +378,7 @@ exports.fn = (root, params) => {
node.name === 'polyline' &&
node.attributes.points == null
) {
- detachNodeFromParent(node, parentNode);
+ removeElement(node, parentNode);
return;
}
@@ -301,9 +392,48 @@ exports.fn = (root, params) => {
node.name === 'polygon' &&
node.attributes.points == null
) {
- detachNodeFromParent(node, parentNode);
+ removeElement(node, parentNode);
return;
}
+
+ for (const [name, value] of Object.entries(node.attributes)) {
+ const ids = findReferences(name, value);
+
+ for (const id of ids) {
+ allReferences.add(id);
+ }
+ }
+ },
+ },
+ root: {
+ exit: () => {
+ for (const id of removedDefIds) {
+ const refs = referencesById.get(id);
+ if (refs) {
+ for (const { node, parentNode } of refs) {
+ detachNodeFromParent(node, parentNode);
+ }
+ }
+ }
+
+ if (!deoptimized) {
+ for (const [
+ nonRenderedNode,
+ nonRenderedParent,
+ ] of nonRenderedNodes.entries()) {
+ const id = nonRenderedNode.attributes.id;
+
+ if (!allReferences.has(id)) {
+ detachNodeFromParent(nonRenderedNode, nonRenderedParent);
+ }
+ }
+ }
+
+ for (const [node, parentNode] of allDefs.entries()) {
+ if (node.children.length === 0) {
+ detachNodeFromParent(node, parentNode);
+ }
+ }
},
},
};
diff --git a/node_modules/svgo/plugins/removeOffCanvasPaths.js b/node_modules/svgo/plugins/removeOffCanvasPaths.js
index 7a3d131d..8afd1e9a 100644
--- a/node_modules/svgo/plugins/removeOffCanvasPaths.js
+++ b/node_modules/svgo/plugins/removeOffCanvasPaths.js
@@ -21,7 +21,7 @@ exports.description =
*/
exports.fn = () => {
/**
- * @type {null | {
+ * @type {?{
* top: number,
* right: number,
* bottom: number,
diff --git a/node_modules/svgo/plugins/removeRasterImages.js b/node_modules/svgo/plugins/removeRasterImages.js
index 8a363531..1a4a4a12 100644
--- a/node_modules/svgo/plugins/removeRasterImages.js
+++ b/node_modules/svgo/plugins/removeRasterImages.js
@@ -21,7 +21,7 @@ exports.fn = () => {
if (
node.name === 'image' &&
node.attributes['xlink:href'] != null &&
- /(\.|image\/)(jpg|png|gif)/.test(node.attributes['xlink:href'])
+ /(\.|image\/)(jpe?g|png|gif)/.test(node.attributes['xlink:href'])
) {
detachNodeFromParent(node, parentNode);
}
diff --git a/node_modules/svgo/plugins/removeScriptElement.js b/node_modules/svgo/plugins/removeScriptElement.js
index 0e42de4c..80fcea3b 100644
--- a/node_modules/svgo/plugins/removeScriptElement.js
+++ b/node_modules/svgo/plugins/removeScriptElement.js
@@ -1,17 +1,26 @@
'use strict';
const { detachNodeFromParent } = require('../lib/xast.js');
+const { attrsGroups } = require('./_collections.js');
exports.name = 'removeScriptElement';
-exports.description = 'removes <script> elements (disabled by default)';
+exports.description = 'removes scripts (disabled by default)';
+
+/** Union of all event attributes. */
+const eventAttrs = [
+ ...attrsGroups.animationEvent,
+ ...attrsGroups.documentEvent,
+ ...attrsGroups.documentElementEvent,
+ ...attrsGroups.globalEvent,
+ ...attrsGroups.graphicalEvent,
+];
/**
- * Remove <script>.
+ * Remove scripts.
*
* https://www.w3.org/TR/SVG11/script.html
*
* @author Patrick Klingemann
- *
* @type {import('./plugins-types').Plugin<'removeScriptElement'>}
*/
exports.fn = () => {
@@ -20,6 +29,40 @@ exports.fn = () => {
enter: (node, parentNode) => {
if (node.name === 'script') {
detachNodeFromParent(node, parentNode);
+ return;
+ }
+
+ for (const attr of eventAttrs) {
+ if (node.attributes[attr] != null) {
+ delete node.attributes[attr];
+ }
+ }
+ },
+ exit: (node, parentNode) => {
+ if (node.name !== 'a') {
+ return;
+ }
+
+ for (const attr of Object.keys(node.attributes)) {
+ if (attr === 'href' || attr.endsWith(':href')) {
+ if (
+ node.attributes[attr] == null ||
+ !node.attributes[attr].trimStart().startsWith('javascript:')
+ ) {
+ continue;
+ }
+
+ 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/node_modules/svgo/plugins/removeTitle.js b/node_modules/svgo/plugins/removeTitle.js
index ee136cd5..906dd3c2 100644
--- a/node_modules/svgo/plugins/removeTitle.js
+++ b/node_modules/svgo/plugins/removeTitle.js
@@ -8,7 +8,7 @@ exports.description = 'removes <title>';
/**
* Remove <title>.
*
- * https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title
+ * https://developer.mozilla.org/docs/Web/SVG/Element/title
*
* @author Igor Kalashnikov
*
diff --git a/node_modules/svgo/plugins/removeUnknownsAndDefaults.js b/node_modules/svgo/plugins/removeUnknownsAndDefaults.js
index c67845cd..7462e8dc 100644
--- a/node_modules/svgo/plugins/removeUnknownsAndDefaults.js
+++ b/node_modules/svgo/plugins/removeUnknownsAndDefaults.js
@@ -99,6 +99,7 @@ exports.fn = (root, params) => {
unknownContent = true,
unknownAttrs = true,
defaultAttrs = true,
+ defaultMarkupDeclarations = true,
uselessOverrides = true,
keepDataAttrs = true,
keepAriaAttrs = true,
@@ -107,6 +108,13 @@ exports.fn = (root, params) => {
const stylesheet = collectStylesheet(root);
return {
+ instruction: {
+ enter: (node) => {
+ if (defaultMarkupDeclarations) {
+ node.value = node.value.replace(/\s*standalone\s*=\s*(["'])no\1/, '');
+ }
+ },
+ },
element: {
enter: (node, parentNode) => {
// skip namespaced elements
@@ -182,16 +190,12 @@ exports.fn = (root, params) => {
attributesDefaults.get(name) === value
) {
// keep defaults if parent has own or inherited style
- if (
- computedParentStyle == null ||
- computedParentStyle[name] == null
- ) {
+ if (computedParentStyle?.[name] == null) {
delete node.attributes[name];
}
}
if (uselessOverrides && node.attributes.id == null) {
- const style =
- computedParentStyle == null ? null : computedParentStyle[name];
+ const style = computedParentStyle?.[name];
if (
presentationNonInheritableGroupAttrs.includes(name) === false &&
style != null &&
diff --git a/node_modules/svgo/plugins/removeUselessStrokeAndFill.js b/node_modules/svgo/plugins/removeUselessStrokeAndFill.js
index 1dff61cc..f5f99855 100644
--- a/node_modules/svgo/plugins/removeUselessStrokeAndFill.js
+++ b/node_modules/svgo/plugins/removeUselessStrokeAndFill.js
@@ -2,6 +2,7 @@
const { visit, visitSkip, detachNodeFromParent } = require('../lib/xast.js');
const { collectStylesheet, computeStyle } = require('../lib/style.js');
+const { hasScripts } = require('../lib/svgo/tools.js');
const { elemsGroups } = require('./_collections.js');
exports.name = 'removeUselessStrokeAndFill';
@@ -26,7 +27,7 @@ exports.fn = (root, params) => {
visit(root, {
element: {
enter: (node) => {
- if (node.name === 'style' || node.name === 'script') {
+ if (node.name === 'style' || hasScripts(node)) {
hasStyleOrScript = true;
}
},
diff --git a/node_modules/svgo/plugins/removeXMLNS.js b/node_modules/svgo/plugins/removeXMLNS.js
index 6312629d..b7ec69a1 100644
--- a/node_modules/svgo/plugins/removeXMLNS.js
+++ b/node_modules/svgo/plugins/removeXMLNS.js
@@ -22,7 +22,6 @@ exports.fn = () => {
enter: (node) => {
if (node.name === 'svg') {
delete node.attributes.xmlns;
- delete node.attributes['xmlns:xlink'];
}
},
},
diff --git a/node_modules/svgo/plugins/removeXlink.js b/node_modules/svgo/plugins/removeXlink.js
new file mode 100644
index 00000000..d341b673
--- /dev/null
+++ b/node_modules/svgo/plugins/removeXlink.js
@@ -0,0 +1,226 @@
+'use strict';
+
+const { elems } = require('./_collections');
+
+/**
+ * @typedef {import('../lib/types').XastElement} XastElement
+ */
+
+exports.name = 'removeXlink';
+exports.description =
+ 'remove xlink namespace and replaces attributes with the SVG 2 equivalent where applicable';
+
+/** URI indicating the Xlink namespace. */
+const XLINK_NAMESPACE = 'http://www.w3.org/1999/xlink';
+
+/**
+ * Map of `xlink:show` values to the SVG 2 `target` attribute values.
+ *
+ * @type {Record<string, string>}
+ * @see https://developer.mozilla.org/docs/Web/SVG/Attribute/xlink:show#usage_notes
+ */
+const SHOW_TO_TARGET = {
+ new: '_blank',
+ replace: '_self',
+};
+
+/**
+ * Elements that use xlink:href, but were deprecated in SVG 2 and therefore
+ * don't support the SVG 2 href attribute.
+ *
+ * @type {string[]}
+ * @see https://developer.mozilla.org/docs/Web/SVG/Attribute/xlink:href
+ * @see https://developer.mozilla.org/docs/Web/SVG/Attribute/href
+ */
+const LEGACY_ELEMENTS = [
+ 'cursor',
+ 'filter',
+ 'font-face-uri',
+ 'glyphRef',
+ 'tref',
+];
+
+/**
+ * @param {XastElement} node
+ * @param {string[]} prefixes
+ * @param {string} attr
+ * @returns {string[]}
+ */
+const findPrefixedAttrs = (node, prefixes, attr) => {
+ return prefixes
+ .map((prefix) => `${prefix}:${attr}`)
+ .filter((attr) => node.attributes[attr] != null);
+};
+
+/**
+ * Removes XLink namespace prefixes and converts references to XLink attributes
+ * to the native SVG equivalent.
+ *
+ * The XLink namespace is deprecated in SVG 2.
+ *
+ * @type {import('./plugins-types').Plugin<'removeXlink'>}
+ * @see https://developer.mozilla.org/docs/Web/SVG/Attribute/xlink:href
+ */
+exports.fn = (_, params) => {
+ const { includeLegacy } = params;
+
+ /**
+ * XLink namespace prefixes that are currently in the stack.
+ *
+ * @type {string[]}
+ */
+ const xlinkPrefixes = [];
+
+ /**
+ * Namespace prefixes that exist in {@link xlinkPrefixes} but were overridden
+ * in a child element to point to another namespace, and so is not treated as
+ * an XLink attribute.
+ *
+ * @type {string[]}
+ */
+ const overriddenPrefixes = [];
+
+ /**
+ * Namespace prefixes that were used in one of the {@link LEGACY_ELEMENTS}.
+ *
+ * @type {string[]}
+ */
+ const usedInLegacyElement = [];
+
+ return {
+ element: {
+ enter: (node) => {
+ for (const [key, value] of Object.entries(node.attributes)) {
+ if (key.startsWith('xmlns:')) {
+ const prefix = key.split(':', 2)[1];
+
+ if (value === XLINK_NAMESPACE) {
+ xlinkPrefixes.push(prefix);
+ continue;
+ }
+
+ if (xlinkPrefixes.includes(prefix)) {
+ overriddenPrefixes.push(prefix);
+ }
+ }
+ }
+
+ if (
+ overriddenPrefixes.some((prefix) => xlinkPrefixes.includes(prefix))
+ ) {
+ return;
+ }
+
+ const showAttrs = findPrefixedAttrs(node, xlinkPrefixes, 'show');
+ let showHandled = node.attributes.target != null;
+ for (let i = showAttrs.length - 1; i >= 0; i--) {
+ const attr = showAttrs[i];
+ const value = node.attributes[attr];
+ const mapping = SHOW_TO_TARGET[value];
+
+ if (showHandled || mapping == null) {
+ delete node.attributes[attr];
+ continue;
+ }
+
+ if (mapping !== elems[node.name]?.defaults?.target) {
+ node.attributes.target = mapping;
+ }
+
+ delete node.attributes[attr];
+ showHandled = true;
+ }
+
+ const titleAttrs = findPrefixedAttrs(node, xlinkPrefixes, 'title');
+ for (let i = titleAttrs.length - 1; i >= 0; i--) {
+ const attr = titleAttrs[i];
+ const value = node.attributes[attr];
+ const hasTitle = node.children.filter(
+ (child) => child.type === 'element' && child.name === 'title'
+ );
+
+ if (hasTitle.length > 0) {
+ delete node.attributes[attr];
+ continue;
+ }
+
+ /** @type {XastElement} */
+ const titleTag = {
+ type: 'element',
+ name: 'title',
+ attributes: {},
+ children: [
+ {
+ type: 'text',
+ value,
+ },
+ ],
+ };
+
+ Object.defineProperty(titleTag, 'parentNode', {
+ writable: true,
+ value: node,
+ });
+
+ node.children.unshift(titleTag);
+ delete node.attributes[attr];
+ }
+
+ const hrefAttrs = findPrefixedAttrs(node, xlinkPrefixes, 'href');
+
+ if (
+ hrefAttrs.length > 0 &&
+ LEGACY_ELEMENTS.includes(node.name) &&
+ !includeLegacy
+ ) {
+ hrefAttrs
+ .map((attr) => attr.split(':', 1)[0])
+ .forEach((prefix) => usedInLegacyElement.push(prefix));
+ return;
+ }
+
+ for (let i = hrefAttrs.length - 1; i >= 0; i--) {
+ const attr = hrefAttrs[i];
+ const value = node.attributes[attr];
+
+ if (node.attributes.href != null) {
+ delete node.attributes[attr];
+ continue;
+ }
+
+ node.attributes.href = value;
+ delete node.attributes[attr];
+ }
+ },
+ exit: (node) => {
+ for (const [key, value] of Object.entries(node.attributes)) {
+ const [prefix, attr] = key.split(':', 2);
+
+ if (
+ xlinkPrefixes.includes(prefix) &&
+ !overriddenPrefixes.includes(prefix) &&
+ !usedInLegacyElement.includes(prefix) &&
+ !includeLegacy
+ ) {
+ delete node.attributes[key];
+ continue;
+ }
+
+ if (key.startsWith('xmlns:') && !usedInLegacyElement.includes(attr)) {
+ if (value === XLINK_NAMESPACE) {
+ const index = xlinkPrefixes.indexOf(attr);
+ xlinkPrefixes.splice(index, 1);
+ delete node.attributes[key];
+ continue;
+ }
+
+ if (overriddenPrefixes.includes(prefix)) {
+ const index = overriddenPrefixes.indexOf(attr);
+ overriddenPrefixes.splice(index, 1);
+ }
+ }
+ }
+ },
+ },
+ };
+};
diff --git a/node_modules/svgo/plugins/reusePaths.js b/node_modules/svgo/plugins/reusePaths.js
index 4d91d6f9..89ff1f6b 100644
--- a/node_modules/svgo/plugins/reusePaths.js
+++ b/node_modules/svgo/plugins/reusePaths.js
@@ -1,5 +1,8 @@
'use strict';
+const { collectStylesheet } = require('../lib/style');
+const { detachNodeFromParent, querySelectorAll } = require('../lib/xast');
+
/**
* @typedef {import('../lib/types').XastElement} XastElement
* @typedef {import('../lib/types').XastParent} XastParent
@@ -20,15 +23,33 @@ exports.description =
*
* @type {import('./plugins-types').Plugin<'reusePaths'>}
*/
-exports.fn = () => {
+exports.fn = (root) => {
+ const stylesheet = collectStylesheet(root);
+
/**
* @type {Map<string, Array<XastElement>>}
*/
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/docs/Web/SVG/Element/defs
+ */
+ let svgDefs;
+
+ /**
+ * Set of hrefs that reference the id of another node.
+ *
+ * @type {Set<string>}
+ */
+ const hrefs = new Set();
+
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,45 +62,73 @@ exports.fn = () => {
}
list.push(node);
}
+
+ if (
+ svgDefs == null &&
+ node.name === 'defs' &&
+ parentNode.type === 'element' &&
+ parentNode.name === 'svg'
+ ) {
+ svgDefs = node;
+ }
+
+ if (node.name === 'use') {
+ for (const name of ['href', 'xlink:href']) {
+ const href = node.attributes[name];
+
+ if (href != null && href.startsWith('#') && href.length > 1) {
+ hrefs.add(href.slice(1));
+ }
+ }
+ }
},
exit: (node, parentNode) => {
if (node.name === 'svg' && parentNode.type === 'root') {
- /**
- * @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) {
- // add reusable path to defs
- /**
- * @type {XastElement}
- */
+ /** @type {XastElement} */
const reusablePath = {
type: 'element',
name: 'path',
- attributes: { ...list[0].attributes },
+ attributes: {},
children: [],
};
- delete reusablePath.attributes.transform;
- let id;
- if (reusablePath.attributes.id == null) {
- id = 'reuse-' + index;
- index += 1;
- reusablePath.attributes.id = id;
+
+ for (const attr of ['fill', 'stroke', 'd']) {
+ if (list[0].attributes[attr] != null) {
+ reusablePath.attributes[attr] = list[0].attributes[attr];
+ }
+ }
+
+ const originalId = list[0].attributes.id;
+ if (
+ originalId == null ||
+ hrefs.has(originalId) ||
+ stylesheet.rules.some(
+ (rule) => rule.selector === `#${originalId}`
+ )
+ ) {
+ reusablePath.attributes.id = 'reuse-' + index++;
} else {
- id = reusablePath.attributes.id;
+ reusablePath.attributes.id = originalId;
delete list[0].attributes.id;
}
// TODO remove legacy parentNode in v4
@@ -90,11 +139,43 @@ exports.fn = () => {
defsTag.children.push(reusablePath);
// convert paths to <use>
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] =
+ '#' + reusablePath.attributes.id;
+ }
+ }
+ }
+ continue;
+ }
+ }
+
+ pathNode.name = 'use';
+ pathNode.attributes['xlink:href'] =
+ '#' + reusablePath.attributes.id;
}
}
}
@@ -102,7 +183,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);
+ }
}
}
},