From 08b177fcc8a870275ba23717b8b1f8e14353b628 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 8 May 2022 00:29:05 +0200 Subject: [PATCH 01/18] register arbitrary variants With the new `addVariant` API, we have a beautiful way of creating new variants. You can use it as: ```js addVariant('children', '& > *') ``` Now you can use the `children:` variant. The API uses a `&` as a reference for the candidate, which means that: ```html children:pl-4 ``` Will result in: ```css .children\:pl-4 > * { .. } ``` Notice that the `&` was replaced by `.children\:pl-4`. We can leverage this API to implement arbitrary variants, this means that you can write those `&>*` (Notice that we don't have spaces) inside a variant directly. An example of this can be: ```html ``` Which generates the following css: ```css .\[\&\>\*\]\:underline > * { text-decoration-line: underline; } ``` Now all the children of the `ul` will have an `underline`. The selector itself is a bit crazy since it contains the candidate which is the selector itself, it is just escaped. --- src/lib/generateRules.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index bbed6812eb48..74754b85ecbc 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -125,6 +125,14 @@ function applyVariant(variant, matches, context) { return matches } + // Register arbitrary variants + if (isArbitraryValue(variant) && !context.variantMap.has(variant)) { + let selector = normalize(variant.slice(1, -1)) + let sort = Array.from(context.variantOrder.values()).pop() << 1n + context.variantMap.set(variant, [[sort, () => selector]]) + context.variantOrder.set(variant, sort) + } + if (context.variantMap.has(variant)) { let variantFunctionTuples = context.variantMap.get(variant) let result = [] From e0700797a0734c475bb505965a4ef2f0c3077e1b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 8 May 2022 00:52:38 +0200 Subject: [PATCH 02/18] add tests for arbitrary variants This still requires some work to the `defaultExtractor` to make sure it all works with existing code. --- src/lib/defaultExtractor.js | 2 +- tests/arbitrary-variants.test.js | 134 +++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 tests/arbitrary-variants.test.js diff --git a/src/lib/defaultExtractor.js b/src/lib/defaultExtractor.js index 523d9bab3dad..c64c8bfd57e0 100644 --- a/src/lib/defaultExtractor.js +++ b/src/lib/defaultExtractor.js @@ -19,7 +19,7 @@ export function defaultExtractor(content) { function* buildRegExps() { yield regex.pattern([ // Variants - /((?=([^\s"'\\\[]+:))\2)?/, + /((?=([^\s"'\\]+:))\2)?/, // Important (optional) /!?/, diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js new file mode 100644 index 000000000000..1d75cfe86b8f --- /dev/null +++ b/tests/arbitrary-variants.test.js @@ -0,0 +1,134 @@ +import { run, html, css, defaults } from './util/run' + +test('basic arbitrary variants', () => { + let config = { + content: [{ raw: html`
` }], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind components; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + ${defaults} + + .\[\&\>\*\]\:underline > * { + text-decoration-line: underline; + } + `) + }) +}) + +test('spaces in selector (using _)', () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind components; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + ${defaults} + + .a.b .\[\.a\.b_\&\]\:underline { + text-decoration-line: underline; + } + `) + }) +}) + +test('arbitrary variants with modifiers', () => { + let config = { + content: [{ raw: html`
` }], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind components; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + ${defaults} + + @media (prefers-color-scheme: dark) { + @media (min-width: 1024px) { + .dark\:lg\:hover\:\[\&\>\*\]\:underline > *:hover { + text-decoration-line: underline; + } + } + } + `) + }) +}) + +test('arbitrary variants are sorted after other variants', () => { + let config = { + content: [{ raw: html`
` }], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind components; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + ${defaults} + + .underline { + text-decoration-line: underline; + } + + @media (min-width: 1024px) { + .lg\:underline { + text-decoration-line: underline; + } + } + + .\[\&\>\*\]\:underline > * { + text-decoration-line: underline; + } + `) + }) +}) + +test('using the important modifier', () => { + let config = { + content: [{ raw: html`
` }], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind components; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + ${defaults} + + .\[\&\>\*\]\:\!underline > * { + text-decoration-line: underline !important; + } + `) + }) +}) From 7d49e55ace9b283c97815d1042bd8aaaf5a629ac Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 8 May 2022 00:55:28 +0200 Subject: [PATCH 03/18] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e940623fa51..9d3cca93636c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `backdrop` variant ([#7924](https://github.com/tailwindlabs/tailwindcss/pull/7924)) - Add `grid-flow-dense` utility ([#8193](https://github.com/tailwindlabs/tailwindcss/pull/8193)) - Add `mix-blend-plus-lighter` utility ([#8288](https://github.com/tailwindlabs/tailwindcss/pull/8288)) +- Add arbitrary variants ([#8299](https://github.com/tailwindlabs/tailwindcss/pull/8299)) ## [3.0.24] - 2022-04-12 From 623718eb2d8023261366969026dc038bd14a4959 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Sat, 7 May 2022 20:49:14 -0400 Subject: [PATCH 04/18] Fix candidate detection for arbitrary variants --- src/lib/defaultExtractor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/defaultExtractor.js b/src/lib/defaultExtractor.js index c64c8bfd57e0..b16f72607dad 100644 --- a/src/lib/defaultExtractor.js +++ b/src/lib/defaultExtractor.js @@ -19,7 +19,7 @@ export function defaultExtractor(content) { function* buildRegExps() { yield regex.pattern([ // Variants - /((?=([^\s"'\\]+:))\2)?/, + /((?=((\[[^\s"'\[\]\\]+\]:|[^\s"'\[\\]+:)+))\2)?/, // Important (optional) /!?/, From 42296f2bef45610424fc1aef80cf78de28cb80df Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Sat, 7 May 2022 22:48:36 -0400 Subject: [PATCH 05/18] Refactor --- src/lib/setupContextUtils.js | 46 ++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index f2dc6a516b2e..4215e0790ded 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -170,6 +170,30 @@ function withIdentifiers(styles) { }) } +export function parseVariant(variant) { + variant = variant + .replace(/\n+/g, '') + .replace(/\s{1,}/g, ' ') + .trim() + + let fns = parseVariantFormatString(variant) + .map((str) => { + if (!str.startsWith('@')) { + return ({ format }) => format(str) + } + + let [, name, params] = /@(.*?) (.*)/g.exec(str) + return ({ wrap }) => wrap(postcss.atRule({ name, params })) + }) + .reverse() + + return (api) => { + for (let fn of fns) { + fn(api) + } + } +} + function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offsets, classList }) { function getConfigValue(path, defaultValue) { return path ? dlv(tailwindConfig, path, defaultValue) : tailwindConfig @@ -201,27 +225,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs } } - variantFunction = variantFunction - .replace(/\n+/g, '') - .replace(/\s{1,}/g, ' ') - .trim() - - let fns = parseVariantFormatString(variantFunction) - .map((str) => { - if (!str.startsWith('@')) { - return ({ format }) => format(str) - } - - let [, name, params] = /@(.*?) (.*)/g.exec(str) - return ({ wrap }) => wrap(postcss.atRule({ name, params })) - }) - .reverse() - - return (api) => { - for (let fn of fns) { - fn(api) - } - } + return parseVariant(variantFunction) }) insertInto(variantList, variantName, options) From 02404ac809cd925ac03aa889a7e1b396592aa1a4 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Sat, 7 May 2022 22:48:44 -0400 Subject: [PATCH 06/18] Add support for at rules --- src/lib/generateRules.js | 7 ++++++- tests/arbitrary-variants.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index 74754b85ecbc..535263cb4b15 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -9,6 +9,7 @@ import * as sharedState from './sharedState' import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector' import { asClass } from '../util/nameClass' import { normalize } from '../util/dataTypes' +import { parseVariant } from './setupContextUtils' import isValidArbitraryValue from '../util/isValidArbitraryValue' let classNameParser = selectorParser((selectors) => { @@ -128,8 +129,12 @@ function applyVariant(variant, matches, context) { // Register arbitrary variants if (isArbitraryValue(variant) && !context.variantMap.has(variant)) { let selector = normalize(variant.slice(1, -1)) + + // TODO: Error recovery for @supports(what:ever) -- note the absence of a space + let fn = parseVariant(selector) + let sort = Array.from(context.variantOrder.values()).pop() << 1n - context.variantMap.set(variant, [[sort, () => selector]]) + context.variantMap.set(variant, [[sort, fn]]) context.variantOrder.set(variant, sort) } diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js index 1d75cfe86b8f..f18e32c433e2 100644 --- a/tests/arbitrary-variants.test.js +++ b/tests/arbitrary-variants.test.js @@ -132,3 +132,28 @@ test('using the important modifier', () => { `) }) }) + +test('at-rules', () => { + let config = { + content: [{ raw: html`
` }], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind components; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + ${defaults} + + @supports (what: ever) { + .\[\@supports_\(what\:ever\)\{\&\:hover\}\]\:underline:hover { + text-decoration-line: underline; + } + } + `) + }) +}) From 3e24b7524a7d461ae2ea8b3ea1997adc3d1b4d1e Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Sun, 8 May 2022 06:23:43 -0400 Subject: [PATCH 07/18] Add test for attribute selectors --- tests/arbitrary-variants.test.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js index f18e32c433e2..abcc505fe11a 100644 --- a/tests/arbitrary-variants.test.js +++ b/tests/arbitrary-variants.test.js @@ -157,3 +157,26 @@ test('at-rules', () => { `) }) }) + +test('attribute selectors', () => { + let config = { + content: [{ raw: html`
` }], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind components; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + ${defaults} + + .\[\&\:data-open\]\:underline > * { + text-decoration-line: underline; + } + `) + }) +}) From 2bd6fba9499a4102c11a1a876266959115fc8b81 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Sun, 8 May 2022 07:13:42 -0400 Subject: [PATCH 08/18] Fix test --- tests/arbitrary-variants.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js index abcc505fe11a..348cc1702404 100644 --- a/tests/arbitrary-variants.test.js +++ b/tests/arbitrary-variants.test.js @@ -160,7 +160,7 @@ test('at-rules', () => { test('attribute selectors', () => { let config = { - content: [{ raw: html`
` }], + content: [{ raw: html`
` }], corePlugins: { preflight: false }, } @@ -174,7 +174,7 @@ test('attribute selectors', () => { expect(result.css).toMatchFormattedCss(css` ${defaults} - .\[\&\:data-open\]\:underline > * { + .\[\&\[data-open\]\]\:underline[data-open] { text-decoration-line: underline; } `) From a96eb72fa7adff4df5ee60515218d66acec64345 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Sun, 8 May 2022 07:13:50 -0400 Subject: [PATCH 09/18] Add attribute selector support --- src/lib/defaultExtractor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/defaultExtractor.js b/src/lib/defaultExtractor.js index b16f72607dad..c75dca2d61b4 100644 --- a/src/lib/defaultExtractor.js +++ b/src/lib/defaultExtractor.js @@ -19,7 +19,7 @@ export function defaultExtractor(content) { function* buildRegExps() { yield regex.pattern([ // Variants - /((?=((\[[^\s"'\[\]\\]+\]:|[^\s"'\[\\]+:)+))\2)?/, + /((?=((\[[^\s"'\\]+\]:|[^\s"'\[\\]+:)+))\2)?/, // Important (optional) /!?/, From 8d2f6bdf6bb9c752323d6c37bdde8bdbf2049b74 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Sun, 8 May 2022 07:38:55 -0400 Subject: [PATCH 10/18] Split top-level comma parsing into a generalized splitting routine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We can now split on any character at the top level with any nesting. We don’t balance brackets directly here but this is probably “enough” --- src/util/parseBoxShadowValue.js | 53 ++------------------------------- src/util/splitAtTopLevelOnly.js | 50 +++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 50 deletions(-) create mode 100644 src/util/splitAtTopLevelOnly.js diff --git a/src/util/parseBoxShadowValue.js b/src/util/parseBoxShadowValue.js index 0806ec699b88..16fc8eb1b03a 100644 --- a/src/util/parseBoxShadowValue.js +++ b/src/util/parseBoxShadowValue.js @@ -1,58 +1,11 @@ +import { splitAtTopLevelOnly } from './splitAtTopLevelOnly' + let KEYWORDS = new Set(['inset', 'inherit', 'initial', 'revert', 'unset']) let SPACE = /\ +(?![^(]*\))/g // Similar to the one above, but with spaces instead. let LENGTH = /^-?(\d+|\.\d+)(.*?)$/g -let SPECIALS = /[(),]/g - -/** - * This splits a string on top-level commas. - * - * Regex doesn't support recursion (at least not the JS-flavored version). - * So we have to use a tiny state machine to keep track of paren vs comma - * placement. Before we'd only exclude commas from the inner-most nested - * set of parens rather than any commas that were not contained in parens - * at all which is the intended behavior here. - * - * Expected behavior: - * var(--a, 0 0 1px rgb(0, 0, 0)), 0 0 1px rgb(0, 0, 0) - * ─┬─ ┬ ┬ ┬ - * x x x ╰──────── Split because top-level - * ╰──────────────┴──┴───────────── Ignored b/c inside >= 1 levels of parens - * - * @param {string} input - */ -function* splitByTopLevelCommas(input) { - SPECIALS.lastIndex = -1 - - let depth = 0 - let lastIndex = 0 - let found = false - - // Find all parens & commas - // And only split on commas if they're top-level - for (let match of input.matchAll(SPECIALS)) { - if (match[0] === '(') depth++ - if (match[0] === ')') depth-- - if (match[0] === ',' && depth === 0) { - found = true - - yield input.substring(lastIndex, match.index) - lastIndex = match.index + match[0].length - } - } - - // Provide the last segment of the string if available - // Otherwise the whole string since no commas were found - // This mirrors the behavior of string.split() - if (found) { - yield input.substring(lastIndex) - } else { - yield input - } -} - export function parseBoxShadowValue(input) { - let shadows = Array.from(splitByTopLevelCommas(input)) + let shadows = Array.from(splitAtTopLevelOnly(input, ',')) return shadows.map((shadow) => { let value = shadow.trim() let result = { raw: value } diff --git a/src/util/splitAtTopLevelOnly.js b/src/util/splitAtTopLevelOnly.js new file mode 100644 index 000000000000..db10804277e3 --- /dev/null +++ b/src/util/splitAtTopLevelOnly.js @@ -0,0 +1,50 @@ +import * as regex from '../lib/regex' + +/** + * This splits a string on a top-level character. + * + * Regex doesn't support recursion (at least not the JS-flavored version). + * So we have to use a tiny state machine to keep track of paren placement. + * + * Expected behavior using commas: + * var(--a, 0 0 1px rgb(0, 0, 0)), 0 0 1px rgb(0, 0, 0) + * ─┬─ ┬ ┬ ┬ + * x x x ╰──────── Split because top-level + * ╰──────────────┴──┴───────────── Ignored b/c inside >= 1 levels of parens + * + * @param {string} input + * @param {string} char + */ +export function* splitAtTopLevelOnly(input, char) { + let SPECIALS = new RegExp(`[(){}\\[\\]${regex.escape(char)}]`, 'g') + + let depth = 0 + let lastIndex = 0 + let found = false + + // Find all paren-like things & character + // And only split on commas if they're top-level + for (let match of input.matchAll(SPECIALS)) { + if (match[0] === '(') depth++ + if (match[0] === ')') depth-- + if (match[0] === '[') depth++ + if (match[0] === ']') depth-- + if (match[0] === '{') depth++ + if (match[0] === '}') depth-- + if (match[0] === char && depth === 0) { + found = true + + yield input.substring(lastIndex, match.index) + lastIndex = match.index + match[0].length + } + } + + // Provide the last segment of the string if available + // Otherwise the whole string since no `char`s were found + // This mirrors the behavior of string.split() + if (found) { + yield input.substring(lastIndex) + } else { + yield input + } +} From 52baafc02095c313e1f1c232780953ce364428bb Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Sun, 8 May 2022 07:39:41 -0400 Subject: [PATCH 11/18] Split variants by separator at the top-level only This means that the separator has to be ouside of balanced brackets --- src/lib/generateRules.js | 3 ++- tests/arbitrary-variants.test.js | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index 535263cb4b15..d039a78017b3 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -11,6 +11,7 @@ import { asClass } from '../util/nameClass' import { normalize } from '../util/dataTypes' import { parseVariant } from './setupContextUtils' import isValidArbitraryValue from '../util/isValidArbitraryValue' +import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly.js' let classNameParser = selectorParser((selectors) => { return selectors.first.filter(({ type }) => type === 'class').pop().value @@ -420,7 +421,7 @@ function splitWithSeparator(input, separator) { return [sharedState.NOT_ON_DEMAND] } - return input.split(new RegExp(`\\${separator}(?![^[]*\\])`, 'g')) + return Array.from(splitAtTopLevelOnly(input, separator)) } function* recordCandidates(matches, classCandidate) { diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js index 348cc1702404..e98dfb8a6af4 100644 --- a/tests/arbitrary-variants.test.js +++ b/tests/arbitrary-variants.test.js @@ -180,3 +180,26 @@ test('attribute selectors', () => { `) }) }) + +test('multiple attribute selectors', () => { + let config = { + content: [{ raw: html`
` }], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind components; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + ${defaults} + + .\[\&\[data-foo\]\[data-bar\]\:not\(\[data-baz\]\)\]\:underline[data-foo][data-bar]:not([data-baz]) { + text-decoration-line: underline; + } + `) + }) +}) From fa829f61d8d4baa8b3545133577bdbfc7fd7971d Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Sun, 8 May 2022 08:18:23 -0400 Subject: [PATCH 12/18] Fix extraction when using custom variant separators --- src/lib/defaultExtractor.js | 37 ++++++++++++++++++++------------ src/lib/expandTailwindAtRules.js | 2 +- tests/default-extractor.test.js | 9 +++++++- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/lib/defaultExtractor.js b/src/lib/defaultExtractor.js index c75dca2d61b4..ff8c6dd50dc3 100644 --- a/src/lib/defaultExtractor.js +++ b/src/lib/defaultExtractor.js @@ -1,25 +1,34 @@ import * as regex from './regex' -let patterns = Array.from(buildRegExps()) - -/** - * @param {string} content - */ -export function defaultExtractor(content) { - /** @type {(string|string)[]} */ - let results = [] +export function defaultExtractor(context) { + let patterns = Array.from(buildRegExps(context)) + + /** + * @param {string} content + */ + return (content) => { + /** @type {(string|string)[]} */ + let results = [] + + for (let pattern of patterns) { + results.push(...(content.match(pattern) ?? [])) + } - for (let pattern of patterns) { - results.push(...(content.match(pattern) ?? [])) + return results.filter((v) => v !== undefined).map(clipAtBalancedParens) } - - return results.filter((v) => v !== undefined).map(clipAtBalancedParens) } -function* buildRegExps() { +function* buildRegExps(context) { + let separator = context.tailwindConfig.separator + yield regex.pattern([ // Variants - /((?=((\[[^\s"'\\]+\]:|[^\s"'\[\\]+:)+))\2)?/, + '((?=((', + regex.any( + [regex.pattern([/\[[^\s"'\\]+\]/, separator]), regex.pattern([/[^\s"'\[\\]+/, separator])], + true + ), + ')+))\\2)?', // Important (optional) /!?/, diff --git a/src/lib/expandTailwindAtRules.js b/src/lib/expandTailwindAtRules.js index 64ba07131e3e..a0c1635a0fba 100644 --- a/src/lib/expandTailwindAtRules.js +++ b/src/lib/expandTailwindAtRules.js @@ -24,7 +24,7 @@ function getExtractor(context, fileExtension) { extractors[fileExtension] || extractors.DEFAULT || builtInExtractors[fileExtension] || - builtInExtractors.DEFAULT + builtInExtractors.DEFAULT(context) ) } diff --git a/tests/default-extractor.test.js b/tests/default-extractor.test.js index b6e0b6789d4a..789f2eb5d685 100644 --- a/tests/default-extractor.test.js +++ b/tests/default-extractor.test.js @@ -1,5 +1,5 @@ import { html } from './util/run' -import { defaultExtractor } from '../src/lib/defaultExtractor' +import { defaultExtractor as createDefaultExtractor } from '../src/lib/defaultExtractor' const jsExamples = ` document.body.classList.add(["pl-1.5"].join(" ")); @@ -163,6 +163,13 @@ const excludes = [ `test`, ] +let defaultExtractor + +beforeEach(() => { + let context = { tailwindConfig: { separator: ':' } } + defaultExtractor = createDefaultExtractor(context) +}) + test('The default extractor works as expected', async () => { const extractions = defaultExtractor([jsExamples, jsxExamples, htmlExamples].join('\n').trim()) From 0e04444db9499883cca6b49f36b6bd1b58b9a854 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Sun, 8 May 2022 08:18:39 -0400 Subject: [PATCH 13/18] Support custom separators when top-level splitting variants --- src/util/splitAtTopLevelOnly.js | 33 ++++++++++++++++++++++++++------ tests/arbitrary-variants.test.js | 26 +++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/util/splitAtTopLevelOnly.js b/src/util/splitAtTopLevelOnly.js index db10804277e3..8297a6feca4e 100644 --- a/src/util/splitAtTopLevelOnly.js +++ b/src/util/splitAtTopLevelOnly.js @@ -13,29 +13,50 @@ import * as regex from '../lib/regex' * ╰──────────────┴──┴───────────── Ignored b/c inside >= 1 levels of parens * * @param {string} input - * @param {string} char + * @param {string} separator */ -export function* splitAtTopLevelOnly(input, char) { - let SPECIALS = new RegExp(`[(){}\\[\\]${regex.escape(char)}]`, 'g') +export function* splitAtTopLevelOnly(input, separator) { + let SPECIALS = new RegExp(`[(){}\\[\\]${regex.escape(separator)}]`, 'g') let depth = 0 let lastIndex = 0 let found = false + let separatorIndex = 0 + let separatorStart = 0 + let separatorLength = separator.length // Find all paren-like things & character // And only split on commas if they're top-level for (let match of input.matchAll(SPECIALS)) { + let matchesSeparator = match[0] === separator[separatorIndex] + let atEndOfSeparator = separatorIndex === separatorLength - 1 + let matchesFullSeparator = matchesSeparator && atEndOfSeparator + if (match[0] === '(') depth++ if (match[0] === ')') depth-- if (match[0] === '[') depth++ if (match[0] === ']') depth-- if (match[0] === '{') depth++ if (match[0] === '}') depth-- - if (match[0] === char && depth === 0) { + + if (matchesSeparator && depth === 0) { + if (separatorStart === 0) { + separatorStart = match.index + } + + separatorIndex++ + } + + if (matchesFullSeparator && depth === 0) { found = true - yield input.substring(lastIndex, match.index) - lastIndex = match.index + match[0].length + yield input.substring(lastIndex, separatorStart) + lastIndex = separatorStart + separatorLength + } + + if (separatorIndex === separatorLength) { + separatorIndex = 0 + separatorStart = 0 } } diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js index e98dfb8a6af4..625083912432 100644 --- a/tests/arbitrary-variants.test.js +++ b/tests/arbitrary-variants.test.js @@ -203,3 +203,29 @@ test('multiple attribute selectors', () => { `) }) }) + +test('multiple attribute selectors with custom separator', () => { + let config = { + separator: '__', + content: [ + { raw: html`
` }, + ], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind components; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + ${defaults} + + .\[\&\[data-foo\]\[data-bar\]\:not\(\[data-baz\]\)\]__underline[data-foo][data-bar]:not([data-baz]) { + text-decoration-line: underline; + } + `) + }) +}) From 3236eb9f645211a4a49e0ac60d67477733810abf Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Sun, 8 May 2022 08:19:50 -0400 Subject: [PATCH 14/18] Add a second multi-character separator test --- tests/arbitrary-variants.test.js | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js index 625083912432..43fcda87bf5a 100644 --- a/tests/arbitrary-variants.test.js +++ b/tests/arbitrary-variants.test.js @@ -204,7 +204,7 @@ test('multiple attribute selectors', () => { }) }) -test('multiple attribute selectors with custom separator', () => { +test('multiple attribute selectors with custom separator (1)', () => { let config = { separator: '__', content: [ @@ -229,3 +229,29 @@ test('multiple attribute selectors with custom separator', () => { `) }) }) + +test('multiple attribute selectors with custom separator (2)', () => { + let config = { + separator: '_@', + content: [ + { raw: html`
` }, + ], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind components; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + ${defaults} + + .\[\&\[data-foo\]\[data-bar\]\:not\(\[data-baz\]\)\]_\@underline[data-foo][data-bar]:not([data-baz]) { + text-decoration-line: underline; + } + `) + }) +}) From 00a79e64479c64a17380a7ee1e742d85f074766e Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Sun, 8 May 2022 09:20:59 -0400 Subject: [PATCH 15/18] Split tests for at-rule and at-rule with selector changes --- tests/arbitrary-variants.test.js | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js index 43fcda87bf5a..de77b0eea8cd 100644 --- a/tests/arbitrary-variants.test.js +++ b/tests/arbitrary-variants.test.js @@ -135,7 +135,7 @@ test('using the important modifier', () => { test('at-rules', () => { let config = { - content: [{ raw: html`
` }], + content: [{ raw: html`
` }], corePlugins: { preflight: false }, } @@ -150,7 +150,32 @@ test('at-rules', () => { ${defaults} @supports (what: ever) { - .\[\@supports_\(what\:ever\)\{\&\:hover\}\]\:underline:hover { + .\[\@supports_\(what\:ever\)\]\:underline { + text-decoration-line: underline; + } + } + `) + }) +}) + +test('at-rules with selector modifications', () => { + let config = { + content: [{ raw: html`
` }], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind components; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + ${defaults} + + @media (hover: hover) { + .\[\@media_\(hover\:hover\)\{\&\:hover\}\]\:underline:hover { text-decoration-line: underline; } } From a4cff3c2760ffad6efebc757520b337272bf34ff Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Sun, 8 May 2022 09:29:27 -0400 Subject: [PATCH 16/18] Add nested at-rule tests --- tests/arbitrary-variants.test.js | 62 ++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js index de77b0eea8cd..0f1453766297 100644 --- a/tests/arbitrary-variants.test.js +++ b/tests/arbitrary-variants.test.js @@ -158,6 +158,37 @@ test('at-rules', () => { }) }) +test('nested at-rules', () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind components; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + ${defaults} + + @media screen { + @media (hover: hover) { + .\[\@media_screen\{\@media_\(hover\:hover\)\}\]\:underline { + text-decoration-line: underline; + } + } + } + `) + }) +}) + test('at-rules with selector modifications', () => { let config = { content: [{ raw: html`
` }], @@ -183,6 +214,37 @@ test('at-rules with selector modifications', () => { }) }) +test('nested at-rules with selector modifications', () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind components; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + ${defaults} + + @media screen { + @media (hover: hover) { + .\[\@media_screen\{\@media_\(hover\:hover\)\{\&\:hover\}\}\]\:underline:hover { + text-decoration-line: underline; + } + } + } + `) + }) +}) + test('attribute selectors', () => { let config = { content: [{ raw: html`
` }], From 8da194880b21fa8ca6b5898db5db34eae1addb79 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Sun, 8 May 2022 09:48:16 -0400 Subject: [PATCH 17/18] Fix space-less at-rule parsing in addVariant --- src/lib/generateRules.js | 1 - src/lib/setupContextUtils.js | 4 ++-- tests/arbitrary-variants.test.js | 16 ++++++++-------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index d039a78017b3..a14f7163a076 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -131,7 +131,6 @@ function applyVariant(variant, matches, context) { if (isArbitraryValue(variant) && !context.variantMap.has(variant)) { let selector = normalize(variant.slice(1, -1)) - // TODO: Error recovery for @supports(what:ever) -- note the absence of a space let fn = parseVariant(selector) let sort = Array.from(context.variantOrder.values()).pop() << 1n diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 4215e0790ded..573341a6c488 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -182,8 +182,8 @@ export function parseVariant(variant) { return ({ format }) => format(str) } - let [, name, params] = /@(.*?) (.*)/g.exec(str) - return ({ wrap }) => wrap(postcss.atRule({ name, params })) + let [, name, params] = /@(.*?)( .+|[({].*)/g.exec(str) + return ({ wrap }) => wrap(postcss.atRule({ name, params: params.trim() })) }) .reverse() diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js index 0f1453766297..abbba5363685 100644 --- a/tests/arbitrary-variants.test.js +++ b/tests/arbitrary-variants.test.js @@ -135,7 +135,7 @@ test('using the important modifier', () => { test('at-rules', () => { let config = { - content: [{ raw: html`
` }], + content: [{ raw: html`
` }], corePlugins: { preflight: false }, } @@ -150,7 +150,7 @@ test('at-rules', () => { ${defaults} @supports (what: ever) { - .\[\@supports_\(what\:ever\)\]\:underline { + .\[\@supports\(what\:ever\)\]\:underline { text-decoration-line: underline; } } @@ -162,7 +162,7 @@ test('nested at-rules', () => { let config = { content: [ { - raw: html`
`, + raw: html`
`, }, ], corePlugins: { preflight: false }, @@ -180,7 +180,7 @@ test('nested at-rules', () => { @media screen { @media (hover: hover) { - .\[\@media_screen\{\@media_\(hover\:hover\)\}\]\:underline { + .\[\@media_screen\{\@media\(hover\:hover\)\}\]\:underline { text-decoration-line: underline; } } @@ -191,7 +191,7 @@ test('nested at-rules', () => { test('at-rules with selector modifications', () => { let config = { - content: [{ raw: html`
` }], + content: [{ raw: html`
` }], corePlugins: { preflight: false }, } @@ -206,7 +206,7 @@ test('at-rules with selector modifications', () => { ${defaults} @media (hover: hover) { - .\[\@media_\(hover\:hover\)\{\&\:hover\}\]\:underline:hover { + .\[\@media\(hover\:hover\)\{\&\:hover\}\]\:underline:hover { text-decoration-line: underline; } } @@ -218,7 +218,7 @@ test('nested at-rules with selector modifications', () => { let config = { content: [ { - raw: html`
`, + raw: html`
`, }, ], corePlugins: { preflight: false }, @@ -236,7 +236,7 @@ test('nested at-rules with selector modifications', () => { @media screen { @media (hover: hover) { - .\[\@media_screen\{\@media_\(hover\:hover\)\{\&\:hover\}\}\]\:underline:hover { + .\[\@media_screen\{\@media\(hover\:hover\)\{\&\:hover\}\}\]\:underline:hover { text-decoration-line: underline; } } From 5487b2725e53daf86eddec10648461c2289ffe70 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Sun, 8 May 2022 10:55:36 -0400 Subject: [PATCH 18/18] Add test for using with `@apply` --- tests/arbitrary-variants.test.js | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js index abbba5363685..45dbf70c5a34 100644 --- a/tests/arbitrary-variants.test.js +++ b/tests/arbitrary-variants.test.js @@ -342,3 +342,38 @@ test('multiple attribute selectors with custom separator (2)', () => { `) }) }) + +test('with @apply', () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + } + + let input = ` + @tailwind base; + @tailwind components; + @tailwind utilities; + + .foo { + @apply [@media_screen{@media(hover:hover){&:hover}}]:underline; + } + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + ${defaults} + + @media screen { + @media (hover: hover) { + .foo:hover { + text-decoration-line: underline; + } + } + } + `) + }) +})