From 16c6977215d2d45dd83cc4448b804ae52513b50f Mon Sep 17 00:00:00 2001 From: Kendell R Date: Fri, 22 Dec 2023 22:21:14 -0500 Subject: [PATCH 01/14] chore: speedier regression testing (#1897) --- test/regression.js | 100 +++++++++++++++++++-------------------------- 1 file changed, 41 insertions(+), 59 deletions(-) diff --git a/test/regression.js b/test/regression.js index fc65d7006..e89dfd3f3 100644 --- a/test/regression.js +++ b/test/regression.js @@ -3,26 +3,17 @@ const fs = require('fs'); const path = require('path'); const http = require('http'); +const os = require('os'); const { chromium } = require('playwright'); const { PNG } = require('pngjs'); const pixelmatch = require('pixelmatch'); const { optimize } = require('../lib/svgo.js'); -const chunkInto = (array, chunksCount) => { - // take upper bound to include tail - const chunkSize = Math.ceil(array.length / chunksCount); - const result = []; - for (let i = 0; i < chunksCount; i += 1) { - const offset = i * chunkSize; - result.push(array.slice(offset, offset + chunkSize)); - } - return result; -}; - const runTests = async ({ list }) => { let skipped = 0; let mismatched = 0; let passed = 0; + list.reverse(); console.info('Start browser...'); const processFile = async (page, name) => { if ( @@ -48,8 +39,6 @@ const runTests = async ({ list }) => { skipped += 1; return; } - const width = 960; - const height = 720; await page.goto(`http://localhost:5000/original/${name}`); await page.setViewportSize({ width, height }); const originalBuffer = await page.screenshot({ @@ -89,17 +78,19 @@ const runTests = async ({ list }) => { } } }; + const worker = async () => { + let item; + const page = await context.newPage(); + while ((item = list.pop())) { + await processFile(page, item); + } + await page.close(); + }; + const browser = await chromium.launch(); const context = await browser.newContext({ javaScriptEnabled: false }); - const chunks = chunkInto(list, 8); await Promise.all( - chunks.map(async (chunk) => { - const page = await context.newPage(); - for (const name of chunk) { - await processFile(page, name); - } - await page.close(); - }), + Array.from(new Array(os.cpus().length * 2), () => worker()), ); await browser.close(); console.info(`Skipped: ${skipped}`); @@ -124,57 +115,48 @@ const readdirRecursive = async (absolute, relative = '') => { return result; }; +const width = 960; +const height = 720; (async () => { try { const start = process.hrtime.bigint(); const fixturesDir = path.join(__dirname, 'regression-fixtures'); const list = await readdirRecursive(fixturesDir); - const originalFiles = new Map(); - const optimizedFiles = new Map(); - // read original and optimize - let failed = 0; - for (const name of list) { + // setup server + const server = http.createServer(async (req, res) => { + const name = req.url.slice(req.url.indexOf('/', 1)); + let file; try { - const file = path.join(fixturesDir, name); - const original = await fs.promises.readFile(file, 'utf-8'); - const result = optimize(original, { path: name, floatPrecision: 4 }); - if (result.error) { - console.error(result.error); - console.error(`File: ${name}`); - failed += 1; - } else { - originalFiles.set(name, original); - optimizedFiles.set(name, result.data); - } + file = await fs.promises.readFile( + path.join(fixturesDir, name), + 'utf-8', + ); } catch (error) { - console.error(error); - console.error(`File: ${name}`); - failed += 1; + res.statusCode = 404; + res.end(); + return; } - } - if (failed !== 0) { - throw Error(`Failed to optimize ${failed} cases`); - } - // setup server - const server = http.createServer((req, res) => { + if (req.url.startsWith('/original/')) { - const name = req.url.slice('/original/'.length); - if (originalFiles.has(name)) { - res.setHeader('Content-Type', 'image/svg+xml'); - res.end(originalFiles.get(name)); - return; - } + res.setHeader('Content-Type', 'image/svg+xml'); + res.end(file); + return; } if (req.url.startsWith('/optimized/')) { - const name = req.url.slice('/optimized/'.length); - if (optimizedFiles.has(name)) { - res.setHeader('Content-Type', 'image/svg+xml'); - res.end(optimizedFiles.get(name)); - return; + const optimized = optimize(file, { + path: name, + floatPrecision: 4, + }); + if (optimized.error) { + throw new Error(`Failed to optimize ${name}`, { + cause: optimized.error, + }); } + res.setHeader('Content-Type', 'image/svg+xml'); + res.end(optimized.data); + return; } - res.statusCode = 404; - res.end(); + throw new Error(`unknown path ${req.url}`); }); await new Promise((resolve) => { server.listen(5000, resolve); From 2c408ce56fef896dfce9e6f596a32e2be23765f4 Mon Sep 17 00:00:00 2001 From: Seth Falco Date: Sat, 23 Dec 2023 06:56:46 +0000 Subject: [PATCH 02/14] refactor: improve regression testing (#1898) --- .github/workflows/main.yml | 2 + package.json | 6 +- test/browser.js | 8 +- test/regression-extract.js | 39 ++++-- test/regression.js | 118 +++++++----------- yarn.lock | 238 ++++++------------------------------- 6 files changed, 118 insertions(+), 293 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ea21e137f..42af4a390 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,6 +36,7 @@ jobs: node-version: ${{ env.NODE }} cache: yarn - run: yarn install + - run: yarn playwright install --with-deps chromium - run: yarn test-regression test: name: ${{ matrix.os }} Node.js ${{ matrix.node-version }} @@ -58,5 +59,6 @@ jobs: node-version: ${{ matrix.node-version }} cache: yarn - run: yarn install + - run: yarn playwright install --with-deps chromium - run: yarn test - run: yarn test-browser diff --git a/package.json b/package.json index a0d699835..e0b4873c4 100644 --- a/package.json +++ b/package.json @@ -96,9 +96,9 @@ "eslint": "^8.55.0", "jest": "^29.5.5", "node-fetch": "^2.7.0", - "pixelmatch": "^5.2.1", - "playwright": "^1.14.1", - "pngjs": "^6.0.0", + "pixelmatch": "^5.3.0", + "playwright": "^1.40.1", + "pngjs": "^7.0.0", "prettier": "^3.1.1", "rollup": "^2.79.1", "rollup-plugin-terser": "^7.0.2", diff --git a/test/browser.js b/test/browser.js index 2d654521f..ac120674b 100644 --- a/test/browser.js +++ b/test/browser.js @@ -1,6 +1,6 @@ -const fs = require('fs'); -const http = require('http'); const assert = require('assert'); +const fs = require('node:fs/promises'); +const http = require('http'); const { chromium } = require('playwright'); const fixture = ` @@ -33,14 +33,14 @@ globalThis.result = result.data; `; -const server = http.createServer((req, res) => { +const server = http.createServer(async (req, res) => { if (req.url === '/') { res.setHeader('Content-Type', 'text/html'); res.end(content); } if (req.url === '/svgo.browser.js') { res.setHeader('Content-Type', 'application/javascript'); - res.end(fs.readFileSync('./dist/svgo.browser.js')); + res.end(await fs.readFile('./dist/svgo.browser.js')); } res.end(); }); diff --git a/test/regression-extract.js b/test/regression-extract.js index 38fb0d00c..2c0e6232f 100644 --- a/test/regression-extract.js +++ b/test/regression-extract.js @@ -2,14 +2,33 @@ const fs = require('fs'); const path = require('path'); +const stream = require('stream'); const util = require('util'); const zlib = require('zlib'); -const stream = require('stream'); const { default: fetch } = require('node-fetch'); const tarStream = require('tar-stream'); const pipeline = util.promisify(stream.pipeline); +const exclude = [ + // animated + 'svg/filters-light-04-f.svg', + 'svg/filters-composite-05-f.svg', + // messed gradients + 'svg/pservers-grad-18-b.svg', + // removing wrapping breaks :first-child pseudo-class + 'svg/styling-pres-04-f.svg', + // rect is converted to path which matches wrong styles + 'svg/styling-css-08-f.svg', + // complex selectors are messed because of converting shapes to paths + 'svg/struct-use-10-f.svg', + 'svg/struct-use-11-f.svg', + 'svg/styling-css-01-b.svg', + 'svg/styling-css-04-f.svg', + // strange artifact breaks inconsistently breaks regression tests + 'svg/filters-conv-05-f.svg', +]; + /** * @param {string} url * @param {string} baseDir @@ -18,15 +37,21 @@ const pipeline = util.promisify(stream.pipeline); const extractTarGz = async (url, baseDir, include) => { const extract = tarStream.extract(); extract.on('entry', async (header, stream, next) => { + const name = header.name; + try { - if (include == null || include.test(header.name)) { - if (header.name.endsWith('.svg')) { - const file = path.join(baseDir, header.name); + if (include == null || include.test(name)) { + if ( + name.endsWith('.svg') && + !exclude.includes(name) && + !name.startsWith('svg/animate-') + ) { + const file = path.join(baseDir, name); await fs.promises.mkdir(path.dirname(file), { recursive: true }); await pipeline(stream, fs.createWriteStream(file)); - } else if (header.name.endsWith('.svgz')) { + } else if (name.endsWith('.svgz')) { // .svgz -> .svg - const file = path.join(baseDir, header.name.slice(0, -1)); + const file = path.join(baseDir, name.slice(0, -1)); await fs.promises.mkdir(path.dirname(file), { recursive: true }); await pipeline( stream, @@ -48,7 +73,7 @@ const extractTarGz = async (url, baseDir, include) => { (async () => { try { - console.info('Download W3C SVG 1.1 Test Suite and extract svg files'); + console.info('Download W3C SVG 1.1 Test Suite and extract SVG files'); await extractTarGz( 'https://www.w3.org/Graphics/SVG/Test/20110816/archives/W3C_SVG_11_TestSuite.tar.gz', path.join(__dirname, 'regression-fixtures', 'w3c-svg-11-test-suite'), diff --git a/test/regression.js b/test/regression.js index e89dfd3f3..c76e3edd2 100644 --- a/test/regression.js +++ b/test/regression.js @@ -1,57 +1,49 @@ 'use strict'; -const fs = require('fs'); -const path = require('path'); +/** + * @typedef {import('playwright').Page} Page + * @typedef {import('playwright').PageScreenshotOptions} PageScreenshotOptions + */ + +const fs = require('node:fs/promises'); const http = require('http'); const os = require('os'); +const path = require('path'); +const pixelmatch = require('pixelmatch'); const { chromium } = require('playwright'); const { PNG } = require('pngjs'); -const pixelmatch = require('pixelmatch'); const { optimize } = require('../lib/svgo.js'); -const runTests = async ({ list }) => { - let skipped = 0; +const width = 960; +const height = 720; + +/** @type {PageScreenshotOptions} */ +const screenshotOptions = { + omitBackground: true, + clip: { x: 0, y: 0, width, height }, + animations: 'disabled', +}; + +/** + * @param {string[]} list + * @returns {Promise} + */ +const runTests = async (list) => { let mismatched = 0; let passed = 0; - list.reverse(); - console.info('Start browser...'); + console.info('Start browser…'); + /** + * @param {Page} page + * @param {string} name + */ const processFile = async (page, name) => { - if ( - // animated - name.startsWith('w3c-svg-11-test-suite/svg/animate-') || - name === 'w3c-svg-11-test-suite/svg/filters-light-04-f.svg' || - name === 'w3c-svg-11-test-suite/svg/filters-composite-05-f.svg' || - // messed gradients - name === 'w3c-svg-11-test-suite/svg/pservers-grad-18-b.svg' || - // removing wrapping breaks :first-child pseudo-class - name === 'w3c-svg-11-test-suite/svg/styling-pres-04-f.svg' || - // rect is converted to path which matches wrong styles - name === 'w3c-svg-11-test-suite/svg/styling-css-08-f.svg' || - // complex selectors are messed because of converting shapes to paths - name === 'w3c-svg-11-test-suite/svg/struct-use-10-f.svg' || - name === 'w3c-svg-11-test-suite/svg/struct-use-11-f.svg' || - name === 'w3c-svg-11-test-suite/svg/styling-css-01-b.svg' || - name === 'w3c-svg-11-test-suite/svg/styling-css-04-f.svg' || - // strange artifact breaks inconsistently breaks regression tests - name === 'w3c-svg-11-test-suite/svg/filters-conv-05-f.svg' - ) { - console.info(`${name} is skipped`); - skipped += 1; - return; - } await page.goto(`http://localhost:5000/original/${name}`); - await page.setViewportSize({ width, height }); - const originalBuffer = await page.screenshot({ - omitBackground: true, - clip: { x: 0, y: 0, width, height }, - }); + const originalBuffer = await page.screenshot(screenshotOptions); await page.goto(`http://localhost:5000/optimized/${name}`); - const optimizedBuffer = await page.screenshot({ - omitBackground: true, - clip: { x: 0, y: 0, width, height }, - }); + const optimizedBufferPromise = page.screenshot(screenshotOptions); + const originalPng = PNG.sync.read(originalBuffer); - const optimizedPng = PNG.sync.read(optimizedBuffer); + const optimizedPng = PNG.sync.read(await optimizedBufferPromise); const diff = new PNG({ width, height }); const matched = pixelmatch( originalPng.data, @@ -63,9 +55,9 @@ const runTests = async ({ list }) => { // ignore small aliasing issues if (matched <= 4) { console.info(`${name} is passed`); - passed += 1; + passed++; } else { - mismatched += 1; + mismatched++; console.error(`${name} is mismatched`); if (process.env.NO_DIFF == null) { const file = path.join( @@ -73,14 +65,15 @@ const runTests = async ({ list }) => { 'regression-diffs', `${name}.diff.png`, ); - await fs.promises.mkdir(path.dirname(file), { recursive: true }); - await fs.promises.writeFile(file, PNG.sync.write(diff)); + await fs.mkdir(path.dirname(file), { recursive: true }); + await fs.writeFile(file, PNG.sync.write(diff)); } } }; const worker = async () => { let item; const page = await context.newPage(); + await page.setViewportSize({ width, height }); while ((item = list.pop())) { await processFile(page, item); } @@ -93,44 +86,21 @@ const runTests = async ({ list }) => { Array.from(new Array(os.cpus().length * 2), () => worker()), ); await browser.close(); - console.info(`Skipped: ${skipped}`); console.info(`Mismatched: ${mismatched}`); console.info(`Passed: ${passed}`); return mismatched === 0; }; -const readdirRecursive = async (absolute, relative = '') => { - let result = []; - const list = await fs.promises.readdir(absolute, { withFileTypes: true }); - for (const item of list) { - const itemAbsolute = path.join(absolute, item.name); - const itemRelative = path.join(relative, item.name); - if (item.isDirectory()) { - const itemList = await readdirRecursive(itemAbsolute, itemRelative); - result = [...result, ...itemList]; - } else if (item.name.endsWith('.svg')) { - result = [...result, itemRelative]; - } - } - return result; -}; - -const width = 960; -const height = 720; (async () => { try { const start = process.hrtime.bigint(); const fixturesDir = path.join(__dirname, 'regression-fixtures'); - const list = await readdirRecursive(fixturesDir); - // setup server + const filesPromise = fs.readdir(fixturesDir, { recursive: true }); const server = http.createServer(async (req, res) => { const name = req.url.slice(req.url.indexOf('/', 1)); let file; try { - file = await fs.promises.readFile( - path.join(fixturesDir, name), - 'utf-8', - ); + file = await fs.readFile(path.join(fixturesDir, name), 'utf-8'); } catch (error) { res.statusCode = 404; res.end(); @@ -144,14 +114,8 @@ const height = 720; } if (req.url.startsWith('/optimized/')) { const optimized = optimize(file, { - path: name, floatPrecision: 4, }); - if (optimized.error) { - throw new Error(`Failed to optimize ${name}`, { - cause: optimized.error, - }); - } res.setHeader('Content-Type', 'image/svg+xml'); res.end(optimized.data); return; @@ -161,9 +125,9 @@ const height = 720; await new Promise((resolve) => { server.listen(5000, resolve); }); - const passed = await runTests({ list }); + const list = (await filesPromise).filter((name) => name.endsWith('.svg')); + const passed = await runTests(list); server.close(); - // compute time const end = process.hrtime.bigint(); const diff = (end - start) / BigInt(1e6); if (passed) { diff --git a/yarn.lock b/yarn.lock index 092db56e8..49e179584 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1082,15 +1082,6 @@ __metadata: languageName: node linkType: hard -"@types/yauzl@npm:^2.9.1": - version: 2.9.2 - resolution: "@types/yauzl@npm:2.9.2" - dependencies: - "@types/node": "*" - checksum: dfb49abe82605615712fc694eaa4f7068fe30aa03f38c085e2c2e74408beaad30471d36da9654a811482ece2ea4405575fd99b19c0aa327ed2a9736b554bbf43 - languageName: node - linkType: hard - "@ungap/structured-clone@npm:^1.2.0": version: 1.2.0 resolution: "@ungap/structured-clone@npm:1.2.0" @@ -1409,13 +1400,6 @@ __metadata: languageName: node linkType: hard -"buffer-crc32@npm:~0.2.3": - version: 0.2.13 - resolution: "buffer-crc32@npm:0.2.13" - checksum: 06252347ae6daca3453b94e4b2f1d3754a3b146a111d81c68924c22d91889a40623264e95e67955b1cb4a68cbedf317abeabb5140a9766ed248973096db5ce1c - languageName: node - linkType: hard - "buffer-from@npm:^1.0.0": version: 1.1.2 resolution: "buffer-from@npm:1.1.2" @@ -1611,13 +1595,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:^6.1.0": - version: 6.2.1 - resolution: "commander@npm:6.2.1" - checksum: d7090410c0de6bc5c67d3ca41c41760d6d268f3c799e530aafb73b7437d1826bbf0d2a3edac33f8b57cc9887b4a986dce307fa5557e109be40eadb7c43b21742 - languageName: node - linkType: hard - "commander@npm:^7.2.0": version: 7.2.0 resolution: "commander@npm:7.2.0" @@ -1904,15 +1881,6 @@ __metadata: languageName: node linkType: hard -"end-of-stream@npm:^1.1.0": - version: 1.4.4 - resolution: "end-of-stream@npm:1.4.4" - dependencies: - once: ^1.4.0 - checksum: 530a5a5a1e517e962854a31693dbb5c0b2fc40b46dad2a56a2deec656ca040631124f4795823acc68238147805f8b021abbe221f4afed5ef3c8e8efc2024908b - languageName: node - linkType: hard - "entities@npm:^4.2.0": version: 4.4.0 resolution: "entities@npm:4.4.0" @@ -2140,23 +2108,6 @@ __metadata: languageName: node linkType: hard -"extract-zip@npm:^2.0.1": - version: 2.0.1 - resolution: "extract-zip@npm:2.0.1" - dependencies: - "@types/yauzl": ^2.9.1 - debug: ^4.1.1 - get-stream: ^5.1.0 - yauzl: ^2.10.0 - dependenciesMeta: - "@types/yauzl": - optional: true - bin: - extract-zip: cli.js - checksum: 8cbda9debdd6d6980819cc69734d874ddd71051c9fe5bde1ef307ebcedfe949ba57b004894b585f758b7c9eeeea0e3d87f2dda89b7d25320459c2c9643ebb635 - languageName: node - linkType: hard - "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -2216,15 +2167,6 @@ __metadata: languageName: node linkType: hard -"fd-slicer@npm:~1.1.0": - version: 1.1.0 - resolution: "fd-slicer@npm:1.1.0" - dependencies: - pend: ~1.2.0 - checksum: c8585fd5713f4476eb8261150900d2cb7f6ff2d87f8feb306ccc8a1122efd152f1783bdb2b8dc891395744583436bfd8081d8e63ece0ec8687eeefea394d4ff2 - languageName: node - linkType: hard - "file-entry-cache@npm:^6.0.1": version: 6.0.1 resolution: "file-entry-cache@npm:6.0.1" @@ -2296,7 +2238,7 @@ __metadata: languageName: node linkType: hard -"fsevents@^2.3.2, fsevents@~2.3.2": +"fsevents@^2.3.2, fsevents@npm:2.3.2, fsevents@~2.3.2": version: 2.3.2 resolution: "fsevents@npm:2.3.2" dependencies: @@ -2306,7 +2248,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@^2.3.2#~builtin, fsevents@patch:fsevents@~2.3.2#~builtin": +"fsevents@patch:fsevents@2.3.2#~builtin, fsevents@patch:fsevents@^2.3.2#~builtin, fsevents@patch:fsevents@~2.3.2#~builtin": version: 2.3.2 resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin::version=2.3.2&hash=18f3a7" dependencies: @@ -2359,15 +2301,6 @@ __metadata: languageName: node linkType: hard -"get-stream@npm:^5.1.0": - version: 5.2.0 - resolution: "get-stream@npm:5.2.0" - dependencies: - pump: ^3.0.0 - checksum: 8bc1a23174a06b2b4ce600df38d6c98d2ef6d84e020c1ddad632ad75bac4e092eeb40e4c09e0761c35fc2dbc5e7fff5dab5e763a383582c4a167dd69a905bd12 - languageName: node - linkType: hard - "get-stream@npm:^6.0.0": version: 6.0.1 resolution: "get-stream@npm:6.0.1" @@ -3266,13 +3199,6 @@ __metadata: languageName: node linkType: hard -"jpeg-js@npm:^0.4.2": - version: 0.4.4 - resolution: "jpeg-js@npm:0.4.4" - checksum: bd7cb61aa8df40a9ee2c2106839c3df6054891e56cfc22c0ac581402e06c6295f962a4754b0b2ac50a401789131b1c6dc9df8d24400f1352168be1894833c590 - languageName: node - linkType: hard - "js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -3488,15 +3414,6 @@ __metadata: languageName: node linkType: hard -"mime@npm:^2.4.6": - version: 2.5.2 - resolution: "mime@npm:2.5.2" - bin: - mime: cli.js - checksum: dd3c93d433d41a09f6a1cfa969b653b769899f3bd573e7bfcea33bdc8b0cc4eba57daa2f95937369c2bd2b6d39d62389b11a4309fe40d1d3a1b736afdedad0ff - languageName: node - linkType: hard - "mimic-fn@npm:^2.1.0": version: 2.1.0 resolution: "mimic-fn@npm:2.1.0" @@ -3723,7 +3640,7 @@ __metadata: languageName: node linkType: hard -"once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0": +"once@npm:^1.3.0": version: 1.4.0 resolution: "once@npm:1.4.0" dependencies: @@ -3863,13 +3780,6 @@ __metadata: languageName: node linkType: hard -"pend@npm:~1.2.0": - version: 1.2.0 - resolution: "pend@npm:1.2.0" - checksum: 6c72f5243303d9c60bd98e6446ba7d30ae29e3d56fdb6fae8767e8ba6386f33ee284c97efe3230a0d0217e2b1723b8ab490b1bbf34fcbb2180dbc8a9de47850d - languageName: node - linkType: hard - "picocolors@npm:^1.0.0": version: 1.0.0 resolution: "picocolors@npm:1.0.0" @@ -3891,14 +3801,14 @@ __metadata: languageName: node linkType: hard -"pixelmatch@npm:^5.2.1": - version: 5.2.1 - resolution: "pixelmatch@npm:5.2.1" +"pixelmatch@npm:^5.3.0": + version: 5.3.0 + resolution: "pixelmatch@npm:5.3.0" dependencies: - pngjs: ^4.0.1 + pngjs: ^6.0.0 bin: pixelmatch: bin/pixelmatch - checksum: 0ec7a87168e51b80812d1c39fe1a278e2266dc1e9c426418c2a9d7f0c6465de3c03c51dbf7e6b97c5ba72a043ec3fb576571cdde1f88b12ef0851bf9bfd16da0 + checksum: f542713d89536551181ad9ddb666a1792ba00a8632d831093232a075cb3ccac05856e7a453ed7d0a41aaef64dcb5962e8ae5cbe646dd2761790d8ee51b0a0743 languageName: node linkType: hard @@ -3911,41 +3821,27 @@ __metadata: languageName: node linkType: hard -"playwright@npm:^1.14.1": - version: 1.14.1 - resolution: "playwright@npm:1.14.1" - dependencies: - commander: ^6.1.0 - debug: ^4.1.1 - extract-zip: ^2.0.1 - https-proxy-agent: ^5.0.0 - jpeg-js: ^0.4.2 - mime: ^2.4.6 - pngjs: ^5.0.0 - progress: ^2.0.3 - proper-lockfile: ^4.1.1 - proxy-from-env: ^1.1.0 - rimraf: ^3.0.2 - stack-utils: ^2.0.3 - ws: ^7.4.6 - yazl: ^2.5.1 +"playwright-core@npm:1.40.1": + version: 1.40.1 + resolution: "playwright-core@npm:1.40.1" bin: - playwright: lib/cli/cli.js - checksum: fbf0cd58f3d9eed01c2b5332bf388c89474494bff9027191baf27be2d0dba74af0c506b666ddab356e2f697c3df9ce158969d0b335be352a41710ce07db88b20 + playwright-core: cli.js + checksum: 84d92fb9b86e3c225b16b6886bf858eb5059b4e60fa1205ff23336e56a06dcb2eac62650992dede72f406c8e70a7b6a5303e511f9b4bc0b85022ede356a01ee0 languageName: node linkType: hard -"pngjs@npm:^4.0.1": - version: 4.0.1 - resolution: "pngjs@npm:4.0.1" - checksum: 9497e08a6c2d850630ba7c8d3738fd36c9db1af7ee8b8c2d4b664e450807a280936dfa1489deb60e6943b968bedd58c9aa93def25a765579d745ea44467fc47f - languageName: node - linkType: hard - -"pngjs@npm:^5.0.0": - version: 5.0.0 - resolution: "pngjs@npm:5.0.0" - checksum: 04e912cc45fb9601564e2284efaf0c5d20d131d9b596244f8a6789fc6cdb6b18d2975a6bbf7a001858d7e159d5c5c5dd7b11592e97629b7137f7f5cef05904c8 +"playwright@npm:^1.40.1": + version: 1.40.1 + resolution: "playwright@npm:1.40.1" + dependencies: + fsevents: 2.3.2 + playwright-core: 1.40.1 + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 9e36791c1b4a649c104aa365fdd9d049924eeb518c5967c0e921aa38b9b00994aa6ee54784d6c2af194b3b494b6f69772673081ef53c6c4a4b2065af9955c4ba languageName: node linkType: hard @@ -3956,6 +3852,13 @@ __metadata: languageName: node linkType: hard +"pngjs@npm:^7.0.0": + version: 7.0.0 + resolution: "pngjs@npm:7.0.0" + checksum: b19a018930d27de26229c1b3ff250b3a25d09caa22cbb0b0459987d91eb0a560a18ab5d67da45a38ed7514140f26d1db58de83c31159ec101f2bb270a3c707f1 + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -3990,13 +3893,6 @@ __metadata: languageName: node linkType: hard -"progress@npm:^2.0.3": - version: 2.0.3 - resolution: "progress@npm:2.0.3" - checksum: f67403fe7b34912148d9252cb7481266a354bd99ce82c835f79070643bb3c6583d10dbcfda4d41e04bbc1d8437e9af0fb1e1f2135727878f5308682a579429b7 - languageName: node - linkType: hard - "promise-inflight@npm:^1.0.1": version: 1.0.1 resolution: "promise-inflight@npm:1.0.1" @@ -4024,34 +3920,6 @@ __metadata: languageName: node linkType: hard -"proper-lockfile@npm:^4.1.1": - version: 4.1.2 - resolution: "proper-lockfile@npm:4.1.2" - dependencies: - graceful-fs: ^4.2.4 - retry: ^0.12.0 - signal-exit: ^3.0.2 - checksum: 00078ee6a61c216a56a6140c7d2a98c6c733b3678503002dc073ab8beca5d50ca271de4c85fca13b9b8ee2ff546c36674d1850509b84a04a5d0363bcb8638939 - languageName: node - linkType: hard - -"proxy-from-env@npm:^1.1.0": - version: 1.1.0 - resolution: "proxy-from-env@npm:1.1.0" - checksum: ed7fcc2ba0a33404958e34d95d18638249a68c430e30fcb6c478497d72739ba64ce9810a24f53a7d921d0c065e5b78e3822759800698167256b04659366ca4d4 - languageName: node - linkType: hard - -"pump@npm:^3.0.0": - version: 3.0.0 - resolution: "pump@npm:3.0.0" - dependencies: - end-of-stream: ^1.1.0 - once: ^1.3.1 - checksum: e42e9229fba14732593a718b04cb5e1cfef8254544870997e0ecd9732b189a48e1256e4e5478148ecb47c8511dca2b09eae56b4d0aad8009e6fac8072923cfc9 - languageName: node - linkType: hard - "punycode@npm:^2.1.0": version: 2.1.1 resolution: "punycode@npm:2.1.1" @@ -4303,7 +4171,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": +"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 @@ -4574,9 +4442,9 @@ __metadata: jest: ^29.5.5 node-fetch: ^2.7.0 picocolors: ^1.0.0 - pixelmatch: ^5.2.1 - playwright: ^1.14.1 - pngjs: ^6.0.0 + pixelmatch: ^5.3.0 + playwright: ^1.40.1 + pngjs: ^7.0.0 prettier: ^3.1.1 rollup: ^2.79.1 rollup-plugin-terser: ^7.0.2 @@ -4857,21 +4725,6 @@ __metadata: languageName: node linkType: hard -"ws@npm:^7.4.6": - version: 7.5.5 - resolution: "ws@npm:7.5.5" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: bd2b437256012af526c69c03d6670a132e7ab0fe5853f3b7092826acea4203fad4ee2a8d0d9bd44834b2b968e747bf34f753ab535f4a3edf40d262da4b1d0805 - languageName: node - linkType: hard - "y18n@npm:^5.0.5": version: 5.0.8 resolution: "y18n@npm:5.0.8" @@ -4908,25 +4761,6 @@ __metadata: languageName: node linkType: hard -"yauzl@npm:^2.10.0": - version: 2.10.0 - resolution: "yauzl@npm:2.10.0" - dependencies: - buffer-crc32: ~0.2.3 - fd-slicer: ~1.1.0 - checksum: 7f21fe0bbad6e2cb130044a5d1d0d5a0e5bf3d8d4f8c4e6ee12163ce798fee3de7388d22a7a0907f563ac5f9d40f8699a223d3d5c1718da90b0156da6904022b - languageName: node - linkType: hard - -"yazl@npm:^2.5.1": - version: 2.5.1 - resolution: "yazl@npm:2.5.1" - dependencies: - buffer-crc32: ~0.2.3 - checksum: daec5154b5485d8621bfea359e905ddca0b2f068430a4aa0a802bf5d67391157a383e0c2767acccbf5964264851da643bc740155a9458e2d8dce55b94c1cc2ed - languageName: node - linkType: hard - "yocto-queue@npm:^0.1.0": version: 0.1.0 resolution: "yocto-queue@npm:0.1.0" From a7859eb177016f722d42a823e3837a2c1bd5804e Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sat, 23 Dec 2023 20:47:29 -0500 Subject: [PATCH 03/14] fix(convertPathData): fix some weird behavior (#1867) --- plugins/convertPathData.js | 62 +++++++++++++++-------------- test/coa/testSvg/test.1.svg | 2 +- test/coa/testSvg/test.svg | 2 +- test/plugins/convertPathData.30.svg | 13 ++++++ test/plugins/convertPathData.31.svg | 13 ++++++ test/svgo/plugins-order.svg | 2 +- 6 files changed, 61 insertions(+), 33 deletions(-) create mode 100644 test/plugins/convertPathData.30.svg create mode 100644 test/plugins/convertPathData.31.svg diff --git a/plugins/convertPathData.js b/plugins/convertPathData.js index b4ef21147..b7b6beefc 100644 --- a/plugins/convertPathData.js +++ b/plugins/convertPathData.js @@ -678,24 +678,6 @@ function filters( } } - // convert going home to z - // m 0 0 h 5 v 5 l -5 -5 -> m 0 0 h 5 v 5 z - if ( - params.convertToZ && - (isSafeToUseZ || next?.command === 'Z' || next?.command === 'z') && - (command === 'l' || command === 'h' || command === 'v') - ) { - if ( - // @ts-ignore - Math.abs(pathBase[0] - item.coords[0]) < error && - // @ts-ignore - Math.abs(pathBase[1] - item.coords[1]) < error - ) { - command = 'z'; - data = []; - } - } - // collapse repeated commands // h 20 h 30 -> h 50 if ( @@ -733,9 +715,9 @@ function filters( // @ts-ignore prev.command === 'c' && // @ts-ignore - data[0] === -(prev.args[2] - prev.args[4]) && + Math.abs(data[0] - -(prev.args[2] - prev.args[4])) < error && // @ts-ignore - data[1] === -(prev.args[3] - prev.args[5]) + Math.abs(data[1] - -(prev.args[3] - prev.args[5])) < error ) { command = 's'; data = data.slice(2); @@ -746,9 +728,9 @@ function filters( // @ts-ignore prev.command === 's' && // @ts-ignore - data[0] === -(prev.args[0] - prev.args[2]) && + Math.abs(data[0] - -(prev.args[0] - prev.args[2])) < error && // @ts-ignore - data[1] === -(prev.args[1] - prev.args[3]) + Math.abs(data[1] - -(prev.args[1] - prev.args[3])) < error ) { command = 's'; data = data.slice(2); @@ -760,8 +742,8 @@ function filters( prev.command !== 'c' && // @ts-ignore prev.command !== 's' && - data[0] === 0 && - data[1] === 0 + Math.abs(data[0]) < error && + Math.abs(data[1]) < error ) { command = 's'; data = data.slice(2); @@ -775,9 +757,9 @@ function filters( // @ts-ignore prev.command === 'q' && // @ts-ignore - data[0] === prev.args[2] - prev.args[0] && + Math.abs(data[0] - (prev.args[2] - prev.args[0])) < error && // @ts-ignore - data[1] === prev.args[3] - prev.args[1] + Math.abs(data[1] - (prev.args[3] - prev.args[1])) < error ) { command = 't'; data = data.slice(2); @@ -788,9 +770,9 @@ function filters( // @ts-ignore prev.command === 't' && // @ts-ignore - data[2] === prev.args[0] && + Math.abs(data[2] - prev.args[0]) < error && // @ts-ignore - data[3] === prev.args[1] + Math.abs(data[3] - prev.args[1]) < error ) { command = 't'; data = data.slice(2); @@ -826,6 +808,24 @@ function filters( } } + // convert going home to z + // m 0 0 h 5 v 5 l -5 -5 -> m 0 0 h 5 v 5 z + if ( + params.convertToZ && + (isSafeToUseZ || next?.command === 'Z' || next?.command === 'z') && + (command === 'l' || command === 'h' || command === 'v') + ) { + if ( + // @ts-ignore + Math.abs(pathBase[0] - item.coords[0]) < error && + // @ts-ignore + Math.abs(pathBase[1] - item.coords[1]) < error + ) { + command = 'z'; + data = []; + } + } + item.command = command; item.args = data; } else { @@ -870,7 +870,8 @@ function convertToMixed(path, params) { var command = item.command, data = item.args, - adata = data.slice(); + adata = data.slice(), + rdata = data.slice(); if ( command === 'm' || @@ -898,9 +899,10 @@ function convertToMixed(path, params) { } roundData(adata); + roundData(rdata); var absoluteDataStr = cleanupOutData(adata, params), - relativeDataStr = cleanupOutData(data, params); + relativeDataStr = cleanupOutData(rdata, params); // Convert to absolute coordinates if it's shorter or forceAbsolutePath is true. // v-20 -> V0 diff --git a/test/coa/testSvg/test.1.svg b/test/coa/testSvg/test.1.svg index 94848f6de..d5f98e04d 100644 --- a/test/coa/testSvg/test.1.svg +++ b/test/coa/testSvg/test.1.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/test/coa/testSvg/test.svg b/test/coa/testSvg/test.svg index 94848f6de..d5f98e04d 100644 --- a/test/coa/testSvg/test.svg +++ b/test/coa/testSvg/test.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/test/plugins/convertPathData.30.svg b/test/plugins/convertPathData.30.svg new file mode 100644 index 000000000..1d29829d6 --- /dev/null +++ b/test/plugins/convertPathData.30.svg @@ -0,0 +1,13 @@ +Should use relative instead of absolute here. (Checking for proper rounding of relative data) + +=== + + + + + +@@@ + + + + \ No newline at end of file diff --git a/test/plugins/convertPathData.31.svg b/test/plugins/convertPathData.31.svg new file mode 100644 index 000000000..9a4d43348 --- /dev/null +++ b/test/plugins/convertPathData.31.svg @@ -0,0 +1,13 @@ +Should have no commands here. (Checking for removing useless before trying to convert to z) + +=== + + + + + +@@@ + + + + \ No newline at end of file diff --git a/test/svgo/plugins-order.svg b/test/svgo/plugins-order.svg index 038d32722..6feaad859 100644 --- a/test/svgo/plugins-order.svg +++ b/test/svgo/plugins-order.svg @@ -8,4 +8,4 @@ @@@ - + From a3da8b3a76cee22bc650801b860e07629c68429d Mon Sep 17 00:00:00 2001 From: Seth Falco Date: Sun, 24 Dec 2023 01:54:17 +0000 Subject: [PATCH 04/14] refactor: use set in _collections (#1899) --- lib/parser.js | 2 +- lib/stringifier.js | 2 +- lib/style.js | 46 +- lib/svgo/tools.js | 2 +- plugins/_collections.js | 1460 +++++++++++---------- plugins/applyTransforms.js | 2 +- plugins/collapseGroups.js | 6 +- plugins/convertColors.js | 2 +- plugins/convertOneStopGradients.js | 2 +- plugins/convertPathData.js | 2 +- plugins/convertStyleToAttrs.js | 2 +- plugins/inlineStyles.js | 2 +- plugins/moveElemsAttrsToGroup.js | 4 +- plugins/moveGroupAttrsToElems.js | 2 +- plugins/removeEditorsNSData.js | 2 +- plugins/removeEmptyAttrs.js | 2 +- plugins/removeEmptyContainers.js | 2 +- plugins/removeHiddenElems.js | 2 +- plugins/removeNonInheritableGroupAttrs.js | 6 +- plugins/removeUnknownsAndDefaults.js | 2 +- plugins/removeUselessDefs.js | 2 +- plugins/removeUselessStrokeAndFill.js | 2 +- plugins/removeViewBox.js | 4 +- plugins/removeXlink.js | 8 +- 24 files changed, 810 insertions(+), 758 deletions(-) diff --git a/lib/parser.js b/lib/parser.js index 73875df64..4359373a9 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -213,7 +213,7 @@ const parseSvg = (data, from) => { sax.ontext = (text) => { if (current.type === 'element') { // prevent trimming of meaningful whitespace inside textual tags - if (textElems.includes(current.name)) { + if (textElems.has(current.name)) { /** * @type {XastText} */ diff --git a/lib/stringifier.js b/lib/stringifier.js index e50e723c5..1a710869d 100644 --- a/lib/stringifier.js +++ b/lib/stringifier.js @@ -235,7 +235,7 @@ const stringifyElement = (node, config, state) => { tagCloseStart = defaults.tagCloseStart; tagCloseEnd = defaults.tagCloseEnd; openIndent = ''; - } else if (textElems.includes(node.name)) { + } else if (textElems.has(node.name)) { tagOpenEnd = defaults.tagOpenEnd; tagCloseStart = defaults.tagCloseStart; closeIndent = ''; diff --git a/lib/style.js b/lib/style.js index 8a993523b..e54993a3c 100644 --- a/lib/style.js +++ b/lib/style.js @@ -130,7 +130,9 @@ const parseStyleDeclarations = (css) => { }; /** - * @type {(stylesheet: Stylesheet, node: XastElement) => ComputedStyles} + * @param {Stylesheet} stylesheet + * @param {XastElement} node + * @returns {ComputedStyles} */ const computeOwnStyle = (stylesheet, node) => { /** @type {ComputedStyles} */ @@ -139,7 +141,7 @@ const computeOwnStyle = (stylesheet, node) => { // collect attributes for (const [name, value] of Object.entries(node.attributes)) { - if (attrsGroups.presentation.includes(name)) { + if (attrsGroups.presentation.has(name)) { computedStyle[name] = { type: 'static', inherited: false, value }; importantStyles.set(name, false); } @@ -217,29 +219,31 @@ exports.compareSpecificity = compareSpecificity; * @type {(root: XastRoot) => Stylesheet} */ const collectStylesheet = (root) => { - /** @type {Array} */ + /** @type {StylesheetRule[]} */ const rules = []; /** @type {Map} */ const parents = new Map(); + visit(root, { element: { enter: (node, parentNode) => { - // store parents parents.set(node, parentNode); - // find and parse all styles - if (node.name === 'style') { + + if (node.name !== 'style') { + return; + } + + if ( + node.attributes.type == null || + node.attributes.type === '' || + node.attributes.type === 'text/css' + ) { const dynamic = node.attributes.media != null && node.attributes.media !== 'all'; - if ( - node.attributes.type == null || - node.attributes.type === '' || - node.attributes.type === 'text/css' - ) { - const children = node.children; - for (const child of children) { - if (child.type === 'text' || child.type === 'cdata') { - rules.push(...parseStylesheet(child.value, dynamic)); - } + + for (const child of node.children) { + if (child.type === 'text' || child.type === 'cdata') { + rules.push(...parseStylesheet(child.value, dynamic)); } } } @@ -253,11 +257,12 @@ const collectStylesheet = (root) => { exports.collectStylesheet = collectStylesheet; /** - * @type {(stylesheet: Stylesheet, node: XastElement) => ComputedStyles} + * @param {Stylesheet} stylesheet + * @param {XastElement} node + * @returns {ComputedStyles} */ const computeStyle = (stylesheet, node) => { const { parents } = stylesheet; - // collect inherited styles const computedStyles = computeOwnStyle(stylesheet, node); let parent = parents.get(node); while (parent != null && parent.type !== 'root') { @@ -265,9 +270,8 @@ const computeStyle = (stylesheet, node) => { for (const [name, computed] of Object.entries(inheritedStyles)) { if ( computedStyles[name] == null && - // ignore not inheritable styles - inheritableAttrs.includes(name) === true && - presentationNonInheritableGroupAttrs.includes(name) === false + inheritableAttrs.has(name) && + !presentationNonInheritableGroupAttrs.has(name) ) { computedStyles[name] = { ...computed, inherited: true }; } diff --git a/lib/svgo/tools.js b/lib/svgo/tools.js index 75a5ba823..e778034dc 100644 --- a/lib/svgo/tools.js +++ b/lib/svgo/tools.js @@ -204,7 +204,7 @@ exports.includesUrlReference = includesUrlReference; const findReferences = (attribute, value) => { const results = []; - if (referencesProps.includes(attribute)) { + if (referencesProps.has(attribute)) { const matches = value.matchAll(regReferencesUrl); for (const match of matches) { results.push(match[2]); diff --git a/plugins/_collections.js b/plugins/_collections.js index b9e67cb7c..6f11b1365 100644 --- a/plugins/_collections.js +++ b/plugins/_collections.js @@ -3,41 +3,50 @@ // https://www.w3.org/TR/SVG11/intro.html#Definitions /** - * @type {Record>} + * @type {Record>} */ exports.elemsGroups = { - animation: [ + animation: new Set([ 'animate', 'animateColor', 'animateMotion', 'animateTransform', 'set', - ], - descriptive: ['desc', 'metadata', 'title'], - shape: ['circle', 'ellipse', 'line', 'path', 'polygon', 'polyline', 'rect'], - structural: ['defs', 'g', 'svg', 'symbol', 'use'], - paintServer: [ - 'solidColor', + ]), + descriptive: new Set(['desc', 'metadata', 'title']), + shape: new Set([ + 'circle', + 'ellipse', + 'line', + 'path', + 'polygon', + 'polyline', + 'rect', + ]), + structural: new Set(['defs', 'g', 'svg', 'symbol', 'use']), + paintServer: new Set([ + 'hatch', 'linearGradient', - 'radialGradient', 'meshGradient', 'pattern', - 'hatch', - ], - nonRendering: [ - 'linearGradient', 'radialGradient', - 'pattern', + 'solidColor', + ]), + nonRendering: new Set([ 'clipPath', - 'mask', - 'marker', - 'symbol', 'filter', + 'linearGradient', + 'marker', + 'mask', + 'pattern', + 'radialGradient', 'solidColor', - ], - container: [ + 'symbol', + ]), + container: new Set([ 'a', 'defs', + 'foreignObject', 'g', 'marker', 'mask', @@ -46,28 +55,27 @@ exports.elemsGroups = { 'svg', 'switch', 'symbol', - 'foreignObject', - ], - textContent: [ + ]), + textContent: new Set([ 'altGlyph', 'altGlyphDef', 'altGlyphItem', 'glyph', 'glyphRef', - 'textPath', 'text', + 'textPath', 'tref', 'tspan', - ], - textContentChild: ['altGlyph', 'textPath', 'tref', 'tspan'], - lightSource: [ + ]), + textContentChild: new Set(['altGlyph', 'textPath', 'tref', 'tspan']), + lightSource: new Set([ 'feDiffuseLighting', - 'feSpecularLighting', 'feDistantLight', 'fePointLight', + 'feSpecularLighting', 'feSpotLight', - ], - filterPrimitive: [ + ]), + filterPrimitive: new Set([ 'feBlend', 'feColorMatrix', 'feComponentTransfer', @@ -90,7 +98,7 @@ exports.elemsGroups = { 'feSpecularLighting', 'feTile', 'feTurbulence', - ], + ]), }; /** @@ -99,81 +107,85 @@ exports.elemsGroups = { * * @see https://developer.mozilla.org/docs/Web/HTML/Element/pre */ -exports.textElems = [...exports.elemsGroups.textContent, 'title', 'pre']; +exports.textElems = new Set([ + ...exports.elemsGroups.textContent, + 'pre', + 'title', +]); -exports.pathElems = ['path', 'glyph', 'missing-glyph']; +exports.pathElems = new Set(['glyph', 'missing-glyph', 'path']); /** - * @type {Record>} + * @type {Record>} * @see https://www.w3.org/TR/SVG11/intro.html#Definitions */ exports.attrsGroups = { - animationAddition: ['additive', 'accumulate'], - animationAttributeTarget: ['attributeType', 'attributeName'], - animationEvent: ['onbegin', 'onend', 'onrepeat', 'onload'], - animationTiming: [ + animationAddition: new Set(['additive', 'accumulate']), + animationAttributeTarget: new Set(['attributeType', 'attributeName']), + animationEvent: new Set(['onbegin', 'onend', 'onrepeat', 'onload']), + animationTiming: new Set([ 'begin', 'dur', 'end', - 'min', + 'fill', 'max', - 'restart', + 'min', 'repeatCount', 'repeatDur', - 'fill', - ], - animationValue: [ + 'restart', + ]), + animationValue: new Set([ + 'by', 'calcMode', - 'values', - 'keyTimes', - 'keySplines', 'from', + 'keySplines', + 'keyTimes', 'to', - 'by', - ], - conditionalProcessing: [ - 'requiredFeatures', + 'values', + ]), + conditionalProcessing: new Set([ 'requiredExtensions', + 'requiredFeatures', 'systemLanguage', - ], - core: ['id', 'tabindex', 'xml:base', 'xml:lang', 'xml:space'], - graphicalEvent: [ - 'onfocusin', - 'onfocusout', + ]), + core: new Set(['id', 'tabindex', 'xml:base', 'xml:lang', 'xml:space']), + graphicalEvent: new Set([ 'onactivate', 'onclick', + 'onfocusin', + 'onfocusout', + 'onload', 'onmousedown', - 'onmouseup', - 'onmouseover', 'onmousemove', 'onmouseout', - 'onload', - ], - presentation: [ + 'onmouseover', + 'onmouseup', + ]), + presentation: new Set([ 'alignment-baseline', 'baseline-shift', - 'clip', 'clip-path', 'clip-rule', - 'color', - 'color-interpolation', + 'clip', 'color-interpolation-filters', + 'color-interpolation', 'color-profile', 'color-rendering', + 'color', 'cursor', 'direction', 'display', 'dominant-baseline', 'enable-background', - 'fill', 'fill-opacity', 'fill-rule', + 'fill', 'filter', 'flood-color', 'flood-opacity', 'font-family', - 'font-size', 'font-size-adjust', + 'font-size', 'font-stretch', 'font-style', 'font-variant', @@ -194,7 +206,6 @@ exports.attrsGroups = { 'shape-rendering', 'stop-color', 'stop-opacity', - 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', @@ -202,37 +213,38 @@ exports.attrsGroups = { 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', + 'stroke', 'text-anchor', 'text-decoration', 'text-overflow', 'text-rendering', - 'transform', 'transform-origin', + 'transform', 'unicode-bidi', 'vector-effect', 'visibility', 'word-spacing', 'writing-mode', - ], - xlink: [ - 'xlink:href', - 'xlink:show', + ]), + xlink: new Set([ 'xlink:actuate', - 'xlink:type', - 'xlink:role', 'xlink:arcrole', + 'xlink:href', + 'xlink:role', + 'xlink:show', 'xlink:title', - ], - documentEvent: [ + 'xlink:type', + ]), + documentEvent: new Set([ 'onabort', 'onerror', 'onresize', 'onscroll', 'onunload', 'onzoom', - ], - documentElementEvent: ['oncopy', 'oncut', 'onpaste'], - globalEvent: [ + ]), + documentElementEvent: new Set(['oncopy', 'oncut', 'onpaste']), + globalEvent: new Set([ 'oncancel', 'oncanplay', 'oncanplaythrough', @@ -289,17 +301,17 @@ exports.attrsGroups = { 'ontoggle', 'onvolumechange', 'onwaiting', - ], - filterPrimitive: ['x', 'y', 'width', 'height', 'result'], - transferFunction: [ - 'type', - 'tableValues', - 'slope', - 'intercept', + ]), + filterPrimitive: new Set(['x', 'y', 'width', 'height', 'result']), + transferFunction: new Set([ 'amplitude', 'exponent', + 'intercept', 'offset', - ], + 'slope', + 'tableValues', + 'type', + ]), }; /** @@ -371,49 +383,49 @@ exports.attrsGroupsDefaults = { /** * @type {Record, - * attrs?: Array, + * attrsGroups: Set, + * attrs?: Set, * defaults?: Record, - * contentGroups?: Array, - * content?: Array, + * contentGroups?: Set, + * content?: Set, * }>} * @see https://www.w3.org/TR/SVG11/eltindex.html */ exports.elems = { a: { - attrsGroups: [ + attrsGroups: new Set([ 'conditionalProcessing', 'core', 'graphicalEvent', 'presentation', 'xlink', - ], - attrs: [ + ]), + attrs: new Set([ 'class', - 'style', 'externalResourcesRequired', - 'transform', + 'style', 'target', - ], + 'transform', + ]), defaults: { target: '_self', }, - contentGroups: [ + contentGroups: new Set([ 'animation', 'descriptive', + 'paintServer', 'shape', 'structural', - 'paintServer', - ], - content: [ + ]), + content: new Set([ 'a', 'altGlyphDef', 'clipPath', 'color-profile', 'cursor', 'filter', - 'font', 'font-face', + 'font', 'foreignObject', 'image', 'marker', @@ -426,183 +438,188 @@ exports.elems = { 'view', // not spec compliant 'tspan', - ], + ]), }, altGlyph: { - attrsGroups: [ + attrsGroups: new Set([ 'conditionalProcessing', 'core', 'graphicalEvent', 'presentation', 'xlink', - ], - attrs: [ + ]), + attrs: new Set([ 'class', - 'style', - 'externalResourcesRequired', - 'x', - 'y', 'dx', 'dy', - 'glyphRef', + 'externalResourcesRequired', 'format', + 'glyphRef', 'rotate', - ], + 'style', + 'x', + 'y', + ]), }, altGlyphDef: { - attrsGroups: ['core'], - content: ['glyphRef'], + attrsGroups: new Set(['core']), + content: new Set(['glyphRef']), }, altGlyphItem: { - attrsGroups: ['core'], - content: ['glyphRef', 'altGlyphItem'], + attrsGroups: new Set(['core']), + content: new Set(['glyphRef', 'altGlyphItem']), }, animate: { - attrsGroups: [ - 'conditionalProcessing', - 'core', + attrsGroups: new Set([ 'animationAddition', 'animationAttributeTarget', 'animationEvent', 'animationTiming', 'animationValue', + 'conditionalProcessing', + 'core', 'presentation', 'xlink', - ], - attrs: ['externalResourcesRequired'], - contentGroups: ['descriptive'], + ]), + attrs: new Set(['externalResourcesRequired']), + contentGroups: new Set(['descriptive']), }, animateColor: { - attrsGroups: [ - 'conditionalProcessing', - 'core', - 'animationEvent', - 'xlink', + attrsGroups: new Set([ + 'animationAddition', 'animationAttributeTarget', + 'animationEvent', 'animationTiming', 'animationValue', - 'animationAddition', + 'conditionalProcessing', + 'core', 'presentation', - ], - attrs: ['externalResourcesRequired'], - contentGroups: ['descriptive'], + 'xlink', + ]), + attrs: new Set(['externalResourcesRequired']), + contentGroups: new Set(['descriptive']), }, animateMotion: { - attrsGroups: [ - 'conditionalProcessing', - 'core', + attrsGroups: new Set([ + 'animationAddition', 'animationEvent', - 'xlink', 'animationTiming', 'animationValue', - 'animationAddition', - ], - attrs: [ + 'conditionalProcessing', + 'core', + 'xlink', + ]), + attrs: new Set([ 'externalResourcesRequired', - 'path', 'keyPoints', - 'rotate', 'origin', - ], + 'path', + 'rotate', + ]), defaults: { rotate: '0', }, - contentGroups: ['descriptive'], - content: ['mpath'], + contentGroups: new Set(['descriptive']), + content: new Set(['mpath']), }, animateTransform: { - attrsGroups: [ - 'conditionalProcessing', - 'core', - 'animationEvent', - 'xlink', + attrsGroups: new Set([ + 'animationAddition', 'animationAttributeTarget', + 'animationEvent', 'animationTiming', 'animationValue', - 'animationAddition', - ], - attrs: ['externalResourcesRequired', 'type'], - contentGroups: ['descriptive'], + 'conditionalProcessing', + 'core', + 'xlink', + ]), + attrs: new Set(['externalResourcesRequired', 'type']), + contentGroups: new Set(['descriptive']), }, circle: { - attrsGroups: [ + attrsGroups: new Set([ 'conditionalProcessing', 'core', 'graphicalEvent', 'presentation', - ], - attrs: [ + ]), + attrs: new Set([ 'class', - 'style', - 'externalResourcesRequired', - 'transform', 'cx', 'cy', + 'externalResourcesRequired', 'r', - ], + 'style', + 'transform', + ]), defaults: { cx: '0', cy: '0', }, - contentGroups: ['animation', 'descriptive'], + contentGroups: new Set(['animation', 'descriptive']), }, clipPath: { - attrsGroups: ['conditionalProcessing', 'core', 'presentation'], - attrs: [ + attrsGroups: new Set(['conditionalProcessing', 'core', 'presentation']), + attrs: new Set([ 'class', - 'style', + 'clipPathUnits', 'externalResourcesRequired', + 'style', 'transform', - 'clipPathUnits', - ], + ]), defaults: { clipPathUnits: 'userSpaceOnUse', }, - contentGroups: ['animation', 'descriptive', 'shape'], - content: ['text', 'use'], + contentGroups: new Set(['animation', 'descriptive', 'shape']), + content: new Set(['text', 'use']), }, 'color-profile': { - attrsGroups: ['core', 'xlink'], - attrs: ['local', 'name', 'rendering-intent'], + attrsGroups: new Set(['core', 'xlink']), + attrs: new Set(['local', 'name', 'rendering-intent']), defaults: { name: 'sRGB', 'rendering-intent': 'auto', }, - contentGroups: ['descriptive'], + contentGroups: new Set(['descriptive']), }, cursor: { - attrsGroups: ['core', 'conditionalProcessing', 'xlink'], - attrs: ['externalResourcesRequired', 'x', 'y'], + attrsGroups: new Set(['core', 'conditionalProcessing', 'xlink']), + attrs: new Set(['externalResourcesRequired', 'x', 'y']), defaults: { x: '0', y: '0', }, - contentGroups: ['descriptive'], + contentGroups: new Set(['descriptive']), }, defs: { - attrsGroups: [ + attrsGroups: new Set([ 'conditionalProcessing', 'core', 'graphicalEvent', 'presentation', - ], - attrs: ['class', 'style', 'externalResourcesRequired', 'transform'], - contentGroups: [ + ]), + attrs: new Set([ + 'class', + 'externalResourcesRequired', + 'style', + 'transform', + ]), + contentGroups: new Set([ 'animation', 'descriptive', + 'paintServer', 'shape', 'structural', - 'paintServer', - ], - content: [ + ]), + content: new Set([ 'a', 'altGlyphDef', 'clipPath', 'color-profile', 'cursor', 'filter', - 'font', 'font-face', + 'font', 'foreignObject', 'image', 'marker', @@ -613,38 +630,38 @@ exports.elems = { 'switch', 'text', 'view', - ], + ]), }, desc: { - attrsGroups: ['core'], - attrs: ['class', 'style'], + attrsGroups: new Set(['core']), + attrs: new Set(['class', 'style']), }, ellipse: { - attrsGroups: [ + attrsGroups: new Set([ 'conditionalProcessing', 'core', 'graphicalEvent', 'presentation', - ], - attrs: [ + ]), + attrs: new Set([ 'class', - 'style', - 'externalResourcesRequired', - 'transform', 'cx', 'cy', + 'externalResourcesRequired', 'rx', 'ry', - ], + 'style', + 'transform', + ]), defaults: { cx: '0', cy: '0', }, - contentGroups: ['animation', 'descriptive'], + contentGroups: new Set(['animation', 'descriptive']), }, feBlend: { - attrsGroups: ['core', 'presentation', 'filterPrimitive'], - attrs: [ + attrsGroups: new Set(['core', 'presentation', 'filterPrimitive']), + attrs: new Set([ 'class', 'style', // TODO: in - 'If no value is provided and this is the first filter primitive, @@ -652,28 +669,38 @@ exports.elems = { 'in', 'in2', 'mode', - ], + ]), defaults: { mode: 'normal', }, - content: ['animate', 'set'], + content: new Set(['animate', 'set']), }, feColorMatrix: { - attrsGroups: ['core', 'presentation', 'filterPrimitive'], - attrs: ['class', 'style', 'in', 'type', 'values'], + attrsGroups: new Set(['core', 'presentation', 'filterPrimitive']), + attrs: new Set(['class', 'style', 'in', 'type', 'values']), defaults: { type: 'matrix', }, - content: ['animate', 'set'], + content: new Set(['animate', 'set']), }, feComponentTransfer: { - attrsGroups: ['core', 'presentation', 'filterPrimitive'], - attrs: ['class', 'style', 'in'], - content: ['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR'], + attrsGroups: new Set(['core', 'presentation', 'filterPrimitive']), + attrs: new Set(['class', 'style', 'in']), + content: new Set(['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), }, feComposite: { - attrsGroups: ['core', 'presentation', 'filterPrimitive'], - attrs: ['class', 'style', 'in', 'in2', 'operator', 'k1', 'k2', 'k3', 'k4'], + attrsGroups: new Set(['core', 'presentation', 'filterPrimitive']), + attrs: new Set([ + 'class', + 'in', + 'in2', + 'k1', + 'k2', + 'k3', + 'k4', + 'operator', + 'style', + ]), defaults: { operator: 'over', k1: '0', @@ -681,203 +708,203 @@ exports.elems = { k3: '0', k4: '0', }, - content: ['animate', 'set'], + content: new Set(['animate', 'set']), }, feConvolveMatrix: { - attrsGroups: ['core', 'presentation', 'filterPrimitive'], - attrs: [ + attrsGroups: new Set(['core', 'presentation', 'filterPrimitive']), + attrs: new Set([ 'class', - 'style', 'in', - 'order', 'kernelMatrix', + 'order', + 'style', // TODO: divisor - 'The default value is the sum of all values in kernelMatrix, // with the exception that if the sum is zero, then the divisor is set to 1' - 'divisor', 'bias', + 'divisor', // TODO: targetX - 'By default, the convolution matrix is centered in X over each // pixel of the input image (i.e., targetX = floor ( orderX / 2 ))' + 'edgeMode', 'targetX', 'targetY', - 'edgeMode', // TODO: kernelUnitLength - 'The first number is the value. The second number // is the value. If the value is not specified, it defaults to the same value as ' 'kernelUnitLength', 'preserveAlpha', - ], + ]), defaults: { order: '3', bias: '0', edgeMode: 'duplicate', preserveAlpha: 'false', }, - content: ['animate', 'set'], + content: new Set(['animate', 'set']), }, feDiffuseLighting: { - attrsGroups: ['core', 'presentation', 'filterPrimitive'], - attrs: [ + attrsGroups: new Set(['core', 'presentation', 'filterPrimitive']), + attrs: new Set([ 'class', - 'style', - 'in', - 'surfaceScale', 'diffuseConstant', + 'in', 'kernelUnitLength', - ], + 'style', + 'surfaceScale', + ]), defaults: { surfaceScale: '1', diffuseConstant: '1', }, - contentGroups: ['descriptive'], - content: [ + contentGroups: new Set(['descriptive']), + content: new Set([ // TODO: 'exactly one light source element, in any order' 'feDistantLight', 'fePointLight', 'feSpotLight', - ], + ]), }, feDisplacementMap: { - attrsGroups: ['core', 'presentation', 'filterPrimitive'], - attrs: [ + attrsGroups: new Set(['core', 'presentation', 'filterPrimitive']), + attrs: new Set([ 'class', - 'style', 'in', 'in2', 'scale', + 'style', 'xChannelSelector', 'yChannelSelector', - ], + ]), defaults: { scale: '0', xChannelSelector: 'A', yChannelSelector: 'A', }, - content: ['animate', 'set'], + content: new Set(['animate', 'set']), }, feDistantLight: { - attrsGroups: ['core'], - attrs: ['azimuth', 'elevation'], + attrsGroups: new Set(['core']), + attrs: new Set(['azimuth', 'elevation']), defaults: { azimuth: '0', elevation: '0', }, - content: ['animate', 'set'], + content: new Set(['animate', 'set']), }, feFlood: { - attrsGroups: ['core', 'presentation', 'filterPrimitive'], - attrs: ['class', 'style'], - content: ['animate', 'animateColor', 'set'], + attrsGroups: new Set(['core', 'presentation', 'filterPrimitive']), + attrs: new Set(['class', 'style']), + content: new Set(['animate', 'animateColor', 'set']), }, feFuncA: { - attrsGroups: ['core', 'transferFunction'], - content: ['set', 'animate'], + attrsGroups: new Set(['core', 'transferFunction']), + content: new Set(['set', 'animate']), }, feFuncB: { - attrsGroups: ['core', 'transferFunction'], - content: ['set', 'animate'], + attrsGroups: new Set(['core', 'transferFunction']), + content: new Set(['set', 'animate']), }, feFuncG: { - attrsGroups: ['core', 'transferFunction'], - content: ['set', 'animate'], + attrsGroups: new Set(['core', 'transferFunction']), + content: new Set(['set', 'animate']), }, feFuncR: { - attrsGroups: ['core', 'transferFunction'], - content: ['set', 'animate'], + attrsGroups: new Set(['core', 'transferFunction']), + content: new Set(['set', 'animate']), }, feGaussianBlur: { - attrsGroups: ['core', 'presentation', 'filterPrimitive'], - attrs: ['class', 'style', 'in', 'stdDeviation'], + attrsGroups: new Set(['core', 'presentation', 'filterPrimitive']), + attrs: new Set(['class', 'style', 'in', 'stdDeviation']), defaults: { stdDeviation: '0', }, - content: ['set', 'animate'], + content: new Set(['set', 'animate']), }, feImage: { - attrsGroups: ['core', 'presentation', 'filterPrimitive', 'xlink'], - attrs: [ + attrsGroups: new Set(['core', 'presentation', 'filterPrimitive', 'xlink']), + attrs: new Set([ 'class', - 'style', 'externalResourcesRequired', - 'preserveAspectRatio', 'href', + 'preserveAspectRatio', + 'style', 'xlink:href', - ], + ]), defaults: { preserveAspectRatio: 'xMidYMid meet', }, - content: ['animate', 'animateTransform', 'set'], + content: new Set(['animate', 'animateTransform', 'set']), }, feMerge: { - attrsGroups: ['core', 'presentation', 'filterPrimitive'], - attrs: ['class', 'style'], - content: ['feMergeNode'], + attrsGroups: new Set(['core', 'presentation', 'filterPrimitive']), + attrs: new Set(['class', 'style']), + content: new Set(['feMergeNode']), }, feMergeNode: { - attrsGroups: ['core'], - attrs: ['in'], - content: ['animate', 'set'], + attrsGroups: new Set(['core']), + attrs: new Set(['in']), + content: new Set(['animate', 'set']), }, feMorphology: { - attrsGroups: ['core', 'presentation', 'filterPrimitive'], - attrs: ['class', 'style', 'in', 'operator', 'radius'], + attrsGroups: new Set(['core', 'presentation', 'filterPrimitive']), + attrs: new Set(['class', 'style', 'in', 'operator', 'radius']), defaults: { operator: 'erode', radius: '0', }, - content: ['animate', 'set'], + content: new Set(['animate', 'set']), }, feOffset: { - attrsGroups: ['core', 'presentation', 'filterPrimitive'], - attrs: ['class', 'style', 'in', 'dx', 'dy'], + attrsGroups: new Set(['core', 'presentation', 'filterPrimitive']), + attrs: new Set(['class', 'style', 'in', 'dx', 'dy']), defaults: { dx: '0', dy: '0', }, - content: ['animate', 'set'], + content: new Set(['animate', 'set']), }, fePointLight: { - attrsGroups: ['core'], - attrs: ['x', 'y', 'z'], + attrsGroups: new Set(['core']), + attrs: new Set(['x', 'y', 'z']), defaults: { x: '0', y: '0', z: '0', }, - content: ['animate', 'set'], + content: new Set(['animate', 'set']), }, feSpecularLighting: { - attrsGroups: ['core', 'presentation', 'filterPrimitive'], - attrs: [ + attrsGroups: new Set(['core', 'presentation', 'filterPrimitive']), + attrs: new Set([ 'class', - 'style', 'in', - 'surfaceScale', + 'kernelUnitLength', 'specularConstant', 'specularExponent', - 'kernelUnitLength', - ], + 'style', + 'surfaceScale', + ]), defaults: { surfaceScale: '1', specularConstant: '1', specularExponent: '1', }, - contentGroups: [ + contentGroups: new Set([ 'descriptive', // TODO: exactly one 'light source element' 'lightSource', - ], + ]), }, feSpotLight: { - attrsGroups: ['core'], - attrs: [ - 'x', - 'y', - 'z', + attrsGroups: new Set(['core']), + attrs: new Set([ + 'limitingConeAngle', 'pointsAtX', 'pointsAtY', 'pointsAtZ', 'specularExponent', - 'limitingConeAngle', - ], + 'x', + 'y', + 'z', + ]), defaults: { x: '0', y: '0', @@ -887,24 +914,24 @@ exports.elems = { pointsAtZ: '0', specularExponent: '1', }, - content: ['animate', 'set'], + content: new Set(['animate', 'set']), }, feTile: { - attrsGroups: ['core', 'presentation', 'filterPrimitive'], - attrs: ['class', 'style', 'in'], - content: ['animate', 'set'], + attrsGroups: new Set(['core', 'presentation', 'filterPrimitive']), + attrs: new Set(['class', 'style', 'in']), + content: new Set(['animate', 'set']), }, feTurbulence: { - attrsGroups: ['core', 'presentation', 'filterPrimitive'], - attrs: [ - 'class', - 'style', + attrsGroups: new Set(['core', 'presentation', 'filterPrimitive']), + attrs: new Set([ 'baseFrequency', + 'class', 'numOctaves', 'seed', 'stitchTiles', + 'style', 'type', - ], + ]), defaults: { baseFrequency: '0', numOctaves: '1', @@ -912,24 +939,24 @@ exports.elems = { stitchTiles: 'noStitch', type: 'turbulence', }, - content: ['animate', 'set'], + content: new Set(['animate', 'set']), }, filter: { - attrsGroups: ['core', 'presentation', 'xlink'], - attrs: [ + attrsGroups: new Set(['core', 'presentation', 'xlink']), + attrs: new Set([ 'class', - 'style', 'externalResourcesRequired', - 'x', - 'y', - 'width', - 'height', 'filterRes', 'filterUnits', - 'primitiveUnits', + 'height', 'href', + 'primitiveUnits', + 'style', + 'width', + 'x', 'xlink:href', - ], + 'y', + ]), defaults: { primitiveUnits: 'userSpaceOnUse', x: '-10%', @@ -937,32 +964,32 @@ exports.elems = { width: '120%', height: '120%', }, - contentGroups: ['descriptive', 'filterPrimitive'], - content: ['animate', 'set'], + contentGroups: new Set(['descriptive', 'filterPrimitive']), + content: new Set(['animate', 'set']), }, font: { - attrsGroups: ['core', 'presentation'], - attrs: [ + attrsGroups: new Set(['core', 'presentation']), + attrs: new Set([ 'class', - 'style', 'externalResourcesRequired', + 'horiz-adv-x', 'horiz-origin-x', 'horiz-origin-y', - 'horiz-adv-x', + 'style', + 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', - 'vert-adv-y', - ], + ]), defaults: { 'horiz-origin-x': '0', 'horiz-origin-y': '0', }, - contentGroups: ['descriptive'], - content: ['font-face', 'glyph', 'hkern', 'missing-glyph', 'vkern'], + contentGroups: new Set(['descriptive']), + content: new Set(['font-face', 'glyph', 'hkern', 'missing-glyph', 'vkern']), }, 'font-face': { - attrsGroups: ['core'], - attrs: [ + attrsGroups: new Set(['core']), + attrs: new Set([ 'font-family', 'font-style', 'font-variant', @@ -996,7 +1023,7 @@ exports.elems = { 'strikethrough-thickness', 'overline-position', 'overline-thickness', - ], + ]), defaults: { 'font-style': 'all', 'font-variant': 'normal', @@ -1007,76 +1034,81 @@ exports.elems = { 'panose-1': '0 0 0 0 0 0 0 0 0 0', slope: '0', }, - contentGroups: ['descriptive'], - content: [ + contentGroups: new Set(['descriptive']), + content: new Set([ // TODO: "at most one 'font-face-src' element" 'font-face-src', - ], + ]), }, // TODO: empty content 'font-face-format': { - attrsGroups: ['core'], - attrs: ['string'], + attrsGroups: new Set(['core']), + attrs: new Set(['string']), }, 'font-face-name': { - attrsGroups: ['core'], - attrs: ['name'], + attrsGroups: new Set(['core']), + attrs: new Set(['name']), }, 'font-face-src': { - attrsGroups: ['core'], - content: ['font-face-name', 'font-face-uri'], + attrsGroups: new Set(['core']), + content: new Set(['font-face-name', 'font-face-uri']), }, 'font-face-uri': { - attrsGroups: ['core', 'xlink'], - attrs: ['href', 'xlink:href'], - content: ['font-face-format'], + attrsGroups: new Set(['core', 'xlink']), + attrs: new Set(['href', 'xlink:href']), + content: new Set(['font-face-format']), }, foreignObject: { - attrsGroups: [ - 'core', + attrsGroups: new Set([ 'conditionalProcessing', + 'core', 'graphicalEvent', 'presentation', - ], - attrs: [ + ]), + attrs: new Set([ 'class', - 'style', 'externalResourcesRequired', + 'height', + 'style', 'transform', + 'width', 'x', 'y', - 'width', - 'height', - ], + ]), defaults: { x: '0', y: '0', }, }, g: { - attrsGroups: [ + attrsGroups: new Set([ 'conditionalProcessing', 'core', 'graphicalEvent', 'presentation', - ], - attrs: ['class', 'style', 'externalResourcesRequired', 'transform'], - contentGroups: [ + ]), + attrs: new Set([ + 'class', + 'externalResourcesRequired', + 'style', + 'transform', + ]), + contentGroups: new Set([ 'animation', 'descriptive', + 'paintServer', 'shape', 'structural', - 'paintServer', - ], - content: [ + ]), + content: new Set([ 'a', 'altGlyphDef', 'clipPath', 'color-profile', 'cursor', 'filter', - 'font', 'font-face', + 'font', 'foreignObject', 'image', 'marker', @@ -1087,43 +1119,43 @@ exports.elems = { 'switch', 'text', 'view', - ], + ]), }, glyph: { - attrsGroups: ['core', 'presentation'], - attrs: [ + attrsGroups: new Set(['core', 'presentation']), + attrs: new Set([ + 'arabic-form', 'class', - 'style', 'd', + 'glyph-name', 'horiz-adv-x', + 'lang', + 'orientation', + 'style', + 'unicode', + 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', - 'vert-adv-y', - 'unicode', - 'glyph-name', - 'orientation', - 'arabic-form', - 'lang', - ], + ]), defaults: { 'arabic-form': 'initial', }, - contentGroups: [ + contentGroups: new Set([ 'animation', 'descriptive', + 'paintServer', 'shape', 'structural', - 'paintServer', - ], - content: [ + ]), + content: new Set([ 'a', 'altGlyphDef', 'clipPath', 'color-profile', 'cursor', 'filter', - 'font', 'font-face', + 'font', 'foreignObject', 'image', 'marker', @@ -1134,35 +1166,35 @@ exports.elems = { 'switch', 'text', 'view', - ], + ]), }, glyphRef: { - attrsGroups: ['core', 'presentation'], - attrs: [ + attrsGroups: new Set(['core', 'presentation']), + attrs: new Set([ 'class', - 'style', 'd', 'horiz-adv-x', + 'style', + 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', - 'vert-adv-y', - ], - contentGroups: [ + ]), + contentGroups: new Set([ 'animation', 'descriptive', + 'paintServer', 'shape', 'structural', - 'paintServer', - ], - content: [ + ]), + content: new Set([ 'a', 'altGlyphDef', 'clipPath', 'color-profile', 'cursor', 'filter', - 'font', 'font-face', + 'font', 'foreignObject', 'image', 'marker', @@ -1173,21 +1205,21 @@ exports.elems = { 'switch', 'text', 'view', - ], + ]), }, hatch: { - attrsGroups: ['core', 'presentation', 'xlink'], - attrs: [ + attrsGroups: new Set(['core', 'presentation', 'xlink']), + attrs: new Set([ 'class', - 'style', - 'x', - 'y', + 'hatchContentUnits', + 'hatchUnits', 'pitch', 'rotate', - 'hatchUnits', - 'hatchContentUnits', + 'style', 'transform', - ], + 'x', + 'y', + ]), defaults: { hatchUnits: 'objectBoundingBox', hatchContentUnits: 'userSpaceOnUse', @@ -1196,90 +1228,90 @@ exports.elems = { pitch: '0', rotate: '0', }, - contentGroups: ['animation', 'descriptive'], - content: ['hatchPath'], + contentGroups: new Set(['animation', 'descriptive']), + content: new Set(['hatchPath']), }, hatchPath: { - attrsGroups: ['core', 'presentation', 'xlink'], - attrs: ['class', 'style', 'd', 'offset'], + attrsGroups: new Set(['core', 'presentation', 'xlink']), + attrs: new Set(['class', 'style', 'd', 'offset']), defaults: { offset: '0', }, - contentGroups: ['animation', 'descriptive'], + contentGroups: new Set(['animation', 'descriptive']), }, hkern: { - attrsGroups: ['core'], - attrs: ['u1', 'g1', 'u2', 'g2', 'k'], + attrsGroups: new Set(['core']), + attrs: new Set(['u1', 'g1', 'u2', 'g2', 'k']), }, image: { - attrsGroups: [ - 'core', + attrsGroups: new Set([ 'conditionalProcessing', + 'core', 'graphicalEvent', - 'xlink', 'presentation', - ], - attrs: [ + 'xlink', + ]), + attrs: new Set([ 'class', - 'style', 'externalResourcesRequired', + 'height', + 'href', 'preserveAspectRatio', + 'style', 'transform', - 'x', - 'y', 'width', - 'height', - 'href', + 'x', 'xlink:href', - ], + 'y', + ]), defaults: { x: '0', y: '0', preserveAspectRatio: 'xMidYMid meet', }, - contentGroups: ['animation', 'descriptive'], + contentGroups: new Set(['animation', 'descriptive']), }, line: { - attrsGroups: [ + attrsGroups: new Set([ 'conditionalProcessing', 'core', 'graphicalEvent', 'presentation', - ], - attrs: [ + ]), + attrs: new Set([ 'class', - 'style', 'externalResourcesRequired', + 'style', 'transform', 'x1', - 'y1', 'x2', + 'y1', 'y2', - ], + ]), defaults: { x1: '0', y1: '0', x2: '0', y2: '0', }, - contentGroups: ['animation', 'descriptive'], + contentGroups: new Set(['animation', 'descriptive']), }, linearGradient: { - attrsGroups: ['core', 'presentation', 'xlink'], - attrs: [ + attrsGroups: new Set(['core', 'presentation', 'xlink']), + attrs: new Set([ 'class', - 'style', 'externalResourcesRequired', - 'x1', - 'y1', - 'x2', - 'y2', - 'gradientUnits', 'gradientTransform', - 'spreadMethod', + 'gradientUnits', 'href', + 'spreadMethod', + 'style', + 'x1', + 'x2', 'xlink:href', - ], + 'y1', + 'y2', + ]), defaults: { x1: '0', y1: '0', @@ -1287,24 +1319,24 @@ exports.elems = { y2: '0', spreadMethod: 'pad', }, - contentGroups: ['descriptive'], - content: ['animate', 'animateTransform', 'set', 'stop'], + contentGroups: new Set(['descriptive']), + content: new Set(['animate', 'animateTransform', 'set', 'stop']), }, marker: { - attrsGroups: ['core', 'presentation'], - attrs: [ + attrsGroups: new Set(['core', 'presentation']), + attrs: new Set([ 'class', - 'style', 'externalResourcesRequired', - 'viewBox', - 'preserveAspectRatio', - 'refX', - 'refY', + 'markerHeight', 'markerUnits', 'markerWidth', - 'markerHeight', 'orient', - ], + 'preserveAspectRatio', + 'refX', + 'refY', + 'style', + 'viewBox', + ]), defaults: { markerUnits: 'strokeWidth', refX: '0', @@ -1312,22 +1344,22 @@ exports.elems = { markerWidth: '3', markerHeight: '3', }, - contentGroups: [ + contentGroups: new Set([ 'animation', 'descriptive', + 'paintServer', 'shape', 'structural', - 'paintServer', - ], - content: [ + ]), + content: new Set([ 'a', 'altGlyphDef', 'clipPath', 'color-profile', 'cursor', 'filter', - 'font', 'font-face', + 'font', 'foreignObject', 'image', 'marker', @@ -1338,22 +1370,22 @@ exports.elems = { 'switch', 'text', 'view', - ], + ]), }, mask: { - attrsGroups: ['conditionalProcessing', 'core', 'presentation'], - attrs: [ + attrsGroups: new Set(['conditionalProcessing', 'core', 'presentation']), + attrs: new Set([ 'class', - 'style', 'externalResourcesRequired', - 'x', - 'y', - 'width', 'height', 'mask-type', - 'maskUnits', 'maskContentUnits', - ], + 'maskUnits', + 'style', + 'width', + 'x', + 'y', + ]), defaults: { maskUnits: 'objectBoundingBox', maskContentUnits: 'userSpaceOnUse', @@ -1362,22 +1394,22 @@ exports.elems = { width: '120%', height: '120%', }, - contentGroups: [ + contentGroups: new Set([ 'animation', 'descriptive', + 'paintServer', 'shape', 'structural', - 'paintServer', - ], - content: [ + ]), + content: new Set([ 'a', 'altGlyphDef', 'clipPath', 'color-profile', 'cursor', 'filter', - 'font', 'font-face', + 'font', 'foreignObject', 'image', 'marker', @@ -1388,38 +1420,38 @@ exports.elems = { 'switch', 'text', 'view', - ], + ]), }, metadata: { - attrsGroups: ['core'], + attrsGroups: new Set(['core']), }, 'missing-glyph': { - attrsGroups: ['core', 'presentation'], - attrs: [ + attrsGroups: new Set(['core', 'presentation']), + attrs: new Set([ 'class', - 'style', 'd', 'horiz-adv-x', + 'style', + 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', - 'vert-adv-y', - ], - contentGroups: [ + ]), + contentGroups: new Set([ 'animation', 'descriptive', + 'paintServer', 'shape', 'structural', - 'paintServer', - ], - content: [ + ]), + content: new Set([ 'a', 'altGlyphDef', 'clipPath', 'color-profile', 'cursor', 'filter', - 'font', 'font-face', + 'font', 'foreignObject', 'image', 'marker', @@ -1430,48 +1462,53 @@ exports.elems = { 'switch', 'text', 'view', - ], + ]), }, mpath: { - attrsGroups: ['core', 'xlink'], - attrs: ['externalResourcesRequired', 'href', 'xlink:href'], - contentGroups: ['descriptive'], + attrsGroups: new Set(['core', 'xlink']), + attrs: new Set(['externalResourcesRequired', 'href', 'xlink:href']), + contentGroups: new Set(['descriptive']), }, path: { - attrsGroups: [ + attrsGroups: new Set([ 'conditionalProcessing', 'core', 'graphicalEvent', 'presentation', - ], - attrs: [ + ]), + attrs: new Set([ 'class', - 'style', - 'externalResourcesRequired', - 'transform', 'd', + 'externalResourcesRequired', 'pathLength', - ], - contentGroups: ['animation', 'descriptive'], + 'style', + 'transform', + ]), + contentGroups: new Set(['animation', 'descriptive']), }, pattern: { - attrsGroups: ['conditionalProcessing', 'core', 'presentation', 'xlink'], - attrs: [ + attrsGroups: new Set([ + 'conditionalProcessing', + 'core', + 'presentation', + 'xlink', + ]), + attrs: new Set([ 'class', - 'style', 'externalResourcesRequired', - 'viewBox', - 'preserveAspectRatio', - 'x', - 'y', - 'width', 'height', - 'patternUnits', + 'href', 'patternContentUnits', 'patternTransform', - 'href', + 'patternUnits', + 'preserveAspectRatio', + 'style', + 'viewBox', + 'width', + 'x', 'xlink:href', - ], + 'y', + ]), defaults: { patternUnits: 'objectBoundingBox', patternContentUnits: 'userSpaceOnUse', @@ -1481,22 +1518,22 @@ exports.elems = { height: '0', preserveAspectRatio: 'xMidYMid meet', }, - contentGroups: [ + contentGroups: new Set([ 'animation', 'descriptive', 'paintServer', 'shape', 'structural', - ], - content: [ + ]), + content: new Set([ 'a', 'altGlyphDef', 'clipPath', 'color-profile', 'cursor', 'filter', - 'font', 'font-face', + 'font', 'foreignObject', 'image', 'marker', @@ -1507,166 +1544,166 @@ exports.elems = { 'switch', 'text', 'view', - ], + ]), }, polygon: { - attrsGroups: [ + attrsGroups: new Set([ 'conditionalProcessing', 'core', 'graphicalEvent', 'presentation', - ], - attrs: [ + ]), + attrs: new Set([ 'class', - 'style', 'externalResourcesRequired', - 'transform', 'points', - ], - contentGroups: ['animation', 'descriptive'], + 'style', + 'transform', + ]), + contentGroups: new Set(['animation', 'descriptive']), }, polyline: { - attrsGroups: [ + attrsGroups: new Set([ 'conditionalProcessing', 'core', 'graphicalEvent', 'presentation', - ], - attrs: [ + ]), + attrs: new Set([ 'class', - 'style', 'externalResourcesRequired', - 'transform', 'points', - ], - contentGroups: ['animation', 'descriptive'], + 'style', + 'transform', + ]), + contentGroups: new Set(['animation', 'descriptive']), }, radialGradient: { - attrsGroups: ['core', 'presentation', 'xlink'], - attrs: [ + attrsGroups: new Set(['core', 'presentation', 'xlink']), + attrs: new Set([ 'class', - 'style', - 'externalResourcesRequired', 'cx', 'cy', - 'r', + 'externalResourcesRequired', + 'fr', 'fx', 'fy', - 'fr', - 'gradientUnits', 'gradientTransform', - 'spreadMethod', + 'gradientUnits', 'href', + 'r', + 'spreadMethod', + 'style', 'xlink:href', - ], + ]), defaults: { gradientUnits: 'objectBoundingBox', cx: '50%', cy: '50%', r: '50%', }, - contentGroups: ['descriptive'], - content: ['animate', 'animateTransform', 'set', 'stop'], + contentGroups: new Set(['descriptive']), + content: new Set(['animate', 'animateTransform', 'set', 'stop']), }, meshGradient: { - attrsGroups: ['core', 'presentation', 'xlink'], - attrs: ['class', 'style', 'x', 'y', 'gradientUnits', 'transform'], - contentGroups: ['descriptive', 'paintServer', 'animation'], - content: ['meshRow'], + attrsGroups: new Set(['core', 'presentation', 'xlink']), + attrs: new Set(['class', 'style', 'x', 'y', 'gradientUnits', 'transform']), + contentGroups: new Set(['descriptive', 'paintServer', 'animation']), + content: new Set(['meshRow']), }, meshRow: { - attrsGroups: ['core', 'presentation'], - attrs: ['class', 'style'], - contentGroups: ['descriptive'], - content: ['meshPatch'], + attrsGroups: new Set(['core', 'presentation']), + attrs: new Set(['class', 'style']), + contentGroups: new Set(['descriptive']), + content: new Set(['meshPatch']), }, meshPatch: { - attrsGroups: ['core', 'presentation'], - attrs: ['class', 'style'], - contentGroups: ['descriptive'], - content: ['stop'], + attrsGroups: new Set(['core', 'presentation']), + attrs: new Set(['class', 'style']), + contentGroups: new Set(['descriptive']), + content: new Set(['stop']), }, rect: { - attrsGroups: [ + attrsGroups: new Set([ 'conditionalProcessing', 'core', 'graphicalEvent', 'presentation', - ], - attrs: [ + ]), + attrs: new Set([ 'class', - 'style', 'externalResourcesRequired', - 'transform', - 'x', - 'y', - 'width', 'height', 'rx', 'ry', - ], + 'style', + 'transform', + 'width', + 'x', + 'y', + ]), defaults: { x: '0', y: '0', }, - contentGroups: ['animation', 'descriptive'], + contentGroups: new Set(['animation', 'descriptive']), }, script: { - attrsGroups: ['core', 'xlink'], - attrs: ['externalResourcesRequired', 'type', 'href', 'xlink:href'], + attrsGroups: new Set(['core', 'xlink']), + attrs: new Set(['externalResourcesRequired', 'type', 'href', 'xlink:href']), }, set: { - attrsGroups: [ - 'conditionalProcessing', - 'core', + attrsGroups: new Set([ 'animation', - 'xlink', 'animationAttributeTarget', 'animationTiming', - ], - attrs: ['externalResourcesRequired', 'to'], - contentGroups: ['descriptive'], + 'conditionalProcessing', + 'core', + 'xlink', + ]), + attrs: new Set(['externalResourcesRequired', 'to']), + contentGroups: new Set(['descriptive']), }, solidColor: { - attrsGroups: ['core', 'presentation'], - attrs: ['class', 'style'], - contentGroups: ['paintServer'], + attrsGroups: new Set(['core', 'presentation']), + attrs: new Set(['class', 'style']), + contentGroups: new Set(['paintServer']), }, stop: { - attrsGroups: ['core', 'presentation'], - attrs: ['class', 'style', 'offset', 'path'], - content: ['animate', 'animateColor', 'set'], + attrsGroups: new Set(['core', 'presentation']), + attrs: new Set(['class', 'style', 'offset', 'path']), + content: new Set(['animate', 'animateColor', 'set']), }, style: { - attrsGroups: ['core'], - attrs: ['type', 'media', 'title'], + attrsGroups: new Set(['core']), + attrs: new Set(['type', 'media', 'title']), defaults: { type: 'text/css', }, }, svg: { - attrsGroups: [ + attrsGroups: new Set([ 'conditionalProcessing', 'core', 'documentEvent', 'graphicalEvent', 'presentation', - ], - attrs: [ + ]), + attrs: new Set([ + 'baseProfile', 'class', + 'contentScriptType', + 'contentStyleType', + 'height', + 'preserveAspectRatio', 'style', + 'version', + 'viewBox', + 'width', 'x', 'y', - 'width', - 'height', - 'viewBox', - 'preserveAspectRatio', 'zoomAndPan', - 'version', - 'baseProfile', - 'contentScriptType', - 'contentStyleType', - ], + ]), defaults: { x: '0', y: '0', @@ -1679,22 +1716,22 @@ exports.elems = { contentScriptType: 'application/ecmascript', contentStyleType: 'text/css', }, - contentGroups: [ + contentGroups: new Set([ 'animation', 'descriptive', + 'paintServer', 'shape', 'structural', - 'paintServer', - ], - content: [ + ]), + content: new Set([ 'a', 'altGlyphDef', 'clipPath', 'color-profile', 'cursor', 'filter', - 'font', 'font-face', + 'font', 'foreignObject', 'image', 'marker', @@ -1705,18 +1742,23 @@ exports.elems = { 'switch', 'text', 'view', - ], + ]), }, switch: { - attrsGroups: [ + attrsGroups: new Set([ 'conditionalProcessing', 'core', 'graphicalEvent', 'presentation', - ], - attrs: ['class', 'style', 'externalResourcesRequired', 'transform'], - contentGroups: ['animation', 'descriptive', 'shape'], - content: [ + ]), + attrs: new Set([ + 'class', + 'externalResourcesRequired', + 'style', + 'transform', + ]), + contentGroups: new Set(['animation', 'descriptive', 'shape']), + content: new Set([ 'a', 'foreignObject', 'g', @@ -1725,39 +1767,39 @@ exports.elems = { 'switch', 'text', 'use', - ], + ]), }, symbol: { - attrsGroups: ['core', 'graphicalEvent', 'presentation'], - attrs: [ + attrsGroups: new Set(['core', 'graphicalEvent', 'presentation']), + attrs: new Set([ 'class', - 'style', 'externalResourcesRequired', 'preserveAspectRatio', - 'viewBox', 'refX', 'refY', - ], + 'style', + 'viewBox', + ]), defaults: { refX: '0', refY: '0', }, - contentGroups: [ + contentGroups: new Set([ 'animation', 'descriptive', + 'paintServer', 'shape', 'structural', - 'paintServer', - ], - content: [ + ]), + content: new Set([ 'a', 'altGlyphDef', 'clipPath', 'color-profile', 'cursor', 'filter', - 'font', 'font-face', + 'font', 'foreignObject', 'image', 'marker', @@ -1768,62 +1810,62 @@ exports.elems = { 'switch', 'text', 'view', - ], + ]), }, text: { - attrsGroups: [ + attrsGroups: new Set([ 'conditionalProcessing', 'core', 'graphicalEvent', 'presentation', - ], - attrs: [ + ]), + attrs: new Set([ 'class', - 'style', - 'externalResourcesRequired', - 'transform', - 'lengthAdjust', - 'x', - 'y', 'dx', 'dy', + 'externalResourcesRequired', + 'lengthAdjust', 'rotate', + 'style', 'textLength', - ], + 'transform', + 'x', + 'y', + ]), defaults: { x: '0', y: '0', lengthAdjust: 'spacing', }, - contentGroups: ['animation', 'descriptive', 'textContentChild'], - content: ['a'], + contentGroups: new Set(['animation', 'descriptive', 'textContentChild']), + content: new Set(['a']), }, textPath: { - attrsGroups: [ + attrsGroups: new Set([ 'conditionalProcessing', 'core', 'graphicalEvent', 'presentation', 'xlink', - ], - attrs: [ + ]), + attrs: new Set([ 'class', - 'style', + 'd', 'externalResourcesRequired', 'href', - 'xlink:href', - 'startOffset', 'method', 'spacing', - 'd', - ], + 'startOffset', + 'style', + 'xlink:href', + ]), defaults: { startOffset: '0', method: 'align', spacing: 'exact', }, - contentGroups: ['descriptive'], - content: [ + contentGroups: new Set(['descriptive']), + content: new Set([ 'a', 'altGlyph', 'animate', @@ -1831,51 +1873,51 @@ exports.elems = { 'set', 'tref', 'tspan', - ], + ]), }, title: { - attrsGroups: ['core'], - attrs: ['class', 'style'], + attrsGroups: new Set(['core']), + attrs: new Set(['class', 'style']), }, tref: { - attrsGroups: [ + attrsGroups: new Set([ 'conditionalProcessing', 'core', 'graphicalEvent', 'presentation', 'xlink', - ], - attrs: [ + ]), + attrs: new Set([ 'class', - 'style', 'externalResourcesRequired', 'href', + 'style', 'xlink:href', - ], - contentGroups: ['descriptive'], - content: ['animate', 'animateColor', 'set'], + ]), + contentGroups: new Set(['descriptive']), + content: new Set(['animate', 'animateColor', 'set']), }, tspan: { - attrsGroups: [ + attrsGroups: new Set([ 'conditionalProcessing', 'core', 'graphicalEvent', 'presentation', - ], - attrs: [ + ]), + attrs: new Set([ 'class', - 'style', - 'externalResourcesRequired', - 'x', - 'y', 'dx', 'dy', + 'externalResourcesRequired', + 'lengthAdjust', 'rotate', + 'style', 'textLength', - 'lengthAdjust', - ], - contentGroups: ['descriptive'], - content: [ + 'x', + 'y', + ]), + contentGroups: new Set(['descriptive']), + content: new Set([ 'a', 'altGlyph', 'animate', @@ -1883,129 +1925,128 @@ exports.elems = { 'set', 'tref', 'tspan', - ], + ]), }, use: { - attrsGroups: [ - 'core', + attrsGroups: new Set([ 'conditionalProcessing', + 'core', 'graphicalEvent', 'presentation', 'xlink', - ], - attrs: [ + ]), + attrs: new Set([ 'class', - 'style', 'externalResourcesRequired', - 'transform', - 'x', - 'y', - 'width', 'height', 'href', + 'style', + 'transform', + 'width', + 'x', 'xlink:href', - ], + 'y', + ]), defaults: { x: '0', y: '0', }, - contentGroups: ['animation', 'descriptive'], + contentGroups: new Set(['animation', 'descriptive']), }, view: { - attrsGroups: ['core'], - attrs: [ + attrsGroups: new Set(['core']), + attrs: new Set([ 'externalResourcesRequired', - 'viewBox', 'preserveAspectRatio', - 'zoomAndPan', + 'viewBox', 'viewTarget', - ], - contentGroups: ['descriptive'], + 'zoomAndPan', + ]), + contentGroups: new Set(['descriptive']), }, vkern: { - attrsGroups: ['core'], - attrs: ['u1', 'g1', 'u2', 'g2', 'k'], + attrsGroups: new Set(['core']), + attrs: new Set(['u1', 'g1', 'u2', 'g2', 'k']), }, }; // https://wiki.inkscape.org/wiki/index.php/Inkscape-specific_XML_attributes -exports.editorNamespaces = [ - 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', +exports.editorNamespaces = new Set([ + 'http://creativecommons.org/ns#', 'http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd', - 'http://www.inkscape.org/namespaces/inkscape', - 'http://www.bohemiancoding.com/sketch/ns', 'http://ns.adobe.com/AdobeIllustrator/10.0/', - 'http://ns.adobe.com/Graphs/1.0/', 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/', - 'http://ns.adobe.com/Variables/1.0/', - 'http://ns.adobe.com/SaveForWeb/1.0/', 'http://ns.adobe.com/Extensibility/1.0/', 'http://ns.adobe.com/Flows/1.0/', - 'http://ns.adobe.com/ImageReplacement/1.0/', 'http://ns.adobe.com/GenericCustomNamespace/1.0/', + 'http://ns.adobe.com/Graphs/1.0/', + 'http://ns.adobe.com/ImageReplacement/1.0/', + 'http://ns.adobe.com/SaveForWeb/1.0/', + 'http://ns.adobe.com/Variables/1.0/', 'http://ns.adobe.com/XPath/1.0/', + 'http://purl.org/dc/elements/1.1/', 'http://schemas.microsoft.com/visio/2003/SVGExtensions/', + 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', 'http://taptrix.com/vectorillustrator/svg_extensions', + 'http://www.bohemiancoding.com/sketch/ns', 'http://www.figma.com/figma/ns', - 'http://purl.org/dc/elements/1.1/', - 'http://creativecommons.org/ns#', - 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'http://www.inkscape.org/namespaces/inkscape', 'http://www.serif.com/', 'http://www.vector.evaxdesign.sk', -]; + 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', +]); /** * @see https://www.w3.org/TR/SVG11/linking.html#processingIRI */ -exports.referencesProps = [ +exports.referencesProps = new Set([ 'clip-path', 'color-profile', 'fill', 'filter', - 'marker-start', - 'marker-mid', 'marker-end', + 'marker-mid', + 'marker-start', 'mask', 'stroke', 'style', -]; +]); /** * @see https://www.w3.org/TR/SVG11/propidx.html */ -exports.inheritableAttrs = [ +exports.inheritableAttrs = new Set([ 'clip-rule', - 'color', - 'color-interpolation', 'color-interpolation-filters', + 'color-interpolation', 'color-profile', 'color-rendering', + 'color', 'cursor', 'direction', 'dominant-baseline', - 'fill', 'fill-opacity', 'fill-rule', - 'font', + 'fill', 'font-family', - 'font-size', 'font-size-adjust', + 'font-size', 'font-stretch', 'font-style', 'font-variant', 'font-weight', + 'font', 'glyph-orientation-horizontal', 'glyph-orientation-vertical', 'image-rendering', 'letter-spacing', - 'marker', 'marker-end', 'marker-mid', 'marker-start', + 'marker', 'paint-order', 'pointer-events', 'shape-rendering', - 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', @@ -2013,24 +2054,25 @@ exports.inheritableAttrs = [ 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', + 'stroke', 'text-anchor', 'text-rendering', 'transform', 'visibility', 'word-spacing', 'writing-mode', -]; +]); -exports.presentationNonInheritableGroupAttrs = [ - 'display', +exports.presentationNonInheritableGroupAttrs = new Set([ 'clip-path', + 'display', 'filter', 'mask', 'opacity', 'text-decoration', 'transform', 'unicode-bidi', -]; +]); /** * https://www.w3.org/TR/SVG11/single-page.html#types-ColorKeywords @@ -2229,63 +2271,69 @@ exports.colorsShortNames = { /** * @see https://www.w3.org/TR/SVG11/single-page.html#types-DataTypeColor */ -exports.colorsProps = [ +exports.colorsProps = new Set([ 'color', 'fill', - 'stroke', - 'stop-color', 'flood-color', 'lighting-color', -]; + 'stop-color', + 'stroke', +]); /** @see https://developer.mozilla.org/docs/Web/CSS/Pseudo-classes */ exports.pseudoClasses = { - displayState: ['fullscreen', 'modal', 'picture-in-picture'], - input: [ + displayState: new Set(['fullscreen', 'modal', 'picture-in-picture']), + input: new Set([ 'autofill', - 'enabled', - 'disabled', - 'read-only', - 'read-write', - 'placeholder-shown', - 'default', + 'blank', 'checked', + 'default', + 'disabled', + 'enabled', + 'in-range', 'indetermined', - 'blank', - 'valid', 'invalid', - 'in-range', + 'optional', 'out-of-range', + 'placeholder-shown', + 'read-only', + 'read-write', 'required', - 'optional', 'user-invalid', - ], - linguistic: ['dir', 'lang'], - location: [ + 'valid', + ]), + linguistic: new Set(['dir', 'lang']), + location: new Set([ 'any-link', 'link', - 'visited', 'local-link', - 'target', - 'target-within', 'scope', - ], - resourceState: ['playing', 'paused'], - timeDimensional: ['current', 'past', 'future'], - treeStructural: [ - 'root', + 'target-within', + 'target', + 'visited', + ]), + resourceState: new Set(['playing', 'paused']), + timeDimensional: new Set(['current', 'past', 'future']), + treeStructural: new Set([ 'empty', - 'nth-child', - 'nth-last-child', 'first-child', - 'last-child', - 'only-child', - 'nth-of-type', - 'nth-last-of-type', 'first-of-type', + 'last-child', 'last-of-type', + 'nth-child', + 'nth-last-child', + 'nth-last-of-type', + 'nth-of-type', + 'only-child', 'only-of-type', - ], - userAction: ['hover', 'active', 'focus', 'focus-visible', 'focus-within'], - functional: ['is', 'not', 'where', 'has'], + 'root', + ]), + userAction: new Set([ + 'active', + 'focus-visible', + 'focus-within', + 'focus', + 'hover', + ]), + functional: new Set(['is', 'not', 'where', 'has']), }; diff --git a/plugins/applyTransforms.js b/plugins/applyTransforms.js index faf03a508..3817c3d64 100644 --- a/plugins/applyTransforms.js +++ b/plugins/applyTransforms.js @@ -57,7 +57,7 @@ const applyTransforms = (root, params) => { node.attributes.style != null || Object.entries(node.attributes).some( ([name, value]) => - referencesProps.includes(name) && includesUrlReference(value), + referencesProps.has(name) && includesUrlReference(value), ) ) { return; diff --git a/plugins/collapseGroups.js b/plugins/collapseGroups.js index 6042a01ce..0534ad775 100644 --- a/plugins/collapseGroups.js +++ b/plugins/collapseGroups.js @@ -15,7 +15,7 @@ exports.description = 'collapses useless groups'; const hasAnimatedAttr = (node, name) => { if (node.type === 'element') { if ( - elemsGroups.animation.includes(node.name) && + elemsGroups.animation.has(node.name) && node.attributes.attributeName === name ) { return true; @@ -95,7 +95,7 @@ exports.fn = () => { } else if (firstChild.attributes[name] === 'inherit') { firstChild.attributes[name] = value; } else if ( - inheritableAttrs.includes(name) === false && + inheritableAttrs.has(name) === false && firstChild.attributes[name] !== value ) { return; @@ -112,7 +112,7 @@ exports.fn = () => { for (const child of node.children) { if ( child.type === 'element' && - elemsGroups.animation.includes(child.name) + elemsGroups.animation.has(child.name) ) { return; } diff --git a/plugins/convertColors.js b/plugins/convertColors.js index 6fcd49321..98517a08a 100644 --- a/plugins/convertColors.js +++ b/plugins/convertColors.js @@ -76,7 +76,7 @@ exports.fn = (_root, params) => { element: { enter: (node) => { for (const [name, value] of Object.entries(node.attributes)) { - if (collections.colorsProps.includes(name)) { + if (collections.colorsProps.has(name)) { let val = value; // convert colors to currentColor diff --git a/plugins/convertOneStopGradients.js b/plugins/convertOneStopGradients.js index bca2e1729..1cf0d66b0 100644 --- a/plugins/convertOneStopGradients.js +++ b/plugins/convertOneStopGradients.js @@ -104,7 +104,7 @@ exports.fn = (root) => { const selectorVal = `url(#${node.attributes.id})`; - const selector = colorsProps + const selector = [...colorsProps] .map((attr) => `[${attr}="${selectorVal}"]`) .join(','); const elements = querySelectorAll(root, selector); diff --git a/plugins/convertPathData.js b/plugins/convertPathData.js index b7b6beefc..5272ac775 100644 --- a/plugins/convertPathData.js +++ b/plugins/convertPathData.js @@ -147,7 +147,7 @@ exports.fn = (root, params) => { return { element: { enter: (node) => { - if (pathElems.includes(node.name) && node.attributes.d != null) { + if (pathElems.has(node.name) && node.attributes.d != null) { const computedStyle = computeStyle(stylesheet, node); precision = floatPrecision; error = diff --git a/plugins/convertStyleToAttrs.js b/plugins/convertStyleToAttrs.js index f04847cb1..a9305fc4b 100644 --- a/plugins/convertStyleToAttrs.js +++ b/plugins/convertStyleToAttrs.js @@ -109,7 +109,7 @@ exports.fn = (_root, params) => { val = val.slice(1, -1); } - if (stylingProps.includes(prop)) { + if (stylingProps.has(prop)) { newAttributes[prop] = val; return false; diff --git a/plugins/inlineStyles.js b/plugins/inlineStyles.js index d83cb6d34..84b264eac 100644 --- a/plugins/inlineStyles.js +++ b/plugins/inlineStyles.js @@ -247,7 +247,7 @@ exports.fn = (root, params) => { const property = ruleDeclaration.property; if ( - attrsGroups.presentation.includes(property) && + attrsGroups.presentation.has(property) && !selectors.some((selector) => includesAttrSelector(selector.item, property), ) diff --git a/plugins/moveElemsAttrsToGroup.js b/plugins/moveElemsAttrsToGroup.js index 5f46e6329..633f44396 100644 --- a/plugins/moveElemsAttrsToGroup.js +++ b/plugins/moveElemsAttrsToGroup.js @@ -64,7 +64,7 @@ exports.fn = (root) => { let everyChildIsPath = true; for (const child of node.children) { if (child.type === 'element') { - if (pathElems.includes(child.name) === false) { + if (!pathElems.has(child.name)) { everyChildIsPath = false; } if (initial) { @@ -72,7 +72,7 @@ exports.fn = (root) => { // 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)) { + if (inheritableAttrs.has(name)) { commonAttributes.set(name, value); } } diff --git a/plugins/moveGroupAttrsToElems.js b/plugins/moveGroupAttrsToElems.js index d774867ef..450fb2654 100644 --- a/plugins/moveGroupAttrsToElems.js +++ b/plugins/moveGroupAttrsToElems.js @@ -37,7 +37,7 @@ exports.fn = () => { node.attributes.transform != null && Object.entries(node.attributes).some( ([name, value]) => - referencesProps.includes(name) && includesUrlReference(value), + referencesProps.has(name) && includesUrlReference(value), ) === false && node.children.every( (child) => diff --git a/plugins/removeEditorsNSData.js b/plugins/removeEditorsNSData.js index e85bef898..992241ea2 100644 --- a/plugins/removeEditorsNSData.js +++ b/plugins/removeEditorsNSData.js @@ -19,7 +19,7 @@ exports.description = 'removes editors namespaces, elements and attributes'; * @type {import('./plugins-types').Plugin<'removeEditorsNSData'>} */ exports.fn = (_root, params) => { - let namespaces = editorNamespaces; + let namespaces = [...editorNamespaces]; if (Array.isArray(params.additionalNamespaces)) { namespaces = [...editorNamespaces, ...params.additionalNamespaces]; } diff --git a/plugins/removeEmptyAttrs.js b/plugins/removeEmptyAttrs.js index 83f567c3b..194d27b81 100644 --- a/plugins/removeEmptyAttrs.js +++ b/plugins/removeEmptyAttrs.js @@ -20,7 +20,7 @@ exports.fn = () => { if ( value === '' && // empty conditional processing attributes prevents elements from rendering - attrsGroups.conditionalProcessing.includes(name) === false + !attrsGroups.conditionalProcessing.has(name) ) { delete node.attributes[name]; } diff --git a/plugins/removeEmptyContainers.js b/plugins/removeEmptyContainers.js index bff3ab9c9..039073449 100644 --- a/plugins/removeEmptyContainers.js +++ b/plugins/removeEmptyContainers.js @@ -28,7 +28,7 @@ exports.fn = () => { // remove only empty non-svg containers if ( node.name === 'svg' || - elemsGroups.container.includes(node.name) === false || + !elemsGroups.container.has(node.name) || node.children.length !== 0 ) { return; diff --git a/plugins/removeHiddenElems.js b/plugins/removeHiddenElems.js index 6ed2750bb..75720a068 100644 --- a/plugins/removeHiddenElems.js +++ b/plugins/removeHiddenElems.js @@ -114,7 +114,7 @@ exports.fn = (root, params) => { element: { enter: (node, parentNode) => { // transparent non-rendering elements still apply where referenced - if (nonRendering.includes(node.name)) { + if (nonRendering.has(node.name)) { if (node.attributes.id == null) { detachNodeFromParent(node, parentNode); return visitSkip; diff --git a/plugins/removeNonInheritableGroupAttrs.js b/plugins/removeNonInheritableGroupAttrs.js index faa93dffe..73e6bcdad 100644 --- a/plugins/removeNonInheritableGroupAttrs.js +++ b/plugins/removeNonInheritableGroupAttrs.js @@ -24,9 +24,9 @@ exports.fn = () => { if (node.name === 'g') { for (const name of Object.keys(node.attributes)) { if ( - attrsGroups.presentation.includes(name) === true && - inheritableAttrs.includes(name) === false && - presentationNonInheritableGroupAttrs.includes(name) === false + attrsGroups.presentation.has(name) && + !inheritableAttrs.has(name) && + !presentationNonInheritableGroupAttrs.has(name) ) { delete node.attributes[name]; } diff --git a/plugins/removeUnknownsAndDefaults.js b/plugins/removeUnknownsAndDefaults.js index c39d3c030..e76e4e5fe 100644 --- a/plugins/removeUnknownsAndDefaults.js +++ b/plugins/removeUnknownsAndDefaults.js @@ -197,7 +197,7 @@ exports.fn = (root, params) => { if (uselessOverrides && node.attributes.id == null) { const style = computedParentStyle?.[name]; if ( - presentationNonInheritableGroupAttrs.includes(name) === false && + presentationNonInheritableGroupAttrs.has(name) === false && style != null && style.type === 'static' && style.value === value diff --git a/plugins/removeUselessDefs.js b/plugins/removeUselessDefs.js index 46461a98f..d642b874e 100644 --- a/plugins/removeUselessDefs.js +++ b/plugins/removeUselessDefs.js @@ -39,7 +39,7 @@ exports.fn = () => { } node.children = usefulNodes; } else if ( - elemsGroups.nonRendering.includes(node.name) && + elemsGroups.nonRendering.has(node.name) && node.attributes.id == null ) { detachNodeFromParent(node, parentNode); diff --git a/plugins/removeUselessStrokeAndFill.js b/plugins/removeUselessStrokeAndFill.js index f5f998551..95193fa63 100644 --- a/plugins/removeUselessStrokeAndFill.js +++ b/plugins/removeUselessStrokeAndFill.js @@ -46,7 +46,7 @@ exports.fn = (root, params) => { if (node.attributes.id != null) { return visitSkip; } - if (elemsGroups.shape.includes(node.name) == false) { + if (!elemsGroups.shape.has(node.name)) { return; } const computedStyle = computeStyle(stylesheet, node); diff --git a/plugins/removeViewBox.js b/plugins/removeViewBox.js index 5618d8bd7..c0f23de26 100644 --- a/plugins/removeViewBox.js +++ b/plugins/removeViewBox.js @@ -3,7 +3,7 @@ exports.name = 'removeViewBox'; exports.description = 'removes viewBox attribute when possible'; -const viewBoxElems = ['svg', 'pattern', 'symbol']; +const viewBoxElems = new Set(['pattern', 'svg', 'symbol']); /** * Remove viewBox attr which coincides with a width/height box. @@ -24,7 +24,7 @@ exports.fn = () => { element: { enter: (node, parentNode) => { if ( - viewBoxElems.includes(node.name) && + viewBoxElems.has(node.name) && node.attributes.viewBox != null && node.attributes.width != null && node.attributes.height != null diff --git a/plugins/removeXlink.js b/plugins/removeXlink.js index 190e4e3d1..0411cc145 100644 --- a/plugins/removeXlink.js +++ b/plugins/removeXlink.js @@ -28,17 +28,17 @@ const SHOW_TO_TARGET = { * Elements that use xlink:href, but were deprecated in SVG 2 and therefore * don't support the SVG 2 href attribute. * - * @type {string[]} + * @type {Set} * @see https://developer.mozilla.org/docs/Web/SVG/Attribute/xlink:href * @see https://developer.mozilla.org/docs/Web/SVG/Attribute/href */ -const LEGACY_ELEMENTS = [ +const LEGACY_ELEMENTS = new Set([ 'cursor', 'filter', 'font-face-uri', 'glyphRef', 'tref', -]; +]); /** * @param {XastElement} node @@ -170,7 +170,7 @@ exports.fn = (_, params) => { if ( hrefAttrs.length > 0 && - LEGACY_ELEMENTS.includes(node.name) && + LEGACY_ELEMENTS.has(node.name) && !includeLegacy ) { hrefAttrs From 6996fca1a6e06e64fde4d54cb2b11c8faffad91c Mon Sep 17 00:00:00 2001 From: Seth Falco Date: Sun, 24 Dec 2023 23:05:12 +0000 Subject: [PATCH 05/14] refactor: improve performance of stringifyPathData (#1900) --- lib/path.js | 130 +++++++++++++++++++++++++++++----------------- lib/svgo/tools.js | 23 ++++---- 2 files changed, 93 insertions(+), 60 deletions(-) diff --git a/lib/path.js b/lib/path.js index 1e383dd98..a5f7d5bfc 100644 --- a/lib/path.js +++ b/lib/path.js @@ -243,15 +243,21 @@ const parsePathData = (string) => { exports.parsePathData = parsePathData; /** - * @type {(number: number, precision?: number) => string} + * @type {(number: number, precision?: number) => { + * roundedStr: string, + * rounded: number + * }} */ -const stringifyNumber = (number, precision) => { +const roundAndStringify = (number, precision) => { if (precision != null) { const ratio = 10 ** precision; number = Math.round(number * ratio) / ratio; } - // remove zero whole from decimal number - return removeLeadingZero(number); + + return { + roundedStr: removeLeadingZero(number), + rounded: number, + }; }; /** @@ -267,29 +273,35 @@ const stringifyNumber = (number, precision) => { */ const stringifyArgs = (command, args, precision, disableSpaceAfterFlags) => { let result = ''; - let prev = ''; - for (let i = 0; i < args.length; i += 1) { - const number = args[i]; - const numberString = stringifyNumber(number, precision); + let previous; + + for (let i = 0; i < args.length; i++) { + const { roundedStr, rounded } = roundAndStringify(args[i], precision); if ( disableSpaceAfterFlags && (command === 'A' || command === 'a') && // consider combined arcs (i % 7 === 4 || i % 7 === 5) ) { - result += numberString; - } else if (i === 0 || numberString.startsWith('-')) { + result += roundedStr; + } else if (i === 0 || rounded < 0) { // avoid space before first and negative numbers - result += numberString; - } else if (prev.includes('.') && numberString.startsWith('.')) { + result += roundedStr; + } else if ( + !Number.isInteger(previous) && + rounded != 0 && + rounded < 1 && + rounded > -1 + ) { // remove space before decimal with zero whole // only when previous number is also decimal - result += numberString; + result += roundedStr; } else { - result += ` ${numberString}`; + result += ` ${roundedStr}`; } - prev = numberString; + previous = rounded; } + return result; }; @@ -302,48 +314,68 @@ const stringifyArgs = (command, args, precision, disableSpaceAfterFlags) => { */ /** - * @type {(options: StringifyPathDataOptions) => string} + * @param {StringifyPathDataOptions} options + * @returns {string} */ const stringifyPathData = ({ pathData, precision, disableSpaceAfterFlags }) => { - // combine sequence of the same commands - let combined = []; - for (let i = 0; i < pathData.length; i += 1) { + if (pathData.length === 1) { + const { command, args } = pathData[0]; + return ( + command + stringifyArgs(command, args, precision, disableSpaceAfterFlags) + ); + } + + let result = ''; + let prev = { ...pathData[0] }; + + // match leading moveto with following lineto + if (pathData[1].command === 'L') { + prev.command = 'M'; + } else if (pathData[1].command === 'l') { + prev.command = 'm'; + } + + for (let i = 1; i < pathData.length; i++) { const { command, args } = pathData[i]; - if (i === 0) { - combined.push({ command, args }); - } else { - /** - * @type {PathDataItem} - */ - const last = combined[combined.length - 1]; - // match leading moveto with following lineto - if (i === 1) { - if (command === 'L') { - last.command = 'M'; - } - if (command === 'l') { - last.command = 'm'; - } + if ( + (prev.command === command && + prev.command !== 'M' && + prev.command !== 'm') || + // combine matching moveto and lineto sequences + (prev.command === 'M' && command === 'L') || + (prev.command === 'm' && command === 'l') + ) { + prev.args = [...prev.args, ...args]; + if (i === pathData.length - 1) { + result += + prev.command + + stringifyArgs( + prev.command, + prev.args, + precision, + disableSpaceAfterFlags, + ); } - if ( - (last.command === command && - last.command !== 'M' && - last.command !== 'm') || - // combine matching moveto and lineto sequences - (last.command === 'M' && command === 'L') || - (last.command === 'm' && command === 'l') - ) { - last.args = [...last.args, ...args]; + } else { + result += + prev.command + + stringifyArgs( + prev.command, + prev.args, + precision, + disableSpaceAfterFlags, + ); + + if (i === pathData.length - 1) { + result += + command + + stringifyArgs(command, args, precision, disableSpaceAfterFlags); } else { - combined.push({ command, args }); + prev = { command, args }; } } } - let result = ''; - for (const { command, args } of combined) { - result += - command + stringifyArgs(command, args, precision, disableSpaceAfterFlags); - } + return result; }; exports.stringifyPathData = stringifyPathData; diff --git a/lib/svgo/tools.js b/lib/svgo/tools.js index e778034dc..afbda50c0 100644 --- a/lib/svgo/tools.js +++ b/lib/svgo/tools.js @@ -124,23 +124,24 @@ exports.cleanupOutData = (data, params, command) => { /** * Remove floating-point numbers leading zero. * + * @param {number} value + * @returns {string} * @example * 0.5 → .5 - * - * @example * -0.5 → -.5 - * - * @type {(num: number) => string} */ -const removeLeadingZero = (num) => { - var strNum = num.toString(); +const removeLeadingZero = (value) => { + const strValue = value.toString(); - if (0 < num && num < 1 && strNum.charAt(0) === '0') { - strNum = strNum.slice(1); - } else if (-1 < num && num < 0 && strNum.charAt(1) === '0') { - strNum = strNum.charAt(0) + strNum.slice(2); + if (0 < value && value < 1 && strValue.startsWith('0')) { + return strValue.slice(1); } - return strNum; + + if (-1 < value && value < 0 && strValue[1] === '0') { + return strValue[0] + strValue.slice(2); + } + + return strValue; }; exports.removeLeadingZero = removeLeadingZero; From db78c4b7b5e3cc04f83cdf91fb0ac658794266c5 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sun, 24 Dec 2023 18:11:22 -0500 Subject: [PATCH 06/14] chore: Remove unused argument "input" in calls to optmize() (#1901) --- test/svgo/_index.test.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/svgo/_index.test.js b/test/svgo/_index.test.js index e9a596d76..a9e5f06c7 100644 --- a/test/svgo/_index.test.js +++ b/test/svgo/_index.test.js @@ -28,17 +28,16 @@ describe('svgo', () => { }); it('should handle plugins order properly', async () => { const [original, expected] = await parseFixture('plugins-order.svg'); - const result = optimize(original, { input: 'file', path: 'input.svg' }); + const result = optimize(original, { path: 'input.svg' }); expect(normalize(result.data)).toEqual(expected); }); it('should handle empty svg tag', async () => { - const result = optimize('', { input: 'file', path: 'input.svg' }); + const result = optimize('', { path: 'input.svg' }); expect(result.data).toEqual(''); }); it('should preserve style specificity over attributes', async () => { const [original, expected] = await parseFixture('style-specifity.svg'); const result = optimize(original, { - input: 'file', path: 'input.svg', js2svg: { pretty: true }, }); From 52961ba28c359a35c7c8dd5e76ca6a1fcc52fa0d Mon Sep 17 00:00:00 2001 From: Seth Falco Date: Mon, 25 Dec 2023 14:06:44 +0000 Subject: [PATCH 07/14] ci: dont create diff png if no_diff (#1903) --- test/regression.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/test/regression.js b/test/regression.js index c76e3edd2..6eed809a3 100644 --- a/test/regression.js +++ b/test/regression.js @@ -42,13 +42,14 @@ const runTests = async (list) => { await page.goto(`http://localhost:5000/optimized/${name}`); const optimizedBufferPromise = page.screenshot(screenshotOptions); + const writeDiffs = process.env.NO_DIFF == null; + const diff = writeDiffs && new PNG({ width, height }); const originalPng = PNG.sync.read(originalBuffer); const optimizedPng = PNG.sync.read(await optimizedBufferPromise); - const diff = new PNG({ width, height }); const matched = pixelmatch( originalPng.data, optimizedPng.data, - diff.data, + diff ? diff.data : null, width, height, ); @@ -59,7 +60,7 @@ const runTests = async (list) => { } else { mismatched++; console.error(`${name} is mismatched`); - if (process.env.NO_DIFF == null) { + if (diff) { const file = path.join( __dirname, 'regression-diffs', @@ -73,7 +74,6 @@ const runTests = async (list) => { const worker = async () => { let item; const page = await context.newPage(); - await page.setViewportSize({ width, height }); while ((item = list.pop())) { await processFile(page, item); } @@ -81,7 +81,10 @@ const runTests = async (list) => { }; const browser = await chromium.launch(); - const context = await browser.newContext({ javaScriptEnabled: false }); + const context = await browser.newContext({ + javaScriptEnabled: false, + viewport: { width, height }, + }); await Promise.all( Array.from(new Array(os.cpus().length * 2), () => worker()), ); From a287a2a8c2e3f86e31996534b6ba00baec3b9cde Mon Sep 17 00:00:00 2001 From: Seth Falco Date: Mon, 25 Dec 2023 20:54:48 +0000 Subject: [PATCH 08/14] refactor(mergePaths): improve performance (#1904) --- plugins/mergePaths.js | 64 +++++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/plugins/mergePaths.js b/plugins/mergePaths.js index 05c7ac705..e7b35e5a7 100644 --- a/plugins/mergePaths.js +++ b/plugins/mergePaths.js @@ -1,6 +1,9 @@ 'use strict'; -const { detachNodeFromParent } = require('../lib/xast.js'); +/** + * @typedef {import('../lib/types').XastChild} XastChild + */ + const { collectStylesheet, computeStyle } = require('../lib/style.js'); const { path2js, js2path, intersects } = require('./_path.js'); @@ -25,12 +28,18 @@ exports.fn = (root, params) => { return { element: { enter: (node) => { - let prevChild = null; + if (node.children.length <= 1) { + return; + } + + /** @type {XastChild[]} */ + const elementsToRemove = []; + let prevChild = node.children[0]; + + for (let i = 1; i < node.children.length; i++) { + const child = node.children[i]; - for (const child of node.children) { - // skip if previous element is not path or contains animation elements if ( - prevChild == null || prevChild.type !== 'element' || prevChild.name !== 'path' || prevChild.children.length !== 0 || @@ -40,7 +49,6 @@ exports.fn = (root, params) => { continue; } - // skip if element is not path or contains animation elements if ( child.type !== 'element' || child.name !== 'path' || @@ -51,7 +59,6 @@ exports.fn = (root, params) => { continue; } - // preserve paths with markers const computedStyle = computeStyle(stylesheet, child); if ( computedStyle['marker-start'] || @@ -62,36 +69,45 @@ exports.fn = (root, params) => { continue; } - const prevChildAttrs = Object.keys(prevChild.attributes); const childAttrs = Object.keys(child.attributes); - let attributesAreEqual = prevChildAttrs.length === childAttrs.length; - for (const name of childAttrs) { - if (name !== 'd') { - if ( - prevChild.attributes[name] == null || - prevChild.attributes[name] !== child.attributes[name] - ) { - attributesAreEqual = false; - } - } + if (childAttrs.length !== Object.keys(prevChild.attributes).length) { + prevChild = child; + continue; + } + + const areAttrsEqual = childAttrs.some((attr) => { + return ( + attr !== 'd' && + prevChild.type === 'element' && + prevChild.attributes[attr] !== child.attributes[attr] + ); + }); + + if (areAttrsEqual) { + prevChild = child; + continue; } + const prevPathJS = path2js(prevChild); const curPathJS = path2js(child); - if ( - attributesAreEqual && - (force || !intersects(prevPathJS, curPathJS)) - ) { - js2path(prevChild, prevPathJS.concat(curPathJS), { + if (force || !intersects(prevPathJS, curPathJS)) { + prevPathJS.push(...curPathJS); + js2path(prevChild, prevPathJS, { floatPrecision, noSpaceAfterFlags, }); - detachNodeFromParent(child, node); + + elementsToRemove.push(child); continue; } prevChild = child; } + + node.children = node.children.filter( + (child) => !elementsToRemove.includes(child), + ); }, }, }; From 90406f796718dc63a6723fcda8b7c84379544972 Mon Sep 17 00:00:00 2001 From: mozzie Date: Tue, 26 Dec 2023 02:53:44 +0200 Subject: [PATCH 09/14] refactor(mergePaths): improve performance on large files (#1764) --- package.json | 2 +- plugins/convertPathData.js | 3 ++- plugins/mergePaths.js | 54 +++++++++++++++++++++++++++++++------- yarn.lock | 18 ++++++------- 4 files changed, 56 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index e0b4873c4..3b51fb00e 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,6 @@ "rollup": "^2.79.1", "rollup-plugin-terser": "^7.0.2", "tar-stream": "^3.1.6", - "typescript": "^4.8.4" + "typescript": "^5.3.3" } } diff --git a/plugins/convertPathData.js b/plugins/convertPathData.js index 5272ac775..23c9b56ab 100644 --- a/plugins/convertPathData.js +++ b/plugins/convertPathData.js @@ -154,7 +154,8 @@ exports.fn = (root, params) => { precision !== false ? +Math.pow(0.1, precision).toFixed(precision) : 1e-2; - roundData = precision > 0 && precision < 20 ? strongRound : round; + roundData = + precision && precision > 0 && precision < 20 ? strongRound : round; if (makeArcs) { arcThreshold = makeArcs.threshold; arcTolerance = makeArcs.tolerance; diff --git a/plugins/mergePaths.js b/plugins/mergePaths.js index e7b35e5a7..fe982c136 100644 --- a/plugins/mergePaths.js +++ b/plugins/mergePaths.js @@ -1,7 +1,9 @@ 'use strict'; /** + * @typedef {import("../lib/types").PathDataItem} PathDataItem * @typedef {import('../lib/types').XastChild} XastChild + * @typedef {import('../lib/types').XastElement} XastElement */ const { collectStylesheet, computeStyle } = require('../lib/style.js'); @@ -35,6 +37,19 @@ exports.fn = (root, params) => { /** @type {XastChild[]} */ const elementsToRemove = []; let prevChild = node.children[0]; + let prevPathData = null; + + /** + * @param {XastElement} child + * @param {PathDataItem[]} pathData + */ + const updatePreviousPath = (child, pathData) => { + js2path(child, pathData, { + floatPrecision, + noSpaceAfterFlags, + }); + prevPathData = null; + }; for (let i = 1; i < node.children.length; i++) { const child = node.children[i]; @@ -45,6 +60,9 @@ exports.fn = (root, params) => { prevChild.children.length !== 0 || prevChild.attributes.d == null ) { + if (prevPathData && prevChild.type === 'element') { + updatePreviousPath(prevChild, prevPathData); + } prevChild = child; continue; } @@ -55,6 +73,9 @@ exports.fn = (root, params) => { child.children.length !== 0 || child.attributes.d == null ) { + if (prevPathData) { + updatePreviousPath(prevChild, prevPathData); + } prevChild = child; continue; } @@ -65,12 +86,17 @@ exports.fn = (root, params) => { computedStyle['marker-mid'] || computedStyle['marker-end'] ) { + if (prevPathData) { + updatePreviousPath(prevChild, prevPathData); + } prevChild = child; continue; } - const childAttrs = Object.keys(child.attributes); if (childAttrs.length !== Object.keys(prevChild.attributes).length) { + if (prevPathData) { + updatePreviousPath(prevChild, prevPathData); + } prevChild = child; continue; } @@ -84,25 +110,33 @@ exports.fn = (root, params) => { }); if (areAttrsEqual) { + if (prevPathData) { + updatePreviousPath(prevChild, prevPathData); + } prevChild = child; continue; } - const prevPathJS = path2js(prevChild); - const curPathJS = path2js(child); - - if (force || !intersects(prevPathJS, curPathJS)) { - prevPathJS.push(...curPathJS); - js2path(prevChild, prevPathJS, { - floatPrecision, - noSpaceAfterFlags, - }); + const hasPrevPath = prevPathData != null; + const currentPathData = path2js(child); + prevPathData = prevPathData ?? path2js(prevChild); + if (force || !intersects(prevPathData, currentPathData)) { + prevPathData.push(...currentPathData); elementsToRemove.push(child); continue; } + if (hasPrevPath) { + updatePreviousPath(prevChild, prevPathData); + } + prevChild = child; + prevPathData = null; + } + + if (prevPathData && prevChild.type === 'element') { + updatePreviousPath(prevChild, prevPathData); } node.children = node.children.filter( diff --git a/yarn.lock b/yarn.lock index 49e179584..8baf0bdf7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4449,7 +4449,7 @@ __metadata: rollup: ^2.79.1 rollup-plugin-terser: ^7.0.2 tar-stream: ^3.1.6 - typescript: ^4.8.4 + typescript: ^5.3.3 bin: svgo: ./bin/svgo languageName: unknown @@ -4572,23 +4572,23 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^4.8.4": - version: 4.8.4 - resolution: "typescript@npm:4.8.4" +"typescript@npm:^5.3.3": + version: 5.3.3 + resolution: "typescript@npm:5.3.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 3e4f061658e0c8f36c820802fa809e0fd812b85687a9a2f5430bc3d0368e37d1c9605c3ce9b39df9a05af2ece67b1d844f9f6ea8ff42819f13bcb80f85629af0 + checksum: 2007ccb6e51bbbf6fde0a78099efe04dc1c3dfbdff04ca3b6a8bc717991862b39fd6126c0c3ebf2d2d98ac5e960bcaa873826bb2bb241f14277034148f41f6a2 languageName: node linkType: hard -"typescript@patch:typescript@^4.8.4#~builtin": - version: 4.8.4 - resolution: "typescript@patch:typescript@npm%3A4.8.4#~builtin::version=4.8.4&hash=a1c5e5" +"typescript@patch:typescript@^5.3.3#~builtin": + version: 5.3.3 + resolution: "typescript@patch:typescript@npm%3A5.3.3#~builtin::version=5.3.3&hash=a1c5e5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 563a0ef47abae6df27a9a3ab38f75fc681f633ccf1a3502b1108e252e187787893de689220f4544aaf95a371a4eb3141e4a337deb9895de5ac3c1ca76430e5f0 + checksum: f61375590b3162599f0f0d5b8737877ac0a7bc52761dbb585d67e7b8753a3a4c42d9a554c4cc929f591ffcf3a2b0602f65ae3ce74714fd5652623a816862b610 languageName: node linkType: hard From 562152a7292ba402992d0e04a2530e1d7e4a55ea Mon Sep 17 00:00:00 2001 From: Kendell R Date: Wed, 27 Dec 2023 07:53:01 -0500 Subject: [PATCH 10/14] feat(convertPathData): use the sagitta of arcs to round and convert to lines when applicable (#1873) --- docs/03-plugins/convert-path-data.mdx | 3 ++ plugins/convertPathData.js | 44 ++++++++++++++++++++++++++- plugins/plugins-types.d.ts | 1 + test/plugins/convertPathData.14.svg | 4 +-- test/plugins/convertPathData.30.svg | 2 +- test/plugins/convertPathData.31.svg | 2 +- test/plugins/convertPathData.32.svg | 13 ++++++++ test/plugins/convertPathData.33.svg | 13 ++++++++ 8 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 test/plugins/convertPathData.32.svg create mode 100644 test/plugins/convertPathData.33.svg diff --git a/docs/03-plugins/convert-path-data.mdx b/docs/03-plugins/convert-path-data.mdx index 23ca9e7e0..66848688f 100644 --- a/docs/03-plugins/convert-path-data.mdx +++ b/docs/03-plugins/convert-path-data.mdx @@ -30,6 +30,9 @@ svgo: transformPrecision: description: Number of decimal places to round to, using conventional rounding rules. default: 5 + smartArcRounding: + description: Round the radius of circular arcs when the effective change is under the error. The effective change is determined using the sagitta of the arc. + default: true removeUseless: description: Remove redundant path commands that don't draw anything. default: true diff --git a/plugins/convertPathData.js b/plugins/convertPathData.js index 23c9b56ab..2a6c66507 100644 --- a/plugins/convertPathData.js +++ b/plugins/convertPathData.js @@ -50,6 +50,7 @@ let arcTolerance; * curveSmoothShorthands: boolean, * floatPrecision: number | false, * transformPrecision: number, + * smartArcRounding: boolean, * removeUseless: boolean, * collapseRepeated: boolean, * utilizeAbsolute: boolean, @@ -100,6 +101,7 @@ exports.fn = (root, params) => { curveSmoothShorthands = true, floatPrecision = 3, transformPrecision = 5, + smartArcRounding = true, removeUseless = true, collapseRepeated = true, utilizeAbsolute = true, @@ -122,6 +124,7 @@ exports.fn = (root, params) => { curveSmoothShorthands, floatPrecision, transformPrecision, + smartArcRounding, removeUseless, collapseRepeated, utilizeAbsolute, @@ -638,6 +641,24 @@ function filters( } } + // round arc radius more accurately + // eg m 0 0 a 1234.567 1234.567 0 0 1 10 0 -> m 0 0 a 1235 1235 0 0 1 10 0 + const sagitta = command === 'a' ? calculateSagitta(data) : undefined; + if (params.smartArcRounding && sagitta !== undefined && precision) { + for (let precisionNew = precision; precisionNew >= 0; precisionNew--) { + const radius = toFixed(data[0], precisionNew); + const sagittaNew = /** @type {number} */ ( + calculateSagitta([radius, radius, ...data.slice(2)]) + ); + if (Math.abs(sagitta - sagittaNew) < error) { + data[0] = radius; + data[1] = radius; + } else { + break; + } + } + } + // convert straight curves into lines segments if (params.straightCurves) { if ( @@ -660,7 +681,12 @@ function filters( ) { command = 'l'; data = data.slice(-2); - } else if (command === 'a' && (data[0] === 0 || data[1] === 0)) { + } else if ( + command === 'a' && + (data[0] === 0 || + data[1] === 0 || + (sagitta !== undefined && sagitta < error)) + ) { command = 'l'; data = data.slice(-2); } @@ -1064,6 +1090,22 @@ function isCurveStraightLine(data) { return true; } +/** + * Calculates the sagitta of an arc if possible. + * + * @type {(data: number[]) => number | undefined} + * @see https://wikipedia.org/wiki/Sagitta_(geometry)#Formulas + */ +function calculateSagitta(data) { + if (data[3] === 1) return undefined; + + const [rx, ry] = data; + if (Math.abs(rx - ry) > error) return undefined; + const chord = Math.sqrt(data[5] ** 2 + data[6] ** 2); + if (chord > rx * 2) return undefined; + return rx - Math.sqrt(rx ** 2 - 0.25 * chord ** 2); +} + /** * Converts next curve from shorthand to full form using the current curve data. * diff --git a/plugins/plugins-types.d.ts b/plugins/plugins-types.d.ts index ff25f68cd..1d4740e7b 100644 --- a/plugins/plugins-types.d.ts +++ b/plugins/plugins-types.d.ts @@ -46,6 +46,7 @@ type DefaultPlugins = { curveSmoothShorthands?: boolean; floatPrecision?: number | false; transformPrecision?: number; + smartArcRounding?: boolean; removeUseless?: boolean; collapseRepeated?: boolean; utilizeAbsolute?: boolean; diff --git a/test/plugins/convertPathData.14.svg b/test/plugins/convertPathData.14.svg index c2b72a9c3..a84c400fb 100644 --- a/test/plugins/convertPathData.14.svg +++ b/test/plugins/convertPathData.14.svg @@ -16,10 +16,10 @@ - + - + diff --git a/test/plugins/convertPathData.30.svg b/test/plugins/convertPathData.30.svg index 1d29829d6..cdca02732 100644 --- a/test/plugins/convertPathData.30.svg +++ b/test/plugins/convertPathData.30.svg @@ -10,4 +10,4 @@ Should use relative instead of absolute here. (Checking for proper rounding of r - \ No newline at end of file + diff --git a/test/plugins/convertPathData.31.svg b/test/plugins/convertPathData.31.svg index 9a4d43348..c66efbb2e 100644 --- a/test/plugins/convertPathData.31.svg +++ b/test/plugins/convertPathData.31.svg @@ -10,4 +10,4 @@ Should have no commands here. (Checking for removing useless before trying to co - \ No newline at end of file + diff --git a/test/plugins/convertPathData.32.svg b/test/plugins/convertPathData.32.svg new file mode 100644 index 000000000..97df96b12 --- /dev/null +++ b/test/plugins/convertPathData.32.svg @@ -0,0 +1,13 @@ +Should convert arc to line + +=== + + + + + +@@@ + + + + diff --git a/test/plugins/convertPathData.33.svg b/test/plugins/convertPathData.33.svg new file mode 100644 index 000000000..b8125535b --- /dev/null +++ b/test/plugins/convertPathData.33.svg @@ -0,0 +1,13 @@ +Should round radius considering the sagitta + +=== + + + + + +@@@ + + + + From c2cacc7060a87ce1c2cd81bb7fa4245838189e78 Mon Sep 17 00:00:00 2001 From: Seth Falco Date: Wed, 27 Dec 2023 12:54:30 +0000 Subject: [PATCH 11/14] chore: move health files to .github (#1906) --- .github/FUNDING.yml | 3 -- SECURITY.md | 5 --- lib/parser.js | 2 +- lib/path.js | 6 +-- lib/path.test.js | 4 +- lib/style.js | 10 ++--- lib/svgo/coa.js | 10 ++--- lib/svgo/tools.js | 2 +- lib/types.d.ts | 10 ++--- lib/xast.js | 2 +- lib/xast.test.js | 12 ++--- plugins/_path.js | 44 +++++++++---------- plugins/_transforms.js | 20 ++++----- plugins/applyTransforms.js | 4 +- plugins/cleanupIds.js | 6 +-- plugins/convertColors.js | 2 +- plugins/convertShapeToPath.js | 10 ++--- plugins/convertTransform.js | 20 ++++----- plugins/inlineStyles.js | 14 +++--- plugins/minifyStyles.js | 2 +- plugins/removeEditorsNSData.js | 2 +- plugins/removeOffCanvasPaths.js | 2 +- plugins/removeUselessDefs.js | 4 +- plugins/reusePaths.js | 2 +- test/regression-extract.js | 2 +- test/svgo/_index.test.js | 2 +- ...le-specifity.svg => style-specificity.svg} | 0 27 files changed, 95 insertions(+), 107 deletions(-) delete mode 100644 .github/FUNDING.yml delete mode 100644 SECURITY.md rename test/svgo/{style-specifity.svg => style-specificity.svg} (100%) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index e09ac4c74..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -# These are supported funding model platforms - -open_collective: svgo diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 7a6e08a62..000000000 --- a/SECURITY.md +++ /dev/null @@ -1,5 +0,0 @@ -# Security Policy - -## Reporting a Vulnerability - -Please report security vulnerabilities to [trysound@yandex.ru](mailto:trysound@yandex.ru). diff --git a/lib/parser.js b/lib/parser.js index 4359373a9..4637acf15 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -100,7 +100,7 @@ const parseSvg = (data, from) => { */ let current = root; /** - * @type {Array} + * @type {XastParent[]} */ const stack = [root]; diff --git a/lib/path.js b/lib/path.js index a5f7d5bfc..8e6b47221 100644 --- a/lib/path.js +++ b/lib/path.js @@ -135,11 +135,11 @@ const readNumber = (string, cursor) => { }; /** - * @type {(string: string) => Array} + * @type {(string: string) => PathDataItem[]} */ const parsePathData = (string) => { /** - * @type {Array} + * @type {PathDataItem[]} */ const pathData = []; /** @@ -307,7 +307,7 @@ const stringifyArgs = (command, args, precision, disableSpaceAfterFlags) => { /** * @typedef {{ - * pathData: Array; + * pathData: PathDataItem[]; * precision?: number; * disableSpaceAfterFlags?: boolean; * }} StringifyPathDataOptions diff --git a/lib/path.test.js b/lib/path.test.js index 8282ea95c..91e6eaa91 100644 --- a/lib/path.test.js +++ b/lib/path.test.js @@ -146,7 +146,7 @@ describe('stringify path data', () => { }); it('should configure precision', () => { /** - * @type {Array} + * @type {PathDataItem[]} */ const pathData = [ { command: 'M', args: [0, -1.9876] }, @@ -169,7 +169,7 @@ describe('stringify path data', () => { }); it('allows to avoid spaces after arc flags', () => { /** - * @type {Array} + * @type {PathDataItem[]} */ const pathData = [ { command: 'M', args: [0, 0] }, diff --git a/lib/style.js b/lib/style.js index e54993a3c..fbfffc6b4 100644 --- a/lib/style.js +++ b/lib/style.js @@ -34,7 +34,7 @@ const csstreeWalkSkip = csstree.walk.skip; */ const parseRule = (ruleNode, dynamic) => { /** - * @type {Array} + * @type {StylesheetDeclaration[]} */ const declarations = []; // collect declarations @@ -74,10 +74,10 @@ const parseRule = (ruleNode, dynamic) => { }; /** - * @type {(css: string, dynamic: boolean) => Array} + * @type {(css: string, dynamic: boolean) => StylesheetRule[]} */ const parseStylesheet = (css, dynamic) => { - /** @type {Array} */ + /** @type {StylesheetRule[]} */ const rules = []; const ast = csstree.parse(css, { parseValue: false, @@ -108,10 +108,10 @@ const parseStylesheet = (css, dynamic) => { }; /** - * @type {(css: string) => Array} + * @type {(css: string) => StylesheetDeclaration[]} */ const parseStyleDeclarations = (css) => { - /** @type {Array} */ + /** @type {StylesheetDeclaration[]} */ const declarations = []; const ast = csstree.parse(css, { context: 'declarationList', diff --git a/lib/svgo/coa.js b/lib/svgo/coa.js index 331812f5e..8e638d068 100644 --- a/lib/svgo/coa.js +++ b/lib/svgo/coa.js @@ -254,10 +254,7 @@ async function action(args, opts, command) { process.stdin .on('data', (chunk) => (data += chunk)) .once('end', () => - processSVGData(config, { input: 'string' }, data, file).then( - resolve, - reject, - ), + processSVGData(config, null, data, file).then(resolve, reject), ); }); // file @@ -271,7 +268,7 @@ async function action(args, opts, command) { } else if (opts.string) { var data = decodeSVGDatauri(opts.string); - return processSVGData(config, { input: 'string' }, data, output[0]); + return processSVGData(config, null, data, output[0]); } } @@ -372,8 +369,7 @@ function getFilesDescriptions(config, dir, files, output) { */ function optimizeFile(config, file, output) { return fs.promises.readFile(file, 'utf8').then( - (data) => - processSVGData(config, { input: 'file', path: file }, data, output, file), + (data) => processSVGData(config, { path: file }, data, output, file), (error) => checkOptimizeFileError(config, file, output, error), ); } diff --git a/lib/svgo/tools.js b/lib/svgo/tools.js index afbda50c0..618ca70aa 100644 --- a/lib/svgo/tools.js +++ b/lib/svgo/tools.js @@ -74,7 +74,7 @@ exports.decodeSVGDatauri = (str) => { * @example * [0, -1, .5, .5] → "0-1 .5.5" * - * @type {(data: Array, params: CleanupOutDataParams, command?: PathDataCommand) => string} + * @type {(data: number[], params: CleanupOutDataParams, command?: PathDataCommand) => string} */ exports.cleanupOutData = (data, params, command) => { let str = ''; diff --git a/lib/types.d.ts b/lib/types.d.ts index 75818c4a6..68dc97478 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -31,7 +31,7 @@ export type XastElement = { type: 'element'; name: string; attributes: Record; - children: Array; + children: XastChild[]; }; export type XastChild = @@ -44,7 +44,7 @@ export type XastChild = export type XastRoot = { type: 'root'; - children: Array; + children: XastChild[]; }; export type XastParent = XastRoot | XastElement; @@ -123,11 +123,11 @@ export type StylesheetRule = { dynamic: boolean; selector: string; specificity: Specificity; - declarations: Array; + declarations: StylesheetDeclaration[]; }; export type Stylesheet = { - rules: Array; + rules: StylesheetRule[]; parents: Map; }; @@ -168,7 +168,7 @@ export type PathDataCommand = export type PathDataItem = { command: PathDataCommand; - args: Array; + args: number[]; }; export type DataUri = 'base64' | 'enc' | 'unenc'; diff --git a/lib/xast.js b/lib/xast.js index 7f14e2a87..9dca96460 100644 --- a/lib/xast.js +++ b/lib/xast.js @@ -16,7 +16,7 @@ const cssSelectOptions = { }; /** - * @type {(node: XastNode, selector: string) => Array} + * @type {(node: XastNode, selector: string) => XastChild[]} */ const querySelectorAll = (node, selector) => { return selectAll(selector, node, cssSelectOptions); diff --git a/lib/xast.test.js b/lib/xast.test.js index aefaaed5e..4371fc938 100644 --- a/lib/xast.test.js +++ b/lib/xast.test.js @@ -8,7 +8,7 @@ const { visit, visitSkip, detachNodeFromParent } = require('./xast.js'); /** - * @type {(children: Array) => XastRoot} + * @type {(children: XastElement[]) => XastRoot} */ const root = (children) => { return { type: 'root', children }; @@ -18,7 +18,7 @@ const root = (children) => { * @type {( * name: string, * attrs?: ?Record, - * children?: Array + * children?: XastElement[] * ) => XastElement} */ const x = (name, attrs = null, children = []) => { @@ -28,7 +28,7 @@ const x = (name, attrs = null, children = []) => { test('visit enters into nodes', () => { const ast = root([x('g', null, [x('rect'), x('circle')]), x('ellipse')]); /** - * @type {Array} + * @type {string[]} */ const entered = []; visit(ast, { @@ -55,7 +55,7 @@ test('visit enters into nodes', () => { test('visit exits from nodes', () => { const ast = root([x('g', null, [x('rect'), x('circle')]), x('ellipse')]); /** - * @type {Array} + * @type {string[]} */ const exited = []; visit(ast, { @@ -82,7 +82,7 @@ test('visit exits from nodes', () => { test('visit skips entering children if node is detached', () => { const ast = root([x('g', null, [x('rect'), x('circle')]), x('ellipse')]); /** - * @type {Array} + * @type {string[]} */ const entered = []; visit(ast, { @@ -102,7 +102,7 @@ test('visit skips entering children if node is detached', () => { test('visit skips entering children when symbol is passed', () => { const ast = root([x('g', null, [x('rect'), x('circle')]), x('ellipse')]); /** - * @type {Array} + * @type {string[]} */ const entered = []; visit(ast, { diff --git a/plugins/_path.js b/plugins/_path.js index 60b4a9d72..7c3a28b93 100644 --- a/plugins/_path.js +++ b/plugins/_path.js @@ -15,13 +15,13 @@ var prevCtrlPoint; /** * Convert path string to JS representation. * - * @type {(path: XastElement) => Array} + * @type {(path: XastElement) => PathDataItem[]} */ const path2js = (path) => { // @ts-ignore legacy if (path.pathJS) return path.pathJS; /** - * @type {Array} + * @type {PathDataItem[]} */ const pathData = []; // JS representation of the path data const newPathData = parsePathData(path.attributes.d); @@ -41,12 +41,12 @@ exports.path2js = path2js; /** * Convert relative Path data to absolute. * - * @type {(data: Array) => Array} + * @type {(data: PathDataItem[]) => PathDataItem[]} * */ const convertRelativeToAbsolute = (data) => { /** - * @type {Array} + * @type {PathDataItem[]} */ const newData = []; let start = [0, 0]; @@ -179,7 +179,7 @@ const convertRelativeToAbsolute = (data) => { /** * Convert path array to string. * - * @type {(path: XastElement, data: Array, params: Js2PathParams) => void} + * @type {(path: XastElement, data: PathDataItem[], params: Js2PathParams) => void} */ exports.js2path = function (path, data, params) { // @ts-ignore legacy @@ -211,7 +211,7 @@ exports.js2path = function (path, data, params) { }; /** - * @type {(dest: Array, source: Array) => Array} + * @type {(dest: number[], source: number[]) => number[]} */ function set(dest, source) { dest[0] = source[source.length - 2]; @@ -224,7 +224,7 @@ function set(dest, source) { * collision using Gilbert-Johnson-Keerthi distance algorithm * https://web.archive.org/web/20180822200027/http://entropyinteractive.com/2011/04/gjk-algorithm/ * - * @type {(path1: Array, path2: Array) => boolean} + * @type {(path1: PathDataItem[], path2: PathDataItem[]) => boolean} */ exports.intersects = function (path1, path2) { // Collect points of every subpath. @@ -284,7 +284,7 @@ exports.intersects = function (path1, path2) { }); /** - * @type {(a: Point, b: Point, direction: Array) => Array} + * @type {(a: Point, b: Point, direction: number[]) => number[]} */ function getSupport(a, b, direction) { return sub(supportPoint(a, direction), supportPoint(b, minus(direction))); @@ -294,7 +294,7 @@ exports.intersects = function (path1, path2) { // Thanks to knowledge of min/max x and y coordinates we can choose a quadrant to search in. // Since we're working on convex hull, the dot product is increasing until we find the farthest point. /** - * @type {(polygon: Point, direction: Array) => Array} + * @type {(polygon: Point, direction: number[]) => number[]} */ function supportPoint(polygon, direction) { var index = @@ -316,7 +316,7 @@ exports.intersects = function (path1, path2) { }; /** - * @type {(simplex: Array>, direction: Array) => boolean} + * @type {(simplex: number[][], direction: number[]) => boolean} */ function processSimplex(simplex, direction) { // we only need to handle to 1-simplex and 2-simplex @@ -373,28 +373,28 @@ function processSimplex(simplex, direction) { } /** - * @type {(v: Array) => Array} + * @type {(v: number[]) => number[]} */ function minus(v) { return [-v[0], -v[1]]; } /** - * @type {(v1: Array, v2: Array) => Array} + * @type {(v1: number[], v2: number[]) => number[]} */ function sub(v1, v2) { return [v1[0] - v2[0], v1[1] - v2[1]]; } /** - * @type {(v1: Array, v2: Array) => number} + * @type {(v1: number[], v2: number[]) => number} */ function dot(v1, v2) { return v1[0] * v2[0] + v1[1] * v2[1]; } /** - * @type {(v1: Array, v2: Array) => Array} + * @type {(v1: number[], v2: number[]) => number[]} */ function orth(v, from) { var o = [-v[1], v[0]]; @@ -403,7 +403,7 @@ function orth(v, from) { /** * @typedef {{ - * list: Array>, + * list: number[][], * minX: number, * minY: number, * maxX: number, @@ -413,7 +413,7 @@ function orth(v, from) { /** * @typedef {{ - * list: Array, + * list: Point[], * minX: number, * minY: number, * maxX: number, @@ -422,7 +422,7 @@ function orth(v, from) { */ /** - * @type {(pathData: Array) => Points} + * @type {(pathData: PathDataItem[]) => Points} */ function gatherPoints(pathData) { /** @@ -432,7 +432,7 @@ function gatherPoints(pathData) { // Writes data about the extreme points on each axle /** - * @type {(path: Point, point: Array) => void} + * @type {(path: Point, point: number[]) => void} */ const addPoint = (path, point) => { if (!path.list.length || point[1] > path.list[path.maxY][1]) { @@ -670,7 +670,7 @@ function convexHull(points) { } /** - * @type {(o: Array, a: Array, b: Array) => number} + * @type {(o: number[], a: number[], b: number[]) => number} */ function cross(o, a, b) { return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]); @@ -690,8 +690,8 @@ function cross(o, a, b) { * sweep_flag: number, * x2: number, * y2: number, - * recursive: Array - * ) => Array} + * recursive: number[] + * ) => number[]} */ const a2c = ( x1, @@ -710,7 +710,7 @@ const a2c = ( const _120 = (Math.PI * 120) / 180; const rad = (Math.PI / 180) * (+angle || 0); /** - * @type {Array} + * @type {number[]} */ let res = []; /** diff --git a/plugins/_transforms.js b/plugins/_transforms.js index df327bc65..5606b6e7d 100644 --- a/plugins/_transforms.js +++ b/plugins/_transforms.js @@ -6,18 +6,18 @@ const regTransformSplit = const regNumericValues = /[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g; /** - * @typedef {{ name: string, data: Array }} TransformItem + * @typedef {{ name: string, data: number[] }} TransformItem */ /** * Convert transform string to JS representation. * - * @type {(transformString: string) => Array} + * @type {(transformString: string) => TransformItem[]} */ exports.transform2js = (transformString) => { // JS representation of the transform data /** - * @type {Array} + * @type {TransformItem[]} */ const transforms = []; // current transform context @@ -53,7 +53,7 @@ exports.transform2js = (transformString) => { /** * Multiply transforms into one. * - * @type {(transforms: Array) => TransformItem} + * @type {(transforms: TransformItem[]) => TransformItem} */ exports.transformsMultiply = (transforms) => { // convert transforms objects to the matrices @@ -153,7 +153,7 @@ const mth = { * Decompose matrix into simple transforms. See * https://frederic-wang.fr/decomposition-of-2d-transform-matrices.html * - * @type {(transform: TransformItem, params: TransformParams) => Array} + * @type {(transform: TransformItem, params: TransformParams) => TransformItem[]} */ exports.matrixToTransform = (transform, params) => { let floatPrecision = params.floatPrecision; @@ -248,7 +248,7 @@ exports.matrixToTransform = (transform, params) => { /** * Convert transform to the matrix data. * - * @type {(transform: TransformItem) => Array } + * @type {(transform: TransformItem) => number[] } */ const transformToMatrix = (transform) => { if (transform.name === 'matrix') { @@ -301,9 +301,9 @@ const transformToMatrix = (transform) => { * * @type {( * cursor: [x: number, y: number], - * arc: Array, - * transform: Array - * ) => Array} + * arc: number[], + * transform: number[] + * ) => number[]} */ exports.transformArc = (cursor, arc, transform) => { const x = arc[5] - cursor[0]; @@ -364,7 +364,7 @@ exports.transformArc = (cursor, arc, transform) => { /** * Multiply transformation matrices. * - * @type {(a: Array, b: Array) => Array} + * @type {(a: number[], b: number[]) => number[]} */ const multiplyTransformMatrices = (a, b) => { return [ diff --git a/plugins/applyTransforms.js b/plugins/applyTransforms.js index 3817c3d64..ae492d54a 100644 --- a/plugins/applyTransforms.js +++ b/plugins/applyTransforms.js @@ -19,8 +19,8 @@ const { const { referencesProps, attrsGroupsDefaults } = require('./_collections.js'); /** - * @typedef {Array} PathData - * @typedef {Array} Matrix + * @typedef {PathDataItem[]} PathData + * @typedef {number[]} Matrix */ const regNumericValues = /[-+]?(\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g; diff --git a/plugins/cleanupIds.js b/plugins/cleanupIds.js index cdbc207a8..bfabcca55 100644 --- a/plugins/cleanupIds.js +++ b/plugins/cleanupIds.js @@ -69,7 +69,7 @@ const maxIdIndex = generateIdChars.length - 1; /** * Check if an ID starts with any one of a list of strings. * - * @type {(string: string, prefixes: Array) => boolean} + * @type {(string: string, prefixes: string[]) => boolean} */ const hasStringPrefix = (string, prefixes) => { for (const prefix of prefixes) { @@ -109,7 +109,7 @@ const generateId = (currentId) => { /** * Get string from generated ID array. * - * @type {(arr: Array) => string} + * @type {(arr: number[]) => string} */ const getIdString = (arr) => { return arr.map((i) => generateIdChars[i]).join(''); @@ -144,7 +144,7 @@ exports.fn = (_root, params) => { */ const nodeById = new Map(); /** - * @type {Map>} + * @type {Map} */ const referencesById = new Map(); let deoptimized = false; diff --git a/plugins/convertColors.js b/plugins/convertColors.js index 98517a08a..d894283f8 100644 --- a/plugins/convertColors.js +++ b/plugins/convertColors.js @@ -22,7 +22,7 @@ const regHEX = /^#(([a-fA-F0-9])\2){3}$/; * * @author Jed Schmidt * - * @type {(rgb: Array) => string} + * @type {(rgb: number[]) => string} */ const convertRgbToHex = ([r, g, b]) => { // combine the octets into a 32-bit integer as: [1][r][g][b] diff --git a/plugins/convertShapeToPath.js b/plugins/convertShapeToPath.js index 8aa41cb72..f0ba0fd84 100644 --- a/plugins/convertShapeToPath.js +++ b/plugins/convertShapeToPath.js @@ -46,7 +46,7 @@ exports.fn = (root, params) => { // TODO: Calculate sizes from % and non-px units if possible. if (Number.isNaN(x - y + width - height)) return; /** - * @type {Array} + * @type {PathDataItem[]} */ const pathData = [ { command: 'M', args: [x, y] }, @@ -71,7 +71,7 @@ exports.fn = (root, params) => { const y2 = Number(node.attributes.y2 || '0'); if (Number.isNaN(x1 - y1 + x2 - y2)) return; /** - * @type {Array} + * @type {PathDataItem[]} */ const pathData = [ { command: 'M', args: [x1, y1] }, @@ -98,7 +98,7 @@ exports.fn = (root, params) => { return; } /** - * @type {Array} + * @type {PathDataItem[]} */ const pathData = []; for (let i = 0; i < coords.length; i += 2) { @@ -124,7 +124,7 @@ exports.fn = (root, params) => { return; } /** - * @type {Array} + * @type {PathDataItem[]} */ const pathData = [ { command: 'M', args: [cx, cy - r] }, @@ -149,7 +149,7 @@ exports.fn = (root, params) => { return; } /** - * @type {Array} + * @type {PathDataItem[]} */ const pathData = [ { command: 'M', args: [ecx, ecy - ry] }, diff --git a/plugins/convertTransform.js b/plugins/convertTransform.js index b8f75602e..730227ea3 100644 --- a/plugins/convertTransform.js +++ b/plugins/convertTransform.js @@ -94,7 +94,7 @@ exports.fn = (_root, params) => { */ /** - * @typedef {{ name: string, data: Array }} TransformItem + * @typedef {{ name: string, data: number[] }} TransformItem */ /** @@ -134,7 +134,7 @@ const convertTransform = (item, attrName, params) => { * degPrecision - for rotate and skew. By default it's equal to (roughly) * transformPrecision - 2 or floatPrecision whichever is lower. Can be set in params. * - * @type {(data: Array, params: TransformParams) => TransformParams} + * @type {(data: TransformItem[], params: TransformParams) => TransformParams} * * clone params so it don't affect other elements transformations. */ @@ -171,7 +171,7 @@ const definePrecision = (data, { ...newParams }) => { }; /** - * @type {(data: Array, params: TransformParams) => Array} + * @type {(data: number[], params: TransformParams) => number[]} */ const degRound = (data, params) => { if ( @@ -185,7 +185,7 @@ const degRound = (data, params) => { } }; /** - * @type {(data: Array, params: TransformParams) => Array} + * @type {(data: number[], params: TransformParams) => number[]} */ const floatRound = (data, params) => { if (params.floatPrecision >= 1 && params.floatPrecision < 20) { @@ -196,7 +196,7 @@ const floatRound = (data, params) => { }; /** - * @type {(data: Array, params: TransformParams) => Array} + * @type {(data: number[], params: TransformParams) => number[]} */ const transformRound = (data, params) => { if (params.transformPrecision >= 1 && params.floatPrecision < 20) { @@ -219,7 +219,7 @@ const floatDigits = (n) => { /** * Convert transforms to the shorthand alternatives. * - * @type {(transforms: Array, params: TransformParams) => Array} + * @type {(transforms: TransformItem[], params: TransformParams) => TransformItem[]} */ const convertToShorts = (transforms, params) => { for (var i = 0; i < transforms.length; i++) { @@ -294,7 +294,7 @@ const convertToShorts = (transforms, params) => { /** * Remove useless transforms. * - * @type {(transforms: Array) => Array} + * @type {(transforms: TransformItem[]) => TransformItem[]} */ const removeUseless = (transforms) => { return transforms.filter((transform) => { @@ -332,7 +332,7 @@ const removeUseless = (transforms) => { /** * Convert transforms JS representation to string. * - * @type {(transformJS: Array, params: TransformParams) => string} + * @type {(transformJS: TransformItem[], params: TransformParams) => string} */ const js2transform = (transformJS, params) => { const transformString = transformJS @@ -379,7 +379,7 @@ const roundTransform = (transform, params) => { /** * Rounds numbers in array. * - * @type {(data: Array) => Array} + * @type {(data: number[]) => number[]} */ const round = (data) => { return data.map(Math.round); @@ -390,7 +390,7 @@ const round = (data) => { * in transforms keeping a specified number of decimals. * Smart rounds values like 2.349 to 2.35. * - * @type {(precision: number, data: Array) => Array} + * @type {(precision: number, data: number[]) => number[]} */ const smartRound = (precision, data) => { for ( diff --git a/plugins/inlineStyles.js b/plugins/inlineStyles.js index 84b264eac..42029d2bd 100644 --- a/plugins/inlineStyles.js +++ b/plugins/inlineStyles.js @@ -51,16 +51,16 @@ exports.fn = (root, params) => { } = params; /** - * @type {Array<{ node: XastElement, parentNode: XastParent, cssAst: csstree.StyleSheet }>} + * @type {{ node: XastElement, parentNode: XastParent, cssAst: csstree.StyleSheet }[]} */ const styles = []; /** - * @type {Array<{ + * @type {{ * node: csstree.Selector, * item: csstree.ListItem, * rule: csstree.Rule, - * matchedElements?: Array - * }>} + * matchedElements?: XastElement[] + * }[]} */ let selectors = []; @@ -123,10 +123,10 @@ exports.fn = (root, params) => { node.prelude.children.forEach((childNode, item) => { if (childNode.type === 'Selector') { /** - * @type {Array<{ + * @type {{ * item: csstree.ListItem, * list: csstree.List - * }>} + * }[]} */ const pseudos = []; @@ -187,7 +187,7 @@ exports.fn = (root, params) => { for (const selector of sortedSelectors) { // match selectors const selectorText = csstree.generate(selector.item.data); - /** @type {Array} */ + /** @type {XastElement[]} */ const matchedElements = []; try { for (const node of querySelectorAll(root, selectorText)) { diff --git a/plugins/minifyStyles.js b/plugins/minifyStyles.js index 598b6db33..ade441b3b 100644 --- a/plugins/minifyStyles.js +++ b/plugins/minifyStyles.js @@ -22,7 +22,7 @@ exports.fn = (_root, { usage, ...params }) => { /** @type {Map} */ const styleElements = new Map(); - /** @type {Array} */ + /** @type {XastElement[]} */ const elementsWithStyleAttributes = []; /** @type {Set} */ diff --git a/plugins/removeEditorsNSData.js b/plugins/removeEditorsNSData.js index 992241ea2..852681bf8 100644 --- a/plugins/removeEditorsNSData.js +++ b/plugins/removeEditorsNSData.js @@ -24,7 +24,7 @@ exports.fn = (_root, params) => { namespaces = [...editorNamespaces, ...params.additionalNamespaces]; } /** - * @type {Array} + * @type {string[]} */ const prefixes = []; return { diff --git a/plugins/removeOffCanvasPaths.js b/plugins/removeOffCanvasPaths.js index 5f625d370..60a69fb1a 100644 --- a/plugins/removeOffCanvasPaths.js +++ b/plugins/removeOffCanvasPaths.js @@ -116,7 +116,7 @@ exports.fn = () => { const { left, top, width, height } = viewBoxData; /** - * @type {Array} + * @type {PathDataItem[]} */ const viewBoxPathData = [ { command: 'M', args: [left, top] }, diff --git a/plugins/removeUselessDefs.js b/plugins/removeUselessDefs.js index d642b874e..5d19810e3 100644 --- a/plugins/removeUselessDefs.js +++ b/plugins/removeUselessDefs.js @@ -23,7 +23,7 @@ exports.fn = () => { enter: (node, parentNode) => { if (node.name === 'defs') { /** - * @type {Array} + * @type {XastElement[]} */ const usefulNodes = []; collectUsefulNodes(node, usefulNodes); @@ -50,7 +50,7 @@ exports.fn = () => { }; /** - * @type {(node: XastElement, usefulNodes: Array) => void} + * @type {(node: XastElement, usefulNodes: XastElement[]) => void} */ const collectUsefulNodes = (node, usefulNodes) => { for (const child of node.children) { diff --git a/plugins/reusePaths.js b/plugins/reusePaths.js index 6079aceb7..8c2ddf0d3 100644 --- a/plugins/reusePaths.js +++ b/plugins/reusePaths.js @@ -27,7 +27,7 @@ exports.fn = (root) => { const stylesheet = collectStylesheet(root); /** - * @type {Map>} + * @type {Map} */ const paths = new Map(); diff --git a/test/regression-extract.js b/test/regression-extract.js index 2c0e6232f..e5a594e65 100644 --- a/test/regression-extract.js +++ b/test/regression-extract.js @@ -73,7 +73,7 @@ const extractTarGz = async (url, baseDir, include) => { (async () => { try { - console.info('Download W3C SVG 1.1 Test Suite and extract SVG files'); + console.info('Downloading W3C SVG 1.1 Test Suite and extracting files'); await extractTarGz( 'https://www.w3.org/Graphics/SVG/Test/20110816/archives/W3C_SVG_11_TestSuite.tar.gz', path.join(__dirname, 'regression-fixtures', 'w3c-svg-11-test-suite'), diff --git a/test/svgo/_index.test.js b/test/svgo/_index.test.js index a9e5f06c7..e855dc6bf 100644 --- a/test/svgo/_index.test.js +++ b/test/svgo/_index.test.js @@ -36,7 +36,7 @@ describe('svgo', () => { expect(result.data).toEqual(''); }); it('should preserve style specificity over attributes', async () => { - const [original, expected] = await parseFixture('style-specifity.svg'); + const [original, expected] = await parseFixture('style-specificity.svg'); const result = optimize(original, { path: 'input.svg', js2svg: { pretty: true }, diff --git a/test/svgo/style-specifity.svg b/test/svgo/style-specificity.svg similarity index 100% rename from test/svgo/style-specifity.svg rename to test/svgo/style-specificity.svg From 4768af63e4ec15a10f5f721db79163341ad9947f Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 27 Dec 2023 08:09:56 -0500 Subject: [PATCH 12/14] chore: Update @types/csso and correct types (#1910) --- lib/style.js | 1 - lib/types.d.ts | 2 +- package.json | 4 ++-- plugins/inlineStyles.js | 1 - yarn.lock | 14 +++++++------- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/style.js b/lib/style.js index fbfffc6b4..0e62afb92 100644 --- a/lib/style.js +++ b/lib/style.js @@ -16,7 +16,6 @@ const csstree = require('css-tree'); const csswhat = require('css-what'); const { - // @ts-ignore internal api syntax: { specificity }, } = require('csso'); const { visit, matches } = require('./xast.js'); diff --git a/lib/types.d.ts b/lib/types.d.ts index 68dc97478..f61d9f18f 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -111,7 +111,7 @@ export type Plugin = ( info: PluginInfo, ) => null | Visitor; -export type Specificity = [number, number, number, number]; +export type Specificity = [number, number, number]; export type StylesheetDeclaration = { name: string; diff --git a/package.json b/package.json index 3b51fb00e..93a06044d 100644 --- a/package.json +++ b/package.json @@ -83,14 +83,14 @@ "css-select": "^5.1.0", "css-tree": "^2.2.1", "css-what": "^6.1.0", - "csso": "5.0.5", + "csso": "^5.0.5", "picocolors": "^1.0.0" }, "devDependencies": { "@rollup/plugin-commonjs": "^22.0.2", "@rollup/plugin-node-resolve": "^14.1.0", "@types/css-tree": "^2.0.0", - "@types/csso": "~5.0.3", + "@types/csso": "^5.0.4", "@types/jest": "^29.5.5", "del": "^6.0.0", "eslint": "^8.55.0", diff --git a/plugins/inlineStyles.js b/plugins/inlineStyles.js index 42029d2bd..e09988de7 100644 --- a/plugins/inlineStyles.js +++ b/plugins/inlineStyles.js @@ -7,7 +7,6 @@ const csstree = require('css-tree'); const { - // @ts-ignore internal api syntax: { specificity }, } = require('csso'); const { diff --git a/yarn.lock b/yarn.lock index 8baf0bdf7..4f417bb81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -983,12 +983,12 @@ __metadata: languageName: node linkType: hard -"@types/csso@npm:~5.0.3": - version: 5.0.3 - resolution: "@types/csso@npm:5.0.3" +"@types/csso@npm:^5.0.4": + version: 5.0.4 + resolution: "@types/csso@npm:5.0.4" dependencies: "@types/css-tree": "*" - checksum: 3d299ea755732f9b913cfb3d94849e173cd1019559058af0a372aa1ca8f48a3c63aa74932fdfa2f2f25ee78255a115feaaec01ae4fe9578e76b7c4acd8ae3f2a + checksum: 606ea4de171e807ffc8908f7ffec774c8a40f3c81e7c60f8a84c7f5327eb00f3c9948b021b91e2e8fe3a76af4623ac58538780f0789f1e03947b7de96c5f7994 languageName: node linkType: hard @@ -1704,7 +1704,7 @@ __metadata: languageName: node linkType: hard -"csso@npm:5.0.5": +"csso@npm:^5.0.5": version: 5.0.5 resolution: "csso@npm:5.0.5" dependencies: @@ -4430,13 +4430,13 @@ __metadata: "@rollup/plugin-node-resolve": ^14.1.0 "@trysound/sax": 0.2.0 "@types/css-tree": ^2.0.0 - "@types/csso": ~5.0.3 + "@types/csso": ^5.0.4 "@types/jest": ^29.5.5 commander: ^7.2.0 css-select: ^5.1.0 css-tree: ^2.2.1 css-what: ^6.1.0 - csso: 5.0.5 + csso: ^5.0.5 del: ^6.0.0 eslint: ^8.55.0 jest: ^29.5.5 From 967d2f1e1452a7c0adf952a9d676416eb31680c9 Mon Sep 17 00:00:00 2001 From: Seth Falco Date: Wed, 27 Dec 2023 13:51:50 +0000 Subject: [PATCH 13/14] deps: update css-tree and clean ts-ignores (#1911) --- package.json | 4 ++-- plugins/prefixIds.js | 3 --- yarn.lock | 31 ++++++++++++++++++++++++------- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 93a06044d..d53741856 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", - "css-tree": "^2.2.1", + "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.0.0" @@ -89,7 +89,7 @@ "devDependencies": { "@rollup/plugin-commonjs": "^22.0.2", "@rollup/plugin-node-resolve": "^14.1.0", - "@types/css-tree": "^2.0.0", + "@types/css-tree": "^2.3.4", "@types/csso": "^5.0.4", "@types/jest": "^29.5.5", "del": "^6.0.0", diff --git a/plugins/prefixIds.js b/plugins/prefixIds.js index eb5145527..ec305c6b0 100644 --- a/plugins/prefixIds.js +++ b/plugins/prefixIds.js @@ -173,15 +173,12 @@ exports.fn = (_root, params, info) => { node.name = prefixId(prefixGenerator, node.name); return; } - // @ts-ignore csstree v2 changed this type if (node.type === 'Url' && node.value.length > 0) { const prefixed = prefixReference( prefixGenerator, - // @ts-ignore unquote(node.value), ); if (prefixed != null) { - // @ts-ignore node.value = prefixed; } } diff --git a/yarn.lock b/yarn.lock index 4f417bb81..e6b23d23c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -976,10 +976,10 @@ __metadata: languageName: node linkType: hard -"@types/css-tree@npm:*, @types/css-tree@npm:^2.0.0": - version: 2.0.0 - resolution: "@types/css-tree@npm:2.0.0" - checksum: 52d89f5c52c6ec5feac674a3f28f35994211f26c662cc8718f21ebabad992329e8c810da1e070aa61c49f89e92cd5ef7b460e81de52f0d0f5973618d87ebc3ae +"@types/css-tree@npm:*, @types/css-tree@npm:^2.3.4": + version: 2.3.4 + resolution: "@types/css-tree@npm:2.3.4" + checksum: b06173d3ae048b74e7030cceae01d3d6e5589f7348e8e4b907427f91baeda76807bbde8c5be4c41e22b952268b46edcba7ab96e16efedb2d0b37ce2bf90d1835 languageName: node linkType: hard @@ -1687,7 +1687,17 @@ __metadata: languageName: node linkType: hard -"css-tree@npm:^2.2.1, css-tree@npm:~2.2.0": +"css-tree@npm:^2.3.1": + version: 2.3.1 + resolution: "css-tree@npm:2.3.1" + dependencies: + mdn-data: 2.0.30 + source-map-js: ^1.0.1 + checksum: 493cc24b5c22b05ee5314b8a0d72d8a5869491c1458017ae5ed75aeb6c3596637dbe1b11dac2548974624adec9f7a1f3a6cf40593dc1f9185eb0e8279543fbc0 + languageName: node + linkType: hard + +"css-tree@npm:~2.2.0": version: 2.2.1 resolution: "css-tree@npm:2.2.1" dependencies: @@ -3390,6 +3400,13 @@ __metadata: languageName: node linkType: hard +"mdn-data@npm:2.0.30": + version: 2.0.30 + resolution: "mdn-data@npm:2.0.30" + checksum: d6ac5ac7439a1607df44b22738ecf83f48e66a0874e4482d6424a61c52da5cde5750f1d1229b6f5fa1b80a492be89465390da685b11f97d62b8adcc6e88189aa + languageName: node + linkType: hard + "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -4429,12 +4446,12 @@ __metadata: "@rollup/plugin-commonjs": ^22.0.2 "@rollup/plugin-node-resolve": ^14.1.0 "@trysound/sax": 0.2.0 - "@types/css-tree": ^2.0.0 + "@types/css-tree": ^2.3.4 "@types/csso": ^5.0.4 "@types/jest": ^29.5.5 commander: ^7.2.0 css-select: ^5.1.0 - css-tree: ^2.2.1 + css-tree: ^2.3.1 css-what: ^6.1.0 csso: ^5.0.5 del: ^6.0.0 From f238d6a213a499deed31895d7ebbf139eb9996c5 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Wed, 27 Dec 2023 18:41:09 -0500 Subject: [PATCH 14/14] feat(convertPathData): allow converting q to t in more cases (#1889) --- plugins/convertPathData.js | 52 +++++++++++++++++++++++++---- test/plugins/convertPathData.34.svg | 16 +++++++++ 2 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 test/plugins/convertPathData.34.svg diff --git a/plugins/convertPathData.js b/plugins/convertPathData.js index 2a6c66507..0ee7824ea 100644 --- a/plugins/convertPathData.js +++ b/plugins/convertPathData.js @@ -397,8 +397,13 @@ function filters( relSubpoint = [0, 0], pathBase = [0, 0], prev = {}; + /** @type {Point | undefined} */ + let qControlPoint; path = path.filter(function (item, index, path) { + const qPoint = qControlPoint; + qControlPoint = undefined; + let command = item.command; let data = item.args; let next = path[index + 1]; @@ -795,14 +800,24 @@ function filters( // t + q → t + t else if ( // @ts-ignore - prev.command === 't' && - // @ts-ignore - Math.abs(data[2] - prev.args[0]) < error && - // @ts-ignore - Math.abs(data[3] - prev.args[1]) < error + prev.command === 't' ) { - command = 't'; - data = data.slice(2); + // @ts-ignore + const predictedControlPoint = reflectPoint(qPoint, item.base); + const realControlPoint = [ + // @ts-ignore + data[0] + item.base[0], + // @ts-ignore + data[1] + item.base[1], + ]; + if ( + Math.abs(predictedControlPoint[0] - realControlPoint[0]) < + error && + Math.abs(predictedControlPoint[1] - realControlPoint[1]) < error + ) { + command = 't'; + data = data.slice(2); + } } } } @@ -873,6 +888,18 @@ function filters( ) return false; + if (command === 'q') { + // @ts-ignore + qControlPoint = [data[0] + item.base[0], data[1] + item.base[1]]; + } else if (command === 't') { + if (qPoint) { + // @ts-ignore + qControlPoint = reflectPoint(qPoint, item.base); + } else { + // @ts-ignore + qControlPoint = item.coords; + } + } prev = item; return true; }); @@ -1138,6 +1165,17 @@ function getDistance(point1, point2) { return Math.hypot(point1[0] - point2[0], point1[1] - point2[1]); } +/** + * Reflects point across another point + * + * @param {Point} input + * @param {Point} base + * @returns {Point} + */ +function reflectPoint(input, base) { + return [2 * base[0] - input[0], 2 * base[1] - input[1]]; +} + /** * Returns coordinates of the curve point corresponding to the certain t * a·(1 - t)³·p1 + b·(1 - t)²·t·p2 + c·(1 - t)·t²·p3 + d·t³·p4, diff --git a/test/plugins/convertPathData.34.svg b/test/plugins/convertPathData.34.svg new file mode 100644 index 000000000..a6e3eba41 --- /dev/null +++ b/test/plugins/convertPathData.34.svg @@ -0,0 +1,16 @@ +Shouldn't incorrectly convert q to t. Should convert q to t when feasible. + +=== + + + + + + +@@@ + + + + + +