🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

svgo

Package Overview
Dependencies
Maintainers
4
Versions
116
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

svgo - npm Package Compare versions

Comparing version
2.8.1
to
2.8.2
+1
-1
package.json
{
"packageManager": "yarn@2.4.3",
"name": "svgo",
"version": "2.8.1",
"version": "2.8.2",
"description": "Nodejs-based tool for optimizing SVG vector graphics files",

@@ -6,0 +6,0 @@ "license": "MIT",

'use strict';
/**
* @typedef {import('../lib/types').PathDataItem} PathDataItem
*/
const { parsePathData, stringifyPathData } = require('./path.js');
describe('parse path data', () => {
it('should allow spaces between commands', () => {
expect(parsePathData('M0 10 L \n\r\t20 30')).toEqual([
{ command: 'M', args: [0, 10] },
{ command: 'L', args: [20, 30] },
]);
});
it('should allow spaces and commas between arguments', () => {
expect(parsePathData('M0 , 10 L 20 \n\r\t30,40,50')).toEqual([
{ command: 'M', args: [0, 10] },
{ command: 'L', args: [20, 30] },
{ command: 'L', args: [40, 50] },
]);
});
it('should forbid commas before commands', () => {
expect(parsePathData(', M0 10')).toEqual([]);
});
it('should forbid commas between commands', () => {
expect(parsePathData('M0,10 , L 20,30')).toEqual([
{ command: 'M', args: [0, 10] },
]);
});
it('should forbid commas between command name and argument', () => {
expect(parsePathData('M0,10 L,20,30')).toEqual([
{ command: 'M', args: [0, 10] },
]);
});
it('should forbid multipe commas in a row', () => {
expect(parsePathData('M0 , , 10')).toEqual([]);
});
it('should stop when unknown char appears', () => {
expect(parsePathData('M0 10 , L 20 #40')).toEqual([
{ command: 'M', args: [0, 10] },
]);
});
it('should stop when not enough arguments', () => {
expect(parsePathData('M0 10 L 20 L 30 40')).toEqual([
{ command: 'M', args: [0, 10] },
]);
});
it('should stop if moveto not the first command', () => {
expect(parsePathData('L 10 20')).toEqual([]);
expect(parsePathData('10 20')).toEqual([]);
});
it('should stop on invalid scientific notation', () => {
expect(parsePathData('M 0 5e++1 L 0 0')).toEqual([
{ command: 'M', args: [0, 5] },
]);
});
it('should stop on invalid numbers', () => {
expect(parsePathData('M ...')).toEqual([]);
});
it('should handle arcs', () => {
expect(
parsePathData(
`
M600,350
l 50,-25
a25,25 -30 0,1 50,-25
25,50 -30 0,1 50,-25
25,75 -30 01.2,-25
a25,100 -30 0150,-25
l 50,-25
`
)
).toEqual([
{ command: 'M', args: [600, 350] },
{ command: 'l', args: [50, -25] },
{ command: 'a', args: [25, 25, -30, 0, 1, 50, -25] },
{ command: 'a', args: [25, 50, -30, 0, 1, 50, -25] },
{ command: 'a', args: [25, 75, -30, 0, 1, 0.2, -25] },
{ command: 'a', args: [25, 100, -30, 0, 1, 50, -25] },
{ command: 'l', args: [50, -25] },
]);
});
});
describe('stringify path data', () => {
it('should combine sequence of the same commands', () => {
expect(
stringifyPathData({
pathData: [
{ command: 'M', args: [0, 0] },
{ command: 'h', args: [10] },
{ command: 'h', args: [20] },
{ command: 'h', args: [30] },
{ command: 'H', args: [40] },
{ command: 'H', args: [50] },
],
})
).toEqual('M0 0h10 20 30H40 50');
});
it('should not combine sequence of moveto', () => {
expect(
stringifyPathData({
pathData: [
{ command: 'M', args: [0, 0] },
{ command: 'M', args: [10, 10] },
{ command: 'm', args: [20, 30] },
{ command: 'm', args: [40, 50] },
],
})
).toEqual('M0 0M10 10m20 30m40 50');
});
it('should combine moveto and sequence of lineto', () => {
expect(
stringifyPathData({
pathData: [
{ command: 'M', args: [0, 0] },
{ command: 'l', args: [10, 10] },
{ command: 'M', args: [0, 0] },
{ command: 'l', args: [10, 10] },
{ command: 'M', args: [0, 0] },
{ command: 'L', args: [10, 10] },
],
})
).toEqual('m0 0 10 10M0 0l10 10M0 0 10 10');
expect(
stringifyPathData({
pathData: [
{ command: 'm', args: [0, 0] },
{ command: 'L', args: [10, 10] },
],
})
).toEqual('M0 0 10 10');
});
it('should avoid space before first, negative and decimals', () => {
expect(
stringifyPathData({
pathData: [
{ command: 'M', args: [0, -1.2] },
{ command: 'L', args: [0.3, 4] },
{ command: 'L', args: [5, -0.6] },
{ command: 'L', args: [7, 0.8] },
],
})
).toEqual('M0-1.2.3 4 5-.6 7 .8');
});
it('should configure precision', () => {
/**
* @type {Array<PathDataItem>}
*/
const pathData = [
{ command: 'M', args: [0, -1.9876] },
{ command: 'L', args: [0.3, 3.14159265] },
{ command: 'L', args: [-0.3, -3.14159265] },
{ command: 'L', args: [100, 200] },
];
expect(
stringifyPathData({
pathData,
precision: 3,
})
).toEqual('M0-1.988.3 3.142-.3-3.142 100 200');
expect(
stringifyPathData({
pathData,
precision: 0,
})
).toEqual('M0-2 0 3 0-3 100 200');
});
it('allows to avoid spaces after arc flags', () => {
/**
* @type {Array<PathDataItem>}
*/
const pathData = [
{ command: 'M', args: [0, 0] },
{ command: 'A', args: [50, 50, 10, 1, 0, 0.2, 20] },
{ command: 'a', args: [50, 50, 10, 1, 0, 0.2, 20] },
{ command: 'a', args: [50, 50, 10, 1, 0, 0.2, 20] },
];
expect(
stringifyPathData({
pathData,
disableSpaceAfterFlags: false,
})
).toEqual('M0 0A50 50 10 1 0 .2 20a50 50 10 1 0 .2 20 50 50 10 1 0 .2 20');
expect(
stringifyPathData({
pathData,
disableSpaceAfterFlags: true,
})
).toEqual('M0 0A50 50 10 10.2 20a50 50 10 10.2 20 50 50 10 10.2 20');
});
});
'use strict';
/**
* @typedef {import('./types').XastParent} XastParent
* @typedef {import('./types').XastElement} XastElement
*/
const { collectStylesheet, computeStyle } = require('./style.js');
const { visit } = require('./xast.js');
const { parseSvg } = require('./parser.js');
/**
* @type {(node: XastParent, id: string) => XastElement}
*/
const getElementById = (node, id) => {
/**
* @type {null | XastElement}
*/
let matched = null;
visit(node, {
element: {
enter: (node) => {
if (node.attributes.id === id) {
matched = node;
}
},
},
});
if (matched == null) {
throw Error('Assert node');
}
return matched;
};
it('collects styles', () => {
const root = parseSvg(`
<svg>
<rect id="class" class="a" />
<rect id="two-classes" class="b a" />
<rect id="attribute" fill="purple" />
<rect id="inline-style" style="fill: grey;" />
<g fill="yellow">
<rect id="inheritance" />
<g style="fill: blue;">
<g>
<rect id="nested-inheritance" />
</g>
</g>
</g>
<style>
.a { fill: red; }
</style>
<style>
<![CDATA[
.b { fill: green; stroke: black; }
]]>
</style>
</svg>
`);
const stylesheet = collectStylesheet(root);
expect(computeStyle(stylesheet, getElementById(root, 'class'))).toEqual({
fill: { type: 'static', inherited: false, value: 'red' },
});
expect(computeStyle(stylesheet, getElementById(root, 'two-classes'))).toEqual(
{
fill: { type: 'static', inherited: false, value: 'green' },
stroke: { type: 'static', inherited: false, value: 'black' },
}
);
expect(computeStyle(stylesheet, getElementById(root, 'attribute'))).toEqual({
fill: { type: 'static', inherited: false, value: 'purple' },
});
expect(
computeStyle(stylesheet, getElementById(root, 'inline-style'))
).toEqual({
fill: { type: 'static', inherited: false, value: 'grey' },
});
expect(computeStyle(stylesheet, getElementById(root, 'inheritance'))).toEqual(
{
fill: { type: 'static', inherited: true, value: 'yellow' },
}
);
expect(
computeStyle(stylesheet, getElementById(root, 'nested-inheritance'))
).toEqual({
fill: { type: 'static', inherited: true, value: 'blue' },
});
});
it('prioritizes different kinds of styles', () => {
const root = parseSvg(`
<svg>
<style>
g > .a { fill: red; }
.a { fill: green; }
.b { fill: blue; }
</style>
<g fill="yellow">
<rect id="complex-selector" class="a" />
<rect id="attribute-over-inheritance" fill="orange" />
<rect id="style-rule-over-attribute" class="b" fill="grey" />
<rect id="inline-style-over-style-rule" style="fill: purple;" class="b" />
</g>
</svg>
`);
const stylesheet = collectStylesheet(root);
expect(
computeStyle(stylesheet, getElementById(root, 'complex-selector'))
).toEqual({
fill: { type: 'static', inherited: false, value: 'red' },
});
expect(
computeStyle(stylesheet, getElementById(root, 'attribute-over-inheritance'))
).toEqual({
fill: { type: 'static', inherited: false, value: 'orange' },
});
expect(
computeStyle(stylesheet, getElementById(root, 'style-rule-over-attribute'))
).toEqual({
fill: { type: 'static', inherited: false, value: 'blue' },
});
expect(
computeStyle(
stylesheet,
getElementById(root, 'inline-style-over-style-rule')
)
).toEqual({
fill: { type: 'static', inherited: false, value: 'purple' },
});
});
it('prioritizes important styles', () => {
const root = parseSvg(`
<svg>
<style>
g > .a { fill: red; }
.b { fill: green !important; }
</style>
<rect id="complex-selector" class="a b" />
<rect id="style-rule-over-inline-style" style="fill: orange;" class="b" />
<rect id="inline-style-over-style-rule" style="fill: purple !important;" class="b" />
</svg>
`);
const stylesheet = collectStylesheet(root);
expect(
computeStyle(stylesheet, getElementById(root, 'complex-selector'))
).toEqual({
fill: { type: 'static', inherited: false, value: 'green' },
});
expect(
computeStyle(
stylesheet,
getElementById(root, 'style-rule-over-inline-style')
)
).toEqual({
fill: { type: 'static', inherited: false, value: 'green' },
});
expect(
computeStyle(
stylesheet,
getElementById(root, 'inline-style-over-style-rule')
)
).toEqual({
fill: { type: 'static', inherited: false, value: 'purple' },
});
});
it('treats at-rules and pseudo-classes as dynamic styles', () => {
const root = parseSvg(`
<svg>
<style>
@media screen {
.a { fill: red; }
}
.b:hover { fill: green; }
.c { fill: blue; }
.d { fill: purple; }
</style>
<rect id="media-query" class="a d" style="fill: orange;" />
<rect id="hover" class="b" style="fill: yellow;" />
<g class="a">
<rect id="inherited" />
<rect id="inherited-overriden" class="c" />
</g>
<rect id="static" class="c" style="fill: black" />
</svg>
`);
const stylesheet = collectStylesheet(root);
expect(computeStyle(stylesheet, getElementById(root, 'media-query'))).toEqual(
{
fill: { type: 'dynamic', inherited: false },
}
);
expect(computeStyle(stylesheet, getElementById(root, 'hover'))).toEqual({
fill: { type: 'dynamic', inherited: false },
});
expect(computeStyle(stylesheet, getElementById(root, 'inherited'))).toEqual({
fill: { type: 'dynamic', inherited: true },
});
expect(
computeStyle(stylesheet, getElementById(root, 'inherited-overriden'))
).toEqual({
fill: { type: 'static', inherited: false, value: 'blue' },
});
expect(computeStyle(stylesheet, getElementById(root, 'static'))).toEqual({
fill: { type: 'static', inherited: false, value: 'black' },
});
});
it('considers <style> media attribute', () => {
const root = parseSvg(`
<svg>
<style media="print">
@media screen {
.a { fill: red; }
}
.b { fill: green; }
</style>
<style media="all">
.c { fill: blue; }
</style>
<rect id="media-query" class="a" />
<rect id="kinda-static" class="b" />
<rect id="static" class="c" />
</svg>
`);
const stylesheet = collectStylesheet(root);
expect(computeStyle(stylesheet, getElementById(root, 'media-query'))).toEqual(
{
fill: { type: 'dynamic', inherited: false },
}
);
expect(
computeStyle(stylesheet, getElementById(root, 'kinda-static'))
).toEqual({
fill: { type: 'dynamic', inherited: false },
});
expect(computeStyle(stylesheet, getElementById(root, 'static'))).toEqual({
fill: { type: 'static', inherited: false, value: 'blue' },
});
});
it('ignores <style> with invalid type', () => {
const root = parseSvg(`
<svg>
<style type="text/css">
.a { fill: red; }
</style>
<style type="">
.b { fill: green; }
</style>
<style type="text/invalid">
.c { fill: blue; }
</style>
<rect id="valid-type" class="a" />
<rect id="empty-type" class="b" />
<rect id="invalid-type" class="c" />
</svg>
`);
const stylesheet = collectStylesheet(root);
expect(computeStyle(stylesheet, getElementById(root, 'valid-type'))).toEqual({
fill: { type: 'static', inherited: false, value: 'red' },
});
expect(computeStyle(stylesheet, getElementById(root, 'empty-type'))).toEqual({
fill: { type: 'static', inherited: false, value: 'green' },
});
expect(
computeStyle(stylesheet, getElementById(root, 'invalid-type'))
).toEqual({});
});
it('ignores keyframes atrule', () => {
const root = parseSvg(`
<svg>
<style>
.a {
animation: loading 4s linear infinite;
}
@keyframes loading {
0% {
stroke-dashoffset: 440;
}
50% {
stroke-dashoffset: 0;
}
50.1% {
stroke-dashoffset: 880;
}
}
</style>
<rect id="element" class="a" />
</svg>
`);
const stylesheet = collectStylesheet(root);
expect(computeStyle(stylesheet, getElementById(root, 'element'))).toEqual({
animation: {
type: 'static',
inherited: false,
value: 'loading 4s linear infinite',
},
});
});
'use strict';
/**
* @typedef {import('../lib/types').Plugin} Plugin
*/
const os = require('os');
const path = require('path');
const { optimize, loadConfig } = require('./svgo-node.js');
const describeLF = os.EOL === '\r\n' ? describe.skip : describe;
const describeCRLF = os.EOL === '\r\n' ? describe : describe.skip;
describeLF('with LF line-endings', () => {
test('should work', () => {
const svg = `
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 120 120">
<desc>
Not standard description
</desc>
<circle fill="#ff0000" cx="60" cy="60" r="50"/>
</svg>
`;
const { data } = optimize(svg);
// using toEqual because line endings matter in these tests
expect(data).toEqual(
'<svg viewBox="0 0 120 120"><circle fill="red" cx="60" cy="60" r="50"/></svg>'
);
});
test('should respect config', () => {
const svg = `
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 120 120">
<desc>
Not standard description
</desc>
<circle fill="#ff0000" cx="60" cy="60" r="50"/>
</svg>
`;
const { data } = optimize(svg, {
js2svg: { pretty: true, indent: 2 },
});
// using toEqual because line endings matter in these tests
expect(data).toEqual(
'<svg viewBox="0 0 120 120">\n <circle fill="red" cx="60" cy="60" r="50"/>\n</svg>\n'
);
});
test('should respect line-ending config', () => {
const svg = `
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 120 120">
<desc>
Not standard description
</desc>
<circle fill="#ff0000" cx="60" cy="60" r="50"/>
</svg>
`;
const { data } = optimize(svg, {
js2svg: { eol: 'crlf', pretty: true, indent: 2 },
});
// using toEqual because line endings matter in these tests
expect(data).toEqual(
'<svg viewBox="0 0 120 120">\r\n <circle fill="red" cx="60" cy="60" r="50"/>\r\n</svg>\r\n'
);
});
});
describeCRLF('with CRLF line-endings', () => {
test('should work', () => {
const svg = `
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 120 120">
<desc>
Not standard description
</desc>
<circle fill="#ff0000" cx="60" cy="60" r="50"/>
</svg>
`;
const { data } = optimize(svg);
// using toEqual because line endings matter in these tests
expect(data).toEqual(
'<svg viewBox="0 0 120 120"><circle fill="red" cx="60" cy="60" r="50"/></svg>'
);
});
test('should respect config', () => {
const svg = `
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 120 120">
<desc>
Not standard description
</desc>
<circle fill="#ff0000" cx="60" cy="60" r="50"/>
</svg>
`;
const { data } = optimize(svg, {
js2svg: { pretty: true, indent: 2 },
});
// using toEqual because line endings matter in these tests
expect(data).toEqual(
'<svg viewBox="0 0 120 120">\r\n <circle fill="red" cx="60" cy="60" r="50"/>\r\n</svg>\r\n'
);
});
test('should respect line-ending config', () => {
const svg = `
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 120 120">
<desc>
Not standard description
</desc>
<circle fill="#ff0000" cx="60" cy="60" r="50"/>
</svg>
`;
const { data } = optimize(svg, {
js2svg: { eol: 'lf', pretty: true, indent: 2 },
});
// using toEqual because line endings matter in these tests
expect(data).toEqual(
'<svg viewBox="0 0 120 120">\n <circle fill="red" cx="60" cy="60" r="50"/>\n</svg>\n'
);
});
});
describe('loadConfig', () => {
const cwd = process.cwd();
const fixtures = path.join(cwd, './test/fixtures/config-loader');
test('loads by absolute path', async () => {
expect(await loadConfig(path.join(fixtures, 'one/two/config.js'))).toEqual({
plugins: [],
});
});
test('loads by relative path to cwd', async () => {
const config = await loadConfig('one/two/config.js', fixtures);
expect(config).toEqual({ plugins: [] });
});
test('searches in cwd and up', async () => {
expect(await loadConfig(null, path.join(fixtures, 'one/two'))).toEqual({
plugins: [],
});
expect(
await loadConfig(null, path.join(cwd, './test/fixtures/missing'))
).toEqual(null);
// TODO remove check in v3
if (process.version.startsWith('v10.') === false) {
expect(await loadConfig(null, path.join(fixtures, 'mjs'))).toEqual({
plugins: ['mjs'],
});
}
expect(await loadConfig(null, path.join(fixtures, 'cjs'))).toEqual({
plugins: ['cjs'],
});
});
test('fails when specified config does not exist', async () => {
try {
await loadConfig('{}');
expect.fail('Config is loaded successfully');
} catch (error) {
expect(error.message).toMatch(/Cannot find module/);
}
});
test('fails when exported config not an object', async () => {
try {
await loadConfig(path.join(fixtures, 'invalid-null.js'));
expect.fail('Config is loaded successfully');
} catch (error) {
expect(error.message).toMatch(/Invalid config file/);
}
try {
await loadConfig(path.join(fixtures, 'invalid-array.js'));
expect.fail('Config is loaded successfully');
} catch (error) {
expect(error.message).toMatch(/Invalid config file/);
}
try {
await loadConfig(path.join(fixtures, 'invalid-string.js'));
expect.fail('Config is loaded successfully');
} catch (error) {
expect(error.message).toMatch(/Invalid config file/);
}
});
test('handles runtime errors properly', async () => {
try {
await loadConfig(path.join(fixtures, 'invalid-runtime.js'));
expect.fail('Config is loaded successfully');
} catch (error) {
expect(error.message).toMatch(/plugins is not defined/);
}
// TODO remove check in v3
if (process.version.startsWith('v10.') === false) {
try {
await loadConfig(path.join(fixtures, 'invalid-runtime.mjs'));
expect.fail('Config is loaded successfully');
} catch (error) {
expect(error.message).toMatch(/plugins is not defined/);
}
}
});
test('handles MODULE_NOT_FOUND properly', async () => {
try {
await loadConfig(path.join(fixtures, 'module-not-found.js'));
expect.fail('Config is loaded successfully');
} catch (error) {
expect(error.message).toMatch(/Cannot find module 'unknown-module'/);
}
});
});
'use strict';
/**
* @typedef {import('../lib/types').Plugin} Plugin
*/
const { optimize } = require('./svgo.js');
test('allow to setup default preset', () => {
const svg = `
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 120 120">
<desc>
Not standard description
</desc>
<circle fill="#ff0000" cx="60" cy="60" r="50"/>
</svg>
`;
const { data } = optimize(svg, {
plugins: ['preset-default'],
js2svg: { pretty: true, indent: 2 },
});
expect(data).toMatchInlineSnapshot(`
"<svg viewBox=\\"0 0 120 120\\">
<circle fill=\\"red\\" cx=\\"60\\" cy=\\"60\\" r=\\"50\\"/>
</svg>
"
`);
});
test('allow to disable and customize plugins in preset', () => {
const svg = `
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 120 120">
<desc>
Not standard description
</desc>
<circle fill="#ff0000" cx="60" cy="60" r="50"/>
</svg>
`;
const { data } = optimize(svg, {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeXMLProcInst: false,
removeDesc: {
removeAny: false,
},
},
},
},
],
js2svg: { pretty: true, indent: 2 },
});
expect(data).toMatchInlineSnapshot(`
"<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?>
<svg viewBox=\\"0 0 120 120\\">
<desc>
Not standard description
</desc>
<circle fill=\\"red\\" cx=\\"60\\" cy=\\"60\\" r=\\"50\\"/>
</svg>
"
`);
});
test('warn when user tries enable plugins in preset', () => {
const svg = `
<svg viewBox="0 0 120 120"></svg>
`;
const warn = jest.spyOn(console, 'warn');
optimize(svg, {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
cleanupListOfValues: true,
},
},
},
],
js2svg: { pretty: true, indent: 2 },
});
expect(warn)
.toBeCalledWith(`You are trying to enable cleanupListOfValues which is not part of preset.
Try to put it before or after preset, for example
plugins: [
{
name: 'preset-default',
},
'cleanupListOfValues'
]
`);
warn.mockRestore();
});
describe('allow to configure EOL', () => {
test('should respect EOL set to LF', () => {
const svg = `
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 120 120">
<desc>
Not standard description
</desc>
<circle fill="#ff0000" cx="60" cy="60" r="50"/>
</svg>
`;
const { data } = optimize(svg, {
js2svg: { eol: 'lf', pretty: true, indent: 2 },
});
// using toEqual because line endings matter in these tests
expect(data).toEqual(
'<svg viewBox="0 0 120 120">\n <circle fill="red" cx="60" cy="60" r="50"/>\n</svg>\n'
);
});
test('should respect EOL set to CRLF', () => {
const svg = `
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 120 120">
<desc>
Not standard description
</desc>
<circle fill="#ff0000" cx="60" cy="60" r="50"/>
</svg>
`;
const { data } = optimize(svg, {
js2svg: { eol: 'crlf', pretty: true, indent: 2 },
});
// using toEqual because line endings matter in these tests
expect(data).toEqual(
'<svg viewBox="0 0 120 120">\r\n <circle fill="red" cx="60" cy="60" r="50"/>\r\n</svg>\r\n'
);
});
test('should default to LF line break for any other EOL values', () => {
const svg = `
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 120 120">
<desc>
Not standard description
</desc>
<circle fill="#ff0000" cx="60" cy="60" r="50"/>
</svg>
`;
const { data } = optimize(svg, {
js2svg: { eol: 'invalid', pretty: true, indent: 2 },
});
// using toEqual because line endings matter in these tests
expect(data).toEqual(
'<svg viewBox="0 0 120 120">\n <circle fill="red" cx="60" cy="60" r="50"/>\n</svg>\n'
);
});
});
describe('allow to configure final newline', () => {
test('should not add final newline when unset', () => {
const svg = `
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 120 120">
<desc>
Not standard description
</desc>
<circle fill="#ff0000" cx="60" cy="60" r="50"/>
</svg>
`;
const { data } = optimize(svg, { js2svg: { eol: 'lf' } });
// using toEqual because line endings matter in these tests
expect(data).toEqual(
'<svg viewBox="0 0 120 120"><circle fill="red" cx="60" cy="60" r="50"/></svg>'
);
});
test('should add final newline when set', () => {
const svg = `
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 120 120">
<desc>
Not standard description
</desc>
<circle fill="#ff0000" cx="60" cy="60" r="50"/>
</svg>
`;
const { data } = optimize(svg, {
js2svg: { finalNewline: true, eol: 'lf' },
});
// using toEqual because line endings matter in these tests
expect(data).toEqual(
'<svg viewBox="0 0 120 120"><circle fill="red" cx="60" cy="60" r="50"/></svg>\n'
);
});
test('should not add extra newlines when using pretty: true', () => {
const svg = `
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 120 120">
<desc>
Not standard description
</desc>
<circle fill="#ff0000" cx="60" cy="60" r="50"/>
</svg>
`;
const { data } = optimize(svg, {
js2svg: { finalNewline: true, pretty: true, indent: 2, eol: 'lf' },
});
// using toEqual because line endings matter in these tests
expect(data).toEqual(
'<svg viewBox="0 0 120 120">\n <circle fill="red" cx="60" cy="60" r="50"/>\n</svg>\n'
);
});
});
test('allow to customize precision for preset', () => {
const svg = `
<svg viewBox="0 0 120 120">
<circle fill="#ff0000" cx="60.444444" cy="60" r="50"/>
</svg>
`;
const { data } = optimize(svg, {
plugins: [
{
name: 'preset-default',
params: {
floatPrecision: 4,
},
},
],
js2svg: { pretty: true, indent: 2 },
});
expect(data).toMatchInlineSnapshot(`
"<svg viewBox=\\"0 0 120 120\\">
<circle fill=\\"red\\" cx=\\"60.4444\\" cy=\\"60\\" r=\\"50\\"/>
</svg>
"
`);
});
test('plugin precision should override preset precision', () => {
const svg = `
<svg viewBox="0 0 120 120">
<circle fill="#ff0000" cx="60.444444" cy="60" r="50"/>
</svg>
`;
const { data } = optimize(svg, {
plugins: [
{
name: 'preset-default',
params: {
floatPrecision: 4,
overrides: {
cleanupNumericValues: {
floatPrecision: 5,
},
},
},
},
],
js2svg: { pretty: true, indent: 2 },
});
expect(data).toMatchInlineSnapshot(`
"<svg viewBox=\\"0 0 120 120\\">
<circle fill=\\"red\\" cx=\\"60.44444\\" cy=\\"60\\" r=\\"50\\"/>
</svg>
"
`);
});
test('provides informative error in result', () => {
const svg = `<svg viewBox="0 0 120 120">
<circle fill="#ff0000" cx=60.444444" cy="60" r="50"/>
</svg>
`;
const { modernError: error } = optimize(svg, { path: 'test.svg' });
expect(error.name).toEqual('SvgoParserError');
expect(error.message).toEqual('test.svg:2:33: Unquoted attribute value');
expect(error.reason).toEqual('Unquoted attribute value');
expect(error.line).toEqual(2);
expect(error.column).toEqual(33);
expect(error.source).toEqual(svg);
});
test('provides code snippet in rendered error', () => {
const svg = `<svg viewBox="0 0 120 120">
<circle fill="#ff0000" cx=60.444444" cy="60" r="50"/>
</svg>
`;
const { modernError: error } = optimize(svg, { path: 'test.svg' });
expect(error.toString())
.toEqual(`SvgoParserError: test.svg:2:29: Unquoted attribute value
1 | <svg viewBox="0 0 120 120">
> 2 | <circle fill="#ff0000" cx=60.444444" cy="60" r="50"/>
| ^
3 | </svg>
4 |
`);
});
test('supports errors without path', () => {
const svg = `<svg viewBox="0 0 120 120">
<circle/>
<circle/>
<circle/>
<circle/>
<circle/>
<circle/>
<circle/>
<circle/>
<circle/>
<circle fill="#ff0000" cx=60.444444" cy="60" r="50"/>
</svg>
`;
const { modernError: error } = optimize(svg);
expect(error.toString())
.toEqual(`SvgoParserError: <input>:11:29: Unquoted attribute value
9 | <circle/>
10 | <circle/>
> 11 | <circle fill="#ff0000" cx=60.444444" cy="60" r="50"/>
| ^
12 | </svg>
13 |
`);
});
test('slices long line in error code snippet', () => {
const svg = `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" viewBox="0 0 230 120">
<path d="M318.198 551.135 530.33 918.56l-289.778-77.646 38.823-144.889c77.646-289.778 294.98-231.543 256.156-86.655s178.51 203.124 217.334 58.235q58.234-217.334 250.955 222.534t579.555 155.292z stroke-width="1.5" fill="red" stroke="red" />
</svg>
`;
const { modernError: error } = optimize(svg);
expect(error.toString())
.toEqual(`SvgoParserError: <input>:2:211: Invalid attribute name
1 | …-0.dtd" viewBox="0 0 230 120">
> 2 | …7.334 250.955 222.534t579.555 155.292z stroke-width="1.5" fill="red" strok…
| ^
3 |
4 |
`);
});
test('provides legacy error message', () => {
const svg = `<svg viewBox="0 0 120 120">
<circle fill="#ff0000" cx=60.444444" cy="60" r="50"/>
</svg>
`;
const { error } = optimize(svg, { path: 'test.svg' });
expect(error)
.toEqual(`SvgoParserError: test.svg:2:29: Unquoted attribute value
1 | <svg viewBox="0 0 120 120">
> 2 | <circle fill="#ff0000" cx=60.444444" cy="60" r="50"/>
| ^
3 | </svg>
4 |
`);
});
test('multipass option should trigger plugins multiple times', () => {
const svg = `<svg id="abcdefghijklmnopqrstuvwxyz"></svg>`;
const list = [];
/**
* @type {Plugin<void>}
*/
const testPlugin = {
type: 'visitor',
name: 'testPlugin',
fn: (_root, _params, info) => {
list.push(info.multipassCount);
return {
element: {
enter: (node) => {
node.attributes.id = node.attributes.id.slice(1);
},
},
};
},
};
const { data } = optimize(svg, { multipass: true, plugins: [testPlugin] });
expect(list).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
expect(data).toEqual(`<svg id="klmnopqrstuvwxyz"/>`);
});
'use strict';
/**
* @typedef {import('./types').XastRoot} XastRoot
* @typedef {import('./types').XastElement} XastElement
*/
const { visit, visitSkip, detachNodeFromParent } = require('./xast.js');
/**
* @type {(children: Array<XastElement>) => XastRoot}
*/
const root = (children) => {
return { type: 'root', children };
};
/**
* @type {(
* name: string,
* attrs?: null | Record<string, string>,
* children?: Array<XastElement>
* ) => XastElement}
*/
const x = (name, attrs = null, children = []) => {
return { type: 'element', name, attributes: attrs || {}, children };
};
test('visit enters into nodes', () => {
const ast = root([x('g', null, [x('rect'), x('circle')]), x('ellipse')]);
/**
* @type {Array<string>}
*/
const entered = [];
visit(ast, {
root: {
enter: (node) => {
entered.push(node.type);
},
},
element: {
enter: (node) => {
entered.push(`${node.type}:${node.name}`);
},
},
});
expect(entered).toEqual([
'root',
'element:g',
'element:rect',
'element:circle',
'element:ellipse',
]);
});
test('visit exits from nodes', () => {
const ast = root([x('g', null, [x('rect'), x('circle')]), x('ellipse')]);
/**
* @type {Array<string>}
*/
const exited = [];
visit(ast, {
root: {
exit: (node) => {
exited.push(node.type);
},
},
element: {
exit: (node) => {
exited.push(`${node.type}:${node.name}`);
},
},
});
expect(exited).toEqual([
'element:rect',
'element:circle',
'element:g',
'element:ellipse',
'root',
]);
});
test('visit skips entering children if node is detached', () => {
const ast = root([x('g', null, [x('rect'), x('circle')]), x('ellipse')]);
/**
* @type {Array<string>}
*/
const entered = [];
visit(ast, {
element: {
enter: (node, parentNode) => {
entered.push(node.name);
if (node.name === 'g') {
detachNodeFromParent(node, parentNode);
}
},
},
});
expect(entered).toEqual(['g', 'ellipse']);
expect(ast).toEqual(root([x('ellipse')]));
});
test('visit skips entering children when symbol is passed', () => {
const ast = root([x('g', null, [x('rect'), x('circle')]), x('ellipse')]);
/**
* @type {Array<string>}
*/
const entered = [];
visit(ast, {
element: {
enter: (node) => {
entered.push(node.name);
if (node.name === 'g') {
return visitSkip;
}
},
},
});
expect(entered).toEqual(['g', 'ellipse']);
expect(ast).toEqual(
root([x('g', null, [x('rect'), x('circle')]), x('ellipse')])
);
});