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:
@ -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
53
lib/xast.js
Normal 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;
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user