From 4c64fadd8f97a26bae0e567e09fa4b22cb8ce26c Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 29 Apr 2024 14:14:34 +0200 Subject: [PATCH 1/7] 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). --- .../tailwindcss/src/utils/segment.test.ts | 28 +++++++++++++++++++ packages/tailwindcss/src/utils/segment.ts | 26 ++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/utils/segment.test.ts b/packages/tailwindcss/src/utils/segment.test.ts index 9cfa91f86ab3..26ca76d89f58 100644 --- a/packages/tailwindcss/src/utils/segment.test.ts +++ b/packages/tailwindcss/src/utils/segment.test.ts @@ -21,6 +21,34 @@ it('should not split inside of curlies', () => { expect(segment('a:{b:c}:d', ':')).toEqual(['a', '{b:c}', 'd']) }) +it('should not split inside of double quotes', () => { + expect(segment('a:"b:c":d', ':')).toEqual(['a', '"b:c"', 'd']) +}) + +it('should not split inside of single quotes', () => { + expect(segment("a:'b:c':d", ':')).toEqual(['a', "'b:c'", 'd']) +}) + +it('should not crash when double quotes are unbalanced', () => { + expect(segment('a:"b:c:d', ':')).toEqual(['a', '"b:c:d']) +}) + +it('should not crash when single quotes are unbalanced', () => { + expect(segment("a:'b:c:d", ':')).toEqual(['a', "'b:c:d"]) +}) + +it('should skip escaped double quotes', () => { + expect(segment(String.raw`a:"b:c\":d":e`, ':')).toEqual(['a', String.raw`"b:c\":d"`, 'e']) +}) + +it('should skip escaped single quotes', () => { + expect(segment(String.raw`a:'b:c\':d':e`, ':')).toEqual(['a', String.raw`'b:c\':d'`, 'e']) +}) + +it('should not crash when single quotes are unbalanced', () => { + expect(segment("a:'b:c:d", ':')).toEqual(['a', "'b:c:d"]) +}) + it('should split by the escape sequence which is escape as well', () => { expect(segment('a\\b\\c\\d', '\\')).toEqual(['a', 'b', 'c', 'd']) expect(segment('a\\(b\\c)\\d', '\\')).toEqual(['a', '(b\\c)', 'd']) diff --git a/packages/tailwindcss/src/utils/segment.ts b/packages/tailwindcss/src/utils/segment.ts index cbb7115f0667..9be69e8d6e45 100644 --- a/packages/tailwindcss/src/utils/segment.ts +++ b/packages/tailwindcss/src/utils/segment.ts @@ -5,6 +5,8 @@ const OPEN_PAREN = 0x28 const CLOSE_PAREN = 0x29 const OPEN_BRACKET = 0x5b const CLOSE_BRACKET = 0x5d +const DOUBLE_QUOTE = 0x22 +const SINGLE_QUOTE = 0x27 // This is a shared buffer that is used to keep track of the current nesting level // 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) { let stackPos = 0 let parts: string[] = [] let lastPos = 0 + let len = input.length let separatorCode = separator.charCodeAt(0) - for (let idx = 0; idx < input.length; idx++) { + for (let idx = 0; idx < len; idx++) { let char = input.charCodeAt(idx) if (stackPos === 0 && char === separatorCode) { @@ -47,6 +50,27 @@ export function segment(input: string, separator: string) { // The next character is escaped, so we skip it. idx += 1 break + // Strings should be handled as-is until the end of the string. No need to + // worry about balancing parens, brackets, or curlies inside a string. + case SINGLE_QUOTE: + case DOUBLE_QUOTE: + while ( + // Ensure we don't go out of bounds. + ++idx < len + ) { + let nextChar = input.charCodeAt(idx) + + // The next character is escaped, so we skip it. + if (nextChar === BACKSLASH) { + idx += 1 + continue + } + + if (nextChar === char) { + break + } + } + break case OPEN_PAREN: closingBracketStack[stackPos] = CLOSE_PAREN stackPos++ From 2c042451d36da76eba546a05cdd40bdece53f5a7 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 29 Apr 2024 14:19:15 +0200 Subject: [PATCH 2/7] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59421276b329..d498c3feaf35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Make sure `contain-*` utility variables resolve to a valid value ([#13521](https://github.com/tailwindlabs/tailwindcss/pull/13521)) +- Ensure strings are consumed as-is when using internal `segment()` ([#13608](https://github.com/tailwindlabs/tailwindcss/pull/13608)) ### Changed From 9615e46e17bf00466d7db8760a92974e16d4206b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 29 Apr 2024 14:25:20 +0200 Subject: [PATCH 3/7] drop unnecessary test Already had this test --- packages/tailwindcss/src/utils/segment.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/tailwindcss/src/utils/segment.test.ts b/packages/tailwindcss/src/utils/segment.test.ts index 26ca76d89f58..e9bd8c49d8bc 100644 --- a/packages/tailwindcss/src/utils/segment.test.ts +++ b/packages/tailwindcss/src/utils/segment.test.ts @@ -45,10 +45,6 @@ it('should skip escaped single quotes', () => { expect(segment(String.raw`a:'b:c\':d':e`, ':')).toEqual(['a', String.raw`'b:c\':d'`, 'e']) }) -it('should not crash when single quotes are unbalanced', () => { - expect(segment("a:'b:c:d", ':')).toEqual(['a', "'b:c:d"]) -}) - it('should split by the escape sequence which is escape as well', () => { expect(segment('a\\b\\c\\d', '\\')).toEqual(['a', 'b', 'c', 'd']) expect(segment('a\\(b\\c)\\d', '\\')).toEqual(['a', '(b\\c)', 'd']) From 2a1a276824bb6ac5d7fe78acf21a2e13f9462724 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 29 Apr 2024 22:48:57 +0200 Subject: [PATCH 4/7] ensure we utilities and variants defined --- packages/tailwindcss/src/candidate.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index 8984212cb363..6966b1bb8d91 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -1031,5 +1031,11 @@ it('should parse arbitrary properties that are important and using stacked arbit }) it('should not parse compound group with a non-compoundable variant', () => { - expect(run('group-*:flex')).toMatchInlineSnapshot(`null`) + let utilities = new Utilities() + utilities.static('flex', () => []) + + let variants = new Variants() + variants.compound('group', () => {}) + + expect(run('group-*:flex', { utilities, variants })).toMatchInlineSnapshot(`null`) }) From 1f6c327b09fb631776febec48bea4473e6502aaa Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 29 Apr 2024 22:49:13 +0200 Subject: [PATCH 5/7] add example test that parses with unbalanced brackets inside quotes --- packages/tailwindcss/src/candidate.test.ts | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index 6966b1bb8d91..28b2b5d7e657 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -1039,3 +1039,32 @@ it('should not parse compound group with a non-compoundable variant', () => { expect(run('group-*:flex', { utilities, variants })).toMatchInlineSnapshot(`null`) }) + +it('should parse a variant containing an arbitrary string with unbalanced parens, brackets, curlies and other quotes', () => { + let utilities = new Utilities() + utilities.static('flex', () => []) + + let variants = new Variants() + variants.functional('string', () => {}) + + expect(run(`string-['}[("\\'']:flex`, { utilities, variants })).toMatchInlineSnapshot(` + { + "important": false, + "kind": "static", + "negative": false, + "root": "flex", + "variants": [ + { + "compounds": true, + "kind": "functional", + "modifier": null, + "root": "string", + "value": { + "kind": "arbitrary", + "value": "'}[("\\''", + }, + }, + ], + } + `) +}) From 5bea4c3c5096d868236458fa0c9a88de0af92097 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 29 Apr 2024 23:09:54 +0200 Subject: [PATCH 6/7] improve changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d498c3feaf35..40baa4c42bbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Make sure `contain-*` utility variables resolve to a valid value ([#13521](https://github.com/tailwindlabs/tailwindcss/pull/13521)) -- Ensure strings are consumed as-is when using internal `segment()` ([#13608](https://github.com/tailwindlabs/tailwindcss/pull/13608)) +- Support unbalanced parentheses and braces in quotes in arbitrary values and variants ([#13608](https://github.com/tailwindlabs/tailwindcss/pull/13608)) ### Changed From a5c9c5d10232612868df7d933102a182b93d72e9 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 29 Apr 2024 23:10:26 +0200 Subject: [PATCH 7/7] hoist comment --- packages/tailwindcss/src/utils/segment.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss/src/utils/segment.ts b/packages/tailwindcss/src/utils/segment.ts index 9be69e8d6e45..018485dbb94f 100644 --- a/packages/tailwindcss/src/utils/segment.ts +++ b/packages/tailwindcss/src/utils/segment.ts @@ -54,10 +54,8 @@ export function segment(input: string, separator: string) { // worry about balancing parens, brackets, or curlies inside a string. case SINGLE_QUOTE: case DOUBLE_QUOTE: - while ( - // Ensure we don't go out of bounds. - ++idx < len - ) { + // Ensure we don't go out of bounds. + while (++idx < len) { let nextChar = input.charCodeAt(idx) // The next character is escaped, so we skip it.