Skip to content

Commit fc25299

Browse files
authored
Add matchVariant API (#8310)
* update regex extractor * implement `matchVariant` API * add `matchVariant` test * add `values` option to the `matchVariant` API * move `matchVariant` tests to own file * update changelog
1 parent 6c63f67 commit fc25299

5 files changed

+184
-4
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4343
- Add `grid-flow-dense` utility ([#8193](https://github.com/tailwindlabs/tailwindcss/pull/8193))
4444
- Add `mix-blend-plus-lighter` utility ([#8288](https://github.com/tailwindlabs/tailwindcss/pull/8288))
4545
- Add arbitrary variants ([#8299](https://github.com/tailwindlabs/tailwindcss/pull/8299))
46+
- Add `matchVariant` API ([#8310](https://github.com/tailwindlabs/tailwindcss/pull/8310))
4647

4748
## [3.0.24] - 2022-04-12
4849

src/lib/defaultExtractor.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ function* buildRegExps(context) {
2525
// Variants
2626
'((?=((',
2727
regex.any(
28-
[regex.pattern([/\[[^\s"'\\]+\]/, separator]), regex.pattern([/[^\s"'\[\\]+/, separator])],
28+
[
29+
regex.pattern([/([^\s"'\[\\]+-)?\[[^\s"'\\]+\]/, separator]),
30+
regex.pattern([/[^\s"'\[\\]+/, separator]),
31+
],
2932
true
3033
),
3134
')+))\\2)?',

src/lib/generateRules.js

+9
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ function applyVariant(variant, matches, context) {
127127
return matches
128128
}
129129

130+
let args
131+
132+
// Find partial arbitrary variants
133+
if (variant.endsWith(']') && !variant.startsWith('[')) {
134+
args = variant.slice(variant.lastIndexOf('[') + 1, -1)
135+
variant = variant.slice(0, variant.indexOf(args) - 1 /* - */ - 1 /* [ */)
136+
}
137+
130138
// Register arbitrary variants
131139
if (isArbitraryValue(variant) && !context.variantMap.has(variant)) {
132140
let selector = normalize(variant.slice(1, -1))
@@ -204,6 +212,7 @@ function applyVariant(variant, matches, context) {
204212
format(selectorFormat) {
205213
collectedFormats.push(selectorFormat)
206214
},
215+
args,
207216
})
208217

209218
if (typeof ruleWithVariant === 'string') {

src/lib/setupContextUtils.js

+38-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import isValidArbitraryValue from '../util/isValidArbitraryValue'
2222
import { generateRules } from './generateRules'
2323
import { hasContentChanged } from './cacheInvalidation.js'
2424

25+
let MATCH_VARIANT = Symbol()
26+
2527
function prefix(context, selector) {
2628
let prefix = context.tailwindConfig.prefix
2729
return typeof prefix === 'function' ? prefix(selector) : prefix + selector
@@ -219,13 +221,18 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
219221
return context.tailwindConfig.prefix + identifier
220222
}
221223

222-
return {
224+
let api = {
223225
addVariant(variantName, variantFunctions, options = {}) {
224226
variantFunctions = [].concat(variantFunctions).map((variantFunction) => {
225227
if (typeof variantFunction !== 'string') {
226228
// Safelist public API functions
227-
return ({ modifySelectors, container, separator }) => {
228-
let result = variantFunction({ modifySelectors, container, separator })
229+
return ({ args, modifySelectors, container, separator, wrap, format }) => {
230+
let result = variantFunction(
231+
Object.assign(
232+
{ modifySelectors, container, separator },
233+
variantFunction[MATCH_VARIANT] && { args, wrap, format }
234+
)
235+
)
229236

230237
if (typeof result === 'string' && !isValidVariantFormatString(result)) {
231238
throw new Error(
@@ -462,7 +469,35 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
462469
context.candidateRuleMap.get(prefixedIdentifier).push(withOffsets)
463470
}
464471
},
472+
matchVariant: function (variants, options) {
473+
for (let variant in variants) {
474+
for (let [k, v] of Object.entries(options?.values ?? {})) {
475+
api.addVariant(`${variant}-${k}`, variants[variant](v))
476+
}
477+
478+
api.addVariant(
479+
variant,
480+
Object.assign(
481+
({ args, wrap }) => {
482+
let formatString = variants[variant](args)
483+
if (!formatString) return null
484+
485+
if (!formatString.startsWith('@')) {
486+
return formatString
487+
}
488+
489+
let [, name, params] = /@(.*?)( .+|[({].*)/g.exec(formatString)
490+
return wrap(postcss.atRule({ name, params: params.trim() }))
491+
},
492+
{ [MATCH_VARIANT]: true }
493+
),
494+
options
495+
)
496+
}
497+
},
465498
}
499+
500+
return api
466501
}
467502

468503
let fileModifiedMapCache = new WeakMap()

tests/match-variants.test.js

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { run, html, css } from './util/run'
2+
3+
test('partial arbitrary variants', () => {
4+
let config = {
5+
content: [
6+
{
7+
raw: html`<div class="potato-[yellow]:bg-yellow-200 potato-[baked]:w-3"></div> `,
8+
},
9+
],
10+
corePlugins: { preflight: false },
11+
plugins: [
12+
({ matchVariant }) => {
13+
matchVariant({
14+
potato: (flavor) => `.potato-${flavor} &`,
15+
})
16+
},
17+
],
18+
}
19+
20+
let input = css`
21+
@tailwind utilities;
22+
`
23+
24+
return run(input, config).then((result) => {
25+
expect(result.css).toMatchFormattedCss(css`
26+
.potato-baked .potato-\[baked\]\:w-3 {
27+
width: 0.75rem;
28+
}
29+
30+
.potato-yellow .potato-\[yellow\]\:bg-yellow-200 {
31+
--tw-bg-opacity: 1;
32+
background-color: rgb(254 240 138 / var(--tw-bg-opacity));
33+
}
34+
`)
35+
})
36+
})
37+
38+
test('partial arbitrary variants with default values', () => {
39+
let config = {
40+
content: [
41+
{
42+
raw: html`<div class="tooltip-bottom:mt-2 tooltip-top:mb-2"></div>`,
43+
},
44+
],
45+
corePlugins: { preflight: false },
46+
plugins: [
47+
({ matchVariant }) => {
48+
matchVariant(
49+
{
50+
tooltip: (side) => `&${side}`,
51+
},
52+
{
53+
values: {
54+
bottom: '[data-location="bottom"]',
55+
top: '[data-location="top"]',
56+
},
57+
}
58+
)
59+
},
60+
],
61+
}
62+
63+
let input = css`
64+
@tailwind utilities;
65+
`
66+
67+
return run(input, config).then((result) => {
68+
expect(result.css).toMatchFormattedCss(css`
69+
.tooltip-bottom\:mt-2[data-location='bottom'] {
70+
margin-top: 0.5rem;
71+
}
72+
73+
.tooltip-top\:mb-2[data-location='top'] {
74+
margin-bottom: 0.5rem;
75+
}
76+
`)
77+
})
78+
})
79+
80+
test('matched variant values maintain the sort order they are registered in', () => {
81+
let config = {
82+
content: [
83+
{
84+
raw: html`<div
85+
class="alphabet-c:underline alphabet-a:underline alphabet-d:underline alphabet-b:underline"
86+
></div>`,
87+
},
88+
],
89+
corePlugins: { preflight: false },
90+
plugins: [
91+
({ matchVariant }) => {
92+
matchVariant(
93+
{
94+
alphabet: (side) => `&${side}`,
95+
},
96+
{
97+
values: {
98+
a: '[data-value="a"]',
99+
b: '[data-value="b"]',
100+
c: '[data-value="c"]',
101+
d: '[data-value="d"]',
102+
},
103+
}
104+
)
105+
},
106+
],
107+
}
108+
109+
let input = css`
110+
@tailwind utilities;
111+
`
112+
113+
return run(input, config).then((result) => {
114+
expect(result.css).toMatchFormattedCss(css`
115+
.alphabet-a\:underline[data-value='a'] {
116+
text-decoration-line: underline;
117+
}
118+
119+
.alphabet-b\:underline[data-value='b'] {
120+
text-decoration-line: underline;
121+
}
122+
123+
.alphabet-c\:underline[data-value='c'] {
124+
text-decoration-line: underline;
125+
}
126+
127+
.alphabet-d\:underline[data-value='d'] {
128+
text-decoration-line: underline;
129+
}
130+
`)
131+
})
132+
})

0 commit comments

Comments
 (0)