CSS Parser: Handle string with semi-colon in custom properties. (#18251)
Strings are not parsed correctly for custom properties which makes the
following CSS raise an `Unterminated string: ";"` error:
```css
:root {
--custom: 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==';
}
```
According to the spec, we should accept semi-colon as long as they are
not at the top level.
> The allowed syntax for [custom
properties](https://drafts.csswg.org/css-variables/#custom-property) is
extremely permissive. The <declaration-value> production matches any
sequence of one or more tokens, so long as the sequence does not contain
bad-string-token, bad-url-token, unmatched )-token, ]-token, or }-token,
or top-level semicolon-token tokens or delim-token tokens with a value
of "!".
Extract from: https://drafts.csswg.org/css-variables/#syntax
I was only able to reproduce with **tailwindcss v4**, the previous
version seems to support this. This issue is mitigated by the fact that
even if you want to use a data URL in a custom property, you would need
to wrap the value in a `url()` anyway:
```css
:root {
--my-icon-url: url('data:image/svg+xml;base64,...==');
}
.icon {
background-image: var(--my-icon-url);
}
```
Which works perfectly fine with the current/latest version (v4.1.8).
The fix suggested is to share the same code between regular property and
custom property when it comes to detect that the value is a string
starting with a `SINGLE_QUOTE` or `DOUBLE_QUOTE`. I have moved the
existing code in a `findEndStringIdx` which returns the position of the
ending single/double quote.
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This commit is contained in:
parent
fd95af4284
commit
bea843c90a
3 changed files with 105 additions and 66 deletions
|
|
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Fixed
|
||||
|
||||
- Correctly parse custom properties with strings containing semicolons ([#18251](https://github.com/tailwindlabs/tailwindcss/pull/18251))
|
||||
- Upgrade: migrate arbitrary modifiers with values without percentage sign to bare values `/[0.16]` -> `/16` ([#18184](https://github.com/tailwindlabs/tailwindcss/pull/18184))
|
||||
- Upgrade: migrate CSS variable shorthand if fallback value contains function call ([#18184](https://github.com/tailwindlabs/tailwindcss/pull/18184))
|
||||
- Upgrade: Migrate negative arbitrary values to negative bare values, e.g.: `mb-[-32rem]` → `-mb-128` ([#18212](https://github.com/tailwindlabs/tailwindcss/pull/18212))
|
||||
|
|
|
|||
|
|
@ -457,6 +457,21 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
|
|||
`),
|
||||
).toEqual([{ kind: 'declaration', property: '--foo', value: 'bar', important: true }])
|
||||
})
|
||||
|
||||
it('should parse custom properties with data URL value', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
--foo: 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==';
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: '--foo',
|
||||
value: "'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=='",
|
||||
important: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it('should parse multiple declarations', () => {
|
||||
|
|
@ -1132,6 +1147,17 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
|
|||
)
|
||||
})
|
||||
|
||||
it('should error when an unterminated string is used in a custom property', () => {
|
||||
expect(() =>
|
||||
parse(css`
|
||||
.foo {
|
||||
--bar: "Hello world!
|
||||
/* ^ missing " */
|
||||
}
|
||||
`),
|
||||
).toThrowErrorMatchingInlineSnapshot(`[Error: Unterminated string: "Hello world!"]`)
|
||||
})
|
||||
|
||||
it('should error when a declaration is incomplete', () => {
|
||||
expect(() => parse('.foo { bar }')).toThrowErrorMatchingInlineSnapshot(
|
||||
`[Error: Invalid declaration: \`bar\`]`,
|
||||
|
|
|
|||
|
|
@ -138,74 +138,11 @@ export function parse(input: string, opts?: ParseOptions) {
|
|||
|
||||
// Start of a string.
|
||||
else if (currentChar === SINGLE_QUOTE || currentChar === DOUBLE_QUOTE) {
|
||||
let start = i
|
||||
|
||||
// We need to ensure that the closing quote is the same as the opening
|
||||
// quote.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// ```css
|
||||
// .foo {
|
||||
// content: "This is a string with a 'quote' in it";
|
||||
// ^ ^ -> These are not the end of the string.
|
||||
// }
|
||||
// ```
|
||||
for (let j = i + 1; j < input.length; j++) {
|
||||
peekChar = input.charCodeAt(j)
|
||||
// Current character is a `\` therefore the next character is escaped.
|
||||
if (peekChar === BACKSLASH) {
|
||||
j += 1
|
||||
}
|
||||
|
||||
// End of the string.
|
||||
else if (peekChar === currentChar) {
|
||||
i = j
|
||||
break
|
||||
}
|
||||
|
||||
// End of the line without ending the string but with a `;` at the end.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// ```css
|
||||
// .foo {
|
||||
// content: "This is a string with a;
|
||||
// ^ Missing "
|
||||
// }
|
||||
// ```
|
||||
else if (
|
||||
peekChar === SEMICOLON &&
|
||||
(input.charCodeAt(j + 1) === LINE_BREAK ||
|
||||
(input.charCodeAt(j + 1) === CARRIAGE_RETURN && input.charCodeAt(j + 2) === LINE_BREAK))
|
||||
) {
|
||||
throw new Error(
|
||||
`Unterminated string: ${input.slice(start, j + 1) + String.fromCharCode(currentChar)}`,
|
||||
)
|
||||
}
|
||||
|
||||
// End of the line without ending the string.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// ```css
|
||||
// .foo {
|
||||
// content: "This is a string with a
|
||||
// ^ Missing "
|
||||
// }
|
||||
// ```
|
||||
else if (
|
||||
peekChar === LINE_BREAK ||
|
||||
(peekChar === CARRIAGE_RETURN && input.charCodeAt(j + 1) === LINE_BREAK)
|
||||
) {
|
||||
throw new Error(
|
||||
`Unterminated string: ${input.slice(start, j) + String.fromCharCode(currentChar)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
let end = parseString(input, i, currentChar)
|
||||
|
||||
// Adjust `buffer` to include the string.
|
||||
buffer += input.slice(start, i + 1)
|
||||
buffer += input.slice(i, end + 1)
|
||||
i = end
|
||||
}
|
||||
|
||||
// Skip whitespace if the next character is also whitespace. This allows us
|
||||
|
|
@ -253,6 +190,11 @@ export function parse(input: string, opts?: ParseOptions) {
|
|||
j += 1
|
||||
}
|
||||
|
||||
// Start of a string.
|
||||
else if (peekChar === SINGLE_QUOTE || peekChar === DOUBLE_QUOTE) {
|
||||
j = parseString(input, j, peekChar)
|
||||
}
|
||||
|
||||
// Start of a comment.
|
||||
else if (peekChar === SLASH && input.charCodeAt(j + 1) === ASTERISK) {
|
||||
for (let k = j + 2; k < input.length; k++) {
|
||||
|
|
@ -651,3 +593,73 @@ function parseDeclaration(
|
|||
importantIdx !== -1,
|
||||
)
|
||||
}
|
||||
|
||||
function parseString(input: string, startIdx: number, quoteChar: number): number {
|
||||
let peekChar: number
|
||||
|
||||
// We need to ensure that the closing quote is the same as the opening
|
||||
// quote.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// ```css
|
||||
// .foo {
|
||||
// content: "This is a string with a 'quote' in it";
|
||||
// ^ ^ -> These are not the end of the string.
|
||||
// }
|
||||
// ```
|
||||
for (let i = startIdx + 1; i < input.length; i++) {
|
||||
peekChar = input.charCodeAt(i)
|
||||
|
||||
// Current character is a `\` therefore the next character is escaped.
|
||||
if (peekChar === BACKSLASH) {
|
||||
i += 1
|
||||
}
|
||||
|
||||
// End of the string.
|
||||
else if (peekChar === quoteChar) {
|
||||
return i
|
||||
}
|
||||
|
||||
// End of the line without ending the string but with a `;` at the end.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// ```css
|
||||
// .foo {
|
||||
// content: "This is a string with a;
|
||||
// ^ Missing "
|
||||
// }
|
||||
// ```
|
||||
else if (
|
||||
peekChar === SEMICOLON &&
|
||||
(input.charCodeAt(i + 1) === LINE_BREAK ||
|
||||
(input.charCodeAt(i + 1) === CARRIAGE_RETURN && input.charCodeAt(i + 2) === LINE_BREAK))
|
||||
) {
|
||||
throw new Error(
|
||||
`Unterminated string: ${input.slice(startIdx, i + 1) + String.fromCharCode(quoteChar)}`,
|
||||
)
|
||||
}
|
||||
|
||||
// End of the line without ending the string.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// ```css
|
||||
// .foo {
|
||||
// content: "This is a string with a
|
||||
// ^ Missing "
|
||||
// }
|
||||
// ```
|
||||
else if (
|
||||
peekChar === LINE_BREAK ||
|
||||
(peekChar === CARRIAGE_RETURN && input.charCodeAt(i + 1) === LINE_BREAK)
|
||||
) {
|
||||
throw new Error(
|
||||
`Unterminated string: ${input.slice(startIdx, i) + String.fromCharCode(quoteChar)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return startIdx
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue