Reputation: 474091
The Story:
I'm currently building a ESLint rule to warn about using bootstrap layout-oriented and angular technical classes inside CSS selector locators. Currently I'm using a simple substring in a string approach:
for (var i = 0; i < prohibitedClasses.length; i++) {
if (node.arguments[0].value.indexOf(prohibitedClasses[i]) >= 0) {
context.report({
node: node,
message: 'Unexpected Bootstrap class "' + prohibitedClasses[i] + '" inside a CSS selector'
})
}
But it has not proved to be reliable. For example, it throws an error 2 times on .col-sm-offset-11
CSS selector reporting both col-sm-offset-1
and col-sm-offset-11
to be used. I can imagine it can easily break on more complex selectors with multiple pseudo-classes used.
The Question:
What is the most reliable way to extract class names from a CSS selector?
Here is a sample test list we should cover (to be improved):
.col-sm-push-4 // -> ['col-sm-push-4']
.myclass.col-lg-pull-8 // -> ['myclass', 'col-lg-pull-8']
[class*='col-md-offset-4'] // -> []
[class$=col-md-offset-11] // -> []
[class~="col-md-10"] .myclass // -> ['col-md-10', 'myclass']
.col-md-10,.col-md-11 // -> ['col-md-10', 'col-md-11']
Note that we need to skip the ^=
, $=
and *=
partial class filter values leaving the ~=
(thanks for the comments).
Upvotes: 4
Views: 2062
Reputation: 474091
There is a specially designed for the problem package called node-css-selector-parser
which lacks the "how to use it" part to extract the class names. Filling the gap, here is how I've applied it to the problem.
With node-css-selector-parser
, we can parse a CSS selector and based on a result type analyze class names used with a dot (e.g. .myclass
) and class names used inside the attribute selector (e.g. [class*=test]
):
// setup up CSS selector parser
var CssSelectorParser = require('css-selector-parser').CssSelectorParser
var parser = new CssSelectorParser()
parser.registerSelectorPseudos('has', 'contains')
parser.registerNestingOperators('>', '+', '~')
parser.registerAttrEqualityMods('^', '$', '*', '~')
parser.enableSubstitutes()
function extractClassNames (rule) {
var classNames = []
// extract class names defined with ".", e.g. .myclass
if (rule.classNames) {
classNames.push.apply(classNames, rule.classNames)
}
// extract class names defined in attributes, e.g. [class*=myclass]
if (rule.attrs) {
rule.attrs.forEach(function (attr) {
if (attr.name === 'class') {
classNames.push(attr.value)
}
})
}
return classNames
}
module.exports = function (cssSelector) {
try {
var result = parser.parse(cssSelector)
} catch (err) {
// ignore parsing errors - we don't want it to fail miserably on a target machine during a ESLint run
console.log('Parsing CSS selector: "' + cssSelector + '". ' + err)
return []
}
// handling empty inputs
if (!result) {
return []
}
var classNames = []
if (result.type === 'ruleSet') {
var rule = result.rule
while (rule) {
classNames.push.apply(classNames, extractClassNames(rule))
rule = rule.rule
}
} else if (result.type === 'selectors' && result.selectors) {
result.selectors.forEach(function (selector) {
var srule = selector.rule
while (srule) {
classNames.push.apply(classNames, extractClassNames(srule))
srule = srule.rule;
}
})
}
return classNames
}
(standard
code style is used - hence, for instance, no ;
at the end of the lines)
This proved to work for me and hopefully would help others with a similar problem. Note that in this state this code would also extract the partial class values passed into the ^=
, $=
and *=
which ideally need to be skipped.
Upvotes: 2