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
This commit is contained in:
Robin Malfait 2024-04-30 18:22:11 +02:00 committed by GitHub
parent 719c0d4883
commit cb17447ff1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 84 additions and 2 deletions

View File

@ -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

View File

@ -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": "'}[("\\''",
},
},
],
}
`)
})

View File

@ -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'])

View File

@ -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++