From 5bb68b5300bf1fa63380a22ca256852f210d28f9 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 12 May 2022 12:32:46 +0200 Subject: [PATCH 1/6] update regex extractor --- src/lib/defaultExtractor.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/defaultExtractor.js b/src/lib/defaultExtractor.js index ff8c6dd50dc3..953f82d7ee22 100644 --- a/src/lib/defaultExtractor.js +++ b/src/lib/defaultExtractor.js @@ -25,7 +25,10 @@ function* buildRegExps(context) { // Variants '((?=((', regex.any( - [regex.pattern([/\[[^\s"'\\]+\]/, separator]), regex.pattern([/[^\s"'\[\\]+/, separator])], + [ + regex.pattern([/([^\s"'\[\\]+-)?\[[^\s"'\\]+\]/, separator]), + regex.pattern([/[^\s"'\[\\]+/, separator]), + ], true ), ')+))\\2)?', From d35744d8c7054ea45d210bb6e7897da24f12e3fb Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 12 May 2022 19:08:02 +0200 Subject: [PATCH 2/6] implement `matchVariant` API --- src/lib/generateRules.js | 9 +++++++++ src/lib/setupContextUtils.js | 37 +++++++++++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index ccba747d9020..929d30156fb1 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -127,6 +127,14 @@ function applyVariant(variant, matches, context) { return matches } + let args + + // Find partial arbitrary variants + if (variant.endsWith(']') && !variant.startsWith('[')) { + args = variant.slice(variant.lastIndexOf('[') + 1, -1) + variant = variant.slice(0, variant.indexOf(args) - 1 /* - */ - 1 /* [ */) + } + // Register arbitrary variants if (isArbitraryValue(variant) && !context.variantMap.has(variant)) { let selector = normalize(variant.slice(1, -1)) @@ -204,6 +212,7 @@ function applyVariant(variant, matches, context) { format(selectorFormat) { collectedFormats.push(selectorFormat) }, + args, }) if (typeof ruleWithVariant === 'string') { diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 488b8f975307..b233ab76a16f 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -22,6 +22,8 @@ import isValidArbitraryValue from '../util/isValidArbitraryValue' import { generateRules } from './generateRules' import { hasContentChanged } from './cacheInvalidation.js' +let MATCH_VARIANT = Symbol() + function prefix(context, selector) { let prefix = context.tailwindConfig.prefix return typeof prefix === 'function' ? prefix(selector) : prefix + selector @@ -219,13 +221,18 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs return context.tailwindConfig.prefix + identifier } - return { + let api = { addVariant(variantName, variantFunctions, options = {}) { variantFunctions = [].concat(variantFunctions).map((variantFunction) => { if (typeof variantFunction !== 'string') { // Safelist public API functions - return ({ modifySelectors, container, separator }) => { - let result = variantFunction({ modifySelectors, container, separator }) + return ({ args, modifySelectors, container, separator, wrap, format }) => { + let result = variantFunction( + Object.assign( + { modifySelectors, container, separator }, + variantFunction[MATCH_VARIANT] && { args, wrap, format } + ) + ) if (typeof result === 'string' && !isValidVariantFormatString(result)) { throw new Error( @@ -462,7 +469,31 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs context.candidateRuleMap.get(prefixedIdentifier).push(withOffsets) } }, + matchVariant: function (variants, options) { + for (let variant in variants) { + api.addVariant( + variant, + Object.assign( + ({ args, wrap }) => { + let formatString = variants[variant](args) + if (!formatString) return null + + if (!formatString.startsWith('@')) { + return formatString + } + + let [, name, params] = /@(.*?)( .+|[({].*)/g.exec(formatString) + return wrap(postcss.atRule({ name, params: params.trim() })) + }, + { [MATCH_VARIANT]: true } + ), + options + ) + } + }, } + + return api } let fileModifiedMapCache = new WeakMap() From da6f8df77a8535067b9f0e1f11768a74a0f021ee Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 12 May 2022 19:08:11 +0200 Subject: [PATCH 3/6] add `matchVariant` test --- tests/arbitrary-variants.test.js | 101 +++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js index ffa4a70732d4..534091cfb470 100644 --- a/tests/arbitrary-variants.test.js +++ b/tests/arbitrary-variants.test.js @@ -405,3 +405,104 @@ test('with @apply', () => { `) }) }) + +test('partial arbitrary variants', () => { + let config = { + content: [ + { + raw: html` +
+
+
+
+
+
+
+ `, + }, + ], + corePlugins: { preflight: false }, + plugins: [ + ({ addUtilities, matchVariant, matchUtilities }) => { + addUtilities({ + '.container-type-size': { 'container-type': 'size' }, + '.container-type-inline-size': { 'container-type': 'inline-size' }, + '.container-type-block-size': { 'container-type': 'block-size' }, + '.container-type-style': { 'container-type': 'style' }, + '.container-type-state': { 'container-type': 'state' }, + }) + + matchUtilities({ + container: (value) => { + return { + 'container-name': value, + } + }, + }) + + matchVariant({ + contain: (args) => { + if (args.includes(',')) { + let [name, query] = args.split(',') + let [type, value] = query.split(':') + return `@container ${name} (${ + { min: 'min-width', max: 'max-width' }[type] + }: ${value})` + } else if (args.includes(':')) { + let [type, value] = args.split(':') + return `@container (${{ min: 'min-width', max: 'max-width' }[type]}: ${value})` + } else { + return `@container (min-width: ${args})` + } + }, + }) + }, + ], + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .container-type-inline-size { + container-type: inline-size; + } + + .container-\[sidebar\] { + container-name: sidebar; + } + + @container sidebar (min-width: 500px) { + .contain-\[sidebar\2c min\:500px\]\:flex { + display: flex; + } + } + + @container sidebar (max-width: 500px) { + .contain-\[sidebar\2c max\:500px\]\:flex { + display: flex; + } + } + + @container (min-width: 500px) { + .contain-\[min\:500px\]\:flex { + display: flex; + } + } + + @container (max-width: 500px) { + .contain-\[max\:500px\]\:flex { + display: flex; + } + } + + @container (min-width: 500px) { + .contain-\[500px\]\:flex { + display: flex; + } + } + `) + }) +}) From d776e747fd72dec4bf6c4e23190f3913de14ba89 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 17 May 2022 16:57:32 +0200 Subject: [PATCH 4/6] add `values` option to the `matchVariant` API --- src/lib/setupContextUtils.js | 4 + tests/arbitrary-variants.test.js | 156 ++++++++++++++++++------------- 2 files changed, 97 insertions(+), 63 deletions(-) diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index b233ab76a16f..d572c2137333 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -471,6 +471,10 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs }, matchVariant: function (variants, options) { for (let variant in variants) { + for (let [k, v] of Object.entries(options?.values ?? {})) { + api.addVariant(`${variant}-${k}`, variants[variant](v)) + } + api.addVariant( variant, Object.assign( diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js index 534091cfb470..191d7fe3ca85 100644 --- a/tests/arbitrary-variants.test.js +++ b/tests/arbitrary-variants.test.js @@ -410,52 +410,58 @@ test('partial arbitrary variants', () => { let config = { content: [ { - raw: html` -
-
-
-
-
-
-
- `, + raw: html`
`, }, ], corePlugins: { preflight: false }, plugins: [ - ({ addUtilities, matchVariant, matchUtilities }) => { - addUtilities({ - '.container-type-size': { 'container-type': 'size' }, - '.container-type-inline-size': { 'container-type': 'inline-size' }, - '.container-type-block-size': { 'container-type': 'block-size' }, - '.container-type-style': { 'container-type': 'style' }, - '.container-type-state': { 'container-type': 'state' }, + ({ matchVariant }) => { + matchVariant({ + potato: (flavor) => `.potato-${flavor} &`, }) + }, + ], + } - matchUtilities({ - container: (value) => { - return { - 'container-name': value, - } - }, - }) + let input = css` + @tailwind utilities; + ` - matchVariant({ - contain: (args) => { - if (args.includes(',')) { - let [name, query] = args.split(',') - let [type, value] = query.split(':') - return `@container ${name} (${ - { min: 'min-width', max: 'max-width' }[type] - }: ${value})` - } else if (args.includes(':')) { - let [type, value] = args.split(':') - return `@container (${{ min: 'min-width', max: 'max-width' }[type]}: ${value})` - } else { - return `@container (min-width: ${args})` - } + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .potato-baked .potato-\[baked\]\:w-3 { + width: 0.75rem; + } + + .potato-yellow .potato-\[yellow\]\:bg-yellow-200 { + --tw-bg-opacity: 1; + background-color: rgb(254 240 138 / var(--tw-bg-opacity)); + } + `) + }) +}) + +test('partial arbitrary variants with default values', () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + plugins: [ + ({ matchVariant }) => { + matchVariant( + { + tooltip: (side) => `&${side}`, }, - }) + { + values: { + bottom: '[data-location="bottom"]', + top: '[data-location="top"]', + }, + } + ) }, ], } @@ -466,42 +472,66 @@ test('partial arbitrary variants', () => { return run(input, config).then((result) => { expect(result.css).toMatchFormattedCss(css` - .container-type-inline-size { - container-type: inline-size; + .tooltip-bottom\:mt-2[data-location='bottom'] { + margin-top: 0.5rem; } - .container-\[sidebar\] { - container-name: sidebar; + .tooltip-top\:mb-2[data-location='top'] { + margin-bottom: 0.5rem; } + `) + }) +}) - @container sidebar (min-width: 500px) { - .contain-\[sidebar\2c min\:500px\]\:flex { - display: flex; - } - } +test('matched variant values maintain the sort order they are registered in', () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + plugins: [ + ({ matchVariant }) => { + matchVariant( + { + alphabet: (side) => `&${side}`, + }, + { + values: { + a: '[data-value="a"]', + b: '[data-value="b"]', + c: '[data-value="c"]', + d: '[data-value="d"]', + }, + } + ) + }, + ], + } - @container sidebar (max-width: 500px) { - .contain-\[sidebar\2c max\:500px\]\:flex { - display: flex; - } + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .alphabet-a\:underline[data-value='a'] { + text-decoration-line: underline; } - @container (min-width: 500px) { - .contain-\[min\:500px\]\:flex { - display: flex; - } + .alphabet-b\:underline[data-value='b'] { + text-decoration-line: underline; } - @container (max-width: 500px) { - .contain-\[max\:500px\]\:flex { - display: flex; - } + .alphabet-c\:underline[data-value='c'] { + text-decoration-line: underline; } - @container (min-width: 500px) { - .contain-\[500px\]\:flex { - display: flex; - } + .alphabet-d\:underline[data-value='d'] { + text-decoration-line: underline; } `) }) From dac4724c7805dabfb2c238025e4aa35b2946bf43 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 17 May 2022 17:05:31 +0200 Subject: [PATCH 5/6] move `matchVariant` tests to own file --- tests/arbitrary-variants.test.js | 131 ------------------------------ tests/match-variants.test.js | 132 +++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 131 deletions(-) create mode 100644 tests/match-variants.test.js diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js index 191d7fe3ca85..ffa4a70732d4 100644 --- a/tests/arbitrary-variants.test.js +++ b/tests/arbitrary-variants.test.js @@ -405,134 +405,3 @@ test('with @apply', () => { `) }) }) - -test('partial arbitrary variants', () => { - let config = { - content: [ - { - raw: html`
`, - }, - ], - corePlugins: { preflight: false }, - plugins: [ - ({ matchVariant }) => { - matchVariant({ - potato: (flavor) => `.potato-${flavor} &`, - }) - }, - ], - } - - let input = css` - @tailwind utilities; - ` - - return run(input, config).then((result) => { - expect(result.css).toMatchFormattedCss(css` - .potato-baked .potato-\[baked\]\:w-3 { - width: 0.75rem; - } - - .potato-yellow .potato-\[yellow\]\:bg-yellow-200 { - --tw-bg-opacity: 1; - background-color: rgb(254 240 138 / var(--tw-bg-opacity)); - } - `) - }) -}) - -test('partial arbitrary variants with default values', () => { - let config = { - content: [ - { - raw: html`
`, - }, - ], - corePlugins: { preflight: false }, - plugins: [ - ({ matchVariant }) => { - matchVariant( - { - tooltip: (side) => `&${side}`, - }, - { - values: { - bottom: '[data-location="bottom"]', - top: '[data-location="top"]', - }, - } - ) - }, - ], - } - - let input = css` - @tailwind utilities; - ` - - return run(input, config).then((result) => { - expect(result.css).toMatchFormattedCss(css` - .tooltip-bottom\:mt-2[data-location='bottom'] { - margin-top: 0.5rem; - } - - .tooltip-top\:mb-2[data-location='top'] { - margin-bottom: 0.5rem; - } - `) - }) -}) - -test('matched variant values maintain the sort order they are registered in', () => { - let config = { - content: [ - { - raw: html`
`, - }, - ], - corePlugins: { preflight: false }, - plugins: [ - ({ matchVariant }) => { - matchVariant( - { - alphabet: (side) => `&${side}`, - }, - { - values: { - a: '[data-value="a"]', - b: '[data-value="b"]', - c: '[data-value="c"]', - d: '[data-value="d"]', - }, - } - ) - }, - ], - } - - let input = css` - @tailwind utilities; - ` - - return run(input, config).then((result) => { - expect(result.css).toMatchFormattedCss(css` - .alphabet-a\:underline[data-value='a'] { - text-decoration-line: underline; - } - - .alphabet-b\:underline[data-value='b'] { - text-decoration-line: underline; - } - - .alphabet-c\:underline[data-value='c'] { - text-decoration-line: underline; - } - - .alphabet-d\:underline[data-value='d'] { - text-decoration-line: underline; - } - `) - }) -}) diff --git a/tests/match-variants.test.js b/tests/match-variants.test.js new file mode 100644 index 000000000000..269edff522e9 --- /dev/null +++ b/tests/match-variants.test.js @@ -0,0 +1,132 @@ +import { run, html, css } from './util/run' + +test('partial arbitrary variants', () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + plugins: [ + ({ matchVariant }) => { + matchVariant({ + potato: (flavor) => `.potato-${flavor} &`, + }) + }, + ], + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .potato-baked .potato-\[baked\]\:w-3 { + width: 0.75rem; + } + + .potato-yellow .potato-\[yellow\]\:bg-yellow-200 { + --tw-bg-opacity: 1; + background-color: rgb(254 240 138 / var(--tw-bg-opacity)); + } + `) + }) +}) + +test('partial arbitrary variants with default values', () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + plugins: [ + ({ matchVariant }) => { + matchVariant( + { + tooltip: (side) => `&${side}`, + }, + { + values: { + bottom: '[data-location="bottom"]', + top: '[data-location="top"]', + }, + } + ) + }, + ], + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .tooltip-bottom\:mt-2[data-location='bottom'] { + margin-top: 0.5rem; + } + + .tooltip-top\:mb-2[data-location='top'] { + margin-bottom: 0.5rem; + } + `) + }) +}) + +test('matched variant values maintain the sort order they are registered in', () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + plugins: [ + ({ matchVariant }) => { + matchVariant( + { + alphabet: (side) => `&${side}`, + }, + { + values: { + a: '[data-value="a"]', + b: '[data-value="b"]', + c: '[data-value="c"]', + d: '[data-value="d"]', + }, + } + ) + }, + ], + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .alphabet-a\:underline[data-value='a'] { + text-decoration-line: underline; + } + + .alphabet-b\:underline[data-value='b'] { + text-decoration-line: underline; + } + + .alphabet-c\:underline[data-value='c'] { + text-decoration-line: underline; + } + + .alphabet-d\:underline[data-value='d'] { + text-decoration-line: underline; + } + `) + }) +}) From 7ff4eb6788c14d9c63a5e24311643f39e6559e4a Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 17 May 2022 17:45:46 +0200 Subject: [PATCH 6/6] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21dbfb319d16..38e6742a9da8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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)) +- Add `matchVariant` API ([#8310](https://github.com/tailwindlabs/tailwindcss/pull/8310)) ## [3.0.24] - 2022-04-12