1
0
mirror of https://github.com/svg/svgo.git synced 2025-08-01 18:46:52 +03:00

Migrate ast traversing into xast module (#1434)

Replaced JSAPI methods with new utilities

- querySelectorAll(node, selector)
- querySelector(node, selector)
- matches(node, selector)
- closestByName(node, elementName)
- traverse(node, fn)

New traverse replaced many in-place implementations.
This commit is contained in:
Bogdan Chadkin
2021-03-19 11:06:41 +03:00
committed by GitHub
parent 6f2f62c5ee
commit 4cacd9e676
10 changed files with 294 additions and 303 deletions

View File

@ -2,6 +2,7 @@
const { expect } = require('chai'); const { expect } = require('chai');
const { computeStyle } = require('./style.js'); const { computeStyle } = require('./style.js');
const { querySelector } = require('./xast.js');
const svg2js = require('./svgo/svg2js.js'); const svg2js = require('./svgo/svg2js.js');
describe('computeStyle', () => { describe('computeStyle', () => {
@ -30,24 +31,24 @@ describe('computeStyle', () => {
</style> </style>
</svg> </svg>
`); `);
expect(computeStyle(root.querySelector('#class'))).to.deep.equal({ expect(computeStyle(querySelector(root, '#class'))).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'red' }, 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' }, fill: { type: 'static', inherited: false, value: 'green' },
stroke: { type: 'static', inherited: false, value: 'black' }, 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' }, 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' }, 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' }, fill: { type: 'static', inherited: true, value: 'yellow' },
}); });
expect( expect(
computeStyle(root.querySelector('#nested-inheritance')) computeStyle(querySelector(root, '#nested-inheritance'))
).to.deep.equal({ ).to.deep.equal({
fill: { type: 'static', inherited: true, value: 'blue' }, fill: { type: 'static', inherited: true, value: 'blue' },
}); });
@ -69,23 +70,23 @@ describe('computeStyle', () => {
</g> </g>
</svg> </svg>
`); `);
expect(computeStyle(root.querySelector('#complex-selector'))).to.deep.equal(
{
fill: { type: 'static', inherited: false, value: 'red' },
}
);
expect( 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({ ).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'orange' }, fill: { type: 'static', inherited: false, value: 'orange' },
}); });
expect( expect(
computeStyle(root.querySelector('#style-rule-over-attribute')) computeStyle(querySelector(root, '#style-rule-over-attribute'))
).to.deep.equal({ ).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'blue' }, fill: { type: 'static', inherited: false, value: 'blue' },
}); });
expect( expect(
computeStyle(root.querySelector('#inline-style-over-style-rule')) computeStyle(querySelector(root, '#inline-style-over-style-rule'))
).to.deep.equal({ ).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'purple' }, fill: { type: 'static', inherited: false, value: 'purple' },
}); });
@ -103,18 +104,18 @@ describe('computeStyle', () => {
<rect id="inline-style-over-style-rule" style="fill: purple !important;" class="b" /> <rect id="inline-style-over-style-rule" style="fill: purple !important;" class="b" />
</svg> </svg>
`); `);
expect(computeStyle(root.querySelector('#complex-selector'))).to.deep.equal(
{
fill: { type: 'static', inherited: false, value: 'green' },
}
);
expect( expect(
computeStyle(root.querySelector('#style-rule-over-inline-style')) computeStyle(querySelector(root, '#complex-selector'))
).to.deep.equal({ ).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'green' }, fill: { type: 'static', inherited: false, value: 'green' },
}); });
expect( 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({ ).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'purple' }, fill: { type: 'static', inherited: false, value: 'purple' },
}); });
@ -140,21 +141,21 @@ describe('computeStyle', () => {
<rect id="static" class="c" style="fill: black" /> <rect id="static" class="c" style="fill: black" />
</svg> </svg>
`); `);
expect(computeStyle(root.querySelector('#media-query'))).to.deep.equal({ expect(computeStyle(querySelector(root, '#media-query'))).to.deep.equal({
fill: { type: 'dynamic', inherited: false }, 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 }, 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 }, fill: { type: 'dynamic', inherited: true },
}); });
expect( expect(
computeStyle(root.querySelector('#inherited-overriden')) computeStyle(querySelector(root, '#inherited-overriden'))
).to.deep.equal({ ).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'blue' }, 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' }, fill: { type: 'static', inherited: false, value: 'black' },
}); });
}); });
@ -176,13 +177,13 @@ describe('computeStyle', () => {
<rect id="static" class="c" /> <rect id="static" class="c" />
</svg> </svg>
`); `);
expect(computeStyle(root.querySelector('#media-query'))).to.deep.equal({ expect(computeStyle(querySelector(root, '#media-query'))).to.deep.equal({
fill: { type: 'dynamic', inherited: false }, 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 }, 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' }, fill: { type: 'static', inherited: false, value: 'blue' },
}); });
}); });
@ -204,13 +205,15 @@ describe('computeStyle', () => {
<rect id="invalid-type" class="c" /> <rect id="invalid-type" class="c" />
</svg> </svg>
`); `);
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' }, 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' }, 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', () => { it('ignores keyframes atrule', () => {
@ -235,7 +238,7 @@ describe('computeStyle', () => {
<rect id="element" class="a" /> <rect id="element" class="a" />
</svg> </svg>
`); `);
expect(computeStyle(root.querySelector('#element'))).to.deep.equal({ expect(computeStyle(querySelector(root, '#element'))).to.deep.equal({
animation: { animation: {
type: 'static', type: 'static',
inherited: false, inherited: false,

53
lib/xast.js Normal file
View File

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

View File

@ -1,5 +1,7 @@
'use strict'; 'use strict';
const { traverse } = require('../lib/xast.js');
exports.type = 'full'; exports.type = 'full';
exports.active = true; exports.active = true;
@ -17,72 +19,55 @@ exports.description =
* ⬇ * ⬇
* <svg width="100" height="50"> * <svg width="100" height="50">
* *
* @param {Object} item current iteration item * @param {Object} root current iteration item
* @return {Boolean} if false, item will be filtered out * @return {Boolean} if false, item will be filtered out
* *
* @author Kir Belevich * @author Kir Belevich
*/ */
exports.fn = function (data) { exports.fn = function (root) {
var regEnableBackground = /^new\s0\s0\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)$/, const regEnableBackground = /^new\s0\s0\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)\s([-+]?\d*\.?\d+([eE][-+]?\d+)?)$/;
hasFilter = false, let hasFilter = false;
elems = ['svg', 'mask', 'pattern']; const elems = ['svg', 'mask', 'pattern'];
function checkEnableBackground(item) { traverse(root, (node) => {
if ( if (node.type === 'element') {
item.isElem(elems) && if (
item.attributes['enable-background'] != null && elems.includes(node.name) &&
item.attributes.width != null && node.attributes['enable-background'] != null &&
item.attributes.height != null node.attributes.width != null &&
) { node.attributes.height != null
var match = item.attributes['enable-background'].match( ) {
regEnableBackground const match = node.attributes['enable-background'].match(
); regEnableBackground
);
if (match) { if (match) {
if ( if (
item.attributes.width === match[1] && node.attributes.width === match[1] &&
item.attributes.height === match[3] node.attributes.height === match[3]
) { ) {
if (item.isElem('svg')) { if (node.name === 'svg') {
delete item.attributes['enable-background']; delete node.attributes['enable-background'];
} else { } else {
item.attributes['enable-background'] = 'new'; node.attributes['enable-background'] = 'new';
}
} }
} }
} }
} if (node.name === 'filter') {
} hasFilter = true;
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);
} }
});
return items;
}
var firstStep = monkeys(data, function (item) {
checkEnableBackground(item);
if (!hasFilter) {
checkForFilter(item);
} }
}); });
return hasFilter if (hasFilter === false) {
? firstStep traverse(root, (node) => {
: monkeys(firstStep, (item) => { if (node.type === 'element') {
if (item.type === 'element') { //we don't need 'enable-background' if we have no filters
//we don't need 'enable-background' if we have no filters delete node.attributes['enable-background'];
delete item.attributes['enable-background']; }
} });
}); }
return root;
}; };

View File

@ -1,5 +1,6 @@
'use strict'; 'use strict';
const { traverse, traverseBreak } = require('../lib/xast.js');
const { parseName } = require('../lib/svgo/tools.js'); const { parseName } = require('../lib/svgo/tools.js');
exports.type = 'full'; exports.type = 'full';
@ -87,7 +88,7 @@ var referencesProps = new Set(require('./_collections').referencesProps),
* *
* @author Kir Belevich * @author Kir Belevich
*/ */
exports.fn = function (data, params) { exports.fn = function (root, params) {
var currentID, var currentID,
currentIDstring, currentIDstring,
IDs = new Map(), IDs = new Map(),
@ -110,88 +111,73 @@ exports.fn = function (data, params) {
idValuePrefix = '#', idValuePrefix = '#',
idValuePostfix = '.'; idValuePostfix = '.';
/** traverse(root, (node) => {
* Bananas! if (hasStyleOrScript === true) {
* return traverseBreak;
* @param {Array} items input items }
* @return {Array} output items
*/ // quit if <style> or <script> present ('force' param prevents quitting)
function monkeys(items) { if (!params.force) {
for (const item of items.children) { if (node.isElem(styleOrScript) && node.children.length !== 0) {
if (hasStyleOrScript === true) { hasStyleOrScript = true;
break; return;
} }
// quit if <style> or <script> present ('force' param prevents quitting) // Don't remove IDs if the whole SVG consists only of defs.
if (!params.force) { if (node.type === 'element' && node.name === 'svg') {
if (item.isElem(styleOrScript) && item.children.length !== 0) { let hasDefsOnly = true;
hasStyleOrScript = true; for (const child of node.children) {
continue; if (child.type !== 'element' || child.name !== 'defs') {
} hasDefsOnly = false;
// Don't remove IDs if the whole SVG consists only of defs.
if (item.isElem('svg')) {
var hasDefsOnly = true;
for (var j = 0; j < item.children.length; j++) {
if (!item.children[j].isElem('defs')) {
hasDefsOnly = false;
break;
}
}
if (hasDefsOnly) {
break; break;
} }
} }
if (hasDefsOnly) {
return traverseBreak;
}
} }
// …and don't remove any ID if yes }
if (item.type === 'element') {
for (const [name, value] of Object.entries(item.attributes)) {
let key;
let match;
// save IDs // …and don't remove any ID if yes
if (name === 'id') { if (node.type === 'element') {
key = value; for (const [name, value] of Object.entries(node.attributes)) {
if (IDs.has(key)) { let key;
delete item.attributes.id; // remove repeated id let match;
} else {
IDs.set(key, item); // save IDs
} if (name === 'id') {
key = value;
if (IDs.has(key)) {
delete node.attributes.id; // remove repeated id
} else { } else {
// save references IDs.set(key, node);
const { local } = parseName(name); }
if ( } else {
referencesProps.has(name) && // save references
(match = value.match(regReferencesUrl)) const { local } = parseName(name);
) { if (
key = match[2]; // url() reference referencesProps.has(name) &&
} else if ( (match = value.match(regReferencesUrl))
(local === 'href' && (match = value.match(regReferencesHref))) || ) {
(name === 'begin' && (match = value.match(regReferencesBegin))) key = match[2]; // url() reference
) { } else if (
key = match[1]; // href reference (local === 'href' && (match = value.match(regReferencesHref))) ||
} (name === 'begin' && (match = value.match(regReferencesBegin)))
if (key) { ) {
const refs = referencesIDs.get(key) || []; key = match[1]; // href reference
refs.push({ element: item, name, value }); }
referencesIDs.set(key, refs); if (key) {
} const refs = referencesIDs.get(key) || [];
refs.push({ element: node, name, value });
referencesIDs.set(key, refs);
} }
} }
} }
// go deeper
if (item.type === 'root' || item.type === 'element') {
monkeys(item);
}
} }
return items; });
}
data = monkeys(data);
if (hasStyleOrScript) { if (hasStyleOrScript) {
return data; return root;
} }
const idPreserved = (id) => const idPreserved = (id) =>
@ -234,7 +220,7 @@ exports.fn = function (data, params) {
} }
} }
} }
return data; return root;
}; };
/** /**

View File

@ -1,5 +1,9 @@
'use strict'; 'use strict';
const csstree = require('css-tree');
const { querySelectorAll, closestByName } = require('../lib/xast.js');
const cssTools = require('../lib/css-tools');
exports.type = 'full'; exports.type = 'full';
exports.active = true; exports.active = true;
@ -13,9 +17,6 @@ exports.params = {
exports.description = 'inline styles (additional options)'; exports.description = 'inline styles (additional options)';
var csstree = require('css-tree'),
cssTools = require('../lib/css-tools');
/** /**
* Moves + merges styles from style elements to element styles * Moves + merges styles from style elements to element styles
* *
@ -35,18 +36,18 @@ var csstree = require('css-tree'),
* what pseudo-classes/-elements to be used * what pseudo-classes/-elements to be used
* empty string element for all non-pseudo-classes and/or -elements * empty string element for all non-pseudo-classes and/or -elements
* *
* @param {Object} document document element * @param {Object} root document element
* @param {Object} opts plugin params * @param {Object} opts plugin params
* *
* @author strarsis <strarsis@gmail.com> * @author strarsis <strarsis@gmail.com>
*/ */
exports.fn = function (document, opts) { exports.fn = function (root, opts) {
// collect <style/>s // collect <style/>s
var styleEls = document.querySelectorAll('style'); var styleEls = querySelectorAll(root, 'style');
//no <styles/>s, nothing to do //no <styles/>s, nothing to do
if (styleEls === null) { if (styleEls.length === 0) {
return document; return root;
} }
var styles = [], var styles = [],
@ -62,7 +63,10 @@ exports.fn = function (document, opts) {
continue; continue;
} }
// skip empty <style/>s or <foreignObject> content. // skip empty <style/>s or <foreignObject> content.
if (styleEl.children.length === 0 || styleEl.closestElem('foreignObject')) { if (
styleEl.children.length === 0 ||
closestByName(styleEl, 'foreignObject')
) {
continue; continue;
} }
@ -108,13 +112,13 @@ exports.fn = function (document, opts) {
selectedEls = null; selectedEls = null;
try { try {
selectedEls = document.querySelectorAll(selectorStr); selectedEls = querySelectorAll(root, selectorStr);
} catch (selectError) { } catch (selectError) {
// console.warn('Warning: Syntax error when trying to select \n\n' + selectorStr + '\n\n, skipped. Error details: ' + selectError); // console.warn('Warning: Syntax error when trying to select \n\n' + selectorStr + '\n\n, skipped. Error details: ' + selectError);
continue; continue;
} }
if (selectedEls === null) { if (selectedEls.length === 0) {
// nothing selected // nothing selected
continue; continue;
} }
@ -181,7 +185,7 @@ exports.fn = function (document, opts) {
} }
if (!opts.removeMatchedSelectors) { if (!opts.removeMatchedSelectors) {
return document; // no further processing required return root; // no further processing required
} }
// clean up matched class + ID attribute values // clean up matched class + ID attribute values
@ -269,5 +273,5 @@ exports.fn = function (document, opts) {
cssTools.setCssStr(style.styleEl, csstree.generate(style.cssAst)); cssTools.setCssStr(style.styleEl, csstree.generate(style.cssAst));
} }
return document; return root;
}; };

View File

@ -1,5 +1,8 @@
'use strict'; 'use strict';
const csso = require('csso');
const { traverse } = require('../lib/xast.js');
exports.type = 'full'; exports.type = 'full';
exports.active = true; exports.active = true;
@ -19,8 +22,6 @@ exports.params = {
}, },
}; };
var csso = require('csso');
/** /**
* Minifies styles (<style> element + style attribute) using CSSO * Minifies styles (<style> element + style attribute) using CSSO
* *
@ -73,26 +74,17 @@ function cloneObject(obj) {
} }
function findStyleElems(ast) { function findStyleElems(ast) {
function walk(items, styles) { const nodesWithStyles = [];
for (var i = 0; i < items.children.length; i++) { traverse(ast, (node) => {
var item = items.children[i]; if (node.type === 'element') {
if (node.name === 'style' && node.children.length !== 0) {
// go deeper nodesWithStyles.push(node);
if (item.children) { } else if (node.attributes.style != null) {
walk(item, styles); nodesWithStyles.push(node);
}
if (item.isElem('style') && item.children.length !== 0) {
styles.push(item);
} else if (item.type === 'element' && item.attributes.style != null) {
styles.push(item);
} }
} }
});
return styles; return nodesWithStyles;
}
return walk(ast, []);
} }
function shouldFilter(options, name) { function shouldFilter(options, name) {
@ -108,49 +100,40 @@ function shouldFilter(options, name) {
} }
function collectUsageData(ast, options) { function collectUsageData(ast, options) {
function walk(items, usageData) { let safe = true;
for (const item of items.children) { const usageData = {};
// go deeper let hasData = false;
if (item.type === 'root' || item.type === 'element') { const rawData = {
walk(item, usageData);
}
if (item.type === 'element') {
if (item.name === 'script') {
safe = false;
}
usageData.tags[item.name] = true;
if (item.attributes.id != null) {
usageData.ids[item.attributes.id] = true;
}
if (item.attributes.class != null) {
item.attributes.class
.replace(/^\s+|\s+$/g, '')
.split(/\s+/)
.forEach(function (className) {
usageData.classes[className] = true;
});
}
if (Object.keys(item.attributes).some((name) => /^on/i.test(name))) {
safe = false;
}
}
}
return usageData;
}
var safe = true;
var usageData = {};
var hasData = false;
var rawData = walk(ast, {
ids: Object.create(null), ids: Object.create(null),
classes: Object.create(null), classes: Object.create(null),
tags: Object.create(null), tags: Object.create(null),
};
traverse(ast, (node) => {
if (node.type === 'element') {
if (node.name === 'script') {
safe = false;
}
rawData.tags[node.name] = true;
if (node.attributes.id != null) {
rawData.ids[node.attributes.id] = true;
}
if (node.attributes.class != null) {
node.attributes.class
.replace(/^\s+|\s+$/g, '')
.split(/\s+/)
.forEach((className) => {
rawData.classes[className] = true;
});
}
if (Object.keys(node.attributes).some((name) => /^on/i.test(name))) {
safe = false;
}
}
}); });
if (!safe && options.usage && options.usage.force) { if (!safe && options.usage && options.usage.force) {

View File

@ -1,5 +1,6 @@
'use strict'; 'use strict';
const { querySelector, closestByName } = require('../lib/xast.js');
const { computeStyle } = require('../lib/style.js'); const { computeStyle } = require('../lib/style.js');
const { parsePathData } = require('../lib/path.js'); const { parsePathData } = require('../lib/path.js');
@ -58,7 +59,7 @@ exports.fn = function (item, params) {
computedStyle.visibility.type === 'static' && computedStyle.visibility.type === 'static' &&
computedStyle.visibility.value === 'hidden' && computedStyle.visibility.value === 'hidden' &&
// keep if any descendant enables visibility // keep if any descendant enables visibility
item.querySelector('[visibility=visible]') == null querySelector(item, '[visibility=visible]') == null
) { ) {
return false; return false;
} }
@ -88,7 +89,7 @@ exports.fn = function (item, params) {
computedStyle.opacity.type === 'static' && computedStyle.opacity.type === 'static' &&
computedStyle.opacity.value === '0' && computedStyle.opacity.value === '0' &&
// transparent element inside clipPath still affect clipped elements // transparent element inside clipPath still affect clipped elements
item.closestElem('clipPath') == null closestByName(item, 'clipPath') == null
) { ) {
return false; return false;
} }

View File

@ -1,5 +1,6 @@
'use strict'; 'use strict';
const { traverse } = require('../lib/xast.js');
const { parseName } = require('../lib/svgo/tools.js'); const { parseName } = require('../lib/svgo/tools.js');
exports.type = 'full'; exports.type = 'full';
@ -16,7 +17,7 @@ exports.description = 'removes unused namespaces declaration';
* *
* @author Kir Belevich * @author Kir Belevich
*/ */
exports.fn = function (data) { exports.fn = function (root) {
let svgElem; let svgElem;
const xmlnsCollection = []; const xmlnsCollection = [];
@ -34,64 +35,46 @@ exports.fn = function (data) {
} }
} }
/** traverse(root, (node) => {
* Bananas! if (node.type === 'element') {
* if (node.name === 'svg') {
* @param {Array} items input items for (const name of Object.keys(node.attributes)) {
* const { prefix, local } = parseName(name);
* @return {Array} output items // collect namespaces
*/ if (prefix === 'xmlns' && local) {
function monkeys(items) { xmlnsCollection.push(local);
for (const item of items.children) {
if (item.type === 'element') {
if (item.name === 'svg') {
for (const name of Object.keys(item.attributes)) {
const { prefix, local } = parseName(name);
// collect namespaces
if (prefix === 'xmlns' && local) {
xmlnsCollection.push(local);
}
}
// if svg element has ns-attr
if (xmlnsCollection.length) {
// save svg element
svgElem = item;
} }
} }
// if svg element has ns-attr
if (xmlnsCollection.length) { if (xmlnsCollection.length) {
const { prefix } = parseName(item.name); // save svg element
// check item for the ns-attrs svgElem = node;
if (prefix) { }
removeNSfromCollection(prefix); }
}
// check each attr for the ns-attrs if (xmlnsCollection.length) {
for (const name of Object.keys(item.attributes)) { const { prefix } = parseName(node.name);
const { prefix } = parseName(name); // check node for the ns-attrs
removeNSfromCollection(prefix); if (prefix) {
} removeNSfromCollection(prefix);
} }
// if nothing is found - go deeper // check each attr for the ns-attrs
if (xmlnsCollection.length && item.children) { for (const name of Object.keys(node.attributes)) {
monkeys(item); const { prefix } = parseName(name);
removeNSfromCollection(prefix);
} }
} }
} }
});
return items;
}
data = monkeys(data);
// remove svg element ns-attributes if they are not used even once // remove svg element ns-attributes if they are not used even once
if (xmlnsCollection.length) { if (xmlnsCollection.length) {
xmlnsCollection.forEach(function (name) { for (const name of xmlnsCollection) {
delete svgElem.attributes['xmlns:' + name]; delete svgElem.attributes['xmlns:' + name];
}); }
} }
return data; return root;
}; };

View File

@ -1,5 +1,7 @@
'use strict'; 'use strict';
const { closestByName } = require('../lib/xast.js');
exports.type = 'perItem'; exports.type = 'perItem';
exports.active = true; exports.active = true;
@ -32,7 +34,7 @@ exports.fn = function (item) {
item.attributes.height != null item.attributes.height != null
) { ) {
// TODO remove width/height for such case instead // TODO remove width/height for such case instead
if (item.name === 'svg' && item.closestElem('svg')) { if (item.name === 'svg' && closestByName(item.parentNode, 'svg')) {
return; return;
} }

View File

@ -1,6 +1,7 @@
'use strict'; 'use strict';
var JSAPI = require('../lib/svgo/jsAPI'); const { traverse } = require('../lib/xast.js');
const JSAPI = require('../lib/svgo/jsAPI');
exports.type = 'full'; exports.type = 'full';
@ -17,25 +18,25 @@ exports.description =
* *
* @author Jacob Howcroft * @author Jacob Howcroft
*/ */
exports.fn = function (data) { exports.fn = function (root) {
const seen = new Map(); const seen = new Map();
let count = 0; let count = 0;
const defs = []; const defs = [];
traverse(data, (item) => { traverse(root, (node) => {
if ( if (
item.type !== 'element' || node.type !== 'element' ||
item.name !== 'path' || node.name !== 'path' ||
item.attributes.d == null node.attributes.d == null
) { ) {
return; return;
} }
const d = item.attributes.d; const d = node.attributes.d;
const fill = item.attributes.fill || ''; const fill = node.attributes.fill || '';
const stroke = item.attributes.stroke || ''; const stroke = node.attributes.stroke || '';
const key = d + ';s:' + stroke + ';f:' + fill; const key = d + ';s:' + stroke + ';f:' + fill;
const hasSeen = seen.get(key); const hasSeen = seen.get(key);
if (!hasSeen) { if (!hasSeen) {
seen.set(key, { elem: item, reused: false }); seen.set(key, { elem: node, reused: false });
return; return;
} }
if (!hasSeen.reused) { if (!hasSeen.reused) {
@ -45,7 +46,7 @@ exports.fn = function (data) {
} }
defs.push(hasSeen.elem); defs.push(hasSeen.elem);
} }
convertToUse(item, hasSeen.elem.attributes.id); convertToUse(node, hasSeen.elem.attributes.id);
}); });
if (defs.length > 0) { if (defs.length > 0) {
const defsTag = new JSAPI( const defsTag = new JSAPI(
@ -55,9 +56,9 @@ exports.fn = function (data) {
attributes: {}, attributes: {},
children: [], children: [],
}, },
data root
); );
data.children[0].spliceContent(0, 0, defsTag); root.children[0].spliceContent(0, 0, defsTag);
for (let def of defs) { for (let def of defs) {
// Remove class and style before copying to avoid circular refs in // Remove class and style before copying to avoid circular refs in
// JSON.stringify. This is fine because we don't actually want class or // JSON.stringify. This is fine because we don't actually want class or
@ -76,7 +77,7 @@ exports.fn = function (data) {
delete def.attributes.id; delete def.attributes.id;
} }
} }
return data; return root;
}; };
/** */ /** */
@ -89,13 +90,3 @@ function convertToUse(item, href) {
delete item.pathJS; delete item.pathJS;
return item; return item;
} }
/** */
function traverse(parent, callback) {
if (parent.type === 'root' || parent.type === 'element') {
for (let child of parent.children) {
callback(child);
traverse(child, callback);
}
}
}