diff --git a/.svgo.yml b/.svgo.yml index a504b9666..a1889e6c7 100644 --- a/.svgo.yml +++ b/.svgo.yml @@ -23,6 +23,7 @@ plugins: - removeXMLNS - removeEditorsNSData - cleanupAttrs + - inlineStyles - minifyStyles - convertStyleToAttrs - cleanupIDs diff --git a/docs/how-it-works/en.md b/docs/how-it-works/en.md index 469367c7d..47b19d927 100644 --- a/docs/how-it-works/en.md +++ b/docs/how-it-works/en.md @@ -199,6 +199,114 @@ And of course, writing plugins would not have been so cool without some sugar AP * @param {Object} [context] callback context * @return {Boolean} false if there are no any attributes + +##### querySelectorAll(selectors) + * Evaluate a string of CSS selectors against the element and returns matched elements + * @param {String} selectors CSS selector(s) string + * @return {Array} null if no elements matched + +##### querySelector(selectors) + * Evaluate a string of CSS selectors against the element and returns only the first matched element + * @param {String} selectors CSS selector(s) string + * @return {Array} null if no element matched + +##### matches(selector) + * Test if a selector matches a given element + * @param {String} selector CSS selector string + * @return {Boolean} true if element would be selected by selector string, false if it does not + + +##### style.getCssText() + * Get the textual representation of the declaration block (equivalent to .cssText attribute). + * @return {String} Textual representation of the declaration block (empty string for no properties) + +##### style.getPropertyPriority(propertyName) + * Return the optional priority, "important". + * @param {String} propertyName representing the property name to be checked. + * @return {String} priority that represents the priority (e.g. "important") if one exists. If none exists, returns the empty string. + +##### style.getPropertyValue(propertyName) + * Return the property value given a property name. + * @param {String} propertyName representing the property name to be checked. + * @return {String} value containing the value of the property. If not set, returns the empty string. + +##### style.item(index) + * Return a property name. + * @param {Number} index of the node to be fetched. The index is zero-based. + * @return {String} propertyName that is the name of the CSS property at the specified index. + +##### style.getProperties() + * Return all properties of the node. + * @return {Map} properties that is a Map with propertyName as key and property (propertyValue + propertyPriority) as value. + +##### style.removeProperty(propertyName) + * Remove a property from the CSS declaration block. + * @param {String} propertyName representing the property name to be removed. + * @return {String} oldValue equal to the value of the CSS property before it was removed. + +##### style.setProperty(propertyName, value, priority) + * Modify an existing CSS property or creates a new CSS property in the declaration block. + * @param {String} propertyName representing the CSS property name to be modified. + * @param {String} [value] containing the new property value. If not specified, treated as the empty string. value must not contain "!important" -- that should be set using the priority parameter. + * @param {String} [priority] allowing the "important" CSS priority to be set. If not specified, treated as the empty string. + * @return {undefined} + + +##### css-tools.flattenToSelectors(cssAst) + * Flatten a CSS AST to a selectors list. + * @param {Object} CSS AST to flatten + * @return {Array} selectors + +##### css-tools.filterByMqs(selectors, useMqs) + * Filter selectors by Media Query. + * @param {Array} Selectors to filter + * @param {Array} Strings of media queries that should pass (<name> <expression>) + * @return {Array} Filtered selectors that match the passed media queries + +##### css-tools.filterByPseudos(selectors, useMqs) + * Filter selectors by the pseudo-elements and/or -classes they contain. + * @param {Array} Selectors to filter + * @param {Array} Strings of single or sequence of pseudo-elements and/or -classes that should pass + * @return {Array} Filtered selectors that match the passed pseudo-elements and/or -classes + +##### css-tools.cleanPseudos(selectors) + * Remove pseudo-elements and/or -classes from the selectors for proper matching. + * @param {Array} Selectors to clean + * @return {Array} Selectors without pseudo-elements and/or -classes + +##### css-tools.compareSpecificity(aSpecificity, bSpecificity) + * Compare two selector specificities. + * @param {Array} Specificity of selector A + * @param {Array} Specificity of selector B + * @return {Number} Score of selector specificity A compared to selector specificity B + +##### css-tools.compareSimpleSelectorNode(aSimpleSelectorNode, bSimpleSelectorNode) + * Compare two simple selectors. + * @param {Object} Simple selector A + * @param {Object} Simple selector B + * @return {Number} Score of selector A compared to selector B + +##### css-tools.sortSelectors(selectors) + * Sort selectors stably by their specificity. + * @param {Array} Selectors to be sorted + * @return {Array} Stable sorted selectors + +##### css-tools.csstreeToStyleDeclaration(declaration) + * Convert a css-tree AST style declaration to CSSStyleDeclaration property. + * @param {Object} css-tree style declaration + * @return {Object} CSSStyleDeclaration property + +##### css-tools.getCssStr(elem) + * Gets the CSS string of a style element + * @param {Object} element style element + * @return {String|Array} CSS string or empty array if no styles are set + +##### css-tools.csstreeToStyleDeclaration(elem, css) + * @param {Object} element style element + * @param {String} CSS string to be set + * @return {Object} reference to field with CSS + + #### 3.3 tests There is nothing easier than testing your plugin: diff --git a/lib/css-tools.js b/lib/css-tools.js new file mode 100644 index 000000000..e054f6bb2 --- /dev/null +++ b/lib/css-tools.js @@ -0,0 +1,222 @@ +'use strict'; + +var csstree = require('css-tree'), + List = csstree.List, + stable = require('stable'), + specificity = require('csso/lib/restructure/prepare/specificity'); + + +/** + * Flatten a CSS AST to a selectors list. + * + * @param {Object} cssAst css-tree AST to flatten + * @return {Array} selectors + */ +function flattenToSelectors(cssAst) { + var selectors = []; + + csstree.walkRules(cssAst, function(node) { + if (node.type !== 'Rule') { + return; + } + + var atrule = this.atrule; + var rule = node; + + node.selector.children.each(function(selectorNode, selectorItem) { + var selector = { + item: selectorItem, + atrule: atrule, + rule: rule, + pseudos: [] + }; + + selectorNode.children.each(function(selectorChildNode, selectorChildItem, selectorChildList) { + if (selectorChildNode.type === 'PseudoClassSelector' || + selectorChildNode.type === 'PseudoElementSelector') { + selector.pseudos.push({ + item: selectorChildItem, + list: selectorChildList + }); + } + }); + + selectors.push(selector); + }); + }); + + return selectors; +} + +/** + * Filter selectors by Media Query. + * + * @param {Array} selectors to filter + * @param {Array} useMqs Array with strings of media queries that should pass (<name> <expression>) + * @return {Array} Filtered selectors that match the passed media queries + */ +function filterByMqs(selectors, useMqs) { + return selectors.filter(function(selector) { + if (selector.atrule === null) { + return ~useMqs.indexOf(''); + } + + var mqName = selector.atrule.name; + var mqStr = mqName; + if (selector.atrule.expression && + selector.atrule.expression.children.first().type === 'MediaQueryList') { + var mqExpr = csstree.translate(selector.atrule.expression); + mqStr = [mqName, mqExpr].join(' '); + } + + return ~useMqs.indexOf(mqStr); + }); +} + +/** + * Filter selectors by the pseudo-elements and/or -classes they contain. + * + * @param {Array} selectors to filter + * @param {Array} usePseudos Array with strings of single or sequence of pseudo-elements and/or -classes that should pass + * @return {Array} Filtered selectors that match the passed pseudo-elements and/or -classes + */ +function filterByPseudos(selectors, usePseudos) { + return selectors.filter(function(selector) { + var pseudoSelectorsStr = csstree.translate({ + type: 'Selector', + children: new List().fromArray(selector.pseudos.map(function(pseudo) { + return pseudo.item.data; + })) + }); + return ~usePseudos.indexOf(pseudoSelectorsStr); + }); +} + +/** + * Remove pseudo-elements and/or -classes from the selectors for proper matching. + * + * @param {Array} selectors to clean + * @return {Array} Selectors without pseudo-elements and/or -classes + */ +function cleanPseudos(selectors) { + selectors.forEach(function(selector) { + selector.pseudos.forEach(function(pseudo) { + pseudo.list.remove(pseudo.item); + }); + }); +} + + +/** + * Compares two selector specificities. + * extracted from https://github.com/keeganstreet/specificity/blob/master/specificity.js#L211 + * + * @param {Array} aSpecificity Specificity of selector A + * @param {Array} bSpecificity Specificity of selector B + * @return {Number} Score of selector specificity A compared to selector specificity B + */ +function compareSpecificity(aSpecificity, bSpecificity) { + for (var i = 0; i < 4; i += 1) { + if (aSpecificity[i] < bSpecificity[i]) { + return -1; + } else if (aSpecificity[i] > bSpecificity[i]) { + return 1; + } + } + + return 0; +} + + +/** + * Compare two simple selectors. + * + * @param {Object} aSimpleSelectorNode Simple selector A + * @param {Object} bSimpleSelectorNode Simple selector B + * @return {Number} Score of selector A compared to selector B + */ +function compareSimpleSelectorNode(aSimpleSelectorNode, bSimpleSelectorNode) { + var aSpecificity = specificity(aSimpleSelectorNode), + bSpecificity = specificity(bSimpleSelectorNode); + return compareSpecificity(aSpecificity, bSpecificity); +} + +function _bySelectorSpecificity(selectorA, selectorB) { + return compareSimpleSelectorNode(selectorA.item.data, selectorB.item.data); +} + + +/** + * Sort selectors stably by their specificity. + * + * @param {Array} selectors to be sorted + * @return {Array} Stable sorted selectors + */ +function sortSelectors(selectors) { + return stable(selectors, _bySelectorSpecificity); +} + + +/** + * Convert a css-tree AST style declaration to CSSStyleDeclaration property. + * + * @param {Object} declaration css-tree style declaration + * @return {Object} CSSStyleDeclaration property + */ +function csstreeToStyleDeclaration(declaration) { + var propertyName = declaration.property, + propertyValue = csstree.translate(declaration.value), + propertyPriority = (declaration.important ? 'important' : ''); + return { + name: propertyName, + value: propertyValue, + priority: propertyPriority + }; +} + + +/** + * Gets the CSS string of a style element + * + * @param {Object} element style element + * @return {String|Array} CSS string or empty array if no styles are set + */ +function getCssStr(elem) { + return elem.content[0].text || elem.content[0].cdata || []; +} + +/** + * Sets the CSS string of a style element + * + * @param {Object} element style element + * @param {String} CSS string to be set + * @return {Object} reference to field with CSS + */ +function setCssStr(elem, css) { + // in case of cdata field + if(elem.content[0].cdata) { + elem.content[0].cdata = css; + return elem.content[0].cdata; + } + + // in case of text field + if nothing was set yet + elem.content[0].text = css; + return elem.content[0].text; +} + + +module.exports.flattenToSelectors = flattenToSelectors; + +module.exports.filterByMqs = filterByMqs; +module.exports.filterByPseudos = filterByPseudos; +module.exports.cleanPseudos = cleanPseudos; + +module.exports.compareSpecificity = compareSpecificity; +module.exports.compareSimpleSelectorNode = compareSimpleSelectorNode; + +module.exports.sortSelectors = sortSelectors; + +module.exports.csstreeToStyleDeclaration = csstreeToStyleDeclaration; + +module.exports.getCssStr = getCssStr; +module.exports.setCssStr = setCssStr; diff --git a/lib/svgo/css-class-list.js b/lib/svgo/css-class-list.js new file mode 100644 index 000000000..28e5816a2 --- /dev/null +++ b/lib/svgo/css-class-list.js @@ -0,0 +1,116 @@ +'use strict'; + +var values = require('object.values'); +if (!Object.values) { + values.shim(); +} + + +var CSSClassList = function(node) { + this.parentNode = node; + this.classNames = new Set(); + this.classAttr = null; + //this.classValue = null; +}; + + +CSSClassList.prototype.hasClass = function() { + this.classAttr = { // empty class attr + 'name': 'class', + 'value': null + }; + + this.addClassHandler(); +}; + + +// attr.class + +CSSClassList.prototype.addClassHandler = function() { + + Object.defineProperty(this.parentNode.attrs, 'class', { + get: this.getClassAttr.bind(this), + set: this.setClassAttr.bind(this), + enumerable: true, + configurable: true + }); + + this.addClassValueHandler(); +}; + +// attr.class.value + +CSSClassList.prototype.addClassValueHandler = function() { + + Object.defineProperty(this.classAttr, 'value', { + get: this.getClassValue.bind(this), + set: this.setClassValue.bind(this), + enumerable: true, + configurable: true + }); +}; + +CSSClassList.prototype.getClassAttr = function() { + return this.classAttr; +}; + +CSSClassList.prototype.setClassAttr = function(newClassAttr) { + this.setClassValue(newClassAttr.value); // must before applying value handler! + + this.classAttr = newClassAttr; + this.addClassValueHandler(); +}; + +CSSClassList.prototype.getClassValue = function() { + var arrClassNames = Array.from(this.classNames); + return arrClassNames.join(' '); +}; + +CSSClassList.prototype.setClassValue = function(newValue) { + if(typeof newValue === 'undefined') { + this.classNames.clear(); + return; + } + var arrClassNames = newValue.split(' '); + this.classNames = new Set(arrClassNames); +}; + + +CSSClassList.prototype.add = function(/* variadic */) { + this.hasClass(); + Object.values(arguments).forEach(this._addSingle.bind(this)); +}; + +CSSClassList.prototype._addSingle = function(className) { + this.classNames.add(className); +}; + + +CSSClassList.prototype.remove = function(/* variadic */) { + this.hasClass(); + Object.values(arguments).forEach(this._removeSingle.bind(this)); +}; + +CSSClassList.prototype._removeSingle = function(className) { + this.classNames.delete(className); +}; + + +CSSClassList.prototype.item = function(index) { + var arrClassNames = Array.from(this.classNames); + return arrClassNames[index]; +}; + +CSSClassList.prototype.toggle = function(className, force) { + if(this.contains(className) || force === false) { + this.classNames.delete(className); + } + this.classNames.add(className); +}; + +CSSClassList.prototype.contains = function(className) { + return this.classNames.has(className); +}; + + +module.exports = CSSClassList; \ No newline at end of file diff --git a/lib/svgo/css-select-adapter.js b/lib/svgo/css-select-adapter.js new file mode 100644 index 000000000..c37678cbf --- /dev/null +++ b/lib/svgo/css-select-adapter.js @@ -0,0 +1,53 @@ +'use strict'; + +var baseCssAdapter = require('css-select-base-adapter'); + +/** + * DOMUtils API for SVGO AST (used by css-select) + */ +var svgoCssSelectAdapterMin = { + + // is the node a tag? + // isTag: ( node:Node ) => isTag:Boolean + isTag: function(node) { + return node.isElem(); + }, + + // get the parent of the node + // getParent: ( node:Node ) => parentNode:Node + // returns null when no parent exists + getParent: function(node) { + return node.parentNode || null; + }, + + // get the node's children + // getChildren: ( node:Node ) => children:[Node] + getChildren: function(node) { + return node.content || []; + }, + + // get the name of the tag + // getName: ( elem:ElementNode ) => tagName:String + getName: function(elemAst) { + return elemAst.elem; + }, + + // get the text content of the node, and its children if it has any + // getText: ( node:Node ) => text:String + // returns empty string when there is no text + getText: function(node) { + return node.content[0].text || node.content[0].cdata || ''; + }, + + // get the attribute value + // getAttributeValue: ( elem:ElementNode, name:String ) => value:String + // returns null when attribute doesn't exist + getAttributeValue: function(elem, name) { + return elem.hasAttr(name) ? elem.attr(name).value : null; + } +}; + +// use base adapter for default implementation +var svgoCssSelectAdapter = baseCssAdapter(svgoCssSelectAdapterMin); + +module.exports = svgoCssSelectAdapter; diff --git a/lib/svgo/css-style-declaration.js b/lib/svgo/css-style-declaration.js new file mode 100644 index 000000000..43527fd10 --- /dev/null +++ b/lib/svgo/css-style-declaration.js @@ -0,0 +1,257 @@ +'use strict'; + +var csstree = require('css-tree'), + csstools = require('../css-tools'); + + +var CSSStyleDeclaration = function(node) { + this.parentNode = node; + + this.properties = new Map(); + this.hasSynced = false; + + this.styleAttr = null; + this.styleValue = null; + + this.parseError = false; +}; + + +CSSStyleDeclaration.prototype.hasStyle = function() { + this.styleAttr = { // empty style attr + 'name': 'style', + 'value': null + }; + + this.addStyleHandler(); +}; + + + + +// attr.style + +CSSStyleDeclaration.prototype.addStyleHandler = function() { + + Object.defineProperty(this.parentNode.attrs, 'style', { + get: this.getStyleAttr.bind(this), + set: this.setStyleAttr.bind(this), + enumerable: true, + configurable: true + }); + + this.addStyleValueHandler(); +}; + +// attr.style.value + +CSSStyleDeclaration.prototype.addStyleValueHandler = function() { + + Object.defineProperty(this.styleAttr, 'value', { + get: this.getStyleValue.bind(this), + set: this.setStyleValue.bind(this), + enumerable: true, + configurable: true + }); +}; + +CSSStyleDeclaration.prototype.getStyleAttr = function() { + return this.styleAttr; +}; + +CSSStyleDeclaration.prototype.setStyleAttr = function(newStyleAttr) { + this.setStyleValue(newStyleAttr.value); // must before applying value handler! + + this.styleAttr = newStyleAttr; + this.addStyleValueHandler(); + this.hasSynced = false; // raw css changed +}; + +CSSStyleDeclaration.prototype.getStyleValue = function() { + return this.getCssText(); +}; + +CSSStyleDeclaration.prototype.setStyleValue = function(newValue) { + this.properties.clear(); // reset all existing properties + this.styleValue = newValue; + this.hasSynced = false; // raw css changed +}; + + + + +CSSStyleDeclaration.prototype._loadCssText = function() { + if (this.hasSynced) { + return; + } + this.hasSynced = true; // must be set here to prevent loop in setProperty(...) + + if (!this.styleValue || this.styleValue.length === 0) { + return; + } + var inlineCssStr = this.styleValue; + + var declarations = {}; + try { + declarations = csstree.parse(inlineCssStr, { + context: 'declarationList', + parseValue: false + }); + } catch (parseError) { + this.parseError = parseError; + return; + } + this.parseError = false; + + var self = this; + declarations.children.each(function(declaration) { + var styleDeclaration = csstools.csstreeToStyleDeclaration(declaration); + self.setProperty(styleDeclaration.name, styleDeclaration.value, styleDeclaration.priority); + }); +}; + + +// only reads from properties + +/** + * Get the textual representation of the declaration block (equivalent to .cssText attribute). + * + * @return {String} Textual representation of the declaration block (empty string for no properties) + */ +CSSStyleDeclaration.prototype.getCssText = function() { + var properties = this.getProperties(); + + if (this.parseError) { + // in case of a parse error, pass through original styles + return this.styleValue; + } + + var cssText = []; + properties.forEach(function(property, propertyName) { + var strImportant = property.priority === 'important' ? '!important' : ''; + cssText.push(propertyName.trim() + ':' + property.value.trim() + strImportant); + }); + return cssText.join(';'); +}; + +CSSStyleDeclaration.prototype._handleParseError = function() { + if (this.parseError) { + console.warn('Warning: Parse error when parsing inline styles, style properties of this element cannot be used. The raw styles can still be get/set using .attr(\'style\').value. Error details: ' + this.parseError); + } +}; + + +CSSStyleDeclaration.prototype._getProperty = function(propertyName) { + if(typeof propertyName === 'undefined') { + throw Error('1 argument required, but only 0 present.'); + } + + var properties = this.getProperties(); + this._handleParseError(); + + var property = properties.get(propertyName.trim()); + return property; +}; + +/** + * Return the optional priority, "important". + * + * @param {String} propertyName representing the property name to be checked. + * @return {String} priority that represents the priority (e.g. "important") if one exists. If none exists, returns the empty string. + */ +CSSStyleDeclaration.prototype.getPropertyPriority = function(propertyName) { + var property = this._getProperty(propertyName); + return property ? property.priority : ''; +}; + +/** + * Return the property value given a property name. + * + * @param {String} propertyName representing the property name to be checked. + * @return {String} value containing the value of the property. If not set, returns the empty string. + */ +CSSStyleDeclaration.prototype.getPropertyValue = function(propertyName) { + var property = this._getProperty(propertyName); + return property ? property.value : null; +}; + +/** + * Return a property name. + * + * @param {Number} index of the node to be fetched. The index is zero-based. + * @return {String} propertyName that is the name of the CSS property at the specified index. + */ +CSSStyleDeclaration.prototype.item = function(index) { + if(typeof index === 'undefined') { + throw Error('1 argument required, but only 0 present.'); + } + + var properties = this.getProperties(); + this._handleParseError(); + + return Array.from(properties.keys())[index]; +}; + +/** + * Return all properties of the node. + * + * @return {Map} properties that is a Map with propertyName as key and property (propertyValue + propertyPriority) as value. + */ +CSSStyleDeclaration.prototype.getProperties = function() { + this._loadCssText(); + return this.properties; +}; + + +// writes to properties + +/** + * Remove a property from the CSS declaration block. + * + * @param {String} propertyName representing the property name to be removed. + * @return {String} oldValue equal to the value of the CSS property before it was removed. + */ +CSSStyleDeclaration.prototype.removeProperty = function(propertyName) { + if(typeof propertyName === 'undefined') { + throw Error('1 argument required, but only 0 present.'); + } + + this.hasStyle(); + + var properties = this.getProperties(); + this._handleParseError(); + + var oldValue = this.getPropertyValue(propertyName); + properties.delete(propertyName.trim()); + return oldValue; +}; + +/** + * Modify an existing CSS property or creates a new CSS property in the declaration block. + * + * @param {String} propertyName representing the CSS property name to be modified. + * @param {String} [value] containing the new property value. If not specified, treated as the empty string. value must not contain "!important" -- that should be set using the priority parameter. + * @param {String} [priority] allowing the "important" CSS priority to be set. If not specified, treated as the empty string. + * @return {undefined} + */ +CSSStyleDeclaration.prototype.setProperty = function(propertyName, value, priority) { + if(typeof propertyName === 'undefined') { + throw Error('propertyName argument required, but only not present.'); + } + + this.hasStyle(); + + var properties = this.getProperties(); + this._handleParseError(); + + var property = { + value: value.trim(), + priority: priority.trim() + }; + properties.set(propertyName.trim(), property); + + return property; +}; + + +module.exports = CSSStyleDeclaration; diff --git a/lib/svgo/jsAPI.js b/lib/svgo/jsAPI.js index 5aad68696..db966ee5b 100644 --- a/lib/svgo/jsAPI.js +++ b/lib/svgo/jsAPI.js @@ -1,5 +1,13 @@ 'use strict'; +var cssSelect = require('css-select'); + +var svgoCssSelectAdapter = require('./css-select-adapter'); +var cssSelectOpts = { + xmlMode: true, + adapter: svgoCssSelectAdapter +}; + var JSAPI = module.exports = function(data, parentNode) { Object.assign(this, data); if (parentNode) { @@ -252,6 +260,14 @@ JSAPI.prototype.renameElem = function(name) { this.attrs = this.attrs || {}; this.attrs[attr.name] = attr; + if(attr.name === 'class') { // newly added class attribute + this.class.hasClass(); + } + + if(attr.name === 'style') { // newly added style attribute + this.style.hasStyle(); + } + return this.attrs[attr.name]; }; @@ -293,3 +309,41 @@ JSAPI.prototype.renameElem = function(name) { return false; }; + +/** + * Evaluate a string of CSS selectors against the element and returns matched elements. + * + * @param {String} selectors CSS selector(s) string + * @return {Array} null if no elements matched + */ + JSAPI.prototype.querySelectorAll = function(selectors) { + + var matchedEls = cssSelect(selectors, this, cssSelectOpts); + + return matchedEls.length > 0 ? matchedEls : null; + +}; + +/** + * Evaluate a string of CSS selectors against the element and returns only the first matched element. + * + * @param {String} selectors CSS selector(s) string + * @return {Array} null if no element matched + */ + JSAPI.prototype.querySelector = function(selectors) { + + return cssSelect.selectOne(selectors, this, cssSelectOpts); + +}; + +/** + * Test if a selector matches a given element. + * + * @param {String} selector CSS selector string + * @return {Boolean} true if element would be selected by selector string, false if it does not + */ + JSAPI.prototype.matches = function(selector) { + + return cssSelect.is(this, selector, cssSelectOpts); + +}; diff --git a/lib/svgo/svg2js.js b/lib/svgo/svg2js.js index 2c36900c3..690e96e8a 100644 --- a/lib/svgo/svg2js.js +++ b/lib/svgo/svg2js.js @@ -2,6 +2,8 @@ var SAX = require('sax'), JSAPI = require('./jsAPI.js'), + CSSClassList = require('./css-class-list'), + CSSStyleDeclaration = require('./css-style-declaration'), entityDeclaration = /<!ENTITY\s+(\S+)\s+(?:'([^\']+)'|"([^\"]+)")\s*>/g; var config = { @@ -87,11 +89,23 @@ module.exports = function(data, callback) { prefix: data.prefix, local: data.local }; + elem.class = new CSSClassList(elem); + elem.style = new CSSStyleDeclaration(elem); if (Object.keys(data.attributes).length) { elem.attrs = {}; + for (var name in data.attributes) { + + if (name === 'class') { // has class attribute + elem.class.hasClass(); + } + + if (name === 'style') { // has style attribute + elem.style.hasStyle(); + } + elem.attrs[name] = { name: name, value: data.attributes[name].value, diff --git a/package.json b/package.json index 3a660da79..343d77a31 100644 --- a/package.json +++ b/package.json @@ -50,20 +50,26 @@ "dependencies": { "coa": "~1.0.1", "colors": "~1.1.2", - "csso": "~2.3.1", + "css-select": "^1.3.0-rc0", + "css-select-base-adapter": "^0.1.0", + "css-tree": "1.0.0-alpha22", + "csso": "^3.0.1", "js-yaml": "~3.7.0", "mkdirp": "~0.5.1", + "object.values": "^1.0.4", "sax": "~1.2.1", + "stable": "^0.1.5", + "whet.extend": "~0.9.9", "util.promisify": "~1.0.0" }, "devDependencies": { + "coveralls": "~2.11.14", "fs-extra": "~4.0.1", - "mocha": "~3.2.0", - "should": "11.2.0", "istanbul": "~0.4.5", + "mocha": "~3.2.0", "mocha-istanbul": "~0.3.0", "mock-stdin": "~0.3.1", - "coveralls": "~2.11.14" + "should": "11.2.0" }, "engines": { "node": ">=4.0.0" diff --git a/plugins/inlineStyles.js b/plugins/inlineStyles.js new file mode 100644 index 000000000..13d9c6161 --- /dev/null +++ b/plugins/inlineStyles.js @@ -0,0 +1,236 @@ +'use strict'; + +exports.type = 'full'; + +exports.active = true; + +exports.params = { + onlyMatchedOnce: true, + removeMatchedSelectors: true, + useMqs: ['', 'screen'], + usePseudos: [''] +}; + +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 + * + * Options + * onlyMatchedOnce (default: true) + * inline only selectors that match once + * + * removeMatchedSelectors (default: true) + * clean up matched selectors, + * leave selectors that hadn't matched + * + * useMqs (default: ['', 'screen']) + * what media queries to be used + * empty string element for styles outside media queries + * + * usePseudos (default: ['']) + * what pseudo-classes/-elements to be used + * empty string element for all non-pseudo-classes and/or -elements + * + * @param {Object} document document element + * @param {Object} opts plugin params + * + * @author strarsis <strarsis@gmail.com> + */ +exports.fn = function(document, opts) { + + // collect <style/>s + var styleEls = document.querySelectorAll('style'); + + //no <styles/>s, nothing to do + if (styleEls === null) { + return document; + } + + var styles = [], + selectors = []; + + for (var styleEl of styleEls) { + if (styleEl.isEmpty()) { + // skip empty <style/>s + continue; + } + var cssStr = cssTools.getCssStr(styleEl); + + // collect <style/>s and their css ast + var cssAst = {}; + try { + cssAst = csstree.parse(cssStr, { + parseValue: false, + parseCustomProperty: false + }); + } catch (parseError) { + console.warn('Warning: Parse error of styles of <style/> element, skipped. Error details: ' + parseError); + continue; + } + + styles.push({ + styleEl: styleEl, + cssAst: cssAst + }); + + selectors = selectors.concat(cssTools.flattenToSelectors(cssAst)); + } + + + // filter for mediaqueries to be used or without any mediaquery + var selectorsMq = cssTools.filterByMqs(selectors, opts.useMqs); + + + // filter for pseudo elements to be used + var selectorsPseudo = cssTools.filterByPseudos(selectorsMq, opts.usePseudos); + + // remove PseudoClass from its SimpleSelector for proper matching + cssTools.cleanPseudos(selectorsPseudo); + + + // stable sort selectors + var sortedSelectors = cssTools.sortSelectors(selectorsPseudo).reverse(); + + + var selector, + selectedEl; + + // match selectors + for (selector of sortedSelectors) { + var selectorStr = csstree.translate(selector.item.data), + selectedEls = null; + + try { + selectedEls = document.querySelectorAll(selectorStr); + } catch (selectError) { + if (selectError.constructor === SyntaxError) { + console.warn('Warning: Syntax error when trying to select \n\n' + selectorStr + '\n\n, skipped. Error details: ' + selectError); + continue; + } + throw selectError; + } + + if (selectedEls === null) { + // nothing selected + continue; + } + + selector.selectedEls = selectedEls; + } + + + // apply <style/> styles to matched elements + for (selector of sortedSelectors) { + if(!selector.selectedEls) { + continue; + } + + if (opts.onlyMatchedOnce && selector.selectedEls !== null && selector.selectedEls.length > 1) { + // skip selectors that match more than once if option onlyMatchedOnce is enabled + continue; + } + + // apply <style/> to matched elements + for (selectedEl of selector.selectedEls) { + if (selector.rule === null) { + continue; + } + + // merge declarations + csstree.walkDeclarations(selector.rule, function(styleCsstreeDeclaration) { + + // existing inline styles have higher priority + // no inline styles, external styles, external styles used + // inline styles, external styles same priority as inline styles, inline styles used + // inline styles, external styles higher priority than inline styles, external styles used + var styleDeclaration = cssTools.csstreeToStyleDeclaration(styleCsstreeDeclaration); + if (selectedEl.style.getPropertyValue(styleDeclaration.name) !== null && + selectedEl.style.getPropertyPriority(styleDeclaration.name) >= styleDeclaration.priority) { + return; + } + selectedEl.style.setProperty(styleDeclaration.name, styleDeclaration.value, styleDeclaration.priority); + }); + } + + if (opts.removeMatchedSelectors && selector.selectedEls !== null && selector.selectedEls.length > 0) { + // clean up matching simple selectors if option removeMatchedSelectors is enabled + selector.rule.selector.children.remove(selector.item); + } + } + + + if (opts.removeMatchedSelectors) { + // clean up matched class + ID attribute values + for (selector of sortedSelectors) { + if(!selector.selectedEls) { + continue; + } + + for (selectedEl of selector.selectedEls) { + // class + var firstSubSelector = selector.item.data.children.first(); + if(firstSubSelector.type === 'ClassSelector') { + selectedEl.class.remove(firstSubSelector.name); + } + // clean up now empty class attributes + if(typeof selectedEl.class.item(0) === 'undefined') { + selectedEl.removeAttr('class'); + } + + // ID + if(firstSubSelector.type === 'IdSelector') { + selectedEl.removeAttr('id', firstSubSelector.name); + } + } + } + } + + + // clean up elements + for (var style of styles) { + csstree.walkRules(style.cssAst, function(node, item, list) { + // clean up <style/> atrules without any rulesets left + if (node.type === 'Atrule' && + // only Atrules containing rulesets + node.block !== null && + node.block.children.isEmpty()) { + list.remove(item); + return; + } + + // clean up <style/> rulesets without any css selectors left + if (node.type === 'Rule' && + node.selector.children.isEmpty()) { + list.remove(item); + } + }); + + + if (style.cssAst.children.isEmpty()) { + // clean up now emtpy <style/>s + var styleParentEl = style.styleEl.parentNode; + styleParentEl.spliceContent(styleParentEl.content.indexOf(style.styleEl), 1); + + if (styleParentEl.elem === 'defs' && + styleParentEl.content.length === 0) { + // also clean up now empty <def/>s + var defsParentEl = styleParentEl.parentNode; + defsParentEl.spliceContent(defsParentEl.content.indexOf(styleParentEl), 1); + } + + continue; + } + + + // update existing, left over <style>s + cssTools.setCssStr(style.styleEl, csstree.translate(style.cssAst)); + } + + + return document; +}; diff --git a/test/plugins/inlineStyles.01.svg b/test/plugins/inlineStyles.01.svg new file mode 100644 index 000000000..24cda3991 --- /dev/null +++ b/test/plugins/inlineStyles.01.svg @@ -0,0 +1,9 @@ +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <rect width="100" height="100" class="st0"/> +</svg> + +@@@ + +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <rect width="100" height="100" class="st0"/> +</svg> diff --git a/test/plugins/inlineStyles.02.svg b/test/plugins/inlineStyles.02.svg new file mode 100644 index 000000000..e7f2794cd --- /dev/null +++ b/test/plugins/inlineStyles.02.svg @@ -0,0 +1,12 @@ +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <style> + .st0{fill:blue;} + </style> + <rect width="100" height="100" class="st0"/> +</svg> + +@@@ + +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <rect width="100" height="100" style="fill:blue"/> +</svg> diff --git a/test/plugins/inlineStyles.03.svg b/test/plugins/inlineStyles.03.svg new file mode 100644 index 000000000..32f554268 --- /dev/null +++ b/test/plugins/inlineStyles.03.svg @@ -0,0 +1,24 @@ +<svg xmlns="http://www.w3.org/2000/svg" id="dark" viewBox="0 0 258.12 225.88"> +<!-- for https://github.com/svg/svgo/pull/592#issuecomment-266327016 --> + <style> + .cls-7 { + only-cls-7: 1; + } + .cls-7, + .cls-8 { + cls-7-and-8: 1; + } + </style> + + <path class="cls-7"/> +</svg> + +@@@ + +<svg xmlns="http://www.w3.org/2000/svg" id="dark" viewBox="0 0 258.12 225.88"> +<!--for https://github.com/svg/svgo/pull/592#issuecomment-266327016--> + <style> + .cls-8{cls-7-and-8: 1} + </style> + <path style="cls-7-and-8:1;only-cls-7:1"/> +</svg> diff --git a/test/plugins/inlineStyles.04.svg b/test/plugins/inlineStyles.04.svg new file mode 100644 index 000000000..a4ba6dc72 --- /dev/null +++ b/test/plugins/inlineStyles.04.svg @@ -0,0 +1,13 @@ +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <style> + .st0{fill:blue;} + .st1{fill:red; } + </style> + <rect width="100" height="100" class="st0 st1"/> +</svg> + +@@@ + +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <rect width="100" height="100" style="fill:red"/> +</svg> diff --git a/test/plugins/inlineStyles.05.svg b/test/plugins/inlineStyles.05.svg new file mode 100644 index 000000000..1d8f86040 --- /dev/null +++ b/test/plugins/inlineStyles.05.svg @@ -0,0 +1,17 @@ +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <style> + .st1 { + fill: red; + } + .st0 { + color: blue; + } + </style> + <rect width="100" height="100" class="st0 st1" style="color:yellow"/> +</svg> + +@@@ + +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <rect width="100" height="100" style="color:yellow;fill:red"/> +</svg> diff --git a/test/plugins/inlineStyles.06.svg b/test/plugins/inlineStyles.06.svg new file mode 100644 index 000000000..3490e8618 --- /dev/null +++ b/test/plugins/inlineStyles.06.svg @@ -0,0 +1,23 @@ +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <style> + .red { + fill: red; + } + .blue { + fill: blue; + } + </style> + <rect width="100" height="100" class="red blue"/> + <rect width="100" height="100" class="blue red"/> +</svg> + +@@@ + +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <rect width="100" height="100" style="fill:blue"/> + <rect width="100" height="100" style="fill:blue"/> +</svg> + +@@@ + +{"onlyMatchedOnce":false} diff --git a/test/plugins/inlineStyles.07.svg b/test/plugins/inlineStyles.07.svg new file mode 100644 index 000000000..a2f2f40cb --- /dev/null +++ b/test/plugins/inlineStyles.07.svg @@ -0,0 +1,17 @@ +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <style> + .red { + fill: red !important; + } + .blue { + fill: blue; + } + </style> + <rect width="100" height="100" class="blue red"/> +</svg> + +@@@ + +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <rect width="100" height="100" style="fill:red!important"/> +</svg> diff --git a/test/plugins/inlineStyles.08.svg b/test/plugins/inlineStyles.08.svg new file mode 100644 index 000000000..3224d0e9f --- /dev/null +++ b/test/plugins/inlineStyles.08.svg @@ -0,0 +1,17 @@ +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <style> + .red { + fill: red !important; + } + .blue { + fill: blue; + } + </style> + <rect width="100" height="100" class="blue red" style="fill:yellow"/> +</svg> + +@@@ + +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <rect width="100" height="100" style="fill:red!important"/> +</svg> diff --git a/test/plugins/inlineStyles.09.svg b/test/plugins/inlineStyles.09.svg new file mode 100644 index 000000000..025064b50 --- /dev/null +++ b/test/plugins/inlineStyles.09.svg @@ -0,0 +1,17 @@ +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <style> + .red { + fill: red !important; + } + .blue { + fill: blue; + } + </style> + <rect width="100" height="100" class="blue red" style="fill:yellow !important"/> +</svg> + +@@@ + +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <rect width="100" height="100" style="fill:yellow!important"/> +</svg> diff --git a/test/plugins/inlineStyles.10.svg b/test/plugins/inlineStyles.10.svg new file mode 100644 index 000000000..a555dcec8 --- /dev/null +++ b/test/plugins/inlineStyles.10.svg @@ -0,0 +1,14 @@ +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <style> + <![CDATA[ + .st0{fill:blue;} + ]]> + </style> + <rect width="100" height="100" class="st0"/> +</svg> + +@@@ + +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <rect width="100" height="100" style="fill:blue"/> +</svg> diff --git a/test/plugins/inlineStyles.11.svg b/test/plugins/inlineStyles.11.svg new file mode 100644 index 000000000..2ce5a9e1a --- /dev/null +++ b/test/plugins/inlineStyles.11.svg @@ -0,0 +1,16 @@ +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <style> + .st0{fill:blue;} + .st0:hover{stroke:red;} + </style> + <rect width="100" height="100" class="st0"/> +</svg> + +@@@ + +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <style> + .st0:hover{stroke:red} + </style> + <rect width="100" height="100" style="fill:blue"/> +</svg> diff --git a/test/plugins/inlineStyles.12.svg b/test/plugins/inlineStyles.12.svg new file mode 100644 index 000000000..0b7312b0a --- /dev/null +++ b/test/plugins/inlineStyles.12.svg @@ -0,0 +1,16 @@ +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <style> + .st0:hover{stroke:red;} + </style> + <rect width="100" height="100" class="st0"/> +</svg> + +@@@ + +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> + <rect width="100" height="100" style="stroke:red"/> +</svg> + +@@@ + +{"usePseudos":[":hover"]} diff --git a/test/plugins/inlineStyles.13.svg b/test/plugins/inlineStyles.13.svg new file mode 100644 index 000000000..5c7b39616 --- /dev/null +++ b/test/plugins/inlineStyles.13.svg @@ -0,0 +1,68 @@ +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 81.285 81.285"> + <defs> + <style> + + /* Simple Atrules */ + @charset 'UTF-8'; + + @namespace svg url(http://www.w3.org/2000/svg); + + @import url('https://fonts.googleapis.com/css?family=Roboto'); + + /* Atrules with block */ + @font-face { + font-family: SomeFont; + src: local("Some Font"), + local("SomeFont"), + url(SomeFont.ttf); + font-weight: bold; + } + + @viewport { + zoom: 0.8; + min-zoom: 0.4; + max-zoom: 0.9; + } + + @keyframes identifier { + 0% { top: 0; } + 50% { top: 30px; left: 20px; } + 50% { top: 10px; } + 100% { top: 0; } + } + + + /* Nested rules */ + @page :first { + margin: 1in; + } + + @supports (display: flex) { + .module { display: flex; } + } + + @document url('http://example.com/test.html') { + rect { + stroke: red; + } + } + + + .blue { + fill: blue; + } + </style> + </defs> + <rect width="100" height="100" class="blue"/> +</svg> + +@@@ + +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 81.285 81.285"> + <defs> + <style> + @charset 'UTF-8';@namespace svg url(http://www.w3.org/2000/svg);@import url('https://fonts.googleapis.com/css?family=Roboto');@font-face{font-family: SomeFont;src: local("Some Font"), local("SomeFont"), url(SomeFont.ttf);font-weight: bold}@viewport{zoom: 0.8;min-zoom: 0.4;max-zoom: 0.9}@keyframes identifier{0%{top: 0}50%{top: 30px;left: 20px}50%{top: 10px}100%{top: 0}}@page :first{margin: 1in}@supports (display: flex){.module{display: flex}}@document url('http://example.com/test.html'){rect{stroke: red}} + </style> + </defs> + <rect width="100" height="100" style="fill:blue"/> +</svg> diff --git a/test/plugins/inlineStyles.14.svg b/test/plugins/inlineStyles.14.svg new file mode 100644 index 000000000..74e17a55f --- /dev/null +++ b/test/plugins/inlineStyles.14.svg @@ -0,0 +1,25 @@ +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 81.285 81.285"> + <defs> + <style> + @media only screen + and (min-device-width: 320px) + and (max-device-width: 480px) + and (-webkit-min-device-pixel-ratio: 2) { + + .blue { fill: blue; } + + } + </style> + </defs> + <rect width="100" height="100" class="blue"/> +</svg> + +@@@ + +<svg id="test" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 81.285 81.285"> + <rect width="100" height="100" style="fill:blue"/> +</svg> + +@@@ + +{"useMqs": ["media only screen and (min-device-width:320px) and (max-device-width:480px) and (-webkit-min-device-pixel-ratio:2)"]} diff --git a/test/plugins/inlineStyles.15.svg b/test/plugins/inlineStyles.15.svg new file mode 100644 index 000000000..eb9dabfc3 --- /dev/null +++ b/test/plugins/inlineStyles.15.svg @@ -0,0 +1,67 @@ +<?xml version="1.0" standalone="no"?> +<svg viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs xmlns="http://www.w3.org/1999/xhtml"> + <style type="text/css"> + html /deep/ [layout][horizontal], html /deep/ [layout][vertical] { display: flex; } + html /deep/ [layout][horizontal][inline], html /deep/ [layout][vertical][inline] { display: inline-flex; } + html /deep/ [layout][horizontal] { flex-direction: row; } + html /deep/ [layout][horizontal][reverse] { flex-direction: row-reverse; } + html /deep/ [layout][vertical] { flex-direction: column; } + html /deep/ [layout][vertical][reverse] { flex-direction: column-reverse; } + html /deep/ [layout][wrap] { flex-wrap: wrap; } + html /deep/ [layout][wrap-reverse] { flex-wrap: wrap-reverse; } + html /deep/ [flex] { flex: 1 1 0px; } + html /deep/ [flex][auto] { flex: 1 1 auto; } + html /deep/ [flex][none] { flex: 0 0 auto; } + html /deep/ [flex][one] { flex: 1 1 0px; } + html /deep/ [flex][two] { flex: 2 1 0px; } + html /deep/ [flex][three] { flex: 3 1 0px; } + html /deep/ [flex][four] { flex: 4 1 0px; } + html /deep/ [flex][five] { flex: 5 1 0px; } + html /deep/ [flex][six] { flex: 6 1 0px; } + html /deep/ [flex][seven] { flex: 7 1 0px; } + html /deep/ [flex][eight] { flex: 8 1 0px; } + html /deep/ [flex][nine] { flex: 9 1 0px; } + html /deep/ [flex][ten] { flex: 10 1 0px; } + html /deep/ [flex][eleven] { flex: 11 1 0px; } + html /deep/ [flex][twelve] { flex: 12 1 0px; } + html /deep/ [layout][start] { align-items: flex-start; } + html /deep/ [layout][center] { align-items: center; } + html /deep/ [layout][end] { align-items: flex-end; } + html /deep/ [layout][start-justified] { justify-content: flex-start; } + html /deep/ [layout][center-justified] { justify-content: center; } + html /deep/ [layout][end-justified] { justify-content: flex-end; } + html /deep/ [layout][around-justified] { justify-content: space-around; } + html /deep/ [layout][justified] { justify-content: space-between; } + html /deep/ [self-start] { align-self: flex-start; } + html /deep/ [self-center] { align-self: center; } + html /deep/ [self-end] { align-self: flex-end; } + html /deep/ [self-stretch] { align-self: stretch; } + html /deep/ [block] { display: block; } + html /deep/ [hidden] { display: none !important; } + html /deep/ [relative] { position: relative; } + html /deep/ [fit] { position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; } + body[fullbleed] { margin: 0px; height: 100vh; } + html /deep/ [segment], html /deep/ segment { display: block; position: relative; box-sizing: border-box; margin: 1em 0.5em; padding: 1em; -webkit-box-shadow: rgba(0, 0, 0, 0.0980392) 0px 0px 0px 1px; box-shadow: rgba(0, 0, 0, 0.0980392) 0px 0px 0px 1px; border-top-left-radius: 5px; border-top-right-radius: 5px; border-bottom-right-radius: 5px; border-bottom-left-radius: 5px; background-color: white; } + html /deep/ core-icon { display: inline-block; vertical-align: middle; background-repeat: no-repeat; } + html /deep/ core-icon[size=""] { position: relative; } + </style> + </defs> + <g id="airplanemode-on"> + <path d="M10.2,9"/> + </g> +</svg> + +@@@ + +<?xml version="1.0" standalone="no"?> +<svg viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs xmlns="http://www.w3.org/1999/xhtml"> + <style type="text/css"> + html /deep/ [layout][horizontal],html /deep/ [layout][vertical]{display: flex}html /deep/ [layout][horizontal][inline],html /deep/ [layout][vertical][inline]{display: inline-flex}html /deep/ [layout][horizontal]{flex-direction: row}html /deep/ [layout][horizontal][reverse]{flex-direction: row-reverse}html /deep/ [layout][vertical]{flex-direction: column}html /deep/ [layout][vertical][reverse]{flex-direction: column-reverse}html /deep/ [layout][wrap]{flex-wrap: wrap}html /deep/ [layout][wrap-reverse]{flex-wrap: wrap-reverse}html /deep/ [flex]{flex: 1 1 0px}html /deep/ [flex][auto]{flex: 1 1 auto}html /deep/ [flex][none]{flex: 0 0 auto}html /deep/ [flex][one]{flex: 1 1 0px}html /deep/ [flex][two]{flex: 2 1 0px}html /deep/ [flex][three]{flex: 3 1 0px}html /deep/ [flex][four]{flex: 4 1 0px}html /deep/ [flex][five]{flex: 5 1 0px}html /deep/ [flex][six]{flex: 6 1 0px}html /deep/ [flex][seven]{flex: 7 1 0px}html /deep/ [flex][eight]{flex: 8 1 0px}html /deep/ [flex][nine]{flex: 9 1 0px}html /deep/ [flex][ten]{flex: 10 1 0px}html /deep/ [flex][eleven]{flex: 11 1 0px}html /deep/ [flex][twelve]{flex: 12 1 0px}html /deep/ [layout][start]{align-items: flex-start}html /deep/ [layout][center]{align-items: center}html /deep/ [layout][end]{align-items: flex-end}html /deep/ [layout][start-justified]{justify-content: flex-start}html /deep/ [layout][center-justified]{justify-content: center}html /deep/ [layout][end-justified]{justify-content: flex-end}html /deep/ [layout][around-justified]{justify-content: space-around}html /deep/ [layout][justified]{justify-content: space-between}html /deep/ [self-start]{align-self: flex-start}html /deep/ [self-center]{align-self: center}html /deep/ [self-end]{align-self: flex-end}html /deep/ [self-stretch]{align-self: stretch}html /deep/ [block]{display: block}html /deep/ [hidden]{display: none !important}html /deep/ [relative]{position: relative}html /deep/ [fit]{position: absolute;top: 0px;right: 0px;bottom: 0px;left: 0px}body[fullbleed]{margin: 0px;height: 100vh}html /deep/ [segment],html /deep/ segment{display: block;position: relative;box-sizing: border-box;margin: 1em 0.5em;padding: 1em;-webkit-box-shadow: rgba(0, 0, 0, 0.0980392) 0px 0px 0px 1px;box-shadow: rgba(0, 0, 0, 0.0980392) 0px 0px 0px 1px;border-top-left-radius: 5px;border-top-right-radius: 5px;border-bottom-right-radius: 5px;border-bottom-left-radius: 5px;background-color: white}html /deep/ core-icon{display: inline-block;vertical-align: middle;background-repeat: no-repeat}html /deep/ core-icon[size=""]{position: relative} + </style> + </defs> + <g id="airplanemode-on"> + <path d="M10.2,9"/> + </g> +</svg> diff --git a/test/plugins/inlineStyles.16.svg b/test/plugins/inlineStyles.16.svg new file mode 100644 index 000000000..bdcb1ad5a --- /dev/null +++ b/test/plugins/inlineStyles.16.svg @@ -0,0 +1,40 @@ +<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 222 57.28"> + <defs> + <style> + #id0 { + stroke: red; + } + + .cls-1 { + fill: #37d0cd; + } + + .cls-2{ + fill: #fff; + } + </style> + </defs> + <title>button</title> + <rect id="id0" class="cls-1" width="222" height="57.28" rx="28.64" ry="28.64"/> + <path class="cls-2" d="M312.75,168.66A2.15,2.15,0,0,1,311.2,165L316,160l-4.8-5a2.15,2.15,0,1,1,3.1-3l6.21,6.49a2.15,2.15,0,0,1,0,3L314.31,168a2.14,2.14,0,0,1-1.56.67Zm0,0" transform="translate(-119 -131.36)"/> + <circle class="cls-2" cx="33.5" cy="27.25" r="2.94"/> + <circle class="cls-2" cx="162.5" cy="158.61" r="2.94" transform="translate(-181.03 61.15) rotate(-52.89)"/> + <circle class="cls-2" cx="172.5" cy="158.61" r="2.94" transform="translate(-157.03 -75.67) rotate(-16.55)"/> +</svg> + +@@@ + +<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 222 57.28"> + <title> + button + </title> + <rect width="222" height="57.28" rx="28.64" ry="28.64" style="stroke:red;fill:#37d0cd"/> + <path d="M312.75,168.66A2.15,2.15,0,0,1,311.2,165L316,160l-4.8-5a2.15,2.15,0,1,1,3.1-3l6.21,6.49a2.15,2.15,0,0,1,0,3L314.31,168a2.14,2.14,0,0,1-1.56.67Zm0,0" transform="translate(-119 -131.36)" style="fill:#fff"/> + <circle cx="33.5" cy="27.25" r="2.94" style="fill:#fff"/> + <circle cx="162.5" cy="158.61" r="2.94" transform="translate(-181.03 61.15) rotate(-52.89)" style="fill:#fff"/> + <circle cx="172.5" cy="158.61" r="2.94" transform="translate(-157.03 -75.67) rotate(-16.55)" style="fill:#fff"/> +</svg> + +@@@ + +{"onlyMatchedOnce":false} diff --git a/test/plugins/removeScriptElement.01.svg b/test/plugins/removeScriptElement.01.svg index b3655db01..e242aa520 100644 --- a/test/plugins/removeScriptElement.01.svg +++ b/test/plugins/removeScriptElement.01.svg @@ -7,6 +7,6 @@ @@@ <?xml version="1.0" encoding="utf-16"?> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve"> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100" xml:space="preserve"> <circle class="st0" cx="50" cy="50" r="50"/> </svg> diff --git a/test/plugins/removeStyleElement.01.svg b/test/plugins/removeStyleElement.01.svg index 2f3c52f88..c3d415091 100644 --- a/test/plugins/removeStyleElement.01.svg +++ b/test/plugins/removeStyleElement.01.svg @@ -11,6 +11,6 @@ @@@ <?xml version="1.0" encoding="utf-16"?> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve"> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100" xml:space="preserve"> <circle class="st0" cx="50" cy="50" r="50"/> </svg> \ No newline at end of file