From 903eacb1ee5dc9536a7c590a9585d9ca404936ae Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 16 Feb 2023 10:13:42 -0500 Subject: [PATCH] Fix use of `:where(.btn)` when matching `!btn` (#10601) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Cleanup code This makes it more explicit that we’re parsing a string selector, modifying it, and turning it back into a string * Fix important modifier when :where is involved * Only parse selector list once when handling the important modifier * Fix import * Fix lint errors --- src/lib/generateRules.js | 30 ++++++++++++++-------- src/util/formatVariantSelector.js | 2 +- src/util/pluginUtils.js | 39 ++++++++-------------------- tests/important-modifier.test.js | 42 +++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 40 deletions(-) diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index 0533a9e86c51..f7096e556602 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -3,10 +3,14 @@ import selectorParser from 'postcss-selector-parser' import parseObjectStyles from '../util/parseObjectStyles' import isPlainObject from '../util/isPlainObject' import prefixSelector from '../util/prefixSelector' -import { updateAllClasses, filterSelectorsForClass, getMatchingTypes } from '../util/pluginUtils' +import { updateAllClasses, getMatchingTypes } from '../util/pluginUtils' import log from '../util/log' import * as sharedState from './sharedState' -import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector' +import { + formatVariantSelector, + finalizeSelector, + eliminateIrrelevantSelectors, +} from '../util/formatVariantSelector' import { asClass } from '../util/nameClass' import { normalize } from '../util/dataTypes' import { isValidVariantFormatString, parseVariant } from './setupContextUtils' @@ -111,22 +115,28 @@ function applyImportant(matches, classCandidate) { if (matches.length === 0) { return matches } + let result = [] for (let [meta, rule] of matches) { let container = postcss.root({ nodes: [rule.clone()] }) + container.walkRules((r) => { - r.selector = updateAllClasses( - filterSelectorsForClass(r.selector, classCandidate), - (className) => { - if (className === classCandidate) { - return `!${className}` - } - return className - } + let ast = selectorParser().astSync(r.selector) + + // Remove extraneous selectors that do not include the base candidate + ast.each((sel) => eliminateIrrelevantSelectors(sel, classCandidate)) + + // Update all instances of the base candidate to include the important marker + updateAllClasses(ast, (className) => + className === classCandidate ? `!${className}` : className ) + + r.selector = ast.toString() + r.walkDecls((d) => (d.important = true)) }) + result.push([{ ...meta, important: true }, container.nodes[0]]) } diff --git a/src/util/formatVariantSelector.js b/src/util/formatVariantSelector.js index ee6350b8ef33..da5e2c78dfbc 100644 --- a/src/util/formatVariantSelector.js +++ b/src/util/formatVariantSelector.js @@ -120,7 +120,7 @@ function resortSelector(sel) { * @param {Selector} ast * @param {string} base */ -function eliminateIrrelevantSelectors(sel, base) { +export function eliminateIrrelevantSelectors(sel, base) { let hasClassesMatchingCandidate = false sel.walk((child) => { diff --git a/src/util/pluginUtils.js b/src/util/pluginUtils.js index 5c4821d3cbca..ecaaf0c90870 100644 --- a/src/util/pluginUtils.js +++ b/src/util/pluginUtils.js @@ -1,4 +1,3 @@ -import selectorParser from 'postcss-selector-parser' import escapeCommas from './escapeCommas' import { withAlphaValue } from './withAlphaVariable' import { @@ -21,37 +20,19 @@ import negateValue from './negateValue' import { backgroundSize } from './validateFormalSyntax' import { flagEnabled } from '../featureFlags.js' +/** + * @param {import('postcss-selector-parser').Container} selectors + * @param {(className: string) => string} updateClass + * @returns {string} + */ export function updateAllClasses(selectors, updateClass) { - let parser = selectorParser((selectors) => { - selectors.walkClasses((sel) => { - let updatedClass = updateClass(sel.value) - sel.value = updatedClass - if (sel.raws && sel.raws.value) { - sel.raws.value = escapeCommas(sel.raws.value) - } - }) - }) - - let result = parser.processSync(selectors) + selectors.walkClasses((sel) => { + sel.value = updateClass(sel.value) - return result -} - -export function filterSelectorsForClass(selectors, classCandidate) { - let parser = selectorParser((selectors) => { - selectors.each((sel) => { - const containsClass = sel.nodes.some( - (node) => node.type === 'class' && node.value === classCandidate - ) - if (!containsClass) { - sel.remove() - } - }) + if (sel.raws && sel.raws.value) { + sel.raws.value = escapeCommas(sel.raws.value) + } }) - - let result = parser.processSync(selectors) - - return result } function resolveArbitraryValue(modifier, validate) { diff --git a/tests/important-modifier.test.js b/tests/important-modifier.test.js index bde7eee2ea84..6d30fd0bceb3 100644 --- a/tests/important-modifier.test.js +++ b/tests/important-modifier.test.js @@ -108,4 +108,46 @@ crosscheck(() => { `) }) }) + + test('the important modifier works on utilities using :where()', () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + plugins: [ + function ({ addComponents }) { + addComponents({ + ':where(.btn)': { + backgroundColor: '#00f', + }, + }) + }, + ], + } + + let input = css` + @tailwind components; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + :where(.\!btn) { + background-color: #00f !important; + } + :where(.btn) { + background-color: #00f; + } + :where(.hover\:btn:hover) { + background-color: #00f; + } + :where(.hover\:focus\:disabled\:\!btn:disabled:focus:hover) { + background-color: #00f !important; + } + `) + }) + }) })