diff options
Diffstat (limited to 'node_modules/svgo/plugins')
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); + } } } }, |
