diff options
| author | littleblack111 <littleblack11111@gmail.com> | 2023-10-14 15:05:55 +0800 |
|---|---|---|
| committer | littleblack111 <littleblack11111@gmail.com> | 2023-10-14 15:05:55 +0800 |
| commit | c06f7ee0a40255214c477e13d9b956a7e07c2900 (patch) | |
| tree | f5982a568ef8fab865d0142e71c7494fea3e4c1b /node_modules/svgo/plugins | |
| parent | 3fd9400d4dccc25aa29b5702216732e0b7542073 (diff) | |
postcss,(plugins): nanocss, postcss-preset-env
Diffstat (limited to 'node_modules/svgo/plugins')
56 files changed, 10410 insertions, 0 deletions
diff --git a/node_modules/svgo/plugins/_collections.js b/node_modules/svgo/plugins/_collections.js new file mode 100644 index 00000000..dfa8bff6 --- /dev/null +++ b/node_modules/svgo/plugins/_collections.js @@ -0,0 +1,2168 @@ +'use strict'; + +// https://www.w3.org/TR/SVG11/intro.html#Definitions + +/** + * @type {Record<string, Array<string>>} + */ +exports.elemsGroups = { + animation: [ + 'animate', + 'animateColor', + 'animateMotion', + 'animateTransform', + 'set', + ], + descriptive: ['desc', 'metadata', 'title'], + shape: ['circle', 'ellipse', 'line', 'path', 'polygon', 'polyline', 'rect'], + structural: ['defs', 'g', 'svg', 'symbol', 'use'], + paintServer: [ + 'solidColor', + 'linearGradient', + 'radialGradient', + 'meshGradient', + 'pattern', + 'hatch', + ], + nonRendering: [ + 'linearGradient', + 'radialGradient', + 'pattern', + 'clipPath', + 'mask', + 'marker', + 'symbol', + 'filter', + 'solidColor', + ], + container: [ + 'a', + 'defs', + 'g', + 'marker', + 'mask', + 'missing-glyph', + 'pattern', + 'svg', + 'switch', + 'symbol', + 'foreignObject', + ], + textContent: [ + 'altGlyph', + 'altGlyphDef', + 'altGlyphItem', + 'glyph', + 'glyphRef', + 'textPath', + 'text', + 'tref', + 'tspan', + ], + textContentChild: ['altGlyph', 'textPath', 'tref', 'tspan'], + lightSource: [ + 'feDiffuseLighting', + 'feSpecularLighting', + 'feDistantLight', + 'fePointLight', + 'feSpotLight', + ], + filterPrimitive: [ + 'feBlend', + 'feColorMatrix', + 'feComponentTransfer', + 'feComposite', + 'feConvolveMatrix', + 'feDiffuseLighting', + 'feDisplacementMap', + 'feDropShadow', + 'feFlood', + 'feFuncA', + 'feFuncB', + 'feFuncG', + 'feFuncR', + 'feGaussianBlur', + 'feImage', + 'feMerge', + 'feMergeNode', + 'feMorphology', + 'feOffset', + 'feSpecularLighting', + 'feTile', + 'feTurbulence', + ], +}; + +exports.textElems = exports.elemsGroups.textContent.concat('title'); + +exports.pathElems = ['path', 'glyph', 'missing-glyph']; + +// https://www.w3.org/TR/SVG11/intro.html#Definitions +/** + * @type {Record<string, Array<string>>} + */ +exports.attrsGroups = { + animationAddition: ['additive', 'accumulate'], + animationAttributeTarget: ['attributeType', 'attributeName'], + animationEvent: ['onbegin', 'onend', 'onrepeat', 'onload'], + animationTiming: [ + 'begin', + 'dur', + 'end', + 'min', + 'max', + 'restart', + 'repeatCount', + 'repeatDur', + 'fill', + ], + animationValue: [ + 'calcMode', + 'values', + 'keyTimes', + 'keySplines', + 'from', + 'to', + 'by', + ], + conditionalProcessing: [ + 'requiredFeatures', + 'requiredExtensions', + 'systemLanguage', + ], + core: ['id', 'tabindex', 'xml:base', 'xml:lang', 'xml:space'], + graphicalEvent: [ + 'onfocusin', + 'onfocusout', + 'onactivate', + 'onclick', + 'onmousedown', + 'onmouseup', + 'onmouseover', + 'onmousemove', + 'onmouseout', + 'onload', + ], + presentation: [ + 'alignment-baseline', + 'baseline-shift', + 'clip', + 'clip-path', + 'clip-rule', + 'color', + 'color-interpolation', + 'color-interpolation-filters', + 'color-profile', + 'color-rendering', + 'cursor', + 'direction', + 'display', + 'dominant-baseline', + 'enable-background', + 'fill', + 'fill-opacity', + 'fill-rule', + 'filter', + 'flood-color', + 'flood-opacity', + 'font-family', + 'font-size', + 'font-size-adjust', + 'font-stretch', + 'font-style', + 'font-variant', + 'font-weight', + 'glyph-orientation-horizontal', + 'glyph-orientation-vertical', + 'image-rendering', + 'letter-spacing', + 'lighting-color', + 'marker-end', + 'marker-mid', + 'marker-start', + 'mask', + 'opacity', + 'overflow', + 'paint-order', + 'pointer-events', + 'shape-rendering', + 'stop-color', + 'stop-opacity', + 'stroke', + 'stroke-dasharray', + 'stroke-dashoffset', + 'stroke-linecap', + 'stroke-linejoin', + 'stroke-miterlimit', + 'stroke-opacity', + 'stroke-width', + 'text-anchor', + 'text-decoration', + 'text-overflow', + 'text-rendering', + 'transform', + 'transform-origin', + 'unicode-bidi', + 'vector-effect', + 'visibility', + 'word-spacing', + 'writing-mode', + ], + xlink: [ + 'xlink:href', + 'xlink:show', + 'xlink:actuate', + 'xlink:type', + 'xlink:role', + 'xlink:arcrole', + 'xlink:title', + ], + documentEvent: [ + 'onunload', + 'onabort', + 'onerror', + 'onresize', + 'onscroll', + 'onzoom', + ], + filterPrimitive: ['x', 'y', 'width', 'height', 'result'], + transferFunction: [ + 'type', + 'tableValues', + 'slope', + 'intercept', + 'amplitude', + 'exponent', + 'offset', + ], +}; + +/** + * @type {Record<string, Record<string, string>>} + */ +exports.attrsGroupsDefaults = { + core: { 'xml:space': 'default' }, + presentation: { + clip: 'auto', + 'clip-path': 'none', + 'clip-rule': 'nonzero', + mask: 'none', + opacity: '1', + 'stop-color': '#000', + 'stop-opacity': '1', + 'fill-opacity': '1', + 'fill-rule': 'nonzero', + fill: '#000', + stroke: 'none', + 'stroke-width': '1', + 'stroke-linecap': 'butt', + 'stroke-linejoin': 'miter', + 'stroke-miterlimit': '4', + 'stroke-dasharray': 'none', + 'stroke-dashoffset': '0', + 'stroke-opacity': '1', + 'paint-order': 'normal', + 'vector-effect': 'none', + display: 'inline', + visibility: 'visible', + 'marker-start': 'none', + 'marker-mid': 'none', + 'marker-end': 'none', + 'color-interpolation': 'sRGB', + 'color-interpolation-filters': 'linearRGB', + 'color-rendering': 'auto', + 'shape-rendering': 'auto', + 'text-rendering': 'auto', + 'image-rendering': 'auto', + 'font-style': 'normal', + 'font-variant': 'normal', + 'font-weight': 'normal', + 'font-stretch': 'normal', + 'font-size': 'medium', + 'font-size-adjust': 'none', + kerning: 'auto', + 'letter-spacing': 'normal', + 'word-spacing': 'normal', + 'text-decoration': 'none', + 'text-anchor': 'start', + 'text-overflow': 'clip', + 'writing-mode': 'lr-tb', + 'glyph-orientation-vertical': 'auto', + 'glyph-orientation-horizontal': '0deg', + direction: 'ltr', + 'unicode-bidi': 'normal', + 'dominant-baseline': 'auto', + 'alignment-baseline': 'baseline', + 'baseline-shift': 'baseline', + }, + transferFunction: { + slope: '1', + intercept: '0', + amplitude: '1', + exponent: '1', + offset: '0', + }, +}; + +// https://www.w3.org/TR/SVG11/eltindex.html +/** + * @type {Record<string, { + * attrsGroups: Array<string>, + * attrs?: Array<string>, + * defaults?: Record<string, string>, + * contentGroups?: Array<string>, + * content?: Array<string>, + * }>} + */ +exports.elems = { + a: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'graphicalEvent', + 'presentation', + 'xlink', + ], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'transform', + 'target', + ], + defaults: { + target: '_self', + }, + contentGroups: [ + 'animation', + 'descriptive', + 'shape', + 'structural', + 'paintServer', + ], + content: [ + 'a', + 'altGlyphDef', + 'clipPath', + 'color-profile', + 'cursor', + 'filter', + 'font', + 'font-face', + 'foreignObject', + 'image', + 'marker', + 'mask', + 'pattern', + 'script', + 'style', + 'switch', + 'text', + 'view', + // not spec compliant + 'tspan', + ], + }, + altGlyph: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'graphicalEvent', + 'presentation', + 'xlink', + ], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'x', + 'y', + 'dx', + 'dy', + 'glyphRef', + 'format', + 'rotate', + ], + }, + altGlyphDef: { + attrsGroups: ['core'], + content: ['glyphRef'], + }, + altGlyphItem: { + attrsGroups: ['core'], + content: ['glyphRef', 'altGlyphItem'], + }, + animate: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'animationAddition', + 'animationAttributeTarget', + 'animationEvent', + 'animationTiming', + 'animationValue', + 'presentation', + 'xlink', + ], + attrs: ['externalResourcesRequired'], + contentGroups: ['descriptive'], + }, + animateColor: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'animationEvent', + 'xlink', + 'animationAttributeTarget', + 'animationTiming', + 'animationValue', + 'animationAddition', + 'presentation', + ], + attrs: ['externalResourcesRequired'], + contentGroups: ['descriptive'], + }, + animateMotion: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'animationEvent', + 'xlink', + 'animationTiming', + 'animationValue', + 'animationAddition', + ], + attrs: [ + 'externalResourcesRequired', + 'path', + 'keyPoints', + 'rotate', + 'origin', + ], + defaults: { + rotate: '0', + }, + contentGroups: ['descriptive'], + content: ['mpath'], + }, + animateTransform: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'animationEvent', + 'xlink', + 'animationAttributeTarget', + 'animationTiming', + 'animationValue', + 'animationAddition', + ], + attrs: ['externalResourcesRequired', 'type'], + contentGroups: ['descriptive'], + }, + circle: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'graphicalEvent', + 'presentation', + ], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'transform', + 'cx', + 'cy', + 'r', + ], + defaults: { + cx: '0', + cy: '0', + }, + contentGroups: ['animation', 'descriptive'], + }, + clipPath: { + attrsGroups: ['conditionalProcessing', 'core', 'presentation'], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'transform', + 'clipPathUnits', + ], + defaults: { + clipPathUnits: 'userSpaceOnUse', + }, + contentGroups: ['animation', 'descriptive', 'shape'], + content: ['text', 'use'], + }, + 'color-profile': { + attrsGroups: ['core', 'xlink'], + attrs: ['local', 'name', 'rendering-intent'], + defaults: { + name: 'sRGB', + 'rendering-intent': 'auto', + }, + contentGroups: ['descriptive'], + }, + cursor: { + attrsGroups: ['core', 'conditionalProcessing', 'xlink'], + attrs: ['externalResourcesRequired', 'x', 'y'], + defaults: { + x: '0', + y: '0', + }, + contentGroups: ['descriptive'], + }, + defs: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'graphicalEvent', + 'presentation', + ], + attrs: ['class', 'style', 'externalResourcesRequired', 'transform'], + contentGroups: [ + 'animation', + 'descriptive', + 'shape', + 'structural', + 'paintServer', + ], + content: [ + 'a', + 'altGlyphDef', + 'clipPath', + 'color-profile', + 'cursor', + 'filter', + 'font', + 'font-face', + 'foreignObject', + 'image', + 'marker', + 'mask', + 'pattern', + 'script', + 'style', + 'switch', + 'text', + 'view', + ], + }, + desc: { + attrsGroups: ['core'], + attrs: ['class', 'style'], + }, + ellipse: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'graphicalEvent', + 'presentation', + ], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'transform', + 'cx', + 'cy', + 'rx', + 'ry', + ], + defaults: { + cx: '0', + cy: '0', + }, + contentGroups: ['animation', 'descriptive'], + }, + feBlend: { + attrsGroups: ['core', 'presentation', 'filterPrimitive'], + attrs: [ + 'class', + 'style', + // TODO: in - 'If no value is provided and this is the first filter primitive, + // then this filter primitive will use SourceGraphic as its input' + 'in', + 'in2', + 'mode', + ], + defaults: { + mode: 'normal', + }, + content: ['animate', 'set'], + }, + feColorMatrix: { + attrsGroups: ['core', 'presentation', 'filterPrimitive'], + attrs: ['class', 'style', 'in', 'type', 'values'], + defaults: { + type: 'matrix', + }, + content: ['animate', 'set'], + }, + feComponentTransfer: { + attrsGroups: ['core', 'presentation', 'filterPrimitive'], + attrs: ['class', 'style', 'in'], + content: ['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR'], + }, + feComposite: { + attrsGroups: ['core', 'presentation', 'filterPrimitive'], + attrs: ['class', 'style', 'in', 'in2', 'operator', 'k1', 'k2', 'k3', 'k4'], + defaults: { + operator: 'over', + k1: '0', + k2: '0', + k3: '0', + k4: '0', + }, + content: ['animate', 'set'], + }, + feConvolveMatrix: { + attrsGroups: ['core', 'presentation', 'filterPrimitive'], + attrs: [ + 'class', + 'style', + 'in', + 'order', + 'kernelMatrix', + // TODO: divisor - 'The default value is the sum of all values in kernelMatrix, + // with the exception that if the sum is zero, then the divisor is set to 1' + 'divisor', + 'bias', + // TODO: targetX - 'By default, the convolution matrix is centered in X over each + // pixel of the input image (i.e., targetX = floor ( orderX / 2 ))' + 'targetX', + 'targetY', + 'edgeMode', + // TODO: kernelUnitLength - 'The first number is the <dx> value. The second number + // is the <dy> value. If the <dy> value is not specified, it defaults to the same value as <dx>' + 'kernelUnitLength', + 'preserveAlpha', + ], + defaults: { + order: '3', + bias: '0', + edgeMode: 'duplicate', + preserveAlpha: 'false', + }, + content: ['animate', 'set'], + }, + feDiffuseLighting: { + attrsGroups: ['core', 'presentation', 'filterPrimitive'], + attrs: [ + 'class', + 'style', + 'in', + 'surfaceScale', + 'diffuseConstant', + 'kernelUnitLength', + ], + defaults: { + surfaceScale: '1', + diffuseConstant: '1', + }, + contentGroups: ['descriptive'], + content: [ + // TODO: 'exactly one light source element, in any order' + 'feDistantLight', + 'fePointLight', + 'feSpotLight', + ], + }, + feDisplacementMap: { + attrsGroups: ['core', 'presentation', 'filterPrimitive'], + attrs: [ + 'class', + 'style', + 'in', + 'in2', + 'scale', + 'xChannelSelector', + 'yChannelSelector', + ], + defaults: { + scale: '0', + xChannelSelector: 'A', + yChannelSelector: 'A', + }, + content: ['animate', 'set'], + }, + feDistantLight: { + attrsGroups: ['core'], + attrs: ['azimuth', 'elevation'], + defaults: { + azimuth: '0', + elevation: '0', + }, + content: ['animate', 'set'], + }, + feFlood: { + attrsGroups: ['core', 'presentation', 'filterPrimitive'], + attrs: ['class', 'style'], + content: ['animate', 'animateColor', 'set'], + }, + feFuncA: { + attrsGroups: ['core', 'transferFunction'], + content: ['set', 'animate'], + }, + feFuncB: { + attrsGroups: ['core', 'transferFunction'], + content: ['set', 'animate'], + }, + feFuncG: { + attrsGroups: ['core', 'transferFunction'], + content: ['set', 'animate'], + }, + feFuncR: { + attrsGroups: ['core', 'transferFunction'], + content: ['set', 'animate'], + }, + feGaussianBlur: { + attrsGroups: ['core', 'presentation', 'filterPrimitive'], + attrs: ['class', 'style', 'in', 'stdDeviation'], + defaults: { + stdDeviation: '0', + }, + content: ['set', 'animate'], + }, + feImage: { + attrsGroups: ['core', 'presentation', 'filterPrimitive', 'xlink'], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'preserveAspectRatio', + 'href', + 'xlink:href', + ], + defaults: { + preserveAspectRatio: 'xMidYMid meet', + }, + content: ['animate', 'animateTransform', 'set'], + }, + feMerge: { + attrsGroups: ['core', 'presentation', 'filterPrimitive'], + attrs: ['class', 'style'], + content: ['feMergeNode'], + }, + feMergeNode: { + attrsGroups: ['core'], + attrs: ['in'], + content: ['animate', 'set'], + }, + feMorphology: { + attrsGroups: ['core', 'presentation', 'filterPrimitive'], + attrs: ['class', 'style', 'in', 'operator', 'radius'], + defaults: { + operator: 'erode', + radius: '0', + }, + content: ['animate', 'set'], + }, + feOffset: { + attrsGroups: ['core', 'presentation', 'filterPrimitive'], + attrs: ['class', 'style', 'in', 'dx', 'dy'], + defaults: { + dx: '0', + dy: '0', + }, + content: ['animate', 'set'], + }, + fePointLight: { + attrsGroups: ['core'], + attrs: ['x', 'y', 'z'], + defaults: { + x: '0', + y: '0', + z: '0', + }, + content: ['animate', 'set'], + }, + feSpecularLighting: { + attrsGroups: ['core', 'presentation', 'filterPrimitive'], + attrs: [ + 'class', + 'style', + 'in', + 'surfaceScale', + 'specularConstant', + 'specularExponent', + 'kernelUnitLength', + ], + defaults: { + surfaceScale: '1', + specularConstant: '1', + specularExponent: '1', + }, + contentGroups: [ + 'descriptive', + // TODO: exactly one 'light source element' + 'lightSource', + ], + }, + feSpotLight: { + attrsGroups: ['core'], + attrs: [ + 'x', + 'y', + 'z', + 'pointsAtX', + 'pointsAtY', + 'pointsAtZ', + 'specularExponent', + 'limitingConeAngle', + ], + defaults: { + x: '0', + y: '0', + z: '0', + pointsAtX: '0', + pointsAtY: '0', + pointsAtZ: '0', + specularExponent: '1', + }, + content: ['animate', 'set'], + }, + feTile: { + attrsGroups: ['core', 'presentation', 'filterPrimitive'], + attrs: ['class', 'style', 'in'], + content: ['animate', 'set'], + }, + feTurbulence: { + attrsGroups: ['core', 'presentation', 'filterPrimitive'], + attrs: [ + 'class', + 'style', + 'baseFrequency', + 'numOctaves', + 'seed', + 'stitchTiles', + 'type', + ], + defaults: { + baseFrequency: '0', + numOctaves: '1', + seed: '0', + stitchTiles: 'noStitch', + type: 'turbulence', + }, + content: ['animate', 'set'], + }, + filter: { + attrsGroups: ['core', 'presentation', 'xlink'], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'x', + 'y', + 'width', + 'height', + 'filterRes', + 'filterUnits', + 'primitiveUnits', + 'href', + 'xlink:href', + ], + defaults: { + primitiveUnits: 'userSpaceOnUse', + x: '-10%', + y: '-10%', + width: '120%', + height: '120%', + }, + contentGroups: ['descriptive', 'filterPrimitive'], + content: ['animate', 'set'], + }, + font: { + attrsGroups: ['core', 'presentation'], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'horiz-origin-x', + 'horiz-origin-y', + 'horiz-adv-x', + 'vert-origin-x', + 'vert-origin-y', + 'vert-adv-y', + ], + defaults: { + 'horiz-origin-x': '0', + 'horiz-origin-y': '0', + }, + contentGroups: ['descriptive'], + content: ['font-face', 'glyph', 'hkern', 'missing-glyph', 'vkern'], + }, + 'font-face': { + attrsGroups: ['core'], + attrs: [ + 'font-family', + 'font-style', + 'font-variant', + 'font-weight', + 'font-stretch', + 'font-size', + 'unicode-range', + 'units-per-em', + 'panose-1', + 'stemv', + 'stemh', + 'slope', + 'cap-height', + 'x-height', + 'accent-height', + 'ascent', + 'descent', + 'widths', + 'bbox', + 'ideographic', + 'alphabetic', + 'mathematical', + 'hanging', + 'v-ideographic', + 'v-alphabetic', + 'v-mathematical', + 'v-hanging', + 'underline-position', + 'underline-thickness', + 'strikethrough-position', + 'strikethrough-thickness', + 'overline-position', + 'overline-thickness', + ], + defaults: { + 'font-style': 'all', + 'font-variant': 'normal', + 'font-weight': 'all', + 'font-stretch': 'normal', + 'unicode-range': 'U+0-10FFFF', + 'units-per-em': '1000', + 'panose-1': '0 0 0 0 0 0 0 0 0 0', + slope: '0', + }, + contentGroups: ['descriptive'], + content: [ + // TODO: "at most one 'font-face-src' element" + 'font-face-src', + ], + }, + // TODO: empty content + 'font-face-format': { + attrsGroups: ['core'], + attrs: ['string'], + }, + 'font-face-name': { + attrsGroups: ['core'], + attrs: ['name'], + }, + 'font-face-src': { + attrsGroups: ['core'], + content: ['font-face-name', 'font-face-uri'], + }, + 'font-face-uri': { + attrsGroups: ['core', 'xlink'], + attrs: ['href', 'xlink:href'], + content: ['font-face-format'], + }, + foreignObject: { + attrsGroups: [ + 'core', + 'conditionalProcessing', + 'graphicalEvent', + 'presentation', + ], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'transform', + 'x', + 'y', + 'width', + 'height', + ], + defaults: { + x: '0', + y: '0', + }, + }, + g: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'graphicalEvent', + 'presentation', + ], + attrs: ['class', 'style', 'externalResourcesRequired', 'transform'], + contentGroups: [ + 'animation', + 'descriptive', + 'shape', + 'structural', + 'paintServer', + ], + content: [ + 'a', + 'altGlyphDef', + 'clipPath', + 'color-profile', + 'cursor', + 'filter', + 'font', + 'font-face', + 'foreignObject', + 'image', + 'marker', + 'mask', + 'pattern', + 'script', + 'style', + 'switch', + 'text', + 'view', + ], + }, + glyph: { + attrsGroups: ['core', 'presentation'], + attrs: [ + 'class', + 'style', + 'd', + 'horiz-adv-x', + 'vert-origin-x', + 'vert-origin-y', + 'vert-adv-y', + 'unicode', + 'glyph-name', + 'orientation', + 'arabic-form', + 'lang', + ], + defaults: { + 'arabic-form': 'initial', + }, + contentGroups: [ + 'animation', + 'descriptive', + 'shape', + 'structural', + 'paintServer', + ], + content: [ + 'a', + 'altGlyphDef', + 'clipPath', + 'color-profile', + 'cursor', + 'filter', + 'font', + 'font-face', + 'foreignObject', + 'image', + 'marker', + 'mask', + 'pattern', + 'script', + 'style', + 'switch', + 'text', + 'view', + ], + }, + glyphRef: { + attrsGroups: ['core', 'presentation'], + attrs: [ + 'class', + 'style', + 'd', + 'horiz-adv-x', + 'vert-origin-x', + 'vert-origin-y', + 'vert-adv-y', + ], + contentGroups: [ + 'animation', + 'descriptive', + 'shape', + 'structural', + 'paintServer', + ], + content: [ + 'a', + 'altGlyphDef', + 'clipPath', + 'color-profile', + 'cursor', + 'filter', + 'font', + 'font-face', + 'foreignObject', + 'image', + 'marker', + 'mask', + 'pattern', + 'script', + 'style', + 'switch', + 'text', + 'view', + ], + }, + hatch: { + attrsGroups: ['core', 'presentation', 'xlink'], + attrs: [ + 'class', + 'style', + 'x', + 'y', + 'pitch', + 'rotate', + 'hatchUnits', + 'hatchContentUnits', + 'transform', + ], + defaults: { + hatchUnits: 'objectBoundingBox', + hatchContentUnits: 'userSpaceOnUse', + x: '0', + y: '0', + pitch: '0', + rotate: '0', + }, + contentGroups: ['animation', 'descriptive'], + content: ['hatchPath'], + }, + hatchPath: { + attrsGroups: ['core', 'presentation', 'xlink'], + attrs: ['class', 'style', 'd', 'offset'], + defaults: { + offset: '0', + }, + contentGroups: ['animation', 'descriptive'], + }, + hkern: { + attrsGroups: ['core'], + attrs: ['u1', 'g1', 'u2', 'g2', 'k'], + }, + image: { + attrsGroups: [ + 'core', + 'conditionalProcessing', + 'graphicalEvent', + 'xlink', + 'presentation', + ], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'preserveAspectRatio', + 'transform', + 'x', + 'y', + 'width', + 'height', + 'href', + 'xlink:href', + ], + defaults: { + x: '0', + y: '0', + preserveAspectRatio: 'xMidYMid meet', + }, + contentGroups: ['animation', 'descriptive'], + }, + line: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'graphicalEvent', + 'presentation', + ], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'transform', + 'x1', + 'y1', + 'x2', + 'y2', + ], + defaults: { + x1: '0', + y1: '0', + x2: '0', + y2: '0', + }, + contentGroups: ['animation', 'descriptive'], + }, + linearGradient: { + attrsGroups: ['core', 'presentation', 'xlink'], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'x1', + 'y1', + 'x2', + 'y2', + 'gradientUnits', + 'gradientTransform', + 'spreadMethod', + 'href', + 'xlink:href', + ], + defaults: { + x1: '0', + y1: '0', + x2: '100%', + y2: '0', + spreadMethod: 'pad', + }, + contentGroups: ['descriptive'], + content: ['animate', 'animateTransform', 'set', 'stop'], + }, + marker: { + attrsGroups: ['core', 'presentation'], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'viewBox', + 'preserveAspectRatio', + 'refX', + 'refY', + 'markerUnits', + 'markerWidth', + 'markerHeight', + 'orient', + ], + defaults: { + markerUnits: 'strokeWidth', + refX: '0', + refY: '0', + markerWidth: '3', + markerHeight: '3', + }, + contentGroups: [ + 'animation', + 'descriptive', + 'shape', + 'structural', + 'paintServer', + ], + content: [ + 'a', + 'altGlyphDef', + 'clipPath', + 'color-profile', + 'cursor', + 'filter', + 'font', + 'font-face', + 'foreignObject', + 'image', + 'marker', + 'mask', + 'pattern', + 'script', + 'style', + 'switch', + 'text', + 'view', + ], + }, + mask: { + attrsGroups: ['conditionalProcessing', 'core', 'presentation'], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'x', + 'y', + 'width', + 'height', + 'mask-type', + 'maskUnits', + 'maskContentUnits', + ], + defaults: { + maskUnits: 'objectBoundingBox', + maskContentUnits: 'userSpaceOnUse', + x: '-10%', + y: '-10%', + width: '120%', + height: '120%', + }, + contentGroups: [ + 'animation', + 'descriptive', + 'shape', + 'structural', + 'paintServer', + ], + content: [ + 'a', + 'altGlyphDef', + 'clipPath', + 'color-profile', + 'cursor', + 'filter', + 'font', + 'font-face', + 'foreignObject', + 'image', + 'marker', + 'mask', + 'pattern', + 'script', + 'style', + 'switch', + 'text', + 'view', + ], + }, + metadata: { + attrsGroups: ['core'], + }, + 'missing-glyph': { + attrsGroups: ['core', 'presentation'], + attrs: [ + 'class', + 'style', + 'd', + 'horiz-adv-x', + 'vert-origin-x', + 'vert-origin-y', + 'vert-adv-y', + ], + contentGroups: [ + 'animation', + 'descriptive', + 'shape', + 'structural', + 'paintServer', + ], + content: [ + 'a', + 'altGlyphDef', + 'clipPath', + 'color-profile', + 'cursor', + 'filter', + 'font', + 'font-face', + 'foreignObject', + 'image', + 'marker', + 'mask', + 'pattern', + 'script', + 'style', + 'switch', + 'text', + 'view', + ], + }, + mpath: { + attrsGroups: ['core', 'xlink'], + attrs: ['externalResourcesRequired', 'href', 'xlink:href'], + contentGroups: ['descriptive'], + }, + path: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'graphicalEvent', + 'presentation', + ], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'transform', + 'd', + 'pathLength', + ], + contentGroups: ['animation', 'descriptive'], + }, + pattern: { + attrsGroups: ['conditionalProcessing', 'core', 'presentation', 'xlink'], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'viewBox', + 'preserveAspectRatio', + 'x', + 'y', + 'width', + 'height', + 'patternUnits', + 'patternContentUnits', + 'patternTransform', + 'href', + 'xlink:href', + ], + defaults: { + patternUnits: 'objectBoundingBox', + patternContentUnits: 'userSpaceOnUse', + x: '0', + y: '0', + width: '0', + height: '0', + preserveAspectRatio: 'xMidYMid meet', + }, + contentGroups: [ + 'animation', + 'descriptive', + 'paintServer', + 'shape', + 'structural', + ], + content: [ + 'a', + 'altGlyphDef', + 'clipPath', + 'color-profile', + 'cursor', + 'filter', + 'font', + 'font-face', + 'foreignObject', + 'image', + 'marker', + 'mask', + 'pattern', + 'script', + 'style', + 'switch', + 'text', + 'view', + ], + }, + polygon: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'graphicalEvent', + 'presentation', + ], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'transform', + 'points', + ], + contentGroups: ['animation', 'descriptive'], + }, + polyline: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'graphicalEvent', + 'presentation', + ], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'transform', + 'points', + ], + contentGroups: ['animation', 'descriptive'], + }, + radialGradient: { + attrsGroups: ['core', 'presentation', 'xlink'], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'cx', + 'cy', + 'r', + 'fx', + 'fy', + 'fr', + 'gradientUnits', + 'gradientTransform', + 'spreadMethod', + 'href', + 'xlink:href', + ], + defaults: { + gradientUnits: 'objectBoundingBox', + cx: '50%', + cy: '50%', + r: '50%', + }, + contentGroups: ['descriptive'], + content: ['animate', 'animateTransform', 'set', 'stop'], + }, + meshGradient: { + attrsGroups: ['core', 'presentation', 'xlink'], + attrs: ['class', 'style', 'x', 'y', 'gradientUnits', 'transform'], + contentGroups: ['descriptive', 'paintServer', 'animation'], + content: ['meshRow'], + }, + meshRow: { + attrsGroups: ['core', 'presentation'], + attrs: ['class', 'style'], + contentGroups: ['descriptive'], + content: ['meshPatch'], + }, + meshPatch: { + attrsGroups: ['core', 'presentation'], + attrs: ['class', 'style'], + contentGroups: ['descriptive'], + content: ['stop'], + }, + rect: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'graphicalEvent', + 'presentation', + ], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'transform', + 'x', + 'y', + 'width', + 'height', + 'rx', + 'ry', + ], + defaults: { + x: '0', + y: '0', + }, + contentGroups: ['animation', 'descriptive'], + }, + script: { + attrsGroups: ['core', 'xlink'], + attrs: ['externalResourcesRequired', 'type', 'href', 'xlink:href'], + }, + set: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'animation', + 'xlink', + 'animationAttributeTarget', + 'animationTiming', + ], + attrs: ['externalResourcesRequired', 'to'], + contentGroups: ['descriptive'], + }, + solidColor: { + attrsGroups: ['core', 'presentation'], + attrs: ['class', 'style'], + contentGroups: ['paintServer'], + }, + stop: { + attrsGroups: ['core', 'presentation'], + attrs: ['class', 'style', 'offset', 'path'], + content: ['animate', 'animateColor', 'set'], + }, + style: { + attrsGroups: ['core'], + attrs: ['type', 'media', 'title'], + defaults: { + type: 'text/css', + }, + }, + svg: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'documentEvent', + 'graphicalEvent', + 'presentation', + ], + attrs: [ + 'class', + 'style', + 'x', + 'y', + 'width', + 'height', + 'viewBox', + 'preserveAspectRatio', + 'zoomAndPan', + 'version', + 'baseProfile', + 'contentScriptType', + 'contentStyleType', + ], + defaults: { + x: '0', + y: '0', + width: '100%', + height: '100%', + preserveAspectRatio: 'xMidYMid meet', + zoomAndPan: 'magnify', + version: '1.1', + baseProfile: 'none', + contentScriptType: 'application/ecmascript', + contentStyleType: 'text/css', + }, + contentGroups: [ + 'animation', + 'descriptive', + 'shape', + 'structural', + 'paintServer', + ], + content: [ + 'a', + 'altGlyphDef', + 'clipPath', + 'color-profile', + 'cursor', + 'filter', + 'font', + 'font-face', + 'foreignObject', + 'image', + 'marker', + 'mask', + 'pattern', + 'script', + 'style', + 'switch', + 'text', + 'view', + ], + }, + switch: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'graphicalEvent', + 'presentation', + ], + attrs: ['class', 'style', 'externalResourcesRequired', 'transform'], + contentGroups: ['animation', 'descriptive', 'shape'], + content: [ + 'a', + 'foreignObject', + 'g', + 'image', + 'svg', + 'switch', + 'text', + 'use', + ], + }, + symbol: { + attrsGroups: ['core', 'graphicalEvent', 'presentation'], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'preserveAspectRatio', + 'viewBox', + 'refX', + 'refY', + ], + defaults: { + refX: '0', + refY: '0', + }, + contentGroups: [ + 'animation', + 'descriptive', + 'shape', + 'structural', + 'paintServer', + ], + content: [ + 'a', + 'altGlyphDef', + 'clipPath', + 'color-profile', + 'cursor', + 'filter', + 'font', + 'font-face', + 'foreignObject', + 'image', + 'marker', + 'mask', + 'pattern', + 'script', + 'style', + 'switch', + 'text', + 'view', + ], + }, + text: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'graphicalEvent', + 'presentation', + ], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'transform', + 'lengthAdjust', + 'x', + 'y', + 'dx', + 'dy', + 'rotate', + 'textLength', + ], + defaults: { + x: '0', + y: '0', + lengthAdjust: 'spacing', + }, + contentGroups: ['animation', 'descriptive', 'textContentChild'], + content: ['a'], + }, + textPath: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'graphicalEvent', + 'presentation', + 'xlink', + ], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'href', + 'xlink:href', + 'startOffset', + 'method', + 'spacing', + 'd', + ], + defaults: { + startOffset: '0', + method: 'align', + spacing: 'exact', + }, + contentGroups: ['descriptive'], + content: [ + 'a', + 'altGlyph', + 'animate', + 'animateColor', + 'set', + 'tref', + 'tspan', + ], + }, + title: { + attrsGroups: ['core'], + attrs: ['class', 'style'], + }, + tref: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'graphicalEvent', + 'presentation', + 'xlink', + ], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'href', + 'xlink:href', + ], + contentGroups: ['descriptive'], + content: ['animate', 'animateColor', 'set'], + }, + tspan: { + attrsGroups: [ + 'conditionalProcessing', + 'core', + 'graphicalEvent', + 'presentation', + ], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'x', + 'y', + 'dx', + 'dy', + 'rotate', + 'textLength', + 'lengthAdjust', + ], + contentGroups: ['descriptive'], + content: [ + 'a', + 'altGlyph', + 'animate', + 'animateColor', + 'set', + 'tref', + 'tspan', + ], + }, + use: { + attrsGroups: [ + 'core', + 'conditionalProcessing', + 'graphicalEvent', + 'presentation', + 'xlink', + ], + attrs: [ + 'class', + 'style', + 'externalResourcesRequired', + 'transform', + 'x', + 'y', + 'width', + 'height', + 'href', + 'xlink:href', + ], + defaults: { + x: '0', + y: '0', + }, + contentGroups: ['animation', 'descriptive'], + }, + view: { + attrsGroups: ['core'], + attrs: [ + 'externalResourcesRequired', + 'viewBox', + 'preserveAspectRatio', + 'zoomAndPan', + 'viewTarget', + ], + contentGroups: ['descriptive'], + }, + vkern: { + attrsGroups: ['core'], + attrs: ['u1', 'g1', 'u2', 'g2', 'k'], + }, +}; + +// https://wiki.inkscape.org/wiki/index.php/Inkscape-specific_XML_attributes +exports.editorNamespaces = [ + 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', + 'http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd', + 'http://www.inkscape.org/namespaces/inkscape', + 'http://www.bohemiancoding.com/sketch/ns', + 'http://ns.adobe.com/AdobeIllustrator/10.0/', + 'http://ns.adobe.com/Graphs/1.0/', + 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/', + 'http://ns.adobe.com/Variables/1.0/', + 'http://ns.adobe.com/SaveForWeb/1.0/', + 'http://ns.adobe.com/Extensibility/1.0/', + 'http://ns.adobe.com/Flows/1.0/', + 'http://ns.adobe.com/ImageReplacement/1.0/', + 'http://ns.adobe.com/GenericCustomNamespace/1.0/', + 'http://ns.adobe.com/XPath/1.0/', + 'http://schemas.microsoft.com/visio/2003/SVGExtensions/', + 'http://taptrix.com/vectorillustrator/svg_extensions', + 'http://www.figma.com/figma/ns', + 'http://purl.org/dc/elements/1.1/', + 'http://creativecommons.org/ns#', + 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'http://www.serif.com/', + 'http://www.vector.evaxdesign.sk', +]; + +// https://www.w3.org/TR/SVG11/linking.html#processingIRI +exports.referencesProps = [ + 'clip-path', + 'color-profile', + 'fill', + 'filter', + 'marker-start', + 'marker-mid', + 'marker-end', + 'mask', + 'stroke', + 'style', +]; + +// https://www.w3.org/TR/SVG11/propidx.html +exports.inheritableAttrs = [ + 'clip-rule', + 'color', + 'color-interpolation', + 'color-interpolation-filters', + 'color-profile', + 'color-rendering', + 'cursor', + 'direction', + 'dominant-baseline', + 'fill', + 'fill-opacity', + 'fill-rule', + 'font', + 'font-family', + 'font-size', + 'font-size-adjust', + 'font-stretch', + 'font-style', + 'font-variant', + 'font-weight', + 'glyph-orientation-horizontal', + 'glyph-orientation-vertical', + 'image-rendering', + 'letter-spacing', + 'marker', + 'marker-end', + 'marker-mid', + 'marker-start', + 'paint-order', + 'pointer-events', + 'shape-rendering', + 'stroke', + 'stroke-dasharray', + 'stroke-dashoffset', + 'stroke-linecap', + 'stroke-linejoin', + 'stroke-miterlimit', + 'stroke-opacity', + 'stroke-width', + 'text-anchor', + 'text-rendering', + 'transform', + 'visibility', + 'word-spacing', + 'writing-mode', +]; + +exports.presentationNonInheritableGroupAttrs = [ + 'display', + 'clip-path', + 'filter', + 'mask', + 'opacity', + 'text-decoration', + 'transform', + 'unicode-bidi', +]; + +/** + * https://www.w3.org/TR/SVG11/single-page.html#types-ColorKeywords + * + * @type {Record<string, string>} + */ +exports.colorsNames = { + aliceblue: '#f0f8ff', + antiquewhite: '#faebd7', + aqua: '#0ff', + aquamarine: '#7fffd4', + azure: '#f0ffff', + beige: '#f5f5dc', + bisque: '#ffe4c4', + black: '#000', + blanchedalmond: '#ffebcd', + blue: '#00f', + blueviolet: '#8a2be2', + brown: '#a52a2a', + burlywood: '#deb887', + cadetblue: '#5f9ea0', + chartreuse: '#7fff00', + chocolate: '#d2691e', + coral: '#ff7f50', + cornflowerblue: '#6495ed', + cornsilk: '#fff8dc', + crimson: '#dc143c', + cyan: '#0ff', + darkblue: '#00008b', + darkcyan: '#008b8b', + darkgoldenrod: '#b8860b', + darkgray: '#a9a9a9', + darkgreen: '#006400', + darkgrey: '#a9a9a9', + darkkhaki: '#bdb76b', + darkmagenta: '#8b008b', + darkolivegreen: '#556b2f', + darkorange: '#ff8c00', + darkorchid: '#9932cc', + darkred: '#8b0000', + darksalmon: '#e9967a', + darkseagreen: '#8fbc8f', + darkslateblue: '#483d8b', + darkslategray: '#2f4f4f', + darkslategrey: '#2f4f4f', + darkturquoise: '#00ced1', + darkviolet: '#9400d3', + deeppink: '#ff1493', + deepskyblue: '#00bfff', + dimgray: '#696969', + dimgrey: '#696969', + dodgerblue: '#1e90ff', + firebrick: '#b22222', + floralwhite: '#fffaf0', + forestgreen: '#228b22', + fuchsia: '#f0f', + gainsboro: '#dcdcdc', + ghostwhite: '#f8f8ff', + gold: '#ffd700', + goldenrod: '#daa520', + gray: '#808080', + green: '#008000', + greenyellow: '#adff2f', + grey: '#808080', + honeydew: '#f0fff0', + hotpink: '#ff69b4', + indianred: '#cd5c5c', + indigo: '#4b0082', + ivory: '#fffff0', + khaki: '#f0e68c', + lavender: '#e6e6fa', + lavenderblush: '#fff0f5', + lawngreen: '#7cfc00', + lemonchiffon: '#fffacd', + lightblue: '#add8e6', + lightcoral: '#f08080', + lightcyan: '#e0ffff', + lightgoldenrodyellow: '#fafad2', + lightgray: '#d3d3d3', + lightgreen: '#90ee90', + lightgrey: '#d3d3d3', + lightpink: '#ffb6c1', + lightsalmon: '#ffa07a', + lightseagreen: '#20b2aa', + lightskyblue: '#87cefa', + lightslategray: '#789', + lightslategrey: '#789', + lightsteelblue: '#b0c4de', + lightyellow: '#ffffe0', + lime: '#0f0', + limegreen: '#32cd32', + linen: '#faf0e6', + magenta: '#f0f', + maroon: '#800000', + mediumaquamarine: '#66cdaa', + mediumblue: '#0000cd', + mediumorchid: '#ba55d3', + mediumpurple: '#9370db', + mediumseagreen: '#3cb371', + mediumslateblue: '#7b68ee', + mediumspringgreen: '#00fa9a', + mediumturquoise: '#48d1cc', + mediumvioletred: '#c71585', + midnightblue: '#191970', + mintcream: '#f5fffa', + mistyrose: '#ffe4e1', + moccasin: '#ffe4b5', + navajowhite: '#ffdead', + navy: '#000080', + oldlace: '#fdf5e6', + olive: '#808000', + olivedrab: '#6b8e23', + orange: '#ffa500', + orangered: '#ff4500', + orchid: '#da70d6', + palegoldenrod: '#eee8aa', + palegreen: '#98fb98', + paleturquoise: '#afeeee', + palevioletred: '#db7093', + papayawhip: '#ffefd5', + peachpuff: '#ffdab9', + peru: '#cd853f', + pink: '#ffc0cb', + plum: '#dda0dd', + powderblue: '#b0e0e6', + purple: '#800080', + rebeccapurple: '#639', + red: '#f00', + rosybrown: '#bc8f8f', + royalblue: '#4169e1', + saddlebrown: '#8b4513', + salmon: '#fa8072', + sandybrown: '#f4a460', + seagreen: '#2e8b57', + seashell: '#fff5ee', + sienna: '#a0522d', + silver: '#c0c0c0', + skyblue: '#87ceeb', + slateblue: '#6a5acd', + slategray: '#708090', + slategrey: '#708090', + snow: '#fffafa', + springgreen: '#00ff7f', + steelblue: '#4682b4', + tan: '#d2b48c', + teal: '#008080', + thistle: '#d8bfd8', + tomato: '#ff6347', + turquoise: '#40e0d0', + violet: '#ee82ee', + wheat: '#f5deb3', + white: '#fff', + whitesmoke: '#f5f5f5', + yellow: '#ff0', + yellowgreen: '#9acd32', +}; + +/** + * @type {Record<string, string>} + */ +exports.colorsShortNames = { + '#f0ffff': 'azure', + '#f5f5dc': 'beige', + '#ffe4c4': 'bisque', + '#a52a2a': 'brown', + '#ff7f50': 'coral', + '#ffd700': 'gold', + '#808080': 'gray', + '#008000': 'green', + '#4b0082': 'indigo', + '#fffff0': 'ivory', + '#f0e68c': 'khaki', + '#faf0e6': 'linen', + '#800000': 'maroon', + '#000080': 'navy', + '#808000': 'olive', + '#ffa500': 'orange', + '#da70d6': 'orchid', + '#cd853f': 'peru', + '#ffc0cb': 'pink', + '#dda0dd': 'plum', + '#800080': 'purple', + '#f00': 'red', + '#ff0000': 'red', + '#fa8072': 'salmon', + '#a0522d': 'sienna', + '#c0c0c0': 'silver', + '#fffafa': 'snow', + '#d2b48c': 'tan', + '#008080': 'teal', + '#ff6347': 'tomato', + '#ee82ee': 'violet', + '#f5deb3': 'wheat', +}; + +// https://www.w3.org/TR/SVG11/single-page.html#types-DataTypeColor +exports.colorsProps = [ + 'color', + 'fill', + 'stroke', + 'stop-color', + 'flood-color', + 'lighting-color', +]; diff --git a/node_modules/svgo/plugins/_path.js b/node_modules/svgo/plugins/_path.js new file mode 100644 index 00000000..60002d7a --- /dev/null +++ b/node_modules/svgo/plugins/_path.js @@ -0,0 +1,816 @@ +'use strict'; + +/** + * @typedef {import('../lib/types').XastElement} XastElement + * @typedef {import('../lib/types').PathDataItem} PathDataItem + */ + +const { parsePathData, stringifyPathData } = require('../lib/path.js'); + +/** + * @type {[number, number]} + */ +var prevCtrlPoint; + +/** + * Convert path string to JS representation. + * + * @type {(path: XastElement) => Array<PathDataItem>} + */ +const path2js = (path) => { + // @ts-ignore legacy + if (path.pathJS) return path.pathJS; + /** + * @type {Array<PathDataItem>} + */ + const pathData = []; // JS representation of the path data + const newPathData = parsePathData(path.attributes.d); + for (const { command, args } of newPathData) { + pathData.push({ command, args }); + } + // First moveto is actually absolute. Subsequent coordinates were separated above. + if (pathData.length && pathData[0].command == 'm') { + pathData[0].command = 'M'; + } + // @ts-ignore legacy + path.pathJS = pathData; + return pathData; +}; +exports.path2js = path2js; + +/** + * Convert relative Path data to absolute. + * + * @type {(data: Array<PathDataItem>) => Array<PathDataItem>} + * + */ +const convertRelativeToAbsolute = (data) => { + /** + * @type {Array<PathDataItem>} + */ + const newData = []; + let start = [0, 0]; + let cursor = [0, 0]; + + for (let { command, args } of data) { + args = args.slice(); + + // moveto (x y) + if (command === 'm') { + args[0] += cursor[0]; + args[1] += cursor[1]; + command = 'M'; + } + if (command === 'M') { + cursor[0] = args[0]; + cursor[1] = args[1]; + start[0] = cursor[0]; + start[1] = cursor[1]; + } + + // horizontal lineto (x) + if (command === 'h') { + args[0] += cursor[0]; + command = 'H'; + } + if (command === 'H') { + cursor[0] = args[0]; + } + + // vertical lineto (y) + if (command === 'v') { + args[0] += cursor[1]; + command = 'V'; + } + if (command === 'V') { + cursor[1] = args[0]; + } + + // lineto (x y) + if (command === 'l') { + args[0] += cursor[0]; + args[1] += cursor[1]; + command = 'L'; + } + if (command === 'L') { + cursor[0] = args[0]; + cursor[1] = args[1]; + } + + // curveto (x1 y1 x2 y2 x y) + if (command === 'c') { + args[0] += cursor[0]; + args[1] += cursor[1]; + args[2] += cursor[0]; + args[3] += cursor[1]; + args[4] += cursor[0]; + args[5] += cursor[1]; + command = 'C'; + } + if (command === 'C') { + cursor[0] = args[4]; + cursor[1] = args[5]; + } + + // smooth curveto (x2 y2 x y) + if (command === 's') { + args[0] += cursor[0]; + args[1] += cursor[1]; + args[2] += cursor[0]; + args[3] += cursor[1]; + command = 'S'; + } + if (command === 'S') { + cursor[0] = args[2]; + cursor[1] = args[3]; + } + + // quadratic Bézier curveto (x1 y1 x y) + if (command === 'q') { + args[0] += cursor[0]; + args[1] += cursor[1]; + args[2] += cursor[0]; + args[3] += cursor[1]; + command = 'Q'; + } + if (command === 'Q') { + cursor[0] = args[2]; + cursor[1] = args[3]; + } + + // smooth quadratic Bézier curveto (x y) + if (command === 't') { + args[0] += cursor[0]; + args[1] += cursor[1]; + command = 'T'; + } + if (command === 'T') { + cursor[0] = args[0]; + cursor[1] = args[1]; + } + + // elliptical arc (rx ry x-axis-rotation large-arc-flag sweep-flag x y) + if (command === 'a') { + args[5] += cursor[0]; + args[6] += cursor[1]; + command = 'A'; + } + if (command === 'A') { + cursor[0] = args[5]; + cursor[1] = args[6]; + } + + // closepath + if (command === 'z' || command === 'Z') { + cursor[0] = start[0]; + cursor[1] = start[1]; + command = 'z'; + } + + newData.push({ command, args }); + } + return newData; +}; + +/** + * @typedef {{ floatPrecision?: number, noSpaceAfterFlags?: boolean }} Js2PathParams + */ + +/** + * Convert path array to string. + * + * @type {(path: XastElement, data: Array<PathDataItem>, params: Js2PathParams) => void} + */ +exports.js2path = function (path, data, params) { + // @ts-ignore legacy + path.pathJS = data; + + const pathData = []; + for (const item of data) { + // remove moveto commands which are followed by moveto commands + if ( + pathData.length !== 0 && + (item.command === 'M' || item.command === 'm') + ) { + const last = pathData[pathData.length - 1]; + if (last.command === 'M' || last.command === 'm') { + pathData.pop(); + } + } + pathData.push({ + command: item.command, + args: item.args, + }); + } + + path.attributes.d = stringifyPathData({ + pathData, + precision: params.floatPrecision, + disableSpaceAfterFlags: params.noSpaceAfterFlags, + }); +}; + +/** + * @type {(dest: Array<number>, source: Array<number>) => Array<number>} + */ +function set(dest, source) { + dest[0] = source[source.length - 2]; + dest[1] = source[source.length - 1]; + return dest; +} + +/** + * Checks if two paths have an intersection by checking convex hulls + * collision using Gilbert-Johnson-Keerthi distance algorithm + * https://web.archive.org/web/20180822200027/http://entropyinteractive.com/2011/04/gjk-algorithm/ + * + * @type {(path1: Array<PathDataItem>, path2: Array<PathDataItem>) => boolean} + */ +exports.intersects = function (path1, path2) { + // Collect points of every subpath. + const points1 = gatherPoints(convertRelativeToAbsolute(path1)); + const points2 = gatherPoints(convertRelativeToAbsolute(path2)); + + // Axis-aligned bounding box check. + if ( + points1.maxX <= points2.minX || + points2.maxX <= points1.minX || + points1.maxY <= points2.minY || + points2.maxY <= points1.minY || + points1.list.every((set1) => { + return points2.list.every((set2) => { + return ( + set1.list[set1.maxX][0] <= set2.list[set2.minX][0] || + set2.list[set2.maxX][0] <= set1.list[set1.minX][0] || + set1.list[set1.maxY][1] <= set2.list[set2.minY][1] || + set2.list[set2.maxY][1] <= set1.list[set1.minY][1] + ); + }); + }) + ) + return false; + + // Get a convex hull from points of each subpath. Has the most complexity O(n·log n). + const hullNest1 = points1.list.map(convexHull); + const hullNest2 = points2.list.map(convexHull); + + // Check intersection of every subpath of the first path with every subpath of the second. + return hullNest1.some(function (hull1) { + if (hull1.list.length < 3) return false; + + return hullNest2.some(function (hull2) { + if (hull2.list.length < 3) return false; + + var simplex = [getSupport(hull1, hull2, [1, 0])], // create the initial simplex + direction = minus(simplex[0]); // set the direction to point towards the origin + + var iterations = 1e4; // infinite loop protection, 10 000 iterations is more than enough + // eslint-disable-next-line no-constant-condition + while (true) { + // eslint-disable-next-line no-constant-condition + if (iterations-- == 0) { + console.error( + 'Error: infinite loop while processing mergePaths plugin.' + ); + return true; // true is the safe value that means “do nothing with paths” + } + // add a new point + simplex.push(getSupport(hull1, hull2, direction)); + // see if the new point was on the correct side of the origin + if (dot(direction, simplex[simplex.length - 1]) <= 0) return false; + // process the simplex + if (processSimplex(simplex, direction)) return true; + } + }); + }); + + /** + * @type {(a: Point, b: Point, direction: Array<number>) => Array<number>} + */ + function getSupport(a, b, direction) { + return sub(supportPoint(a, direction), supportPoint(b, minus(direction))); + } + + // Computes farthest polygon point in particular direction. + // Thanks to knowledge of min/max x and y coordinates we can choose a quadrant to search in. + // Since we're working on convex hull, the dot product is increasing until we find the farthest point. + /** + * @type {(polygon: Point, direction: Array<number>) => Array<number>} + */ + function supportPoint(polygon, direction) { + var index = + direction[1] >= 0 + ? direction[0] < 0 + ? polygon.maxY + : polygon.maxX + : direction[0] < 0 + ? polygon.minX + : polygon.minY, + max = -Infinity, + value; + while ((value = dot(polygon.list[index], direction)) > max) { + max = value; + index = ++index % polygon.list.length; + } + return polygon.list[(index || polygon.list.length) - 1]; + } +}; + +/** + * @type {(simplex: Array<Array<number>>, direction: Array<number>) => boolean} + */ +function processSimplex(simplex, direction) { + // we only need to handle to 1-simplex and 2-simplex + if (simplex.length == 2) { + // 1-simplex + let a = simplex[1], + b = simplex[0], + AO = minus(simplex[1]), + AB = sub(b, a); + // AO is in the same direction as AB + if (dot(AO, AB) > 0) { + // get the vector perpendicular to AB facing O + set(direction, orth(AB, a)); + } else { + set(direction, AO); + // only A remains in the simplex + simplex.shift(); + } + } else { + // 2-simplex + let a = simplex[2], // [a, b, c] = simplex + b = simplex[1], + c = simplex[0], + AB = sub(b, a), + AC = sub(c, a), + AO = minus(a), + ACB = orth(AB, AC), // the vector perpendicular to AB facing away from C + ABC = orth(AC, AB); // the vector perpendicular to AC facing away from B + + if (dot(ACB, AO) > 0) { + if (dot(AB, AO) > 0) { + // region 4 + set(direction, ACB); + simplex.shift(); // simplex = [b, a] + } else { + // region 5 + set(direction, AO); + simplex.splice(0, 2); // simplex = [a] + } + } else if (dot(ABC, AO) > 0) { + if (dot(AC, AO) > 0) { + // region 6 + set(direction, ABC); + simplex.splice(1, 1); // simplex = [c, a] + } else { + // region 5 (again) + set(direction, AO); + simplex.splice(0, 2); // simplex = [a] + } + } // region 7 + else return true; + } + return false; +} + +/** + * @type {(v: Array<number>) => Array<number>} + */ +function minus(v) { + return [-v[0], -v[1]]; +} + +/** + * @type {(v1: Array<number>, v2: Array<number>) => Array<number>} + */ +function sub(v1, v2) { + return [v1[0] - v2[0], v1[1] - v2[1]]; +} + +/** + * @type {(v1: Array<number>, v2: Array<number>) => number} + */ +function dot(v1, v2) { + return v1[0] * v2[0] + v1[1] * v2[1]; +} + +/** + * @type {(v1: Array<number>, v2: Array<number>) => Array<number>} + */ +function orth(v, from) { + var o = [-v[1], v[0]]; + return dot(o, minus(from)) < 0 ? minus(o) : o; +} + +/** + * @typedef {{ + * list: Array<Array<number>>, + * minX: number, + * minY: number, + * maxX: number, + * maxY: number + * }} Point + */ + +/** + * @typedef {{ + * list: Array<Point>, + * minX: number, + * minY: number, + * maxX: number, + * maxY: number + * }} Points + */ + +/** + * @type {(pathData: Array<PathDataItem>) => Points} + */ +function gatherPoints(pathData) { + /** + * @type {Points} + */ + const points = { list: [], minX: 0, minY: 0, maxX: 0, maxY: 0 }; + + // Writes data about the extreme points on each axle + /** + * @type {(path: Point, point: Array<number>) => void} + */ + const addPoint = (path, point) => { + if (!path.list.length || point[1] > path.list[path.maxY][1]) { + path.maxY = path.list.length; + points.maxY = points.list.length + ? Math.max(point[1], points.maxY) + : point[1]; + } + if (!path.list.length || point[0] > path.list[path.maxX][0]) { + path.maxX = path.list.length; + points.maxX = points.list.length + ? Math.max(point[0], points.maxX) + : point[0]; + } + if (!path.list.length || point[1] < path.list[path.minY][1]) { + path.minY = path.list.length; + points.minY = points.list.length + ? Math.min(point[1], points.minY) + : point[1]; + } + if (!path.list.length || point[0] < path.list[path.minX][0]) { + path.minX = path.list.length; + points.minX = points.list.length + ? Math.min(point[0], points.minX) + : point[0]; + } + path.list.push(point); + }; + + for (let i = 0; i < pathData.length; i += 1) { + const pathDataItem = pathData[i]; + let subPath = + points.list.length === 0 + ? { list: [], minX: 0, minY: 0, maxX: 0, maxY: 0 } + : points.list[points.list.length - 1]; + let prev = i === 0 ? null : pathData[i - 1]; + let basePoint = + subPath.list.length === 0 ? null : subPath.list[subPath.list.length - 1]; + let data = pathDataItem.args; + let ctrlPoint = basePoint; + + /** + * @type {(n: number, i: number) => number} + * TODO fix null hack + */ + const toAbsolute = (n, i) => n + (basePoint == null ? 0 : basePoint[i % 2]); + + switch (pathDataItem.command) { + case 'M': + subPath = { list: [], minX: 0, minY: 0, maxX: 0, maxY: 0 }; + points.list.push(subPath); + break; + + case 'H': + if (basePoint != null) { + addPoint(subPath, [data[0], basePoint[1]]); + } + break; + + case 'V': + if (basePoint != null) { + addPoint(subPath, [basePoint[0], data[0]]); + } + break; + + case 'Q': + addPoint(subPath, data.slice(0, 2)); + prevCtrlPoint = [data[2] - data[0], data[3] - data[1]]; // Save control point for shorthand + break; + + case 'T': + if ( + basePoint != null && + prev != null && + (prev.command == 'Q' || prev.command == 'T') + ) { + ctrlPoint = [ + basePoint[0] + prevCtrlPoint[0], + basePoint[1] + prevCtrlPoint[1], + ]; + addPoint(subPath, ctrlPoint); + prevCtrlPoint = [data[0] - ctrlPoint[0], data[1] - ctrlPoint[1]]; + } + break; + + case 'C': + if (basePoint != null) { + // Approximate quibic Bezier curve with middle points between control points + addPoint(subPath, [ + 0.5 * (basePoint[0] + data[0]), + 0.5 * (basePoint[1] + data[1]), + ]); + } + addPoint(subPath, [ + 0.5 * (data[0] + data[2]), + 0.5 * (data[1] + data[3]), + ]); + addPoint(subPath, [ + 0.5 * (data[2] + data[4]), + 0.5 * (data[3] + data[5]), + ]); + prevCtrlPoint = [data[4] - data[2], data[5] - data[3]]; // Save control point for shorthand + break; + + case 'S': + if ( + basePoint != null && + prev != null && + (prev.command == 'C' || prev.command == 'S') + ) { + addPoint(subPath, [ + basePoint[0] + 0.5 * prevCtrlPoint[0], + basePoint[1] + 0.5 * prevCtrlPoint[1], + ]); + ctrlPoint = [ + basePoint[0] + prevCtrlPoint[0], + basePoint[1] + prevCtrlPoint[1], + ]; + } + if (ctrlPoint != null) { + addPoint(subPath, [ + 0.5 * (ctrlPoint[0] + data[0]), + 0.5 * (ctrlPoint[1] + data[1]), + ]); + } + addPoint(subPath, [ + 0.5 * (data[0] + data[2]), + 0.5 * (data[1] + data[3]), + ]); + prevCtrlPoint = [data[2] - data[0], data[3] - data[1]]; + break; + + case 'A': + if (basePoint != null) { + // Convert the arc to bezier curves and use the same approximation + // @ts-ignore no idea what's going on here + var curves = a2c.apply(0, basePoint.concat(data)); + for ( + var cData; + (cData = curves.splice(0, 6).map(toAbsolute)).length; + + ) { + if (basePoint != null) { + addPoint(subPath, [ + 0.5 * (basePoint[0] + cData[0]), + 0.5 * (basePoint[1] + cData[1]), + ]); + } + addPoint(subPath, [ + 0.5 * (cData[0] + cData[2]), + 0.5 * (cData[1] + cData[3]), + ]); + addPoint(subPath, [ + 0.5 * (cData[2] + cData[4]), + 0.5 * (cData[3] + cData[5]), + ]); + if (curves.length) addPoint(subPath, (basePoint = cData.slice(-2))); + } + } + break; + } + + // Save final command coordinates + if (data.length >= 2) addPoint(subPath, data.slice(-2)); + } + + return points; +} + +/** + * Forms a convex hull from set of points of every subpath using monotone chain convex hull algorithm. + * https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain + * + * @type {(points: Point) => Point} + */ +function convexHull(points) { + points.list.sort(function (a, b) { + return a[0] == b[0] ? a[1] - b[1] : a[0] - b[0]; + }); + + var lower = [], + minY = 0, + bottom = 0; + for (let i = 0; i < points.list.length; i++) { + while ( + lower.length >= 2 && + cross(lower[lower.length - 2], lower[lower.length - 1], points.list[i]) <= + 0 + ) { + lower.pop(); + } + if (points.list[i][1] < points.list[minY][1]) { + minY = i; + bottom = lower.length; + } + lower.push(points.list[i]); + } + + var upper = [], + maxY = points.list.length - 1, + top = 0; + for (let i = points.list.length; i--; ) { + while ( + upper.length >= 2 && + cross(upper[upper.length - 2], upper[upper.length - 1], points.list[i]) <= + 0 + ) { + upper.pop(); + } + if (points.list[i][1] > points.list[maxY][1]) { + maxY = i; + top = upper.length; + } + upper.push(points.list[i]); + } + + // last points are equal to starting points of the other part + upper.pop(); + lower.pop(); + + const hullList = lower.concat(upper); + + /** + * @type {Point} + */ + const hull = { + list: hullList, + minX: 0, // by sorting + maxX: lower.length, + minY: bottom, + maxY: (lower.length + top) % hullList.length, + }; + + return hull; +} + +/** + * @type {(o: Array<number>, a: Array<number>, b: Array<number>) => number} + */ +function cross(o, a, b) { + return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]); +} + +/** + * Based on code from Snap.svg (Apache 2 license). http://snapsvg.io/ + * Thanks to Dmitry Baranovskiy for his great work! + * + * @type {( + * x1: number, + * y1: number, + * rx: number, + * ry: number, + * angle: number, + * large_arc_flag: number, + * sweep_flag: number, + * x2: number, + * y2: number, + * recursive: Array<number> + * ) => Array<number>} + */ +const a2c = ( + x1, + y1, + rx, + ry, + angle, + large_arc_flag, + sweep_flag, + x2, + y2, + recursive +) => { + // for more information of where this Math came from visit: + // https://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes + const _120 = (Math.PI * 120) / 180; + const rad = (Math.PI / 180) * (+angle || 0); + /** + * @type {Array<number>} + */ + let res = []; + /** + * @type {(x: number, y: number, rad: number) => number} + */ + const rotateX = (x, y, rad) => { + return x * Math.cos(rad) - y * Math.sin(rad); + }; + /** + * @type {(x: number, y: number, rad: number) => number} + */ + const rotateY = (x, y, rad) => { + return x * Math.sin(rad) + y * Math.cos(rad); + }; + if (!recursive) { + x1 = rotateX(x1, y1, -rad); + y1 = rotateY(x1, y1, -rad); + x2 = rotateX(x2, y2, -rad); + y2 = rotateY(x2, y2, -rad); + var x = (x1 - x2) / 2, + y = (y1 - y2) / 2; + var h = (x * x) / (rx * rx) + (y * y) / (ry * ry); + if (h > 1) { + h = Math.sqrt(h); + rx = h * rx; + ry = h * ry; + } + var rx2 = rx * rx; + var ry2 = ry * ry; + var k = + (large_arc_flag == sweep_flag ? -1 : 1) * + Math.sqrt( + Math.abs( + (rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x) + ) + ); + var cx = (k * rx * y) / ry + (x1 + x2) / 2; + var cy = (k * -ry * x) / rx + (y1 + y2) / 2; + var f1 = Math.asin(Number(((y1 - cy) / ry).toFixed(9))); + var f2 = Math.asin(Number(((y2 - cy) / ry).toFixed(9))); + + f1 = x1 < cx ? Math.PI - f1 : f1; + f2 = x2 < cx ? Math.PI - f2 : f2; + f1 < 0 && (f1 = Math.PI * 2 + f1); + f2 < 0 && (f2 = Math.PI * 2 + f2); + if (sweep_flag && f1 > f2) { + f1 = f1 - Math.PI * 2; + } + if (!sweep_flag && f2 > f1) { + f2 = f2 - Math.PI * 2; + } + } else { + f1 = recursive[0]; + f2 = recursive[1]; + cx = recursive[2]; + cy = recursive[3]; + } + var df = f2 - f1; + if (Math.abs(df) > _120) { + var f2old = f2, + x2old = x2, + y2old = y2; + f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1); + x2 = cx + rx * Math.cos(f2); + y2 = cy + ry * Math.sin(f2); + res = a2c(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [ + f2, + f2old, + cx, + cy, + ]); + } + df = f2 - f1; + var c1 = Math.cos(f1), + s1 = Math.sin(f1), + c2 = Math.cos(f2), + s2 = Math.sin(f2), + t = Math.tan(df / 4), + hx = (4 / 3) * rx * t, + hy = (4 / 3) * ry * t, + m = [ + -hx * s1, + hy * c1, + x2 + hx * s2 - x1, + y2 - hy * c2 - y1, + x2 - x1, + y2 - y1, + ]; + if (recursive) { + return m.concat(res); + } else { + res = m.concat(res); + var newres = []; + for (var i = 0, n = res.length; i < n; i++) { + newres[i] = + i % 2 + ? rotateY(res[i - 1], res[i], rad) + : rotateX(res[i], res[i + 1], rad); + } + return newres; + } +}; diff --git a/node_modules/svgo/plugins/_transforms.js b/node_modules/svgo/plugins/_transforms.js new file mode 100644 index 00000000..159f1a3b --- /dev/null +++ b/node_modules/svgo/plugins/_transforms.js @@ -0,0 +1,379 @@ +'use strict'; + +const regTransformTypes = /matrix|translate|scale|rotate|skewX|skewY/; +const regTransformSplit = + /\s*(matrix|translate|scale|rotate|skewX|skewY)\s*\(\s*(.+?)\s*\)[\s,]*/; +const regNumericValues = /[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g; + +/** + * @typedef {{ name: string, data: Array<number> }} TransformItem + */ + +/** + * Convert transform string to JS representation. + * + * @type {(transformString: string) => Array<TransformItem>} + */ +exports.transform2js = (transformString) => { + // JS representation of the transform data + /** + * @type {Array<TransformItem>} + */ + const transforms = []; + // current transform context + /** + * @type {null | TransformItem} + */ + let current = null; + // split value into ['', 'translate', '10 50', '', 'scale', '2', '', 'rotate', '-45', ''] + for (const item of transformString.split(regTransformSplit)) { + var num; + if (item) { + // if item is a translate function + if (regTransformTypes.test(item)) { + // then collect it and change current context + current = { name: item, data: [] }; + transforms.push(current); + // else if item is data + } else { + // then split it into [10, 50] and collect as context.data + // eslint-disable-next-line no-cond-assign + while ((num = regNumericValues.exec(item))) { + num = Number(num); + if (current != null) { + current.data.push(num); + } + } + } + } + } + // return empty array if broken transform (no data) + return current == null || current.data.length == 0 ? [] : transforms; +}; + +/** + * Multiply transforms into one. + * + * @type {(transforms: Array<TransformItem>) => TransformItem} + */ +exports.transformsMultiply = (transforms) => { + // convert transforms objects to the matrices + const matrixData = transforms.map((transform) => { + if (transform.name === 'matrix') { + return transform.data; + } + return transformToMatrix(transform); + }); + // multiply all matrices into one + const matrixTransform = { + name: 'matrix', + data: + matrixData.length > 0 ? matrixData.reduce(multiplyTransformMatrices) : [], + }; + return matrixTransform; +}; + +/** + * math utilities in radians. + */ +const mth = { + /** + * @type {(deg: number) => number} + */ + rad: (deg) => { + return (deg * Math.PI) / 180; + }, + + /** + * @type {(rad: number) => number} + */ + deg: (rad) => { + return (rad * 180) / Math.PI; + }, + + /** + * @type {(deg: number) => number} + */ + cos: (deg) => { + return Math.cos(mth.rad(deg)); + }, + + /** + * @type {(val: number, floatPrecision: number) => number} + */ + acos: (val, floatPrecision) => { + return Number(mth.deg(Math.acos(val)).toFixed(floatPrecision)); + }, + + /** + * @type {(deg: number) => number} + */ + sin: (deg) => { + return Math.sin(mth.rad(deg)); + }, + + /** + * @type {(val: number, floatPrecision: number) => number} + */ + asin: (val, floatPrecision) => { + return Number(mth.deg(Math.asin(val)).toFixed(floatPrecision)); + }, + + /** + * @type {(deg: number) => number} + */ + tan: (deg) => { + return Math.tan(mth.rad(deg)); + }, + + /** + * @type {(val: number, floatPrecision: number) => number} + */ + atan: (val, floatPrecision) => { + return Number(mth.deg(Math.atan(val)).toFixed(floatPrecision)); + }, +}; + +/** + * @typedef {{ + * convertToShorts: boolean, + * floatPrecision: number, + * transformPrecision: number, + * matrixToTransform: boolean, + * shortTranslate: boolean, + * shortScale: boolean, + * shortRotate: boolean, + * removeUseless: boolean, + * collapseIntoOne: boolean, + * leadingZero: boolean, + * negativeExtraSpace: boolean, + * }} TransformParams + */ + +/** + * Decompose matrix into simple transforms. See + * https://frederic-wang.fr/decomposition-of-2d-transform-matrices.html + * + * @type {(transform: TransformItem, params: TransformParams) => Array<TransformItem>} + */ +exports.matrixToTransform = (transform, params) => { + let floatPrecision = params.floatPrecision; + let data = transform.data; + let transforms = []; + let sx = Number( + Math.hypot(data[0], data[1]).toFixed(params.transformPrecision) + ); + let sy = Number( + ((data[0] * data[3] - data[1] * data[2]) / sx).toFixed( + params.transformPrecision + ) + ); + let colsSum = data[0] * data[2] + data[1] * data[3]; + let rowsSum = data[0] * data[1] + data[2] * data[3]; + let scaleBefore = rowsSum != 0 || sx == sy; + + // [..., ..., ..., ..., tx, ty] → translate(tx, ty) + if (data[4] || data[5]) { + transforms.push({ + name: 'translate', + data: data.slice(4, data[5] ? 6 : 5), + }); + } + + // [sx, 0, tan(a)·sy, sy, 0, 0] → skewX(a)·scale(sx, sy) + if (!data[1] && data[2]) { + transforms.push({ + name: 'skewX', + data: [mth.atan(data[2] / sy, floatPrecision)], + }); + + // [sx, sx·tan(a), 0, sy, 0, 0] → skewY(a)·scale(sx, sy) + } else if (data[1] && !data[2]) { + transforms.push({ + name: 'skewY', + data: [mth.atan(data[1] / data[0], floatPrecision)], + }); + sx = data[0]; + sy = data[3]; + + // [sx·cos(a), sx·sin(a), sy·-sin(a), sy·cos(a), x, y] → rotate(a[, cx, cy])·(scale or skewX) or + // [sx·cos(a), sy·sin(a), sx·-sin(a), sy·cos(a), x, y] → scale(sx, sy)·rotate(a[, cx, cy]) (if !scaleBefore) + } else if (!colsSum || (sx == 1 && sy == 1) || !scaleBefore) { + if (!scaleBefore) { + sx = (data[0] < 0 ? -1 : 1) * Math.hypot(data[0], data[2]); + sy = (data[3] < 0 ? -1 : 1) * Math.hypot(data[1], data[3]); + transforms.push({ name: 'scale', data: [sx, sy] }); + } + var angle = Math.min(Math.max(-1, data[0] / sx), 1), + rotate = [ + mth.acos(angle, floatPrecision) * + ((scaleBefore ? 1 : sy) * data[1] < 0 ? -1 : 1), + ]; + + if (rotate[0]) transforms.push({ name: 'rotate', data: rotate }); + + if (rowsSum && colsSum) + transforms.push({ + name: 'skewX', + data: [mth.atan(colsSum / (sx * sx), floatPrecision)], + }); + + // rotate(a, cx, cy) can consume translate() within optional arguments cx, cy (rotation point) + if (rotate[0] && (data[4] || data[5])) { + transforms.shift(); + var cos = data[0] / sx, + sin = data[1] / (scaleBefore ? sx : sy), + x = data[4] * (scaleBefore ? 1 : sy), + y = data[5] * (scaleBefore ? 1 : sx), + denom = + (Math.pow(1 - cos, 2) + Math.pow(sin, 2)) * + (scaleBefore ? 1 : sx * sy); + rotate.push(((1 - cos) * x - sin * y) / denom); + rotate.push(((1 - cos) * y + sin * x) / denom); + } + + // Too many transformations, return original matrix if it isn't just a scale/translate + } else if (data[1] || data[2]) { + return [transform]; + } + + if ((scaleBefore && (sx != 1 || sy != 1)) || !transforms.length) + transforms.push({ + name: 'scale', + data: sx == sy ? [sx] : [sx, sy], + }); + + return transforms; +}; + +/** + * Convert transform to the matrix data. + * + * @type {(transform: TransformItem) => Array<number> } + */ +const transformToMatrix = (transform) => { + if (transform.name === 'matrix') { + return transform.data; + } + switch (transform.name) { + case 'translate': + // [1, 0, 0, 1, tx, ty] + return [1, 0, 0, 1, transform.data[0], transform.data[1] || 0]; + case 'scale': + // [sx, 0, 0, sy, 0, 0] + return [ + transform.data[0], + 0, + 0, + transform.data[1] || transform.data[0], + 0, + 0, + ]; + case 'rotate': + // [cos(a), sin(a), -sin(a), cos(a), x, y] + var cos = mth.cos(transform.data[0]), + sin = mth.sin(transform.data[0]), + cx = transform.data[1] || 0, + cy = transform.data[2] || 0; + return [ + cos, + sin, + -sin, + cos, + (1 - cos) * cx + sin * cy, + (1 - cos) * cy - sin * cx, + ]; + case 'skewX': + // [1, 0, tan(a), 1, 0, 0] + return [1, 0, mth.tan(transform.data[0]), 1, 0, 0]; + case 'skewY': + // [1, tan(a), 0, 1, 0, 0] + return [1, mth.tan(transform.data[0]), 0, 1, 0, 0]; + default: + throw Error(`Unknown transform ${transform.name}`); + } +}; + +/** + * Applies transformation to an arc. To do so, we represent ellipse as a matrix, multiply it + * by the transformation matrix and use a singular value decomposition to represent in a form + * rotate(θ)·scale(a b)·rotate(φ). This gives us new ellipse params a, b and θ. + * SVD is being done with the formulae provided by Wolffram|Alpha (svd {{m0, m2}, {m1, m3}}) + * + * @type {( + * cursor: [x: number, y: number], + * arc: Array<number>, + * transform: Array<number> + * ) => Array<number>} + */ +exports.transformArc = (cursor, arc, transform) => { + const x = arc[5] - cursor[0]; + const y = arc[6] - cursor[1]; + let a = arc[0]; + let b = arc[1]; + const rot = (arc[2] * Math.PI) / 180; + const cos = Math.cos(rot); + const sin = Math.sin(rot); + // skip if radius is 0 + if (a > 0 && b > 0) { + let h = + Math.pow(x * cos + y * sin, 2) / (4 * a * a) + + Math.pow(y * cos - x * sin, 2) / (4 * b * b); + if (h > 1) { + h = Math.sqrt(h); + a *= h; + b *= h; + } + } + const ellipse = [a * cos, a * sin, -b * sin, b * cos, 0, 0]; + const m = multiplyTransformMatrices(transform, ellipse); + // Decompose the new ellipse matrix + const lastCol = m[2] * m[2] + m[3] * m[3]; + const squareSum = m[0] * m[0] + m[1] * m[1] + lastCol; + const root = + Math.hypot(m[0] - m[3], m[1] + m[2]) * Math.hypot(m[0] + m[3], m[1] - m[2]); + + if (!root) { + // circle + arc[0] = arc[1] = Math.sqrt(squareSum / 2); + arc[2] = 0; + } else { + const majorAxisSqr = (squareSum + root) / 2; + const minorAxisSqr = (squareSum - root) / 2; + const major = Math.abs(majorAxisSqr - lastCol) > 1e-6; + const sub = (major ? majorAxisSqr : minorAxisSqr) - lastCol; + const rowsSum = m[0] * m[2] + m[1] * m[3]; + const term1 = m[0] * sub + m[2] * rowsSum; + const term2 = m[1] * sub + m[3] * rowsSum; + arc[0] = Math.sqrt(majorAxisSqr); + arc[1] = Math.sqrt(minorAxisSqr); + arc[2] = + (((major ? term2 < 0 : term1 > 0) ? -1 : 1) * + Math.acos((major ? term1 : term2) / Math.hypot(term1, term2)) * + 180) / + Math.PI; + } + + if (transform[0] < 0 !== transform[3] < 0) { + // Flip the sweep flag if coordinates are being flipped horizontally XOR vertically + arc[4] = 1 - arc[4]; + } + + return arc; +}; + +/** + * Multiply transformation matrices. + * + * @type {(a: Array<number>, b: Array<number>) => Array<number>} + */ +const multiplyTransformMatrices = (a, b) => { + return [ + a[0] * b[0] + a[2] * b[1], + a[1] * b[0] + a[3] * b[1], + a[0] * b[2] + a[2] * b[3], + a[1] * b[2] + a[3] * b[3], + a[0] * b[4] + a[2] * b[5] + a[4], + a[1] * b[4] + a[3] * b[5] + a[5], + ]; +}; diff --git a/node_modules/svgo/plugins/addAttributesToSVGElement.js b/node_modules/svgo/plugins/addAttributesToSVGElement.js new file mode 100644 index 00000000..845cb056 --- /dev/null +++ b/node_modules/svgo/plugins/addAttributesToSVGElement.js @@ -0,0 +1,82 @@ +'use strict'; + +exports.name = 'addAttributesToSVGElement'; +exports.description = 'adds attributes to an outer <svg> element'; + +var ENOCLS = `Error in plugin "addAttributesToSVGElement": absent parameters. +It should have a list of "attributes" or one "attribute". +Config example: + +plugins: [ + { + name: 'addAttributesToSVGElement', + params: { + attribute: "mySvg" + } + } +] + +plugins: [ + { + name: 'addAttributesToSVGElement', + params: { + attributes: ["mySvg", "size-big"] + } + } +] + +plugins: [ + { + name: 'addAttributesToSVGElement', + params: { + attributes: [ + { + focusable: false + }, + { + 'data-image': icon + } + ] + } + } +] +`; + +/** + * Add attributes to an outer <svg> element. Example config: + * + * @author April Arcus + * + * @type {import('./plugins-types').Plugin<'addAttributesToSVGElement'>} + */ +exports.fn = (root, params) => { + if (!Array.isArray(params.attributes) && !params.attribute) { + console.error(ENOCLS); + return null; + } + const attributes = params.attributes || [params.attribute]; + return { + element: { + enter: (node, parentNode) => { + if (node.name === 'svg' && parentNode.type === 'root') { + for (const attribute of attributes) { + if (typeof attribute === 'string') { + if (node.attributes[attribute] == null) { + // @ts-ignore disallow explicit nullable attribute value + node.attributes[attribute] = undefined; + } + } + if (typeof attribute === 'object') { + for (const key of Object.keys(attribute)) { + if (node.attributes[key] == null) { + // @ts-ignore disallow explicit nullable attribute value + node.attributes[key] = attribute[key]; + } + } + } + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/addClassesToSVGElement.js b/node_modules/svgo/plugins/addClassesToSVGElement.js new file mode 100644 index 00000000..9a015c7b --- /dev/null +++ b/node_modules/svgo/plugins/addClassesToSVGElement.js @@ -0,0 +1,82 @@ +'use strict'; + +exports.name = 'addClassesToSVGElement'; +exports.description = 'adds classnames to an outer <svg> element'; + +var ENOCLS = `Error in plugin "addClassesToSVGElement": absent parameters. +It should have a list of classes in "classNames" or one "className". +Config example: + +plugins: [ + { + name: "addClassesToSVGElement", + params: { + className: "mySvg" + } + } +] + +plugins: [ + { + name: "addClassesToSVGElement", + params: { + classNames: ["mySvg", "size-big"] + } + } +] +`; + +/** + * Add classnames to an outer <svg> element. Example config: + * + * plugins: [ + * { + * name: "addClassesToSVGElement", + * params: { + * className: "mySvg" + * } + * } + * ] + * + * plugins: [ + * { + * name: "addClassesToSVGElement", + * params: { + * classNames: ["mySvg", "size-big"] + * } + * } + * ] + * + * @author April Arcus + * + * @type {import('./plugins-types').Plugin<'addClassesToSVGElement'>} + */ +exports.fn = (root, params) => { + if ( + !(Array.isArray(params.classNames) && params.classNames.some(String)) && + !params.className + ) { + console.error(ENOCLS); + return null; + } + const classNames = params.classNames || [params.className]; + return { + element: { + enter: (node, parentNode) => { + if (node.name === 'svg' && parentNode.type === 'root') { + const classList = new Set( + node.attributes.class == null + ? null + : node.attributes.class.split(' ') + ); + for (const className of classNames) { + if (className != null) { + classList.add(className); + } + } + node.attributes.class = Array.from(classList).join(' '); + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/applyTransforms.js b/node_modules/svgo/plugins/applyTransforms.js new file mode 100644 index 00000000..bf60ac89 --- /dev/null +++ b/node_modules/svgo/plugins/applyTransforms.js @@ -0,0 +1,379 @@ +'use strict'; + +/** + * @typedef {import('../lib/types').PathDataItem} PathDataItem + * @typedef {import('../lib/types').XastElement} XastElement + */ + +const { collectStylesheet, computeStyle } = require('../lib/style.js'); +const { + transformsMultiply, + transform2js, + transformArc, +} = require('./_transforms.js'); +const { path2js } = require('./_path.js'); +const { removeLeadingZero } = require('../lib/svgo/tools.js'); +const { referencesProps, attrsGroupsDefaults } = require('./_collections.js'); + +/** + * @typedef {Array<PathDataItem>} PathData + * @typedef {Array<number>} Matrix + */ + +const regNumericValues = /[-+]?(\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g; + +/** + * Apply transformation(s) to the Path data. + * + * @type {import('../lib/types').Plugin<{ + * transformPrecision: number, + * applyTransformsStroked: boolean, + * }>} + */ +const applyTransforms = (root, params) => { + const stylesheet = collectStylesheet(root); + return { + element: { + enter: (node) => { + const computedStyle = computeStyle(stylesheet, node); + + // used only for paths for now + if (node.attributes.d == null) { + return; + } + + // stroke and stroke-width can be redefined with <use> + if (node.attributes.id != null) { + return; + } + + // if there are no 'stroke' attr and references to other objects such as + // gradiends or clip-path which are also subjects to transform. + if ( + node.attributes.transform == null || + node.attributes.transform === '' || + // styles are not considered when applying transform + // can be fixed properly with new style engine + node.attributes.style != null || + Object.entries(node.attributes).some( + ([name, value]) => + referencesProps.includes(name) && value.includes('url(') + ) + ) { + return; + } + + const matrix = transformsMultiply( + transform2js(node.attributes.transform) + ); + const stroke = + computedStyle.stroke != null && computedStyle.stroke.type === 'static' + ? computedStyle.stroke.value + : null; + + const strokeWidth = + computedStyle['stroke-width'] != null && + 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') + ) { + return; + } + + const scale = Number( + Math.sqrt( + matrix.data[0] * matrix.data[0] + matrix.data[1] * matrix.data[1] + ).toFixed(transformPrecision) + ); + + if (stroke && stroke != 'none') { + if (params.applyTransformsStroked === false) { + return; + } + + // stroke cannot be transformed with different vertical and horizontal scale or skew + if ( + (matrix.data[0] !== matrix.data[3] || + matrix.data[1] !== -matrix.data[2]) && + (matrix.data[0] !== -matrix.data[3] || + matrix.data[1] !== matrix.data[2]) + ) { + return; + } + + // apply transform to stroke-width, stroke-dashoffset and stroke-dasharray + if (scale !== 1) { + if (node.attributes['vector-effect'] !== 'non-scaling-stroke') { + node.attributes['stroke-width'] = ( + strokeWidth || attrsGroupsDefaults.presentation['stroke-width'] + ) + .trim() + .replace(regNumericValues, (num) => + removeLeadingZero(Number(num) * scale) + ); + + if (node.attributes['stroke-dashoffset'] != null) { + node.attributes['stroke-dashoffset'] = node.attributes[ + 'stroke-dashoffset' + ] + .trim() + .replace(regNumericValues, (num) => + removeLeadingZero(Number(num) * scale) + ); + } + + if (node.attributes['stroke-dasharray'] != null) { + node.attributes['stroke-dasharray'] = node.attributes[ + 'stroke-dasharray' + ] + .trim() + .replace(regNumericValues, (num) => + removeLeadingZero(Number(num) * scale) + ); + } + } + } + } + + const pathData = path2js(node); + applyMatrixToPathData(pathData, matrix.data); + + // remove transform attr + delete node.attributes.transform; + }, + }, + }; +}; +exports.applyTransforms = applyTransforms; + +/** + * @type {(matrix: Matrix, x: number, y: number) => [number, number]} + */ +const transformAbsolutePoint = (matrix, x, y) => { + const newX = matrix[0] * x + matrix[2] * y + matrix[4]; + const newY = matrix[1] * x + matrix[3] * y + matrix[5]; + return [newX, newY]; +}; + +/** + * @type {(matrix: Matrix, x: number, y: number) => [number, number]} + */ +const transformRelativePoint = (matrix, x, y) => { + const newX = matrix[0] * x + matrix[2] * y; + const newY = matrix[1] * x + matrix[3] * y; + return [newX, newY]; +}; + +/** + * @type {(pathData: PathData, matrix: Matrix) => void} + */ +const applyMatrixToPathData = (pathData, matrix) => { + /** + * @type {[number, number]} + */ + const start = [0, 0]; + /** + * @type {[number, number]} + */ + const cursor = [0, 0]; + + for (const pathItem of pathData) { + let { command, args } = pathItem; + + // moveto (x y) + if (command === 'M') { + cursor[0] = args[0]; + cursor[1] = args[1]; + start[0] = cursor[0]; + start[1] = cursor[1]; + const [x, y] = transformAbsolutePoint(matrix, args[0], args[1]); + args[0] = x; + args[1] = y; + } + if (command === 'm') { + cursor[0] += args[0]; + cursor[1] += args[1]; + start[0] = cursor[0]; + start[1] = cursor[1]; + const [x, y] = transformRelativePoint(matrix, args[0], args[1]); + args[0] = x; + args[1] = y; + } + + // horizontal lineto (x) + // convert to lineto to handle two-dimentional transforms + if (command === 'H') { + command = 'L'; + args = [args[0], cursor[1]]; + } + if (command === 'h') { + command = 'l'; + args = [args[0], 0]; + } + + // vertical lineto (y) + // convert to lineto to handle two-dimentional transforms + if (command === 'V') { + command = 'L'; + args = [cursor[0], args[0]]; + } + if (command === 'v') { + command = 'l'; + args = [0, args[0]]; + } + + // lineto (x y) + if (command === 'L') { + cursor[0] = args[0]; + cursor[1] = args[1]; + const [x, y] = transformAbsolutePoint(matrix, args[0], args[1]); + args[0] = x; + args[1] = y; + } + if (command === 'l') { + cursor[0] += args[0]; + cursor[1] += args[1]; + const [x, y] = transformRelativePoint(matrix, args[0], args[1]); + args[0] = x; + args[1] = y; + } + + // curveto (x1 y1 x2 y2 x y) + if (command === 'C') { + cursor[0] = args[4]; + cursor[1] = args[5]; + const [x1, y1] = transformAbsolutePoint(matrix, args[0], args[1]); + const [x2, y2] = transformAbsolutePoint(matrix, args[2], args[3]); + const [x, y] = transformAbsolutePoint(matrix, args[4], args[5]); + args[0] = x1; + args[1] = y1; + args[2] = x2; + args[3] = y2; + args[4] = x; + args[5] = y; + } + if (command === 'c') { + cursor[0] += args[4]; + cursor[1] += args[5]; + const [x1, y1] = transformRelativePoint(matrix, args[0], args[1]); + const [x2, y2] = transformRelativePoint(matrix, args[2], args[3]); + const [x, y] = transformRelativePoint(matrix, args[4], args[5]); + args[0] = x1; + args[1] = y1; + args[2] = x2; + args[3] = y2; + args[4] = x; + args[5] = y; + } + + // smooth curveto (x2 y2 x y) + if (command === 'S') { + cursor[0] = args[2]; + cursor[1] = args[3]; + const [x2, y2] = transformAbsolutePoint(matrix, args[0], args[1]); + const [x, y] = transformAbsolutePoint(matrix, args[2], args[3]); + args[0] = x2; + args[1] = y2; + args[2] = x; + args[3] = y; + } + if (command === 's') { + cursor[0] += args[2]; + cursor[1] += args[3]; + const [x2, y2] = transformRelativePoint(matrix, args[0], args[1]); + const [x, y] = transformRelativePoint(matrix, args[2], args[3]); + args[0] = x2; + args[1] = y2; + args[2] = x; + args[3] = y; + } + + // quadratic Bézier curveto (x1 y1 x y) + if (command === 'Q') { + cursor[0] = args[2]; + cursor[1] = args[3]; + const [x1, y1] = transformAbsolutePoint(matrix, args[0], args[1]); + const [x, y] = transformAbsolutePoint(matrix, args[2], args[3]); + args[0] = x1; + args[1] = y1; + args[2] = x; + args[3] = y; + } + if (command === 'q') { + cursor[0] += args[2]; + cursor[1] += args[3]; + const [x1, y1] = transformRelativePoint(matrix, args[0], args[1]); + const [x, y] = transformRelativePoint(matrix, args[2], args[3]); + args[0] = x1; + args[1] = y1; + args[2] = x; + args[3] = y; + } + + // smooth quadratic Bézier curveto (x y) + if (command === 'T') { + cursor[0] = args[0]; + cursor[1] = args[1]; + const [x, y] = transformAbsolutePoint(matrix, args[0], args[1]); + args[0] = x; + args[1] = y; + } + if (command === 't') { + cursor[0] += args[0]; + cursor[1] += args[1]; + const [x, y] = transformRelativePoint(matrix, args[0], args[1]); + args[0] = x; + args[1] = y; + } + + // elliptical arc (rx ry x-axis-rotation large-arc-flag sweep-flag x y) + if (command === 'A') { + transformArc(cursor, args, matrix); + cursor[0] = args[5]; + cursor[1] = args[6]; + // reduce number of digits in rotation angle + if (Math.abs(args[2]) > 80) { + const a = args[0]; + const rotation = args[2]; + args[0] = args[1]; + args[1] = a; + args[2] = rotation + (rotation > 0 ? -90 : 90); + } + const [x, y] = transformAbsolutePoint(matrix, args[5], args[6]); + args[5] = x; + args[6] = y; + } + if (command === 'a') { + transformArc([0, 0], args, matrix); + cursor[0] += args[5]; + cursor[1] += args[6]; + // reduce number of digits in rotation angle + if (Math.abs(args[2]) > 80) { + const a = args[0]; + const rotation = args[2]; + args[0] = args[1]; + args[1] = a; + args[2] = rotation + (rotation > 0 ? -90 : 90); + } + const [x, y] = transformRelativePoint(matrix, args[5], args[6]); + args[5] = x; + args[6] = y; + } + + // closepath + if (command === 'z' || command === 'Z') { + cursor[0] = start[0]; + cursor[1] = start[1]; + } + + pathItem.command = command; + pathItem.args = args; + } +}; diff --git a/node_modules/svgo/plugins/cleanupAttrs.js b/node_modules/svgo/plugins/cleanupAttrs.js new file mode 100644 index 00000000..399de194 --- /dev/null +++ b/node_modules/svgo/plugins/cleanupAttrs.js @@ -0,0 +1,49 @@ +'use strict'; + +exports.name = 'cleanupAttrs'; +exports.description = + 'cleanups attributes from newlines, trailing and repeating spaces'; + +const regNewlinesNeedSpace = /(\S)\r?\n(\S)/g; +const regNewlines = /\r?\n/g; +const regSpaces = /\s{2,}/g; + +/** + * Cleanup attributes values from newlines, trailing and repeating spaces. + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'cleanupAttrs'>} + */ +exports.fn = (root, params) => { + const { newlines = true, trim = true, spaces = true } = params; + return { + element: { + enter: (node) => { + for (const name of Object.keys(node.attributes)) { + if (newlines) { + // new line which requires a space instead of themselve + node.attributes[name] = node.attributes[name].replace( + regNewlinesNeedSpace, + (match, p1, p2) => p1 + ' ' + p2 + ); + // simple new line + node.attributes[name] = node.attributes[name].replace( + regNewlines, + '' + ); + } + if (trim) { + node.attributes[name] = node.attributes[name].trim(); + } + if (spaces) { + node.attributes[name] = node.attributes[name].replace( + regSpaces, + ' ' + ); + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/cleanupEnableBackground.js b/node_modules/svgo/plugins/cleanupEnableBackground.js new file mode 100644 index 00000000..80be32be --- /dev/null +++ b/node_modules/svgo/plugins/cleanupEnableBackground.js @@ -0,0 +1,73 @@ +'use strict'; + +const { visit } = require('../lib/xast.js'); + +exports.name = 'cleanupEnableBackground'; +exports.description = + 'remove or cleanup enable-background attribute when possible'; + +/** + * 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) => { + if (node.name === 'filter') { + hasFilter = true; + } + }, + }, + }); + + return { + element: { + enter: (node) => { + if (node.attributes['enable-background'] == null) { + return; + } + if (hasFilter) { + if ( + (node.name === 'svg' || + node.name === 'mask' || + node.name === 'pattern') && + node.attributes.width != null && + node.attributes.height != null + ) { + 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'; + } + } + } + } else { + //we don't need 'enable-background' if we have no filters + delete node.attributes['enable-background']; + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/cleanupIds.js b/node_modules/svgo/plugins/cleanupIds.js new file mode 100644 index 00000000..1d2532b2 --- /dev/null +++ b/node_modules/svgo/plugins/cleanupIds.js @@ -0,0 +1,287 @@ +'use strict'; + +/** + * @typedef {import('../lib/types').XastElement} XastElement + */ + +const { visitSkip } = require('../lib/xast.js'); +const { referencesProps } = require('./_collections.js'); + +exports.name = 'cleanupIds'; +exports.description = 'removes unused IDs and minifies used'; + +const regReferencesUrl = /\burl\((["'])?#(.+?)\1\)/; +const regReferencesHref = /^#(.+?)$/; +const regReferencesBegin = /(\D+)\./; +const generateIdChars = [ + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + 'm', + 'n', + 'o', + 'p', + 'q', + 'r', + 's', + 't', + 'u', + 'v', + 'w', + 'x', + 'y', + 'z', + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + 'H', + 'I', + 'J', + 'K', + 'L', + 'M', + 'N', + 'O', + 'P', + 'Q', + 'R', + 'S', + 'T', + 'U', + 'V', + 'W', + 'X', + 'Y', + 'Z', +]; +const maxIdIndex = generateIdChars.length - 1; + +/** + * Check if an ID starts with any one of a list of strings. + * + * @type {(string: string, prefixes: Array<string>) => boolean} + */ +const hasStringPrefix = (string, prefixes) => { + for (const prefix of prefixes) { + if (string.startsWith(prefix)) { + return true; + } + } + return false; +}; + +/** + * Generate unique minimal ID. + * + * @type {(currentId: null | Array<number>) => Array<number>} + */ +const generateId = (currentId) => { + if (currentId == null) { + return [0]; + } + currentId[currentId.length - 1] += 1; + for (let i = currentId.length - 1; i > 0; i--) { + if (currentId[i] > maxIdIndex) { + currentId[i] = 0; + if (currentId[i - 1] !== undefined) { + currentId[i - 1]++; + } + } + } + if (currentId[0] > maxIdIndex) { + currentId[0] = 0; + currentId.unshift(0); + } + return currentId; +}; + +/** + * Get string from generated ID array. + * + * @type {(arr: Array<number>) => string} + */ +const getIdString = (arr) => { + return arr.map((i) => generateIdChars[i]).join(''); +}; + +/** + * Remove unused and minify used IDs + * (only if there are no any <style> or <script>). + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'cleanupIds'>} + */ +exports.fn = (_root, params) => { + const { + remove = true, + minify = true, + preserve = [], + preservePrefixes = [], + force = false, + } = params; + const preserveIds = new Set( + Array.isArray(preserve) ? preserve : preserve ? [preserve] : [] + ); + const preserveIdPrefixes = Array.isArray(preservePrefixes) + ? preservePrefixes + : preservePrefixes + ? [preservePrefixes] + : []; + /** + * @type {Map<string, XastElement>} + */ + const nodeById = new Map(); + /** + * @type {Map<string, Array<{element: XastElement, name: string, value: string }>>} + */ + const referencesById = new Map(); + let deoptimized = false; + + return { + element: { + enter: (node) => { + if (force == false) { + // deoptimize if style or script elements are present + if ( + (node.name === 'style' || node.name === 'script') && + node.children.length !== 0 + ) { + deoptimized = true; + return; + } + + // avoid removing IDs if the whole SVG consists only of defs + if (node.name === 'svg') { + let hasDefsOnly = true; + for (const child of node.children) { + if (child.type !== 'element' || child.name !== 'defs') { + hasDefsOnly = false; + break; + } + } + if (hasDefsOnly) { + return visitSkip; + } + } + } + + for (const [name, value] of Object.entries(node.attributes)) { + if (name === 'id') { + // collect all ids + const id = value; + if (nodeById.has(id)) { + delete node.attributes.id; // remove repeated id + } else { + 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) { + let refs = referencesById.get(id); + if (refs == null) { + refs = []; + referencesById.set(id, refs); + } + refs.push({ element: node, name, value }); + } + } + } + }, + }, + + root: { + exit: () => { + if (deoptimized) { + return; + } + /** + * @type {(id: string) => boolean} + **/ + const isIdPreserved = (id) => + preserveIds.has(id) || hasStringPrefix(id, preserveIdPrefixes); + /** + * @type {null | Array<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} + */ + let currentIdString = null; + do { + currentId = generateId(currentId); + currentIdString = getIdString(currentId); + } while (isIdPreserved(currentIdString)); + node.attributes.id = currentIdString; + for (const { element, name, value } of refs) { + if (value.includes('#')) { + // replace id in href and url() + element.attributes[name] = value.replace( + `#${id}`, + `#${currentIdString}` + ); + } else { + // replace id in begin attribute + element.attributes[name] = value.replace( + `${id}.`, + `${currentIdString}.` + ); + } + } + } + // keep referenced node + nodeById.delete(id); + } + } + // remove non-referenced IDs attributes from elements + if (remove) { + for (const [id, node] of nodeById) { + if (isIdPreserved(id) === false) { + delete node.attributes.id; + } + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/cleanupListOfValues.js b/node_modules/svgo/plugins/cleanupListOfValues.js new file mode 100644 index 00000000..7150449e --- /dev/null +++ b/node_modules/svgo/plugins/cleanupListOfValues.js @@ -0,0 +1,147 @@ +'use strict'; + +const { removeLeadingZero } = require('../lib/svgo/tools.js'); + +exports.name = 'cleanupListOfValues'; +exports.description = 'rounds list of values to the fixed precision'; + +const regNumericValues = + /^([-+]?\d*\.?\d+([eE][-+]?\d+)?)(px|pt|pc|mm|cm|m|in|ft|em|ex|%)?$/; +const regSeparator = /\s+,?\s*|,\s*/; +const absoluteLengths = { + // relative to px + cm: 96 / 2.54, + mm: 96 / 25.4, + in: 96, + pt: 4 / 3, + pc: 16, + px: 1, +}; + +/** + * Round list of values to the fixed precision. + * + * @example + * <svg viewBox="0 0 200.28423 200.28423" enable-background="new 0 0 200.28423 200.28423"> + * ⬇ + * <svg viewBox="0 0 200.284 200.284" enable-background="new 0 0 200.284 200.284"> + * + * <polygon points="208.250977 77.1308594 223.069336 ... "/> + * ⬇ + * <polygon points="208.251 77.131 223.069 ... "/> + * + * @author kiyopikko + * + * @type {import('./plugins-types').Plugin<'cleanupListOfValues'>} + */ +exports.fn = (_root, params) => { + const { + floatPrecision = 3, + leadingZero = true, + defaultPx = true, + convertToPx = true, + } = params; + + /** + * @type {(lists: string) => string} + */ + const roundValues = (lists) => { + const roundedList = []; + + for (const elem of lists.split(regSeparator)) { + const match = elem.match(regNumericValues); + const matchNew = elem.match(/new/); + + // if attribute value matches regNumericValues + if (match) { + // round it to the fixed precision + let num = Number(Number(match[1]).toFixed(floatPrecision)); + /** + * @type {any} + */ + let matchedUnit = match[3] || ''; + /** + * @type{'' | keyof typeof absoluteLengths} + */ + let units = matchedUnit; + + // convert absolute values to pixels + if (convertToPx && units && units in absoluteLengths) { + const pxNum = Number( + (absoluteLengths[units] * Number(match[1])).toFixed(floatPrecision) + ); + + if (pxNum.toString().length < match[0].length) { + num = pxNum; + units = 'px'; + } + } + + // and remove leading zero + let str; + if (leadingZero) { + str = removeLeadingZero(num); + } else { + str = num.toString(); + } + + // remove default 'px' units + if (defaultPx && units === 'px') { + units = ''; + } + + roundedList.push(str + units); + } + // if attribute value is "new"(only enable-background). + else if (matchNew) { + roundedList.push('new'); + } else if (elem) { + roundedList.push(elem); + } + } + + return roundedList.join(' '); + }; + + return { + element: { + enter: (node) => { + if (node.attributes.points != null) { + node.attributes.points = roundValues(node.attributes.points); + } + + if (node.attributes['enable-background'] != null) { + node.attributes['enable-background'] = roundValues( + node.attributes['enable-background'] + ); + } + + if (node.attributes.viewBox != null) { + node.attributes.viewBox = roundValues(node.attributes.viewBox); + } + + if (node.attributes['stroke-dasharray'] != null) { + node.attributes['stroke-dasharray'] = roundValues( + node.attributes['stroke-dasharray'] + ); + } + + if (node.attributes.dx != null) { + node.attributes.dx = roundValues(node.attributes.dx); + } + + if (node.attributes.dy != null) { + node.attributes.dy = roundValues(node.attributes.dy); + } + + if (node.attributes.x != null) { + node.attributes.x = roundValues(node.attributes.x); + } + + if (node.attributes.y != null) { + node.attributes.y = roundValues(node.attributes.y); + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/cleanupNumericValues.js b/node_modules/svgo/plugins/cleanupNumericValues.js new file mode 100644 index 00000000..6a15dddd --- /dev/null +++ b/node_modules/svgo/plugins/cleanupNumericValues.js @@ -0,0 +1,106 @@ +'use strict'; + +const { removeLeadingZero } = require('../lib/svgo/tools'); + +exports.name = 'cleanupNumericValues'; +exports.description = + 'rounds numeric values to the fixed precision, removes default ‘px’ units'; + +const regNumericValues = + /^([-+]?\d*\.?\d+([eE][-+]?\d+)?)(px|pt|pc|mm|cm|m|in|ft|em|ex|%)?$/; + +const absoluteLengths = { + // relative to px + cm: 96 / 2.54, + mm: 96 / 25.4, + in: 96, + pt: 4 / 3, + pc: 16, + px: 1, +}; + +/** + * Round numeric values to the fixed precision, + * remove default 'px' units. + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'cleanupNumericValues'>} + */ +exports.fn = (_root, params) => { + const { + floatPrecision = 3, + leadingZero = true, + defaultPx = true, + convertToPx = true, + } = params; + + return { + element: { + enter: (node) => { + if (node.attributes.viewBox != null) { + const nums = node.attributes.viewBox.split(/\s,?\s*|,\s*/g); + node.attributes.viewBox = nums + .map((value) => { + const num = Number(value); + return Number.isNaN(num) + ? value + : Number(num.toFixed(floatPrecision)); + }) + .join(' '); + } + + for (const [name, value] of Object.entries(node.attributes)) { + // The `version` attribute is a text string and cannot be rounded + if (name === 'version') { + continue; + } + + const match = value.match(regNumericValues); + + // if attribute value matches regNumericValues + if (match) { + // round it to the fixed precision + let num = Number(Number(match[1]).toFixed(floatPrecision)); + /** + * @type {any} + */ + let matchedUnit = match[3] || ''; + /** + * @type{'' | keyof typeof absoluteLengths} + */ + let units = matchedUnit; + + // convert absolute values to pixels + if (convertToPx && units !== '' && units in absoluteLengths) { + const pxNum = Number( + (absoluteLengths[units] * Number(match[1])).toFixed( + floatPrecision + ) + ); + if (pxNum.toString().length < match[0].length) { + num = pxNum; + units = 'px'; + } + } + + // and remove leading zero + let str; + if (leadingZero) { + str = removeLeadingZero(num); + } else { + str = num.toString(); + } + + // remove default 'px' units + if (defaultPx && units === 'px') { + units = ''; + } + + node.attributes[name] = str + units; + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/collapseGroups.js b/node_modules/svgo/plugins/collapseGroups.js new file mode 100644 index 00000000..ff38caff --- /dev/null +++ b/node_modules/svgo/plugins/collapseGroups.js @@ -0,0 +1,134 @@ +'use strict'; + +/** + * @typedef {import('../lib/types').XastNode} XastNode + */ + +const { inheritableAttrs, elemsGroups } = require('./_collections.js'); + +exports.name = 'collapseGroups'; +exports.description = 'collapses useless groups'; + +/** + * @type {(node: XastNode, name: string) => boolean} + */ +const hasAnimatedAttr = (node, name) => { + if (node.type === 'element') { + if ( + elemsGroups.animation.includes(node.name) && + node.attributes.attributeName === name + ) { + return true; + } + for (const child of node.children) { + if (hasAnimatedAttr(child, name)) { + return true; + } + } + } + return false; +}; + +/** + * Collapse useless groups. + * + * @example + * <g> + * <g attr1="val1"> + * <path d="..."/> + * </g> + * </g> + * ⬇ + * <g> + * <g> + * <path attr1="val1" d="..."/> + * </g> + * </g> + * ⬇ + * <path attr1="val1" d="..."/> + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'collapseGroups'>} + */ +exports.fn = () => { + return { + element: { + exit: (node, parentNode) => { + if (parentNode.type === 'root' || parentNode.name === 'switch') { + return; + } + // non-empty groups + if (node.name !== 'g' || node.children.length === 0) { + return; + } + + // move group attibutes to the single child element + if ( + Object.keys(node.attributes).length !== 0 && + node.children.length === 1 + ) { + const firstChild = node.children[0]; + // TODO untangle this mess + if ( + firstChild.type === 'element' && + firstChild.attributes.id == null && + node.attributes.filter == null && + (node.attributes.class == null || + firstChild.attributes.class == null) && + ((node.attributes['clip-path'] == null && + node.attributes.mask == null) || + (firstChild.name === 'g' && + node.attributes.transform == null && + firstChild.attributes.transform == null)) + ) { + for (const [name, value] of Object.entries(node.attributes)) { + // avoid copying to not conflict with animated attribute + if (hasAnimatedAttr(firstChild, name)) { + return; + } + if (firstChild.attributes[name] == null) { + firstChild.attributes[name] = value; + } else if (name === 'transform') { + firstChild.attributes[name] = + value + ' ' + firstChild.attributes[name]; + } else if (firstChild.attributes[name] === 'inherit') { + firstChild.attributes[name] = value; + } else if ( + inheritableAttrs.includes(name) === false && + firstChild.attributes[name] !== value + ) { + return; + } + delete node.attributes[name]; + } + } + } + + // collapse groups without attributes + if (Object.keys(node.attributes).length === 0) { + // animation elements "add" attributes to group + // group should be preserved + for (const child of node.children) { + if ( + child.type === 'element' && + elemsGroups.animation.includes(child.name) + ) { + return; + } + } + // replace current node with all its children + const index = parentNode.children.indexOf(node); + parentNode.children.splice(index, 1, ...node.children); + // TODO remove legacy parentNode in v4 + for (const child of node.children) { + Object.defineProperty(child, 'parentNode', { + writable: true, + value: parentNode, + }); + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/convertColors.js b/node_modules/svgo/plugins/convertColors.js new file mode 100644 index 00000000..c9609d8b --- /dev/null +++ b/node_modules/svgo/plugins/convertColors.js @@ -0,0 +1,144 @@ +'use strict'; + +const collections = require('./_collections.js'); + +exports.name = 'convertColors'; +exports.description = 'converts colors: rgb() to #rrggbb and #rrggbb to #rgb'; + +const rNumber = '([+-]?(?:\\d*\\.\\d+|\\d+\\.?)%?)'; +const rComma = '\\s*,\\s*'; +const regRGB = new RegExp( + '^rgb\\(\\s*' + rNumber + rComma + rNumber + rComma + rNumber + '\\s*\\)$' +); +const regHEX = /^#(([a-fA-F0-9])\2){3}$/; + +/** + * Convert [r, g, b] to #rrggbb. + * + * @see https://gist.github.com/983535 + * + * @example + * rgb2hex([255, 255, 255]) // '#ffffff' + * + * @author Jed Schmidt + * + * @type {(rgb: Array<number>) => string} + */ +const convertRgbToHex = ([r, g, b]) => { + // combine the octets into a 32-bit integer as: [1][r][g][b] + const hexNumber = + // operator precedence is (+) > (<<) > (|) + ((((256 + // [1][0] + r) << // [1][r] + 8) | // [1][r][0] + g) << // [1][r][g] + 8) | // [1][r][g][0] + b; + // serialize [1][r][g][b] to a hex string, and + // remove the 1 to get the number with 0s intact + return '#' + hexNumber.toString(16).slice(1).toUpperCase(); +}; + +/** + * Convert different colors formats in element attributes to hex. + * + * @see https://www.w3.org/TR/SVG11/types.html#DataTypeColor + * @see https://www.w3.org/TR/SVG11/single-page.html#types-ColorKeywords + * + * @example + * Convert color name keyword to long hex: + * fuchsia ➡ #ff00ff + * + * Convert rgb() to long hex: + * rgb(255, 0, 255) ➡ #ff00ff + * rgb(50%, 100, 100%) ➡ #7f64ff + * + * Convert long hex to short hex: + * #aabbcc ➡ #abc + * + * Convert hex to short name + * #000080 ➡ navy + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'convertColors'>} + */ +exports.fn = (_root, params) => { + const { + currentColor = false, + names2hex = true, + rgb2hex = true, + shorthex = true, + shortname = true, + } = params; + + return { + element: { + enter: (node) => { + for (const [name, value] of Object.entries(node.attributes)) { + if (collections.colorsProps.includes(name)) { + let val = value; + + // convert colors to currentColor + if (currentColor) { + let matched; + if (typeof currentColor === 'string') { + matched = val === currentColor; + } else if (currentColor instanceof RegExp) { + matched = currentColor.exec(val) != null; + } else { + matched = val !== 'none'; + } + if (matched) { + val = 'currentColor'; + } + } + + // convert color name keyword to long hex + if (names2hex) { + const colorName = val.toLowerCase(); + if (collections.colorsNames[colorName] != null) { + val = collections.colorsNames[colorName]; + } + } + + // convert rgb() to long hex + if (rgb2hex) { + let match = val.match(regRGB); + if (match != null) { + let nums = match.slice(1, 4).map((m) => { + let n; + if (m.indexOf('%') > -1) { + n = Math.round(parseFloat(m) * 2.55); + } else { + n = Number(m); + } + return Math.max(0, Math.min(n, 255)); + }); + val = convertRgbToHex(nums); + } + } + + // convert long hex to short hex + if (shorthex) { + let match = val.match(regHEX); + if (match != null) { + val = '#' + match[0][1] + match[0][3] + match[0][5]; + } + } + + // convert hex to short name + if (shortname) { + const colorName = val.toLowerCase(); + if (collections.colorsShortNames[colorName] != null) { + val = collections.colorsShortNames[colorName]; + } + } + + node.attributes[name] = val; + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/convertEllipseToCircle.js b/node_modules/svgo/plugins/convertEllipseToCircle.js new file mode 100644 index 00000000..50fa20db --- /dev/null +++ b/node_modules/svgo/plugins/convertEllipseToCircle.js @@ -0,0 +1,37 @@ +'use strict'; + +exports.name = 'convertEllipseToCircle'; +exports.description = 'converts non-eccentric <ellipse>s to <circle>s'; + +/** + * Converts non-eccentric <ellipse>s to <circle>s. + * + * @see https://www.w3.org/TR/SVG11/shapes.html + * + * @author Taylor Hunt + * + * @type {import('./plugins-types').Plugin<'convertEllipseToCircle'>} + */ +exports.fn = () => { + return { + element: { + enter: (node) => { + if (node.name === 'ellipse') { + const rx = node.attributes.rx || '0'; + const ry = node.attributes.ry || '0'; + if ( + rx === ry || + rx === 'auto' || + ry === 'auto' // SVG2 + ) { + node.name = 'circle'; + const radius = rx === 'auto' ? ry : rx; + delete node.attributes.rx; + delete node.attributes.ry; + node.attributes.r = radius; + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/convertPathData.js b/node_modules/svgo/plugins/convertPathData.js new file mode 100644 index 00000000..a5f375ba --- /dev/null +++ b/node_modules/svgo/plugins/convertPathData.js @@ -0,0 +1,1175 @@ +'use strict'; + +/** + * @typedef {import('../lib//types').PathDataItem} PathDataItem + */ + +const { collectStylesheet, computeStyle } = require('../lib/style.js'); +const { visit } = require('../lib/xast.js'); +const { pathElems } = require('./_collections.js'); +const { path2js, js2path } = require('./_path.js'); +const { applyTransforms } = require('./applyTransforms.js'); +const { cleanupOutData } = require('../lib/svgo/tools'); + +exports.name = 'convertPathData'; +exports.description = + 'optimizes path data: writes in shorter form, applies transformations'; + +/** + * @type {(data: number[]) => number[]} + */ +let roundData; +/** + * @type {number | false} + */ +let precision; +/** + * @type {number} + */ +let error; +/** + * @type {number} + */ +let arcThreshold; +/** + * @type {number} + */ +let arcTolerance; + +/** + * @typedef {{ + * applyTransforms: boolean, + * applyTransformsStroked: boolean, + * makeArcs: { + * threshold: number, + * tolerance: number, + * }, + * straightCurves: boolean, + * lineShorthands: boolean, + * curveSmoothShorthands: boolean, + * floatPrecision: number | false, + * transformPrecision: number, + * removeUseless: boolean, + * collapseRepeated: boolean, + * utilizeAbsolute: boolean, + * leadingZero: boolean, + * negativeExtraSpace: boolean, + * noSpaceAfterFlags: boolean, + * forceAbsolutePath: boolean, + * }} InternalParams + */ + +/** + * @typedef {[number, number]} Point + */ + +/** + * @typedef {{ + * center: Point, + * radius: number + * }} Circle + */ + +/** + * Convert absolute Path to relative, + * collapse repeated instructions, + * detect and convert Lineto shorthands, + * remove useless instructions like "l0,0", + * trim useless delimiters and leading zeros, + * decrease accuracy of floating-point numbers. + * + * @see https://www.w3.org/TR/SVG11/paths.html#PathData + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'convertPathData'>} + */ +exports.fn = (root, params) => { + const { + // TODO convert to separate plugin in v3 + applyTransforms: _applyTransforms = true, + applyTransformsStroked = true, + makeArcs = { + threshold: 2.5, // coefficient of rounding error + tolerance: 0.5, // percentage of radius + }, + straightCurves = true, + lineShorthands = true, + curveSmoothShorthands = true, + floatPrecision = 3, + transformPrecision = 5, + removeUseless = true, + collapseRepeated = true, + utilizeAbsolute = true, + leadingZero = true, + negativeExtraSpace = true, + noSpaceAfterFlags = false, // a20 60 45 0 1 30 20 → a20 60 45 0130 20 + forceAbsolutePath = false, + } = params; + + /** + * @type {InternalParams} + */ + const newParams = { + applyTransforms: _applyTransforms, + applyTransformsStroked, + makeArcs, + straightCurves, + lineShorthands, + curveSmoothShorthands, + floatPrecision, + transformPrecision, + removeUseless, + collapseRepeated, + utilizeAbsolute, + leadingZero, + negativeExtraSpace, + noSpaceAfterFlags, + forceAbsolutePath, + }; + + // invoke applyTransforms plugin + if (_applyTransforms) { + visit( + root, + // @ts-ignore + applyTransforms(root, { + transformPrecision, + applyTransformsStroked, + }) + ); + } + + const stylesheet = collectStylesheet(root); + return { + element: { + enter: (node) => { + if (pathElems.includes(node.name) && node.attributes.d != null) { + const computedStyle = computeStyle(stylesheet, node); + precision = floatPrecision; + error = + precision !== false + ? +Math.pow(0.1, precision).toFixed(precision) + : 1e-2; + roundData = precision > 0 && precision < 20 ? strongRound : round; + if (makeArcs) { + arcThreshold = makeArcs.threshold; + arcTolerance = makeArcs.tolerance; + } + const hasMarkerMid = computedStyle['marker-mid'] != null; + + const maybeHasStroke = + computedStyle.stroke && + (computedStyle.stroke.type === 'dynamic' || + computedStyle.stroke.value !== 'none'); + const maybeHasLinecap = + computedStyle['stroke-linecap'] && + (computedStyle['stroke-linecap'].type === 'dynamic' || + computedStyle['stroke-linecap'].value !== 'butt'); + const maybeHasStrokeAndLinecap = maybeHasStroke && maybeHasLinecap; + + var data = path2js(node); + + // TODO: get rid of functions returns + if (data.length) { + convertToRelative(data); + + data = filters(data, newParams, { + maybeHasStrokeAndLinecap, + hasMarkerMid, + }); + + if (utilizeAbsolute) { + data = convertToMixed(data, newParams); + } + + // @ts-ignore + js2path(node, data, newParams); + } + } + }, + }, + }; +}; + +/** + * Convert absolute path data coordinates to relative. + * + * @type {(pathData: PathDataItem[]) => PathDataItem[]} + */ +const convertToRelative = (pathData) => { + let start = [0, 0]; + let cursor = [0, 0]; + let prevCoords = [0, 0]; + + for (let i = 0; i < pathData.length; i += 1) { + const pathItem = pathData[i]; + let { command, args } = pathItem; + + // moveto (x y) + if (command === 'm') { + // update start and cursor + cursor[0] += args[0]; + cursor[1] += args[1]; + start[0] = cursor[0]; + start[1] = cursor[1]; + } + if (command === 'M') { + // M → m + // skip first moveto + if (i !== 0) { + command = 'm'; + } + args[0] -= cursor[0]; + args[1] -= cursor[1]; + // update start and cursor + cursor[0] += args[0]; + cursor[1] += args[1]; + start[0] = cursor[0]; + start[1] = cursor[1]; + } + + // lineto (x y) + if (command === 'l') { + cursor[0] += args[0]; + cursor[1] += args[1]; + } + if (command === 'L') { + // L → l + command = 'l'; + args[0] -= cursor[0]; + args[1] -= cursor[1]; + cursor[0] += args[0]; + cursor[1] += args[1]; + } + + // horizontal lineto (x) + if (command === 'h') { + cursor[0] += args[0]; + } + if (command === 'H') { + // H → h + command = 'h'; + args[0] -= cursor[0]; + cursor[0] += args[0]; + } + + // vertical lineto (y) + if (command === 'v') { + cursor[1] += args[0]; + } + if (command === 'V') { + // V → v + command = 'v'; + args[0] -= cursor[1]; + cursor[1] += args[0]; + } + + // curveto (x1 y1 x2 y2 x y) + if (command === 'c') { + cursor[0] += args[4]; + cursor[1] += args[5]; + } + if (command === 'C') { + // C → c + command = 'c'; + args[0] -= cursor[0]; + args[1] -= cursor[1]; + args[2] -= cursor[0]; + args[3] -= cursor[1]; + args[4] -= cursor[0]; + args[5] -= cursor[1]; + cursor[0] += args[4]; + cursor[1] += args[5]; + } + + // smooth curveto (x2 y2 x y) + if (command === 's') { + cursor[0] += args[2]; + cursor[1] += args[3]; + } + if (command === 'S') { + // S → s + command = 's'; + args[0] -= cursor[0]; + args[1] -= cursor[1]; + args[2] -= cursor[0]; + args[3] -= cursor[1]; + cursor[0] += args[2]; + cursor[1] += args[3]; + } + + // quadratic Bézier curveto (x1 y1 x y) + if (command === 'q') { + cursor[0] += args[2]; + cursor[1] += args[3]; + } + if (command === 'Q') { + // Q → q + command = 'q'; + args[0] -= cursor[0]; + args[1] -= cursor[1]; + args[2] -= cursor[0]; + args[3] -= cursor[1]; + cursor[0] += args[2]; + cursor[1] += args[3]; + } + + // smooth quadratic Bézier curveto (x y) + if (command === 't') { + cursor[0] += args[0]; + cursor[1] += args[1]; + } + if (command === 'T') { + // T → t + command = 't'; + args[0] -= cursor[0]; + args[1] -= cursor[1]; + cursor[0] += args[0]; + cursor[1] += args[1]; + } + + // elliptical arc (rx ry x-axis-rotation large-arc-flag sweep-flag x y) + if (command === 'a') { + cursor[0] += args[5]; + cursor[1] += args[6]; + } + if (command === 'A') { + // A → a + command = 'a'; + args[5] -= cursor[0]; + args[6] -= cursor[1]; + cursor[0] += args[5]; + cursor[1] += args[6]; + } + + // closepath + if (command === 'Z' || command === 'z') { + // reset cursor + cursor[0] = start[0]; + cursor[1] = start[1]; + } + + pathItem.command = command; + pathItem.args = args; + // store absolute coordinates for later use + // base should preserve reference from other element + // @ts-ignore + pathItem.base = prevCoords; + // @ts-ignore + pathItem.coords = [cursor[0], cursor[1]]; + // @ts-ignore + prevCoords = pathItem.coords; + } + + return pathData; +}; + +/** + * Main filters loop. + * + * @type {( + * path: PathDataItem[], + * params: InternalParams, + * aux: { maybeHasStrokeAndLinecap: boolean, hasMarkerMid: boolean } + * ) => PathDataItem[]} + */ +function filters(path, params, { maybeHasStrokeAndLinecap, hasMarkerMid }) { + var stringify = data2Path.bind(null, params), + relSubpoint = [0, 0], + pathBase = [0, 0], + prev = {}; + + path = path.filter(function (item, index, path) { + let command = item.command; + let data = item.args; + let next = path[index + 1]; + + if (command !== 'Z' && command !== 'z') { + var sdata = data, + circle; + + if (command === 's') { + sdata = [0, 0].concat(data); + + // @ts-ignore + var pdata = prev.args, + n = pdata.length; + + // (-x, -y) of the prev tangent point relative to the current point + sdata[0] = pdata[n - 2] - pdata[n - 4]; + sdata[1] = pdata[n - 1] - pdata[n - 3]; + } + + // convert curves to arcs if possible + if ( + params.makeArcs && + (command == 'c' || command == 's') && + isConvex(sdata) && + (circle = findCircle(sdata)) + ) { + var r = roundData([circle.radius])[0], + angle = findArcAngle(sdata, circle), + sweep = sdata[5] * sdata[0] - sdata[4] * sdata[1] > 0 ? 1 : 0, + /** + * @type {PathDataItem} + */ + arc = { + command: 'a', + args: [r, r, 0, 0, sweep, sdata[4], sdata[5]], + // @ts-ignore + coords: item.coords.slice(), + // @ts-ignore + base: item.base, + }, + /** + * @type {PathDataItem[]} + */ + output = [arc], + // relative coordinates to adjust the found circle + /** + * @type {Point} + */ + relCenter = [ + circle.center[0] - sdata[4], + circle.center[1] - sdata[5], + ], + relCircle = { center: relCenter, radius: circle.radius }, + arcCurves = [item], + hasPrev = 0, + suffix = '', + nextLonghand; + + if ( + // @ts-ignore + (prev.command == 'c' && + // @ts-ignore + isConvex(prev.args) && + // @ts-ignore + isArcPrev(prev.args, circle)) || + // @ts-ignore + (prev.command == 'a' && prev.sdata && isArcPrev(prev.sdata, circle)) + ) { + // @ts-ignore + arcCurves.unshift(prev); + // @ts-ignore + arc.base = prev.base; + // @ts-ignore + arc.args[5] = arc.coords[0] - arc.base[0]; + // @ts-ignore + arc.args[6] = arc.coords[1] - arc.base[1]; + // @ts-ignore + var prevData = prev.command == 'a' ? prev.sdata : prev.args; + var prevAngle = findArcAngle(prevData, { + center: [ + prevData[4] + circle.center[0], + prevData[5] + circle.center[1], + ], + radius: circle.radius, + }); + angle += prevAngle; + if (angle > Math.PI) arc.args[3] = 1; + hasPrev = 1; + } + + // check if next curves are fitting the arc + for ( + var j = index; + (next = path[++j]) && ~'cs'.indexOf(next.command); + + ) { + var nextData = next.args; + if (next.command == 's') { + nextLonghand = makeLonghand( + { command: 's', args: next.args.slice() }, + path[j - 1].args + ); + nextData = nextLonghand.args; + nextLonghand.args = nextData.slice(0, 2); + suffix = stringify([nextLonghand]); + } + if (isConvex(nextData) && isArc(nextData, relCircle)) { + angle += findArcAngle(nextData, relCircle); + if (angle - 2 * Math.PI > 1e-3) break; // more than 360° + if (angle > Math.PI) arc.args[3] = 1; + arcCurves.push(next); + if (2 * Math.PI - angle > 1e-3) { + // less than 360° + // @ts-ignore + arc.coords = next.coords; + // @ts-ignore + arc.args[5] = arc.coords[0] - arc.base[0]; + // @ts-ignore + arc.args[6] = arc.coords[1] - arc.base[1]; + } else { + // full circle, make a half-circle arc and add a second one + arc.args[5] = 2 * (relCircle.center[0] - nextData[4]); + arc.args[6] = 2 * (relCircle.center[1] - nextData[5]); + // @ts-ignore + arc.coords = [ + // @ts-ignore + arc.base[0] + arc.args[5], + // @ts-ignore + arc.base[1] + arc.args[6], + ]; + arc = { + command: 'a', + args: [ + r, + r, + 0, + 0, + sweep, + // @ts-ignore + next.coords[0] - arc.coords[0], + // @ts-ignore + next.coords[1] - arc.coords[1], + ], + // @ts-ignore + coords: next.coords, + // @ts-ignore + base: arc.coords, + }; + output.push(arc); + j++; + break; + } + relCenter[0] -= nextData[4]; + relCenter[1] -= nextData[5]; + } else break; + } + + if ((stringify(output) + suffix).length < stringify(arcCurves).length) { + if (path[j] && path[j].command == 's') { + makeLonghand(path[j], path[j - 1].args); + } + if (hasPrev) { + var prevArc = output.shift(); + // @ts-ignore + roundData(prevArc.args); + // @ts-ignore + relSubpoint[0] += prevArc.args[5] - prev.args[prev.args.length - 2]; + // @ts-ignore + relSubpoint[1] += prevArc.args[6] - prev.args[prev.args.length - 1]; + // @ts-ignore + prev.command = 'a'; + // @ts-ignore + prev.args = prevArc.args; + // @ts-ignore + item.base = prev.coords = prevArc.coords; + } + // @ts-ignore + arc = output.shift(); + if (arcCurves.length == 1) { + // @ts-ignore + item.sdata = sdata.slice(); // preserve curve data for future checks + } else if (arcCurves.length - 1 - hasPrev > 0) { + // filter out consumed next items + path.splice.apply( + path, + // @ts-ignore + [index + 1, arcCurves.length - 1 - hasPrev].concat(output) + ); + } + if (!arc) return false; + command = 'a'; + data = arc.args; + // @ts-ignore + item.coords = arc.coords; + } + } + + // Rounding relative coordinates, taking in account accummulating 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) { + if ( + command === 'm' || + command === 'l' || + command === 't' || + command === 'q' || + command === 's' || + command === 'c' + ) { + for (var i = data.length; i--; ) { + // @ts-ignore + data[i] += item.base[i % 2] - relSubpoint[i % 2]; + } + } else if (command == 'h') { + // @ts-ignore + data[0] += item.base[0] - relSubpoint[0]; + } else if (command == 'v') { + // @ts-ignore + data[0] += item.base[1] - relSubpoint[1]; + } else if (command == 'a') { + // @ts-ignore + data[5] += item.base[0] - relSubpoint[0]; + // @ts-ignore + data[6] += item.base[1] - relSubpoint[1]; + } + roundData(data); + + if (command == 'h') relSubpoint[0] += data[0]; + else if (command == 'v') relSubpoint[1] += data[0]; + else { + relSubpoint[0] += data[data.length - 2]; + relSubpoint[1] += data[data.length - 1]; + } + roundData(relSubpoint); + + if (command === 'M' || command === 'm') { + pathBase[0] = relSubpoint[0]; + pathBase[1] = relSubpoint[1]; + } + } + + // convert straight curves into lines segments + if (params.straightCurves) { + if ( + (command === 'c' && isCurveStraightLine(data)) || + (command === 's' && isCurveStraightLine(sdata)) + ) { + if (next && next.command == 's') makeLonghand(next, data); // fix up next curve + command = 'l'; + data = data.slice(-2); + } else if (command === 'q' && isCurveStraightLine(data)) { + if (next && next.command == 't') makeLonghand(next, data); // fix up next curve + command = 'l'; + data = data.slice(-2); + } else if ( + command === 't' && + // @ts-ignore + prev.command !== 'q' && + // @ts-ignore + prev.command !== 't' + ) { + command = 'l'; + data = data.slice(-2); + } else if (command === 'a' && (data[0] === 0 || data[1] === 0)) { + command = 'l'; + data = data.slice(-2); + } + } + + // horizontal and vertical line shorthands + // l 50 0 → h 50 + // l 0 50 → v 50 + if (params.lineShorthands && command === 'l') { + if (data[1] === 0) { + command = 'h'; + data.pop(); + } else if (data[0] === 0) { + command = 'v'; + data.shift(); + } + } + + // collapse repeated commands + // h 20 h 30 -> h 50 + if ( + params.collapseRepeated && + hasMarkerMid === false && + (command === 'm' || command === 'h' || command === 'v') && + // @ts-ignore + prev.command && + // @ts-ignore + command == prev.command.toLowerCase() && + ((command != 'h' && command != 'v') || + // @ts-ignore + prev.args[0] >= 0 == data[0] >= 0) + ) { + // @ts-ignore + prev.args[0] += data[0]; + if (command != 'h' && command != 'v') { + // @ts-ignore + prev.args[1] += data[1]; + } + // @ts-ignore + prev.coords = item.coords; + // @ts-ignore + path[index] = prev; + return false; + } + + // convert curves into smooth shorthands + // @ts-ignore + if (params.curveSmoothShorthands && prev.command) { + // curveto + if (command === 'c') { + // c + c → c + s + if ( + // @ts-ignore + prev.command === 'c' && + // @ts-ignore + data[0] === -(prev.args[2] - prev.args[4]) && + // @ts-ignore + data[1] === -(prev.args[3] - prev.args[5]) + ) { + command = 's'; + data = data.slice(2); + } + + // s + c → s + s + else if ( + // @ts-ignore + prev.command === 's' && + // @ts-ignore + data[0] === -(prev.args[0] - prev.args[2]) && + // @ts-ignore + data[1] === -(prev.args[1] - prev.args[3]) + ) { + command = 's'; + data = data.slice(2); + } + + // [^cs] + c → [^cs] + s + else if ( + // @ts-ignore + prev.command !== 'c' && + // @ts-ignore + prev.command !== 's' && + data[0] === 0 && + data[1] === 0 + ) { + command = 's'; + data = data.slice(2); + } + } + + // quadratic Bézier curveto + else if (command === 'q') { + // q + q → q + t + if ( + // @ts-ignore + prev.command === 'q' && + // @ts-ignore + data[0] === prev.args[2] - prev.args[0] && + // @ts-ignore + data[1] === prev.args[3] - prev.args[1] + ) { + command = 't'; + data = data.slice(2); + } + + // t + q → t + t + else if ( + // @ts-ignore + prev.command === 't' && + // @ts-ignore + data[2] === prev.args[0] && + // @ts-ignore + data[3] === prev.args[1] + ) { + command = 't'; + data = data.slice(2); + } + } + } + + // remove useless non-first path segments + if (params.removeUseless && !maybeHasStrokeAndLinecap) { + // l 0,0 / h 0 / v 0 / q 0,0 0,0 / t 0,0 / c 0,0 0,0 0,0 / s 0,0 0,0 + if ( + (command === 'l' || + command === 'h' || + command === 'v' || + command === 'q' || + command === 't' || + command === 'c' || + command === 's') && + data.every(function (i) { + return i === 0; + }) + ) { + // @ts-ignore + path[index] = prev; + return false; + } + + // a 25,25 -30 0,1 0,0 + if (command === 'a' && data[5] === 0 && data[6] === 0) { + // @ts-ignore + path[index] = prev; + return false; + } + } + + 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; + } + + return true; + }); + + return path; +} + +/** + * Writes data in shortest form using absolute or relative coordinates. + * + * @type {(path: PathDataItem[], params: InternalParams) => PathDataItem[]} + */ +function convertToMixed(path, params) { + var prev = path[0]; + + path = path.filter(function (item, index) { + if (index == 0) return true; + if (item.command === 'Z' || item.command === 'z') { + prev = item; + return true; + } + + var command = item.command, + data = item.args, + adata = data.slice(); + + if ( + command === 'm' || + command === 'l' || + command === 't' || + command === 'q' || + command === 's' || + command === 'c' + ) { + for (var i = adata.length; i--; ) { + // @ts-ignore + adata[i] += item.base[i % 2]; + } + } else if (command == 'h') { + // @ts-ignore + adata[0] += item.base[0]; + } else if (command == 'v') { + // @ts-ignore + adata[0] += item.base[1]; + } else if (command == 'a') { + // @ts-ignore + adata[5] += item.base[0]; + // @ts-ignore + adata[6] += item.base[1]; + } + + roundData(adata); + + var absoluteDataStr = cleanupOutData(adata, params), + relativeDataStr = cleanupOutData(data, params); + + // Convert to absolute coordinates if it's shorter or forceAbsolutePath is true. + // v-20 -> V0 + // Don't convert if it fits following previous command. + // l20 30-10-50 instead of l20 30L20 30 + if ( + params.forceAbsolutePath || + (absoluteDataStr.length < relativeDataStr.length && + !( + params.negativeExtraSpace && + command == prev.command && + prev.command.charCodeAt(0) > 96 && + absoluteDataStr.length == relativeDataStr.length - 1 && + (data[0] < 0 || + // @ts-ignore + (/^0\./.test(data[0]) && prev.args[prev.args.length - 1] % 1)) + )) + ) { + // @ts-ignore + item.command = command.toUpperCase(); + item.args = adata; + } + + prev = item; + + return true; + }); + + return path; +} + +/** + * Checks if curve is convex. Control points of such a curve must form + * a convex quadrilateral with diagonals crosspoint inside of it. + * + * @type {(data: number[]) => boolean} + */ +function isConvex(data) { + var center = getIntersection([ + 0, + 0, + data[2], + data[3], + data[0], + data[1], + data[4], + data[5], + ]); + + return ( + center != null && + data[2] < center[0] == center[0] < 0 && + data[3] < center[1] == center[1] < 0 && + data[4] < center[0] == center[0] < data[0] && + data[5] < center[1] == center[1] < data[1] + ); +} + +/** + * Computes lines equations by two points and returns their intersection point. + * + * @type {(coords: number[]) => undefined | Point} + */ +function getIntersection(coords) { + // Prev line equation parameters. + var a1 = coords[1] - coords[3], // y1 - y2 + b1 = coords[2] - coords[0], // x2 - x1 + c1 = coords[0] * coords[3] - coords[2] * coords[1], // x1 * y2 - x2 * y1 + // Next line equation parameters + a2 = coords[5] - coords[7], // y1 - y2 + b2 = coords[6] - coords[4], // x2 - x1 + 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 + + /** + * @type {Point} + */ + var cross = [(b1 * c2 - b2 * c1) / denom, (a1 * c2 - a2 * c1) / -denom]; + if ( + !isNaN(cross[0]) && + !isNaN(cross[1]) && + isFinite(cross[0]) && + isFinite(cross[1]) + ) { + return cross; + } +} + +/** + * 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. + * Doesn't apply "smartness" if the number precision fits already. + * + * @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); + data[i] = + // @ts-ignore + +Math.abs(rounded - data[i]).toFixed(precision + 1) >= error + ? // @ts-ignore + +data[i].toFixed(precision) + : rounded; + } + } + return data; +} + +/** + * Simple rounding function if precision is 0. + * + * @type {(data: number[]) => number[]} + */ +function round(data) { + for (var i = data.length; i-- > 0; ) { + data[i] = Math.round(data[i]); + } + return data; +} + +/** + * Checks if a curve is a straight line by measuring distance + * from middle points to the line formed by end points. + * + * @type {(data: number[]) => boolean} + */ + +function isCurveStraightLine(data) { + // Get line equation a·x + b·y + c = 0 coefficients a, b (c = 0) by start and end points. + var i = data.length - 2, + a = -data[i + 1], // y1 − y2 (y1 = 0) + b = data[i], // x2 − x1 (x1 = 0) + d = 1 / (a * a + b * b); // same part for all points + + if (i <= 1 || !isFinite(d)) return false; // curve that ends at start point isn't the case + + // Distance from point (x0, y0) to the line is sqrt((c − a·x0 − b·y0)² / (a² + b²)) + while ((i -= 2) >= 0) { + if (Math.sqrt(Math.pow(a * data[i] + b * data[i + 1], 2) * d) > error) + return false; + } + + return true; +} + +/** + * Converts next curve from shorthand to full form using the current curve data. + * + * @type {(item: PathDataItem, data: number[]) => PathDataItem} + */ + +function makeLonghand(item, data) { + switch (item.command) { + case 's': + item.command = 'c'; + break; + case 't': + item.command = 'q'; + break; + } + item.args.unshift( + data[data.length - 2] - data[data.length - 4], + data[data.length - 1] - data[data.length - 3] + ); + return item; +} + +/** + * Returns distance between two points + * + * @type {(point1: Point, point2: Point) => number} + */ + +function getDistance(point1, point2) { + return Math.hypot(point1[0] - point2[0], point1[1] - point2[1]); +} + +/** + * Returns coordinates of the curve point corresponding to the certain t + * a·(1 - t)³·p1 + b·(1 - t)²·t·p2 + c·(1 - t)·t²·p3 + d·t³·p4, + * where pN are control points and p1 is zero due to relative coordinates. + * + * @type {(curve: number[], t: number) => Point} + */ + +function getCubicBezierPoint(curve, t) { + var sqrT = t * t, + cubT = sqrT * t, + mt = 1 - t, + sqrMt = mt * mt; + + return [ + 3 * sqrMt * t * curve[0] + 3 * mt * sqrT * curve[2] + cubT * curve[4], + 3 * sqrMt * t * curve[1] + 3 * mt * sqrT * curve[3] + cubT * curve[5], + ]; +} + +/** + * Finds circle by 3 points of the curve and checks if the curve fits the found circle. + * + * @type {(curve: number[]) => undefined | Circle} + */ + +function findCircle(curve) { + var midPoint = getCubicBezierPoint(curve, 1 / 2), + m1 = [midPoint[0] / 2, midPoint[1] / 2], + m2 = [(midPoint[0] + curve[4]) / 2, (midPoint[1] + curve[5]) / 2], + center = getIntersection([ + m1[0], + m1[1], + m1[0] + m1[1], + m1[1] - m1[0], + m2[0], + m2[1], + m2[0] + (m2[1] - midPoint[1]), + m2[1] - (m2[0] - midPoint[0]), + ]), + radius = center && getDistance([0, 0], center), + // @ts-ignore + tolerance = Math.min(arcThreshold * error, (arcTolerance * radius) / 100); + + if ( + center && + // @ts-ignore + radius < 1e15 && + [1 / 4, 3 / 4].every(function (point) { + return ( + Math.abs( + // @ts-ignore + getDistance(getCubicBezierPoint(curve, point), center) - radius + ) <= tolerance + ); + }) + ) + // @ts-ignore + return { center: center, radius: radius }; +} + +/** + * Checks if a curve fits the given circle. + * + * @type {(curve: number[], circle: Circle) => boolean} + */ + +function isArc(curve, circle) { + var tolerance = Math.min( + arcThreshold * error, + (arcTolerance * circle.radius) / 100 + ); + + return [0, 1 / 4, 1 / 2, 3 / 4, 1].every(function (point) { + return ( + Math.abs( + getDistance(getCubicBezierPoint(curve, point), circle.center) - + circle.radius + ) <= tolerance + ); + }); +} + +/** + * Checks if a previous curve fits the given circle. + * + * @type {(curve: number[], circle: Circle) => boolean} + */ + +function isArcPrev(curve, circle) { + return isArc(curve, { + center: [circle.center[0] + curve[4], circle.center[1] + curve[5]], + radius: circle.radius, + }); +} + +/** + * Finds angle of a curve fitting the given arc. + + * @type {(curve: number[], relCircle: Circle) => number} + */ + +function findArcAngle(curve, relCircle) { + var x1 = -relCircle.center[0], + y1 = -relCircle.center[1], + x2 = curve[4] - relCircle.center[0], + y2 = curve[5] - relCircle.center[1]; + + return Math.acos( + (x1 * x2 + y1 * y2) / Math.sqrt((x1 * x1 + y1 * y1) * (x2 * x2 + y2 * y2)) + ); +} + +/** + * Converts given path data to string. + * + * @type {(params: InternalParams, pathData: PathDataItem[]) => string} + */ + +function data2Path(params, pathData) { + return pathData.reduce(function (pathString, item) { + var strData = ''; + if (item.args) { + strData = cleanupOutData(roundData(item.args.slice()), params); + } + return pathString + item.command + strData; + }, ''); +} diff --git a/node_modules/svgo/plugins/convertShapeToPath.js b/node_modules/svgo/plugins/convertShapeToPath.js new file mode 100644 index 00000000..7705fa97 --- /dev/null +++ b/node_modules/svgo/plugins/convertShapeToPath.js @@ -0,0 +1,170 @@ +'use strict'; + +/** + * @typedef {import('../lib/types').PathDataItem} PathDataItem + */ + +const { stringifyPathData } = require('../lib/path.js'); +const { detachNodeFromParent } = require('../lib/xast.js'); + +exports.name = 'convertShapeToPath'; +exports.description = 'converts basic shapes to more compact path form'; + +const regNumber = /[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g; + +/** + * Converts basic shape to more compact path. + * It also allows further optimizations like + * combining paths with similar attributes. + * + * @see https://www.w3.org/TR/SVG11/shapes.html + * + * @author Lev Solntsev + * + * @type {import('./plugins-types').Plugin<'convertShapeToPath'>} + */ +exports.fn = (root, params) => { + const { convertArcs = false, floatPrecision: precision } = params; + + return { + element: { + enter: (node, parentNode) => { + // convert rect to path + if ( + node.name === 'rect' && + node.attributes.width != null && + node.attributes.height != null && + node.attributes.rx == null && + node.attributes.ry == null + ) { + const x = Number(node.attributes.x || '0'); + const y = Number(node.attributes.y || '0'); + const width = Number(node.attributes.width); + const height = Number(node.attributes.height); + // Values like '100%' compute to NaN, thus running after + // cleanupNumericValues when 'px' units has already been removed. + // TODO: Calculate sizes from % and non-px units if possible. + if (Number.isNaN(x - y + width - height)) return; + /** + * @type {Array<PathDataItem>} + */ + const pathData = [ + { command: 'M', args: [x, y] }, + { command: 'H', args: [x + width] }, + { command: 'V', args: [y + height] }, + { command: 'H', args: [x] }, + { command: 'z', args: [] }, + ]; + node.name = 'path'; + node.attributes.d = stringifyPathData({ pathData, precision }); + delete node.attributes.x; + delete node.attributes.y; + delete node.attributes.width; + delete node.attributes.height; + } + + // convert line to path + if (node.name === 'line') { + const x1 = Number(node.attributes.x1 || '0'); + const y1 = Number(node.attributes.y1 || '0'); + const x2 = Number(node.attributes.x2 || '0'); + const y2 = Number(node.attributes.y2 || '0'); + if (Number.isNaN(x1 - y1 + x2 - y2)) return; + /** + * @type {Array<PathDataItem>} + */ + const pathData = [ + { command: 'M', args: [x1, y1] }, + { command: 'L', args: [x2, y2] }, + ]; + node.name = 'path'; + node.attributes.d = stringifyPathData({ pathData, precision }); + delete node.attributes.x1; + delete node.attributes.y1; + delete node.attributes.x2; + delete node.attributes.y2; + } + + // convert polyline and polygon to path + if ( + (node.name === 'polyline' || node.name === 'polygon') && + node.attributes.points != null + ) { + const coords = (node.attributes.points.match(regNumber) || []).map( + Number + ); + if (coords.length < 4) { + detachNodeFromParent(node, parentNode); + return; + } + /** + * @type {Array<PathDataItem>} + */ + const pathData = []; + for (let i = 0; i < coords.length; i += 2) { + pathData.push({ + command: i === 0 ? 'M' : 'L', + args: coords.slice(i, i + 2), + }); + } + if (node.name === 'polygon') { + pathData.push({ command: 'z', args: [] }); + } + node.name = 'path'; + node.attributes.d = stringifyPathData({ pathData, precision }); + delete node.attributes.points; + } + + // optionally convert circle + if (node.name === 'circle' && convertArcs) { + const cx = Number(node.attributes.cx || '0'); + const cy = Number(node.attributes.cy || '0'); + const r = Number(node.attributes.r || '0'); + if (Number.isNaN(cx - cy + r)) { + return; + } + /** + * @type {Array<PathDataItem>} + */ + const pathData = [ + { command: 'M', args: [cx, cy - r] }, + { command: 'A', args: [r, r, 0, 1, 0, cx, cy + r] }, + { command: 'A', args: [r, r, 0, 1, 0, cx, cy - r] }, + { command: 'z', args: [] }, + ]; + node.name = 'path'; + node.attributes.d = stringifyPathData({ pathData, precision }); + delete node.attributes.cx; + delete node.attributes.cy; + delete node.attributes.r; + } + + // optionally covert ellipse + if (node.name === 'ellipse' && convertArcs) { + const ecx = Number(node.attributes.cx || '0'); + const ecy = Number(node.attributes.cy || '0'); + const rx = Number(node.attributes.rx || '0'); + const ry = Number(node.attributes.ry || '0'); + if (Number.isNaN(ecx - ecy + rx - ry)) { + return; + } + /** + * @type {Array<PathDataItem>} + */ + const pathData = [ + { command: 'M', args: [ecx, ecy - ry] }, + { command: 'A', args: [rx, ry, 0, 1, 0, ecx, ecy + ry] }, + { command: 'A', args: [rx, ry, 0, 1, 0, ecx, ecy - ry] }, + { command: 'z', args: [] }, + ]; + node.name = 'path'; + node.attributes.d = stringifyPathData({ pathData, precision }); + delete node.attributes.cx; + delete node.attributes.cy; + delete node.attributes.rx; + delete node.attributes.ry; + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/convertStyleToAttrs.js b/node_modules/svgo/plugins/convertStyleToAttrs.js new file mode 100644 index 00000000..5d5107d6 --- /dev/null +++ b/node_modules/svgo/plugins/convertStyleToAttrs.js @@ -0,0 +1,137 @@ +'use strict'; + +const { attrsGroups } = require('./_collections'); + +exports.name = 'convertStyleToAttrs'; +exports.description = 'converts style to attributes'; + +/** + * @type {(...args: string[]) => string} + */ +const g = (...args) => { + return '(?:' + args.join('|') + ')'; +}; + +const stylingProps = attrsGroups.presentation; +const rEscape = '\\\\(?:[0-9a-f]{1,6}\\s?|\\r\\n|.)'; // Like \" or \2051. Code points consume one space. +const rAttr = '\\s*(' + g('[^:;\\\\]', rEscape) + '*?)\\s*'; // attribute name like ‘fill’ +const rSingleQuotes = "'(?:[^'\\n\\r\\\\]|" + rEscape + ")*?(?:'|$)"; // string in single quotes: 'smth' +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.) +const rParenthesis = + '\\(' + g('[^\'"()\\\\]+', rEscape, rSingleQuotes, rQuotes) + '*?' + '\\)'; +// The value. It can have strings and parentheses (see above). Fallbacks to anything in case of unexpected input. +const rValue = + '\\s*(' + + g( + '[^!\'"();\\\\]+?', + rEscape, + rSingleQuotes, + rQuotes, + rParenthesis, + '[^;]*?' + ) + + '*?' + + ')'; +// End of declaration. Spaces outside of capturing groups help to do natural trimming. +const rDeclEnd = '\\s*(?:;\\s*|$)'; +// Important rule +const rImportant = '(\\s*!important(?![-(\\w]))?'; +// Final RegExp to parse CSS declarations. +const regDeclarationBlock = new RegExp( + rAttr + ':' + rValue + rImportant + rDeclEnd, + 'ig' +); +// Comments expression. Honors escape sequences and strings. +const regStripComments = new RegExp( + g(rEscape, rSingleQuotes, rQuotes, '/\\*[^]*?\\*/'), + 'ig' +); + +/** + * Convert style in attributes. Cleanups comments and illegal declarations (without colon) as a side effect. + * + * @example + * <g style="fill:#000; color: #fff;"> + * ⬇ + * <g fill="#000" color="#fff"> + * + * @example + * <g style="fill:#000; color: #fff; -webkit-blah: blah"> + * ⬇ + * <g fill="#000" color="#fff" style="-webkit-blah: blah"> + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'convertStyleToAttrs'>} + */ +exports.fn = (_root, params) => { + const { keepImportant = false } = params; + return { + element: { + enter: (node) => { + if (node.attributes.style != null) { + // ['opacity: 1', 'color: #000'] + let styles = []; + /** + * @type {Record<string, string>} + */ + const newAttributes = {}; + + // Strip CSS comments preserving escape sequences and strings. + const styleValue = node.attributes.style.replace( + regStripComments, + (match) => { + return match[0] == '/' + ? '' + : match[0] == '\\' && /[-g-z]/i.test(match[1]) + ? match[1] + : match; + } + ); + + regDeclarationBlock.lastIndex = 0; + // eslint-disable-next-line no-cond-assign + for (var rule; (rule = regDeclarationBlock.exec(styleValue)); ) { + if (!keepImportant || !rule[3]) { + styles.push([rule[1], rule[2]]); + } + } + + if (styles.length) { + styles = styles.filter(function (style) { + if (style[0]) { + var prop = style[0].toLowerCase(), + val = style[1]; + + if (rQuotedString.test(val)) { + val = val.slice(1, -1); + } + + if (stylingProps.includes(prop)) { + newAttributes[prop] = val; + + return false; + } + } + + return true; + }); + + Object.assign(node.attributes, newAttributes); + + if (styles.length) { + node.attributes.style = styles + .map((declaration) => declaration.join(':')) + .join(';'); + } else { + delete node.attributes.style; + } + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/convertTransform.js b/node_modules/svgo/plugins/convertTransform.js new file mode 100644 index 00000000..d90c3efd --- /dev/null +++ b/node_modules/svgo/plugins/convertTransform.js @@ -0,0 +1,417 @@ +'use strict'; + +/** + * @typedef {import('../lib/types').XastElement} XastElement + */ + +const { cleanupOutData } = require('../lib/svgo/tools.js'); +const { + transform2js, + transformsMultiply, + matrixToTransform, +} = require('./_transforms.js'); + +exports.name = 'convertTransform'; +exports.description = 'collapses multiple transformations and optimizes it'; + +/** + * Convert matrices to the short aliases, + * convert long translate, scale or rotate transform notations to the shorts ones, + * convert transforms to the matrices and multiply them all into one, + * remove useless transforms. + * + * @see https://www.w3.org/TR/SVG11/coords.html#TransformMatrixDefined + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'convertTransform'>} + */ +exports.fn = (_root, params) => { + const { + convertToShorts = true, + // degPrecision = 3, // transformPrecision (or matrix precision) - 2 by default + degPrecision, + floatPrecision = 3, + transformPrecision = 5, + matrixToTransform = true, + shortTranslate = true, + shortScale = true, + shortRotate = true, + removeUseless = true, + collapseIntoOne = true, + leadingZero = true, + negativeExtraSpace = false, + } = params; + const newParams = { + convertToShorts, + degPrecision, + floatPrecision, + transformPrecision, + matrixToTransform, + shortTranslate, + shortScale, + shortRotate, + removeUseless, + collapseIntoOne, + leadingZero, + negativeExtraSpace, + }; + return { + element: { + enter: (node) => { + // transform + if (node.attributes.transform != null) { + convertTransform(node, 'transform', newParams); + } + // gradientTransform + if (node.attributes.gradientTransform != null) { + convertTransform(node, 'gradientTransform', newParams); + } + // patternTransform + if (node.attributes.patternTransform != null) { + convertTransform(node, 'patternTransform', newParams); + } + }, + }, + }; +}; + +/** + * @typedef {{ + * convertToShorts: boolean, + * degPrecision?: number, + * floatPrecision: number, + * transformPrecision: number, + * matrixToTransform: boolean, + * shortTranslate: boolean, + * shortScale: boolean, + * shortRotate: boolean, + * removeUseless: boolean, + * collapseIntoOne: boolean, + * leadingZero: boolean, + * negativeExtraSpace: boolean, + * }} TransformParams + */ + +/** + * @typedef {{ name: string, data: Array<number> }} TransformItem + */ + +/** + * Main function. + * + * @type {(item: XastElement, attrName: string, params: TransformParams) => void} + */ +const convertTransform = (item, attrName, params) => { + let data = transform2js(item.attributes[attrName]); + params = definePrecision(data, params); + + if (params.collapseIntoOne && data.length > 1) { + data = [transformsMultiply(data)]; + } + + if (params.convertToShorts) { + data = convertToShorts(data, params); + } else { + data.forEach((item) => roundTransform(item, params)); + } + + if (params.removeUseless) { + data = removeUseless(data); + } + + if (data.length) { + item.attributes[attrName] = js2transform(data, params); + } else { + delete item.attributes[attrName]; + } +}; + +/** + * 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) + * transformPrecision - 2 or floatPrecision whichever is lower. Can be set in params. + * + * @type {(data: Array<TransformItem>, params: TransformParams) => TransformParams} + * + * clone params so it don't affect other elements transformations. + */ +const definePrecision = (data, { ...newParams }) => { + const matrixData = []; + for (const item of data) { + if (item.name == 'matrix') { + matrixData.push(...item.data.slice(0, 4)); + } + } + let significantDigits = newParams.transformPrecision; + // Limit transform precision with matrix one. Calculating with larger precision doesn't add any value. + if (matrixData.length) { + newParams.transformPrecision = Math.min( + newParams.transformPrecision, + Math.max.apply(Math, matrixData.map(floatDigits)) || + newParams.transformPrecision + ); + significantDigits = Math.max.apply( + Math, + matrixData.map( + (n) => n.toString().replace(/\D+/g, '').length // Number of digits in a number. 123.45 → 5 + ) + ); + } + // No sense in angle precision more then number of significant digits in matrix. + if (newParams.degPrecision == null) { + newParams.degPrecision = Math.max( + 0, + Math.min(newParams.floatPrecision, significantDigits - 2) + ); + } + return newParams; +}; + +/** + * @type {(data: Array<number>, params: TransformParams) => Array<number>} + */ +const degRound = (data, params) => { + if ( + params.degPrecision != null && + params.degPrecision >= 1 && + params.floatPrecision < 20 + ) { + return smartRound(params.degPrecision, data); + } else { + return round(data); + } +}; +/** + * @type {(data: Array<number>, params: TransformParams) => Array<number>} + */ +const floatRound = (data, params) => { + if (params.floatPrecision >= 1 && params.floatPrecision < 20) { + return smartRound(params.floatPrecision, data); + } else { + return round(data); + } +}; + +/** + * @type {(data: Array<number>, params: TransformParams) => Array<number>} + */ +const transformRound = (data, params) => { + if (params.transformPrecision >= 1 && params.floatPrecision < 20) { + return smartRound(params.transformPrecision, data); + } else { + return round(data); + } +}; + +/** + * Returns number of digits after the point. 0.125 → 3 + * + * @type {(n: number) => number} + */ +const floatDigits = (n) => { + const str = n.toString(); + return str.slice(str.indexOf('.')).length - 1; +}; + +/** + * Convert transforms to the shorthand alternatives. + * + * @type {(transforms: Array<TransformItem>, params: TransformParams) => Array<TransformItem>} + */ +const convertToShorts = (transforms, params) => { + for (var i = 0; i < transforms.length; i++) { + var transform = transforms[i]; + + // convert matrix to the short aliases + if (params.matrixToTransform && transform.name === 'matrix') { + var decomposed = matrixToTransform(transform, params); + if ( + js2transform(decomposed, params).length <= + js2transform([transform], params).length + ) { + transforms.splice(i, 1, ...decomposed); + } + transform = transforms[i]; + } + + // fixed-point numbers + // 12.754997 → 12.755 + roundTransform(transform, params); + + // convert long translate transform notation to the shorts one + // translate(10 0) → translate(10) + if ( + params.shortTranslate && + transform.name === 'translate' && + transform.data.length === 2 && + !transform.data[1] + ) { + transform.data.pop(); + } + + // convert long scale transform notation to the shorts one + // scale(2 2) → scale(2) + if ( + params.shortScale && + transform.name === 'scale' && + transform.data.length === 2 && + transform.data[0] === transform.data[1] + ) { + transform.data.pop(); + } + + // convert long rotate transform notation to the short one + // translate(cx cy) rotate(a) translate(-cx -cy) → rotate(a cx cy) + if ( + params.shortRotate && + transforms[i - 2] && + transforms[i - 2].name === 'translate' && + transforms[i - 1].name === 'rotate' && + transforms[i].name === 'translate' && + transforms[i - 2].data[0] === -transforms[i].data[0] && + transforms[i - 2].data[1] === -transforms[i].data[1] + ) { + transforms.splice(i - 2, 3, { + name: 'rotate', + data: [ + transforms[i - 1].data[0], + transforms[i - 2].data[0], + transforms[i - 2].data[1], + ], + }); + + // splice compensation + i -= 2; + } + } + + return transforms; +}; + +/** + * Remove useless transforms. + * + * @type {(trasforms: Array<TransformItem>) => Array<TransformItem>} + */ +const removeUseless = (transforms) => { + return transforms.filter((transform) => { + // translate(0), rotate(0[, cx, cy]), skewX(0), skewY(0) + if ( + (['translate', 'rotate', 'skewX', 'skewY'].indexOf(transform.name) > -1 && + (transform.data.length == 1 || transform.name == 'rotate') && + !transform.data[0]) || + // translate(0, 0) + (transform.name == 'translate' && + !transform.data[0] && + !transform.data[1]) || + // scale(1) + (transform.name == 'scale' && + transform.data[0] == 1 && + (transform.data.length < 2 || transform.data[1] == 1)) || + // matrix(1 0 0 1 0 0) + (transform.name == 'matrix' && + transform.data[0] == 1 && + transform.data[3] == 1 && + !( + transform.data[1] || + transform.data[2] || + transform.data[4] || + transform.data[5] + )) + ) { + return false; + } + + return true; + }); +}; + +/** + * Convert transforms JS representation to string. + * + * @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) + + ')'; + }); + + return transformString; +}; + +/** + * @type {(transform: TransformItem, params: TransformParams) => TransformItem} + */ +const roundTransform = (transform, params) => { + switch (transform.name) { + case 'translate': + transform.data = floatRound(transform.data, params); + break; + case 'rotate': + transform.data = [ + ...degRound(transform.data.slice(0, 1), params), + ...floatRound(transform.data.slice(1), params), + ]; + break; + case 'skewX': + case 'skewY': + transform.data = degRound(transform.data, params); + break; + case 'scale': + transform.data = transformRound(transform.data, params); + break; + case 'matrix': + transform.data = [ + ...transformRound(transform.data.slice(0, 4), params), + ...floatRound(transform.data.slice(4), params), + ]; + break; + } + return transform; +}; + +/** + * Rounds numbers in array. + * + * @type {(data: Array<number>) => Array<number>} + */ +const round = (data) => { + return data.map(Math.round); +}; + +/** + * Decrease accuracy of floating-point numbers + * in transforms keeping a specified number of decimals. + * Smart rounds values like 2.349 to 2.35. + * + * @type {(precision: number, data: Array<number>) => Array<number>} + */ +const smartRound = (precision, data) => { + for ( + var i = data.length, + tolerance = +Math.pow(0.1, precision).toFixed(precision); + i--; + + ) { + if (Number(data[i].toFixed(precision)) !== data[i]) { + var rounded = +data[i].toFixed(precision - 1); + data[i] = + +Math.abs(rounded - data[i]).toFixed(precision + 1) >= tolerance + ? +data[i].toFixed(precision) + : rounded; + } + } + return data; +}; diff --git a/node_modules/svgo/plugins/inlineStyles.js b/node_modules/svgo/plugins/inlineStyles.js new file mode 100644 index 00000000..e931b5f6 --- /dev/null +++ b/node_modules/svgo/plugins/inlineStyles.js @@ -0,0 +1,386 @@ +'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 + syntax: { specificity }, +} = require('csso'); +const { + visitSkip, + querySelectorAll, + detachNodeFromParent, +} = require('../lib/xast.js'); + +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 + * + * @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} + */ +const toAny = (value) => value; + +/** + * 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> + * + * @type {import('./plugins-types').Plugin<'inlineStyles'>} + */ +exports.fn = (root, params) => { + const { + onlyMatchedOnce = true, + removeMatchedSelectors = true, + useMqs = ['', 'screen'], + usePseudos = [''], + } = params; + + /** + * @type {Array<{ node: XastElement, parentNode: XastParent, cssAst: csstree.StyleSheet }>} + */ + const styles = []; + /** + * @type {Array<{ + * node: csstree.Selector, + * item: csstree.ListItem<csstree.CssNode>, + * rule: csstree.Rule, + * matchedElements?: Array<XastElement> + * }>} + */ + let selectors = []; + + 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 !== '' && + node.attributes.type !== 'text/css' + ) { + 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} + */ + let cssAst = null; + try { + cssAst = csstree.parse(cssText, { + parseValue: false, + parseCustomProperty: false, + }); + } catch { + return; + } + if (cssAst.type === 'StyleSheet') { + styles.push({ node, parentNode, cssAst }); + } + + // collect selectors + csstree.walk(cssAst, { + visit: 'Selector', + enter(node, item) { + const atrule = this.atrule; + const rule = this.rule; + if (rule == null) { + return; + } + + // skip media queries not included into useMqs param + let mq = ''; + if (atrule != null) { + mq = atrule.name; + if (atrule.prelude != null) { + mq += ` ${csstree.generate(atrule.prelude)}`; + } + } + if (useMqs.includes(mq) === false) { + 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 }); + } + }); + } + + // 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; + } + + // 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); + } + + selectors.push({ node, item, rule }); + }, + }); + }, + }, + + root: { + exit: () => { + if (styles.length === 0) { + return; + } + // stable sort selectors + const sortedSelectors = [...selectors] + .sort((a, b) => { + const aSpecificity = specificity(a.item.data); + const bSpecificity = specificity(b.item.data); + return compareSpecificity(aSpecificity, bSpecificity); + }) + .reverse(); + + for (const selector of sortedSelectors) { + // match selectors + const selectorText = csstree.generate(selector.item.data); + /** + * @type {Array<XastElement>} + */ + const matchedElements = []; + try { + for (const node of querySelectorAll(root, selectorText)) { + if (node.type === 'element') { + matchedElements.push(node); + } + } + } catch (selectError) { + continue; + } + // nothing selected + if (matchedElements.length === 0) { + continue; + } + + // apply styles to matched elements + // skip selectors that match more than once if option onlyMatchedOnce is enabled + if (onlyMatchedOnce && matchedElements.length > 1) { + continue; + } + + // apply <style/> to matched elements + for (const selectedEl of matchedElements) { + const styleDeclarationList = csstree.parse( + selectedEl.attributes.style == null + ? '' + : selectedEl.attributes.style, + { + context: 'declarationList', + parseValue: false, + } + ); + if (styleDeclarationList.type !== 'DeclarationList') { + continue; + } + const styleDeclarationItems = new Map(); + csstree.walk(styleDeclarationList, { + visit: 'Declaration', + enter(node, item) { + styleDeclarationItems.set(node.property, item); + }, + }); + // merge declarations + csstree.walk(selector.rule, { + visit: 'Declaration', + enter(ruleDeclaration) { + // existing inline styles have higher priority + // 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 ruleDeclarationItem = + styleDeclarationList.children.createItem(ruleDeclaration); + if (matchedItem == null) { + styleDeclarationList.children.append(ruleDeclarationItem); + } else if ( + matchedItem.data.important !== true && + ruleDeclaration.important === true + ) { + styleDeclarationList.children.replace( + matchedItem, + ruleDeclarationItem + ); + styleDeclarationItems.set( + ruleDeclaration.property, + ruleDeclarationItem + ); + } + }, + }); + selectedEl.attributes.style = + csstree.generate(styleDeclarationList); + } + + if ( + removeMatchedSelectors && + matchedElements.length !== 0 && + selector.rule.prelude.type === 'SelectorList' + ) { + // clean up matching simple selectors if option removeMatchedSelectors is enabled + selector.rule.prelude.children.remove(selector.item); + } + selector.matchedElements = matchedElements; + } + + // no further processing required + if (removeMatchedSelectors === false) { + return; + } + + // clean up matched class + ID attribute values + for (const selector of sortedSelectors) { + if (selector.matchedElements == null) { + continue; + } + + if (onlyMatchedOnce && selector.matchedElements.length > 1) { + // skip selectors that match more than once if option onlyMatchedOnce is enabled + continue; + } + + for (const selectedEl of selector.matchedElements) { + // class + const classList = new Set( + selectedEl.attributes.class == null + ? 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); + } + if (classList.size === 0) { + delete selectedEl.attributes.class; + } else { + selectedEl.attributes.class = Array.from(classList).join(' '); + } + + // ID + if ( + firstSubSelector != null && + firstSubSelector.type === 'IdSelector' + ) { + if (selectedEl.attributes.id === firstSubSelector.name) { + delete selectedEl.attributes.id; + } + } + } + } + + for (const style of styles) { + csstree.walk(style.cssAst, { + visit: 'Rule', + enter: function (node, item, list) { + // clean up <style/> rulesets without any css selectors left + if ( + node.type === 'Rule' && + node.prelude.type === 'SelectorList' && + // csstree v2 changed this type + toAny(node.prelude.children.isEmpty) + ) { + list.remove(item); + } + }, + }); + + // csstree v2 changed this type + if (toAny(style.cssAst.children.isEmpty)) { + // remove emtpy style element + detachNodeFromParent(style.node, style.parentNode); + } else { + // update style element if any styles left + const firstChild = style.node.children[0]; + if (firstChild.type === 'text' || firstChild.type === 'cdata') { + firstChild.value = csstree.generate(style.cssAst); + } + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/mergePaths.js b/node_modules/svgo/plugins/mergePaths.js new file mode 100644 index 00000000..05c7ac70 --- /dev/null +++ b/node_modules/svgo/plugins/mergePaths.js @@ -0,0 +1,98 @@ +'use strict'; + +const { detachNodeFromParent } = require('../lib/xast.js'); +const { collectStylesheet, computeStyle } = require('../lib/style.js'); +const { path2js, js2path, intersects } = require('./_path.js'); + +exports.name = 'mergePaths'; +exports.description = 'merges multiple paths in one if possible'; + +/** + * Merge multiple Paths into one. + * + * @author Kir Belevich, Lev Solntsev + * + * @type {import('./plugins-types').Plugin<'mergePaths'>} + */ +exports.fn = (root, params) => { + const { + force = false, + floatPrecision, + noSpaceAfterFlags = false, // a20 60 45 0 1 30 20 → a20 60 45 0130 20 + } = params; + const stylesheet = collectStylesheet(root); + + return { + element: { + enter: (node) => { + let prevChild = null; + + for (const child of node.children) { + // skip if previous element is not path or contains animation elements + if ( + prevChild == null || + prevChild.type !== 'element' || + prevChild.name !== 'path' || + prevChild.children.length !== 0 || + prevChild.attributes.d == null + ) { + prevChild = child; + continue; + } + + // skip if element is not path or contains animation elements + if ( + child.type !== 'element' || + child.name !== 'path' || + child.children.length !== 0 || + child.attributes.d == null + ) { + prevChild = child; + continue; + } + + // preserve paths with markers + const computedStyle = computeStyle(stylesheet, child); + if ( + computedStyle['marker-start'] || + computedStyle['marker-mid'] || + computedStyle['marker-end'] + ) { + prevChild = child; + continue; + } + + const prevChildAttrs = Object.keys(prevChild.attributes); + const childAttrs = Object.keys(child.attributes); + let attributesAreEqual = prevChildAttrs.length === childAttrs.length; + for (const name of childAttrs) { + if (name !== 'd') { + if ( + prevChild.attributes[name] == null || + prevChild.attributes[name] !== child.attributes[name] + ) { + attributesAreEqual = false; + } + } + } + const prevPathJS = path2js(prevChild); + const curPathJS = path2js(child); + + if ( + attributesAreEqual && + (force || !intersects(prevPathJS, curPathJS)) + ) { + js2path(prevChild, prevPathJS.concat(curPathJS), { + floatPrecision, + noSpaceAfterFlags, + }); + detachNodeFromParent(child, node); + continue; + } + + prevChild = child; + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/mergeStyles.js b/node_modules/svgo/plugins/mergeStyles.js new file mode 100644 index 00000000..1cc32015 --- /dev/null +++ b/node_modules/svgo/plugins/mergeStyles.js @@ -0,0 +1,98 @@ +'use strict'; + +/** + * @typedef {import('../lib/types').XastElement} XastElement + * @typedef {import('../lib/types').XastChild} XastChild + */ + +const { visitSkip, detachNodeFromParent } = require('../lib/xast.js'); + +exports.name = 'mergeStyles'; +exports.description = 'merge multiple style elements into one'; + +/** + * Merge multiple style elements into one. + * + * @author strarsis <strarsis@gmail.com> + * + * @type {import('./plugins-types').Plugin<'mergeStyles'>} + */ +exports.fn = () => { + /** + * @type {null | XastElement} + */ + let firstStyleElement = null; + let collectedStyles = ''; + /** + * @type {'text' | 'cdata'} + */ + let styleContentType = 'text'; + + return { + element: { + enter: (node, parentNode) => { + // skip <foreignObject> content + if (node.name === 'foreignObject') { + return visitSkip; + } + + // collect style elements + if (node.name !== 'style') { + return; + } + + // skip <style> with invalid type attribute + if ( + node.attributes.type != null && + node.attributes.type !== '' && + node.attributes.type !== 'text/css' + ) { + return; + } + + // extract style element content + let css = ''; + for (const child of node.children) { + if (child.type === 'text') { + css += child.value; + } + if (child.type === 'cdata') { + styleContentType = 'cdata'; + css += child.value; + } + } + + // remove empty style elements + if (css.trim().length === 0) { + detachNodeFromParent(node, parentNode); + return; + } + + // collect css and wrap with media query if present in attribute + if (node.attributes.media == null) { + collectedStyles += css; + } else { + collectedStyles += `@media ${node.attributes.media}{${css}}`; + delete node.attributes.media; + } + + // combine collected styles in the first style element + if (firstStyleElement == null) { + firstStyleElement = node; + } else { + detachNodeFromParent(node, parentNode); + /** + * @type {XastChild} + */ + const child = { type: styleContentType, value: collectedStyles }; + // TODO remove legacy parentNode in v4 + Object.defineProperty(child, 'parentNode', { + writable: true, + value: firstStyleElement, + }); + firstStyleElement.children = [child]; + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/minifyStyles.js b/node_modules/svgo/plugins/minifyStyles.js new file mode 100644 index 00000000..a10b8d20 --- /dev/null +++ b/node_modules/svgo/plugins/minifyStyles.js @@ -0,0 +1,139 @@ +'use strict'; + +/** + * @typedef {import('../lib/types').XastElement} XastElement + */ + +const csso = require('csso'); + +exports.name = 'minifyStyles'; +exports.description = + 'minifies styles and removes unused styles based on usage data'; + +/** + * Minifies styles (<style> element + style attribute) using CSSO + * + * @author strarsis <strarsis@gmail.com> + * + * @type {import('./plugins-types').Plugin<'minifyStyles'>} + */ +exports.fn = (_root, { usage, ...params }) => { + let enableTagsUsage = true; + let enableIdsUsage = true; + let enableClassesUsage = true; + // force to use usage data even if it unsafe (document contains <script> or on* attributes) + let forceUsageDeoptimized = false; + if (typeof usage === 'boolean') { + enableTagsUsage = usage; + enableIdsUsage = usage; + enableClassesUsage = usage; + } else if (usage) { + enableTagsUsage = usage.tags == null ? true : usage.tags; + enableIdsUsage = usage.ids == null ? true : usage.ids; + 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) => { + // detect deoptimisations + if (node.name === 'script') { + 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) { + idsUsage.add(node.attributes.id); + } + if (node.attributes.class != null) { + for (const className of node.attributes.class.split(/\s+/)) { + classesUsage.add(className); + } + } + // collect style elements or elements with style attribute + if (node.name === 'style' && node.children.length !== 0) { + styleElements.push(node); + } else if (node.attributes.style != null) { + elementsWithStyleAttributes.push(node); + } + }, + }, + + root: { + exit: () => { + /** + * @type {csso.Usage} + */ + const cssoUsage = {}; + if (deoptimized === false || forceUsageDeoptimized === true) { + if (enableTagsUsage && tagsUsage.size !== 0) { + cssoUsage.tags = Array.from(tagsUsage); + } + if (enableIdsUsage && idsUsage.size !== 0) { + cssoUsage.ids = Array.from(idsUsage); + } + if (enableClassesUsage && classesUsage.size !== 0) { + cssoUsage.classes = Array.from(classesUsage); + } + } + // minify style elements + for (const node of styleElements) { + if ( + node.children[0].type === 'text' || + node.children[0].type === 'cdata' + ) { + const cssText = node.children[0].value; + const minified = csso.minify(cssText, { + ...params, + usage: cssoUsage, + }).css; + // 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; + } else { + node.children[0].type = 'text'; + node.children[0].value = minified; + } + } + } + // minify style attributes + for (const node of elementsWithStyleAttributes) { + // style attribute + const elemStyle = node.attributes.style; + node.attributes.style = csso.minifyBlock(elemStyle, { + ...params, + }).css; + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/moveElemsAttrsToGroup.js b/node_modules/svgo/plugins/moveElemsAttrsToGroup.js new file mode 100644 index 00000000..5f46e632 --- /dev/null +++ b/node_modules/svgo/plugins/moveElemsAttrsToGroup.js @@ -0,0 +1,128 @@ +'use strict'; + +const { visit } = require('../lib/xast.js'); +const { inheritableAttrs, pathElems } = require('./_collections.js'); + +exports.name = 'moveElemsAttrsToGroup'; +exports.description = 'Move common attributes of group children to the group'; + +/** + * Move common attributes of group children to the group + * + * @example + * <g attr1="val1"> + * <g attr2="val2"> + * text + * </g> + * <circle attr2="val2" attr3="val3"/> + * </g> + * ⬇ + * <g attr1="val1" attr2="val2"> + * <g> + * text + * </g> + * <circle attr3="val3"/> + * </g> + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'moveElemsAttrsToGroup'>} + */ +exports.fn = (root) => { + // find if any style element is present + let deoptimizedWithStyles = false; + visit(root, { + element: { + enter: (node) => { + if (node.name === 'style') { + deoptimizedWithStyles = true; + } + }, + }, + }); + + return { + element: { + exit: (node) => { + // process only groups with more than 1 children + if (node.name !== 'g' || node.children.length <= 1) { + return; + } + + // deoptimize the plugin when style elements are present + // selectors may rely on id, classes or tag names + if (deoptimizedWithStyles) { + return; + } + + /** + * find common attributes in group children + * @type {Map<string, string>} + */ + const commonAttributes = new Map(); + let initial = true; + let everyChildIsPath = true; + for (const child of node.children) { + if (child.type === 'element') { + if (pathElems.includes(child.name) === false) { + everyChildIsPath = false; + } + if (initial) { + initial = false; + // collect all inheritable attributes from first child element + for (const [name, value] of Object.entries(child.attributes)) { + // consider only inheritable attributes + if (inheritableAttrs.includes(name)) { + commonAttributes.set(name, value); + } + } + } else { + // exclude uncommon attributes from initial list + for (const [name, value] of commonAttributes) { + if (child.attributes[name] !== value) { + commonAttributes.delete(name); + } + } + } + } + } + + // preserve transform on children when group has clip-path or mask + if ( + node.attributes['clip-path'] != null || + node.attributes.mask != null + ) { + commonAttributes.delete('transform'); + } + + // preserve transform when all children are paths + // so the transform could be applied to path data by other plugins + if (everyChildIsPath) { + commonAttributes.delete('transform'); + } + + // add common children attributes to group + for (const [name, value] of commonAttributes) { + if (name === 'transform') { + if (node.attributes.transform != null) { + node.attributes.transform = `${node.attributes.transform} ${value}`; + } else { + node.attributes.transform = value; + } + } else { + node.attributes[name] = value; + } + } + + // delete common attributes from children + for (const child of node.children) { + if (child.type === 'element') { + for (const [name] of commonAttributes) { + delete child.attributes[name]; + } + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/moveGroupAttrsToElems.js b/node_modules/svgo/plugins/moveGroupAttrsToElems.js new file mode 100644 index 00000000..60b55676 --- /dev/null +++ b/node_modules/svgo/plugins/moveGroupAttrsToElems.js @@ -0,0 +1,64 @@ +'use strict'; + +const { pathElems, referencesProps } = require('./_collections.js'); + +exports.name = 'moveGroupAttrsToElems'; +exports.description = 'moves some group attributes to the content elements'; + +const pathElemsWithGroupsAndText = [...pathElems, 'g', 'text']; + +/** + * Move group attrs to the content elements. + * + * @example + * <g transform="scale(2)"> + * <path transform="rotate(45)" d="M0,0 L10,20"/> + * <path transform="translate(10, 20)" d="M0,10 L20,30"/> + * </g> + * ⬇ + * <g> + * <path transform="scale(2) rotate(45)" d="M0,0 L10,20"/> + * <path transform="scale(2) translate(10, 20)" d="M0,10 L20,30"/> + * </g> + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'moveGroupAttrsToElems'>} + */ +exports.fn = () => { + return { + element: { + enter: (node) => { + // move group transform attr to content's pathElems + if ( + node.name === 'g' && + node.children.length !== 0 && + node.attributes.transform != null && + Object.entries(node.attributes).some( + ([name, value]) => + referencesProps.includes(name) && value.includes('url(') + ) === false && + node.children.every( + (child) => + child.type === 'element' && + pathElemsWithGroupsAndText.includes(child.name) && + child.attributes.id == null + ) + ) { + for (const child of node.children) { + const value = node.attributes.transform; + if (child.type === 'element') { + if (child.attributes.transform != null) { + child.attributes.transform = `${value} ${child.attributes.transform}`; + } else { + child.attributes.transform = value; + } + } + } + + delete node.attributes.transform; + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/plugins-types.ts b/node_modules/svgo/plugins/plugins-types.ts new file mode 100644 index 00000000..a4cca210 --- /dev/null +++ b/node_modules/svgo/plugins/plugins-types.ts @@ -0,0 +1,251 @@ +import type { + Plugin as PluginDef, + PluginInfo, + XastElement, +} from '../lib/types'; + +type DefaultPlugins = { + cleanupAttrs: { + newlines?: boolean; + trim?: boolean; + spaces?: boolean; + }; + cleanupEnableBackground: void; + cleanupIds: { + remove?: boolean; + minify?: boolean; + preserve?: Array<string>; + preservePrefixes?: Array<string>; + force?: boolean; + }; + cleanupNumericValues: { + floatPrecision?: number; + leadingZero?: boolean; + defaultPx?: boolean; + convertToPx?: boolean; + }; + collapseGroups: void; + convertColors: { + currentColor?: boolean | string | RegExp; + names2hex?: boolean; + rgb2hex?: boolean; + shorthex?: boolean; + shortname?: boolean; + }; + convertEllipseToCircle: void; + convertPathData: { + applyTransforms?: boolean; + applyTransformsStroked?: boolean; + makeArcs?: { + threshold: number; + tolerance: number; + }; + straightCurves?: boolean; + lineShorthands?: boolean; + curveSmoothShorthands?: boolean; + floatPrecision?: number | false; + transformPrecision?: number; + removeUseless?: boolean; + collapseRepeated?: boolean; + utilizeAbsolute?: boolean; + leadingZero?: boolean; + negativeExtraSpace?: boolean; + noSpaceAfterFlags?: boolean; + forceAbsolutePath?: boolean; + }; + convertShapeToPath: { + convertArcs?: boolean; + floatPrecision?: number; + }; + convertTransform: { + convertToShorts?: boolean; + degPrecision?: number; + floatPrecision?: number; + transformPrecision?: number; + matrixToTransform?: boolean; + shortTranslate?: boolean; + shortScale?: boolean; + shortRotate?: boolean; + removeUseless?: boolean; + collapseIntoOne?: boolean; + leadingZero?: boolean; + negativeExtraSpace?: boolean; + }; + mergeStyles: void; + inlineStyles: { + onlyMatchedOnce?: boolean; + removeMatchedSelectors?: boolean; + useMqs?: Array<string>; + usePseudos?: Array<string>; + }; + mergePaths: { + force?: boolean; + floatPrecision?: number; + noSpaceAfterFlags?: boolean; + }; + + minifyStyles: { + /** + * Disable or enable a structure optimisations. + * @default true + */ + restructure?: boolean | undefined; + /** + * Enables merging of @media rules with the same media query by splitted 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; + /** + * Specify what comments to leave: + * - 'exclamation' or true – leave all exclamation comments + * - 'first-exclamation' – remove every comment except first one + * - false – remove all comments + * @default true + */ + comments?: string | boolean | undefined; + /** + * Advanced optimizations + */ + usage?: + | boolean + | { + force?: boolean; + ids?: boolean; + classes?: boolean; + tags?: boolean; + }; + }; + + moveElemsAttrsToGroup: void; + moveGroupAttrsToElems: void; + removeComments: void; + removeDesc: { + removeAny?: boolean; + }; + removeDoctype: void; + removeEditorsNSData: { + additionalNamespaces?: Array<string>; + }; + removeEmptyAttrs: void; + removeEmptyContainers: void; + removeEmptyText: { + text?: boolean; + tspan?: boolean; + tref?: boolean; + }; + removeHiddenElems: { + isHidden?: boolean; + displayNone?: boolean; + opacity0?: boolean; + circleR0?: boolean; + ellipseRX0?: boolean; + ellipseRY0?: boolean; + rectWidth0?: boolean; + rectHeight0?: boolean; + patternWidth0?: boolean; + patternHeight0?: boolean; + imageWidth0?: boolean; + imageHeight0?: boolean; + pathEmptyD?: boolean; + polylineEmptyPoints?: boolean; + polygonEmptyPoints?: boolean; + }; + removeMetadata: void; + removeNonInheritableGroupAttrs: void; + removeTitle: void; + removeUnknownsAndDefaults: { + unknownContent?: boolean; + unknownAttrs?: boolean; + defaultAttrs?: boolean; + uselessOverrides?: boolean; + keepDataAttrs?: boolean; + keepAriaAttrs?: boolean; + keepRoleAttr?: boolean; + }; + removeUnusedNS: void; + removeUselessDefs: void; + removeUselessStrokeAndFill: { + stroke?: boolean; + fill?: boolean; + removeNone?: boolean; + }; + removeViewBox: void; + removeXMLProcInst: void; + sortAttrs: { + order?: Array<string>; + xmlnsOrder?: 'front' | 'alphabetical'; + }; + sortDefsChildren: void; +}; + +type PresetDefaultOverrides = { + [Name in keyof DefaultPlugins]?: DefaultPlugins[Name] | false; +}; + +export type BuiltinsWithOptionalParams = DefaultPlugins & { + 'preset-default': { + floatPrecision?: number; + /** + * All default plugins can be customized or disabled here + * for example + * { + * sortAttrs: { xmlnsOrder: "alphabetical" }, + * cleanupAttrs: false, + * } + */ + overrides?: PresetDefaultOverrides; + }; + cleanupListOfValues: { + floatPrecision?: number; + leadingZero?: boolean; + defaultPx?: boolean; + convertToPx?: boolean; + }; + convertStyleToAttrs: { + keepImportant?: boolean; + }; + prefixIds: { + prefix?: + | boolean + | string + | ((node: XastElement, info: PluginInfo) => string); + delim?: string; + prefixIds?: boolean; + prefixClassNames?: boolean; + }; + removeDimensions: void; + removeOffCanvasPaths: void; + removeRasterImages: void; + removeScriptElement: void; + removeStyleElement: void; + removeXMLNS: void; + reusePaths: void; +}; + +export type BuiltinsWithRequiredParams = { + addAttributesToSVGElement: { + attribute?: string | Record<string, null | string>; + attributes?: Array<string | Record<string, null | string>>; + }; + addClassesToSVGElement: { + className?: string; + classNames?: Array<string>; + }; + removeAttributesBySelector: any; + removeAttrs: { + elemSeparator?: string; + preserveCurrentColor?: boolean; + attrs: string | Array<string>; + }; + removeElementsByAttr: { + id?: string | Array<string>; + class?: string | Array<string>; + }; +}; + +type PluginsParams = BuiltinsWithOptionalParams & BuiltinsWithRequiredParams; + +export type Plugin<Name extends keyof PluginsParams> = PluginDef< + PluginsParams[Name] +>; diff --git a/node_modules/svgo/plugins/prefixIds.js b/node_modules/svgo/plugins/prefixIds.js new file mode 100644 index 00000000..66762936 --- /dev/null +++ b/node_modules/svgo/plugins/prefixIds.js @@ -0,0 +1,229 @@ +'use strict'; + +const csstree = require('css-tree'); +const { referencesProps } = require('./_collections.js'); + +exports.name = 'prefixIds'; +exports.description = 'prefix IDs'; + +/** + * extract basename from path + * @type {(path: string) => string} + */ +const getBasename = (path) => { + // extract everything after latest slash or backslash + const matched = path.match(/[/\\]?([^/\\]+)$/); + if (matched) { + return matched[1]; + } + return ''; +}; + +/** + * escapes a string for being used as ID + * @type {(string: string) => string} + */ +const escapeIdentifierName = (str) => { + return str.replace(/[. ]/g, '_'); +}; + +/** + * @type {(string: string) => string} + */ +const unquote = (string) => { + if ( + (string.startsWith('"') && string.endsWith('"')) || + (string.startsWith("'") && string.endsWith("'")) + ) { + return string.slice(1, -1); + } + return string; +}; + +/** + * prefix an ID + * @type {(prefix: string, name: string) => string} + */ +const prefixId = (prefix, value) => { + if (value.startsWith(prefix)) { + return value; + } + return prefix + value; +}; + +/** + * prefix an #ID + * @type {(prefix: string, name: string) => string | null} + */ +const prefixReference = (prefix, value) => { + if (value.startsWith('#')) { + return '#' + prefixId(prefix, value.slice(1)); + } + return null; +}; + +/** @type {(value: any) => any} */ +const toAny = (value) => value; + +/** + * 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; + + return { + element: { + enter: (node) => { + /** + * prefix, from file name or option + * @type {string} + */ + 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; + } + + // prefix id/class selectors and url() references in styles + if (node.name === 'style') { + // skip empty <style/> elements + if (node.children.length === 0) { + 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; + } + + csstree.walk(cssAst, (node) => { + // #ID, .class selectors + if ( + (prefixIds && node.type === 'IdSelector') || + (prefixClassNames && node.type === 'ClassSelector') + ) { + node.name = prefixId(prefix, node.name); + 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; + } + } + }); + + // update styles + if ( + node.children[0].type === 'text' || + node.children[0].type === 'cdata' + ) { + node.children[0].value = csstree.generate(cssAst); + } + return; + } + + // prefix an ID attribute value + if ( + prefixIds && + node.attributes.id != null && + node.attributes.id.length !== 0 + ) { + node.attributes.id = prefixId(prefix, node.attributes.id); + } + + // prefix a class attribute value + if ( + prefixClassNames && + node.attributes.class != null && + node.attributes.class.length !== 0 + ) { + node.attributes.class = node.attributes.class + .split(/\s+/) + .map((name) => prefixId(prefix, name)) + .join(' '); + } + + // prefix a href attribute value + // xlink:href is deprecated, must be still supported + for (const name of ['href', 'xlink:href']) { + if ( + node.attributes[name] != null && + node.attributes[name].length !== 0 + ) { + const prefixed = prefixReference(prefix, node.attributes[name]); + if (prefixed != null) { + node.attributes[name] = prefixed; + } + } + } + + // prefix an 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); + if (prefixed == null) { + return match; + } + return `url(${prefixed})`; + } + ); + } + } + + // prefix begin/end attribute value + for (const name of ['begin', 'end']) { + if ( + node.attributes[name] != null && + node.attributes[name].length !== 0 + ) { + 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 val; + }); + node.attributes[name] = parts.join('; '); + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/preset-default.js b/node_modules/svgo/plugins/preset-default.js new file mode 100644 index 00000000..86516fd7 --- /dev/null +++ b/node_modules/svgo/plugins/preset-default.js @@ -0,0 +1,82 @@ +'use strict'; + +const { createPreset } = require('../lib/svgo/plugins.js'); + +const removeDoctype = require('./removeDoctype.js'); +const removeXMLProcInst = require('./removeXMLProcInst.js'); +const removeComments = require('./removeComments.js'); +const removeMetadata = require('./removeMetadata.js'); +const removeEditorsNSData = require('./removeEditorsNSData.js'); +const cleanupAttrs = require('./cleanupAttrs.js'); +const mergeStyles = require('./mergeStyles.js'); +const inlineStyles = require('./inlineStyles.js'); +const minifyStyles = require('./minifyStyles.js'); +const cleanupIds = require('./cleanupIds.js'); +const removeUselessDefs = require('./removeUselessDefs.js'); +const cleanupNumericValues = require('./cleanupNumericValues.js'); +const convertColors = require('./convertColors.js'); +const removeUnknownsAndDefaults = require('./removeUnknownsAndDefaults.js'); +const removeNonInheritableGroupAttrs = require('./removeNonInheritableGroupAttrs.js'); +const removeUselessStrokeAndFill = require('./removeUselessStrokeAndFill.js'); +const removeViewBox = require('./removeViewBox.js'); +const cleanupEnableBackground = require('./cleanupEnableBackground.js'); +const removeHiddenElems = require('./removeHiddenElems.js'); +const removeEmptyText = require('./removeEmptyText.js'); +const convertShapeToPath = require('./convertShapeToPath.js'); +const convertEllipseToCircle = require('./convertEllipseToCircle.js'); +const moveElemsAttrsToGroup = require('./moveElemsAttrsToGroup.js'); +const moveGroupAttrsToElems = require('./moveGroupAttrsToElems.js'); +const collapseGroups = require('./collapseGroups.js'); +const convertPathData = require('./convertPathData.js'); +const convertTransform = require('./convertTransform.js'); +const removeEmptyAttrs = require('./removeEmptyAttrs.js'); +const removeEmptyContainers = require('./removeEmptyContainers.js'); +const mergePaths = require('./mergePaths.js'); +const removeUnusedNS = require('./removeUnusedNS.js'); +const sortAttrs = require('./sortAttrs.js'); +const sortDefsChildren = require('./sortDefsChildren.js'); +const removeTitle = require('./removeTitle.js'); +const removeDesc = require('./removeDesc.js'); + +const presetDefault = createPreset({ + name: 'preset-default', + plugins: [ + removeDoctype, + removeXMLProcInst, + removeComments, + removeMetadata, + removeEditorsNSData, + cleanupAttrs, + mergeStyles, + inlineStyles, + minifyStyles, + cleanupIds, + removeUselessDefs, + cleanupNumericValues, + convertColors, + removeUnknownsAndDefaults, + removeNonInheritableGroupAttrs, + removeUselessStrokeAndFill, + removeViewBox, + cleanupEnableBackground, + removeHiddenElems, + removeEmptyText, + convertShapeToPath, + convertEllipseToCircle, + moveElemsAttrsToGroup, + moveGroupAttrsToElems, + collapseGroups, + convertPathData, + convertTransform, + removeEmptyAttrs, + removeEmptyContainers, + mergePaths, + removeUnusedNS, + sortAttrs, + sortDefsChildren, + removeTitle, + removeDesc, + ], +}); + +module.exports = presetDefault; diff --git a/node_modules/svgo/plugins/removeAttributesBySelector.js b/node_modules/svgo/plugins/removeAttributesBySelector.js new file mode 100644 index 00000000..09db8a8d --- /dev/null +++ b/node_modules/svgo/plugins/removeAttributesBySelector.js @@ -0,0 +1,97 @@ +'use strict'; + +const { querySelectorAll } = require('../lib/xast.js'); + +exports.name = 'removeAttributesBySelector'; +exports.description = + 'removes attributes of elements that match a css selector'; + +/** + * Removes attributes of elements that match a css selector. + * + * @example + * <caption>A selector removing a single attribute</caption> + * plugins: [ + * { + * name: "removeAttributesBySelector", + * params: { + * selector: "[fill='#00ff00']" + * attributes: "fill" + * } + * } + * ] + * + * <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/> + * ↓ + * <rect x="0" y="0" width="100" height="100" stroke="#00ff00"/> + * + * <caption>A selector removing multiple attributes</caption> + * plugins: [ + * { + * name: "removeAttributesBySelector", + * params: { + * selector: "[fill='#00ff00']", + * attributes: [ + * "fill", + * "stroke" + * ] + * } + * } + * ] + * + * <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/> + * ↓ + * <rect x="0" y="0" width="100" height="100"/> + * + * <caption>Multiple selectors removing attributes</caption> + * plugins: [ + * { + * name: "removeAttributesBySelector", + * params: { + * selectors: [ + * { + * selector: "[fill='#00ff00']", + * attributes: "fill" + * }, + * { + * selector: "#remove", + * attributes: [ + * "stroke", + * "id" + * ] + * } + * ] + * } + * } + * ] + * + * <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/> + * ↓ + * <rect x="0" y="0" width="100" height="100"/> + * + * @link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|MDN CSS Selectors + * + * @author Bradley Mease + * + * @type {import('./plugins-types').Plugin<'removeAttributesBySelector'>} + */ +exports.fn = (root, params) => { + const selectors = Array.isArray(params.selectors) + ? params.selectors + : [params]; + for (const { selector, attributes } of selectors) { + const nodes = querySelectorAll(root, selector); + for (const node of nodes) { + if (node.type === 'element') { + if (Array.isArray(attributes)) { + for (const name of attributes) { + delete node.attributes[name]; + } + } else { + delete node.attributes[attributes]; + } + } + } + } + return {}; +}; diff --git a/node_modules/svgo/plugins/removeAttrs.js b/node_modules/svgo/plugins/removeAttrs.js new file mode 100644 index 00000000..c289d8ca --- /dev/null +++ b/node_modules/svgo/plugins/removeAttrs.js @@ -0,0 +1,153 @@ +'use strict'; + +exports.name = 'removeAttrs'; +exports.description = 'removes specified attributes'; + +const DEFAULT_SEPARATOR = ':'; +const ENOATTRS = `Warning: The plugin "removeAttrs" requires the "attrs" parameter. +It should have a pattern to remove, otherwise the plugin is a noop. +Config example: + +plugins: [ + { + name: "removeAttrs", + params: { + attrs: "(fill|stroke)" + } + } +] +`; + +/** + * Remove attributes + * + * @example elemSeparator + * format: string + * + * @example preserveCurrentColor + * format: boolean + * + * @example attrs: + * + * format: [ element* : attribute* : value* ] + * + * element : regexp (wrapped into ^...$), single * or omitted > all elements (must be present when value is used) + * attribute : regexp (wrapped into ^...$) + * value : regexp (wrapped into ^...$), single * or omitted > all values + * + * examples: + * + * > basic: remove fill attribute + * --- + * removeAttrs: + * attrs: 'fill' + * + * > remove fill attribute on path element + * --- + * attrs: 'path:fill' + * + * > remove fill attribute on path element where value is none + * --- + * attrs: 'path:fill:none' + * + * + * > remove all fill and stroke attribute + * --- + * attrs: + * - 'fill' + * - 'stroke' + * + * [is same as] + * + * attrs: '(fill|stroke)' + * + * [is same as] + * + * attrs: '*:(fill|stroke)' + * + * [is same as] + * + * attrs: '.*:(fill|stroke)' + * + * [is same as] + * + * attrs: '.*:(fill|stroke):.*' + * + * + * > remove all stroke related attributes + * ---- + * attrs: 'stroke.*' + * + * + * @author Benny Schudel + * + * @type {import('./plugins-types').Plugin<'removeAttrs'>} + */ +exports.fn = (root, params) => { + if (typeof params.attrs == 'undefined') { + console.warn(ENOATTRS); + return null; + } + + const elemSeparator = + typeof params.elemSeparator == 'string' + ? params.elemSeparator + : DEFAULT_SEPARATOR; + const preserveCurrentColor = + typeof params.preserveCurrentColor == 'boolean' + ? params.preserveCurrentColor + : false; + const attrs = Array.isArray(params.attrs) ? params.attrs : [params.attrs]; + + return { + element: { + 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 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(''); + } + + // create regexps for element, attribute name, and attribute value + const list = pattern.split(elemSeparator).map((value) => { + // adjust single * to match anything + if (value === '*') { + value = '.*'; + } + return new RegExp(['^', value, '$'].join(''), 'i'); + }); + + // matches element + if (list[0].test(node.name)) { + // loop attributes + for (const [name, value] of Object.entries(node.attributes)) { + const isFillCurrentColor = + preserveCurrentColor && + name == 'fill' && + value == 'currentColor'; + const isStrokeCurrentColor = + preserveCurrentColor && + name == 'stroke' && + value == 'currentColor'; + if ( + !isFillCurrentColor && + !isStrokeCurrentColor && + // matches attribute name + list[1].test(name) && + // matches attribute value + list[2].test(value) + ) { + delete node.attributes[name]; + } + } + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeComments.js b/node_modules/svgo/plugins/removeComments.js new file mode 100644 index 00000000..70e46271 --- /dev/null +++ b/node_modules/svgo/plugins/removeComments.js @@ -0,0 +1,29 @@ +'use strict'; + +const { detachNodeFromParent } = require('../lib/xast.js'); + +exports.name = 'removeComments'; +exports.description = 'removes comments'; + +/** + * Remove comments. + * + * @example + * <!-- Generator: Adobe Illustrator 15.0.0, SVG Export + * Plug-In . SVG Version: 6.00 Build 0) --> + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'removeComments'>} + */ +exports.fn = () => { + return { + comment: { + enter: (node, parentNode) => { + if (node.value.charAt(0) !== '!') { + detachNodeFromParent(node, parentNode); + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeDesc.js b/node_modules/svgo/plugins/removeDesc.js new file mode 100644 index 00000000..5d9e57ae --- /dev/null +++ b/node_modules/svgo/plugins/removeDesc.js @@ -0,0 +1,39 @@ +'use strict'; + +const { detachNodeFromParent } = require('../lib/xast.js'); + +exports.name = 'removeDesc'; +exports.description = 'removes <desc>'; + +const standardDescs = /^(Created with|Created using)/; + +/** + * Removes <desc>. + * 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 + * + * @author Daniel Wabyick + * + * @type {import('./plugins-types').Plugin<'removeDesc'>} + */ +exports.fn = (root, params) => { + const { removeAny = true } = params; + return { + element: { + enter: (node, parentNode) => { + if (node.name === 'desc') { + if ( + removeAny || + node.children.length === 0 || + (node.children[0].type === 'text' && + standardDescs.test(node.children[0].value)) + ) { + detachNodeFromParent(node, parentNode); + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeDimensions.js b/node_modules/svgo/plugins/removeDimensions.js new file mode 100644 index 00000000..4f4f3984 --- /dev/null +++ b/node_modules/svgo/plugins/removeDimensions.js @@ -0,0 +1,43 @@ +'use strict'; + +exports.name = 'removeDimensions'; +exports.description = + 'removes width and height in presence of viewBox (opposite to removeViewBox, disable it first)'; + +/** + * Remove width/height attributes and add the viewBox attribute if it's missing + * + * @example + * <svg width="100" height="50" /> + * ↓ + * <svg viewBox="0 0 100 50" /> + * + * @author Benny Schudel + * + * @type {import('./plugins-types').Plugin<'removeDimensions'>} + */ +exports.fn = () => { + return { + element: { + enter: (node) => { + if (node.name === 'svg') { + if (node.attributes.viewBox != null) { + delete node.attributes.width; + delete node.attributes.height; + } else if ( + node.attributes.width != null && + node.attributes.height != null && + Number.isNaN(Number(node.attributes.width)) === false && + Number.isNaN(Number(node.attributes.height)) === false + ) { + const width = Number(node.attributes.width); + const height = Number(node.attributes.height); + node.attributes.viewBox = `0 0 ${width} ${height}`; + delete node.attributes.width; + delete node.attributes.height; + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeDoctype.js b/node_modules/svgo/plugins/removeDoctype.js new file mode 100644 index 00000000..cbdcd013 --- /dev/null +++ b/node_modules/svgo/plugins/removeDoctype.js @@ -0,0 +1,40 @@ +'use strict'; + +const { detachNodeFromParent } = require('../lib/xast.js'); + +exports.name = 'removeDoctype'; +exports.description = 'removes doctype declaration'; + +/** + * Remove DOCTYPE declaration. + * + * "Unfortunately the SVG DTDs are a source of so many + * issues that the SVG WG has decided not to write one + * for the upcoming SVG 1.2 standard. In fact SVG WG + * members are even telling people not to use a DOCTYPE + * declaration in SVG 1.0 and 1.1 documents" + * https://jwatt.org/svg/authoring/#doctype-declaration + * + * @example + * <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + * q"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> + * + * @example + * <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + * "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" [ + * <!-- an internal subset can be embedded here --> + * ]> + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'removeDoctype'>} + */ +exports.fn = () => { + return { + doctype: { + enter: (node, parentNode) => { + detachNodeFromParent(node, parentNode); + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeEditorsNSData.js b/node_modules/svgo/plugins/removeEditorsNSData.js new file mode 100644 index 00000000..cf8016cd --- /dev/null +++ b/node_modules/svgo/plugins/removeEditorsNSData.js @@ -0,0 +1,64 @@ +'use strict'; + +const { detachNodeFromParent } = require('../lib/xast.js'); +const { editorNamespaces } = require('./_collections.js'); + +exports.name = 'removeEditorsNSData'; +exports.description = 'removes editors namespaces, elements and attributes'; + +/** + * Remove editors namespaces, elements and attributes. + * + * @example + * <svg xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"> + * <sodipodi:namedview/> + * <path sodipodi:nodetypes="cccc"/> + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'removeEditorsNSData'>} + */ +exports.fn = (_root, params) => { + let namespaces = editorNamespaces; + if (Array.isArray(params.additionalNamespaces)) { + namespaces = [...editorNamespaces, ...params.additionalNamespaces]; + } + /** + * @type {Array<string>} + */ + const prefixes = []; + return { + element: { + enter: (node, parentNode) => { + // collect namespace aliases from svg element + if (node.name === 'svg') { + for (const [name, value] of Object.entries(node.attributes)) { + if (name.startsWith('xmlns:') && namespaces.includes(value)) { + prefixes.push(name.slice('xmlns:'.length)); + // <svg xmlns:sodipodi=""> + delete node.attributes[name]; + } + } + } + // remove editor attributes, for example + // <* sodipodi:*=""> + for (const name of Object.keys(node.attributes)) { + if (name.includes(':')) { + const [prefix] = name.split(':'); + if (prefixes.includes(prefix)) { + delete node.attributes[name]; + } + } + } + // remove editor elements, for example + // <sodipodi:*> + if (node.name.includes(':')) { + const [prefix] = node.name.split(':'); + if (prefixes.includes(prefix)) { + detachNodeFromParent(node, parentNode); + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeElementsByAttr.js b/node_modules/svgo/plugins/removeElementsByAttr.js new file mode 100644 index 00000000..b5fd91ba --- /dev/null +++ b/node_modules/svgo/plugins/removeElementsByAttr.js @@ -0,0 +1,73 @@ +'use strict'; + +const { detachNodeFromParent } = require('../lib/xast.js'); + +exports.name = 'removeElementsByAttr'; +exports.description = + 'removes arbitrary elements by ID or className (disabled by default)'; + +/** + * Remove arbitrary SVG elements by ID or className. + * + * @example id + * > single: remove element with ID of `elementID` + * --- + * removeElementsByAttr: + * id: 'elementID' + * + * > list: remove multiple elements by ID + * --- + * removeElementsByAttr: + * id: + * - 'elementID' + * - 'anotherID' + * + * @example class + * > single: remove all elements with class of `elementClass` + * --- + * removeElementsByAttr: + * class: 'elementClass' + * + * > list: remove all elements with class of `elementClass` or `anotherClass` + * --- + * removeElementsByAttr: + * class: + * - 'elementClass' + * - 'anotherClass' + * + * @author Eli Dupuis (@elidupuis) + * + * @type {import('./plugins-types').Plugin<'removeElementsByAttr'>} + */ +exports.fn = (root, params) => { + const ids = + params.id == null ? [] : Array.isArray(params.id) ? params.id : [params.id]; + const classes = + params.class == null + ? [] + : Array.isArray(params.class) + ? params.class + : [params.class]; + return { + element: { + enter: (node, parentNode) => { + // remove element if it's `id` matches configured `id` params + if (node.attributes.id != null && ids.length !== 0) { + if (ids.includes(node.attributes.id)) { + detachNodeFromParent(node, parentNode); + } + } + // remove element if it's `class` contains any of the configured `class` params + if (node.attributes.class && classes.length !== 0) { + const classList = node.attributes.class.split(' '); + for (const item of classes) { + if (classList.includes(item)) { + detachNodeFromParent(node, parentNode); + break; + } + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeEmptyAttrs.js b/node_modules/svgo/plugins/removeEmptyAttrs.js new file mode 100644 index 00000000..83f567c3 --- /dev/null +++ b/node_modules/svgo/plugins/removeEmptyAttrs.js @@ -0,0 +1,31 @@ +'use strict'; + +const { attrsGroups } = require('./_collections.js'); + +exports.name = 'removeEmptyAttrs'; +exports.description = 'removes empty attributes'; + +/** + * Remove attributes with empty values. + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'removeEmptyAttrs'>} + */ +exports.fn = () => { + return { + element: { + enter: (node) => { + for (const [name, value] of Object.entries(node.attributes)) { + if ( + value === '' && + // empty conditional processing attributes prevents elements from rendering + attrsGroups.conditionalProcessing.includes(name) === false + ) { + delete node.attributes[name]; + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeEmptyContainers.js b/node_modules/svgo/plugins/removeEmptyContainers.js new file mode 100644 index 00000000..d803fc9c --- /dev/null +++ b/node_modules/svgo/plugins/removeEmptyContainers.js @@ -0,0 +1,56 @@ +'use strict'; + +const { detachNodeFromParent } = require('../lib/xast.js'); +const { elemsGroups } = require('./_collections.js'); + +exports.name = 'removeEmptyContainers'; +exports.description = 'removes empty container elements'; + +/** + * Remove empty containers. + * + * @see https://www.w3.org/TR/SVG11/intro.html#TermContainerElement + * + * @example + * <defs/> + * + * @example + * <g><marker><a/></marker></g> + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'removeEmptyContainers'>} + */ +exports.fn = () => { + return { + element: { + exit: (node, parentNode) => { + // remove only empty non-svg containers + if ( + node.name === 'svg' || + elemsGroups.container.includes(node.name) === false || + node.children.length !== 0 + ) { + return; + } + // empty patterns may contain reusable configuration + if ( + node.name === 'pattern' && + Object.keys(node.attributes).length !== 0 + ) { + return; + } + // The <g> may not have content, but the filter may cause a rectangle + // to be created and filled with pattern. + if (node.name === 'g' && node.attributes.filter != null) { + return; + } + // empty <mask> hides masked element + if (node.name === 'mask' && node.attributes.id != null) { + return; + } + detachNodeFromParent(node, parentNode); + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeEmptyText.js b/node_modules/svgo/plugins/removeEmptyText.js new file mode 100644 index 00000000..5f6db3d3 --- /dev/null +++ b/node_modules/svgo/plugins/removeEmptyText.js @@ -0,0 +1,51 @@ +'use strict'; + +const { detachNodeFromParent } = require('../lib/xast.js'); + +exports.name = 'removeEmptyText'; +exports.description = 'removes empty <text> elements'; + +/** + * Remove empty Text elements. + * + * @see https://www.w3.org/TR/SVG11/text.html + * + * @example + * Remove empty text element: + * <text/> + * + * Remove empty tspan element: + * <tspan/> + * + * Remove tref with empty xlink:href attribute: + * <tref xlink:href=""/> + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'removeEmptyText'>} + */ +exports.fn = (root, params) => { + const { text = true, tspan = true, tref = true } = params; + return { + element: { + enter: (node, parentNode) => { + // Remove empty text element + if (text && node.name === 'text' && node.children.length === 0) { + detachNodeFromParent(node, parentNode); + } + // Remove empty tspan element + if (tspan && node.name === 'tspan' && node.children.length === 0) { + detachNodeFromParent(node, parentNode); + } + // Remove tref with empty xlink:href attribute + if ( + tref && + node.name === 'tref' && + node.attributes['xlink:href'] == null + ) { + detachNodeFromParent(node, parentNode); + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeHiddenElems.js b/node_modules/svgo/plugins/removeHiddenElems.js new file mode 100644 index 00000000..5c1a0135 --- /dev/null +++ b/node_modules/svgo/plugins/removeHiddenElems.js @@ -0,0 +1,310 @@ +'use strict'; + +const { + visit, + visitSkip, + querySelector, + detachNodeFromParent, +} = require('../lib/xast.js'); +const { collectStylesheet, computeStyle } = require('../lib/style.js'); +const { parsePathData } = require('../lib/path.js'); + +exports.name = 'removeHiddenElems'; +exports.description = + 'removes hidden elements (zero sized, with absent attributes)'; + +/** + * Remove hidden elements with disabled rendering: + * - display="none" + * - opacity="0" + * - circle with zero radius + * - ellipse with zero x-axis or y-axis radius + * - rectangle with zero width or height + * - pattern with zero width or height + * - image with zero width or height + * - path with empty data + * - polyline with empty points + * - polygon with empty points + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'removeHiddenElems'>} + */ +exports.fn = (root, params) => { + const { + isHidden = true, + displayNone = true, + opacity0 = true, + circleR0 = true, + ellipseRX0 = true, + ellipseRY0 = true, + rectWidth0 = true, + rectHeight0 = true, + patternWidth0 = true, + patternHeight0 = true, + imageWidth0 = true, + imageHeight0 = true, + pathEmptyD = true, + polylineEmptyPoints = true, + polygonEmptyPoints = true, + } = params; + const stylesheet = collectStylesheet(root); + + visit(root, { + element: { + enter: (node, parentNode) => { + // transparent element inside clipPath still affect clipped elements + if (node.name === 'clipPath') { + return visitSkip; + } + const computedStyle = computeStyle(stylesheet, node); + // opacity="0" + // + // https://www.w3.org/TR/SVG11/masking.html#ObjectAndGroupOpacityProperties + if ( + opacity0 && + computedStyle.opacity && + computedStyle.opacity.type === 'static' && + computedStyle.opacity.value === '0' + ) { + detachNodeFromParent(node, parentNode); + return; + } + }, + }, + }); + + return { + element: { + enter: (node, parentNode) => { + // Removes hidden elements + // https://www.w3schools.com/cssref/pr_class_visibility.asp + const computedStyle = computeStyle(stylesheet, node); + if ( + isHidden && + computedStyle.visibility && + computedStyle.visibility.type === 'static' && + computedStyle.visibility.value === 'hidden' && + // keep if any descendant enables visibility + querySelector(node, '[visibility=visible]') == null + ) { + detachNodeFromParent(node, parentNode); + return; + } + + // display="none" + // + // https://www.w3.org/TR/SVG11/painting.html#DisplayProperty + // "A value of display: none indicates that the given element + // and its children shall not be rendered directly" + if ( + displayNone && + computedStyle.display && + computedStyle.display.type === 'static' && + computedStyle.display.value === 'none' && + // markers with display: none still rendered + node.name !== 'marker' + ) { + detachNodeFromParent(node, parentNode); + return; + } + + // Circles with zero radius + // + // https://www.w3.org/TR/SVG11/shapes.html#CircleElementRAttribute + // "A value of zero disables rendering of the element" + // + // <circle r="0"> + if ( + circleR0 && + node.name === 'circle' && + node.children.length === 0 && + node.attributes.r === '0' + ) { + detachNodeFromParent(node, parentNode); + return; + } + + // Ellipse with zero x-axis radius + // + // https://www.w3.org/TR/SVG11/shapes.html#EllipseElementRXAttribute + // "A value of zero disables rendering of the element" + // + // <ellipse rx="0"> + if ( + ellipseRX0 && + node.name === 'ellipse' && + node.children.length === 0 && + node.attributes.rx === '0' + ) { + detachNodeFromParent(node, parentNode); + return; + } + + // Ellipse with zero y-axis radius + // + // https://www.w3.org/TR/SVG11/shapes.html#EllipseElementRYAttribute + // "A value of zero disables rendering of the element" + // + // <ellipse ry="0"> + if ( + ellipseRY0 && + node.name === 'ellipse' && + node.children.length === 0 && + node.attributes.ry === '0' + ) { + detachNodeFromParent(node, parentNode); + return; + } + + // Rectangle with zero width + // + // https://www.w3.org/TR/SVG11/shapes.html#RectElementWidthAttribute + // "A value of zero disables rendering of the element" + // + // <rect width="0"> + if ( + rectWidth0 && + node.name === 'rect' && + node.children.length === 0 && + node.attributes.width === '0' + ) { + detachNodeFromParent(node, parentNode); + return; + } + + // Rectangle with zero height + // + // https://www.w3.org/TR/SVG11/shapes.html#RectElementHeightAttribute + // "A value of zero disables rendering of the element" + // + // <rect height="0"> + if ( + rectHeight0 && + rectWidth0 && + node.name === 'rect' && + node.children.length === 0 && + node.attributes.height === '0' + ) { + detachNodeFromParent(node, parentNode); + return; + } + + // Pattern with zero width + // + // https://www.w3.org/TR/SVG11/pservers.html#PatternElementWidthAttribute + // "A value of zero disables rendering of the element (i.e., no paint is applied)" + // + // <pattern width="0"> + if ( + patternWidth0 && + node.name === 'pattern' && + node.attributes.width === '0' + ) { + detachNodeFromParent(node, parentNode); + return; + } + + // Pattern with zero height + // + // https://www.w3.org/TR/SVG11/pservers.html#PatternElementHeightAttribute + // "A value of zero disables rendering of the element (i.e., no paint is applied)" + // + // <pattern height="0"> + if ( + patternHeight0 && + node.name === 'pattern' && + node.attributes.height === '0' + ) { + detachNodeFromParent(node, parentNode); + return; + } + + // Image with zero width + // + // https://www.w3.org/TR/SVG11/struct.html#ImageElementWidthAttribute + // "A value of zero disables rendering of the element" + // + // <image width="0"> + if ( + imageWidth0 && + node.name === 'image' && + node.attributes.width === '0' + ) { + detachNodeFromParent(node, parentNode); + return; + } + + // Image with zero height + // + // https://www.w3.org/TR/SVG11/struct.html#ImageElementHeightAttribute + // "A value of zero disables rendering of the element" + // + // <image height="0"> + if ( + imageHeight0 && + node.name === 'image' && + node.attributes.height === '0' + ) { + detachNodeFromParent(node, parentNode); + return; + } + + // Path with empty data + // + // https://www.w3.org/TR/SVG11/paths.html#DAttribute + // + // <path d=""/> + if (pathEmptyD && node.name === 'path') { + if (node.attributes.d == null) { + detachNodeFromParent(node, parentNode); + return; + } + const pathData = parsePathData(node.attributes.d); + if (pathData.length === 0) { + detachNodeFromParent(node, parentNode); + return; + } + // keep single point paths for markers + if ( + pathData.length === 1 && + computedStyle['marker-start'] == null && + computedStyle['marker-end'] == null + ) { + detachNodeFromParent(node, parentNode); + return; + } + return; + } + + // Polyline with empty points + // + // https://www.w3.org/TR/SVG11/shapes.html#PolylineElementPointsAttribute + // + // <polyline points=""> + if ( + polylineEmptyPoints && + node.name === 'polyline' && + node.attributes.points == null + ) { + detachNodeFromParent(node, parentNode); + return; + } + + // Polygon with empty points + // + // https://www.w3.org/TR/SVG11/shapes.html#PolygonElementPointsAttribute + // + // <polygon points=""> + if ( + polygonEmptyPoints && + node.name === 'polygon' && + node.attributes.points == null + ) { + detachNodeFromParent(node, parentNode); + return; + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeMetadata.js b/node_modules/svgo/plugins/removeMetadata.js new file mode 100644 index 00000000..8c9de27e --- /dev/null +++ b/node_modules/svgo/plugins/removeMetadata.js @@ -0,0 +1,27 @@ +'use strict'; + +const { detachNodeFromParent } = require('../lib/xast.js'); + +exports.name = 'removeMetadata'; +exports.description = 'removes <metadata>'; + +/** + * Remove <metadata>. + * + * https://www.w3.org/TR/SVG11/metadata.html + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'removeMetadata'>} + */ +exports.fn = () => { + return { + element: { + enter: (node, parentNode) => { + if (node.name === 'metadata') { + detachNodeFromParent(node, parentNode); + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeNonInheritableGroupAttrs.js b/node_modules/svgo/plugins/removeNonInheritableGroupAttrs.js new file mode 100644 index 00000000..faa93dff --- /dev/null +++ b/node_modules/svgo/plugins/removeNonInheritableGroupAttrs.js @@ -0,0 +1,38 @@ +'use strict'; + +const { + inheritableAttrs, + attrsGroups, + presentationNonInheritableGroupAttrs, +} = require('./_collections'); + +exports.name = 'removeNonInheritableGroupAttrs'; +exports.description = + 'removes non-inheritable group’s presentational attributes'; + +/** + * Remove non-inheritable group's "presentation" attributes. + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'removeNonInheritableGroupAttrs'>} + */ +exports.fn = () => { + return { + element: { + enter: (node) => { + if (node.name === 'g') { + for (const name of Object.keys(node.attributes)) { + if ( + attrsGroups.presentation.includes(name) === true && + inheritableAttrs.includes(name) === false && + presentationNonInheritableGroupAttrs.includes(name) === false + ) { + delete node.attributes[name]; + } + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeOffCanvasPaths.js b/node_modules/svgo/plugins/removeOffCanvasPaths.js new file mode 100644 index 00000000..7a3d131d --- /dev/null +++ b/node_modules/svgo/plugins/removeOffCanvasPaths.js @@ -0,0 +1,136 @@ +'use strict'; + +/** + * @typedef {import('../lib/types').PathDataItem} PathDataItem + */ + +const { visitSkip, detachNodeFromParent } = require('../lib/xast.js'); +const { parsePathData } = require('../lib/path.js'); +const { intersects } = require('./_path.js'); + +exports.name = 'removeOffCanvasPaths'; +exports.description = + 'removes elements that are drawn outside of the viewbox (disabled by default)'; + +/** + * Remove elements that are drawn outside of the viewbox. + * + * @author JoshyPHP + * + * @type {import('./plugins-types').Plugin<'removeOffCanvasPaths'>} + */ +exports.fn = () => { + /** + * @type {null | { + * top: number, + * right: number, + * bottom: number, + * left: number, + * width: number, + * height: number + * }} + */ + let viewBoxData = null; + + return { + element: { + enter: (node, parentNode) => { + if (node.name === 'svg' && parentNode.type === 'root') { + let viewBox = ''; + // find viewbox + if (node.attributes.viewBox != null) { + // remove commas and plus signs, normalize and trim whitespace + viewBox = node.attributes.viewBox; + } else if ( + node.attributes.height != null && + node.attributes.width != null + ) { + viewBox = `0 0 ${node.attributes.width} ${node.attributes.height}`; + } + + // parse viewbox + // remove commas and plus signs, normalize and trim whitespace + viewBox = viewBox + .replace(/[,+]|px/g, ' ') + .replace(/\s+/g, ' ') + .replace(/^\s*|\s*$/g, ''); + // ensure that the dimensions are 4 values separated by space + const m = + /^(-?\d*\.?\d+) (-?\d*\.?\d+) (\d*\.?\d+) (\d*\.?\d+)$/.exec( + viewBox + ); + if (m == null) { + return; + } + const left = Number.parseFloat(m[1]); + const top = Number.parseFloat(m[2]); + const width = Number.parseFloat(m[3]); + const height = Number.parseFloat(m[4]); + + // store the viewBox boundaries + viewBoxData = { + left, + top, + right: left + width, + bottom: top + height, + width, + height, + }; + } + + // consider that any item with a transform attribute is visible + if (node.attributes.transform != null) { + return visitSkip; + } + + if ( + node.name === 'path' && + node.attributes.d != null && + viewBoxData != null + ) { + const pathData = parsePathData(node.attributes.d); + + // consider that a M command within the viewBox is visible + let visible = false; + for (const pathDataItem of pathData) { + if (pathDataItem.command === 'M') { + const [x, y] = pathDataItem.args; + if ( + x >= viewBoxData.left && + x <= viewBoxData.right && + y >= viewBoxData.top && + y <= viewBoxData.bottom + ) { + visible = true; + } + } + } + if (visible) { + return; + } + + if (pathData.length === 2) { + // close the path too short for intersects() + pathData.push({ command: 'z', args: [] }); + } + + const { left, top, width, height } = viewBoxData; + /** + * @type {Array<PathDataItem>} + */ + const viewBoxPathData = [ + { command: 'M', args: [left, top] }, + { command: 'h', args: [width] }, + { command: 'v', args: [height] }, + { command: 'H', args: [left] }, + { command: 'z', args: [] }, + ]; + + if (intersects(viewBoxPathData, pathData) === false) { + detachNodeFromParent(node, parentNode); + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeRasterImages.js b/node_modules/svgo/plugins/removeRasterImages.js new file mode 100644 index 00000000..8a363531 --- /dev/null +++ b/node_modules/svgo/plugins/removeRasterImages.js @@ -0,0 +1,31 @@ +'use strict'; + +const { detachNodeFromParent } = require('../lib/xast.js'); + +exports.name = 'removeRasterImages'; +exports.description = 'removes raster images (disabled by default)'; + +/** + * Remove raster images references in <image>. + * + * @see https://bugs.webkit.org/show_bug.cgi?id=63548 + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'removeRasterImages'>} + */ +exports.fn = () => { + return { + element: { + enter: (node, parentNode) => { + if ( + node.name === 'image' && + node.attributes['xlink:href'] != null && + /(\.|image\/)(jpg|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 new file mode 100644 index 00000000..0e42de4c --- /dev/null +++ b/node_modules/svgo/plugins/removeScriptElement.js @@ -0,0 +1,27 @@ +'use strict'; + +const { detachNodeFromParent } = require('../lib/xast.js'); + +exports.name = 'removeScriptElement'; +exports.description = 'removes <script> elements (disabled by default)'; + +/** + * Remove <script>. + * + * https://www.w3.org/TR/SVG11/script.html + * + * @author Patrick Klingemann + * + * @type {import('./plugins-types').Plugin<'removeScriptElement'>} + */ +exports.fn = () => { + return { + element: { + enter: (node, parentNode) => { + if (node.name === 'script') { + detachNodeFromParent(node, parentNode); + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeStyleElement.js b/node_modules/svgo/plugins/removeStyleElement.js new file mode 100644 index 00000000..d869a50a --- /dev/null +++ b/node_modules/svgo/plugins/removeStyleElement.js @@ -0,0 +1,27 @@ +'use strict'; + +const { detachNodeFromParent } = require('../lib/xast.js'); + +exports.name = 'removeStyleElement'; +exports.description = 'removes <style> element (disabled by default)'; + +/** + * Remove <style>. + * + * https://www.w3.org/TR/SVG11/styling.html#StyleElement + * + * @author Betsy Dupuis + * + * @type {import('./plugins-types').Plugin<'removeStyleElement'>} + */ +exports.fn = () => { + return { + element: { + enter: (node, parentNode) => { + if (node.name === 'style') { + detachNodeFromParent(node, parentNode); + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeTitle.js b/node_modules/svgo/plugins/removeTitle.js new file mode 100644 index 00000000..ee136cd5 --- /dev/null +++ b/node_modules/svgo/plugins/removeTitle.js @@ -0,0 +1,27 @@ +'use strict'; + +const { detachNodeFromParent } = require('../lib/xast.js'); + +exports.name = 'removeTitle'; +exports.description = 'removes <title>'; + +/** + * Remove <title>. + * + * https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title + * + * @author Igor Kalashnikov + * + * @type {import('./plugins-types').Plugin<'removeTitle'>} + */ +exports.fn = () => { + return { + element: { + enter: (node, parentNode) => { + if (node.name === 'title') { + detachNodeFromParent(node, parentNode); + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeUnknownsAndDefaults.js b/node_modules/svgo/plugins/removeUnknownsAndDefaults.js new file mode 100644 index 00000000..c67845cd --- /dev/null +++ b/node_modules/svgo/plugins/removeUnknownsAndDefaults.js @@ -0,0 +1,208 @@ +'use strict'; + +const { visitSkip, detachNodeFromParent } = require('../lib/xast.js'); +const { collectStylesheet, computeStyle } = require('../lib/style.js'); +const { + elems, + attrsGroups, + elemsGroups, + attrsGroupsDefaults, + presentationNonInheritableGroupAttrs, +} = require('./_collections'); + +exports.name = 'removeUnknownsAndDefaults'; +exports.description = + 'removes unknown elements content and attributes, removes attrs with default values'; + +// resolve all groups references + +/** + * @type {Map<string, Set<string>>} + */ +const allowedChildrenPerElement = new Map(); +/** + * @type {Map<string, Set<string>>} + */ +const allowedAttributesPerElement = new Map(); +/** + * @type {Map<string, Map<string, string>>} + */ +const attributesDefaultsPerElement = new Map(); + +for (const [name, config] of Object.entries(elems)) { + /** + * @type {Set<string>} + */ + const allowedChildren = new Set(); + if (config.content) { + for (const elementName of config.content) { + allowedChildren.add(elementName); + } + } + if (config.contentGroups) { + for (const contentGroupName of config.contentGroups) { + const elemsGroup = elemsGroups[contentGroupName]; + if (elemsGroup) { + for (const elementName of elemsGroup) { + allowedChildren.add(elementName); + } + } + } + } + /** + * @type {Set<string>} + */ + const allowedAttributes = new Set(); + if (config.attrs) { + for (const attrName of config.attrs) { + allowedAttributes.add(attrName); + } + } + /** + * @type {Map<string, string>} + */ + const attributesDefaults = new Map(); + if (config.defaults) { + for (const [attrName, defaultValue] of Object.entries(config.defaults)) { + attributesDefaults.set(attrName, defaultValue); + } + } + for (const attrsGroupName of config.attrsGroups) { + const attrsGroup = attrsGroups[attrsGroupName]; + if (attrsGroup) { + for (const attrName of attrsGroup) { + allowedAttributes.add(attrName); + } + } + const groupDefaults = attrsGroupsDefaults[attrsGroupName]; + if (groupDefaults) { + for (const [attrName, defaultValue] of Object.entries(groupDefaults)) { + attributesDefaults.set(attrName, defaultValue); + } + } + } + allowedChildrenPerElement.set(name, allowedChildren); + allowedAttributesPerElement.set(name, allowedAttributes); + attributesDefaultsPerElement.set(name, attributesDefaults); +} + +/** + * Remove unknown elements content and attributes, + * remove attributes with default values. + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'removeUnknownsAndDefaults'>} + */ +exports.fn = (root, params) => { + const { + unknownContent = true, + unknownAttrs = true, + defaultAttrs = true, + uselessOverrides = true, + keepDataAttrs = true, + keepAriaAttrs = true, + keepRoleAttr = false, + } = params; + const stylesheet = collectStylesheet(root); + + return { + element: { + enter: (node, parentNode) => { + // skip namespaced elements + if (node.name.includes(':')) { + return; + } + // skip visiting foreignObject subtree + if (node.name === 'foreignObject') { + return visitSkip; + } + + // remove unknown element's content + if (unknownContent && parentNode.type === 'element') { + const allowedChildren = allowedChildrenPerElement.get( + parentNode.name + ); + if (allowedChildren == null || allowedChildren.size === 0) { + // remove unknown elements + if (allowedChildrenPerElement.get(node.name) == null) { + detachNodeFromParent(node, parentNode); + return; + } + } else { + // remove not allowed children + if (allowedChildren.has(node.name) === false) { + detachNodeFromParent(node, parentNode); + return; + } + } + } + + const allowedAttributes = allowedAttributesPerElement.get(node.name); + const attributesDefaults = attributesDefaultsPerElement.get(node.name); + const computedParentStyle = + parentNode.type === 'element' + ? computeStyle(stylesheet, parentNode) + : null; + + // remove element's unknown attrs and attrs with default values + for (const [name, value] of Object.entries(node.attributes)) { + if (keepDataAttrs && name.startsWith('data-')) { + continue; + } + if (keepAriaAttrs && name.startsWith('aria-')) { + continue; + } + if (keepRoleAttr && name === 'role') { + continue; + } + // skip xmlns attribute + if (name === 'xmlns') { + continue; + } + // skip namespaced attributes except xml:* and xlink:* + if (name.includes(':')) { + const [prefix] = name.split(':'); + if (prefix !== 'xml' && prefix !== 'xlink') { + continue; + } + } + + if ( + unknownAttrs && + allowedAttributes && + allowedAttributes.has(name) === false + ) { + delete node.attributes[name]; + } + if ( + defaultAttrs && + node.attributes.id == null && + attributesDefaults && + attributesDefaults.get(name) === value + ) { + // keep defaults if parent has own or inherited style + if ( + computedParentStyle == null || + computedParentStyle[name] == null + ) { + delete node.attributes[name]; + } + } + if (uselessOverrides && node.attributes.id == null) { + const style = + computedParentStyle == null ? null : computedParentStyle[name]; + if ( + presentationNonInheritableGroupAttrs.includes(name) === false && + style != null && + style.type === 'static' && + style.value === value + ) { + delete node.attributes[name]; + } + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeUnusedNS.js b/node_modules/svgo/plugins/removeUnusedNS.js new file mode 100644 index 00000000..ac81eeaf --- /dev/null +++ b/node_modules/svgo/plugins/removeUnusedNS.js @@ -0,0 +1,59 @@ +'use strict'; + +exports.name = 'removeUnusedNS'; +exports.description = 'removes unused namespaces declaration'; + +/** + * Remove unused namespaces declaration from svg element + * which are not used in elements or attributes + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'removeUnusedNS'>} + */ +exports.fn = () => { + /** + * @type {Set<string>} + */ + const unusedNamespaces = new Set(); + return { + element: { + enter: (node, parentNode) => { + // collect all namespaces from svg element + // (such as xmlns:xlink="http://www.w3.org/1999/xlink") + if (node.name === 'svg' && parentNode.type === 'root') { + for (const name of Object.keys(node.attributes)) { + if (name.startsWith('xmlns:')) { + const local = name.slice('xmlns:'.length); + unusedNamespaces.add(local); + } + } + } + if (unusedNamespaces.size !== 0) { + // preserve namespace used in nested elements names + if (node.name.includes(':')) { + const [ns] = node.name.split(':'); + if (unusedNamespaces.has(ns)) { + unusedNamespaces.delete(ns); + } + } + // preserve namespace used in nested elements attributes + for (const name of Object.keys(node.attributes)) { + if (name.includes(':')) { + const [ns] = name.split(':'); + unusedNamespaces.delete(ns); + } + } + } + }, + exit: (node, parentNode) => { + // remove unused namespace attributes from svg element + if (node.name === 'svg' && parentNode.type === 'root') { + for (const name of unusedNamespaces) { + delete node.attributes[`xmlns:${name}`]; + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeUselessDefs.js b/node_modules/svgo/plugins/removeUselessDefs.js new file mode 100644 index 00000000..46461a98 --- /dev/null +++ b/node_modules/svgo/plugins/removeUselessDefs.js @@ -0,0 +1,65 @@ +'use strict'; + +/** + * @typedef {import('../lib/types').XastElement} XastElement + */ + +const { detachNodeFromParent } = require('../lib/xast.js'); +const { elemsGroups } = require('./_collections.js'); + +exports.name = 'removeUselessDefs'; +exports.description = 'removes elements in <defs> without id'; + +/** + * Removes content of defs and properties that aren't rendered directly without ids. + * + * @author Lev Solntsev + * + * @type {import('./plugins-types').Plugin<'removeUselessDefs'>} + */ +exports.fn = () => { + return { + element: { + enter: (node, parentNode) => { + if (node.name === 'defs') { + /** + * @type {Array<XastElement>} + */ + const usefulNodes = []; + collectUsefulNodes(node, usefulNodes); + if (usefulNodes.length === 0) { + detachNodeFromParent(node, parentNode); + } + // TODO remove legacy parentNode in v4 + for (const usefulNode of usefulNodes) { + Object.defineProperty(usefulNode, 'parentNode', { + writable: true, + value: node, + }); + } + node.children = usefulNodes; + } else if ( + elemsGroups.nonRendering.includes(node.name) && + node.attributes.id == null + ) { + detachNodeFromParent(node, parentNode); + } + }, + }, + }; +}; + +/** + * @type {(node: XastElement, usefulNodes: Array<XastElement>) => void} + */ +const collectUsefulNodes = (node, usefulNodes) => { + for (const child of node.children) { + if (child.type === 'element') { + if (child.attributes.id != null || child.name === 'style') { + usefulNodes.push(child); + } else { + collectUsefulNodes(child, usefulNodes); + } + } + } +}; diff --git a/node_modules/svgo/plugins/removeUselessStrokeAndFill.js b/node_modules/svgo/plugins/removeUselessStrokeAndFill.js new file mode 100644 index 00000000..1dff61cc --- /dev/null +++ b/node_modules/svgo/plugins/removeUselessStrokeAndFill.js @@ -0,0 +1,138 @@ +'use strict'; + +const { visit, visitSkip, detachNodeFromParent } = require('../lib/xast.js'); +const { collectStylesheet, computeStyle } = require('../lib/style.js'); +const { elemsGroups } = require('./_collections.js'); + +exports.name = 'removeUselessStrokeAndFill'; +exports.description = 'removes useless stroke and fill attributes'; + +/** + * Remove useless stroke and fill attrs. + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'removeUselessStrokeAndFill'>} + */ +exports.fn = (root, params) => { + const { + stroke: removeStroke = true, + fill: removeFill = true, + removeNone = false, + } = params; + + // style and script elements deoptimise this plugin + let hasStyleOrScript = false; + visit(root, { + element: { + enter: (node) => { + if (node.name === 'style' || node.name === 'script') { + hasStyleOrScript = true; + } + }, + }, + }); + if (hasStyleOrScript) { + return null; + } + + const stylesheet = collectStylesheet(root); + + return { + element: { + enter: (node, parentNode) => { + // id attribute deoptimise the whole subtree + if (node.attributes.id != null) { + return visitSkip; + } + if (elemsGroups.shape.includes(node.name) == false) { + return; + } + const computedStyle = computeStyle(stylesheet, node); + const stroke = computedStyle.stroke; + const strokeOpacity = computedStyle['stroke-opacity']; + const strokeWidth = computedStyle['stroke-width']; + const markerEnd = computedStyle['marker-end']; + const fill = computedStyle.fill; + const fillOpacity = computedStyle['fill-opacity']; + const computedParentStyle = + parentNode.type === 'element' + ? computeStyle(stylesheet, parentNode) + : null; + const parentStroke = + computedParentStyle == null ? null : computedParentStyle.stroke; + + // remove stroke* + if (removeStroke) { + if ( + stroke == null || + (stroke.type === 'static' && stroke.value == 'none') || + (strokeOpacity != null && + strokeOpacity.type === 'static' && + strokeOpacity.value === '0') || + (strokeWidth != null && + strokeWidth.type === 'static' && + strokeWidth.value === '0') + ) { + // stroke-width may affect the size of marker-end + // marker is not visible when stroke-width is 0 + if ( + (strokeWidth != null && + strokeWidth.type === 'static' && + strokeWidth.value === '0') || + markerEnd == null + ) { + for (const name of Object.keys(node.attributes)) { + if (name.startsWith('stroke')) { + delete node.attributes[name]; + } + } + // set explicit none to not inherit from parent + if ( + parentStroke != null && + parentStroke.type === 'static' && + parentStroke.value !== 'none' + ) { + node.attributes.stroke = 'none'; + } + } + } + } + + // remove fill* + if (removeFill) { + if ( + (fill != null && fill.type === 'static' && fill.value === 'none') || + (fillOpacity != null && + fillOpacity.type === 'static' && + fillOpacity.value === '0') + ) { + for (const name of Object.keys(node.attributes)) { + if (name.startsWith('fill-')) { + delete node.attributes[name]; + } + } + if ( + fill == null || + (fill.type === 'static' && fill.value !== 'none') + ) { + node.attributes.fill = 'none'; + } + } + } + + if (removeNone) { + if ( + (stroke == null || node.attributes.stroke === 'none') && + ((fill != null && + fill.type === 'static' && + fill.value === 'none') || + node.attributes.fill === 'none') + ) { + detachNodeFromParent(node, parentNode); + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeViewBox.js b/node_modules/svgo/plugins/removeViewBox.js new file mode 100644 index 00000000..5618d8bd --- /dev/null +++ b/node_modules/svgo/plugins/removeViewBox.js @@ -0,0 +1,49 @@ +'use strict'; + +exports.name = 'removeViewBox'; +exports.description = 'removes viewBox attribute when possible'; + +const viewBoxElems = ['svg', 'pattern', 'symbol']; + +/** + * Remove viewBox attr which coincides with a width/height box. + * + * @see https://www.w3.org/TR/SVG11/coords.html#ViewBoxAttribute + * + * @example + * <svg width="100" height="50" viewBox="0 0 100 50"> + * ⬇ + * <svg width="100" height="50"> + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'removeViewBox'>} + */ +exports.fn = () => { + return { + element: { + enter: (node, parentNode) => { + if ( + viewBoxElems.includes(node.name) && + node.attributes.viewBox != null && + node.attributes.width != null && + node.attributes.height != null + ) { + // TODO remove width/height for such case instead + if (node.name === 'svg' && parentNode.type !== 'root') { + return; + } + const nums = node.attributes.viewBox.split(/[ ,]+/g); + if ( + nums[0] === '0' && + nums[1] === '0' && + node.attributes.width.replace(/px$/, '') === nums[2] && // could use parseFloat too + node.attributes.height.replace(/px$/, '') === nums[3] + ) { + delete node.attributes.viewBox; + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeXMLNS.js b/node_modules/svgo/plugins/removeXMLNS.js new file mode 100644 index 00000000..6312629d --- /dev/null +++ b/node_modules/svgo/plugins/removeXMLNS.js @@ -0,0 +1,30 @@ +'use strict'; + +exports.name = 'removeXMLNS'; +exports.description = + 'removes xmlns attribute (for inline svg, disabled by default)'; + +/** + * Remove the xmlns attribute when present. + * + * @example + * <svg viewBox="0 0 100 50" xmlns="http://www.w3.org/2000/svg"> + * ↓ + * <svg viewBox="0 0 100 50"> + * + * @author Ricardo Tomasi + * + * @type {import('./plugins-types').Plugin<'removeXMLNS'>} + */ +exports.fn = () => { + return { + element: { + enter: (node) => { + if (node.name === 'svg') { + delete node.attributes.xmlns; + delete node.attributes['xmlns:xlink']; + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/removeXMLProcInst.js b/node_modules/svgo/plugins/removeXMLProcInst.js new file mode 100644 index 00000000..73ec2335 --- /dev/null +++ b/node_modules/svgo/plugins/removeXMLProcInst.js @@ -0,0 +1,28 @@ +'use strict'; + +const { detachNodeFromParent } = require('../lib/xast.js'); + +exports.name = 'removeXMLProcInst'; +exports.description = 'removes XML processing instructions'; + +/** + * Remove XML Processing Instruction. + * + * @example + * <?xml version="1.0" encoding="utf-8"?> + * + * @author Kir Belevich + * + * @type {import('./plugins-types').Plugin<'removeXMLProcInst'>} + */ +exports.fn = () => { + return { + instruction: { + enter: (node, parentNode) => { + if (node.name === 'xml') { + detachNodeFromParent(node, parentNode); + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/reusePaths.js b/node_modules/svgo/plugins/reusePaths.js new file mode 100644 index 00000000..4d91d6f9 --- /dev/null +++ b/node_modules/svgo/plugins/reusePaths.js @@ -0,0 +1,111 @@ +'use strict'; + +/** + * @typedef {import('../lib/types').XastElement} XastElement + * @typedef {import('../lib/types').XastParent} XastParent + * @typedef {import('../lib/types').XastNode} XastNode + */ + +exports.name = 'reusePaths'; +exports.description = + 'Finds <path> elements with the same d, fill, and ' + + 'stroke, and converts them to <use> elements ' + + 'referencing a single <path> def.'; + +/** + * Finds <path> elements with the same d, fill, and stroke, and converts them to + * <use> elements referencing a single <path> def. + * + * @author Jacob Howcroft + * + * @type {import('./plugins-types').Plugin<'reusePaths'>} + */ +exports.fn = () => { + /** + * @type {Map<string, Array<XastElement>>} + */ + const paths = new Map(); + + return { + element: { + enter: (node) => { + if (node.name === 'path' && node.attributes.d != null) { + const d = node.attributes.d; + const fill = node.attributes.fill || ''; + const stroke = node.attributes.stroke || ''; + const key = d + ';s:' + stroke + ';f:' + fill; + let list = paths.get(key); + if (list == null) { + list = []; + paths.set(key, list); + } + list.push(node); + } + }, + + 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 index = 0; + for (const list of paths.values()) { + if (list.length > 1) { + // add reusable path to defs + /** + * @type {XastElement} + */ + const reusablePath = { + type: 'element', + name: 'path', + attributes: { ...list[0].attributes }, + children: [], + }; + delete reusablePath.attributes.transform; + let id; + if (reusablePath.attributes.id == null) { + id = 'reuse-' + index; + index += 1; + reusablePath.attributes.id = id; + } else { + id = reusablePath.attributes.id; + delete list[0].attributes.id; + } + // TODO remove legacy parentNode in v4 + Object.defineProperty(reusablePath, 'parentNode', { + writable: true, + value: defsTag, + }); + defsTag.children.push(reusablePath); + // convert paths to <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.length !== 0) { + if (node.attributes['xmlns:xlink'] == null) { + node.attributes['xmlns:xlink'] = 'http://www.w3.org/1999/xlink'; + } + node.children.unshift(defsTag); + } + } + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/sortAttrs.js b/node_modules/svgo/plugins/sortAttrs.js new file mode 100644 index 00000000..6347f65e --- /dev/null +++ b/node_modules/svgo/plugins/sortAttrs.js @@ -0,0 +1,108 @@ +'use strict'; + +exports.name = 'sortAttrs'; +exports.description = 'Sort element attributes for better compression'; + +/** + * Sort element attributes for better compression + * + * @author Nikolay Frantsev + * + * @type {import('./plugins-types').Plugin<'sortAttrs'>} + */ +exports.fn = (_root, params) => { + const { + order = [ + 'id', + 'width', + 'height', + 'x', + 'x1', + 'x2', + 'y', + 'y1', + 'y2', + 'cx', + 'cy', + 'r', + 'fill', + 'stroke', + 'marker', + 'd', + 'points', + ], + xmlnsOrder = 'front', + } = params; + + /** + * @type {(name: string) => number} + */ + const getNsPriority = (name) => { + if (xmlnsOrder === 'front') { + // put xmlns first + if (name === 'xmlns') { + return 3; + } + // xmlns:* attributes second + if (name.startsWith('xmlns:')) { + return 2; + } + } + // other namespaces after and sort them alphabetically + if (name.includes(':')) { + return 1; + } + // other attributes + return 0; + }; + + /** + * @type {(a: [string, string], b: [string, string]) => number} + */ + const compareAttrs = ([aName], [bName]) => { + // sort namespaces + const aPriority = getNsPriority(aName); + const bPriority = getNsPriority(bName); + const priorityNs = bPriority - aPriority; + if (priorityNs !== 0) { + return priorityNs; + } + // extract the first part from attributes + // for example "fill" from "fill" and "fill-opacity" + const [aPart] = aName.split('-'); + const [bPart] = bName.split('-'); + // rely on alphabetical sort when the first part is the same + if (aPart !== bPart) { + const aInOrderFlag = order.includes(aPart) ? 1 : 0; + const bInOrderFlag = order.includes(bPart) ? 1 : 0; + // sort by position in order param + if (aInOrderFlag === 1 && bInOrderFlag === 1) { + return order.indexOf(aPart) - order.indexOf(bPart); + } + // put attributes from order param before others + const priorityOrder = bInOrderFlag - aInOrderFlag; + if (priorityOrder !== 0) { + return priorityOrder; + } + } + // sort alphabetically + return aName < bName ? -1 : 1; + }; + + return { + element: { + enter: (node) => { + const attrs = Object.entries(node.attributes); + attrs.sort(compareAttrs); + /** + * @type {Record<string, string>} + */ + const sortedAttributes = {}; + for (const [name, value] of attrs) { + sortedAttributes[name] = value; + } + node.attributes = sortedAttributes; + }, + }, + }; +}; diff --git a/node_modules/svgo/plugins/sortDefsChildren.js b/node_modules/svgo/plugins/sortDefsChildren.js new file mode 100644 index 00000000..6ae5d421 --- /dev/null +++ b/node_modules/svgo/plugins/sortDefsChildren.js @@ -0,0 +1,58 @@ +'use strict'; + +exports.name = 'sortDefsChildren'; +exports.description = 'Sorts children of <defs> to improve compression'; + +/** + * Sorts children of defs in order to improve compression. + * Sorted first by frequency then by element name length then by element name (to ensure grouping). + * + * @author David Leston + * + * @type {import('./plugins-types').Plugin<'sortDefsChildren'>} + */ +exports.fn = () => { + return { + element: { + enter: (node) => { + if (node.name === 'defs') { + /** + * @type {Map<string, number>} + */ + const frequencies = new Map(); + for (const child of node.children) { + if (child.type === 'element') { + const frequency = frequencies.get(child.name); + if (frequency == null) { + frequencies.set(child.name, 1); + } else { + frequencies.set(child.name, frequency + 1); + } + } + } + node.children.sort((a, b) => { + if (a.type !== 'element' || b.type !== 'element') { + return 0; + } + const aFrequency = frequencies.get(a.name); + const bFrequency = frequencies.get(b.name); + if (aFrequency != null && bFrequency != null) { + const frequencyComparison = bFrequency - aFrequency; + if (frequencyComparison !== 0) { + return frequencyComparison; + } + } + const lengthComparison = b.name.length - a.name.length; + if (lengthComparison !== 0) { + return lengthComparison; + } + if (a.name !== b.name) { + return a.name > b.name ? -1 : 1; + } + return 0; + }); + } + }, + }, + }; +}; |
