Comparing version 3.0.2 to 3.0.3
@@ -15,2 +15,3 @@ 'use strict'; | ||
require('../plugins/convertEllipseToCircle.js'), | ||
require('../plugins/convertOneStopGradients.js'), | ||
require('../plugins/convertPathData.js'), | ||
@@ -17,0 +18,0 @@ require('../plugins/convertShapeToPath.js'), |
'use strict'; | ||
const { removeLeadingZero } = require('./svgo/tools'); | ||
/** | ||
@@ -69,3 +71,3 @@ * @typedef {import('./types').PathDataItem} PathDataItem | ||
/** | ||
* @type {(string: string, cursor: number) => [number, number | null]} | ||
* @type {(string: string, cursor: number) => [number, ?number]} | ||
*/ | ||
@@ -145,3 +147,3 @@ const readNumber = (string, cursor) => { | ||
/** | ||
* @type {null | PathDataCommand} | ||
* @type {?PathDataCommand} | ||
*/ | ||
@@ -254,3 +256,3 @@ let command = null; | ||
// remove zero whole from decimal number | ||
return number.toString().replace(/^0\./, '.').replace(/^-0\./, '-.'); | ||
return removeLeadingZero(number); | ||
}; | ||
@@ -257,0 +259,0 @@ |
@@ -20,3 +20,3 @@ 'use strict'; | ||
* indent: string, | ||
* textContext: null | XastElement, | ||
* textContext: ?XastElement, | ||
* indentLevel: number, | ||
@@ -23,0 +23,0 @@ * }} State |
@@ -18,3 +18,3 @@ 'use strict'; | ||
const { | ||
// @ts-ignore not defined in @types/csso | ||
// @ts-ignore internal api | ||
syntax: { specificity }, | ||
@@ -51,5 +51,3 @@ } = require('csso'); | ||
/** | ||
* @type {StylesheetRule[]} | ||
*/ | ||
/** @type {StylesheetRule[]} */ | ||
const rules = []; | ||
@@ -83,5 +81,3 @@ csstree.walk(ruleNode.prelude, (node) => { | ||
const parseStylesheet = (css, dynamic) => { | ||
/** | ||
* @type {Array<StylesheetRule>} | ||
*/ | ||
/** @type {Array<StylesheetRule>} */ | ||
const rules = []; | ||
@@ -117,5 +113,3 @@ const ast = csstree.parse(css, { | ||
const parseStyleDeclarations = (css) => { | ||
/** | ||
* @type {Array<StylesheetDeclaration>} | ||
*/ | ||
/** @type {Array<StylesheetDeclaration>} */ | ||
const declarations = []; | ||
@@ -142,5 +136,3 @@ const ast = csstree.parse(css, { | ||
const computeOwnStyle = (stylesheet, node) => { | ||
/** | ||
* @type {ComputedStyles} | ||
*/ | ||
/** @type {ComputedStyles} */ | ||
const computedStyle = {}; | ||
@@ -205,6 +197,8 @@ const importantStyles = new Map(); | ||
/** | ||
* Compares two selector specificities. | ||
* extracted from https://github.com/keeganstreet/specificity/blob/main/specificity.js#L211 | ||
* Compares selector specificities. | ||
* Derived from https://github.com/keeganstreet/specificity/blob/8757133ddd2ed0163f120900047ff0f92760b536/specificity.js#L207 | ||
* | ||
* @type {(a: Specificity, b: Specificity) => number} | ||
* @param {Specificity} a | ||
* @param {Specificity} b | ||
* @returns {number} | ||
*/ | ||
@@ -222,2 +216,3 @@ const compareSpecificity = (a, b) => { | ||
}; | ||
exports.compareSpecificity = compareSpecificity; | ||
@@ -228,9 +223,5 @@ /** | ||
const collectStylesheet = (root) => { | ||
/** | ||
* @type {Array<StylesheetRule>} | ||
*/ | ||
/** @type {Array<StylesheetRule>} */ | ||
const rules = []; | ||
/** | ||
* @type {Map<XastElement, XastParent>} | ||
*/ | ||
/** @type {Map<XastElement, XastParent>} */ | ||
const parents = new Map(); | ||
@@ -237,0 +228,0 @@ visit(root, { |
@@ -68,8 +68,16 @@ 'use strict'; | ||
const plugins = config.plugins || ['preset-default']; | ||
if (Array.isArray(plugins) === false) { | ||
if (!Array.isArray(plugins)) { | ||
throw Error( | ||
"Invalid plugins list. Provided 'plugins' in config should be an array." | ||
'malformed config, `plugins` property must be an array.\nSee more info here: https://github.com/svg/svgo#configuration' | ||
); | ||
} | ||
const resolvedPlugins = plugins.map(resolvePluginConfig); | ||
const resolvedPlugins = plugins | ||
.filter((plugin) => plugin != null) | ||
.map(resolvePluginConfig); | ||
if (resolvedPlugins.length < plugins.length) { | ||
console.warn( | ||
'Warning: plugins list includes null or undefined elements, these will be ignored.' | ||
); | ||
} | ||
const globalOverrides = {}; | ||
@@ -76,0 +84,0 @@ if (config.floatPrecision != null) { |
@@ -17,3 +17,3 @@ 'use strict'; | ||
for (const plugin of plugins) { | ||
const override = overrides == null ? null : overrides[plugin.name]; | ||
const override = overrides?.[plugin.name]; | ||
if (override === false) { | ||
@@ -20,0 +20,0 @@ continue; |
'use strict'; | ||
/** | ||
* @typedef {import('../../lib/types').XastElement} XastElement | ||
* @typedef {import('../types').PathDataCommand} PathDataCommand | ||
@@ -8,2 +9,4 @@ * @typedef {import('../types').DataUri} DataUri | ||
const { attrsGroups } = require('../../plugins/_collections'); | ||
/** | ||
@@ -140,1 +143,38 @@ * Encode plain SVG data string into Data URI string. | ||
exports.removeLeadingZero = removeLeadingZero; | ||
/** | ||
* If the current node contains any scripts. This does not check parents or | ||
* children of the node, only the properties and attributes of the node itself. | ||
* | ||
* @param {XastElement} node Current node to check against. | ||
* @returns {boolean} If the current node contains scripts. | ||
*/ | ||
const hasScripts = (node) => { | ||
if (node.name === 'script' && node.children.length !== 0) { | ||
return true; | ||
} | ||
if (node.name === 'a') { | ||
const hasJsLinks = Object.entries(node.attributes).some( | ||
([attrKey, attrValue]) => | ||
(attrKey === 'href' || attrKey.endsWith(':href')) && | ||
attrValue != null && | ||
attrValue.trimStart().startsWith('javascript:') | ||
); | ||
if (hasJsLinks) { | ||
return true; | ||
} | ||
} | ||
const eventAttrs = [ | ||
...attrsGroups.animationEvent, | ||
...attrsGroups.documentEvent, | ||
...attrsGroups.documentElementEvent, | ||
...attrsGroups.globalEvent, | ||
...attrsGroups.graphicalEvent, | ||
]; | ||
return eventAttrs.some((attr) => node.attributes[attr] != null); | ||
}; | ||
exports.hasScripts = hasScripts; |
@@ -27,3 +27,3 @@ 'use strict'; | ||
/** | ||
* @type {(node: XastNode, selector: string) => null | XastChild} | ||
* @type {(node: XastNode, selector: string) => ?XastChild} | ||
*/ | ||
@@ -30,0 +30,0 @@ const querySelector = (node, selector) => { |
{ | ||
"packageManager": "yarn@2.4.3", | ||
"name": "svgo", | ||
"version": "3.0.2", | ||
"version": "3.0.3", | ||
"description": "Nodejs-based tool for optimizing SVG vector graphics files", | ||
@@ -13,3 +13,3 @@ "license": "MIT", | ||
], | ||
"homepage": "https://github.com/svg/svgo", | ||
"homepage": "https://svgo.dev", | ||
"bugs": { | ||
@@ -38,2 +38,7 @@ "url": "https://github.com/svg/svgo/issues" | ||
"url": "https://github.com/TrySound" | ||
}, | ||
{ | ||
"name": "Seth Falco", | ||
"email": "seth@falco.fun", | ||
"url": "https://falco.fun/" | ||
} | ||
@@ -69,3 +74,4 @@ ], | ||
"test-regression": "node ./test/regression-extract.js && NO_DIFF=1 node ./test/regression.js", | ||
"prepublishOnly": "rm -rf dist && rollup -c" | ||
"prepublishOnly": "rm -rf dist && rollup -c", | ||
"qa": "yarn lint && yarn typecheck && yarn test && yarn test-browser && yarn test-regression" | ||
}, | ||
@@ -77,3 +83,3 @@ "prettier": { | ||
"parserOptions": { | ||
"ecmaVersion": "2021" | ||
"ecmaVersion": 2021 | ||
}, | ||
@@ -116,3 +122,3 @@ "env": { | ||
"css-tree": "^2.2.1", | ||
"csso": "^5.0.5", | ||
"csso": "5.0.5", | ||
"picocolors": "^1.0.0" | ||
@@ -124,8 +130,8 @@ }, | ||
"@types/css-tree": "^2.0.0", | ||
"@types/csso": "^5.0.0", | ||
"@types/jest": "^29.1.1", | ||
"@types/csso": "~5.0.3", | ||
"@types/jest": "^29.5.5", | ||
"del": "^6.0.0", | ||
"eslint": "^8.24.0", | ||
"jest": "^29.1.2", | ||
"node-fetch": "^2.6.2", | ||
"jest": "^29.5.5", | ||
"node-fetch": "^2.7.0", | ||
"pixelmatch": "^5.2.1", | ||
@@ -137,5 +143,5 @@ "playwright": "^1.14.1", | ||
"rollup-plugin-terser": "^7.0.2", | ||
"tar-stream": "^2.2.0", | ||
"tar-stream": "^3.1.6", | ||
"typescript": "^4.8.4" | ||
} | ||
} | ||
} |
@@ -221,3 +221,2 @@ 'use strict'; | ||
documentEvent: [ | ||
'onunload', | ||
'onabort', | ||
@@ -227,4 +226,64 @@ 'onerror', | ||
'onscroll', | ||
'onunload', | ||
'onzoom', | ||
], | ||
documentElementEvent: ['oncopy', 'oncut', 'onpaste'], | ||
globalEvent: [ | ||
'oncancel', | ||
'oncanplay', | ||
'oncanplaythrough', | ||
'onchange', | ||
'onclick', | ||
'onclose', | ||
'oncuechange', | ||
'ondblclick', | ||
'ondrag', | ||
'ondragend', | ||
'ondragenter', | ||
'ondragleave', | ||
'ondragover', | ||
'ondragstart', | ||
'ondrop', | ||
'ondurationchange', | ||
'onemptied', | ||
'onended', | ||
'onerror', | ||
'onfocus', | ||
'oninput', | ||
'oninvalid', | ||
'onkeydown', | ||
'onkeypress', | ||
'onkeyup', | ||
'onload', | ||
'onloadeddata', | ||
'onloadedmetadata', | ||
'onloadstart', | ||
'onmousedown', | ||
'onmouseenter', | ||
'onmouseleave', | ||
'onmousemove', | ||
'onmouseout', | ||
'onmouseover', | ||
'onmouseup', | ||
'onmousewheel', | ||
'onpause', | ||
'onplay', | ||
'onplaying', | ||
'onprogress', | ||
'onratechange', | ||
'onreset', | ||
'onresize', | ||
'onscroll', | ||
'onseeked', | ||
'onseeking', | ||
'onselect', | ||
'onshow', | ||
'onstalled', | ||
'onsubmit', | ||
'onsuspend', | ||
'ontimeupdate', | ||
'ontoggle', | ||
'onvolumechange', | ||
'onwaiting', | ||
], | ||
filterPrimitive: ['x', 'y', 'width', 'height', 'result'], | ||
@@ -231,0 +290,0 @@ transferFunction: [ |
@@ -25,3 +25,3 @@ 'use strict'; | ||
/** | ||
* @type {null | TransformItem} | ||
* @type {?TransformItem} | ||
*/ | ||
@@ -28,0 +28,0 @@ let current = null; |
@@ -8,2 +8,3 @@ 'use strict'; | ||
const { visitSkip } = require('../lib/xast.js'); | ||
const { hasScripts } = require('../lib/svgo/tools'); | ||
const { referencesProps } = require('./_collections.js'); | ||
@@ -14,3 +15,3 @@ | ||
const regReferencesUrl = /\burl\((["'])?#(.+?)\1\)/; | ||
const regReferencesUrl = /\burl\((["'])?#(.+?)\1\)/g; | ||
const regReferencesHref = /^#(.+?)$/; | ||
@@ -91,3 +92,4 @@ const regReferencesBegin = /(\D+)\./; | ||
* | ||
* @type {(currentId: null | Array<number>) => Array<number>} | ||
* @param {?number[]} currentId | ||
* @returns {number[]} | ||
*/ | ||
@@ -152,3 +154,3 @@ const generateId = (currentId) => { | ||
/** | ||
* @type {Map<string, Array<{element: XastElement, name: string, value: string }>>} | ||
* @type {Map<string, Array<{element: XastElement, name: string }>>} | ||
*/ | ||
@@ -161,7 +163,7 @@ const referencesById = new Map(); | ||
enter: (node) => { | ||
if (force == false) { | ||
// deoptimize if style or script elements are present | ||
if (!force) { | ||
// deoptimize if style or scripts are present | ||
if ( | ||
(node.name === 'style' || node.name === 'script') && | ||
node.children.length !== 0 | ||
(node.name === 'style' && node.children.length !== 0) || | ||
hasScripts(node) | ||
) { | ||
@@ -199,9 +201,9 @@ deoptimized = true; | ||
/** | ||
* @type {null | string} | ||
* @type {string[]} | ||
*/ | ||
let id = null; | ||
let ids = []; | ||
if (referencesProps.includes(name)) { | ||
const match = value.match(regReferencesUrl); | ||
if (match != null) { | ||
id = match[2]; // url() reference | ||
const matches = value.matchAll(regReferencesUrl); | ||
for (const match of matches) { | ||
ids.push(match[2]); // url() reference | ||
} | ||
@@ -212,3 +214,3 @@ } | ||
if (match != null) { | ||
id = match[1]; // href reference | ||
ids.push(match[1]); // href reference | ||
} | ||
@@ -219,6 +221,6 @@ } | ||
if (match != null) { | ||
id = match[1]; // href reference | ||
ids.push(match[1]); // href reference | ||
} | ||
} | ||
if (id != null) { | ||
for (const id of ids) { | ||
let refs = referencesById.get(id); | ||
@@ -229,3 +231,3 @@ if (refs == null) { | ||
} | ||
refs.push({ element: node, name, value }); | ||
refs.push({ element: node, name }); | ||
} | ||
@@ -243,9 +245,8 @@ } | ||
/** | ||
* @type {(id: string) => boolean} | ||
**/ | ||
* @param {string} id | ||
* @returns {boolean} | ||
*/ | ||
const isIdPreserved = (id) => | ||
preserveIds.has(id) || hasStringPrefix(id, preserveIdPrefixes); | ||
/** | ||
* @type {null | Array<number>} | ||
*/ | ||
/** @type {?number[]} */ | ||
let currentId = null; | ||
@@ -257,5 +258,3 @@ for (const [id, refs] of referencesById) { | ||
if (minify && isIdPreserved(id) === false) { | ||
/** | ||
* @type {null | string} | ||
*/ | ||
/** @type {?string} */ | ||
let currentIdString = null; | ||
@@ -265,5 +264,10 @@ do { | ||
currentIdString = getIdString(currentId); | ||
} while (isIdPreserved(currentIdString)); | ||
} while ( | ||
isIdPreserved(currentIdString) || | ||
(referencesById.has(currentIdString) && | ||
nodeById.get(currentIdString) == null) | ||
); | ||
node.attributes.id = currentIdString; | ||
for (const { element, name, value } of refs) { | ||
for (const { element, name } of refs) { | ||
const value = element.attributes[name]; | ||
if (value.includes('#')) { | ||
@@ -270,0 +274,0 @@ // replace id in href and url() |
@@ -955,2 +955,12 @@ 'use strict'; | ||
/** | ||
* Does the same as `Number.prototype.toFixed` but without casting | ||
* the return value to a string. | ||
* @type {(num: number, precision: number) => number} | ||
*/ | ||
function toFixed(num, precision) { | ||
const pow = 10 ** precision; | ||
return Math.round(num * pow) / pow; | ||
} | ||
/** | ||
* Decrease accuracy of floating-point numbers | ||
@@ -964,12 +974,10 @@ * in path data keeping a specified number of decimals. | ||
function strongRound(data) { | ||
for (var i = data.length; i-- > 0; ) { | ||
// @ts-ignore | ||
if (data[i].toFixed(precision) != data[i]) { | ||
// @ts-ignore | ||
var rounded = +data[i].toFixed(precision - 1); | ||
const precisionNum = precision || 0; | ||
for (let i = data.length; i-- > 0; ) { | ||
const fixed = toFixed(data[i], precisionNum); | ||
if (fixed !== data[i]) { | ||
const rounded = toFixed(data[i], precisionNum - 1); | ||
data[i] = | ||
// @ts-ignore | ||
+Math.abs(rounded - data[i]).toFixed(precision + 1) >= error | ||
? // @ts-ignore | ||
+data[i].toFixed(precision) | ||
toFixed(Math.abs(rounded - data[i]), precisionNum + 1) >= error | ||
? fixed | ||
: rounded; | ||
@@ -976,0 +984,0 @@ } |
@@ -338,15 +338,9 @@ 'use strict'; | ||
const js2transform = (transformJS, params) => { | ||
var transformString = ''; | ||
const transformString = transformJS | ||
.map((transform) => { | ||
roundTransform(transform, params); | ||
return `${transform.name}(${cleanupOutData(transform.data, params)})`; | ||
}) | ||
.join(''); | ||
// collect output value string | ||
transformJS.forEach((transform) => { | ||
roundTransform(transform, params); | ||
transformString += | ||
(transformString && ' ') + | ||
transform.name + | ||
'(' + | ||
cleanupOutData(transform.data, params) + | ||
')'; | ||
}); | ||
return transformString; | ||
@@ -353,0 +347,0 @@ }; |
@@ -11,3 +11,3 @@ 'use strict'; | ||
const { | ||
// @ts-ignore not defined in @types/csso | ||
// @ts-ignore internal api | ||
syntax: { specificity }, | ||
@@ -20,2 +20,3 @@ } = require('csso'); | ||
} = require('../lib/xast.js'); | ||
const { compareSpecificity } = require('../lib/style'); | ||
@@ -26,45 +27,6 @@ exports.name = 'inlineStyles'; | ||
/** | ||
* Compares two selector specificities. | ||
* extracted from https://github.com/keeganstreet/specificity/blob/main/specificity.js#L211 | ||
* Merges styles from style nodes into inline styles. | ||
* | ||
* @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 | ||
* | ||
* @type {import('./plugins-types').Plugin<'inlineStyles'>} | ||
* @author strarsis <strarsis@gmail.com> | ||
* | ||
* @type {import('./plugins-types').Plugin<'inlineStyles'>} | ||
*/ | ||
@@ -120,3 +82,3 @@ exports.fn = (root, params) => { | ||
/** | ||
* @type {null | csstree.CssNode} | ||
* @type {?csstree.CssNode} | ||
*/ | ||
@@ -217,5 +179,3 @@ let cssAst = null; | ||
const selectorText = csstree.generate(selector.item.data); | ||
/** | ||
* @type {Array<XastElement>} | ||
*/ | ||
/** @type {Array<XastElement>} */ | ||
const matchedElements = []; | ||
@@ -245,5 +205,3 @@ try { | ||
const styleDeclarationList = csstree.parse( | ||
selectedEl.attributes.style == null | ||
? '' | ||
: selectedEl.attributes.style, | ||
selectedEl.attributes.style ?? '', | ||
{ | ||
@@ -261,3 +219,3 @@ context: 'declarationList', | ||
enter(node, item) { | ||
styleDeclarationItems.set(node.property, item); | ||
styleDeclarationItems.set(node.property.toLowerCase(), item); | ||
}, | ||
@@ -295,4 +253,7 @@ }); | ||
}); | ||
selectedEl.attributes.style = | ||
csstree.generate(styleDeclarationList); | ||
const newStyles = csstree.generate(styleDeclarationList); | ||
if (newStyles.length !== 0) { | ||
selectedEl.attributes.style = newStyles; | ||
} | ||
} | ||
@@ -334,13 +295,9 @@ | ||
); | ||
/** | ||
* csstree v2 changed this type | ||
* @type {csstree.CssNode} | ||
*/ | ||
const firstSubSelector = toAny(selector.node.children.first); | ||
if ( | ||
firstSubSelector != null && | ||
firstSubSelector.type === 'ClassSelector' | ||
) { | ||
classList.delete(firstSubSelector.name); | ||
for (const child of selector.node.children) { | ||
if (child.type === 'ClassSelector') { | ||
classList.delete(child.name); | ||
} | ||
} | ||
if (classList.size === 0) { | ||
@@ -353,9 +310,9 @@ delete selectedEl.attributes.class; | ||
// ID | ||
const firstSubSelector = selector.node.children.first; | ||
if ( | ||
firstSubSelector != null && | ||
firstSubSelector.type === 'IdSelector' | ||
firstSubSelector.type === 'IdSelector' && | ||
selectedEl.attributes.id === firstSubSelector.name | ||
) { | ||
if (selectedEl.attributes.id === firstSubSelector.name) { | ||
delete selectedEl.attributes.id; | ||
} | ||
delete selectedEl.attributes.id; | ||
} | ||
@@ -373,4 +330,3 @@ } | ||
node.prelude.type === 'SelectorList' && | ||
// csstree v2 changed this type | ||
toAny(node.prelude.children.isEmpty) | ||
node.prelude.children.isEmpty | ||
) { | ||
@@ -383,3 +339,3 @@ list.remove(item); | ||
// csstree v2 changed this type | ||
if (toAny(style.cssAst.children.isEmpty)) { | ||
if (style.cssAst.children.isEmpty) { | ||
// remove emtpy style element | ||
@@ -386,0 +342,0 @@ detachNodeFromParent(style.node, style.parentNode); |
@@ -22,3 +22,3 @@ 'use strict'; | ||
/** | ||
* @type {null | XastElement} | ||
* @type {?XastElement} | ||
*/ | ||
@@ -25,0 +25,0 @@ let firstStyleElement = null; |
@@ -5,23 +5,44 @@ 'use strict'; | ||
* @typedef {import('../lib/types').XastElement} XastElement | ||
* @typedef {import('../lib/types').XastParent} XastParent | ||
*/ | ||
const csso = require('csso'); | ||
const { detachNodeFromParent } = require('../lib/xast'); | ||
const { hasScripts } = require('../lib/svgo/tools'); | ||
exports.name = 'minifyStyles'; | ||
exports.description = | ||
'minifies styles and removes unused styles based on usage data'; | ||
exports.description = 'minifies styles and removes unused styles'; | ||
/** | ||
* Minifies styles (<style> element + style attribute) using CSSO | ||
* Minifies styles (<style> element + style attribute) using CSSO. | ||
* | ||
* @author strarsis <strarsis@gmail.com> | ||
* | ||
* @type {import('./plugins-types').Plugin<'minifyStyles'>} | ||
*/ | ||
exports.fn = (_root, { usage, ...params }) => { | ||
/** @type {Map<XastElement, XastParent>} */ | ||
const styleElements = new Map(); | ||
/** @type {Array<XastElement>} */ | ||
const elementsWithStyleAttributes = []; | ||
/** @type {Set<string>} */ | ||
const tagsUsage = new Set(); | ||
/** @type {Set<string>} */ | ||
const idsUsage = new Set(); | ||
/** @type {Set<string>} */ | ||
const classesUsage = new Set(); | ||
let enableTagsUsage = true; | ||
let enableIdsUsage = true; | ||
let enableClassesUsage = true; | ||
// force to use usage data even if it unsafe (document contains <script> or on* attributes) | ||
/** | ||
* Force to use usage data even if it unsafe. For example, the document | ||
* contains scripts or in attributes.. | ||
*/ | ||
let forceUsageDeoptimized = false; | ||
if (typeof usage === 'boolean') { | ||
@@ -37,36 +58,13 @@ enableTagsUsage = usage; | ||
} | ||
/** | ||
* @type {Array<XastElement>} | ||
*/ | ||
const styleElements = []; | ||
/** | ||
* @type {Array<XastElement>} | ||
*/ | ||
const elementsWithStyleAttributes = []; | ||
let deoptimized = false; | ||
/** | ||
* @type {Set<string>} | ||
*/ | ||
const tagsUsage = new Set(); | ||
/** | ||
* @type {Set<string>} | ||
*/ | ||
const idsUsage = new Set(); | ||
/** | ||
* @type {Set<string>} | ||
*/ | ||
const classesUsage = new Set(); | ||
return { | ||
element: { | ||
enter: (node) => { | ||
enter: (node, parentNode) => { | ||
// detect deoptimisations | ||
if (node.name === 'script') { | ||
if (hasScripts(node)) { | ||
deoptimized = true; | ||
} | ||
for (const name of Object.keys(node.attributes)) { | ||
if (name.startsWith('on')) { | ||
deoptimized = true; | ||
} | ||
} | ||
// collect tags, ids and classes usage | ||
@@ -84,3 +82,3 @@ tagsUsage.add(node.name); | ||
if (node.name === 'style' && node.children.length !== 0) { | ||
styleElements.push(node); | ||
styleElements.set(node, parentNode); | ||
} else if (node.attributes.style != null) { | ||
@@ -94,14 +92,12 @@ elementsWithStyleAttributes.push(node); | ||
exit: () => { | ||
/** | ||
* @type {csso.Usage} | ||
*/ | ||
/** @type {csso.Usage} */ | ||
const cssoUsage = {}; | ||
if (deoptimized === false || forceUsageDeoptimized === true) { | ||
if (enableTagsUsage && tagsUsage.size !== 0) { | ||
if (!deoptimized || forceUsageDeoptimized) { | ||
if (enableTagsUsage) { | ||
cssoUsage.tags = Array.from(tagsUsage); | ||
} | ||
if (enableIdsUsage && idsUsage.size !== 0) { | ||
if (enableIdsUsage) { | ||
cssoUsage.ids = Array.from(idsUsage); | ||
} | ||
if (enableClassesUsage && classesUsage.size !== 0) { | ||
if (enableClassesUsage) { | ||
cssoUsage.classes = Array.from(classesUsage); | ||
@@ -111,8 +107,8 @@ } | ||
// minify style elements | ||
for (const node of styleElements) { | ||
for (const [styleNode, styleNodeParent] of styleElements.entries()) { | ||
if ( | ||
node.children[0].type === 'text' || | ||
node.children[0].type === 'cdata' | ||
styleNode.children[0].type === 'text' || | ||
styleNode.children[0].type === 'cdata' | ||
) { | ||
const cssText = node.children[0].value; | ||
const cssText = styleNode.children[0].value; | ||
const minified = csso.minify(cssText, { | ||
@@ -122,10 +118,16 @@ ...params, | ||
}).css; | ||
if (minified.length === 0) { | ||
detachNodeFromParent(styleNode, styleNodeParent); | ||
continue; | ||
} | ||
// preserve cdata if necessary | ||
// TODO split cdata -> text optimisation into separate plugin | ||
if (cssText.indexOf('>') >= 0 || cssText.indexOf('<') >= 0) { | ||
node.children[0].type = 'cdata'; | ||
node.children[0].value = minified; | ||
styleNode.children[0].type = 'cdata'; | ||
styleNode.children[0].value = minified; | ||
} else { | ||
node.children[0].type = 'text'; | ||
node.children[0].value = minified; | ||
styleNode.children[0].type = 'text'; | ||
styleNode.children[0].value = minified; | ||
} | ||
@@ -132,0 +134,0 @@ } |
@@ -76,6 +76,24 @@ import type { | ||
inlineStyles: { | ||
/** | ||
* Inlines selectors that match once only. | ||
* | ||
* @default true | ||
*/ | ||
onlyMatchedOnce?: boolean; | ||
/** | ||
* Clean up matched selectors. Unused selects are left as-is. | ||
* | ||
* @default true | ||
*/ | ||
removeMatchedSelectors?: boolean; | ||
useMqs?: Array<string>; | ||
usePseudos?: Array<string>; | ||
/** | ||
* Media queries to use. An empty string indicates all selectors outside of | ||
* media queries. | ||
*/ | ||
useMqs?: string[]; | ||
/** | ||
* Pseudo-classes and elements to use. An empty string indicates all | ||
* all non-pseudo-classes and elements. | ||
*/ | ||
usePseudos?: string[]; | ||
}; | ||
@@ -93,3 +111,3 @@ mergePaths: { | ||
*/ | ||
restructure?: boolean | undefined; | ||
restructure?: boolean; | ||
/** | ||
@@ -100,3 +118,3 @@ * Enables merging of @media rules with the same media query by splitted by other rules. | ||
*/ | ||
forceMediaMerge?: boolean | undefined; | ||
forceMediaMerge?: boolean; | ||
/** | ||
@@ -109,3 +127,3 @@ * Specify what comments to leave: | ||
*/ | ||
comments?: string | boolean | undefined; | ||
comments?: string | boolean; | ||
/** | ||
@@ -126,3 +144,5 @@ * Advanced optimizations | ||
moveGroupAttrsToElems: void; | ||
removeComments: void; | ||
removeComments: { | ||
preservePatterns: Array<RegExp|string> | false | ||
}; | ||
removeDesc: { | ||
@@ -210,2 +230,3 @@ removeAny?: boolean; | ||
}; | ||
convertOneStopGradients: void; | ||
convertStyleToAttrs: { | ||
@@ -212,0 +233,0 @@ keepImportant?: boolean; |
'use strict'; | ||
/** | ||
* @typedef {import('../lib/types.js').PluginInfo} PluginInfo | ||
* @typedef {import('../lib/types').XastElement} XastElement | ||
*/ | ||
const csstree = require('css-tree'); | ||
@@ -44,19 +49,27 @@ const { referencesProps } = require('./_collections.js'); | ||
/** | ||
* prefix an ID | ||
* @type {(prefix: string, name: string) => string} | ||
* Prefix the given string, unless it already starts with the generated prefix. | ||
* | ||
* @param {(id: string) => string} prefixGenerator Function to generate a prefix. | ||
* @param {string} body An arbitrary string. | ||
* @returns {string} The given string with a prefix prepended to it. | ||
*/ | ||
const prefixId = (prefix, value) => { | ||
if (value.startsWith(prefix)) { | ||
return value; | ||
const prefixId = (prefixGenerator, body) => { | ||
const prefix = prefixGenerator(body); | ||
if (body.startsWith(prefix)) { | ||
return body; | ||
} | ||
return prefix + value; | ||
return prefix + body; | ||
}; | ||
/** | ||
* prefix an #ID | ||
* @type {(prefix: string, name: string) => string | null} | ||
* Insert the prefix in a reference string. A reference string is already | ||
* prefixed with #, so the prefix is inserted after the first character. | ||
* | ||
* @param {(id: string) => string} prefixGenerator Function to generate a prefix. | ||
* @param {string} reference An arbitrary string, should start with "#". | ||
* @returns {?string} The given string with a prefix inserted, or null if the string did not start with "#". | ||
*/ | ||
const prefixReference = (prefix, value) => { | ||
if (value.startsWith('#')) { | ||
return '#' + prefixId(prefix, value.slice(1)); | ||
const prefixReference = (prefixGenerator, reference) => { | ||
if (reference.startsWith('#')) { | ||
return '#' + prefixId(prefixGenerator, reference.slice(1)); | ||
} | ||
@@ -66,5 +79,41 @@ return null; | ||
/** @type {(value: any) => any} */ | ||
const toAny = (value) => value; | ||
/** | ||
* Generates a prefix for the given string. | ||
* | ||
* @param {string} body An arbitrary string. | ||
* @param {XastElement} node XML node that the identifier belongs to. | ||
* @param {PluginInfo} info | ||
* @param {((node: XastElement, info: PluginInfo) => string)|string|boolean|undefined} prefixGenerator Some way of obtaining a prefix. | ||
* @param {string} delim Content to insert between the prefix and original value. | ||
* @param {Map<string, string>} history Map of previously generated prefixes to IDs. | ||
* @returns {string} A generated prefix. | ||
*/ | ||
const generatePrefix = (body, node, info, prefixGenerator, delim, history) => { | ||
if (typeof prefixGenerator === 'function') { | ||
let prefix = history.get(body); | ||
if (prefix != null) { | ||
return prefix; | ||
} | ||
prefix = prefixGenerator(node, info) + delim; | ||
history.set(body, prefix); | ||
return prefix; | ||
} | ||
if (typeof prefixGenerator === 'string') { | ||
return prefixGenerator + delim; | ||
} | ||
if (prefixGenerator === false) { | ||
return ''; | ||
} | ||
if (info.path != null && info.path.length > 0) { | ||
return escapeIdentifierName(getBasename(info.path)) + delim; | ||
} | ||
return 'prefix' + delim; | ||
}; | ||
/** | ||
@@ -78,4 +127,12 @@ * Prefixes identifiers | ||
exports.fn = (_root, params, info) => { | ||
const { delim = '__', prefixIds = true, prefixClassNames = true } = params; | ||
const { | ||
delim = '__', | ||
prefix, | ||
prefixIds = true, | ||
prefixClassNames = true, | ||
} = params; | ||
/** @type {Map<string, string>} */ | ||
const prefixMap = new Map(); | ||
return { | ||
@@ -85,15 +142,7 @@ element: { | ||
/** | ||
* prefix, from file name or option | ||
* @type {string} | ||
* @param {string} id A node identifier or class. | ||
* @returns {string} Given string with a prefix inserted, or null if the string did not start with "#". | ||
*/ | ||
let prefix = 'prefix' + delim; | ||
if (typeof params.prefix === 'function') { | ||
prefix = params.prefix(node, info) + delim; | ||
} else if (typeof params.prefix === 'string') { | ||
prefix = params.prefix + delim; | ||
} else if (params.prefix === false) { | ||
prefix = ''; | ||
} else if (info.path != null && info.path.length > 0) { | ||
prefix = escapeIdentifierName(getBasename(info.path)) + delim; | ||
} | ||
const prefixGenerator = (id) => | ||
generatePrefix(id, node, info, prefix, delim, prefixMap); | ||
@@ -116,3 +165,3 @@ // prefix id/class selectors and url() references in styles | ||
/** | ||
* @type {null | csstree.CssNode} | ||
* @type {?csstree.CssNode} | ||
*/ | ||
@@ -135,3 +184,3 @@ let cssAst = null; | ||
) { | ||
node.name = prefixId(prefix, node.name); | ||
node.name = prefixId(prefixGenerator, node.name); | ||
return; | ||
@@ -141,9 +190,12 @@ } | ||
// csstree v2 changed this type | ||
if (node.type === 'Url' && toAny(node.value).length > 0) { | ||
// @ts-ignore | ||
if (node.type === 'Url' && node.value.length > 0) { | ||
const prefixed = prefixReference( | ||
prefix, | ||
unquote(toAny(node.value)) | ||
prefixGenerator, | ||
// @ts-ignore | ||
unquote(node.value) | ||
); | ||
if (prefixed != null) { | ||
toAny(node).value = prefixed; | ||
// @ts-ignore | ||
node.value = prefixed; | ||
} | ||
@@ -169,3 +221,3 @@ } | ||
) { | ||
node.attributes.id = prefixId(prefix, node.attributes.id); | ||
node.attributes.id = prefixId(prefixGenerator, node.attributes.id); | ||
} | ||
@@ -181,3 +233,3 @@ | ||
.split(/\s+/) | ||
.map((name) => prefixId(prefix, name)) | ||
.map((name) => prefixId(prefixGenerator, name)) | ||
.join(' '); | ||
@@ -193,3 +245,6 @@ } | ||
) { | ||
const prefixed = prefixReference(prefix, node.attributes[name]); | ||
const prefixed = prefixReference( | ||
prefixGenerator, | ||
node.attributes[name] | ||
); | ||
if (prefixed != null) { | ||
@@ -210,3 +265,3 @@ node.attributes[name] = prefixed; | ||
(match, url) => { | ||
const prefixed = prefixReference(prefix, url); | ||
const prefixed = prefixReference(prefixGenerator, url); | ||
if (prefixed == null) { | ||
@@ -230,3 +285,3 @@ return match; | ||
const [id, postfix] = val.split('.'); | ||
return `${prefixId(prefix, id)}.${postfix}`; | ||
return `${prefixId(prefixGenerator, id)}.${postfix}`; | ||
} | ||
@@ -233,0 +288,0 @@ return val; |
@@ -9,2 +9,8 @@ 'use strict'; | ||
/** | ||
* If a comment matches one of the following patterns, it will be | ||
* preserved by default. Particularly for copyright/license information. | ||
*/ | ||
const DEFAULT_PRESERVE_PATTERNS = [/^!/]; | ||
/** | ||
* Remove comments. | ||
@@ -20,9 +26,25 @@ * | ||
*/ | ||
exports.fn = () => { | ||
exports.fn = (_root, params) => { | ||
const { preservePatterns = DEFAULT_PRESERVE_PATTERNS } = params; | ||
return { | ||
comment: { | ||
enter: (node, parentNode) => { | ||
if (node.value.charAt(0) !== '!') { | ||
detachNodeFromParent(node, parentNode); | ||
if (preservePatterns) { | ||
if (!Array.isArray(preservePatterns)) { | ||
throw Error( | ||
`Expected array in removeComments preservePatterns parameter but received ${preservePatterns}` | ||
); | ||
} | ||
const matches = preservePatterns.some((pattern) => { | ||
return new RegExp(pattern).test(node.value); | ||
}); | ||
if (matches) { | ||
return; | ||
} | ||
} | ||
detachNodeFromParent(node, parentNode); | ||
}, | ||
@@ -29,0 +51,0 @@ }, |
@@ -22,3 +22,3 @@ 'use strict'; | ||
exports.fn = (root, params) => { | ||
const { removeAny = true } = params; | ||
const { removeAny = false } = params; | ||
return { | ||
@@ -25,0 +25,0 @@ element: { |
'use strict'; | ||
/** | ||
* @typedef {import('../lib/types').XastElement} XastElement | ||
* @typedef {import('../lib/types').XastParent} XastParent | ||
*/ | ||
const { elemsGroups, referencesProps } = require('./_collections.js'); | ||
const { | ||
@@ -12,2 +18,4 @@ visit, | ||
const nonRendering = elemsGroups.nonRendering; | ||
exports.name = 'removeHiddenElems'; | ||
@@ -54,7 +62,16 @@ exports.description = | ||
/** | ||
* Skip non-rendered nodes initially, and only detach if they have no ID, or | ||
* their ID is not referenced by another node. | ||
* | ||
* @type {Map<XastElement, XastParent>} | ||
*/ | ||
const nonRenderedNodes = new Map(); | ||
visit(root, { | ||
element: { | ||
enter: (node, parentNode) => { | ||
// transparent element inside clipPath still affect clipped elements | ||
if (node.name === 'clipPath') { | ||
// transparent non-rendering elements still apply where referenced | ||
if (nonRendering.includes(node.name)) { | ||
nonRenderedNodes.set(node, parentNode); | ||
return visitSkip; | ||
@@ -310,4 +327,28 @@ } | ||
}, | ||
exit: (node, parentNode) => { | ||
if (node.name !== 'svg' || parentNode.type !== 'root') { | ||
return; | ||
} | ||
for (const [ | ||
nonRenderedNode, | ||
nonRenderedParent, | ||
] of nonRenderedNodes.entries()) { | ||
if (nonRenderedNode.attributes.id == null) { | ||
detachNodeFromParent(node, nonRenderedParent); | ||
continue; | ||
} | ||
const selector = referencesProps | ||
.map((attr) => `[${attr}="url(#${nonRenderedNode.attributes.id})"]`) | ||
.join(','); | ||
const element = querySelector(root, selector); | ||
if (element == null) { | ||
detachNodeFromParent(node, nonRenderedParent); | ||
} | ||
} | ||
}, | ||
}, | ||
}; | ||
}; |
@@ -24,3 +24,3 @@ 'use strict'; | ||
/** | ||
* @type {null | { | ||
* @type {?{ | ||
* top: number, | ||
@@ -27,0 +27,0 @@ * right: number, |
@@ -24,3 +24,3 @@ 'use strict'; | ||
node.attributes['xlink:href'] != null && | ||
/(\.|image\/)(jpg|png|gif)/.test(node.attributes['xlink:href']) | ||
/(\.|image\/)(jpe?g|png|gif)/.test(node.attributes['xlink:href']) | ||
) { | ||
@@ -27,0 +27,0 @@ detachNodeFromParent(node, parentNode); |
'use strict'; | ||
const { detachNodeFromParent } = require('../lib/xast.js'); | ||
const { attrsGroups } = require('./_collections.js'); | ||
exports.name = 'removeScriptElement'; | ||
exports.description = 'removes <script> elements (disabled by default)'; | ||
exports.description = 'removes scripts (disabled by default)'; | ||
/** Union of all event attributes. */ | ||
const eventAttrs = [ | ||
...attrsGroups.animationEvent, | ||
...attrsGroups.documentEvent, | ||
...attrsGroups.documentElementEvent, | ||
...attrsGroups.globalEvent, | ||
...attrsGroups.graphicalEvent, | ||
]; | ||
/** | ||
* Remove <script>. | ||
* Remove scripts. | ||
* | ||
@@ -14,3 +24,2 @@ * https://www.w3.org/TR/SVG11/script.html | ||
* @author Patrick Klingemann | ||
* | ||
* @type {import('./plugins-types').Plugin<'removeScriptElement'>} | ||
@@ -24,6 +33,40 @@ */ | ||
detachNodeFromParent(node, parentNode); | ||
return; | ||
} | ||
for (const attr of eventAttrs) { | ||
if (node.attributes[attr] != null) { | ||
delete node.attributes[attr]; | ||
} | ||
} | ||
}, | ||
exit: (node, parentNode) => { | ||
if (node.name !== 'a') { | ||
return; | ||
} | ||
for (const attr of Object.keys(node.attributes)) { | ||
if (attr === 'href' || attr.endsWith(':href')) { | ||
if ( | ||
node.attributes[attr] == null || | ||
!node.attributes[attr].trimStart().startsWith('javascript:') | ||
) { | ||
continue; | ||
} | ||
const index = parentNode.children.indexOf(node); | ||
parentNode.children.splice(index, 1, ...node.children); | ||
// TODO remove legacy parentNode in v4 | ||
for (const child of node.children) { | ||
Object.defineProperty(child, 'parentNode', { | ||
writable: true, | ||
value: parentNode, | ||
}); | ||
} | ||
} | ||
} | ||
}, | ||
}, | ||
}; | ||
}; |
@@ -185,6 +185,3 @@ 'use strict'; | ||
// keep defaults if parent has own or inherited style | ||
if ( | ||
computedParentStyle == null || | ||
computedParentStyle[name] == null | ||
) { | ||
if (computedParentStyle?.[name] == null) { | ||
delete node.attributes[name]; | ||
@@ -194,4 +191,3 @@ } | ||
if (uselessOverrides && node.attributes.id == null) { | ||
const style = | ||
computedParentStyle == null ? null : computedParentStyle[name]; | ||
const style = computedParentStyle?.[name]; | ||
if ( | ||
@@ -198,0 +194,0 @@ presentationNonInheritableGroupAttrs.includes(name) === false && |
@@ -5,2 +5,3 @@ 'use strict'; | ||
const { collectStylesheet, computeStyle } = require('../lib/style.js'); | ||
const { hasScripts } = require('../lib/svgo/tools.js'); | ||
const { elemsGroups } = require('./_collections.js'); | ||
@@ -30,3 +31,3 @@ | ||
enter: (node) => { | ||
if (node.name === 'style' || node.name === 'script') { | ||
if (node.name === 'style' || hasScripts(node)) { | ||
hasStyleOrScript = true; | ||
@@ -33,0 +34,0 @@ } |
'use strict'; | ||
const { collectStylesheet } = require('../lib/style'); | ||
const { detachNodeFromParent, querySelectorAll } = require('../lib/xast'); | ||
/** | ||
@@ -23,3 +26,5 @@ * @typedef {import('../lib/types').XastElement} XastElement | ||
*/ | ||
exports.fn = () => { | ||
exports.fn = (root) => { | ||
const stylesheet = collectStylesheet(root); | ||
/** | ||
@@ -30,5 +35,21 @@ * @type {Map<string, Array<XastElement>>} | ||
/** | ||
* Reference to the first defs element that is a direct child of the svg | ||
* element if one exists. | ||
* | ||
* @type {XastElement} | ||
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs | ||
*/ | ||
let svgDefs; | ||
/** | ||
* Set of hrefs that reference the id of another node. | ||
* | ||
* @type {Set<string>} | ||
*/ | ||
const hrefs = new Set(); | ||
return { | ||
element: { | ||
enter: (node) => { | ||
enter: (node, parentNode) => { | ||
if (node.name === 'path' && node.attributes.d != null) { | ||
@@ -46,2 +67,21 @@ const d = node.attributes.d; | ||
} | ||
if ( | ||
svgDefs == null && | ||
node.name === 'defs' && | ||
parentNode.type === 'element' && | ||
parentNode.name === 'svg' | ||
) { | ||
svgDefs = node; | ||
} | ||
if (node.name === 'use') { | ||
for (const name of ['href', 'xlink:href']) { | ||
const href = node.attributes[name]; | ||
if (href != null && href.startsWith('#') && href.length > 1) { | ||
hrefs.add(href.slice(1)); | ||
} | ||
} | ||
} | ||
}, | ||
@@ -51,37 +91,46 @@ | ||
if (node.name === 'svg' && parentNode.type === 'root') { | ||
/** | ||
* @type {XastElement} | ||
*/ | ||
const defsTag = { | ||
type: 'element', | ||
name: 'defs', | ||
attributes: {}, | ||
children: [], | ||
}; | ||
// TODO remove legacy parentNode in v4 | ||
Object.defineProperty(defsTag, 'parentNode', { | ||
writable: true, | ||
value: node, | ||
}); | ||
let defsTag = svgDefs; | ||
if (defsTag == null) { | ||
defsTag = { | ||
type: 'element', | ||
name: 'defs', | ||
attributes: {}, | ||
children: [], | ||
}; | ||
// TODO remove legacy parentNode in v4 | ||
Object.defineProperty(defsTag, 'parentNode', { | ||
writable: true, | ||
value: node, | ||
}); | ||
} | ||
let index = 0; | ||
for (const list of paths.values()) { | ||
if (list.length > 1) { | ||
// add reusable path to defs | ||
/** | ||
* @type {XastElement} | ||
*/ | ||
/** @type {XastElement} */ | ||
const reusablePath = { | ||
type: 'element', | ||
name: 'path', | ||
attributes: { ...list[0].attributes }, | ||
attributes: {}, | ||
children: [], | ||
}; | ||
delete reusablePath.attributes.transform; | ||
let id; | ||
if (reusablePath.attributes.id == null) { | ||
id = 'reuse-' + index; | ||
index += 1; | ||
reusablePath.attributes.id = id; | ||
for (const attr of ['fill', 'stroke', 'd']) { | ||
if (list[0].attributes[attr] != null) { | ||
reusablePath.attributes[attr] = list[0].attributes[attr]; | ||
} | ||
} | ||
const originalId = list[0].attributes.id; | ||
if ( | ||
originalId == null || | ||
hrefs.has(originalId) || | ||
stylesheet.rules.some( | ||
(rule) => rule.selector === `#${originalId}` | ||
) | ||
) { | ||
reusablePath.attributes.id = 'reuse-' + index++; | ||
} else { | ||
id = reusablePath.attributes.id; | ||
reusablePath.attributes.id = originalId; | ||
delete list[0].attributes.id; | ||
@@ -97,7 +146,39 @@ } | ||
for (const pathNode of list) { | ||
pathNode.name = 'use'; | ||
pathNode.attributes['xlink:href'] = '#' + id; | ||
delete pathNode.attributes.d; | ||
delete pathNode.attributes.stroke; | ||
delete pathNode.attributes.fill; | ||
if ( | ||
defsTag.children.includes(pathNode) && | ||
pathNode.children.length === 0 | ||
) { | ||
if (Object.keys(pathNode.attributes).length === 0) { | ||
detachNodeFromParent(pathNode, defsTag); | ||
continue; | ||
} | ||
if ( | ||
Object.keys(pathNode.attributes).length === 1 && | ||
pathNode.attributes.id != null | ||
) { | ||
detachNodeFromParent(pathNode, defsTag); | ||
const selector = `[xlink\\:href=#${pathNode.attributes.id}], [href=#${pathNode.attributes.id}]`; | ||
for (const child of querySelectorAll(node, selector)) { | ||
if (child.type !== 'element') { | ||
continue; | ||
} | ||
for (const name of ['href', 'xlink:href']) { | ||
if (child.attributes[name] != null) { | ||
child.attributes[name] = | ||
'#' + reusablePath.attributes.id; | ||
} | ||
} | ||
} | ||
continue; | ||
} | ||
} | ||
pathNode.name = 'use'; | ||
pathNode.attributes['xlink:href'] = | ||
'#' + reusablePath.attributes.id; | ||
} | ||
@@ -110,3 +191,6 @@ } | ||
} | ||
node.children.unshift(defsTag); | ||
if (svgDefs == null) { | ||
node.children.unshift(defsTag); | ||
} | ||
} | ||
@@ -113,0 +197,0 @@ } |
216
README.md
@@ -15,6 +15,8 @@ <div align="center"> | ||
Via npm: | ||
```sh | ||
# Via npm | ||
npm -g install svgo | ||
# Via yarn | ||
``` | ||
Via yarn: | ||
```sh | ||
yarn global add svgo | ||
@@ -25,8 +27,12 @@ ``` | ||
Processing single files: | ||
```sh | ||
# Processing single files: | ||
svgo one.svg two.svg -o one.min.svg two.min.svg | ||
# Processing directory of svg files, recursively using `-f`, `--folder` : | ||
``` | ||
Processing directory of svg files, recursively using `-f`, `--folder`: | ||
```sh | ||
svgo -f ./path/to/folder/with/svg/files -o ./path/to/folder/with/svg/output | ||
# Help for advanced usage | ||
``` | ||
Help for advanced usage: | ||
```sh | ||
svgo --help | ||
@@ -93,39 +99,4 @@ ``` | ||
Default preset includes the following list of plugins: | ||
The default preset includes plugins marked with 'Yes' in the [plugin list](#built-in-plugins) below. | ||
- 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 | ||
- sortDefsChildren | ||
- removeTitle | ||
- removeDesc | ||
### Custom plugin | ||
@@ -177,86 +148,99 @@ | ||
const config = await loadConfig(); | ||
``` | ||
// you can also specify a relative or absolute path and customize the current working directory | ||
You can also specify a relative or absolute path and customize the current working directory. | ||
```js | ||
const config = await loadConfig(configFile, cwd); | ||
``` | ||
## Troubleshooting | ||
### SVG won't scale when CSS is applied on it. | ||
**Observed Problem:** I'm using my SVG files on a website. It looks like the rendered SVG doesn't scale when the dimensions are altered using CSS. | ||
**Possible Solution:** Try disabling `removeViewBox` in the configuration. See [issue #1128](https://github.com/svg/svgo/issues/1128) for details and discussion. | ||
## Built-in plugins | ||
| Plugin | Description | Default | | ||
| ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | ||
| [cleanupAttrs](https://github.com/svg/svgo/blob/main/plugins/cleanupAttrs.js) | cleanup attributes from newlines, trailing, and repeating spaces | `enabled` | | ||
| [mergeStyles](https://github.com/svg/svgo/blob/main/plugins/mergeStyles.js) | merge multiple style elements into one | `enabled` | | ||
| [inlineStyles](https://github.com/svg/svgo/blob/main/plugins/inlineStyles.js) | move and merge styles from `<style>` elements to element `style` attributes | `enabled` | | ||
| [removeDoctype](https://github.com/svg/svgo/blob/main/plugins/removeDoctype.js) | remove `doctype` declaration | `enabled` | | ||
| [removeXMLProcInst](https://github.com/svg/svgo/blob/main/plugins/removeXMLProcInst.js) | remove XML processing instructions | `enabled` | | ||
| [removeComments](https://github.com/svg/svgo/blob/main/plugins/removeComments.js) | remove comments | `enabled` | | ||
| [removeMetadata](https://github.com/svg/svgo/blob/main/plugins/removeMetadata.js) | remove `<metadata>` | `enabled` | | ||
| [removeTitle](https://github.com/svg/svgo/blob/main/plugins/removeTitle.js) | remove `<title>` | `enabled` | | ||
| [removeDesc](https://github.com/svg/svgo/blob/main/plugins/removeDesc.js) | remove `<desc>` | `enabled` | | ||
| [removeUselessDefs](https://github.com/svg/svgo/blob/main/plugins/removeUselessDefs.js) | remove elements of `<defs>` without `id` | `enabled` | | ||
| [removeXMLNS](https://github.com/svg/svgo/blob/main/plugins/removeXMLNS.js) | removes the `xmlns` attribute (for inline SVG) | `disabled` | | ||
| [removeEditorsNSData](https://github.com/svg/svgo/blob/main/plugins/removeEditorsNSData.js) | remove editors namespaces, elements, and attributes | `enabled` | | ||
| [removeEmptyAttrs](https://github.com/svg/svgo/blob/main/plugins/removeEmptyAttrs.js) | remove empty attributes | `enabled` | | ||
| [removeHiddenElems](https://github.com/svg/svgo/blob/main/plugins/removeHiddenElems.js) | remove hidden elements | `enabled` | | ||
| [removeEmptyText](https://github.com/svg/svgo/blob/main/plugins/removeEmptyText.js) | remove empty Text elements | `enabled` | | ||
| [removeEmptyContainers](https://github.com/svg/svgo/blob/main/plugins/removeEmptyContainers.js) | remove empty Container elements | `enabled` | | ||
| [removeViewBox](https://github.com/svg/svgo/blob/main/plugins/removeViewBox.js) | remove `viewBox` attribute when possible | `enabled` | | ||
| [cleanupEnableBackground](https://github.com/svg/svgo/blob/main/plugins/cleanupEnableBackground.js) | remove or cleanup `enable-background` attribute when possible | `enabled` | | ||
| [minifyStyles](https://github.com/svg/svgo/blob/main/plugins/minifyStyles.js) | minify `<style>` elements content with [CSSO](https://github.com/css/csso) | `enabled` | | ||
| [convertStyleToAttrs](https://github.com/svg/svgo/blob/main/plugins/convertStyleToAttrs.js) | convert styles into attributes | `disabled` | | ||
| [convertColors](https://github.com/svg/svgo/blob/main/plugins/convertColors.js) | convert colors (from `rgb()` to `#rrggbb`, from `#rrggbb` to `#rgb`) | `enabled` | | ||
| [convertPathData](https://github.com/svg/svgo/blob/main/plugins/convertPathData.js) | convert Path data to relative or absolute (whichever is shorter), convert one segment to another, trim useless delimiters, smart rounding, and much more | `enabled` | | ||
| [convertTransform](https://github.com/svg/svgo/blob/main/plugins/convertTransform.js) | collapse multiple transforms into one, convert matrices to the short aliases, and much more | `enabled` | | ||
| [removeUnknownsAndDefaults](https://github.com/svg/svgo/blob/main/plugins/removeUnknownsAndDefaults.js) | remove unknown elements content and attributes, remove attributes with default values | `enabled` | | ||
| [removeNonInheritableGroupAttrs](https://github.com/svg/svgo/blob/main/plugins/removeNonInheritableGroupAttrs.js) | remove non-inheritable group's "presentation" attributes | `enabled` | | ||
| [removeUselessStrokeAndFill](https://github.com/svg/svgo/blob/main/plugins/removeUselessStrokeAndFill.js) | remove useless `stroke` and `fill` attributes | `enabled` | | ||
| [removeUnusedNS](https://github.com/svg/svgo/blob/main/plugins/removeUnusedNS.js) | remove unused namespaces declaration | `enabled` | | ||
| [prefixIds](https://github.com/svg/svgo/blob/main/plugins/prefixIds.js) | prefix IDs and classes with the SVG filename or an arbitrary string | `disabled` | | ||
| [cleanupIds](https://github.com/svg/svgo/blob/main/plugins/cleanupIds.js) | remove unused and minify used IDs | `enabled` | | ||
| [cleanupNumericValues](https://github.com/svg/svgo/blob/main/plugins/cleanupNumericValues.js) | round numeric values to the fixed precision, remove default `px` units | `enabled` | | ||
| [cleanupListOfValues](https://github.com/svg/svgo/blob/main/plugins/cleanupListOfValues.js) | round numeric values in attributes that take a list of numbers (like `viewBox` or `enable-background`) | `disabled` | | ||
| [moveElemsAttrsToGroup](https://github.com/svg/svgo/blob/main/plugins/moveElemsAttrsToGroup.js) | move elements' attributes to their enclosing group | `enabled` | | ||
| [moveGroupAttrsToElems](https://github.com/svg/svgo/blob/main/plugins/moveGroupAttrsToElems.js) | move some group attributes to the contained elements | `enabled` | | ||
| [collapseGroups](https://github.com/svg/svgo/blob/main/plugins/collapseGroups.js) | collapse useless groups | `enabled` | | ||
| [removeRasterImages](https://github.com/svg/svgo/blob/main/plugins/removeRasterImages.js) | remove raster images | `disabled` | | ||
| [mergePaths](https://github.com/svg/svgo/blob/main/plugins/mergePaths.js) | merge multiple Paths into one | `enabled` | | ||
| [convertShapeToPath](https://github.com/svg/svgo/blob/main/plugins/convertShapeToPath.js) | convert some basic shapes to `<path>` | `enabled` | | ||
| [convertEllipseToCircle](https://github.com/svg/svgo/blob/main/plugins/convertEllipseToCircle.js) | convert non-eccentric `<ellipse>` to `<circle>` | `enabled` | | ||
| [sortAttrs](https://github.com/svg/svgo/blob/main/plugins/sortAttrs.js) | sort element attributes for epic readability | `enabled` | | ||
| [sortDefsChildren](https://github.com/svg/svgo/blob/main/plugins/sortDefsChildren.js) | sort children of `<defs>` in order to improve compression | `enabled` | | ||
| [removeDimensions](https://github.com/svg/svgo/blob/main/plugins/removeDimensions.js) | remove `width`/`height` and add `viewBox` if it's missing (opposite to removeViewBox, disable it first) | `disabled` | | ||
| [removeAttrs](https://github.com/svg/svgo/blob/main/plugins/removeAttrs.js) | remove attributes by pattern | `disabled` | | ||
| [removeAttributesBySelector](https://github.com/svg/svgo/blob/main/plugins/removeAttributesBySelector.js) | removes attributes of elements that match a CSS selector | `disabled` | | ||
| [removeElementsByAttr](https://github.com/svg/svgo/blob/main/plugins/removeElementsByAttr.js) | remove arbitrary elements by `ID` or `className` | `disabled` | | ||
| [addClassesToSVGElement](https://github.com/svg/svgo/blob/main/plugins/addClassesToSVGElement.js) | add classnames to an outer `<svg>` element | `disabled` | | ||
| [addAttributesToSVGElement](https://github.com/svg/svgo/blob/main/plugins/addAttributesToSVGElement.js) | adds attributes to an outer `<svg>` element | `disabled` | | ||
| [removeOffCanvasPaths](https://github.com/svg/svgo/blob/main/plugins/removeOffCanvasPaths.js) | removes elements that are drawn outside of the viewbox | `disabled` | | ||
| [removeStyleElement](https://github.com/svg/svgo/blob/main/plugins/removeStyleElement.js) | remove `<style>` elements | `disabled` | | ||
| [removeScriptElement](https://github.com/svg/svgo/blob/main/plugins/removeScriptElement.js) | remove `<script>` elements | `disabled` | | ||
| [reusePaths](https://github.com/svg/svgo/blob/main/plugins/reusePaths.js) | Find duplicated <path> elements and replace them with <use> links | `disabled` | | ||
| Plugin | Description | Default | | ||
| ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | ||
| [addAttributesToSVGElement](https://github.com/svg/svgo/blob/main/plugins/addAttributesToSVGElement.js) | adds attributes to an outer `<svg>` element | | | ||
| [addClassesToSVGElement](https://github.com/svg/svgo/blob/main/plugins/addClassesToSVGElement.js) | add classnames to an outer `<svg>` element | | | ||
| [cleanupAttrs](https://github.com/svg/svgo/blob/main/plugins/cleanupAttrs.js) | cleanup attributes from newlines, trailing, and repeating spaces | Yes | | ||
| [cleanupEnableBackground](https://github.com/svg/svgo/blob/main/plugins/cleanupEnableBackground.js) | remove or cleanup `enable-background` attribute when possible | Yes | | ||
| [cleanupIds](https://github.com/svg/svgo/blob/main/plugins/cleanupIds.js) | remove unused and minify used IDs | Yes | | ||
| [cleanupListOfValues](https://github.com/svg/svgo/blob/main/plugins/cleanupListOfValues.js) | round numeric values in attributes that take a list of numbers (like `viewBox` or `enable-background`) | | | ||
| [cleanupNumericValues](https://github.com/svg/svgo/blob/main/plugins/cleanupNumericValues.js) | round numeric values to the fixed precision, remove default `px` units | Yes | | ||
| [collapseGroups](https://github.com/svg/svgo/blob/main/plugins/collapseGroups.js) | collapse useless groups | Yes | | ||
| [convertColors](https://github.com/svg/svgo/blob/main/plugins/convertColors.js) | convert colors (from `rgb()` to `#rrggbb`, from `#rrggbb` to `#rgb`) | Yes | | ||
| [convertEllipseToCircle](https://github.com/svg/svgo/blob/main/plugins/convertEllipseToCircle.js) | convert non-eccentric `<ellipse>` to `<circle>` | Yes | | ||
| [convertOneStopGradients](https://github.com/svg/svgo/blob/main/plugins/convertOneStopGradients.js) | converts one-stop (single color) gradients to a plain color | | | ||
| [convertPathData](https://github.com/svg/svgo/blob/main/plugins/convertPathData.js) | convert Path data to relative or absolute (whichever is shorter), convert one segment to another, trim useless delimiters, smart rounding, and much more | Yes | | ||
| [convertShapeToPath](https://github.com/svg/svgo/blob/main/plugins/convertShapeToPath.js) | convert some basic shapes to `<path>` | Yes | | ||
| [convertStyleToAttrs](https://github.com/svg/svgo/blob/main/plugins/convertStyleToAttrs.js) | convert styles into attributes | | | ||
| [convertTransform](https://github.com/svg/svgo/blob/main/plugins/convertTransform.js) | collapse multiple transforms into one, convert matrices to the short aliases, and much more | Yes | | ||
| [inlineStyles](https://github.com/svg/svgo/blob/main/plugins/inlineStyles.js) | move and merge styles from `<style>` elements to element `style` attributes | Yes | | ||
| [mergePaths](https://github.com/svg/svgo/blob/main/plugins/mergePaths.js) | merge multiple Paths into one | Yes | | ||
| [mergeStyles](https://github.com/svg/svgo/blob/main/plugins/mergeStyles.js) | merge multiple style elements into one | Yes | | ||
| [minifyStyles](https://github.com/svg/svgo/blob/main/plugins/minifyStyles.js) | minify `<style>` elements content with [CSSO](https://github.com/css/csso) | Yes | | ||
| [moveElemsAttrsToGroup](https://github.com/svg/svgo/blob/main/plugins/moveElemsAttrsToGroup.js) | move elements' attributes to their enclosing group | Yes | | ||
| [moveGroupAttrsToElems](https://github.com/svg/svgo/blob/main/plugins/moveGroupAttrsToElems.js) | move some group attributes to the contained elements | Yes | | ||
| [prefixIds](https://github.com/svg/svgo/blob/main/plugins/prefixIds.js) | prefix IDs and classes with the SVG filename or an arbitrary string | | | ||
| [removeAttributesBySelector](https://github.com/svg/svgo/blob/main/plugins/removeAttributesBySelector.js) | removes attributes of elements that match a CSS selector | | | ||
| [removeAttrs](https://github.com/svg/svgo/blob/main/plugins/removeAttrs.js) | remove attributes by pattern | | | ||
| [removeComments](https://github.com/svg/svgo/blob/main/plugins/removeComments.js) | remove comments | Yes | | ||
| [removeDesc](https://github.com/svg/svgo/blob/main/plugins/removeDesc.js) | remove `<desc>` | Yes | | ||
| [removeDimensions](https://github.com/svg/svgo/blob/main/plugins/removeDimensions.js) | remove `width`/`height` and add `viewBox` if it's missing (opposite to removeViewBox, disable it first) | | | ||
| [removeDoctype](https://github.com/svg/svgo/blob/main/plugins/removeDoctype.js) | remove `doctype` declaration | Yes | | ||
| [removeEditorsNSData](https://github.com/svg/svgo/blob/main/plugins/removeEditorsNSData.js) | remove editors namespaces, elements, and attributes | Yes | | ||
| [removeElementsByAttr](https://github.com/svg/svgo/blob/main/plugins/removeElementsByAttr.js) | remove arbitrary elements by `ID` or `className` | | | ||
| [removeEmptyAttrs](https://github.com/svg/svgo/blob/main/plugins/removeEmptyAttrs.js) | remove empty attributes | Yes | | ||
| [removeEmptyContainers](https://github.com/svg/svgo/blob/main/plugins/removeEmptyContainers.js) | remove empty Container elements | Yes | | ||
| [removeEmptyText](https://github.com/svg/svgo/blob/main/plugins/removeEmptyText.js) | remove empty Text elements | Yes | | ||
| [removeHiddenElems](https://github.com/svg/svgo/blob/main/plugins/removeHiddenElems.js) | remove hidden elements | Yes | | ||
| [removeMetadata](https://github.com/svg/svgo/blob/main/plugins/removeMetadata.js) | remove `<metadata>` | Yes | | ||
| [removeNonInheritableGroupAttrs](https://github.com/svg/svgo/blob/main/plugins/removeNonInheritableGroupAttrs.js) | remove non-inheritable group's "presentation" attributes | Yes | | ||
| [removeOffCanvasPaths](https://github.com/svg/svgo/blob/main/plugins/removeOffCanvasPaths.js) | removes elements that are drawn outside of the viewbox | | | ||
| [removeRasterImages](https://github.com/svg/svgo/blob/main/plugins/removeRasterImages.js) | remove raster images | | | ||
| [removeScriptElement](https://github.com/svg/svgo/blob/main/plugins/removeScriptElement.js) | remove scripts | | | ||
| [removeStyleElement](https://github.com/svg/svgo/blob/main/plugins/removeStyleElement.js) | remove `<style>` elements | | | ||
| [removeTitle](https://github.com/svg/svgo/blob/main/plugins/removeTitle.js) | remove `<title>` | Yes | | ||
| [removeUnknownsAndDefaults](https://github.com/svg/svgo/blob/main/plugins/removeUnknownsAndDefaults.js) | remove unknown elements content and attributes, remove attributes with default values | Yes | | ||
| [removeUnusedNS](https://github.com/svg/svgo/blob/main/plugins/removeUnusedNS.js) | remove unused namespaces declaration | Yes | | ||
| [removeUselessDefs](https://github.com/svg/svgo/blob/main/plugins/removeUselessDefs.js) | remove elements of `<defs>` without `id` | Yes | | ||
| [removeUselessStrokeAndFill](https://github.com/svg/svgo/blob/main/plugins/removeUselessStrokeAndFill.js) | remove useless `stroke` and `fill` attributes | Yes | | ||
| [removeViewBox](https://github.com/svg/svgo/blob/main/plugins/removeViewBox.js) | remove `viewBox` attribute when possible | Yes | | ||
| [removeXMLNS](https://github.com/svg/svgo/blob/main/plugins/removeXMLNS.js) | removes the `xmlns` attribute (for inline SVG) | | | ||
| [removeXMLProcInst](https://github.com/svg/svgo/blob/main/plugins/removeXMLProcInst.js) | remove XML processing instructions | Yes | | ||
| [reusePaths](https://github.com/svg/svgo/blob/main/plugins/reusePaths.js) | Find duplicated <path> elements and replace them with <use> links | | | ||
| [sortAttrs](https://github.com/svg/svgo/blob/main/plugins/sortAttrs.js) | sort element attributes for epic readability | Yes | | ||
| [sortDefsChildren](https://github.com/svg/svgo/blob/main/plugins/sortDefsChildren.js) | sort children of `<defs>` in order to improve compression | Yes | | ||
## Other Ways to Use SVGO | ||
## Other ways to use SVGO | ||
- as a web app – [SVGOMG](https://jakearchibald.github.io/svgomg/) | ||
- as a GitHub Action – [SVGO Action](https://github.com/marketplace/actions/svgo-action) | ||
- as a Grunt task – [grunt-svgmin](https://github.com/sindresorhus/grunt-svgmin) | ||
- as a Gulp task – [gulp-svgmin](https://github.com/ben-eb/gulp-svgmin) | ||
- as a Mimosa module – [mimosa-minify-svg](https://github.com/dbashford/mimosa-minify-svg) | ||
- as an OSX Folder Action – [svgo-osx-folder-action](https://github.com/svg/svgo-osx-folder-action) | ||
- as a webpack loader – [image-webpack-loader](https://github.com/tcoopman/image-webpack-loader) | ||
- as a Telegram Bot – [svgo_bot](https://github.com/maksugr/svgo_bot) | ||
- as a PostCSS plugin – [postcss-svgo](https://github.com/ben-eb/postcss-svgo) | ||
- as an Inkscape plugin – [inkscape-svgo](https://github.com/konsumer/inkscape-svgo) | ||
- as a Sketch plugin - [svgo-compressor](https://github.com/BohemianCoding/svgo-compressor) | ||
- as a macOS app - [Image Shrinker](https://image-shrinker.com) | ||
- as a Rollup plugin - [rollup-plugin-svgo](https://github.com/porsager/rollup-plugin-svgo) | ||
- as a VS Code plugin - [vscode-svgo](https://github.com/1000ch/vscode-svgo) | ||
- as a Atom plugin - [atom-svgo](https://github.com/1000ch/atom-svgo) | ||
- as a Sublime plugin - [Sublime-svgo](https://github.com/1000ch/Sublime-svgo) | ||
- as a Figma plugin - [Advanced SVG Export](https://www.figma.com/c/plugin/782713260363070260/Advanced-SVG-Export) | ||
- as a Linux app - [Oh My SVG](https://github.com/sonnyp/OhMySVG) | ||
- as a Browser extension - [SVG Gobbler](https://github.com/rossmoody/svg-gobbler) | ||
- as an API - [Vector Express](https://github.com/smidyo/vectorexpress-api#convertor-svgo) | ||
| Method | Reference | | ||
| ------ | --------- | | ||
| Web app | [SVGOMG](https://jakearchibald.github.io/svgomg/) | | ||
| GitHub Action | [SVGO Action](https://github.com/marketplace/actions/svgo-action) | | ||
| Grunt task | [grunt-svgmin](https://github.com/sindresorhus/grunt-svgmin) | | ||
| Gulp task | [gulp-svgmin](https://github.com/ben-eb/gulp-svgmin) | | ||
| Mimosa module | [mimosa-minify-svg](https://github.com/dbashford/mimosa-minify-svg) | | ||
| OSX Folder Action | [svgo-osx-folder-action](https://github.com/svg/svgo-osx-folder-action) | | ||
| Webpack loader | [image-minimizer-webpack-plugin](https://github.com/webpack-contrib/image-minimizer-webpack-plugin/#optimize-with-svgo) | | ||
| Telegram Bot | [svgo_bot](https://github.com/maksugr/svgo_bot) | | ||
| PostCSS plugin | [postcss-svgo](https://github.com/cssnano/cssnano/tree/master/packages/postcss-svgo) | | ||
| Inkscape plugin | [inkscape-svgo](https://github.com/konsumer/inkscape-svgo) | | ||
| Sketch plugin | [svgo-compressor](https://github.com/BohemianCoding/svgo-compressor) | | ||
| macOS app | [Image Shrinker](https://image-shrinker.com) | | ||
| Rollup plugin | [rollup-plugin-svgo](https://github.com/porsager/rollup-plugin-svgo) | | ||
| VS Code plugin | [vscode-svgo](https://github.com/1000ch/vscode-svgo) | | ||
| Atom plugin | [atom-svgo](https://github.com/1000ch/atom-svgo) | | ||
| Sublime plugin | [Sublime-svgo](https://github.com/1000ch/Sublime-svgo) | | ||
| Figma plugin | [Advanced SVG Export](https://www.figma.com/c/plugin/782713260363070260/Advanced-SVG-Export) | | ||
| Linux app | [Oh My SVG](https://github.com/sonnyp/OhMySVG) | | ||
| Browser extension | [SVG Gobbler](https://github.com/rossmoody/svg-gobbler) | | ||
| API | [Vector Express](https://github.com/smidyo/vectorexpress-api#convertor-svgo) | | ||
## Donators | ||
## Donors | ||
@@ -263,0 +247,0 @@ | [<img src="https://sheetjs.com/sketch128.png" width="80">](https://sheetjs.com/) | [<img src="https://raw.githubusercontent.com/fontello/fontello/8.0.0/fontello-image.svg" width="80">](https://fontello.com/) | |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1031179
77
15110
252
8
Updatedcsso@5.0.5