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 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,26 +126,21 @@ 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) {
// find and parse all styles
visit(root, {
element: {
enter: (node) => {
if (node.name === 'style') {
const dynamic =
styleNode.attributes.media != null &&
styleNode.attributes.media !== 'all';
node.attributes.media != null && node.attributes.media !== 'all';
if (
styleNode.attributes.type == null ||
styleNode.attributes.type === '' ||
styleNode.attributes.type === 'text/css'
node.attributes.type == null ||
node.attributes.type === '' ||
node.attributes.type === 'text/css'
) {
const children = styleNode.children;
const children = node.children;
for (const child of children) {
if (child.type === 'text' || child.type === 'cdata') {
stylesheet.push(...parseStylesheet(child.value, dynamic));
@ -159,16 +148,23 @@ const computeStyle = (node) => {
}
}
}
},
},
});
// 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 &&

View File

@ -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', () => {
</style>
</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' },
});
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', () => {
</g>
</svg>
`);
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', () => {
<rect id="inline-style-over-style-rule" style="fill: purple !important;" class="b" />
</svg>
`);
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', () => {
<rect id="static" class="c" style="fill: black" />
</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 },
});
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', () => {
<rect id="static" class="c" />
</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 },
});
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', () => {
<rect id="invalid-type" class="c" />
</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' },
});
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', () => {
<rect id="element" class="a" />
</svg>
`);
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,

View File

@ -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

View File

@ -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'] ||

View File

@ -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 &&