Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add chainFilters plugin #733

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions lib/dom-tools.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use strict';

var CssRx = require('css-url-regex'),
rxId = /^#(.*)$/; // regular expression for matching an ID + extracing its name

// Checks if attribute is empty
var attrNotEmpty = function(attr) {
return (attr && attr.value && attr.value.length > 0);
};

// Escapes a string for being used as ID
var escapeIdentifierName = function(str) {
return str.replace(/[\. ]/g, '_');
};

// Matches an #ID value, captures the ID name
var matchId = function(urlVal) {
var idUrlMatches = urlVal.match(rxId);
if (idUrlMatches === null) {
return false;
}
return idUrlMatches[1];
};

// Matches an url(...) value, captures the URL
var matchUrl = function(val) {
var cssRx = new CssRx(); // workaround for /~https://github.com/cssstats/css-url-regex/issues/4
var urlMatches = cssRx.exec(val);
if (urlMatches === null) {
return false;
}
return urlMatches[1];
};

module.exports.rxId = rxId;
module.exports.attrNotEmpty = attrNotEmpty;
module.exports.escapeIdentifierName = escapeIdentifierName;
module.exports.matchId = matchId;
module.exports.matchUrl = matchUrl;
9 changes: 9 additions & 0 deletions lib/dom-walker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

var walk = require('tree-walk');

var domWalker = walk(function(node) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added traverse utility in lib/xast.js.

return node.content;
});

module.exports = domWalker;
3 changes: 2 additions & 1 deletion lib/svgo/css-style-declaration.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ CSSStyleDeclaration.prototype.item = function(index) {
var properties = this.getProperties();
this._handleParseError();

return Array.from(properties.keys())[index];
var name = Array.from(properties.keys())[index];
return name ? name : ''; // empty when no names
};

/**
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,12 @@
"jshint": "npm run lint"
},
"dependencies": {
"camelcase": "^4.1.0",
"coa": "~2.0.1",
"colors": "~1.1.2",
"css-select": "~1.3.0-rc0",
"css-select-base-adapter": "~0.1.0",
"css-url-regex": "^1.1.0",
"css-tree": "1.0.0-alpha25",
"css-url-regex": "^1.1.0",
"csso": "^3.5.0",
Expand All @@ -61,6 +63,7 @@
"object.values": "^1.0.4",
"sax": "~1.2.4",
"stable": "~0.1.6",
"tree-walk": "^0.4.0",
"unquote": "~1.1.1",
"util.promisify": "~1.0.0"
},
Expand Down
197 changes: 197 additions & 0 deletions plugins/chainFilters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
'use strict';

exports.type = 'full';

exports.active = false;

exports.params = {};

exports.description = 'chain filter elements using CSS filter(...)';


var csstree = require('css-tree'),
camelCase = require('camelcase'),
domWalker = require('../lib/dom-walker'),
domTools = require('../lib/dom-tools');


/**
* Chain filter elements using CSS filter(...) (Workaround for some browsers like Firefox).
*
* @param {Object} document document element
* @param {Object} opts plugin params
*
* @author strarsis <strarsis@gmail.com>
*/
exports.fn = function(document) {

// Collect <filter> elements and elements that use a <filter> element by ID
var filterIds = new Set(),
elementsUsingFilterById = [];
domWalker.preorder(document, function(node) {

// <filter> elements
if (node.elem === 'filter') {
if(!node.hasAttr('id')) {
return; // skip if no ID attribute
}
var filterElemId = node.attr('id').value;

if (filterIds.has(filterElemId)) {
console.warn('Warning: \'#' + filterElemId + '\' is used multiple times for <filter> elements.');
return; // skip if ID already added
}
filterIds.add(filterElemId);

return; // done with <filter> element
}


// elements that use a filter (filter attribute) or
// a filter style
if (!node.hasAttr('filter') &&
!(node.style && node.style.getPropertyValue('filter') !== null)) {
return; // skip if no filter attribute
}

var useFilterVal;
if(node.style.getPropertyValue('filter') !== null) {
// style filter
useFilterVal = node.style.getPropertyValue('filter');
} else {
// attribute filter
useFilterVal = node.attr('filter').value;
}


if (useFilterVal.length === 0) {
return; // skip if empty filter attribute
}

var useFilterUrl = domTools.matchUrl(useFilterVal);
if (!useFilterUrl) {
return; // skip if no url(...) used
}

var useFilterId = domTools.matchId(useFilterUrl);
if (!useFilterId) {
return; // skip if no #id in url(...) used
}

elementsUsingFilterById.push({
filterId: useFilterId,
node: node
});
});
if(filterIds.length === 0) {
return document; // No <filter> elements, skip this SVG.
}


// elements that use a <filter> element that actually exists
var elementsUsingExistingFilterById = elementsUsingFilterById.filter(function(element) {
var filterExists = filterIds.has(element.filterId);
if (!filterExists) {
console.warn('Warning: Element uses non-existing <filter> \'#' + element.filterId + '\', element skipped.');
}
return filterExists;
});

if(elementsUsingExistingFilterById.length === 0) {
return document; // No existing <filter> elements are used, skip this SVG.
}


// Generate CSS class list + styles for the <filter> elements
var usedFilterIds = new Set(
elementsUsingExistingFilterById.map(function(element) {
return element.filterId;
})
);

var filterClasses = new Map(),
filterClassesStyles = csstree.fromPlainObject({type:'StyleSheet', children: []});
for (var filterId of usedFilterIds) {
var filterClassName = camelCase('filter ' + filterId);
filterClasses.set(filterId, filterClassName);

var filterClassRuleObj = {
type: 'Rule',
prelude: {
type: 'SelectorList',
children: [
{
type: 'Selector',
children: [
{
type: 'ClassSelector',
name: filterClassName
}
]
}
]
},
block: {
type: 'Block',
children: [
{
type: 'Declaration',
important: false,
property: 'filter',
value: {
type: 'Value',
children: [
{
type: 'String',
value: '"url(' + '#' + filterId + ')"'
}
]
}
}
]
}
};
filterClassesStyles.children.appendData(csstree.fromPlainObject(filterClassRuleObj));
}


if(!filterClassesStyles.children.isEmpty()) {
// Add new style element with these filter classes styles
var svgElem = document.querySelector('svg');

// New <style>
var styleFilterClasses = new document.constructor({
elem: 'style',
prefix: '',
local: 'style',
content: [] // has to be added in extra step
}, svgElem);

// New text content for <style>
var styleFilterClassesText = new document.constructor({
text: csstree.translate(filterClassesStyles)
}, styleFilterClasses);
// Add text content to <style>
styleFilterClasses.spliceContent(0, 0, styleFilterClassesText);

// Add new <style> to <svg>
svgElem.spliceContent(0, 0, styleFilterClasses);
}


for (var element of elementsUsingExistingFilterById) {
// Assign filter-using classes to corresponding filter-using elements
element.node.class.add(filterClasses.get(element.filterId));

// Remove the then redundant filter attribute + styles
element.node.removeAttr('filter');
element.node.style.removeProperty('filter');
if(element.node.style.item(0) === '') {
// clean up now empty style attributes
element.node.removeAttr('style');
}
}


return document;
};
22 changes: 22 additions & 0 deletions test/plugins/chainFilters.01.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions test/plugins/chainFilters.02.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions test/plugins/chainFilters.03.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions test/plugins/chainFilters.04.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions test/plugins/chainFilters.05.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading