Skip to content

Commit 4f8539c

Browse files
Fix bug replacing modifier variable shorthand syntax underscores (#17889)
Resolves #17888 **Reproduction URL:** https://play.tailwindcss.com/YvIekuzVRd Changes: * Don't use `decodeArbitraryValue` when parsing variable shorthand syntax in modifiers * replace `decodeArbitraryValue(modifier.slice(1, -1))` with `modifier.slice(1, -1)` * added test case, passing ✅ --------- Co-authored-by: Robin Malfait <[email protected]>
1 parent ed45952 commit 4f8539c

File tree

3 files changed

+169
-11
lines changed

3 files changed

+169
-11
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- Ensure negative arbitrary `scale` values generate negative values ([#17831](https://github.com/tailwindlabs/tailwindcss/pull/17831))
1717
- Fix HAML extraction with embedded Ruby ([#17846](https://github.com/tailwindlabs/tailwindcss/pull/17846))
1818
- Don't scan files for utilities when using `@reference` ([#17836](https://github.com/tailwindlabs/tailwindcss/pull/17836))
19+
- Fix incorrectly replacing `_` with ` ` in arbitrary modifier shorthand `bg-red-500/(--my_opacity)` ([#17889](https://github.com/tailwindlabs/tailwindcss/pull/17889))
1920

2021
## [4.1.5] - 2025-04-30
2122

packages/tailwindcss/src/candidate.test.ts

+151
Original file line numberDiff line numberDiff line change
@@ -1087,6 +1087,7 @@ it('should parse a utility with an implicit variable as the modifier using the s
10871087
let utilities = new Utilities()
10881088
utilities.functional('bg', () => [])
10891089

1090+
// Standard case (no underscores)
10901091
expect(run('bg-red-500/(--value)', { utilities })).toMatchInlineSnapshot(`
10911092
[
10921093
{
@@ -1107,6 +1108,156 @@ it('should parse a utility with an implicit variable as the modifier using the s
11071108
},
11081109
]
11091110
`)
1111+
1112+
// Should preserve underscores
1113+
expect(run('bg-red-500/(--with_underscore)', { utilities })).toMatchInlineSnapshot(`
1114+
[
1115+
{
1116+
"important": false,
1117+
"kind": "functional",
1118+
"modifier": {
1119+
"kind": "arbitrary",
1120+
"value": "var(--with_underscore)",
1121+
},
1122+
"raw": "bg-red-500/(--with_underscore)",
1123+
"root": "bg",
1124+
"value": {
1125+
"fraction": null,
1126+
"kind": "named",
1127+
"value": "red-500",
1128+
},
1129+
"variants": [],
1130+
},
1131+
]
1132+
`)
1133+
1134+
// Should remove underscores in fallback values
1135+
expect(run('bg-red-500/(--with_underscore,fallback_value)', { utilities }))
1136+
.toMatchInlineSnapshot(`
1137+
[
1138+
{
1139+
"important": false,
1140+
"kind": "functional",
1141+
"modifier": {
1142+
"kind": "arbitrary",
1143+
"value": "var(--with_underscore,fallback value)",
1144+
},
1145+
"raw": "bg-red-500/(--with_underscore,fallback_value)",
1146+
"root": "bg",
1147+
"value": {
1148+
"fraction": null,
1149+
"kind": "named",
1150+
"value": "red-500",
1151+
},
1152+
"variants": [],
1153+
},
1154+
]
1155+
`)
1156+
1157+
// Should keep underscores in the CSS variable itself, but remove underscores
1158+
// in fallback values
1159+
expect(run('bg-(--a_b,c_d_var(--e_f,g_h))/(--i_j,k_l_var(--m_n,o_p))', { utilities }))
1160+
.toMatchInlineSnapshot(`
1161+
[
1162+
{
1163+
"important": false,
1164+
"kind": "functional",
1165+
"modifier": {
1166+
"kind": "arbitrary",
1167+
"value": "var(--i_j,k l var(--m_n,o p))",
1168+
},
1169+
"raw": "bg-(--a_b,c_d_var(--e_f,g_h))/(--i_j,k_l_var(--m_n,o_p))",
1170+
"root": "bg",
1171+
"value": {
1172+
"dataType": null,
1173+
"kind": "arbitrary",
1174+
"value": "var(--a_b,c d var(--e_f,g h))",
1175+
},
1176+
"variants": [],
1177+
},
1178+
]
1179+
`)
1180+
})
1181+
1182+
it('should not parse an invalid arbitrary shorthand modifier', () => {
1183+
let utilities = new Utilities()
1184+
utilities.functional('bg', () => [])
1185+
1186+
// Completely empty
1187+
expect(run('bg-red-500/()', { utilities })).toMatchInlineSnapshot(`[]`)
1188+
1189+
// Invalid due to leading spaces
1190+
expect(run('bg-red-500/(_--)', { utilities })).toMatchInlineSnapshot(`[]`)
1191+
expect(run('bg-red-500/(_--x)', { utilities })).toMatchInlineSnapshot(`[]`)
1192+
1193+
// Invalid due to leading spaces
1194+
expect(run('bg-red-500/(_--)', { utilities })).toMatchInlineSnapshot(`[]`)
1195+
expect(run('bg-red-500/(_--x)', { utilities })).toMatchInlineSnapshot(`[]`)
1196+
1197+
// Invalid due to top-level `;` or `}` characters
1198+
expect(run('bg-red-500/(--x;--y)', { utilities })).toMatchInlineSnapshot(`[]`)
1199+
expect(run('bg-red-500/(--x:{foo:bar})', { utilities })).toMatchInlineSnapshot(`[]`)
1200+
1201+
// Valid, but ensuring that we didn't make an off-by-one error
1202+
expect(run('bg-red-500/(--x)', { utilities })).toMatchInlineSnapshot(`
1203+
[
1204+
{
1205+
"important": false,
1206+
"kind": "functional",
1207+
"modifier": {
1208+
"kind": "arbitrary",
1209+
"value": "var(--x)",
1210+
},
1211+
"raw": "bg-red-500/(--x)",
1212+
"root": "bg",
1213+
"value": {
1214+
"fraction": null,
1215+
"kind": "named",
1216+
"value": "red-500",
1217+
},
1218+
"variants": [],
1219+
},
1220+
]
1221+
`)
1222+
})
1223+
1224+
it('should not parse an invalid arbitrary shorthand value', () => {
1225+
let utilities = new Utilities()
1226+
utilities.functional('bg', () => [])
1227+
1228+
// Completely empty
1229+
expect(run('bg-()', { utilities })).toMatchInlineSnapshot(`[]`)
1230+
1231+
// Invalid due to leading spaces
1232+
expect(run('bg-(_--)', { utilities })).toMatchInlineSnapshot(`[]`)
1233+
expect(run('bg-(_--x)', { utilities })).toMatchInlineSnapshot(`[]`)
1234+
1235+
// Invalid due to leading spaces
1236+
expect(run('bg-(_--)', { utilities })).toMatchInlineSnapshot(`[]`)
1237+
expect(run('bg-(_--x)', { utilities })).toMatchInlineSnapshot(`[]`)
1238+
1239+
// Invalid due to top-level `;` or `}` characters
1240+
expect(run('bg-(--x;--y)', { utilities })).toMatchInlineSnapshot(`[]`)
1241+
expect(run('bg-(--x:{foo:bar})', { utilities })).toMatchInlineSnapshot(`[]`)
1242+
1243+
// Valid, but ensuring that we didn't make an off-by-one error
1244+
expect(run('bg-(--x)', { utilities })).toMatchInlineSnapshot(`
1245+
[
1246+
{
1247+
"important": false,
1248+
"kind": "functional",
1249+
"modifier": null,
1250+
"raw": "bg-(--x)",
1251+
"root": "bg",
1252+
"value": {
1253+
"dataType": null,
1254+
"kind": "arbitrary",
1255+
"value": "var(--x)",
1256+
},
1257+
"variants": [],
1258+
},
1259+
]
1260+
`)
11101261
})
11111262

11121263
it('should not parse a utility with an implicit invalid variable as the modifier using the shorthand', () => {

packages/tailwindcss/src/candidate.ts

+17-11
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,10 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter
411411

412412
// An arbitrary value with `(…)` should always start with `--` since it
413413
// represents a CSS variable.
414-
if (value[0] !== '-' && value[1] !== '-') return
414+
if (value[0] !== '-' || value[1] !== '-') return
415+
416+
// Values can't contain `;` or `}` characters at the top-level.
417+
if (!isValidArbitrary(value)) return
415418

416419
roots = [[root, dataType === null ? `[var(${value})]` : `[${dataType}:var(${value})]`]]
417420
}
@@ -523,21 +526,24 @@ function parseModifier(modifier: string): CandidateModifier | null {
523526
}
524527

525528
if (modifier[0] === '(' && modifier[modifier.length - 1] === ')') {
526-
let arbitraryValue = decodeArbitraryValue(modifier.slice(1, -1))
529+
// Drop the `(` and `)` characters
530+
modifier = modifier.slice(1, -1)
531+
532+
// A modifier with `(…)` should always start with `--` since it
533+
// represents a CSS variable.
534+
if (modifier[0] !== '-' || modifier[1] !== '-') return null
527535

528536
// Values can't contain `;` or `}` characters at the top-level.
529-
if (!isValidArbitrary(arbitraryValue)) return null
537+
if (!isValidArbitrary(modifier)) return null
530538

531-
// Empty arbitrary values are invalid. E.g.: `data-():`
532-
// ^^
533-
if (arbitraryValue.length === 0 || arbitraryValue.trim().length === 0) return null
539+
// Wrap the value in `var(…)` to ensure that it is a valid CSS variable.
540+
modifier = `var(${modifier})`
534541

535-
// Arbitrary values must start with `--` since it represents a CSS variable.
536-
if (arbitraryValue[0] !== '-' && arbitraryValue[1] !== '-') return null
542+
let arbitraryValue = decodeArbitraryValue(modifier)
537543

538544
return {
539545
kind: 'arbitrary',
540-
value: `var(${arbitraryValue})`,
546+
value: arbitraryValue,
541547
}
542548
}
543549

@@ -679,7 +685,7 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia
679685
if (arbitraryValue.length === 0 || arbitraryValue.trim().length === 0) return null
680686

681687
// Arbitrary values must start with `--` since it represents a CSS variable.
682-
if (arbitraryValue[0] !== '-' && arbitraryValue[1] !== '-') return null
688+
if (arbitraryValue[0] !== '-' || arbitraryValue[1] !== '-') return null
683689

684690
return {
685691
kind: 'functional',
@@ -1030,7 +1036,7 @@ function recursivelyEscapeUnderscores(ast: ValueParser.ValueAstNode[]) {
10301036
case 'word': {
10311037
// Dashed idents and variables `var(--my-var)` and `--my-var` should not
10321038
// have underscores escaped
1033-
if (node.value[0] !== '-' && node.value[1] !== '-') {
1039+
if (node.value[0] !== '-' || node.value[1] !== '-') {
10341040
node.value = escapeUnderscore(node.value)
10351041
}
10361042
break

0 commit comments

Comments
 (0)