You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
239 lines
6.3 KiB
239 lines
6.3 KiB
'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 {import('css-tree').CssNode} cssAst css-tree AST to flatten |
|
* @return {Array} selectors |
|
*/ |
|
function flattenToSelectors(cssAst) { |
|
var selectors = []; |
|
|
|
csstree.walk(cssAst, { |
|
visit: 'Rule', |
|
enter: function (node) { |
|
if (node.type !== 'Rule') { |
|
return; |
|
} |
|
|
|
var atrule = this.atrule; |
|
var rule = node; |
|
|
|
node.prelude.children.each(function (selectorNode, selectorItem) { |
|
var selector = { |
|
item: selectorItem, |
|
atrule: atrule, |
|
rule: rule, |
|
pseudos: /** @type {{item: any; list: any[]}[]} */ ([]), |
|
}; |
|
|
|
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.generate(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.generate({ |
|
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 {void} |
|
*/ |
|
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 {import('css-tree').CssNode} declaration css-tree style declaration |
|
* @return {Object} CSSStyleDeclaration property |
|
*/ |
|
function csstreeToStyleDeclaration(declaration) { |
|
var propertyName = declaration.property, |
|
propertyValue = csstree.generate(declaration.value), |
|
propertyPriority = declaration.important ? 'important' : ''; |
|
return { |
|
name: propertyName, |
|
value: propertyValue, |
|
priority: propertyPriority, |
|
}; |
|
} |
|
|
|
/** |
|
* Gets the CSS string of a style element |
|
* |
|
* @param {Object} elem style element |
|
* @return {string} CSS string or empty array if no styles are set |
|
*/ |
|
function getCssStr(elem) { |
|
if ( |
|
elem.children.length > 0 && |
|
(elem.children[0].type === 'text' || elem.children[0].type === 'cdata') |
|
) { |
|
return elem.children[0].value; |
|
} |
|
return ''; |
|
} |
|
|
|
/** |
|
* Sets the CSS string of a style element |
|
* |
|
* @param {Object} elem style element |
|
* @param {string} css string to be set |
|
* @return {string} reference to field with CSS |
|
*/ |
|
function setCssStr(elem, css) { |
|
if (elem.children.length === 0) { |
|
elem.children.push({ |
|
type: 'text', |
|
value: '', |
|
}); |
|
} |
|
|
|
if (elem.children[0].type !== 'text' && elem.children[0].type !== 'cdata') { |
|
return css; |
|
} |
|
|
|
elem.children[0].value = css; |
|
|
|
return css; |
|
} |
|
|
|
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;
|
|
|