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 ( ) + * @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 ( ) + * @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 = //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 + */ +exports.fn = function(document, opts) { + + // collect + + + +@@@ + + + + 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 @@ + + + + + + + +@@@ + + + + + + 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 @@ + + + + + +@@@ + + + + 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 @@ + + + + + +@@@ + + + + 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 @@ + + + + + + +@@@ + + + + + + +@@@ + +{"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 @@ + + + + + +@@@ + + + + 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 @@ + + + + + +@@@ + + + + 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 @@ + + + + + +@@@ + + + + 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 @@ + + + + + +@@@ + + + + 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 @@ + + + + + +@@@ + + + + + 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 @@ + + + + + +@@@ + + + + + +@@@ + +{"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 @@ + + + + + + + +@@@ + + + + + + + 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 @@ + + + + + + + +@@@ + + + + + +@@@ + +{"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 @@ + + + + + + + + + + +@@@ + + + + + + + + + + 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 @@ + + + + + button + + + + + + + +@@@ + + + + button + + + + + + + + +@@@ + +{"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 @@ @@@ - + 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 @@ @@@ - + \ No newline at end of file