Skip to content

Commit

Permalink
Ensure strings are consumed as-is when using internal segment() (#1…
Browse files Browse the repository at this point in the history
…3608)

* 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
  • Loading branch information
RobinMalfait committed Apr 30, 2024
1 parent 719c0d4 commit cb17447
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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

Expand Down
37 changes: 36 additions & 1 deletion packages/tailwindcss/src/candidate.test.ts
Expand Up @@ -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": "'}[("\\''",
},
},
],
}
`)
})
24 changes: 24 additions & 0 deletions packages/tailwindcss/src/utils/segment.test.ts
Expand Up @@ -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'])
Expand Down
24 changes: 23 additions & 1 deletion packages/tailwindcss/src/utils/segment.ts
Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -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++
Expand Down

0 comments on commit cb17447

Please sign in to comment.