diff --git a/lib/style.js b/lib/style.js index 865daf299..29e4a5388 100644 --- a/lib/style.js +++ b/lib/style.js @@ -14,6 +14,7 @@ */ const csstree = require('css-tree'); +const csswhat = require('css-what'); const { // @ts-ignore internal api syntax: { specificity }, @@ -278,20 +279,55 @@ const computeStyle = (stylesheet, node) => { exports.computeStyle = computeStyle; /** - * Determines if the CSS selector references the given attribute. + * Determines if the CSS selector includes or traverses the given attribute. + * + * Classes and IDs are generated as attribute selectors, so you can check for + * if a `.class` or `#id` is included by passing `name=class` or `name=id` + * respectively. * * @param {csstree.ListItem|string} selector - * @param {string} attr + * @param {string} name + * @param {?string} value + * @param {boolean} traversed * @returns {boolean} - * @see https://developer.mozilla.org/docs/Web/CSS/Attribute_selectors */ -const includesAttrSelector = (selector, attr) => { - const attrSelectorPattern = new RegExp(`\\[\\s*${attr}\\s*[\\]=~|^$*]`, 'i'); +const includesAttrSelector = ( + selector, + name, + value = null, + traversed = false +) => { + const selectors = + typeof selector === 'string' + ? csswhat.parse(selector) + : csswhat.parse(csstree.generate(selector.data)); + + for (const subselector of selectors) { + const hasAttrSelector = subselector.some((segment, index) => { + if (traversed) { + if (index === subselector.length - 1) { + return false; + } - if (typeof selector === 'string') { - return attrSelectorPattern.test(attr); + const isNextTraversal = csswhat.isTraversal(subselector[index + 1]); + + if (!isNextTraversal) { + return false; + } + } + + if (segment.type !== 'attribute' || segment.name !== name) { + return false; + } + + return value == null ? true : segment.value === value; + }); + + if (hasAttrSelector) { + return true; + } } - return attrSelectorPattern.test(csstree.generate(selector.data)); + return false; }; exports.includesAttrSelector = includesAttrSelector; diff --git a/package.json b/package.json index 27c72e969..6415c95ac 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,7 @@ "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.2.1", + "css-what": "^6.1.0", "csso": "5.0.5", "picocolors": "^1.0.0" }, diff --git a/plugins/inlineStyles.js b/plugins/inlineStyles.js index cd06d0ca3..7a4a13609 100644 --- a/plugins/inlineStyles.js +++ b/plugins/inlineStyles.js @@ -299,7 +299,12 @@ exports.fn = (root, params) => { ); for (const child of selector.node.children) { - if (child.type === 'ClassSelector') { + if ( + child.type === 'ClassSelector' && + !selectors.some((selector) => + includesAttrSelector(selector.item, 'class', child.name, true) + ) + ) { classList.delete(child.name); } } diff --git a/test/plugins/inlineStyles.26.svg b/test/plugins/inlineStyles.26.svg new file mode 100644 index 000000000..fc093d0ec --- /dev/null +++ b/test/plugins/inlineStyles.26.svg @@ -0,0 +1,32 @@ +Don't remove the class from a wrapper element if it's traversed in another +selector. + +=== + + + + + + + + + + +@@@ + + + + + + + + diff --git a/yarn.lock b/yarn.lock index bf9ad077b..9b6bf521c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4568,6 +4568,7 @@ __metadata: commander: ^7.2.0 css-select: ^5.1.0 css-tree: ^2.2.1 + css-what: ^6.1.0 csso: 5.0.5 del: ^6.0.0 eslint: ^8.24.0