diff --git a/lib/dom-tools.js b/lib/dom-tools.js new file mode 100644 index 000000000..8af64095b --- /dev/null +++ b/lib/dom-tools.js @@ -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; diff --git a/lib/dom-walker.js b/lib/dom-walker.js new file mode 100644 index 000000000..528932b62 --- /dev/null +++ b/lib/dom-walker.js @@ -0,0 +1,9 @@ +'use strict'; + +var walk = require('tree-walk'); + +var domWalker = walk(function(node) { + return node.content; +}); + +module.exports = domWalker; diff --git a/lib/svgo/css-style-declaration.js b/lib/svgo/css-style-declaration.js index 5b2300b14..bbf02ff3a 100644 --- a/lib/svgo/css-style-declaration.js +++ b/lib/svgo/css-style-declaration.js @@ -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 }; /** diff --git a/package.json b/package.json index 175f697de..31d7af78c 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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" }, diff --git a/plugins/chainFilters.js b/plugins/chainFilters.js new file mode 100644 index 000000000..8d0cb46b6 --- /dev/null +++ b/plugins/chainFilters.js @@ -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 + */ +exports.fn = function(document) { + + // Collect elements and elements that use a element by ID + var filterIds = new Set(), + elementsUsingFilterById = []; + domWalker.preorder(document, function(node) { + + // 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 elements.'); + return; // skip if ID already added + } + filterIds.add(filterElemId); + + return; // done with 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 elements, skip this SVG. + } + + + // elements that use a 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 \'#' + element.filterId + '\', element skipped.'); + } + return filterExists; + }); + + if(elementsUsingExistingFilterById.length === 0) { + return document; // No existing elements are used, skip this SVG. + } + + + // Generate CSS class list + styles for the 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 + + + + + + + diff --git a/test/plugins/chainFilters.02.svg b/test/plugins/chainFilters.02.svg new file mode 100644 index 000000000..43376d5fa --- /dev/null +++ b/test/plugins/chainFilters.02.svg @@ -0,0 +1,22 @@ + + + + + + + + + +@@@ + + + + + + + + + + diff --git a/test/plugins/chainFilters.03.svg b/test/plugins/chainFilters.03.svg new file mode 100644 index 000000000..282e8e064 --- /dev/null +++ b/test/plugins/chainFilters.03.svg @@ -0,0 +1,25 @@ + + + + + + + + + + +@@@ + + + + + + + + + + diff --git a/test/plugins/chainFilters.04.svg b/test/plugins/chainFilters.04.svg new file mode 100644 index 000000000..368825d46 --- /dev/null +++ b/test/plugins/chainFilters.04.svg @@ -0,0 +1,24 @@ + + + + + + + + + + +@@@ + + + + + + + + + + + diff --git a/test/plugins/chainFilters.05.svg b/test/plugins/chainFilters.05.svg new file mode 100644 index 000000000..dedf05aa0 --- /dev/null +++ b/test/plugins/chainFilters.05.svg @@ -0,0 +1,22 @@ + + + + + + + + + +@@@ + + + + + + + + + + diff --git a/test/plugins/chainFilters.06.svg b/test/plugins/chainFilters.06.svg new file mode 100644 index 000000000..410ca6b41 --- /dev/null +++ b/test/plugins/chainFilters.06.svg @@ -0,0 +1,25 @@ + + + + + + + + + + +@@@ + + + + + + + + + + diff --git a/test/plugins/chainFilters.07.svg b/test/plugins/chainFilters.07.svg new file mode 100644 index 000000000..745be8e00 --- /dev/null +++ b/test/plugins/chainFilters.07.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + +@@@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/test/plugins/chainFilters.08.svg b/test/plugins/chainFilters.08.svg new file mode 100644 index 000000000..c82bdf0be --- /dev/null +++ b/test/plugins/chainFilters.08.svg @@ -0,0 +1,9 @@ + + + + +@@@ + + + +