mirror of
https://github.com/svg/svgo.git
synced 2025-07-31 07:44:22 +03:00
Implement style computing (#1399)
Ref https://github.com/svg/svgo/issues/777 Currently a lot of optimisations are attributes specific and may be broken because of inline or shared styles. In this diff I'm trying to solve the problem with getComputedStyle analog. `computeStyle` collects attributes, shared css rules, inline styles and inherited styles and checks whether they can be statically optimised or left as deoptimisation.
This commit is contained in:
184
lib/style.js
Normal file
184
lib/style.js
Normal file
@ -0,0 +1,184 @@
|
||||
'use strict';
|
||||
|
||||
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 { compareSpecificity } = require('./css-tools.js');
|
||||
const {
|
||||
attrsGroups,
|
||||
inheritableAttrs,
|
||||
presentationNonInheritableGroupAttrs,
|
||||
} = require('../plugins/_collections.js');
|
||||
|
||||
const cssSelectOptions = {
|
||||
xmlMode: true,
|
||||
adapter: svgoCssSelectAdapter,
|
||||
};
|
||||
|
||||
const parseRule = (ruleNode, dynamic) => {
|
||||
let selectors;
|
||||
let selectorsSpecificity;
|
||||
const declarations = [];
|
||||
csstree.walk(ruleNode, (cssNode) => {
|
||||
if (cssNode.type === 'SelectorList') {
|
||||
// compute specificity from original node to consider pseudo classes
|
||||
selectorsSpecificity = specificity(cssNode);
|
||||
const newSelectorsNode = csstree.clone(cssNode);
|
||||
csstree.walk(newSelectorsNode, (pseudoClassNode, item, list) => {
|
||||
if (pseudoClassNode.type === 'PseudoClassSelector') {
|
||||
dynamic = true;
|
||||
list.remove(item);
|
||||
}
|
||||
});
|
||||
selectors = csstree.generate(newSelectorsNode);
|
||||
return csstree.walk.skip;
|
||||
}
|
||||
if (cssNode.type === 'Declaration') {
|
||||
declarations.push({
|
||||
name: cssNode.property,
|
||||
value: csstree.generate(cssNode.value),
|
||||
important: cssNode.important,
|
||||
});
|
||||
return csstree.walk.skip;
|
||||
}
|
||||
});
|
||||
return {
|
||||
dynamic,
|
||||
selectors,
|
||||
specificity: selectorsSpecificity,
|
||||
declarations,
|
||||
};
|
||||
};
|
||||
|
||||
const parseStylesheet = (css, dynamic) => {
|
||||
const rules = [];
|
||||
const ast = csstree.parse(css);
|
||||
csstree.walk(ast, (cssNode) => {
|
||||
if (cssNode.type === 'Rule') {
|
||||
rules.push(parseRule(cssNode, dynamic || false));
|
||||
return csstree.walk.skip;
|
||||
}
|
||||
if (cssNode.type === 'Atrule') {
|
||||
csstree.walk(cssNode, (ruleNode) => {
|
||||
if (ruleNode.type === 'Rule') {
|
||||
rules.push(parseRule(ruleNode, dynamic || true));
|
||||
return csstree.walk.skip;
|
||||
}
|
||||
});
|
||||
return csstree.walk.skip;
|
||||
}
|
||||
});
|
||||
return rules;
|
||||
};
|
||||
|
||||
const computeOwnStyle = (node, stylesheet) => {
|
||||
const computedStyle = {};
|
||||
const importantStyles = new Map();
|
||||
|
||||
// collect attributes
|
||||
if (node.attrs) {
|
||||
for (const { name, value } of Object.values(node.attrs)) {
|
||||
if (attrsGroups.presentation.includes(name)) {
|
||||
computedStyle[name] = { type: 'static', inherited: false, value };
|
||||
importantStyles.set(name, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// collect matching rules
|
||||
for (const { selectors, declarations, dynamic } of stylesheet) {
|
||||
if (is(node, selectors, cssSelectOptions)) {
|
||||
for (const { name, value, important } of declarations) {
|
||||
const computed = computedStyle[name];
|
||||
if (computed && computed.type === 'dynamic') {
|
||||
continue;
|
||||
}
|
||||
if (dynamic) {
|
||||
computedStyle[name] = { type: 'dynamic', inherited: false };
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
computed == null ||
|
||||
important === true ||
|
||||
importantStyles.get(name) === false
|
||||
) {
|
||||
computedStyle[name] = { type: 'static', inherited: false, value };
|
||||
importantStyles.set(name, important);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// collect inline styles
|
||||
for (const [name, { value, priority }] of node.style.properties) {
|
||||
const computed = computedStyle[name];
|
||||
const important = priority === 'important';
|
||||
if (computed && computed.type === 'dynamic') {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
computed == null ||
|
||||
important === true ||
|
||||
importantStyles.get(name) === false
|
||||
) {
|
||||
computedStyle[name] = { type: 'static', inherited: false, value };
|
||||
importantStyles.set(name, important);
|
||||
}
|
||||
}
|
||||
|
||||
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 stylesheet = [];
|
||||
for (const styleNode of styleNodes) {
|
||||
const dynamic =
|
||||
styleNode.hasAttr('media') && styleNode.attr('media').value !== 'all';
|
||||
if (
|
||||
styleNode.hasAttr('type') === false ||
|
||||
styleNode.attr('type').value === '' ||
|
||||
styleNode.attr('type').value === 'text/css'
|
||||
) {
|
||||
const children = styleNode.content || [];
|
||||
for (const child of children) {
|
||||
const css = child.text || child.cdata;
|
||||
stylesheet.push(...parseStylesheet(css, dynamic));
|
||||
}
|
||||
}
|
||||
}
|
||||
// sort by selectors specificity
|
||||
stable.inplace(stylesheet, (a, b) =>
|
||||
compareSpecificity(a.specificity, b.specificity)
|
||||
);
|
||||
|
||||
// collect inherited styles
|
||||
const computedStyles = computeOwnStyle(node, stylesheet);
|
||||
let parent = node;
|
||||
while (parent.parentNode && parent.parentNode.elem !== '#document') {
|
||||
const inheritedStyles = computeOwnStyle(parent.parentNode, stylesheet);
|
||||
for (const [name, computed] of Object.entries(inheritedStyles)) {
|
||||
if (
|
||||
computedStyles[name] == null &&
|
||||
// ignore not inheritable styles
|
||||
inheritableAttrs.includes(name) === true &&
|
||||
presentationNonInheritableGroupAttrs.includes(name) === false
|
||||
) {
|
||||
computedStyles[name] = { ...computed, inherited: true };
|
||||
}
|
||||
}
|
||||
parent = parent.parentNode;
|
||||
}
|
||||
|
||||
return computedStyles;
|
||||
};
|
||||
exports.computeStyle = computeStyle;
|
215
lib/style.test.js
Normal file
215
lib/style.test.js
Normal file
@ -0,0 +1,215 @@
|
||||
'use strict';
|
||||
|
||||
const { expect } = require('chai');
|
||||
const { computeStyle } = require('./style.js');
|
||||
const svg2js = require('./svgo/svg2js.js');
|
||||
|
||||
describe('computeStyle', () => {
|
||||
it('collects styles', () => {
|
||||
const root = svg2js(`
|
||||
<svg>
|
||||
<rect id="class" class="a" />
|
||||
<rect id="two-classes" class="b a" />
|
||||
<rect id="attribute" fill="purple" />
|
||||
<rect id="inline-style" style="fill: grey;" />
|
||||
<g fill="yellow">
|
||||
<rect id="inheritance" />
|
||||
<g style="fill: blue;">
|
||||
<g>
|
||||
<rect id="nested-inheritance" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<style>
|
||||
.a { fill: red; }
|
||||
</style>
|
||||
<style>
|
||||
<![CDATA[
|
||||
.b { fill: green; stroke: black; }
|
||||
]]>
|
||||
</style>
|
||||
</svg>
|
||||
`);
|
||||
expect(computeStyle(root.querySelector('#class'))).to.deep.equal({
|
||||
fill: { type: 'static', inherited: false, value: 'red' },
|
||||
});
|
||||
expect(computeStyle(root.querySelector('#two-classes'))).to.deep.equal({
|
||||
fill: { type: 'static', inherited: false, value: 'green' },
|
||||
stroke: { type: 'static', inherited: false, value: 'black' },
|
||||
});
|
||||
expect(computeStyle(root.querySelector('#attribute'))).to.deep.equal({
|
||||
fill: { type: 'static', inherited: false, value: 'purple' },
|
||||
});
|
||||
expect(computeStyle(root.querySelector('#inline-style'))).to.deep.equal({
|
||||
fill: { type: 'static', inherited: false, value: 'grey' },
|
||||
});
|
||||
expect(computeStyle(root.querySelector('#inheritance'))).to.deep.equal({
|
||||
fill: { type: 'static', inherited: true, value: 'yellow' },
|
||||
});
|
||||
expect(
|
||||
computeStyle(root.querySelector('#nested-inheritance'))
|
||||
).to.deep.equal({
|
||||
fill: { type: 'static', inherited: true, value: 'blue' },
|
||||
});
|
||||
});
|
||||
|
||||
it('prioritizes different kinds of styles', () => {
|
||||
const root = svg2js(`
|
||||
<svg>
|
||||
<style>
|
||||
g > .a { fill: red; }
|
||||
.a { fill: green; }
|
||||
.b { fill: blue; }
|
||||
</style>
|
||||
<g fill="yellow">
|
||||
<rect id="complex-selector" class="a" />
|
||||
<rect id="attribute-over-inheritance" fill="orange" />
|
||||
<rect id="style-rule-over-attribute" class="b" fill="grey" />
|
||||
<rect id="inline-style-over-style-rule" style="fill: purple;" class="b" />
|
||||
</g>
|
||||
</svg>
|
||||
`);
|
||||
expect(computeStyle(root.querySelector('#complex-selector'))).to.deep.equal(
|
||||
{
|
||||
fill: { type: 'static', inherited: false, value: 'red' },
|
||||
}
|
||||
);
|
||||
expect(
|
||||
computeStyle(root.querySelector('#attribute-over-inheritance'))
|
||||
).to.deep.equal({
|
||||
fill: { type: 'static', inherited: false, value: 'orange' },
|
||||
});
|
||||
expect(
|
||||
computeStyle(root.querySelector('#style-rule-over-attribute'))
|
||||
).to.deep.equal({
|
||||
fill: { type: 'static', inherited: false, value: 'blue' },
|
||||
});
|
||||
expect(
|
||||
computeStyle(root.querySelector('#inline-style-over-style-rule'))
|
||||
).to.deep.equal({
|
||||
fill: { type: 'static', inherited: false, value: 'purple' },
|
||||
});
|
||||
});
|
||||
|
||||
it('prioritizes important styles', () => {
|
||||
const root = svg2js(`
|
||||
<svg>
|
||||
<style>
|
||||
g > .a { fill: red; }
|
||||
.b { fill: green !important; }
|
||||
</style>
|
||||
<rect id="complex-selector" class="a b" />
|
||||
<rect id="style-rule-over-inline-style" style="fill: orange;" class="b" />
|
||||
<rect id="inline-style-over-style-rule" style="fill: purple !important;" class="b" />
|
||||
</svg>
|
||||
`);
|
||||
expect(computeStyle(root.querySelector('#complex-selector'))).to.deep.equal(
|
||||
{
|
||||
fill: { type: 'static', inherited: false, value: 'green' },
|
||||
}
|
||||
);
|
||||
expect(
|
||||
computeStyle(root.querySelector('#style-rule-over-inline-style'))
|
||||
).to.deep.equal({
|
||||
fill: { type: 'static', inherited: false, value: 'green' },
|
||||
});
|
||||
expect(
|
||||
computeStyle(root.querySelector('#inline-style-over-style-rule'))
|
||||
).to.deep.equal({
|
||||
fill: { type: 'static', inherited: false, value: 'purple' },
|
||||
});
|
||||
});
|
||||
|
||||
it('treats at-rules and pseudo-classes as dynamic styles', () => {
|
||||
const root = svg2js(`
|
||||
<svg>
|
||||
<style>
|
||||
@media screen {
|
||||
.a { fill: red; }
|
||||
}
|
||||
.b:hover { fill: green; }
|
||||
.c { fill: blue; }
|
||||
.d { fill: purple; }
|
||||
</style>
|
||||
<rect id="media-query" class="a d" style="fill: orange;" />
|
||||
<rect id="hover" class="b" style="fill: yellow;" />
|
||||
<g class="a">
|
||||
<rect id="inherited" />
|
||||
<rect id="inherited-overriden" class="c" />
|
||||
</g>
|
||||
<rect id="static" class="c" style="fill: black" />
|
||||
</svg>
|
||||
`);
|
||||
expect(computeStyle(root.querySelector('#media-query'))).to.deep.equal({
|
||||
fill: { type: 'dynamic', inherited: false },
|
||||
});
|
||||
expect(computeStyle(root.querySelector('#hover'))).to.deep.equal({
|
||||
fill: { type: 'dynamic', inherited: false },
|
||||
});
|
||||
expect(computeStyle(root.querySelector('#inherited'))).to.deep.equal({
|
||||
fill: { type: 'dynamic', inherited: true },
|
||||
});
|
||||
expect(
|
||||
computeStyle(root.querySelector('#inherited-overriden'))
|
||||
).to.deep.equal({
|
||||
fill: { type: 'static', inherited: false, value: 'blue' },
|
||||
});
|
||||
expect(computeStyle(root.querySelector('#static'))).to.deep.equal({
|
||||
fill: { type: 'static', inherited: false, value: 'black' },
|
||||
});
|
||||
});
|
||||
|
||||
it('considers <style> media attribute', () => {
|
||||
const root = svg2js(`
|
||||
<svg>
|
||||
<style media="print">
|
||||
@media screen {
|
||||
.a { fill: red; }
|
||||
}
|
||||
.b { fill: green; }
|
||||
</style>
|
||||
<style media="all">
|
||||
.c { fill: blue; }
|
||||
</style>
|
||||
<rect id="media-query" class="a" />
|
||||
<rect id="kinda-static" class="b" />
|
||||
<rect id="static" class="c" />
|
||||
</svg>
|
||||
`);
|
||||
expect(computeStyle(root.querySelector('#media-query'))).to.deep.equal({
|
||||
fill: { type: 'dynamic', inherited: false },
|
||||
});
|
||||
expect(computeStyle(root.querySelector('#kinda-static'))).to.deep.equal({
|
||||
fill: { type: 'dynamic', inherited: false },
|
||||
});
|
||||
expect(computeStyle(root.querySelector('#static'))).to.deep.equal({
|
||||
fill: { type: 'static', inherited: false, value: 'blue' },
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores <style> with invalid type', () => {
|
||||
const root = svg2js(`
|
||||
<svg>
|
||||
<style type="text/css">
|
||||
.a { fill: red; }
|
||||
</style>
|
||||
<style type="">
|
||||
.b { fill: green; }
|
||||
</style>
|
||||
<style type="text/invalid">
|
||||
.c { fill: blue; }
|
||||
</style>
|
||||
<rect id="valid-type" class="a" />
|
||||
<rect id="empty-type" class="b" />
|
||||
<rect id="invalid-type" class="c" />
|
||||
</svg>
|
||||
`);
|
||||
expect(computeStyle(root.querySelector('#valid-type'))).to.deep.equal({
|
||||
fill: { type: 'static', inherited: false, value: 'red' },
|
||||
});
|
||||
expect(computeStyle(root.querySelector('#empty-type'))).to.deep.equal({
|
||||
fill: { type: 'static', inherited: false, value: 'green' },
|
||||
});
|
||||
expect(computeStyle(root.querySelector('#invalid-type'))).to.deep.equal({});
|
||||
});
|
||||
});
|
@ -1,27 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
const { computeStyle } = require('../lib/style.js');
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'removes hidden elements (zero sized, with absent attributes)';
|
||||
exports.description =
|
||||
'removes hidden elements (zero sized, with absent attributes)';
|
||||
|
||||
exports.params = {
|
||||
isHidden: true,
|
||||
displayNone: true,
|
||||
opacity0: true,
|
||||
circleR0: true,
|
||||
ellipseRX0: true,
|
||||
ellipseRY0: true,
|
||||
rectWidth0: true,
|
||||
rectHeight0: true,
|
||||
patternWidth0: true,
|
||||
patternHeight0: true,
|
||||
imageWidth0: true,
|
||||
imageHeight0: true,
|
||||
pathEmptyD: true,
|
||||
polylineEmptyPoints: true,
|
||||
polygonEmptyPoints: true
|
||||
isHidden: true,
|
||||
displayNone: true,
|
||||
opacity0: true,
|
||||
circleR0: true,
|
||||
ellipseRX0: true,
|
||||
ellipseRY0: true,
|
||||
rectWidth0: true,
|
||||
rectHeight0: true,
|
||||
patternWidth0: true,
|
||||
patternHeight0: true,
|
||||
imageWidth0: true,
|
||||
imageHeight0: true,
|
||||
pathEmptyD: true,
|
||||
polylineEmptyPoints: true,
|
||||
polygonEmptyPoints: true,
|
||||
};
|
||||
|
||||
var regValidPath = /M\s*(?:[-+]?(?:\d*\.\d+|\d+(?:\.|(?!\.)))([eE][-+]?\d+)?(?!\d)\s*,?\s*){2}\D*\d/i;
|
||||
@ -46,184 +49,218 @@ var regValidPath = /M\s*(?:[-+]?(?:\d*\.\d+|\d+(?:\.|(?!\.)))([eE][-+]?\d+)?(?!\
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function (item, params) {
|
||||
|
||||
if (item.elem) {
|
||||
// Removes hidden elements
|
||||
// https://www.w3schools.com/cssref/pr_class_visibility.asp
|
||||
if (
|
||||
params.isHidden &&
|
||||
item.hasAttr('visibility', 'hidden') &&
|
||||
// keep if any descendant enables visibility
|
||||
item.querySelector('[visibility=visible]') == null
|
||||
) return false;
|
||||
|
||||
// display="none"
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/painting.html#DisplayProperty
|
||||
// "A value of display: none indicates that the given element
|
||||
// and its children shall not be rendered directly"
|
||||
if (
|
||||
params.displayNone &&
|
||||
item.hasAttr('display', 'none')
|
||||
) return false;
|
||||
|
||||
// opacity="0"
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/masking.html#ObjectAndGroupOpacityProperties
|
||||
if (
|
||||
params.opacity0 &&
|
||||
item.hasAttr('opacity', '0') &&
|
||||
// transparent element inside clipPath still affect clipped elements
|
||||
item.closestElem('clipPath') == null
|
||||
) return false;
|
||||
|
||||
// Circles with zero radius
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/shapes.html#CircleElementRAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <circle r="0">
|
||||
if (
|
||||
params.circleR0 &&
|
||||
item.isElem('circle') &&
|
||||
item.isEmpty() &&
|
||||
item.hasAttr('r', '0')
|
||||
) return false;
|
||||
|
||||
// Ellipse with zero x-axis radius
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/shapes.html#EllipseElementRXAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <ellipse rx="0">
|
||||
if (
|
||||
params.ellipseRX0 &&
|
||||
item.isElem('ellipse') &&
|
||||
item.isEmpty() &&
|
||||
item.hasAttr('rx', '0')
|
||||
) return false;
|
||||
|
||||
// Ellipse with zero y-axis radius
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/shapes.html#EllipseElementRYAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <ellipse ry="0">
|
||||
if (
|
||||
params.ellipseRY0 &&
|
||||
item.isElem('ellipse') &&
|
||||
item.isEmpty() &&
|
||||
item.hasAttr('ry', '0')
|
||||
) return false;
|
||||
|
||||
// Rectangle with zero width
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/shapes.html#RectElementWidthAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <rect width="0">
|
||||
if (
|
||||
params.rectWidth0 &&
|
||||
item.isElem('rect') &&
|
||||
item.isEmpty() &&
|
||||
item.hasAttr('width', '0')
|
||||
) return false;
|
||||
|
||||
// Rectangle with zero height
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/shapes.html#RectElementHeightAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <rect height="0">
|
||||
if (
|
||||
params.rectHeight0 &&
|
||||
params.rectWidth0 &&
|
||||
item.isElem('rect') &&
|
||||
item.isEmpty() &&
|
||||
item.hasAttr('height', '0')
|
||||
) return false;
|
||||
|
||||
// Pattern with zero width
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/pservers.html#PatternElementWidthAttribute
|
||||
// "A value of zero disables rendering of the element (i.e., no paint is applied)"
|
||||
//
|
||||
// <pattern width="0">
|
||||
if (
|
||||
params.patternWidth0 &&
|
||||
item.isElem('pattern') &&
|
||||
item.hasAttr('width', '0')
|
||||
) return false;
|
||||
|
||||
// Pattern with zero height
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/pservers.html#PatternElementHeightAttribute
|
||||
// "A value of zero disables rendering of the element (i.e., no paint is applied)"
|
||||
//
|
||||
// <pattern height="0">
|
||||
if (
|
||||
params.patternHeight0 &&
|
||||
item.isElem('pattern') &&
|
||||
item.hasAttr('height', '0')
|
||||
) return false;
|
||||
|
||||
// Image with zero width
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/struct.html#ImageElementWidthAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <image width="0">
|
||||
if (
|
||||
params.imageWidth0 &&
|
||||
item.isElem('image') &&
|
||||
item.hasAttr('width', '0')
|
||||
) return false;
|
||||
|
||||
// Image with zero height
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/struct.html#ImageElementHeightAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <image height="0">
|
||||
if (
|
||||
params.imageHeight0 &&
|
||||
item.isElem('image') &&
|
||||
item.hasAttr('height', '0')
|
||||
) return false;
|
||||
|
||||
// Path with empty data
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/paths.html#DAttribute
|
||||
//
|
||||
// <path d=""/>
|
||||
if (
|
||||
params.pathEmptyD &&
|
||||
item.isElem('path') &&
|
||||
(!item.hasAttr('d') || !regValidPath.test(item.attr('d').value))
|
||||
) return false;
|
||||
|
||||
// Polyline with empty points
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/shapes.html#PolylineElementPointsAttribute
|
||||
//
|
||||
// <polyline points="">
|
||||
if (
|
||||
params.polylineEmptyPoints &&
|
||||
item.isElem('polyline') &&
|
||||
!item.hasAttr('points')
|
||||
) return false;
|
||||
|
||||
// Polygon with empty points
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/shapes.html#PolygonElementPointsAttribute
|
||||
//
|
||||
// <polygon points="">
|
||||
if (
|
||||
params.polygonEmptyPoints &&
|
||||
item.isElem('polygon') &&
|
||||
!item.hasAttr('points')
|
||||
) return false;
|
||||
|
||||
if (item.elem) {
|
||||
// Removes hidden elements
|
||||
// https://www.w3schools.com/cssref/pr_class_visibility.asp
|
||||
const computedStyle = computeStyle(item);
|
||||
if (
|
||||
params.isHidden &&
|
||||
computedStyle.visibility &&
|
||||
computedStyle.visibility.type === 'static' &&
|
||||
computedStyle.visibility.value === 'hidden' &&
|
||||
// keep if any descendant enables visibility
|
||||
item.querySelector('[visibility=visible]') == null
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// display="none"
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/painting.html#DisplayProperty
|
||||
// "A value of display: none indicates that the given element
|
||||
// and its children shall not be rendered directly"
|
||||
if (
|
||||
params.displayNone &&
|
||||
computedStyle.display &&
|
||||
computedStyle.display.type === 'static' &&
|
||||
computedStyle.display.value === 'none'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// opacity="0"
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/masking.html#ObjectAndGroupOpacityProperties
|
||||
if (
|
||||
params.opacity0 &&
|
||||
computedStyle.opacity &&
|
||||
computedStyle.opacity.type === 'static' &&
|
||||
computedStyle.opacity.value === '0' &&
|
||||
// transparent element inside clipPath still affect clipped elements
|
||||
item.closestElem('clipPath') == null
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Circles with zero radius
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/shapes.html#CircleElementRAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <circle r="0">
|
||||
if (
|
||||
params.circleR0 &&
|
||||
item.isElem('circle') &&
|
||||
item.isEmpty() &&
|
||||
item.hasAttr('r', '0')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ellipse with zero x-axis radius
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/shapes.html#EllipseElementRXAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <ellipse rx="0">
|
||||
if (
|
||||
params.ellipseRX0 &&
|
||||
item.isElem('ellipse') &&
|
||||
item.isEmpty() &&
|
||||
item.hasAttr('rx', '0')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ellipse with zero y-axis radius
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/shapes.html#EllipseElementRYAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <ellipse ry="0">
|
||||
if (
|
||||
params.ellipseRY0 &&
|
||||
item.isElem('ellipse') &&
|
||||
item.isEmpty() &&
|
||||
item.hasAttr('ry', '0')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rectangle with zero width
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/shapes.html#RectElementWidthAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <rect width="0">
|
||||
if (
|
||||
params.rectWidth0 &&
|
||||
item.isElem('rect') &&
|
||||
item.isEmpty() &&
|
||||
item.hasAttr('width', '0')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rectangle with zero height
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/shapes.html#RectElementHeightAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <rect height="0">
|
||||
if (
|
||||
params.rectHeight0 &&
|
||||
params.rectWidth0 &&
|
||||
item.isElem('rect') &&
|
||||
item.isEmpty() &&
|
||||
item.hasAttr('height', '0')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pattern with zero width
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/pservers.html#PatternElementWidthAttribute
|
||||
// "A value of zero disables rendering of the element (i.e., no paint is applied)"
|
||||
//
|
||||
// <pattern width="0">
|
||||
if (
|
||||
params.patternWidth0 &&
|
||||
item.isElem('pattern') &&
|
||||
item.hasAttr('width', '0')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pattern with zero height
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/pservers.html#PatternElementHeightAttribute
|
||||
// "A value of zero disables rendering of the element (i.e., no paint is applied)"
|
||||
//
|
||||
// <pattern height="0">
|
||||
if (
|
||||
params.patternHeight0 &&
|
||||
item.isElem('pattern') &&
|
||||
item.hasAttr('height', '0')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Image with zero width
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/struct.html#ImageElementWidthAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <image width="0">
|
||||
if (
|
||||
params.imageWidth0 &&
|
||||
item.isElem('image') &&
|
||||
item.hasAttr('width', '0')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Image with zero height
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/struct.html#ImageElementHeightAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <image height="0">
|
||||
if (
|
||||
params.imageHeight0 &&
|
||||
item.isElem('image') &&
|
||||
item.hasAttr('height', '0')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Path with empty data
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/paths.html#DAttribute
|
||||
//
|
||||
// <path d=""/>
|
||||
if (
|
||||
params.pathEmptyD &&
|
||||
item.isElem('path') &&
|
||||
(!item.hasAttr('d') || !regValidPath.test(item.attr('d').value))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Polyline with empty points
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/shapes.html#PolylineElementPointsAttribute
|
||||
//
|
||||
// <polyline points="">
|
||||
if (
|
||||
params.polylineEmptyPoints &&
|
||||
item.isElem('polyline') &&
|
||||
!item.hasAttr('points')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Polygon with empty points
|
||||
//
|
||||
// https://www.w3.org/TR/SVG11/shapes.html#PolygonElementPointsAttribute
|
||||
//
|
||||
// <polygon points="">
|
||||
if (
|
||||
params.polygonEmptyPoints &&
|
||||
item.isElem('polygon') &&
|
||||
!item.hasAttr('points')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,11 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.a { display: block; }
|
||||
</style>
|
||||
<g>
|
||||
<path display="none" d="..."/>
|
||||
<rect display="none" x="0" y="0" width="20" height="20" />
|
||||
<rect display="none" class="a" x="0" y="0" width="20" height="20" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
@@@
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<g/>
|
||||
<style>
|
||||
.a { display: block; }
|
||||
</style>
|
||||
<g>
|
||||
<rect display="none" class="a" x="0" y="0" width="20" height="20"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 167 B After Width: | Height: | Size: 466 B |
@ -1,11 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.a { opacity: 0.5; }
|
||||
</style>
|
||||
<g>
|
||||
<path opacity="0" d="..."/>
|
||||
<rect opacity="0" x="0" y="0" width="20" height="20" />
|
||||
<rect opacity="0" class="a" x="0" y="0" width="20" height="20" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
@@@
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<g/>
|
||||
<style>
|
||||
.a { opacity: 0.5; }
|
||||
</style>
|
||||
<g>
|
||||
<rect opacity="0" class="a" x="0" y="0" width="20" height="20"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 164 B After Width: | Height: | Size: 453 B |
@ -1,4 +1,12 @@
|
||||
Keep invisible elements which have visibile ones inside
|
||||
and resolve styles
|
||||
|
||||
===
|
||||
|
||||
<svg width="480" height="360" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.a { visibility: visible; }
|
||||
</style>
|
||||
<rect x="96" y="96" width="96" height="96" fill="lime" />
|
||||
<g visibility="hidden">
|
||||
<rect x="96" y="96" width="96" height="96" fill="red" />
|
||||
@ -7,14 +15,19 @@
|
||||
<g visibility="hidden">
|
||||
<rect x="196" y="196" width="96" height="96" fill="lime" visibility="visible" />
|
||||
</g>
|
||||
<rect x="96" y="96" width="96" height="96" visibility="hidden" class="a" />
|
||||
</svg>
|
||||
|
||||
@@@
|
||||
|
||||
<svg width="480" height="360" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.a { visibility: visible; }
|
||||
</style>
|
||||
<rect x="96" y="96" width="96" height="96" fill="lime"/>
|
||||
<rect x="196.5" y="196.5" width="95" height="95" fill="red"/>
|
||||
<g visibility="hidden">
|
||||
<rect x="196" y="196" width="96" height="96" fill="lime" visibility="visible"/>
|
||||
</g>
|
||||
<rect x="96" y="96" width="96" height="96" visibility="hidden" class="a"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 740 B After Width: | Height: | Size: 1.1 KiB |
Reference in New Issue
Block a user