A utility-first CSS framework for rapid UI development.
Ensure math operators are surrounded by whitespace in arbitrary values (#20011)
This PR fixes an issue where some `calc(…)` expressions become invalid
after we canonicalize them to a different syntax.
Let's say you start with:
`left-[calc(-1*(var(--my-var1)+var(--my-var2)))]`, then the produced AST
for this candidate looks like this:
```js
[
{
"kind": "functional",
"root": "left",
"modifier": null,
"value": {
"kind": "arbitrary",
"dataType": null,
"value": "calc(-1 * (var(--my-var1) + var(--my-var2)))"
},
"variants": [],
"important": false,
"raw": "left-[calc(-1*(var(--my-var1)+var(--my-var2)))]"
}
]
```
Notice that the `+` in between the vars already contain spaces.
And the generated CSS looks like this:
```css
.left-\[calc\(-1\*\(var\(--my-var1\)\+var\(--my-var2\)\)\)\] {
left: calc(-1 * (var(--my-var1) + var(--my-var2)));
}
```
Again, the `+` has spaces aroudn it.
However, we canoncialize this syntax where we remove the `calc(-1 *
<value>)` and move the `-` in front:
`-left-[(var(--my-var1)+var(--my-var2))]`, which should behave the same,
but it did not. The parsed value for this looks like:
```js
[
{
"kind": "functional",
"root": "-left",
"modifier": null,
"value": {
"kind": "arbitrary",
"dataType": null,
"value": "(var(--my-var1)+var(--my-var2))"
},
"variants": [],
"important": false,
"raw": "-left-[(var(--my-var1)+var(--my-var2))]"
}
]
```
Notice that the `+` does not contain spaces around it. That's because we
add them when we parse arbitrary values and when we are in a `calc(…)`
expression. But the `calc(<value> * -1)` is added later, so at this
point, no spaces are added yet.
This also means that the generated CSS currently looks like:
```css
.-left-\[\(var\(--my-var1\)\+var\(--my-var2\)\)\] {
left: calc((var(--my-var1)+var(--my-var2)) * -1);
}
```
Which is invalid.
To solve this, we will make sure to add the whitespace around operators
when we re-insert the `calc(<value> * -1)`. With this fix, the CSS looks
like this:
```css
.-left-\[\(var\(--my-var1\)\+var\(--my-var2\)\)\] {
left: calc((var(--my-var1) + var(--my-var2)) * -1);
}
```
Which is correct again.
---
There are a few other issues that need a bit more work, but are not
required for this fix.
1. Can we get rid of the additional `(` and `)` parens? E.g.:
```diff
- left-[calc(-1*(var(--my-var1)+var(--my-var2)))]
- -left-[(var(--my-var1)+var(--my-var2))]
+ -left-[var(--my-var1)+var(--my-var2)]
```
Right now this means that we should use `calc((<value>) * -1)` instead
of
`calc(<value> * -1)` since `<value>` can be an expression on its own.
This
could lead to unwanted behavior, but this will be a follow up PR _if_
it's
worth it.
2. Why did we even allow this canoncialization from A to B if it's not
the same result?
This last question is a bit more scary, but it has to do with how we
compare results. We create a signature where we normalize a bunch of
values to make sure that we can compare them. As a silly example
`calc(var(--a) + var(--b))` and `calc(var(--b) + var(--a))` will result
in the same value, therefor should have the same signature and should be
swappable.
But what's happening is that the signature of
`left-[calc(-1*(var(--my-var1)+var(--my-var2)))]` and
`-left-[(var(--my-var1)+var(--my-var2))]` result in:
```
/* Signature of: left-[calc(-1*(var(--my-var1)+var(--my-var2)))] */
.x {
left: calc((var(--my-var1)+var(--my-var2))*-1);
}
/* Signature of: -left-[(var(--my-var1)+var(--my-var2))] */
.x {
left: calc((var(--my-var1)+var(--my-var2))*-1);
}
```
This gets rid of whitespace to store less data, but this is obviously
wrong now, so we need to improve the signatures around this. That said,
that will be a follow up PR as well because this requires much more
testing.
But this PR at least fixes the #20010 issue because we will properly
insert the whitespace around the math operators.
Fixes: #20010
## Test plan
1. Added a regression test to make sure that the new canonicalized
syntax results in the correct CSS. Before the fix, the test would fail:
<img width="623" height="106" alt="image"
src="https://github.com/user-attachments/assets/c470ce38-c3fa-4080-92ca-8a3e509f700b"
/>
2. Existing tests still pass R
Robin Malfait committed
8c779899bbc94f9f642db47cafdd363dd094984a
Parent: b4db3b9
Committed by GitHub <noreply@github.com>
on 5/5/2026, 3:08:24 PM