Canonicalization: make hex colors case insensitive (#20298)
This PR fixes an issue where hex-based colors in arbitrary properties
and values were considered case-sensitive even though they are
case-insensitive in CSS.
If you look at the linked issue, there is this input CSS:
```css
@theme {
--color-brand-purple: #3f3cbb;
}
```
We expect that both `bg-[#3f3cbb]` and `bg-[#3F3CBB]` get canonicalized
to `color-brand-purple` but before this pr, only the first one would get
canonicalized that way (since it's a perfect match).
Technically a bunch more values are case-insensitive but a lot of them
_are_ sensitive so to get this 100% correct, a lot more parsing needs to
happen. I think we can start with this and expand the logic when needed.
Fixes: #20295
## Test plan
1. Added a regression test based on the linked issue
2. Added tests for arbitrary properties (`[color:#fff]` vs
`[color:#FFF]`), and tests for arbitrary properties (`bg-[#fff]` vs
`bg-[#FFF]`)
This commit is contained in:
parent
39656f7d9a
commit
e46b3d74ee
3 changed files with 57 additions and 0 deletions
|
|
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
### Fixed
|
||||
|
||||
- Allow `@tailwindcss/cli` in `--watch` mode to use polling with `--poll` when filesystem events are unreliable or unavailable ([#20297](https://github.com/tailwindlabs/tailwindcss/pull/20297))
|
||||
- Canonicalization: match arbitrary hex colors against theme colors case-insensitively (e.g. `bg-[#fff]` and `bg-[#FFF]` → `bg-white`) ([#20298](https://github.com/tailwindlabs/tailwindcss/pull/20298))
|
||||
|
||||
## [4.3.2] - 2026-06-26
|
||||
|
||||
|
|
|
|||
|
|
@ -316,6 +316,10 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
|
|||
['[color:var(--color-red-500)]', 'text-red-500'],
|
||||
['[background-color:var(--color-red-500)]', 'bg-red-500'],
|
||||
|
||||
// Arbitrary property to named utility (case insensitive)
|
||||
['[color:#fff]', 'text-white'],
|
||||
['[color:#FFF]', 'text-white'],
|
||||
|
||||
// Arbitrary property with modifier to named functional utility with modifier
|
||||
['[color:var(--color-red-500)]/25', 'text-red-500/25'],
|
||||
|
||||
|
|
@ -354,6 +358,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
|
|||
--*: initial;
|
||||
--spacing: 0.25rem;
|
||||
--color-red-500: red;
|
||||
--color-white: #fff;
|
||||
|
||||
/* Equivalent of blue-500/50 */
|
||||
--color-primary: color-mix(in oklab, oklch(62.3% 0.214 259.815) 50%, transparent);
|
||||
|
|
@ -394,6 +399,10 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
|
|||
['bg-[theme(colors.red.500/75%)]', 'bg-red-500/75'],
|
||||
['bg-[theme(colors.red.500/12.34%)]', 'bg-red-500/[12.34%]'],
|
||||
|
||||
// Arbitrary value with different casing
|
||||
['bg-[#fff]', 'bg-white'],
|
||||
['bg-[#FFF]', 'bg-white'],
|
||||
|
||||
// Values that don't contain only `theme(…)` calls should not be converted to
|
||||
// use a modifier since the color is not the whole value.
|
||||
[
|
||||
|
|
@ -478,6 +487,9 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
|
|||
])(testName, { timeout }, async (candidate, expected) => {
|
||||
let input = css`
|
||||
@import 'tailwindcss';
|
||||
@theme {
|
||||
--color-white: #fff;
|
||||
}
|
||||
`
|
||||
|
||||
await expectCanonicalization(input, candidate, expected)
|
||||
|
|
@ -1508,4 +1520,30 @@ describe('regressions', () => {
|
|||
|
||||
expect(designSystem.canonicalizeCandidates(['px-[calc(1rem+0px)]'], options)).toEqual(['px-4'])
|
||||
})
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20295
|
||||
test('canonicalizations work when casing of arbitrary values is different', async () => {
|
||||
let designSystem = await designSystems.get(__dirname).get(css`
|
||||
@import 'tailwindcss';
|
||||
@theme {
|
||||
--color-brand-purple: #3f3cbb;
|
||||
}
|
||||
`)
|
||||
|
||||
let options: CanonicalizeOptions = {
|
||||
collapse: true,
|
||||
logicalToPhysical: true,
|
||||
rem: 16,
|
||||
}
|
||||
|
||||
expect(designSystem.canonicalizeCandidates(['bg-[#3F3cbb]'], options)).toEqual([
|
||||
'bg-brand-purple',
|
||||
])
|
||||
expect(designSystem.canonicalizeCandidates(['bg-[#3F3Cbb]'], options)).toEqual([
|
||||
'bg-brand-purple',
|
||||
])
|
||||
expect(designSystem.canonicalizeCandidates(['bg-[#3F3CBB]'], options)).toEqual([
|
||||
'bg-brand-purple',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2425,6 +2425,12 @@ function createUtilitySignatureCache(
|
|||
})
|
||||
}
|
||||
|
||||
// #RGB
|
||||
// #RGBA
|
||||
// #RRGGBB
|
||||
// #RRGGBBAA
|
||||
const HEX_REGEX = /#(?:[a-f0-9]{8}|[a-f0-9]{6}|[a-f0-9]{4}|[a-f0-9]{3})/gi
|
||||
|
||||
// Optimize the CSS AST to make it suitable for signature comparison. We want to
|
||||
// expand declarations, ignore comments, sort declarations etc...
|
||||
function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: SignatureOptions) {
|
||||
|
|
@ -2536,6 +2542,18 @@ function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: Si
|
|||
return a.property.localeCompare(b.property)
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
else if (node.kind === 'declaration' && node.value) {
|
||||
// Leave CSS variables alone
|
||||
if (node.property[0] === '-' && node.property[1] === '-') return
|
||||
|
||||
// Ensure hex colors are always lowercased
|
||||
{
|
||||
HEX_REGEX.lastIndex = 0
|
||||
node.value = node.value.replace(HEX_REGEX, (color) => color.toLowerCase())
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue