From 47cdab55b9479f27c8209f1ac6501ba4031b6f94 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 5 Sep 2022 11:20:28 -0400 Subject: [PATCH] wip --- src/util/formatVariantSelector.js | 91 ++++++++++++++++++++++++++++++- tests/variants.test.js | 54 ++++++++++++++++++ 2 files changed, 143 insertions(+), 2 deletions(-) diff --git a/src/util/formatVariantSelector.js b/src/util/formatVariantSelector.js index 3c2e1f9db733..47d9ade7a4fc 100644 --- a/src/util/formatVariantSelector.js +++ b/src/util/formatVariantSelector.js @@ -29,6 +29,58 @@ export function formatVariantSelector(current, ...others) { return current } +/** + * Given any node in a selector this gets the "simple" selector it's a part of + * A simple selector is just a list of nodes without any combinators + * Technically :is(), :not(), :has(), etc… can have combinators but those are nested + * inside the relevant node and won't be picked up so they're fine to ignore + * + * @param {import('postcss-selector-parser').Node} node + * @returns {import('postcss-selector-parser').Node[]} + **/ +function simpleSelectorForNode(node) { + /** @type {import('postcss-selector-parser').Node[]} */ + let nodes = [] + + // Walk backwards until we hit a combinator node (or the start) + while (node.prev() && node.prev().type !== 'combinator') { + node = node.prev() + } + + // Now record all non-combinator nodes until we hit one (or the end) + while (node && node.type !== 'combinator') { + nodes.push(node) + node = node.next() + } + + return nodes +} + +/** + * Resorts the nodes in a selector to ensure they're in the correct order + * Tags go before classes, and pseudo classes go after classes + * + * @param {import('postcss-selector-parser').Selector} sel + * @returns {import('postcss-selector-parser').Selector} + **/ +function resortSelector(sel) { + sel.sort((a, b) => { + if (a.type === 'tag' && b.type === 'class') { + return -1 + } else if (a.type === 'class' && b.type === 'tag') { + return 1 + } else if (a.type === 'class' && b.type === 'pseudo' && b.value !== ':merge') { + return -1 + } else if (a.type === 'pseudo' && a.value !== ':merge' && b.type === 'class') { + return 1 + } + + return sel.index(a) - sel.index(b) + }) + + return sel +} + export function finalizeSelector( format, { @@ -88,12 +140,47 @@ export function finalizeSelector( } }) + let simpleStart = selectorParser.comment({ value: '/*__simple__*/' }) + let simpleEnd = selectorParser.comment({ value: '/*__simple__*/' }) + // We can safely replace the escaped base now, since the `base` section is // now in a normalized escaped value. ast.walkClasses((node) => { - if (node.value === base) { - node.replaceWith(...formatAst.nodes) + if (node.value !== base) { + return + } + + let parent = node.parent + let formatNodes = formatAst.nodes[0].nodes + + // Perf optimization: if the parent is a single class we can just replace it and be done + if (parent.nodes.length === 1) { + node.replaceWith(...formatNodes) + return } + + let simpleSelector = simpleSelectorForNode(node) + parent.insertBefore(simpleSelector[0], simpleStart) + parent.insertAfter(simpleSelector[simpleSelector.length - 1], simpleEnd) + + for (let child of formatNodes) { + parent.insertBefore(simpleSelector[0], child) + } + + node.remove() + + // Re-sort the simple selector to ensure it's in the correct order + simpleSelector = simpleSelectorForNode(simpleStart) + let firstNode = parent.index(simpleStart) + + parent.nodes.splice( + firstNode, + simpleSelector.length, + ...resortSelector(selectorParser.selector({ nodes: simpleSelector })).nodes + ) + + simpleStart.remove() + simpleEnd.remove() }) // This will make sure to move pseudo's to the correct spot (the end for diff --git a/tests/variants.test.js b/tests/variants.test.js index 06fc18456304..c03a11c3117b 100644 --- a/tests/variants.test.js +++ b/tests/variants.test.js @@ -855,3 +855,57 @@ test('hoverOnlyWhenSupported adds hover and pointer media features by default', `) }) }) + +test('multi-class utilities handle selector-mutating variants correctly', () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind utilities; + @layer utilities { + .foo.bar.baz { + color: red; + } + } + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .hover\:foo.bar.baz:hover { + color: red; + } + .foo.hover\:bar.baz:hover { + color: red; + } + .foo.bar.hover\:baz:hover { + color: red; + } + .group:hover .group-hover\:foo.bar.baz { + color: red; + } + .group:hover .foo.group-hover\:bar.baz { + color: red; + } + .group:hover .foo.bar.group-hover\:baz { + color: red; + } + .peer:checked ~ .peer-checked\:foo.bar.baz { + color: red; + } + .peer:checked ~ .foo.peer-checked\:bar.baz { + color: red; + } + .peer:checked ~ .foo.bar.peer-checked\:baz { + color: red; + } + `) + }) +})