+1
-1
| { | ||
| "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", |
-193
| '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'/); | ||
| } | ||
| }); | ||
| }); |
-387
| '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"/>`); | ||
| }); |
-122
| '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')]) | ||
| ); | ||
| }); |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
0
-100%977264
-3.43%80
-5.88%15183
-7.09%