diff --git a/lib/style.js b/lib/style.js
new file mode 100644
index 00000000..aedea265
--- /dev/null
+++ b/lib/style.js
@@ -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;
diff --git a/lib/style.test.js b/lib/style.test.js
new file mode 100644
index 00000000..56857246
--- /dev/null
+++ b/lib/style.test.js
@@ -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(`
+
+ `);
+ 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(`
+
+ `);
+ 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(`
+
+ `);
+ 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(`
+
+ `);
+ 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
+
+
+
+
+
+ `);
+ 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
+
+
+
+
+
+
+ `);
+ 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({});
+ });
+});
diff --git a/plugins/removeHiddenElems.js b/plugins/removeHiddenElems.js
index cf7fdd52..87aa5fc6 100644
--- a/plugins/removeHiddenElems.js
+++ b/plugins/removeHiddenElems.js
@@ -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"
- //
- //
- 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"
- //
- //
- 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"
- //
- //
- 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"
- //
- //
- 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"
- //
- //
- 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)"
- //
- //
- 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)"
- //
- //
- 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"
- //
- //
- 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"
- //
- //
- 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
- //
- //
- 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
- //
- //
- if (
- params.polylineEmptyPoints &&
- item.isElem('polyline') &&
- !item.hasAttr('points')
- ) return false;
-
- // Polygon with empty points
- //
- // https://www.w3.org/TR/SVG11/shapes.html#PolygonElementPointsAttribute
- //
- //
- 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"
+ //
+ //
+ 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"
+ //
+ //
+ 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"
+ //
+ //
+ 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"
+ //
+ //
+ 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"
+ //
+ //
+ 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)"
+ //
+ //
+ 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)"
+ //
+ //
+ 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"
+ //
+ //
+ 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"
+ //
+ //
+ 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
+ //
+ //
+ 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
+ //
+ //
+ if (
+ params.polylineEmptyPoints &&
+ item.isElem('polyline') &&
+ !item.hasAttr('points')
+ ) {
+ return false;
+ }
+
+ // Polygon with empty points
+ //
+ // https://www.w3.org/TR/SVG11/shapes.html#PolygonElementPointsAttribute
+ //
+ //
+ if (
+ params.polygonEmptyPoints &&
+ item.isElem('polygon') &&
+ !item.hasAttr('points')
+ ) {
+ return false;
+ }
+ }
};
diff --git a/test/plugins/removeHiddenElems.01.svg b/test/plugins/removeHiddenElems.01.svg
index 49b9eb70..fdf97078 100644
--- a/test/plugins/removeHiddenElems.01.svg
+++ b/test/plugins/removeHiddenElems.01.svg
@@ -1,11 +1,20 @@
@@@
diff --git a/test/plugins/removeHiddenElems.02.svg b/test/plugins/removeHiddenElems.02.svg
index 0c4c78fd..63cb005f 100644
--- a/test/plugins/removeHiddenElems.02.svg
+++ b/test/plugins/removeHiddenElems.02.svg
@@ -1,11 +1,20 @@
@@@
diff --git a/test/plugins/removeHiddenElems.12.svg b/test/plugins/removeHiddenElems.12.svg
index dee42820..b092c77e 100644
--- a/test/plugins/removeHiddenElems.12.svg
+++ b/test/plugins/removeHiddenElems.12.svg
@@ -1,4 +1,12 @@
+Keep invisible elements which have visibile ones inside
+and resolve styles
+
+===
+
@@@