Skip to content

Commit f821c71

Browse files
Handle group/peer variants with quoted strings (#10400)
* Handle group/peer variants with quoted strings * Fix CS * Use `splitAtTopLevelOnly` instead This solution isn’t that pretty but it is reusing existing machinery * inline return Co-authored-by: Robin Malfait <[email protected]> * Fix return type Co-authored-by: Robin Malfait <[email protected]> * Fixup * Update changelog Co-authored-by: Robin Malfait <[email protected]>
1 parent 667eac5 commit f821c71

File tree

5 files changed

+112
-5
lines changed

5 files changed

+112
-5
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4040
- Consider earlier variants before sorting functions ([#10288](https://github.com/tailwindlabs/tailwindcss/pull/10288))
4141
- Allow variants with slashes ([#10336](https://github.com/tailwindlabs/tailwindcss/pull/10336))
4242
- Ensure generated CSS is always sorted in the same order for a given set of templates ([#10382](https://github.com/tailwindlabs/tailwindcss/pull/10382))
43+
- Handle variants when the same class appears multiple times in a selector ([#10397](https://github.com/tailwindlabs/tailwindcss/pull/10397))
44+
- Handle group/peer variants with quoted strings ([#10400](https://github.com/tailwindlabs/tailwindcss/pull/10400))
4345

4446
### Changed
4547

src/corePlugins.js

+24-1
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,30 @@ export let variantPlugins = {
168168
if (!result.includes('&')) result = '&' + result
169169

170170
let [a, b] = fn('', extra)
171-
return result.replace(/&(\S+)?/g, (_, pseudo = '') => a + pseudo + b)
171+
172+
let start = null
173+
let end = null
174+
let quotes = 0
175+
176+
for (let i = 0; i < result.length; ++i) {
177+
let c = result[i]
178+
if (c === '&') {
179+
start = i
180+
} else if (c === "'" || c === '"') {
181+
quotes += 1
182+
} else if (start !== null && c === ' ' && !quotes) {
183+
end = i
184+
}
185+
}
186+
187+
if (start !== null && end === null) {
188+
end = result.length
189+
}
190+
191+
// Basically this but can handle quotes:
192+
// result.replace(/&(\S+)?/g, (_, pseudo = '') => a + pseudo + b)
193+
194+
return result.slice(0, start) + a + result.slice(start + 1, end) + b + result.slice(end)
172195
},
173196
{ values: Object.fromEntries(pseudoVariants) }
174197
)

src/lib/generateRules.js

+12-4
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,18 @@ function applyVariant(variant, matches, context) {
152152

153153
// Retrieve "modifier"
154154
{
155-
let match = /(.*)\/(.*)$/g.exec(variant)
156-
if (match && !context.variantMap.has(variant)) {
157-
variant = match[1]
158-
args.modifier = match[2]
155+
let [baseVariant, ...modifiers] = splitAtTopLevelOnly(variant, '/')
156+
157+
// This is a hack to support variants with `/` in them, like `ar-1/10/20:text-red-500`
158+
// In this case 1/10 is a value but /20 is a modifier
159+
if (modifiers.length > 1) {
160+
baseVariant = baseVariant + '/' + modifiers.slice(0, -1).join('/')
161+
modifiers = modifiers.slice(-1)
162+
}
163+
164+
if (modifiers.length && !context.variantMap.has(variant)) {
165+
variant = baseVariant
166+
args.modifier = modifiers[0]
159167

160168
if (!flagEnabled(context.tailwindConfig, 'generalizedModifiers')) {
161169
return []

tests/basic-usage.test.js

+62
Original file line numberDiff line numberDiff line change
@@ -949,4 +949,66 @@ crosscheck(({ stable, oxide }) => {
949949
`)
950950
})
951951
})
952+
953+
test('detects quoted arbitrary values containing a slash', async () => {
954+
let config = {
955+
content: [
956+
{
957+
raw: html`<div class="group-[[href^='/']]:hidden"></div>`,
958+
},
959+
],
960+
}
961+
962+
let input = css`
963+
@tailwind utilities;
964+
`
965+
966+
let result = await run(input, config)
967+
968+
oxide.expect(result.css).toMatchFormattedCss(css`
969+
.group[href^='/'] .group-\[\[href\^\=\'\/\'\]\]\:hidden {
970+
display: none;
971+
}
972+
`)
973+
974+
stable.expect(result.css).toMatchFormattedCss(css`
975+
.hidden {
976+
display: none;
977+
}
978+
.group[href^='/'] .group-\[\[href\^\=\'\/\'\]\]\:hidden {
979+
display: none;
980+
}
981+
`)
982+
})
983+
984+
test('handled quoted arbitrary values containing escaped spaces', async () => {
985+
let config = {
986+
content: [
987+
{
988+
raw: html`<div class="group-[[href^='_bar']]:hidden"></div>`,
989+
},
990+
],
991+
}
992+
993+
let input = css`
994+
@tailwind utilities;
995+
`
996+
997+
let result = await run(input, config)
998+
999+
oxide.expect(result.css).toMatchFormattedCss(css`
1000+
.group[href^=' bar'] .group-\[\[href\^\=\'_bar\'\]\]\:hidden {
1001+
display: none;
1002+
}
1003+
`)
1004+
1005+
stable.expect(result.css).toMatchFormattedCss(css`
1006+
.hidden {
1007+
display: none;
1008+
}
1009+
.group[href^=' bar'] .group-\[\[href\^\=\'_bar\'\]\]\:hidden {
1010+
display: none;
1011+
}
1012+
`)
1013+
})
9521014
})

tests/util/run.js

+12
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ let nullProxy = new Proxy(
6060
}
6161
)
6262

63+
/**
64+
* @typedef {object} CrossCheck
65+
* @property {typeof import('@jest/globals')} oxide
66+
* @property {typeof import('@jest/globals')} stable
67+
* @property {object} engine
68+
* @property {boolean} engine.oxide
69+
* @property {boolean} engine.stable
70+
*/
71+
72+
/**
73+
* @param {(data: CrossCheck) => void} fn
74+
*/
6375
export function crosscheck(fn) {
6476
let engines =
6577
env.ENGINE === 'oxide' ? [{ engine: 'Stable' }, { engine: 'Oxide' }] : [{ engine: 'Stable' }]

0 commit comments

Comments
 (0)