diff --git a/lib/style.test.js b/lib/style.test.js index 97c0900b..9171ffe1 100644 --- a/lib/style.test.js +++ b/lib/style.test.js @@ -2,6 +2,7 @@ const { expect } = require('chai'); const { computeStyle } = require('./style.js'); +const { querySelector } = require('./xast.js'); const svg2js = require('./svgo/svg2js.js'); describe('computeStyle', () => { @@ -30,24 +31,24 @@ describe('computeStyle', () => { `); - expect(computeStyle(root.querySelector('#class'))).to.deep.equal({ + expect(computeStyle(querySelector(root, '#class'))).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'red' }, }); - expect(computeStyle(root.querySelector('#two-classes'))).to.deep.equal({ + expect(computeStyle(querySelector(root, '#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({ + expect(computeStyle(querySelector(root, '#attribute'))).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'purple' }, }); - expect(computeStyle(root.querySelector('#inline-style'))).to.deep.equal({ + expect(computeStyle(querySelector(root, '#inline-style'))).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'grey' }, }); - expect(computeStyle(root.querySelector('#inheritance'))).to.deep.equal({ + expect(computeStyle(querySelector(root, '#inheritance'))).to.deep.equal({ fill: { type: 'static', inherited: true, value: 'yellow' }, }); expect( - computeStyle(root.querySelector('#nested-inheritance')) + computeStyle(querySelector(root, '#nested-inheritance')) ).to.deep.equal({ fill: { type: 'static', inherited: true, value: 'blue' }, }); @@ -69,23 +70,23 @@ describe('computeStyle', () => { `); - expect(computeStyle(root.querySelector('#complex-selector'))).to.deep.equal( - { - fill: { type: 'static', inherited: false, value: 'red' }, - } - ); expect( - computeStyle(root.querySelector('#attribute-over-inheritance')) + computeStyle(querySelector(root, '#complex-selector')) + ).to.deep.equal({ + fill: { type: 'static', inherited: false, value: 'red' }, + }); + expect( + computeStyle(querySelector(root, '#attribute-over-inheritance')) ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'orange' }, }); expect( - computeStyle(root.querySelector('#style-rule-over-attribute')) + computeStyle(querySelector(root, '#style-rule-over-attribute')) ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'blue' }, }); expect( - computeStyle(root.querySelector('#inline-style-over-style-rule')) + computeStyle(querySelector(root, '#inline-style-over-style-rule')) ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'purple' }, }); @@ -103,18 +104,18 @@ describe('computeStyle', () => { `); - 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')) + computeStyle(querySelector(root, '#complex-selector')) ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'green' }, }); expect( - computeStyle(root.querySelector('#inline-style-over-style-rule')) + computeStyle(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')) ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'purple' }, }); @@ -140,21 +141,21 @@ describe('computeStyle', () => { `); - expect(computeStyle(root.querySelector('#media-query'))).to.deep.equal({ + expect(computeStyle(querySelector(root, '#media-query'))).to.deep.equal({ fill: { type: 'dynamic', inherited: false }, }); - expect(computeStyle(root.querySelector('#hover'))).to.deep.equal({ + expect(computeStyle(querySelector(root, '#hover'))).to.deep.equal({ fill: { type: 'dynamic', inherited: false }, }); - expect(computeStyle(root.querySelector('#inherited'))).to.deep.equal({ + expect(computeStyle(querySelector(root, '#inherited'))).to.deep.equal({ fill: { type: 'dynamic', inherited: true }, }); expect( - computeStyle(root.querySelector('#inherited-overriden')) + computeStyle(querySelector(root, '#inherited-overriden')) ).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'blue' }, }); - expect(computeStyle(root.querySelector('#static'))).to.deep.equal({ + expect(computeStyle(querySelector(root, '#static'))).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'black' }, }); }); @@ -176,13 +177,13 @@ describe('computeStyle', () => { `); - expect(computeStyle(root.querySelector('#media-query'))).to.deep.equal({ + expect(computeStyle(querySelector(root, '#media-query'))).to.deep.equal({ fill: { type: 'dynamic', inherited: false }, }); - expect(computeStyle(root.querySelector('#kinda-static'))).to.deep.equal({ + expect(computeStyle(querySelector(root, '#kinda-static'))).to.deep.equal({ fill: { type: 'dynamic', inherited: false }, }); - expect(computeStyle(root.querySelector('#static'))).to.deep.equal({ + expect(computeStyle(querySelector(root, '#static'))).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'blue' }, }); }); @@ -204,13 +205,15 @@ describe('computeStyle', () => { `); - expect(computeStyle(root.querySelector('#valid-type'))).to.deep.equal({ + expect(computeStyle(querySelector(root, '#valid-type'))).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'red' }, }); - expect(computeStyle(root.querySelector('#empty-type'))).to.deep.equal({ + expect(computeStyle(querySelector(root, '#empty-type'))).to.deep.equal({ fill: { type: 'static', inherited: false, value: 'green' }, }); - expect(computeStyle(root.querySelector('#invalid-type'))).to.deep.equal({}); + expect(computeStyle(querySelector(root, '#invalid-type'))).to.deep.equal( + {} + ); }); it('ignores keyframes atrule', () => { @@ -235,7 +238,7 @@ describe('computeStyle', () => { `); - expect(computeStyle(root.querySelector('#element'))).to.deep.equal({ + expect(computeStyle(querySelector(root, '#element'))).to.deep.equal({ animation: { type: 'static', inherited: false, diff --git a/lib/xast.js b/lib/xast.js new file mode 100644 index 00000000..8a8118bc --- /dev/null +++ b/lib/xast.js @@ -0,0 +1,53 @@ +'use strict'; + +const { selectAll, selectOne, is } = require('css-select'); +const xastAdaptor = require('./svgo/css-select-adapter.js'); + +const cssSelectOptions = { + xmlMode: true, + adapter: xastAdaptor, +}; + +const querySelectorAll = (node, selector) => { + return selectAll(selector, node, cssSelectOptions); +}; +exports.querySelectorAll = querySelectorAll; + +const querySelector = (node, selector) => { + return selectOne(selector, node, cssSelectOptions); +}; +exports.querySelector = querySelector; + +const matches = (node, selector) => { + return is(node, selector, cssSelectOptions); +}; +exports.matches = matches; + +const closestByName = (node, name) => { + let currentNode = node; + while (currentNode) { + if (currentNode.type === 'element' && currentNode.name === name) { + return currentNode; + } + currentNode = currentNode.parentNode; + } + return null; +}; +exports.closestByName = closestByName; + +const traverseBreak = Symbol(); +exports.traverseBreak = traverseBreak; + +const traverse = (node, fn) => { + if (fn(node) === traverseBreak) { + return traverseBreak; + } + if (node.type === 'root' || node.type === 'element') { + for (const child of node.children) { + if (traverse(child, fn) === traverseBreak) { + return traverseBreak; + } + } + } +}; +exports.traverse = traverse; diff --git a/plugins/cleanupEnableBackground.js b/plugins/cleanupEnableBackground.js index 5ef55193..3abae8bb 100644 --- a/plugins/cleanupEnableBackground.js +++ b/plugins/cleanupEnableBackground.js @@ -1,5 +1,7 @@ 'use strict'; +const { traverse } = require('../lib/xast.js'); + exports.type = 'full'; exports.active = true; @@ -17,72 +19,55 @@ exports.description = * ⬇ * * - * @param {Object} item current iteration item + * @param {Object} root current iteration item * @return {Boolean} if false, item will be filtered out * * @author Kir Belevich */ -exports.fn = function (data) { - var regEnableBackground = /^new\s0\s0\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)$/, - hasFilter = false, - elems = ['svg', 'mask', 'pattern']; +exports.fn = function (root) { + const regEnableBackground = /^new\s0\s0\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)$/; + let hasFilter = false; + const elems = ['svg', 'mask', 'pattern']; - function checkEnableBackground(item) { - if ( - item.isElem(elems) && - item.attributes['enable-background'] != null && - item.attributes.width != null && - item.attributes.height != null - ) { - var match = item.attributes['enable-background'].match( - regEnableBackground - ); + traverse(root, (node) => { + if (node.type === 'element') { + if ( + elems.includes(node.name) && + node.attributes['enable-background'] != null && + node.attributes.width != null && + node.attributes.height != null + ) { + const match = node.attributes['enable-background'].match( + regEnableBackground + ); - if (match) { - if ( - item.attributes.width === match[1] && - item.attributes.height === match[3] - ) { - if (item.isElem('svg')) { - delete item.attributes['enable-background']; - } else { - item.attributes['enable-background'] = 'new'; + if (match) { + if ( + node.attributes.width === match[1] && + node.attributes.height === match[3] + ) { + if (node.name === 'svg') { + delete node.attributes['enable-background']; + } else { + node.attributes['enable-background'] = 'new'; + } } } } - } - } - - function checkForFilter(item) { - if (item.isElem('filter')) { - hasFilter = true; - } - } - - function monkeys(items, fn) { - items.children.forEach(function (item) { - fn(item); - - if (item.children) { - monkeys(item, fn); + if (node.name === 'filter') { + hasFilter = true; } - }); - return items; - } - - var firstStep = monkeys(data, function (item) { - checkEnableBackground(item); - if (!hasFilter) { - checkForFilter(item); } }); - return hasFilter - ? firstStep - : monkeys(firstStep, (item) => { - if (item.type === 'element') { - //we don't need 'enable-background' if we have no filters - delete item.attributes['enable-background']; - } - }); + if (hasFilter === false) { + traverse(root, (node) => { + if (node.type === 'element') { + //we don't need 'enable-background' if we have no filters + delete node.attributes['enable-background']; + } + }); + } + + return root; }; diff --git a/plugins/cleanupIDs.js b/plugins/cleanupIDs.js index 62d382a2..08c99400 100644 --- a/plugins/cleanupIDs.js +++ b/plugins/cleanupIDs.js @@ -1,5 +1,6 @@ 'use strict'; +const { traverse, traverseBreak } = require('../lib/xast.js'); const { parseName } = require('../lib/svgo/tools.js'); exports.type = 'full'; @@ -87,7 +88,7 @@ var referencesProps = new Set(require('./_collections').referencesProps), * * @author Kir Belevich */ -exports.fn = function (data, params) { +exports.fn = function (root, params) { var currentID, currentIDstring, IDs = new Map(), @@ -110,88 +111,73 @@ exports.fn = function (data, params) { idValuePrefix = '#', idValuePostfix = '.'; - /** - * Bananas! - * - * @param {Array} items input items - * @return {Array} output items - */ - function monkeys(items) { - for (const item of items.children) { - if (hasStyleOrScript === true) { - break; + traverse(root, (node) => { + if (hasStyleOrScript === true) { + return traverseBreak; + } + + // quit if