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 <hello@philippspiess.com>
This commit is contained in:
Robin Malfait 2024-11-06 12:06:49 +01:00 committed by GitHub
parent 8bd3c85420
commit 4e164107dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 82 additions and 3 deletions

View File

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

View File

@ -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', () => {

View File

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