1
0
mirror of https://github.com/svg/svgo.git synced 2025-08-01 18:46:52 +03:00

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.
This commit is contained in:
Bogdan Chadkin
2021-08-12 03:08:39 +03:00
committed by GitHub
parent 69d01746cd
commit 2c0c361074
5 changed files with 122 additions and 70 deletions

View File

@ -3,8 +3,7 @@
const stable = require('stable'); const stable = require('stable');
const csstree = require('css-tree'); const csstree = require('css-tree');
const specificity = require('csso/lib/restructure/prepare/specificity'); const specificity = require('csso/lib/restructure/prepare/specificity');
const { selectAll, is } = require('css-select'); const { visit, matches } = require('./xast.js');
const svgoCssSelectAdapter = require('./svgo/css-select-adapter.js');
const { compareSpecificity } = require('./css-tools.js'); const { compareSpecificity } = require('./css-tools.js');
const { const {
attrsGroups, attrsGroups,
@ -12,11 +11,6 @@ const {
presentationNonInheritableGroupAttrs, presentationNonInheritableGroupAttrs,
} = require('../plugins/_collections.js'); } = require('../plugins/_collections.js');
const cssSelectOptions = {
xmlMode: true,
adapter: svgoCssSelectAdapter,
};
const parseRule = (ruleNode, dynamic) => { const parseRule = (ruleNode, dynamic) => {
let selectors; let selectors;
let selectorsSpecificity; let selectorsSpecificity;
@ -76,7 +70,7 @@ const parseStylesheet = (css, dynamic) => {
return rules; return rules;
}; };
const computeOwnStyle = (node, stylesheet) => { const computeOwnStyle = (stylesheet, node) => {
const computedStyle = {}; const computedStyle = {};
const importantStyles = new Map(); const importantStyles = new Map();
@ -90,7 +84,7 @@ const computeOwnStyle = (node, stylesheet) => {
// collect matching rules // collect matching rules
for (const { selectors, declarations, dynamic } of stylesheet) { for (const { selectors, declarations, dynamic } of stylesheet) {
if (is(node, selectors, cssSelectOptions)) { if (matches(node, selectors)) {
for (const { name, value, important } of declarations) { for (const { name, value, important } of declarations) {
const computed = computedStyle[name]; const computed = computedStyle[name];
if (computed && computed.type === 'dynamic') { if (computed && computed.type === 'dynamic') {
@ -132,43 +126,45 @@ const computeOwnStyle = (node, stylesheet) => {
return computedStyle; return computedStyle;
}; };
const computeStyle = (node) => { const collectStylesheet = (root) => {
// find root
let root = node;
while (root.parentNode) {
root = root.parentNode;
}
// find all styles
const styleNodes = selectAll('style', root, cssSelectOptions);
// parse all styles
const stylesheet = []; const stylesheet = [];
for (const styleNode of styleNodes) { // find and parse all styles
const dynamic = visit(root, {
styleNode.attributes.media != null && element: {
styleNode.attributes.media !== 'all'; enter: (node) => {
if ( if (node.name === 'style') {
styleNode.attributes.type == null || const dynamic =
styleNode.attributes.type === '' || node.attributes.media != null && node.attributes.media !== 'all';
styleNode.attributes.type === 'text/css' if (
) { node.attributes.type == null ||
const children = styleNode.children; node.attributes.type === '' ||
for (const child of children) { node.attributes.type === 'text/css'
if (child.type === 'text' || child.type === 'cdata') { ) {
stylesheet.push(...parseStylesheet(child.value, dynamic)); 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 // sort by selectors specificity
stable.inplace(stylesheet, (a, b) => stable.inplace(stylesheet, (a, b) =>
compareSpecificity(a.specificity, b.specificity) compareSpecificity(a.specificity, b.specificity)
); );
return stylesheet;
};
exports.collectStylesheet = collectStylesheet;
const computeStyle = (stylesheet, node) => {
// collect inherited styles // collect inherited styles
const computedStyles = computeOwnStyle(node, stylesheet); const computedStyles = computeOwnStyle(stylesheet, node);
let parent = node; let parent = node;
while (parent.parentNode && parent.parentNode.type !== 'root') { 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)) { for (const [name, computed] of Object.entries(inheritedStyles)) {
if ( if (
computedStyles[name] == null && computedStyles[name] == null &&

View File

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const { expect } = require('chai'); const { expect } = require('chai');
const { computeStyle } = require('./style.js'); const { collectStylesheet, computeStyle } = require('./style.js');
const { querySelector } = require('./xast.js'); const { querySelector } = require('./xast.js');
const svg2js = require('./svgo/svg2js.js'); const svg2js = require('./svgo/svg2js.js');
@ -31,24 +31,35 @@ describe('computeStyle', () => {
</style> </style>
</svg> </svg>
`); `);
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' }, 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' }, fill: { type: 'static', inherited: false, value: 'green' },
stroke: { type: 'static', inherited: false, value: 'black' }, 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' }, 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' }, 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' }, fill: { type: 'static', inherited: true, value: 'yellow' },
}); });
expect( expect(
computeStyle(querySelector(root, '#nested-inheritance')) computeStyle(stylesheet, querySelector(root, '#nested-inheritance'))
).to.deep.equal({ ).to.deep.equal({
fill: { type: 'static', inherited: true, value: 'blue' }, fill: { type: 'static', inherited: true, value: 'blue' },
}); });
@ -70,23 +81,33 @@ describe('computeStyle', () => {
</g> </g>
</svg> </svg>
`); `);
const stylesheet = collectStylesheet(root);
expect( expect(
computeStyle(querySelector(root, '#complex-selector')) computeStyle(stylesheet, querySelector(root, '#complex-selector'))
).to.deep.equal({ ).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'red' }, fill: { type: 'static', inherited: false, value: 'red' },
}); });
expect( expect(
computeStyle(querySelector(root, '#attribute-over-inheritance')) computeStyle(
stylesheet,
querySelector(root, '#attribute-over-inheritance')
)
).to.deep.equal({ ).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'orange' }, fill: { type: 'static', inherited: false, value: 'orange' },
}); });
expect( expect(
computeStyle(querySelector(root, '#style-rule-over-attribute')) computeStyle(
stylesheet,
querySelector(root, '#style-rule-over-attribute')
)
).to.deep.equal({ ).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'blue' }, fill: { type: 'static', inherited: false, value: 'blue' },
}); });
expect( expect(
computeStyle(querySelector(root, '#inline-style-over-style-rule')) computeStyle(
stylesheet,
querySelector(root, '#inline-style-over-style-rule')
)
).to.deep.equal({ ).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'purple' }, fill: { type: 'static', inherited: false, value: 'purple' },
}); });
@ -104,18 +125,25 @@ describe('computeStyle', () => {
<rect id="inline-style-over-style-rule" style="fill: purple !important;" class="b" /> <rect id="inline-style-over-style-rule" style="fill: purple !important;" class="b" />
</svg> </svg>
`); `);
const stylesheet = collectStylesheet(root);
expect( expect(
computeStyle(querySelector(root, '#complex-selector')) computeStyle(stylesheet, querySelector(root, '#complex-selector'))
).to.deep.equal({ ).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'green' }, fill: { type: 'static', inherited: false, value: 'green' },
}); });
expect( expect(
computeStyle(querySelector(root, '#style-rule-over-inline-style')) computeStyle(
stylesheet,
querySelector(root, '#style-rule-over-inline-style')
)
).to.deep.equal({ ).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'green' }, fill: { type: 'static', inherited: false, value: 'green' },
}); });
expect( expect(
computeStyle(querySelector(root, '#inline-style-over-style-rule')) computeStyle(
stylesheet,
querySelector(root, '#inline-style-over-style-rule')
)
).to.deep.equal({ ).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'purple' }, fill: { type: 'static', inherited: false, value: 'purple' },
}); });
@ -141,21 +169,30 @@ describe('computeStyle', () => {
<rect id="static" class="c" style="fill: black" /> <rect id="static" class="c" style="fill: black" />
</svg> </svg>
`); `);
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 }, 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 }, 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 }, fill: { type: 'dynamic', inherited: true },
}); });
expect( expect(
computeStyle(querySelector(root, '#inherited-overriden')) computeStyle(stylesheet, querySelector(root, '#inherited-overriden'))
).to.deep.equal({ ).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'blue' }, 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' }, fill: { type: 'static', inherited: false, value: 'black' },
}); });
}); });
@ -177,13 +214,20 @@ describe('computeStyle', () => {
<rect id="static" class="c" /> <rect id="static" class="c" />
</svg> </svg>
`); `);
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 }, 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 }, 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' }, fill: { type: 'static', inherited: false, value: 'blue' },
}); });
}); });
@ -205,15 +249,20 @@ describe('computeStyle', () => {
<rect id="invalid-type" class="c" /> <rect id="invalid-type" class="c" />
</svg> </svg>
`); `);
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' }, 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' }, 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', () => { it('ignores keyframes atrule', () => {
@ -238,7 +287,10 @@ describe('computeStyle', () => {
<rect id="element" class="a" /> <rect id="element" class="a" />
</svg> </svg>
`); `);
expect(computeStyle(querySelector(root, '#element'))).to.deep.equal({ const stylesheet = collectStylesheet(root);
expect(
computeStyle(stylesheet, querySelector(root, '#element'))
).to.deep.equal({
animation: { animation: {
type: 'static', type: 'static',
inherited: false, inherited: false,

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const { computeStyle } = require('../lib/style.js'); const { collectStylesheet, computeStyle } = require('../lib/style.js');
const { pathElems } = require('./_collections.js'); const { pathElems } = require('./_collections.js');
const { path2js, js2path } = require('./_path.js'); const { path2js, js2path } = require('./_path.js');
const { applyTransforms } = require('./_applyTransforms.js'); const { applyTransforms } = require('./_applyTransforms.js');
@ -55,11 +55,12 @@ let arcTolerance;
* @author Kir Belevich * @author Kir Belevich
*/ */
exports.fn = (root, params) => { exports.fn = (root, params) => {
const stylesheet = collectStylesheet(root);
return { return {
element: { element: {
enter: (node) => { enter: (node) => {
if (pathElems.includes(node.name) && node.attributes.d != null) { if (pathElems.includes(node.name) && node.attributes.d != null) {
const computedStyle = computeStyle(node); const computedStyle = computeStyle(stylesheet, node);
precision = params.floatPrecision; precision = params.floatPrecision;
error = error =
precision !== false precision !== false

View File

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const { detachNodeFromParent } = require('../lib/xast.js'); 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'); const { path2js, js2path, intersects } = require('./_path.js');
exports.type = 'visitor'; exports.type = 'visitor';
@ -22,6 +22,7 @@ exports.fn = (root, params) => {
floatPrecision, floatPrecision,
noSpaceAfterFlags = false, // a20 60 45 0 1 30 20 → a20 60 45 0130 20 noSpaceAfterFlags = false, // a20 60 45 0 1 30 20 → a20 60 45 0130 20
} = params; } = params;
const stylesheet = collectStylesheet(root);
return { return {
element: { element: {
@ -53,7 +54,7 @@ exports.fn = (root, params) => {
} }
// preserve paths with markers // preserve paths with markers
const computedStyle = computeStyle(child); const computedStyle = computeStyle(stylesheet, child);
if ( if (
computedStyle['marker-start'] || computedStyle['marker-start'] ||
computedStyle['marker-mid'] || computedStyle['marker-mid'] ||

View File

@ -5,7 +5,7 @@ const {
closestByName, closestByName,
detachNodeFromParent, detachNodeFromParent,
} = require('../lib/xast.js'); } = require('../lib/xast.js');
const { computeStyle } = require('../lib/style.js'); const { collectStylesheet, computeStyle } = require('../lib/style.js');
const { parsePathData } = require('../lib/path.js'); const { parsePathData } = require('../lib/path.js');
exports.type = 'visitor'; exports.type = 'visitor';
@ -49,12 +49,14 @@ exports.fn = (root, params) => {
polylineEmptyPoints = true, polylineEmptyPoints = true,
polygonEmptyPoints = true, polygonEmptyPoints = true,
} = params; } = params;
const stylesheet = collectStylesheet(root);
return { return {
element: { element: {
enter: (node) => { enter: (node) => {
// Removes hidden elements // Removes hidden elements
// https://www.w3schools.com/cssref/pr_class_visibility.asp // https://www.w3schools.com/cssref/pr_class_visibility.asp
const computedStyle = computeStyle(node); const computedStyle = computeStyle(stylesheet, node);
if ( if (
isHidden && isHidden &&
computedStyle.visibility && computedStyle.visibility &&