Socket
Socket
Sign inDemoInstall

svgo

Package Overview
Dependencies
17
Maintainers
3
Versions
99
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 2.6.1 to 2.7.0

lib/parser.js

39

lib/style.js

@@ -6,2 +6,3 @@ 'use strict';

* @typedef {import('./types').Specificity} Specificity
* @typedef {import('./types').Stylesheet} Stylesheet
* @typedef {import('./types').StylesheetRule} StylesheetRule

@@ -132,3 +133,3 @@ * @typedef {import('./types').StylesheetDeclaration} StylesheetDeclaration

/**
* @type {(stylesheet: Array<StylesheetRule>, node: XastElement) => ComputedStyles}
* @type {(stylesheet: Stylesheet, node: XastElement) => ComputedStyles}
*/

@@ -151,3 +152,3 @@ const computeOwnStyle = (stylesheet, node) => {

// collect matching rules
for (const { selectors, declarations, dynamic } of stylesheet) {
for (const { selectors, declarations, dynamic } of stylesheet.rules) {
if (matches(node, selectors)) {

@@ -217,3 +218,3 @@ for (const { name, value, important } of declarations) {

/**
* @type {(root: XastRoot) => Array<StylesheetRule>}
* @type {(root: XastRoot) => Stylesheet}
*/

@@ -224,7 +225,13 @@ const collectStylesheet = (root) => {

*/
const stylesheet = [];
// find and parse all styles
const rules = [];
/**
* @type {Map<XastElement, XastParent>}
*/
const parents = new Map();
visit(root, {
element: {
enter: (node) => {
enter: (node, parentNode) => {
// store parents
parents.set(node, parentNode);
// find and parse all styles
if (node.name === 'style') {

@@ -241,3 +248,3 @@ const dynamic =

if (child.type === 'text' || child.type === 'cdata') {
stylesheet.push(...parseStylesheet(child.value, dynamic));
rules.push(...parseStylesheet(child.value, dynamic));
}

@@ -251,6 +258,6 @@ }

// sort by selectors specificity
stable.inplace(stylesheet, (a, b) =>
stable.inplace(rules, (a, b) =>
compareSpecificity(a.specificity, b.specificity)
);
return stylesheet;
return { rules, parents };
};

@@ -260,12 +267,11 @@ exports.collectStylesheet = collectStylesheet;

/**
* @type {(stylesheet: Array<StylesheetRule>, node: XastElement) => ComputedStyles}
* @type {(stylesheet: Stylesheet, node: XastElement) => ComputedStyles}
*/
const computeStyle = (stylesheet, node) => {
const { parents } = stylesheet;
// collect inherited styles
const computedStyles = computeOwnStyle(stylesheet, node);
let parent = node;
// @ts-ignore parentNode is forbidden in public usage
while (parent.parentNode && parent.parentNode.type !== 'root') {
// @ts-ignore parentNode is forbidden in public usage
const inheritedStyles = computeOwnStyle(stylesheet, parent.parentNode);
let parent = parents.get(node);
while (parent != null && parent.type !== 'root') {
const inheritedStyles = computeOwnStyle(stylesheet, parent);
for (const [name, computed] of Object.entries(inheritedStyles)) {

@@ -281,4 +287,3 @@ if (

}
// @ts-ignore parentNode is forbidden in public usage
parent = parent.parentNode;
parent = parents.get(parent);
}

@@ -285,0 +290,0 @@ return computedStyles;

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

const fs = require('fs');
const { pathToFileURL } = require('url');
const path = require('path');

@@ -17,3 +18,21 @@ const {

const importConfig = async (configFile) => {
const config = require(configFile);
let config;
try {
// dynamic import expects file url instead of path and may fail
// when windows path is provided
const { default: imported } = await import(pathToFileURL(configFile));
config = imported;
} catch (importError) {
// TODO remove require in v3
try {
config = require(configFile);
} catch (requireError) {
// throw original error if es module is detected
if (requireError.code === 'ERR_REQUIRE_ESM') {
throw importError;
} else {
throw requireError;
}
}
}
if (config == null || typeof config !== 'object' || Array.isArray(config)) {

@@ -45,6 +64,14 @@ throw Error(`Invalid config file "${configFile}"`);

while (true) {
const file = path.join(dir, 'svgo.config.js');
if (await isFile(file)) {
return await importConfig(file);
const js = path.join(dir, 'svgo.config.js');
if (await isFile(js)) {
return await importConfig(js);
}
const mjs = path.join(dir, 'svgo.config.mjs');
if (await isFile(mjs)) {
return await importConfig(mjs);
}
const cjs = path.join(dir, 'svgo.config.cjs');
if (await isFile(cjs)) {
return await importConfig(cjs);
}
const parent = path.dirname(dir);

@@ -51,0 +78,0 @@ if (dir === parent) {

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

} = require('./svgo/config.js');
const svg2js = require('./svgo/svg2js.js');
const { parseSvg } = require('./parser.js');
const js2svg = require('./svgo/js2svg.js');

@@ -35,3 +35,3 @@ const { invokePlugins } = require('./svgo/plugins.js');

try {
svgjs = svg2js(input, config.path);
svgjs = parseSvg(input, config.path);
} catch (error) {

@@ -38,0 +38,0 @@ return { error: error.toString(), modernError: error };

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

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

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

const { encodeSVGDatauri, decodeSVGDatauri } = require('./tools.js');
const regSVGFile = /\.svg$/i;

@@ -13,0 +14,0 @@

@@ -1,2 +0,2 @@

type XastDoctype = {
export type XastDoctype = {
type: 'doctype';

@@ -9,3 +9,3 @@ name: string;

type XastInstruction = {
export type XastInstruction = {
type: 'instruction';

@@ -16,3 +16,3 @@ name: string;

type XastComment = {
export type XastComment = {
type: 'comment';

@@ -22,3 +22,3 @@ value: string;

type XastCdata = {
export type XastCdata = {
type: 'cdata';

@@ -28,3 +28,3 @@ value: string;

type XastText = {
export type XastText = {
type: 'text';

@@ -104,2 +104,7 @@ value: string;

export type Stylesheet = {
rules: Array<StylesheetRule>;
parents: Map<XastElement, XastParent>;
};
type StaticStyle = {

@@ -106,0 +111,0 @@ type: 'static';

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

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

"scripts": {
"test": "jest --coverage",
"test": "NODE_OPTIONS=--experimental-vm-modules jest --maxWorkers=3 --coverage",
"lint": "eslint --ignore-path .gitignore . && prettier --check \"**/*.js\" --ignore-path .gitignore",

@@ -102,3 +102,2 @@ "fix": "eslint --ignore-path .gitignore --fix . && prettier --write \"**/*.js\" --ignore-path .gitignore",

"@trysound/sax": "0.2.0",
"colorette": "^1.4.0",
"commander": "^7.2.0",

@@ -108,2 +107,3 @@ "css-select": "^4.1.3",

"csso": "^4.2.0",
"nanocolors": "^0.1.12",
"stable": "^0.1.8"

@@ -120,3 +120,3 @@ },

"eslint": "^7.32.0",
"jest": "^27.2.0",
"jest": "^27.2.1",
"mock-stdin": "^1.0.0",

@@ -123,0 +123,0 @@ "node-fetch": "^2.6.2",

'use strict';
const { inheritableAttrs, elemsGroups } = require('./_collections');
/**
* @typedef {import('../lib/types').XastNode} XastNode
*/
const { inheritableAttrs, elemsGroups } = require('./_collections.js');
exports.type = 'visitor';
exports.name = 'collapseGroups';
exports.type = 'perItemReverse';
exports.active = true;
exports.description = 'collapses useless groups';
function hasAnimatedAttr(item, name) {
if (item.type === 'element') {
return (
(elemsGroups.animation.includes(item.name) &&
item.attributes.attributeName === name) ||
(item.children.length !== 0 &&
item.children.some((child) => hasAnimatedAttr(child, name)))
);
/**
* @type {(node: XastNode, name: string) => boolean}
*/
const hasAnimatedAttr = (node, name) => {
if (node.type === 'element') {
if (
elemsGroups.animation.includes(node.name) &&
node.attributes.attributeName === name
) {
return true;
}
for (const child of node.children) {
if (hasAnimatedAttr(child, name)) {
return true;
}
}
}
return false;
}
};
/*
/**
* Collapse useless groups.

@@ -43,50 +52,56 @@ *

*
* @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 (item) {
// non-empty elements
if (
item.type === 'element' &&
item.name !== 'switch' &&
item.children.length !== 0
) {
item.children.forEach(function (g, i) {
// non-empty groups
if (g.type === 'element' && g.name === 'g' && g.children.length !== 0) {
exports.fn = () => {
return {
element: {
exit: (node, parentNode) => {
if (parentNode.type === 'root' || parentNode.name === 'switch') {
return;
}
// non-empty groups
if (node.name !== 'g' || node.children.length === 0) {
return;
}
// move group attibutes to the single child element
if (Object.keys(g.attributes).length !== 0 && g.children.length === 1) {
var inner = g.children[0];
if (
Object.keys(node.attributes).length !== 0 &&
node.children.length === 1
) {
const firstChild = node.children[0];
// TODO untangle this mess
if (
inner.type === 'element' &&
inner.attributes.id == null &&
g.attributes.filter == null &&
(g.attributes.class == null || inner.attributes.class == null) &&
((g.attributes['clip-path'] == null && g.attributes.mask == null) ||
(inner.type === 'element' &&
inner.name === 'g' &&
g.attributes.transform == null &&
inner.attributes.transform == null))
firstChild.type === 'element' &&
firstChild.attributes.id == null &&
node.attributes.filter == null &&
(node.attributes.class == null ||
firstChild.attributes.class == null) &&
((node.attributes['clip-path'] == null &&
node.attributes.mask == null) ||
(firstChild.name === 'g' &&
node.attributes.transform == null &&
firstChild.attributes.transform == null))
) {
for (const [name, value] of Object.entries(g.attributes)) {
if (g.children.some((item) => hasAnimatedAttr(item, name)))
for (const [name, value] of Object.entries(node.attributes)) {
// avoid copying to not conflict with animated attribute
if (hasAnimatedAttr(firstChild, name)) {
return;
if (inner.attributes[name] == null) {
inner.attributes[name] = value;
} else if (name == 'transform') {
inner.attributes[name] = value + ' ' + inner.attributes[name];
} else if (inner.attributes[name] === 'inherit') {
inner.attributes[name] = value;
}
if (firstChild.attributes[name] == null) {
firstChild.attributes[name] = value;
} else if (name === 'transform') {
firstChild.attributes[name] =
value + ' ' + firstChild.attributes[name];
} else if (firstChild.attributes[name] === 'inherit') {
firstChild.attributes[name] = value;
} else if (
inheritableAttrs.includes(name) === false &&
inner.attributes[name] !== value
firstChild.attributes[name] !== value
) {
return;
}
delete g.attributes[name];
delete node.attributes[name];
}

@@ -97,11 +112,26 @@ }

// collapse groups without attributes
if (
Object.keys(g.attributes).length === 0 &&
!g.children.some((item) => item.isElem(elemsGroups.animation))
) {
item.spliceContent(i, 1, g.children);
if (Object.keys(node.attributes).length === 0) {
// animation elements "add" attributes to group
// group should be preserved
for (const child of node.children) {
if (
child.type === 'element' &&
elemsGroups.animation.includes(child.name)
) {
return;
}
}
// replace current node with all its children
const index = parentNode.children.indexOf(node);
parentNode.children.splice(index, 1, ...node.children);
// TODO remove in v3
for (const child of node.children) {
// @ts-ignore parentNode is forbidden for public usage
// and will be moved in v3
child.parentNode = parentNode;
}
}
}
});
}
},
},
};
};
'use strict';
const { closestByName, detachNodeFromParent } = require('../lib/xast.js');
/**
* @typedef {import('../lib/types').XastElement} XastElement
*/
const { visitSkip, detachNodeFromParent } = require('../lib/xast.js');
const JSAPI = require('../lib/svgo/jsAPI.js');

@@ -15,4 +19,9 @@

* @author strarsis <strarsis@gmail.com>
*
* @type {import('../lib/types').Plugin<void>}
*/
exports.fn = () => {
/**
* @type {null | XastElement}
*/
let firstStyleElement = null;

@@ -22,67 +31,65 @@ let collectedStyles = '';

const enterElement = (node, parentNode) => {
// collect style elements
if (node.name !== 'style') {
return;
}
return {
element: {
enter: (node, parentNode) => {
// skip <foreignObject> content
if (node.name === 'foreignObject') {
return visitSkip;
}
// skip <style> with invalid type attribute
if (
node.attributes.type != null &&
node.attributes.type !== '' &&
node.attributes.type !== 'text/css'
) {
return;
}
// collect style elements
if (node.name !== 'style') {
return;
}
// skip <foreignObject> content
if (closestByName(node, 'foreignObject')) {
return;
}
// skip <style> with invalid type attribute
if (
node.attributes.type != null &&
node.attributes.type !== '' &&
node.attributes.type !== 'text/css'
) {
return;
}
// extract style element content
let css = '';
for (const child of node.children) {
if (child.type === 'text') {
css += child.value;
}
if (child.type === 'cdata') {
styleContentType = 'cdata';
css += child.value;
}
}
// extract style element content
let css = '';
for (const child of node.children) {
if (child.type === 'text') {
css += child.value;
}
if (child.type === 'cdata') {
styleContentType = 'cdata';
css += child.value;
}
}
// remove empty style elements
if (css.trim().length === 0) {
detachNodeFromParent(node, parentNode);
return;
}
// remove empty style elements
if (css.trim().length === 0) {
detachNodeFromParent(node, parentNode);
return;
}
// collect css and wrap with media query if present in attribute
if (node.attributes.media == null) {
collectedStyles += css;
} else {
collectedStyles += `@media ${node.attributes.media}{${css}}`;
delete node.attributes.media;
}
// collect css and wrap with media query if present in attribute
if (node.attributes.media == null) {
collectedStyles += css;
} else {
collectedStyles += `@media ${node.attributes.media}{${css}}`;
delete node.attributes.media;
}
// combine collected styles in the first style element
if (firstStyleElement == null) {
firstStyleElement = node;
} else {
detachNodeFromParent(node, parentNode);
firstStyleElement.children = [
new JSAPI(
{ type: styleContentType, value: collectedStyles },
firstStyleElement
),
];
}
};
return {
element: {
enter: enterElement,
// combine collected styles in the first style element
if (firstStyleElement == null) {
firstStyleElement = node;
} else {
detachNodeFromParent(node, parentNode);
firstStyleElement.children = [
new JSAPI(
{ type: styleContentType, value: collectedStyles },
firstStyleElement
),
];
}
},
},
};
};
'use strict';
const { inheritableAttrs, pathElems } = require('./_collections');
const { visit } = require('../lib/xast.js');
const { inheritableAttrs, pathElems } = require('./_collections.js');
exports.type = 'visitor';
exports.name = 'moveElemsAttrsToGroup';
exports.type = 'perItemReverse';
exports.active = true;
exports.description = 'Move common attributes of group children to the group';
exports.description = 'moves elements attributes to the existing group wrapper';
/**
* Collapse content's intersected and inheritable
* attributes to the existing group wrapper.
* Move common attributes of group children to the group
*

@@ -32,96 +29,103 @@ * @example

*
* @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 (item) {
if (
item.type === 'element' &&
item.name === 'g' &&
item.children.length > 1
) {
var intersection = {},
hasTransform = false,
hasClip =
item.attributes['clip-path'] != null || item.attributes.mask != null,
intersected = item.children.every(function (inner) {
if (
inner.type === 'element' &&
Object.keys(inner.attributes).length !== 0
) {
// don't mess with possible styles (hack until CSS parsing is implemented)
if (inner.attributes.class) return false;
if (!Object.keys(intersection).length) {
intersection = inner.attributes;
} else {
intersection = intersectInheritableAttrs(
intersection,
inner.attributes
);
exports.fn = (root) => {
// find if any style element is present
let deoptimizedWithStyles = false;
visit(root, {
element: {
enter: (node) => {
if (node.name === 'style') {
deoptimizedWithStyles = true;
}
},
},
});
if (!intersection) return false;
}
return {
element: {
exit: (node) => {
// process only groups with more than 1 children
if (node.name !== 'g' || node.children.length <= 1) {
return;
}
return true;
// deoptimize the plugin when style elements are present
// selectors may rely on id, classes or tag names
if (deoptimizedWithStyles) {
return;
}
return false;
}),
allPath = item.children.every(function (inner) {
return inner.isElem(pathElems);
});
if (intersected) {
item.children.forEach(function (g) {
for (const [name, value] of Object.entries(intersection)) {
if ((!allPath && !hasClip) || name !== 'transform') {
delete g.attributes[name];
if (name === 'transform') {
if (!hasTransform) {
if (item.attributes.transform != null) {
item.attributes.transform =
item.attributes.transform + ' ' + value;
} else {
item.attributes.transform = value;
/**
* find common attributes in group children
* @type {Map<string, string>}
*/
const commonAttributes = new Map();
let initial = true;
let everyChildIsPath = true;
for (const child of node.children) {
if (child.type === 'element') {
if (pathElems.includes(child.name) === false) {
everyChildIsPath = false;
}
if (initial) {
initial = false;
// collect all inheritable attributes from first child element
for (const [name, value] of Object.entries(child.attributes)) {
// consider only inheritable attributes
if (inheritableAttrs.includes(name)) {
commonAttributes.set(name, value);
}
hasTransform = true;
}
} else {
item.attributes[name] = value;
// exclude uncommon attributes from initial list
for (const [name, value] of commonAttributes) {
if (child.attributes[name] !== value) {
commonAttributes.delete(name);
}
}
}
}
}
});
}
}
};
/**
* Intersect inheritable attributes.
*
* @param {Object} a first attrs object
* @param {Object} b second attrs object
*
* @return {Object} intersected attrs object
*/
function intersectInheritableAttrs(a, b) {
var c = {};
// preserve transform on children when group has clip-path or mask
if (
node.attributes['clip-path'] != null ||
node.attributes.mask != null
) {
commonAttributes.delete('transform');
}
for (const [name, value] of Object.entries(a)) {
if (
// eslint-disable-next-line no-prototype-builtins
b.hasOwnProperty(name) &&
inheritableAttrs.includes(name) &&
value === b[name]
) {
c[name] = value;
}
}
// preserve transform when all children are paths
// so the transform could be applied to path data by other plugins
if (everyChildIsPath) {
commonAttributes.delete('transform');
}
if (!Object.keys(c).length) return false;
// add common children attributes to group
for (const [name, value] of commonAttributes) {
if (name === 'transform') {
if (node.attributes.transform != null) {
node.attributes.transform = `${node.attributes.transform} ${value}`;
} else {
node.attributes.transform = value;
}
} else {
node.attributes[name] = value;
}
}
return c;
}
// delete common attributes from children
for (const child of node.children) {
if (child.type === 'element') {
for (const [name] of commonAttributes) {
delete child.attributes[name];
}
}
}
},
},
};
};

@@ -9,3 +9,16 @@ 'use strict';

const DEFAULT_SEPARATOR = ':';
const ENOATTRS = `Warning: The plugin "removeAttrs" requires the "attrs" parameter.
It should have a pattern to remove, otherwise the plugin is a noop.
Config example:
plugins: [
{
name: "removeAttrs",
params: {
attrs: "(fill|stroke)"
}
}
]
`;
/**

@@ -81,3 +94,7 @@ * Remove attributes

exports.fn = (root, params) => {
// wrap into an array if params is not
if (typeof params.attrs == 'undefined') {
console.warn(ENOATTRS);
return null;
}
const elemSeparator =

@@ -84,0 +101,0 @@ typeof params.elemSeparator == 'string'

Sorry, the diff of this file is not supported yet

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