From 2c0c3610747587a7731ccf6db2372983d6e5f643 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Thu, 12 Aug 2021 03:08:39 +0300 Subject: [PATCH] Collect stylesheet once per plugin (#1456) computeStyle(node) in isolation is quite slow utility because it collects style elements across whole document, parses and sort them. In this diff I splitted it into `collectStylesheet(root)` and `computeStyle(stylesheet, node)` which are easy integrate with new visitor plugin api. --- lib/style.js | 68 +++++++++++----------- lib/style.test.js | 108 ++++++++++++++++++++++++++--------- plugins/convertPathData.js | 5 +- plugins/mergePaths.js | 5 +- plugins/removeHiddenElems.js | 6 +- 5 files changed, 122 insertions(+), 70 deletions(-) diff --git a/lib/style.js b/lib/style.js index ef4dad0c..2d86ef47 100644 --- a/lib/style.js +++ b/lib/style.js @@ -3,8 +3,7 @@ const stable = require('stable'); const csstree = require('css-tree'); const specificity = require('csso/lib/restructure/prepare/specificity'); -const { selectAll, is } = require('css-select'); -const svgoCssSelectAdapter = require('./svgo/css-select-adapter.js'); +const { visit, matches } = require('./xast.js'); const { compareSpecificity } = require('./css-tools.js'); const { attrsGroups, @@ -12,11 +11,6 @@ const { presentationNonInheritableGroupAttrs, } = require('../plugins/_collections.js'); -const cssSelectOptions = { - xmlMode: true, - adapter: svgoCssSelectAdapter, -}; - const parseRule = (ruleNode, dynamic) => { let selectors; let selectorsSpecificity; @@ -76,7 +70,7 @@ const parseStylesheet = (css, dynamic) => { return rules; }; -const computeOwnStyle = (node, stylesheet) => { +const computeOwnStyle = (stylesheet, node) => { const computedStyle = {}; const importantStyles = new Map(); @@ -90,7 +84,7 @@ const computeOwnStyle = (node, stylesheet) => { // collect matching rules for (const { selectors, declarations, dynamic } of stylesheet) { - if (is(node, selectors, cssSelectOptions)) { + if (matches(node, selectors)) { for (const { name, value, important } of declarations) { const computed = computedStyle[name]; if (computed && computed.type === 'dynamic') { @@ -132,43 +126,45 @@ const computeOwnStyle = (node, stylesheet) => { return computedStyle; }; -const computeStyle = (node) => { - // find root - let root = node; - while (root.parentNode) { - root = root.parentNode; - } - // find all styles - const styleNodes = selectAll('style', root, cssSelectOptions); - // parse all styles +const collectStylesheet = (root) => { const stylesheet = []; - for (const styleNode of styleNodes) { - const dynamic = - styleNode.attributes.media != null && - styleNode.attributes.media !== 'all'; - if ( - styleNode.attributes.type == null || - styleNode.attributes.type === '' || - styleNode.attributes.type === 'text/css' - ) { - const children = styleNode.children; - for (const child of children) { - if (child.type === 'text' || child.type === 'cdata') { - stylesheet.push(...parseStylesheet(child.value, dynamic)); + // find and parse all styles + visit(root, { + element: { + enter: (node) => { + if (node.name === 'style') { + 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') { + stylesheet.push(...parseStylesheet(child.value, dynamic)); + } + } + } } - } - } - } + }, + }, + }); // sort by selectors specificity stable.inplace(stylesheet, (a, b) => compareSpecificity(a.specificity, b.specificity) ); + return stylesheet; +}; +exports.collectStylesheet = collectStylesheet; +const computeStyle = (stylesheet, node) => { // collect inherited styles - const computedStyles = computeOwnStyle(node, stylesheet); + const computedStyles = computeOwnStyle(stylesheet, node); let parent = node; while (parent.parentNode && parent.parentNode.type !== 'root') { - const inheritedStyles = computeOwnStyle(parent.parentNode, stylesheet); + const inheritedStyles = computeOwnStyle(stylesheet, parent.parentNode); for (const [name, computed] of Object.entries(inheritedStyles)) { if ( computedStyles[name] == null && diff --git a/lib/style.test.js b/lib/style.test.js index 9171ffe1..42c7da35 100644 --- a/lib/style.test.js +++ b/lib/style.test.js @@ -1,7 +1,7 @@ 'use strict'; const { expect } = require('chai'); -const { computeStyle } = require('./style.js'); +const { collectStylesheet, computeStyle } = require('./style.js'); const { querySelector } = require('./xast.js'); const svg2js = require('./svgo/svg2js.js'); @@ -31,24 +31,35 @@ describe('computeStyle', () => { `); - expect(computeStyle(querySelector(root, '#class'))).to.deep.equal({ + const stylesheet = collectStylesheet(root); + expect( + computeStyle(stylesheet, querySelector(root, '#class')) + ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'red' }, }); - expect(computeStyle(querySelector(root, '#two-classes'))).to.deep.equal({ + expect( + computeStyle(stylesheet, querySelector(root, '#two-classes')) + ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'green' }, stroke: { type: 'static', inherited: false, value: 'black' }, }); - expect(computeStyle(querySelector(root, '#attribute'))).to.deep.equal({ + expect( + computeStyle(stylesheet, querySelector(root, '#attribute')) + ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'purple' }, }); - expect(computeStyle(querySelector(root, '#inline-style'))).to.deep.equal({ + expect( + computeStyle(stylesheet, querySelector(root, '#inline-style')) + ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'grey' }, }); - expect(computeStyle(querySelector(root, '#inheritance'))).to.deep.equal({ + expect( + computeStyle(stylesheet, querySelector(root, '#inheritance')) + ).to.deep.equal({ fill: { type: 'static', inherited: true, value: 'yellow' }, }); expect( - computeStyle(querySelector(root, '#nested-inheritance')) + computeStyle(stylesheet, querySelector(root, '#nested-inheritance')) ).to.deep.equal({ fill: { type: 'static', inherited: true, value: 'blue' }, }); @@ -70,23 +81,33 @@ describe('computeStyle', () => { `); + const stylesheet = collectStylesheet(root); expect( - computeStyle(querySelector(root, '#complex-selector')) + computeStyle(stylesheet, querySelector(root, '#complex-selector')) ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'red' }, }); expect( - computeStyle(querySelector(root, '#attribute-over-inheritance')) + computeStyle( + stylesheet, + querySelector(root, '#attribute-over-inheritance') + ) ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'orange' }, }); expect( - computeStyle(querySelector(root, '#style-rule-over-attribute')) + computeStyle( + stylesheet, + querySelector(root, '#style-rule-over-attribute') + ) ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'blue' }, }); expect( - computeStyle(querySelector(root, '#inline-style-over-style-rule')) + computeStyle( + stylesheet, + querySelector(root, '#inline-style-over-style-rule') + ) ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'purple' }, }); @@ -104,18 +125,25 @@ describe('computeStyle', () => { `); + const stylesheet = collectStylesheet(root); expect( - computeStyle(querySelector(root, '#complex-selector')) + computeStyle(stylesheet, querySelector(root, '#complex-selector')) ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'green' }, }); expect( - computeStyle(querySelector(root, '#style-rule-over-inline-style')) + computeStyle( + stylesheet, + querySelector(root, '#style-rule-over-inline-style') + ) ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'green' }, }); expect( - computeStyle(querySelector(root, '#inline-style-over-style-rule')) + computeStyle( + stylesheet, + querySelector(root, '#inline-style-over-style-rule') + ) ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'purple' }, }); @@ -141,21 +169,30 @@ describe('computeStyle', () => { `); - expect(computeStyle(querySelector(root, '#media-query'))).to.deep.equal({ + const stylesheet = collectStylesheet(root); + expect( + computeStyle(stylesheet, querySelector(root, '#media-query')) + ).to.deep.equal({ fill: { type: 'dynamic', inherited: false }, }); - expect(computeStyle(querySelector(root, '#hover'))).to.deep.equal({ + expect( + computeStyle(stylesheet, querySelector(root, '#hover')) + ).to.deep.equal({ fill: { type: 'dynamic', inherited: false }, }); - expect(computeStyle(querySelector(root, '#inherited'))).to.deep.equal({ + expect( + computeStyle(stylesheet, querySelector(root, '#inherited')) + ).to.deep.equal({ fill: { type: 'dynamic', inherited: true }, }); expect( - computeStyle(querySelector(root, '#inherited-overriden')) + computeStyle(stylesheet, querySelector(root, '#inherited-overriden')) ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'blue' }, }); - expect(computeStyle(querySelector(root, '#static'))).to.deep.equal({ + expect( + computeStyle(stylesheet, querySelector(root, '#static')) + ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'black' }, }); }); @@ -177,13 +214,20 @@ describe('computeStyle', () => { `); - expect(computeStyle(querySelector(root, '#media-query'))).to.deep.equal({ + const stylesheet = collectStylesheet(root); + expect( + computeStyle(stylesheet, querySelector(root, '#media-query')) + ).to.deep.equal({ fill: { type: 'dynamic', inherited: false }, }); - expect(computeStyle(querySelector(root, '#kinda-static'))).to.deep.equal({ + expect( + computeStyle(stylesheet, querySelector(root, '#kinda-static')) + ).to.deep.equal({ fill: { type: 'dynamic', inherited: false }, }); - expect(computeStyle(querySelector(root, '#static'))).to.deep.equal({ + expect( + computeStyle(stylesheet, querySelector(root, '#static')) + ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'blue' }, }); }); @@ -205,15 +249,20 @@ describe('computeStyle', () => { `); - expect(computeStyle(querySelector(root, '#valid-type'))).to.deep.equal({ + const stylesheet = collectStylesheet(root); + expect( + computeStyle(stylesheet, querySelector(root, '#valid-type')) + ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'red' }, }); - expect(computeStyle(querySelector(root, '#empty-type'))).to.deep.equal({ + expect( + computeStyle(stylesheet, querySelector(root, '#empty-type')) + ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'green' }, }); - expect(computeStyle(querySelector(root, '#invalid-type'))).to.deep.equal( - {} - ); + expect( + computeStyle(stylesheet, querySelector(root, '#invalid-type')) + ).to.deep.equal({}); }); it('ignores keyframes atrule', () => { @@ -238,7 +287,10 @@ describe('computeStyle', () => { `); - expect(computeStyle(querySelector(root, '#element'))).to.deep.equal({ + const stylesheet = collectStylesheet(root); + expect( + computeStyle(stylesheet, querySelector(root, '#element')) + ).to.deep.equal({ animation: { type: 'static', inherited: false, diff --git a/plugins/convertPathData.js b/plugins/convertPathData.js index e09c2890..cb4cd2b4 100644 --- a/plugins/convertPathData.js +++ b/plugins/convertPathData.js @@ -1,6 +1,6 @@ 'use strict'; -const { computeStyle } = require('../lib/style.js'); +const { collectStylesheet, computeStyle } = require('../lib/style.js'); const { pathElems } = require('./_collections.js'); const { path2js, js2path } = require('./_path.js'); const { applyTransforms } = require('./_applyTransforms.js'); @@ -55,11 +55,12 @@ let arcTolerance; * @author Kir Belevich */ exports.fn = (root, params) => { + const stylesheet = collectStylesheet(root); return { element: { enter: (node) => { if (pathElems.includes(node.name) && node.attributes.d != null) { - const computedStyle = computeStyle(node); + const computedStyle = computeStyle(stylesheet, node); precision = params.floatPrecision; error = precision !== false diff --git a/plugins/mergePaths.js b/plugins/mergePaths.js index 8bff3de2..cd5863e1 100644 --- a/plugins/mergePaths.js +++ b/plugins/mergePaths.js @@ -1,7 +1,7 @@ 'use strict'; const { detachNodeFromParent } = require('../lib/xast.js'); -const { computeStyle } = require('../lib/style.js'); +const { collectStylesheet, computeStyle } = require('../lib/style.js'); const { path2js, js2path, intersects } = require('./_path.js'); exports.type = 'visitor'; @@ -22,6 +22,7 @@ exports.fn = (root, params) => { floatPrecision, noSpaceAfterFlags = false, // a20 60 45 0 1 30 20 → a20 60 45 0130 20 } = params; + const stylesheet = collectStylesheet(root); return { element: { @@ -53,7 +54,7 @@ exports.fn = (root, params) => { } // preserve paths with markers - const computedStyle = computeStyle(child); + const computedStyle = computeStyle(stylesheet, child); if ( computedStyle['marker-start'] || computedStyle['marker-mid'] || diff --git a/plugins/removeHiddenElems.js b/plugins/removeHiddenElems.js index 2f98e3fb..8d7be78c 100644 --- a/plugins/removeHiddenElems.js +++ b/plugins/removeHiddenElems.js @@ -5,7 +5,7 @@ const { closestByName, detachNodeFromParent, } = require('../lib/xast.js'); -const { computeStyle } = require('../lib/style.js'); +const { collectStylesheet, computeStyle } = require('../lib/style.js'); const { parsePathData } = require('../lib/path.js'); exports.type = 'visitor'; @@ -49,12 +49,14 @@ exports.fn = (root, params) => { polylineEmptyPoints = true, polygonEmptyPoints = true, } = params; + const stylesheet = collectStylesheet(root); + return { element: { enter: (node) => { // Removes hidden elements // https://www.w3schools.com/cssref/pr_class_visibility.asp - const computedStyle = computeStyle(node); + const computedStyle = computeStyle(stylesheet, node); if ( isHidden && computedStyle.visibility &&