Skip to content

Commit

Permalink
Merge pull request #31 from merkle-open/fix/26-pseudo-classes
Browse files Browse the repository at this point in the history
#26 fix for pseudo classes
  • Loading branch information
ernscht authored Aug 31, 2023
2 parents d043c74 + 0944daf commit a234961
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 47 deletions.
2 changes: 1 addition & 1 deletion .node-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18.17.0
18.17.1
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// @see https://github.com/stylelint/stylelint/blob/master/docs/developer-guide/plugins.md
const stylelint = require('stylelint');
const resolvedNestedSelector = require('postcss-resolve-nested-selector');
const extractCssClasses = require('css-selector-classes');
const extractCssClasses = require('./lib/css-selector-classes');
const util = require('util');

const ruleName = 'plugin/stylelint-bem-namics';
Expand Down
65 changes: 65 additions & 0 deletions lib/css-selector-classes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use strict';

const createParser = require('css-selector-parser').createParser;
const parse = createParser({ syntax: 'progressive' });

/* eslint-disable complexity */

/**
* Recursively visits nodes in a CSS rule tree and applies a function to each rule node.
*
* @param {Object} node - The root node of the CSS rule tree.
* @param {function} fn - The function to apply to each rule node.
* @returns {void}
*/
function visitRules(node, fn) {

if (!node) { return; }

if (node.rules) {
node.rules.forEach((rule) => visitRules(rule, fn));
}

if (node.nestedRule?.pseudoClasses) {
node.nestedRule.pseudoClasses.forEach((pseudo) => visitRules(pseudo.argument, fn));
}

if (node.attributes) {
const classAttribute = node.attributes.find((attribute) => attribute.name === 'class');
if (classAttribute?.value?.value) {
fn({ classNames: [classAttribute.value.value] });
}
}

if (node.pseudoClasses) {
node.pseudoClasses.forEach((pseudo) => {
if (pseudo.argument?.rules) {
pseudo.argument.rules.forEach((rule) => visitRules(rule, fn));
}
});
}

if (node.type === 'Rule') {
fn(node);
}
}
/* eslint-enable complexity */

/**
* Return all the classes in a CSS selector.
*
* @param {string} selector A CSS selector
* @returns {string[]} An array of every class present in the CSS selector
*/
function getCssSelectorClasses(selector) {
let list = [];
const ast = parse(selector);
visitRules(ast, (ruleSet) => {
if (ruleSet.classNames) {
list = list.concat(ruleSet.classNames);
}
});
return Array.from(new Set(list));
}

module.exports = getCssSelectorClasses;
75 changes: 31 additions & 44 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"author": "Merkle Inc.",
"license": "MIT",
"dependencies": {
"css-selector-classes": "0.1.2",
"css-selector-parser": "2.3.2",
"postcss-resolve-nested-selector": "0.1.1"
},
"peerDependencies": {
Expand Down
12 changes: 12 additions & 0 deletions test/edge-cases.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ testRule({
{
code: '@mixin specialCase { &:hover { } }',
},
// Should not conflict with unknown pseudo elements
{
code: '.m-search { &::-ms-clear { } &::-webkit-search-cancel-button { } }',
},
// Should not conflict with scss nested outer selector
{
code: `.a-button {
&:focus {
[data-whatintent="keyboard"] & {}
}
}`,
},
],
reject: [
{
Expand Down
81 changes: 81 additions & 0 deletions test/pseudo-classes.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* eslint max-len:off */
const { ruleName } = require('../index');

// Sass and Less edgecases
testRule({
ruleName,
config: {},
skipBasicChecks: true,
accept: [
// Should not conflict with :not pseudo class
{
code: '.h-block__element:not(:first-child) {}',
},
{
code: '.a-block:not(.a-block--link) {}',
},
{
code: '.h-block__element:not(.h-block__element--not) {}',
},
{
code: 'p > :not(strong, b.h-important) {}',
},
{
code: 'ul li:not(:last-of-type) {}',
},
// Should not conflict with :is pseudo class
{
code: ':is(ol, ul, menu, dir) :is(ol, ul, menu, dir) :is(ul, menu, dir) {}',
},
{
code: 'p > :is(strong, b.h-important, .h-strong) {}',
},
// Should not conflict with :has pseudo class
{
code: '.m-select:has(> .a-icon) {};',
},
{
code: 'h1:has(+ p.h-lead) {}',
},
// Should not conflict with combinations of pseudo classes
{
code: '.a-block:is(:not(a)) {}',
},
{
code: '.a-block:is(:not(.a-block--link)) {}',
},
{
code: '.a-block:is(:focus:not(.state-a-block--invisible-focus), :hover:not([disabled])) {}',
},
{
code: '.a-block:is(:not(.a-block--link)) {}',
},
{
code: '.a-block:is(:hover:not([disabled])) {}',
},
{
code: '.a-block:is(:focus:not(.a-block--invisible-focus), :hover:not([disabled])) {}',
},
{
code: '.m-select:has(> .a-icon:not(.a-icon--chevron)) {};',
},
],
reject: [
{
code: 'p > :not(strong, b.important, .h-strong) {}',
message: `Expected class name "important" to start with a valid prefix: "a-", "m-", "o-", "l-", "g-", "h-", "state-". (${ruleName})`,
},
{
code: '.a-block:is(:focus:not(.has-invisible-focus), :hover:not([disabled])) {}',
message: `Expected class name "has-invisible-focus" to start with a valid prefix: "a-", "m-", "o-", "l-", "g-", "h-", "state-". (${ruleName})`,
},
{
code: 'h1:has(+ p.lead) {}',
message: `Expected class name "lead" to start with a valid prefix: "a-", "m-", "o-", "l-", "g-", "h-", "state-". (${ruleName})`,
},
{
code: '.a-block:is(:not(.is-link)) {}',
message: `Expected class name "is-link" to start with a valid prefix: "a-", "m-", "o-", "l-", "g-", "h-", "state-". (${ruleName})`,
},
],
});

0 comments on commit a234961

Please sign in to comment.