mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
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:
parent
5c1bfd3a91
commit
b7c4d25ae4
@ -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))
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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[]) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user