Skip to content

Commit

Permalink
Add inlineStyles plugin (rewrite of localStyles plugin) (svg#592)
Browse files Browse the repository at this point in the history
Add inlineStyles plugin
  • Loading branch information
strarsis authored and GreLI committed Oct 22, 2017
1 parent f2fa7a3 commit 2523799
Show file tree
Hide file tree
Showing 28 changed files with 1,468 additions and 6 deletions.
1 change: 1 addition & 0 deletions .svgo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ plugins:
- removeXMLNS
- removeEditorsNSData
- cleanupAttrs
- inlineStyles
- minifyStyles
- convertStyleToAttrs
- cleanupIDs
Expand Down
108 changes: 108 additions & 0 deletions docs/how-it-works/en.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
222 changes: 222 additions & 0 deletions lib/css-tools.js
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 2523799

Please sign in to comment.