Ensure existing spaces in attribute selectors are valid (#14703)

This PR fixes an issue where spaces in a selector generated invalid CSS.

Lightning CSS will throw those incorrect lines of CSS out, but if you
are in an environment where Lightning CSS doesn't run then invalid CSS
is generated.

Given this input:

```html
data-[foo_=_"true"]:flex
```

This will be generated:

```css
.data-\[foo_\=_\"true\"\]\:flex[data-foo=""true] {
  display: flex;
}
```

With this PR in place, the generated CSS will now be:

```css
.data-\[foo_\=_\"true\"\]\:flex[data-foo="true"] {
  display: flex;
}
```

---------

Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This commit is contained in:
Robin Malfait 2024-10-18 19:51:43 +02:00 committed by GitHub
parent 5c1bfd3a91
commit b7c4d25ae4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 56 additions and 24 deletions

View File

@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- _Upgrade (experimental)_: Migrate `plugins` with options to CSS ([#14700](https://github.com/tailwindlabs/tailwindcss/pull/14700))
### Fixed
- Allow spaces spaces around operators in attribute selector variants ([#14703](https://github.com/tailwindlabs/tailwindcss/pull/14703))
### Changed
- _Upgrade (experimental)_: Don't create `@source` rules for `content` paths that are already covered by automatic source detection ([#14714](https://github.com/tailwindlabs/tailwindcss/pull/14714))

View File

@ -1985,6 +1985,7 @@ test('aria', async () => {
'aria-checked:flex',
'aria-[invalid=spelling]:flex',
'aria-[valuenow=1]:flex',
'aria-[valuenow_=_"1"]:flex',
'group-aria-[modal]:flex',
'group-aria-checked:flex',
@ -2059,6 +2060,10 @@ test('aria', async () => {
.aria-\\[valuenow\\=1\\]\\:flex[aria-valuenow="1"] {
display: flex;
}
.aria-\\[valuenow_\\=_\\"1\\"\\]\\:flex[aria-valuenow="1"] {
display: flex;
}"
`)
expect(await run(['aria-checked/foo:flex', 'aria-[invalid=spelling]/foo:flex'])).toEqual('')
@ -2069,6 +2074,9 @@ test('data', async () => {
await run([
'data-disabled:flex',
'data-[potato=salad]:flex',
'data-[potato_=_"salad"]:flex',
'data-[potato_^=_"salad"]:flex',
'data-[potato="^_="]:flex',
'data-[foo=1]:flex',
'data-[foo=bar_baz]:flex',
"data-[foo$='bar'_i]:flex",
@ -2155,6 +2163,18 @@ test('data', async () => {
display: flex;
}
.data-\\[potato_\\=_\\"salad\\"\\]\\:flex[data-potato="salad"] {
display: flex;
}
.data-\\[potato_\\^\\=_\\"salad\\"\\]\\:flex[data-potato^="salad"] {
display: flex;
}
.data-\\[potato\\=\\"\\^_\\=\\"\\]\\:flex[data-potato="^ ="] {
display: flex;
}
.data-\\[foo\\=1\\]\\:flex[data-foo="1"] {
display: flex;
}
@ -2171,7 +2191,13 @@ test('data', async () => {
display: flex;
}"
`)
expect(await run(['data-disabled/foo:flex', 'data-[potato=salad]/foo:flex'])).toEqual('')
expect(
await run([
'data-[foo_^_=_"bar"]:flex', // Can't have spaces between `^` and `=`
'data-disabled/foo:flex',
'data-[potato=salad]/foo:flex',
]),
).toEqual('')
})
test('portrait', async () => {

View File

@ -898,32 +898,34 @@ export function createVariants(theme: Theme): Variants {
return variants
}
function quoteAttributeValue(value: string) {
if (value.includes('=')) {
value = value.replace(/(=.*)/g, (_fullMatch, match) => {
// If the value is already quoted, skip.
if (match[1] === "'" || match[1] === '"') {
return match
}
function quoteAttributeValue(input: string) {
if (input.includes('=')) {
let [attribute, ...after] = segment(input, '=')
let value = after.join('=').trim()
// Handle regex flags on unescaped values
if (match.length > 2) {
let trailingCharacter = match[match.length - 1]
if (
match[match.length - 2] === ' ' &&
(trailingCharacter === 'i' ||
trailingCharacter === 'I' ||
trailingCharacter === 's' ||
trailingCharacter === 'S')
) {
return `="${match.slice(1, -2)}" ${match[match.length - 1]}`
}
}
// If the value is already quoted, skip.
if (value[0] === "'" || value[0] === '"') {
return input
}
return `="${match.slice(1)}"`
})
// Handle case sensitivity flags on unescaped values
if (value.length > 1) {
let trailingCharacter = value[value.length - 1]
if (
value[value.length - 2] === ' ' &&
(trailingCharacter === 'i' ||
trailingCharacter === 'I' ||
trailingCharacter === 's' ||
trailingCharacter === 'S')
) {
return `${attribute}="${value.slice(0, -2)}" ${trailingCharacter}`
}
}
return `${attribute}="${value}"`
}
return value
return input
}
export function substituteAtSlot(ast: AstNode[], nodes: AstNode[]) {