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:
50
lib/style.js
50
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,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 &&
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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'] ||
|
||||
|
@ -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 &&
|
||||
|
Reference in New Issue
Block a user