Socket
Socket
Sign inDemoInstall

svgo

Package Overview
Dependencies
17
Maintainers
3
Versions
103
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 2.5.0 to 2.6.0

lib/svgo/jsAPI.d.ts

23

lib/svgo-node.js
'use strict';
const os = require('os');
const fs = require('fs');

@@ -7,6 +8,9 @@ const path = require('path');

extendDefaultPlugins,
optimize,
optimize: optimizeAgnostic,
createContentItem,
} = require('./svgo.js');
exports.extendDefaultPlugins = extendDefaultPlugins;
exports.createContentItem = createContentItem;
const importConfig = async (configFile) => {

@@ -51,6 +55,17 @@ const config = require(configFile);

};
exports.loadConfig = loadConfig;
exports.loadConfig = loadConfig;
exports.extendDefaultPlugins = extendDefaultPlugins;
const optimize = (input, config) => {
if (typeof config !== 'object') {
throw Error('Config should be an object');
}
return optimizeAgnostic(input, {
...config,
js2svg: {
// platform specific default for end of line
eol: os.EOL === '\r\n' ? 'crlf' : 'lf',
...(config == null ? null : config.js2svg),
},
});
};
exports.optimize = optimize;
exports.createContentItem = createContentItem;

@@ -32,3 +32,8 @@ 'use strict';

info.multipassCount = i;
svgjs = svg2js(input);
// TODO throw this error in v3
try {
svgjs = svg2js(input, config.path);
} catch (error) {
return { error: error.toString(), modernError: error };
}
if (svgjs.error != null) {

@@ -35,0 +40,0 @@ if (config.path != null) {

35

lib/svgo/coa.js

@@ -5,3 +5,3 @@ 'use strict';

const PATH = require('path');
const { green } = require('colorette');
const { green, red } = require('colorette');
const { loadConfig, optimize } = require('../svgo-node.js');

@@ -59,2 +59,7 @@ const pluginsMap = require('../../plugins/plugins.js');

.option(
'--eol <EOL>',
'Line break to use when outputting SVG: lf, crlf. If unspecified, uses platform default.'
)
.option('--final-newline', 'Ensure SVG ends with a line break')
.option(
'-r, --recursive',

@@ -117,2 +122,9 @@ "Use with '--folder'. Optimizes *.svg files in folders recursively."

if (opts.eol != null && opts.eol !== 'lf' && opts.eol !== 'crlf') {
console.error(
"error: option '--eol' must have one of the following values: 'lf' or 'crlf'"
);
process.exit(1);
}
// --show-plugins

@@ -191,2 +203,14 @@ if (opts.showPlugins) {

// --eol
if (opts.eol) {
config.js2svg = config.js2svg || {};
config.js2svg.eol = opts.eol;
}
// --final-newline
if (opts.finalNewline) {
config.js2svg = config.js2svg || {};
config.js2svg.finalNewline = true;
}
// --output

@@ -366,8 +390,5 @@ if (output) {

const result = optimize(data, { ...config, ...info });
if (result.error) {
let message = result.error;
if (result.path != null) {
message += `\nFile: ${result.path}`;
}
throw Error(message);
if (result.modernError) {
console.error(red(result.modernError.toString()));
process.exit(1);
}

@@ -374,0 +395,0 @@ if (config.datauri) {

'use strict';
var EOL = require('os').EOL,
textElems = require('../../plugins/_collections.js').textElems;
const { textElems } = require('../../plugins/_collections.js');

@@ -31,2 +30,4 @@ var defaults = {

useShortTags: true,
eol: 'lf',
finalNewline: false,
};

@@ -68,11 +69,17 @@

if (this.config.eol === 'crlf') {
this.eol = '\r\n';
} else {
this.eol = '\n';
}
if (this.config.pretty) {
this.config.doctypeEnd += EOL;
this.config.procInstEnd += EOL;
this.config.commentEnd += EOL;
this.config.cdataEnd += EOL;
this.config.tagShortEnd += EOL;
this.config.tagOpenEnd += EOL;
this.config.tagCloseEnd += EOL;
this.config.textEnd += EOL;
this.config.doctypeEnd += this.eol;
this.config.procInstEnd += this.eol;
this.config.commentEnd += this.eol;
this.config.cdataEnd += this.eol;
this.config.tagShortEnd += this.eol;
this.config.tagOpenEnd += this.eol;
this.config.tagCloseEnd += this.eol;
this.config.textEnd += this.eol;
}

@@ -123,2 +130,11 @@

if (
this.config.finalNewline &&
this.indentLevel === 0 &&
svg.length > 0 &&
svg[svg.length - 1] !== '\n'
) {
svg += this.eol;
}
return {

@@ -125,0 +141,0 @@ data: svg,

'use strict';
const { selectAll, selectOne, is } = require('css-select');
const { parseName } = require('./tools.js');
const svgoCssSelectAdapter = require('./css-select-adapter');

@@ -9,2 +8,31 @@ const CSSClassList = require('./css-class-list');

/**
* @type {(name: string) => { prefix: string, local: string }}
*/
const parseName = (name) => {
if (name == null) {
return {
prefix: '',
local: '',
};
}
if (name === 'xmlns') {
return {
prefix: 'xmlns',
local: '',
};
}
const chunks = name.split(':');
if (chunks.length === 1) {
return {
prefix: '',
local: chunks[0],
};
}
return {
prefix: chunks[0],
local: chunks[1],
};
};
var cssSelectOpts = {

@@ -11,0 +39,0 @@ xmlMode: true,

@@ -37,3 +37,5 @@ 'use strict';

const visitor = plugin.fn(ast, params, info);
visit(ast, visitor);
if (visitor != null) {
visit(ast, visitor);
}
}

@@ -40,0 +42,0 @@ }

@@ -7,2 +7,51 @@ 'use strict';

class SvgoParserError extends Error {
constructor(message, line, column, source, file) {
super(message);
this.name = 'SvgoParserError';
this.message = `${file || '<input>'}:${line}:${column}: ${message}`;
this.reason = message;
this.line = line;
this.column = column;
this.source = source;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, SvgoParserError);
}
}
toString() {
const lines = this.source.split(/\r?\n/);
const startLine = Math.max(this.line - 3, 0);
const endLine = Math.min(this.line + 2, lines.length);
const lineNumberWidth = String(endLine).length;
const startColumn = Math.max(this.column - 54, 0);
const endColumn = Math.max(this.column + 20, 80);
const code = lines
.slice(startLine, endLine)
.map((line, index) => {
const lineSlice = line.slice(startColumn, endColumn);
let ellipsisPrefix = '';
let ellipsisSuffix = '';
if (startColumn !== 0) {
ellipsisPrefix = startColumn > line.length - 1 ? ' ' : '…';
}
if (endColumn < line.length - 1) {
ellipsisSuffix = '…';
}
const number = startLine + 1 + index;
const gutter = ` ${number.toString().padStart(lineNumberWidth)} | `;
if (number === this.line) {
const gutterSpacing = gutter.replace(/[^|]/g, ' ');
const lineSpacing = (
ellipsisPrefix + line.slice(startColumn, this.column - 1)
).replace(/[^\t]/g, ' ');
const spacing = gutterSpacing + lineSpacing;
return `>${gutter}${ellipsisPrefix}${lineSlice}${ellipsisSuffix}\n ${spacing}^`;
}
return ` ${gutter}${ellipsisPrefix}${lineSlice}${ellipsisSuffix}`;
})
.join('\n');
return `${this.name}: ${this.message}\n\n${code}\n`;
}
}
const entityDeclaration = /<!ENTITY\s+(\S+)\s+(?:'([^']+)'|"([^"]+)")\s*>/g;

@@ -24,3 +73,3 @@

*/
module.exports = function (data) {
module.exports = function (data, from) {
const sax = SAX.parser(config.strict, config);

@@ -120,14 +169,16 @@ const root = new JSAPI({ type: 'root', children: [] });

sax.onerror = function (e) {
e.message = 'Error in parsing SVG: ' + e.message;
if (e.message.indexOf('Unexpected end') < 0) {
throw e;
const error = new SvgoParserError(
e.reason,
e.line + 1,
e.column,
data,
from
);
if (e.message.indexOf('Unexpected end') === -1) {
throw error;
}
};
try {
sax.write(data).close();
return root;
} catch (e) {
return { error: e.message };
}
sax.write(data).close();
return root;
};

@@ -138,31 +138,1 @@ 'use strict';

exports.removeLeadingZero = removeLeadingZero;
/**
* @type {(name: string) => { prefix: string, local: string }}
*/
const parseName = (name) => {
if (name == null) {
return {
prefix: '',
local: '',
};
}
if (name === 'xmlns') {
return {
prefix: 'xmlns',
local: '',
};
}
const chunks = name.split(':');
if (chunks.length === 1) {
return {
prefix: '',
local: chunks[0],
};
}
return {
prefix: chunks[0],
local: chunks[1],
};
};
exports.parseName = parseName;

@@ -74,4 +74,13 @@ type XastDoctype = {

export type Plugin<Params> = (root: XastRoot, params: Params) => null | Visitor;
export type PluginInfo = {
path?: string;
multipassCount: number;
};
export type Plugin<Params> = (
root: XastRoot,
params: Params,
info: PluginInfo
) => null | Visitor;
export type Specificity = [number, number, number, number];

@@ -78,0 +87,0 @@

@@ -58,22 +58,2 @@ 'use strict';

const traverseBreak = Symbol();
exports.traverseBreak = traverseBreak;
/**
* @type {(node: any, fn: any) => any}
*/
const traverse = (node, fn) => {
if (fn(node) === traverseBreak) {
return traverseBreak;
}
if (node.type === 'root' || node.type === 'element') {
for (const child of node.children) {
if (traverse(child, fn) === traverseBreak) {
return traverseBreak;
}
}
}
};
exports.traverse = traverse;
const visitSkip = Symbol();

@@ -80,0 +60,0 @@ exports.visitSkip = visitSkip;

{
"name": "svgo",
"version": "2.5.0",
"version": "2.6.0",
"description": "Nodejs-based tool for optimizing SVG vector graphics files",

@@ -95,3 +95,3 @@ "keywords": [

"dependencies": {
"@trysound/sax": "0.1.1",
"@trysound/sax": "0.2.0",
"colorette": "^1.3.0",

@@ -105,5 +105,5 @@ "commander": "^7.2.0",

"devDependencies": {
"@rollup/plugin-commonjs": "^17.1.0",
"@rollup/plugin-commonjs": "^20.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^11.2.1",
"@rollup/plugin-node-resolve": "^13.0.4",
"@types/css-tree": "^1.0.6",

@@ -114,13 +114,14 @@ "@types/csso": "^4.2.0",

"eslint": "^7.32.0",
"jest": "^27.0.6",
"jest": "^27.1.0",
"mock-stdin": "^1.0.0",
"node-fetch": "^2.6.1",
"pixelmatch": "^5.2.1",
"playwright": "^1.14.0",
"playwright": "^1.14.1",
"pngjs": "^6.0.0",
"prettier": "^2.3.2",
"rollup": "^2.56.2",
"rollup": "^2.56.3",
"rollup-plugin-terser": "^7.0.2",
"strip-ansi": "^6.0.0",
"tar-stream": "^2.2.0",
"typescript": "^4.3.5"
"typescript": "^4.4.2"
},

@@ -127,0 +128,0 @@ "engines": {

'use strict';
const { traverse, traverseBreak } = require('../lib/xast.js');
const { parseName } = require('../lib/svgo/tools.js');
/**
* @typedef {import('../lib/types').XastElement} XastElement
*/
const { visitSkip } = require('../lib/xast.js');
const { referencesProps } = require('./_collections.js');
exports.type = 'visitor';
exports.name = 'cleanupIDs';
exports.type = 'full';
exports.active = true;
exports.description = 'removes unused IDs and minifies used';
exports.params = {
remove: true,
minify: true,
prefix: '',
preserve: [],
preservePrefixes: [],
force: false,
};
const regReferencesUrl = /\burl\(("|')?#(.+?)\1\)/;
const regReferencesHref = /^#(.+?)$/;
const regReferencesBegin = /(\w+)\./;
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;
var referencesProps = new Set(require('./_collections').referencesProps),
regReferencesUrl = /\burl\(("|')?#(.+?)\1\)/,
regReferencesHref = /^#(.+?)$/,
regReferencesBegin = /(\w+)\./,
styleOrScript = ['style', 'script'],
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',
],
maxIDindex = generateIDchars.length - 1;
/**
* Remove unused and minify used IDs
* (only if there are no any <style> or <script>).
* Check if an ID starts with any one of a list of strings.
*
* @param {Object} item current iteration item
* @param {Object} params plugin params
*
* @author Kir Belevich
* @type {(string: string, prefixes: Array<string>) => boolean}
*/
exports.fn = function (root, params) {
var currentID,
currentIDstring,
IDs = new Map(),
referencesIDs = new Map(),
hasStyleOrScript = false,
preserveIDs = new Set(
Array.isArray(params.preserve)
? params.preserve
: params.preserve
? [params.preserve]
: []
),
preserveIDPrefixes = new Set(
Array.isArray(params.preservePrefixes)
? params.preservePrefixes
: params.preservePrefixes
? [params.preservePrefixes]
: []
),
idValuePrefix = '#',
idValuePostfix = '.';
traverse(root, (node) => {
if (hasStyleOrScript === true) {
return traverseBreak;
const hasStringPrefix = (string, prefixes) => {
for (const prefix of prefixes) {
if (string.startsWith(prefix)) {
return true;
}
// quit if <style> or <script> present ('force' param prevents quitting)
if (!params.force) {
if (node.isElem(styleOrScript) && node.children.length !== 0) {
hasStyleOrScript = true;
return;
}
// Don't remove IDs if the whole SVG consists only of defs.
if (node.type === 'element' && 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 traverseBreak;
}
}
}
// …and don't remove any ID if yes
if (node.type === 'element') {
for (const [name, value] of Object.entries(node.attributes)) {
let key;
let match;
// save IDs
if (name === 'id') {
key = value;
if (IDs.has(key)) {
delete node.attributes.id; // remove repeated id
} else {
IDs.set(key, node);
}
} else {
// save references
const { local } = parseName(name);
if (
referencesProps.has(name) &&
(match = value.match(regReferencesUrl))
) {
key = match[2]; // url() reference
} else if (
(local === 'href' && (match = value.match(regReferencesHref))) ||
(name === 'begin' && (match = value.match(regReferencesBegin)))
) {
key = match[1]; // href reference
}
if (key) {
const refs = referencesIDs.get(key) || [];
refs.push({ element: node, name, value });
referencesIDs.set(key, refs);
}
}
}
}
});
if (hasStyleOrScript) {
return root;
}
const idPreserved = (id) =>
preserveIDs.has(id) || idMatchesPrefix(preserveIDPrefixes, id);
for (const [key, refs] of referencesIDs) {
if (IDs.has(key)) {
// replace referenced IDs with the minified ones
if (params.minify && !idPreserved(key)) {
do {
currentIDstring = getIDstring(
(currentID = generateID(currentID)),
params
);
} while (idPreserved(currentIDstring));
IDs.get(key).attributes.id = currentIDstring;
for (const { element, name, value } of refs) {
element.attributes[name] = value.includes(idValuePrefix)
? value.replace(
idValuePrefix + key,
idValuePrefix + currentIDstring
)
: value.replace(
key + idValuePostfix,
currentIDstring + idValuePostfix
);
}
}
// don't remove referenced IDs
IDs.delete(key);
}
}
// remove non-referenced IDs attributes from elements
if (params.remove) {
for (var keyElem of IDs) {
if (!idPreserved(keyElem[0])) {
delete keyElem[1].attributes.id;
}
}
}
return root;
return false;
};
/**
* Check if an ID starts with any one of a list of strings.
*
* @param {Array} of prefix strings
* @param {String} current ID
* @return {Boolean} if currentID starts with one of the strings in prefixArray
*/
function idMatchesPrefix(prefixArray, currentID) {
if (!currentID) return false;
for (var prefix of prefixArray) if (currentID.startsWith(prefix)) return true;
return false;
}
/**
* Generate unique minimal ID.
*
* @param {Array} [currentID] current ID
* @return {Array} generated ID array
* @type {(currentID: null | Array<number>) => Array<number>}
*/
function generateID(currentID) {
if (!currentID) return [0];
currentID[currentID.length - 1]++;
for (var i = currentID.length - 1; i > 0; i--) {
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) {

@@ -267,3 +111,3 @@ currentID[i - 1]++;

return currentID;
}
};

@@ -273,8 +117,183 @@ /**

*
* @param {Array} arr input ID array
* @return {String} output ID string
* @type {(arr: Array<number>, prefix: string) => string}
*/
function getIDstring(arr, params) {
var str = params.prefix;
return str + arr.map((i) => generateIDchars[i]).join('');
}
const getIDstring = (arr, prefix) => {
return prefix + 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('../lib/types').Plugin<{
* remove?: boolean,
* minify?: boolean,
* prefix?: string,
* preserve?: Array<string>,
* preservePrefixes?: Array<string>,
* force?: boolean,
* }>}
*/
exports.fn = (_root, params) => {
const {
remove = true,
minify = true,
prefix = '',
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, prefix);
} 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;
}
}
}
},
},
};
};
'use strict';
/**
* @typedef {import('../lib/types').XastElement} XastElement
*/
const csso = require('csso');
const { traverse } = require('../lib/xast.js');
exports.type = 'visitor';
exports.name = 'minifyStyles';
exports.type = 'full';
exports.active = true;
exports.description =
'minifies styles and removes unused styles based on usage data';
exports.params = {
// ... CSSO options goes here
// additional
usage: {
force: false, // force to use usage data even if it unsafe (document contains <script> or on* attributes)
ids: true,
classes: true,
tags: true,
},
};
/**

@@ -31,124 +19,131 @@ * Minifies styles (<style> element + style attribute) using CSSO

* @author strarsis <strarsis@gmail.com>
*
* @type {import('../lib/types').Plugin<csso.MinifyOptions & Omit<csso.CompressOptions, 'usage'> & {
* usage?: boolean | {
* force?: boolean,
* ids?: boolean,
* classes?: boolean,
* tags?: boolean
* }
* }>}
*/
exports.fn = function (ast, options) {
options = options || {};
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();
var minifyOptionsForStylesheet = cloneObject(options);
var minifyOptionsForAttribute = cloneObject(options);
var elems = findStyleElems(ast);
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);
}
},
},
minifyOptionsForStylesheet.usage = collectUsageData(ast, options);
minifyOptionsForAttribute.usage = null;
elems.forEach(function (elem) {
if (elem.isElem('style')) {
if (
elem.children[0].type === 'text' ||
elem.children[0].type === 'cdata'
) {
const styleCss = elem.children[0].value;
const minified = csso.minify(styleCss, minifyOptionsForStylesheet).css;
// preserve cdata if necessary
// TODO split cdata -> text optimisation into separate plugin
if (styleCss.indexOf('>') >= 0 || styleCss.indexOf('<') >= 0) {
elem.children[0].type = 'cdata';
elem.children[0].value = minified;
} else {
elem.children[0].type = 'text';
elem.children[0].value = minified;
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);
}
}
}
} else {
// style attribute
var elemStyle = elem.attributes.style;
elem.attributes.style = csso.minifyBlock(
elemStyle,
minifyOptionsForAttribute
).css;
}
});
return ast;
// 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;
}
},
},
};
};
function cloneObject(obj) {
return { ...obj };
}
function findStyleElems(ast) {
const nodesWithStyles = [];
traverse(ast, (node) => {
if (node.type === 'element') {
if (node.name === 'style' && node.children.length !== 0) {
nodesWithStyles.push(node);
} else if (node.attributes.style != null) {
nodesWithStyles.push(node);
}
}
});
return nodesWithStyles;
}
function shouldFilter(options, name) {
if ('usage' in options === false) {
return true;
}
if (options.usage && name in options.usage === false) {
return true;
}
return Boolean(options.usage && options.usage[name]);
}
function collectUsageData(ast, options) {
let safe = true;
const usageData = {};
let hasData = false;
const rawData = {
ids: Object.create(null),
classes: Object.create(null),
tags: Object.create(null),
};
traverse(ast, (node) => {
if (node.type === 'element') {
if (node.name === 'script') {
safe = false;
}
rawData.tags[node.name] = true;
if (node.attributes.id != null) {
rawData.ids[node.attributes.id] = true;
}
if (node.attributes.class != null) {
node.attributes.class
.replace(/^\s+|\s+$/g, '')
.split(/\s+/)
.forEach((className) => {
rawData.classes[className] = true;
});
}
if (Object.keys(node.attributes).some((name) => /^on/i.test(name))) {
safe = false;
}
}
});
if (!safe && options.usage && options.usage.force) {
safe = true;
}
for (const [key, data] of Object.entries(rawData)) {
if (shouldFilter(options, key)) {
usageData[key] = Object.keys(data);
hasData = true;
}
}
return safe && hasData ? usageData : null;
}
'use strict';
exports.name = 'prefixIds';
const csstree = require('css-tree');
const { referencesProps } = require('./_collections.js');
exports.type = 'perItem';
/**
* @typedef {import('../lib/types').XastElement} XastElement
* @typedef {import('../lib/types').PluginInfo} PluginInfo
*/
exports.type = 'visitor';
exports.name = 'prefixIds';
exports.active = false;
exports.params = {
delim: '__',
prefixIds: true,
prefixClassNames: true,
};
exports.description = 'prefix IDs';
var csstree = require('css-tree'),
collections = require('./_collections.js'),
referencesProps = collections.referencesProps,
rxId = /^#(.*)$/, // regular expression for matching an ID + extracing its name
addPrefix = null;
const unquote = (string) => {
const first = string.charAt(0);
if (first === "'" || first === '"') {
if (first === string.charAt(string.length - 1)) {
return string.slice(1, -1);
}
/**
* 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 string;
return '';
};
// Escapes a string for being used as ID
var escapeIdentifierName = function (str) {
/**
* escapes a string for being used as ID
* @type {(string: string) => string}
*/
const escapeIdentifierName = (str) => {
return str.replace(/[. ]/g, '_');
};
// Matches an #ID value, captures the ID name
var matchId = function (urlVal) {
var idUrlMatches = urlVal.match(rxId);
if (idUrlMatches === null) {
return false;
}
return idUrlMatches[1];
};
// Matches an url(...) value, captures the URL
var matchUrl = function (val) {
var urlMatches = /url\((.*?)\)/gi.exec(val);
if (urlMatches === null) {
return false;
}
return urlMatches[1];
};
// prefixes an #ID
var prefixId = function (val) {
var idName = matchId(val);
if (!idName) {
return false;
}
return '#' + addPrefix(idName);
};
// prefixes a class attribute value
const addPrefixToClassAttr = (element, name) => {
/**
* @type {(string: string) => string}
*/
const unquote = (string) => {
if (
element.attributes[name] == null ||
element.attributes[name].length === 0
(string.startsWith('"') && string.endsWith('"')) ||
(string.startsWith("'") && string.endsWith("'"))
) {
return;
return string.slice(1, -1);
}
element.attributes[name] = element.attributes[name]
.split(/\s+/)
.map(addPrefix)
.join(' ');
return string;
};
// prefixes an ID attribute value
const addPrefixToIdAttr = (element, name) => {
if (
element.attributes[name] == null ||
element.attributes[name].length === 0
) {
return;
/**
* prefix an ID
* @type {(prefix: string, name: string) => string}
*/
const prefixId = (prefix, value) => {
if (value.startsWith(prefix)) {
return value;
}
element.attributes[name] = addPrefix(element.attributes[name]);
return prefix + value;
};
// prefixes a href attribute value
const addPrefixToHrefAttr = (element, name) => {
if (
element.attributes[name] == null ||
element.attributes[name].length === 0
) {
return;
/**
* prefix an #ID
* @type {(prefix: string, name: string) => string | null}
*/
const prefixReference = (prefix, value) => {
if (value.startsWith('#')) {
return '#' + prefixId(prefix, value.slice(1));
}
const idPrefixed = prefixId(element.attributes[name]);
if (!idPrefixed) {
return;
}
element.attributes[name] = idPrefixed;
return null;
};
// prefixes an URL attribute value
const addPrefixToUrlAttr = (element, name) => {
if (
element.attributes[name] == null ||
element.attributes[name].length === 0
) {
return;
}
// url(...) in value
const urlVal = matchUrl(element.attributes[name]);
if (!urlVal) {
return;
}
const idPrefixed = prefixId(urlVal);
if (!idPrefixed) {
return;
}
element.attributes[name] = 'url(' + idPrefixed + ')';
};
// prefixes begin/end attribute value
const addPrefixToBeginEndAttr = (element, name) => {
if (
element.attributes[name] == null ||
element.attributes[name].length === 0
) {
return;
}
const parts = element.attributes[name].split('; ').map((val) => {
val = val.trim();
if (val.endsWith('.end') || val.endsWith('.start')) {
const [id, postfix] = val.split('.');
let idPrefixed = prefixId(`#${id}`);
if (!idPrefixed) {
return val;
}
idPrefixed = idPrefixed.slice(1);
return `${idPrefixed}.${postfix}`;
} else {
return val;
}
});
element.attributes[name] = parts.join('; ');
};
const getBasename = (path) => {
// extract everything after latest slash or backslash
const matched = path.match(/[/\\]([^/\\]+)$/);
if (matched) {
return matched[1];
}
return '';
};
/**
* Prefixes identifiers
*
* @param {Object} node node
* @param {Object} opts plugin params
* @param {Object} extra plugin extra information
* @author strarsis <strarsis@gmail.com>
*
* @author strarsis <strarsis@gmail.com>
* @type {import('../lib/types').Plugin<{
* prefix?: boolean | string | ((node: XastElement, info: PluginInfo) => string),
* delim?: string,
* prefixIds?: boolean,
* prefixClassNames?: boolean,
* }>}
*/
exports.fn = function (node, opts, extra) {
// skip subsequent passes when multipass is used
if (extra.multipassCount && extra.multipassCount > 0) {
return;
}
exports.fn = (_root, params, info) => {
const { delim = '__', prefixIds = true, prefixClassNames = true } = params;
// prefix, from file name or option
var prefix = 'prefix';
if (opts.prefix) {
if (typeof opts.prefix === 'function') {
prefix = opts.prefix(node, extra);
} else {
prefix = opts.prefix;
}
} else if (opts.prefix === false) {
prefix = false;
} else if (extra && extra.path && extra.path.length > 0) {
var filename = getBasename(extra.path);
prefix = filename;
}
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;
}
// prefixes a normal value
addPrefix = function (name) {
if (prefix === false) {
return escapeIdentifierName(name);
}
return escapeIdentifierName(prefix + opts.delim + name);
};
// prefix id/class selectors and url() references in styles
if (node.name === 'style') {
// skip empty <style/> elements
if (node.children.length === 0) {
return;
}
// <style/> property values
// 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;
}
if (node.type === 'element' && node.name === 'style') {
if (node.children.length === 0) {
// skip empty <style/>s
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
if (
node.type === 'Url' &&
node.value.value &&
node.value.value.length > 0
) {
const prefixed = prefixReference(
prefix,
unquote(node.value.value)
);
if (prefixed != null) {
node.value.value = prefixed;
}
}
});
var cssStr = '';
if (node.children[0].type === 'text' || node.children[0].type === 'cdata') {
cssStr = node.children[0].value;
}
var cssAst = {};
try {
cssAst = csstree.parse(cssStr, {
parseValue: true,
parseCustomProperty: false,
});
} catch (parseError) {
console.warn(
'Warning: Parse error of styles of <style/> element, skipped. Error details: ' +
parseError
);
return;
}
var idPrefixed = '';
csstree.walk(cssAst, function (node) {
// #ID, .class
if (
((opts.prefixIds && node.type === 'IdSelector') ||
(opts.prefixClassNames && node.type === 'ClassSelector')) &&
node.name
) {
node.name = addPrefix(node.name);
return;
}
// url(...) in value
if (
node.type === 'Url' &&
node.value.value &&
node.value.value.length > 0
) {
idPrefixed = prefixId(unquote(node.value.value));
if (!idPrefixed) {
// update styles
if (
node.children[0].type === 'text' ||
node.children[0].type === 'cdata'
) {
node.children[0].value = csstree.generate(cssAst);
}
return;
}
node.value.value = idPrefixed;
}
});
// update <style>s
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);
}
// element attributes
// 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(' ');
}
if (node.type !== 'element') {
return;
}
// 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;
}
}
}
// Nodes
// prefix an URL attribute value
for (const name of referencesProps) {
if (
node.attributes[name] != null &&
node.attributes[name].length !== 0
) {
// extract id reference from url(...) value
const matches = /url\((.*?)\)/gi.exec(node.attributes[name]);
if (matches != null) {
const value = matches[1];
const prefixed = prefixReference(prefix, value);
if (prefixed != null) {
node.attributes[name] = `url(${prefixed})`;
}
}
}
}
if (opts.prefixIds) {
// ID
addPrefixToIdAttr(node, 'id');
}
if (opts.prefixClassNames) {
// Class
addPrefixToClassAttr(node, 'class');
}
// References
// href
addPrefixToHrefAttr(node, 'href');
// (xlink:)href (deprecated, must be still supported)
addPrefixToHrefAttr(node, 'xlink:href');
// (referenceable) properties
for (var referencesProp of referencesProps) {
addPrefixToUrlAttr(node, referencesProp);
}
addPrefixToBeginEndAttr(node, 'begin');
addPrefixToBeginEndAttr(node, 'end');
// 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('; ');
}
}
},
},
};
};
'use strict';
const { parseName } = require('../lib/svgo/tools.js');
const { editorNamespaces } = require('./_collections');
const { detachNodeFromParent } = require('../lib/xast.js');
const { editorNamespaces } = require('./_collections.js');
exports.type = 'visitor';
exports.name = 'removeEditorsNSData';
exports.type = 'perItem';
exports.active = true;
exports.description = 'removes editors namespaces, elements and attributes';
const prefixes = [];
exports.params = {
additionalNamespaces: [],
};
/**

@@ -28,9 +19,9 @@ * Remove editors namespaces, elements and attributes.

*
* @param {Object} item current iteration item
* @param {Object} params plugin params
* @return {Boolean} if false, item will be filtered out
* @author Kir Belevich
*
* @author Kir Belevich
* @type {import('../lib/types').Plugin<{
* additionalNamespaces?: Array<string>
* }>}
*/
exports.fn = function (item, params) {
exports.fn = (_root, params) => {
let namespaces = editorNamespaces;

@@ -40,30 +31,40 @@ if (Array.isArray(params.additionalNamespaces)) {

}
if (item.type === 'element') {
if (item.isElem('svg')) {
for (const [name, value] of Object.entries(item.attributes)) {
const { prefix, local } = parseName(name);
if (prefix === 'xmlns' && namespaces.includes(value)) {
prefixes.push(local);
// <svg xmlns:sodipodi="">
delete item.attributes[name];
/**
* @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];
}
}
}
}
}
// <* sodipodi:*="">
for (const name of Object.keys(item.attributes)) {
const { prefix } = parseName(name);
if (prefixes.includes(prefix)) {
delete item.attributes[name];
}
}
// <sodipodi:*>
const { prefix } = parseName(item.name);
if (prefixes.includes(prefix)) {
return false;
}
}
// 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);
}
}
},
},
};
};
'use strict';
const { traverse } = require('../lib/xast.js');
const { parseName } = require('../lib/svgo/tools.js');
exports.type = 'visitor';
exports.name = 'removeUnusedNS';
exports.type = 'full';
exports.active = true;
exports.description = 'removes unused namespaces declaration';
/**
* Remove unused namespaces declaration.
* Remove unused namespaces declaration from svg element
* which are not used in elements or attributes
*
* @param {Object} item current iteration item
* @return {Boolean} if false, item will be filtered out
* @author Kir Belevich
*
* @author Kir Belevich
* @type {import('../lib/types').Plugin<void>}
*/
exports.fn = function (root) {
let svgElem;
const xmlnsCollection = [];
exports.fn = () => {
/**
* Remove namespace from collection.
*
* @param {String} ns namescape name
* @type {Set<string>}
*/
function removeNSfromCollection(ns) {
const pos = xmlnsCollection.indexOf(ns);
// if found - remove ns from the namespaces collection
if (pos > -1) {
xmlnsCollection.splice(pos, 1);
}
}
traverse(root, (node) => {
if (node.type === 'element') {
if (node.name === 'svg') {
for (const name of Object.keys(node.attributes)) {
const { prefix, local } = parseName(name);
// collect namespaces
if (prefix === 'xmlns' && local) {
xmlnsCollection.push(local);
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 svg element has ns-attr
if (xmlnsCollection.length) {
// save svg element
svgElem = node;
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);
}
}
}
}
if (xmlnsCollection.length) {
const { prefix } = parseName(node.name);
// check node for the ns-attrs
if (prefix) {
removeNSfromCollection(prefix);
},
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}`];
}
}
// check each attr for the ns-attrs
for (const name of Object.keys(node.attributes)) {
const { prefix } = parseName(name);
removeNSfromCollection(prefix);
}
}
}
});
// remove svg element ns-attributes if they are not used even once
if (xmlnsCollection.length) {
for (const name of xmlnsCollection) {
delete svgElem.attributes['xmlns:' + name];
}
}
return root;
},
},
};
};
'use strict';
const { visit, visitSkip, detachNodeFromParent } = require('../lib/xast.js');
const { collectStylesheet, computeStyle } = require('../lib/style.js');
const { elemsGroups } = require('./_collections.js');
exports.type = 'visitor';
exports.name = 'removeUselessStrokeAndFill';
exports.type = 'perItem';
exports.active = true;
exports.description = 'removes useless stroke and fill attributes';
exports.params = {
stroke: true,
fill: true,
removeNone: false,
hasStyleOrScript: false,
};
var shape = require('./_collections').elemsGroups.shape,
styleOrScript = ['style', 'script'];
/**
* Remove useless stroke and fill attrs.
*
* @param {Object} item current iteration item
* @param {Object} params plugin params
* @return {Boolean} if false, item will be filtered out
* @author Kir Belevich
*
* @author Kir Belevich
* @type {import('../lib/types').Plugin<{
* stroke?: boolean,
* fill?: boolean,
* removeNone?: boolean
* }>}
*/
exports.fn = function (item, params) {
if (item.isElem(styleOrScript)) {
params.hasStyleOrScript = true;
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;
}
if (
!params.hasStyleOrScript &&
item.isElem(shape) &&
!item.computedAttr('id')
) {
var stroke = params.stroke && item.computedAttr('stroke'),
fill = params.fill && !item.computedAttr('fill', 'none');
const stylesheet = collectStylesheet(root);
// remove stroke*
if (
params.stroke &&
(!stroke ||
stroke == 'none' ||
item.computedAttr('stroke-opacity', '0') ||
item.computedAttr('stroke-width', '0'))
) {
// stroke-width may affect the size of marker-end
if (
item.computedAttr('stroke-width', '0') === true ||
item.computedAttr('marker-end') == null
) {
var parentStroke = item.parentNode.computedAttr('stroke'),
declineStroke = parentStroke && parentStroke != 'none';
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;
for (const name of Object.keys(item.attributes)) {
if (name.startsWith('stroke')) {
delete item.attributes[name];
// 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';
}
}
}
}
if (declineStroke) {
item.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';
}
}
}
}
}
// remove fill*
if (params.fill && (!fill || item.computedAttr('fill-opacity', '0'))) {
for (const name of Object.keys(item.attributes)) {
if (name.startsWith('fill-')) {
delete item.attributes[name];
if (removeNone) {
if (
(stroke == null || node.attributes.stroke === 'none') &&
((fill != null &&
fill.type === 'static' &&
fill.value === 'none') ||
node.attributes.fill === 'none')
) {
detachNodeFromParent(node, parentNode);
}
}
}
if (fill) {
item.attributes.fill = 'none';
}
}
if (
params.removeNone &&
(!stroke || item.attributes.stroke == 'none') &&
(!fill || item.attributes.fill == 'none')
) {
return false;
}
}
},
},
};
};
'use strict';
const { traverse } = require('../lib/xast.js');
const JSAPI = require('../lib/svgo/jsAPI');
/**
* @typedef {import('../lib/types').XastElement} XastElement
* @typedef {import('../lib/types').XastParent} XastParent
* @typedef {import('../lib/types').XastNode} XastNode
*/
const JSAPI = require('../lib/svgo/jsAPI.js');
exports.type = 'visitor';
exports.name = 'reusePaths';
exports.type = 'full';
exports.active = false;
exports.description =

@@ -22,65 +24,91 @@ 'Finds <path> elements with the same d, fill, and ' +

* @author Jacob Howcroft
*
* @type {import('../lib/types').Plugin<void>}
*/
exports.fn = function (root) {
const seen = new Map();
let count = 0;
const defs = [];
traverse(root, (node) => {
if (
node.type !== 'element' ||
node.name !== 'path' ||
node.attributes.d == null
) {
return;
}
const d = node.attributes.d;
const fill = node.attributes.fill || '';
const stroke = node.attributes.stroke || '';
const key = d + ';s:' + stroke + ';f:' + fill;
const hasSeen = seen.get(key);
if (!hasSeen) {
seen.set(key, { elem: node, reused: false });
return;
}
if (!hasSeen.reused) {
hasSeen.reused = true;
if (hasSeen.elem.attributes.id == null) {
hasSeen.elem.attributes.id = 'reuse-' + count++;
}
defs.push(hasSeen.elem);
}
convertToUse(node, hasSeen.elem.attributes.id);
});
if (defs.length > 0) {
const defsTag = new JSAPI(
{
type: 'element',
name: 'defs',
attributes: {},
children: [],
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);
}
},
root
);
root.children[0].spliceContent(0, 0, defsTag);
for (let def of defs) {
const defClone = def.clone();
delete defClone.attributes.transform;
defsTag.spliceContent(0, 0, defClone);
// Convert the original def to a use so the first usage isn't duplicated.
def = convertToUse(def, defClone.attributes.id);
delete def.attributes.id;
}
}
return root;
exit: (node, parentNode) => {
if (node.name === 'svg' && parentNode.type === 'root') {
/**
* @type {XastElement}
*/
const rawDefs = {
type: 'element',
name: 'defs',
attributes: {},
children: [],
};
/**
* @type {XastElement}
*/
const defsTag = new JSAPI(rawDefs, node);
let index = 0;
for (const list of paths.values()) {
if (list.length > 1) {
// add reusable path to defs
/**
* @type {XastElement}
*/
const rawPath = {
type: 'element',
name: 'path',
attributes: { ...list[0].attributes },
children: [],
};
delete rawPath.attributes.transform;
let id;
if (rawPath.attributes.id == null) {
id = 'reuse-' + index;
index += 1;
rawPath.attributes.id = id;
} else {
id = rawPath.attributes.id;
delete list[0].attributes.id;
}
/**
* @type {XastElement}
*/
const reusablePath = new JSAPI(rawPath, 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);
}
}
},
},
};
};
/** */
function convertToUse(item, href) {
item.renameElem('use');
delete item.attributes.d;
delete item.attributes.stroke;
delete item.attributes.fill;
item.attributes['xlink:href'] = '#' + href;
delete item.pathJS;
return item;
}
'use strict';
const { parseName } = require('../lib/svgo/tools.js');
exports.type = 'visitor';
exports.name = 'sortAttrs';
exports.type = 'perItem';
exports.active = false;
exports.description = 'Sort element attributes for better compression';
exports.description = 'sorts element attributes (disabled by default)';
exports.params = {
order: [
'id',
'width',
'height',
'x',
'x1',
'x2',
'y',
'y1',
'y2',
'cx',
'cy',
'r',
'fill',
'stroke',
'marker',
'd',
'points',
],
};
/**
* Sort element attributes for epic readability.
* Sort element attributes for better compression
*
* @param {Object} item current iteration item
* @param {Object} params plugin params
* @author Nikolay Frantsev
*
* @author Nikolay Frantsev
* @type {import('../lib/types').Plugin<{
* order?: Array<string>
* xmlnsOrder?: 'front' | 'alphabetical'
* }>}
*/
exports.fn = function (item, params) {
const orderlen = params.order.length + 1;
const xmlnsOrder = params.xmlnsOrder || 'front';
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;
if (item.type === 'element') {
const attrs = Object.entries(item.attributes);
attrs.sort(([aName], [bName]) => {
const { prefix: aPrefix } = parseName(aName);
const { prefix: bPrefix } = parseName(bName);
if (aPrefix != bPrefix) {
// xmlns attributes implicitly have the prefix xmlns
if (xmlnsOrder == 'front') {
if (aPrefix === 'xmlns') return -1;
if (bPrefix === 'xmlns') return 1;
}
return aPrefix < bPrefix ? -1 : 1;
/**
* @type {(name: string) => number}
*/
const getNsPriority = (name) => {
if (xmlnsOrder === 'front') {
// put xmlns first
if (name === 'xmlns') {
return 3;
}
let aindex = orderlen;
let bindex = orderlen;
for (let i = 0; i < params.order.length; i++) {
if (aName == params.order[i]) {
aindex = i;
} else if (aName.indexOf(params.order[i] + '-') === 0) {
aindex = i + 0.5;
}
if (bName == params.order[i]) {
bindex = i;
} else if (bName.indexOf(params.order[i] + '-') === 0) {
bindex = i + 0.5;
}
// 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;
};
if (aindex != bindex) {
return aindex - bindex;
/**
* @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);
}
return aName < bName ? -1 : 1;
});
// put attributes from order param before others
const priorityOrder = bInOrderFlag - aInOrderFlag;
if (priorityOrder !== 0) {
return priorityOrder;
}
}
// sort alphabetically
return aName < bName ? -1 : 1;
};
const sorted = {};
for (const [name, value] of attrs) {
sorted[name] = value;
}
item.attributes = sorted;
}
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;
},
},
};
};

Sorry, the diff of this file is too big to display

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc