From 4e164107ddd3892ed04739150c61d5590b68343e Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 6 Nov 2024 12:06:49 +0100 Subject: [PATCH] =?UTF-8?q?Fix=20parsing=20`url(=E2=80=A6)`=20with=20speci?= =?UTF-8?q?al=20characters=20such=20as=20`;`=20or=20`{}`=20(#14879)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 1 + packages/tailwindcss/src/css-parser.test.ts | 53 +++++++++++++++++++++ packages/tailwindcss/src/css-parser.ts | 31 ++++++++++-- 3 files changed, 82 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49353b907..710e27792 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix crash when using `@source` containing `..` ([#14831](https://github.com/tailwindlabs/tailwindcss/pull/14831)) - 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)) - Ensure `--inset-ring=*` and `--inset-shadow-*` variables are ignored by `inset-*` utilities ([#14855](https://github.com/tailwindlabs/tailwindcss/pull/14855)) +- Ensure `url(…)` containing special characters such as `;` or `{}` end up in one declaration ([#14879](https://github.com/tailwindlabs/tailwindcss/pull/14879)) - _Upgrade (experimental)_: Install `@tailwindcss/postcss` next to `tailwindcss` ([#14830](https://github.com/tailwindlabs/tailwindcss/pull/14830)) - _Upgrade (experimental)_: Remove whitespace around `,` separator when print arbitrary values ([#14838](https://github.com/tailwindlabs/tailwindcss/pull/14838)) diff --git a/packages/tailwindcss/src/css-parser.test.ts b/packages/tailwindcss/src/css-parser.test.ts index 0da36c40a..a4b123b28 100644 --- a/packages/tailwindcss/src/css-parser.test.ts +++ b/packages/tailwindcss/src/css-parser.test.ts @@ -505,6 +505,59 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => { }, ]) }) + + it('should parse url(…) without quotes and special characters such as `;`, `{}`, and `[]`', () => { + expect( + parse(css` + .foo { + /* ';' should be valid inside the 'url(…)' function */ + background: url(data:image/png;base64,abc==); + + /* '{', '}', '[' and ']' should be valid inside the 'url(…)' function */ + /* '{' and '}' should not start a new block (nesting) */ + background: url(https://example-image-search.org?q={query;limit=5}&ids=[1,2,3]); + + /* '{' and '}' don't need to be balanced */ + background: url(https://example-image-search.org?curlies=}}); + + /* '(' and ')' are not valid, unless we are in a string with quotes */ + background: url('https://example-image-search.org?q={query;limit=5}&ids=[1,2,3]&format=(png|jpg)'); + } + `), + ).toEqual([ + { + kind: 'rule', + selector: '.foo', + nodes: [ + { + kind: 'declaration', + property: 'background', + value: 'url(data:image/png;base64,abc==)', + important: false, + }, + { + kind: 'declaration', + property: 'background', + value: 'url(https://example-image-search.org?q={query;limit=5}&ids=[1,2,3])', + important: false, + }, + { + kind: 'declaration', + property: 'background', + value: 'url(https://example-image-search.org?curlies=}})', + important: false, + }, + { + kind: 'declaration', + property: 'background', + value: + "url('https://example-image-search.org?q={query;limit=5}&ids=[1,2,3]&format=(png|jpg)')", + important: false, + }, + ], + }, + ]) + }) }) describe('selectors', () => { diff --git a/packages/tailwindcss/src/css-parser.ts b/packages/tailwindcss/src/css-parser.ts index 8fb4a546d..3b6e283e5 100644 --- a/packages/tailwindcss/src/css-parser.ts +++ b/packages/tailwindcss/src/css-parser.ts @@ -331,7 +331,10 @@ export function parse(input: string) { // } // ``` // - else if (currentChar === SEMICOLON) { + else if ( + currentChar === SEMICOLON && + closingBracketStack[closingBracketStack.length - 1] !== ')' + ) { let declaration = parseDeclaration(buffer) if (parent) { parent.nodes.push(declaration) @@ -343,7 +346,10 @@ export function parse(input: string) { } // Start of a block. - else if (currentChar === OPEN_CURLY) { + else if ( + currentChar === OPEN_CURLY && + closingBracketStack[closingBracketStack.length - 1] !== ')' + ) { closingBracketStack += '}' // At this point `buffer` should resemble a selector or an at-rule. @@ -368,7 +374,10 @@ export function parse(input: string) { } // End of a block. - else if (currentChar === CLOSE_CURLY) { + else if ( + currentChar === CLOSE_CURLY && + closingBracketStack[closingBracketStack.length - 1] !== ')' + ) { if (closingBracketStack === '') { throw new Error('Missing opening {') } @@ -456,6 +465,22 @@ export function parse(input: string) { node = null } + // `(` + else if (currentChar === OPEN_PAREN) { + closingBracketStack += ')' + buffer += '(' + } + + // `)` + else if (currentChar === CLOSE_PAREN) { + if (closingBracketStack[closingBracketStack.length - 1] !== ')') { + throw new Error('Missing opening (') + } + + closingBracketStack = closingBracketStack.slice(0, -1) + buffer += ')' + } + // Any other character is part of the current node. else { // Skip whitespace at the start of a new node.