SIGN IN SIGN UP

A utility-first CSS framework for rapid UI development.

0 0 25 TypeScript

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