Skip to content

Commit 4e16410

Browse files
Fix parsing url(…) with special characters such as ; or {} (#14879)
This PR fixes an issue where some special characters (with an actual meaning CSS) were used inside of the `url(…)` function, would result in incorrectly parsed CSS. For example, when we encounter a `{`, then we would start a new "block" for nesting purposes. If we encounter an `}`, then the block would end. If we encounter a `;`, then that would be the end of a declaration. All of that is true, unless we are in a `url(…)` function. In that case, we should ignore all of those characters and treat them as part of the URL. This is only an issue because: 1. We are allowed to use these characters in URLs. 2. We can write an url inside `url(…)` without quotes. With quotes, this would not be an issue. --------- Co-authored-by: Philipp Spiess <[email protected]>
1 parent 8bd3c85 commit 4e16410

File tree

3 files changed

+82
-3
lines changed

3 files changed

+82
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
- Fix crash when using `@source` containing `..` ([#14831](https://github.com/tailwindlabs/tailwindcss/pull/14831))
2121
- Ensure instances of the same variant with different values are always sorted deterministically (e.g. `data-focus:flex` and `data-active:flex`) ([#14835](https://github.com/tailwindlabs/tailwindcss/pull/14835))
2222
- Ensure `--inset-ring=*` and `--inset-shadow-*` variables are ignored by `inset-*` utilities ([#14855](https://github.com/tailwindlabs/tailwindcss/pull/14855))
23+
- Ensure `url(…)` containing special characters such as `;` or `{}` end up in one declaration ([#14879](https://github.com/tailwindlabs/tailwindcss/pull/14879))
2324
- _Upgrade (experimental)_: Install `@tailwindcss/postcss` next to `tailwindcss` ([#14830](https://github.com/tailwindlabs/tailwindcss/pull/14830))
2425
- _Upgrade (experimental)_: Remove whitespace around `,` separator when print arbitrary values ([#14838](https://github.com/tailwindlabs/tailwindcss/pull/14838))
2526

packages/tailwindcss/src/css-parser.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,59 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
505505
},
506506
])
507507
})
508+
509+
it('should parse url(…) without quotes and special characters such as `;`, `{}`, and `[]`', () => {
510+
expect(
511+
parse(css`
512+
.foo {
513+
/* ';' should be valid inside the 'url(…)' function */
514+
background: url(data:image/png;base64,abc==);
515+
516+
/* '{', '}', '[' and ']' should be valid inside the 'url(…)' function */
517+
/* '{' and '}' should not start a new block (nesting) */
518+
background: url(https://example-image-search.org?q={query;limit=5}&ids=[1,2,3]);
519+
520+
/* '{' and '}' don't need to be balanced */
521+
background: url(https://example-image-search.org?curlies=}});
522+
523+
/* '(' and ')' are not valid, unless we are in a string with quotes */
524+
background: url('https://example-image-search.org?q={query;limit=5}&ids=[1,2,3]&format=(png|jpg)');
525+
}
526+
`),
527+
).toEqual([
528+
{
529+
kind: 'rule',
530+
selector: '.foo',
531+
nodes: [
532+
{
533+
kind: 'declaration',
534+
property: 'background',
535+
value: 'url(data:image/png;base64,abc==)',
536+
important: false,
537+
},
538+
{
539+
kind: 'declaration',
540+
property: 'background',
541+
value: 'url(https://example-image-search.org?q={query;limit=5}&ids=[1,2,3])',
542+
important: false,
543+
},
544+
{
545+
kind: 'declaration',
546+
property: 'background',
547+
value: 'url(https://example-image-search.org?curlies=}})',
548+
important: false,
549+
},
550+
{
551+
kind: 'declaration',
552+
property: 'background',
553+
value:
554+
"url('https://example-image-search.org?q={query;limit=5}&ids=[1,2,3]&format=(png|jpg)')",
555+
important: false,
556+
},
557+
],
558+
},
559+
])
560+
})
508561
})
509562

510563
describe('selectors', () => {

packages/tailwindcss/src/css-parser.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,10 @@ export function parse(input: string) {
331331
// }
332332
// ```
333333
//
334-
else if (currentChar === SEMICOLON) {
334+
else if (
335+
currentChar === SEMICOLON &&
336+
closingBracketStack[closingBracketStack.length - 1] !== ')'
337+
) {
335338
let declaration = parseDeclaration(buffer)
336339
if (parent) {
337340
parent.nodes.push(declaration)
@@ -343,7 +346,10 @@ export function parse(input: string) {
343346
}
344347

345348
// Start of a block.
346-
else if (currentChar === OPEN_CURLY) {
349+
else if (
350+
currentChar === OPEN_CURLY &&
351+
closingBracketStack[closingBracketStack.length - 1] !== ')'
352+
) {
347353
closingBracketStack += '}'
348354

349355
// At this point `buffer` should resemble a selector or an at-rule.
@@ -368,7 +374,10 @@ export function parse(input: string) {
368374
}
369375

370376
// End of a block.
371-
else if (currentChar === CLOSE_CURLY) {
377+
else if (
378+
currentChar === CLOSE_CURLY &&
379+
closingBracketStack[closingBracketStack.length - 1] !== ')'
380+
) {
372381
if (closingBracketStack === '') {
373382
throw new Error('Missing opening {')
374383
}
@@ -456,6 +465,22 @@ export function parse(input: string) {
456465
node = null
457466
}
458467

468+
// `(`
469+
else if (currentChar === OPEN_PAREN) {
470+
closingBracketStack += ')'
471+
buffer += '('
472+
}
473+
474+
// `)`
475+
else if (currentChar === CLOSE_PAREN) {
476+
if (closingBracketStack[closingBracketStack.length - 1] !== ')') {
477+
throw new Error('Missing opening (')
478+
}
479+
480+
closingBracketStack = closingBracketStack.slice(0, -1)
481+
buffer += ')'
482+
}
483+
459484
// Any other character is part of the current node.
460485
else {
461486
// Skip whitespace at the start of a new node.

0 commit comments

Comments
 (0)