From cb17447ff1aadbef482a3daceb5035b7decb6b79 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 30 Apr 2024 18:22:11 +0200 Subject: [PATCH] Ensure strings are consumed as-is when using internal `segment()` (#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 --- CHANGELOG.md | 1 + packages/tailwindcss/src/candidate.test.ts | 37 ++++++++++++++++++- .../tailwindcss/src/utils/segment.test.ts | 24 ++++++++++++ packages/tailwindcss/src/utils/segment.ts | 24 +++++++++++- 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59421276b..40baa4c42 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)) +- Support unbalanced parentheses and braces in quotes in arbitrary values and variants ([#13608](https://github.com/tailwindlabs/tailwindcss/pull/13608)) ### Changed diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index 8984212cb..28b2b5d7e 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -1031,5 +1031,40 @@ 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`) +}) + +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": "'}[("\\''", + }, + }, + ], + } + `) }) diff --git a/packages/tailwindcss/src/utils/segment.test.ts b/packages/tailwindcss/src/utils/segment.test.ts index 9cfa91f86..e9bd8c49d 100644 --- a/packages/tailwindcss/src/utils/segment.test.ts +++ b/packages/tailwindcss/src/utils/segment.test.ts @@ -21,6 +21,30 @@ 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 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 cbb7115f0..018485dbb 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,25 @@ 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: + // 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. + if (nextChar === BACKSLASH) { + idx += 1 + continue + } + + if (nextChar === char) { + break + } + } + break case OPEN_PAREN: closingBracketStack[stackPos] = CLOSE_PAREN stackPos++