Skip to content

Commit cb17447

Browse files
authored
Ensure strings are consumed as-is when using internal segment() (tailwindlabs#13608)
* ensure we handle strings as-in When encountering strings when using `segment` we didn't really treat them as actual strings. This means that if you used any parens, brackets, or curlies then we wanted them to be properly balanced. This should not be the case, whenever we encounter a string, we want to consume it as-is and don't want to worry about bracket balancing. We will now consume it until the end of the string (and make sure that escaped closing quotes are not seen as real closing quotes). * update changelog * drop unnecessary test Already had this test * ensure we utilities and variants defined * add example test that parses with unbalanced brackets inside quotes * improve changelog entry * hoist comment
1 parent 719c0d4 commit cb17447

File tree

4 files changed

+84
-2
lines changed

4 files changed

+84
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111

1212
- Make sure `contain-*` utility variables resolve to a valid value ([#13521](https://github.com/tailwindlabs/tailwindcss/pull/13521))
13+
- Support unbalanced parentheses and braces in quotes in arbitrary values and variants ([#13608](https://github.com/tailwindlabs/tailwindcss/pull/13608))
1314

1415
### Changed
1516

packages/tailwindcss/src/candidate.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1031,5 +1031,40 @@ it('should parse arbitrary properties that are important and using stacked arbit
10311031
})
10321032

10331033
it('should not parse compound group with a non-compoundable variant', () => {
1034-
expect(run('group-*:flex')).toMatchInlineSnapshot(`null`)
1034+
let utilities = new Utilities()
1035+
utilities.static('flex', () => [])
1036+
1037+
let variants = new Variants()
1038+
variants.compound('group', () => {})
1039+
1040+
expect(run('group-*:flex', { utilities, variants })).toMatchInlineSnapshot(`null`)
1041+
})
1042+
1043+
it('should parse a variant containing an arbitrary string with unbalanced parens, brackets, curlies and other quotes', () => {
1044+
let utilities = new Utilities()
1045+
utilities.static('flex', () => [])
1046+
1047+
let variants = new Variants()
1048+
variants.functional('string', () => {})
1049+
1050+
expect(run(`string-['}[("\\'']:flex`, { utilities, variants })).toMatchInlineSnapshot(`
1051+
{
1052+
"important": false,
1053+
"kind": "static",
1054+
"negative": false,
1055+
"root": "flex",
1056+
"variants": [
1057+
{
1058+
"compounds": true,
1059+
"kind": "functional",
1060+
"modifier": null,
1061+
"root": "string",
1062+
"value": {
1063+
"kind": "arbitrary",
1064+
"value": "'}[("\\''",
1065+
},
1066+
},
1067+
],
1068+
}
1069+
`)
10351070
})

packages/tailwindcss/src/utils/segment.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,30 @@ it('should not split inside of curlies', () => {
2121
expect(segment('a:{b:c}:d', ':')).toEqual(['a', '{b:c}', 'd'])
2222
})
2323

24+
it('should not split inside of double quotes', () => {
25+
expect(segment('a:"b:c":d', ':')).toEqual(['a', '"b:c"', 'd'])
26+
})
27+
28+
it('should not split inside of single quotes', () => {
29+
expect(segment("a:'b:c':d", ':')).toEqual(['a', "'b:c'", 'd'])
30+
})
31+
32+
it('should not crash when double quotes are unbalanced', () => {
33+
expect(segment('a:"b:c:d', ':')).toEqual(['a', '"b:c:d'])
34+
})
35+
36+
it('should not crash when single quotes are unbalanced', () => {
37+
expect(segment("a:'b:c:d", ':')).toEqual(['a', "'b:c:d"])
38+
})
39+
40+
it('should skip escaped double quotes', () => {
41+
expect(segment(String.raw`a:"b:c\":d":e`, ':')).toEqual(['a', String.raw`"b:c\":d"`, 'e'])
42+
})
43+
44+
it('should skip escaped single quotes', () => {
45+
expect(segment(String.raw`a:'b:c\':d':e`, ':')).toEqual(['a', String.raw`'b:c\':d'`, 'e'])
46+
})
47+
2448
it('should split by the escape sequence which is escape as well', () => {
2549
expect(segment('a\\b\\c\\d', '\\')).toEqual(['a', 'b', 'c', 'd'])
2650
expect(segment('a\\(b\\c)\\d', '\\')).toEqual(['a', '(b\\c)', 'd'])

packages/tailwindcss/src/utils/segment.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const OPEN_PAREN = 0x28
55
const CLOSE_PAREN = 0x29
66
const OPEN_BRACKET = 0x5b
77
const CLOSE_BRACKET = 0x5d
8+
const DOUBLE_QUOTE = 0x22
9+
const SINGLE_QUOTE = 0x27
810

911
// This is a shared buffer that is used to keep track of the current nesting level
1012
// of parens, brackets, and braces. It is used to determine if a character is at
@@ -30,10 +32,11 @@ export function segment(input: string, separator: string) {
3032
let stackPos = 0
3133
let parts: string[] = []
3234
let lastPos = 0
35+
let len = input.length
3336

3437
let separatorCode = separator.charCodeAt(0)
3538

36-
for (let idx = 0; idx < input.length; idx++) {
39+
for (let idx = 0; idx < len; idx++) {
3740
let char = input.charCodeAt(idx)
3841

3942
if (stackPos === 0 && char === separatorCode) {
@@ -47,6 +50,25 @@ export function segment(input: string, separator: string) {
4750
// The next character is escaped, so we skip it.
4851
idx += 1
4952
break
53+
// Strings should be handled as-is until the end of the string. No need to
54+
// worry about balancing parens, brackets, or curlies inside a string.
55+
case SINGLE_QUOTE:
56+
case DOUBLE_QUOTE:
57+
// Ensure we don't go out of bounds.
58+
while (++idx < len) {
59+
let nextChar = input.charCodeAt(idx)
60+
61+
// The next character is escaped, so we skip it.
62+
if (nextChar === BACKSLASH) {
63+
idx += 1
64+
continue
65+
}
66+
67+
if (nextChar === char) {
68+
break
69+
}
70+
}
71+
break
5072
case OPEN_PAREN:
5173
closingBracketStack[stackPos] = CLOSE_PAREN
5274
stackPos++

0 commit comments

Comments
 (0)