tailwindcss/src/util/parseBoxShadowValue.js
Robin Malfait 527031d5f6
Improve data type analyses for arbitrary values (#9320)
* improve split logic by delimiter

The original RegEx did mostly what we want, the idea is that we wanted
to split by a `,` but one that was not within `()`. This is useful when
you define multiple background colors for example:
```html
<div class="bg-[rgb(0,0,0),rgb(255,255,255)]"></div>
```

In this case splitting by the regex would result in the proper result:
```js
let result = [
  'rgb(0,0,0)',
  'rgb(255,255,255)'
]
```

Visually, you can think of it like:
```
    ┌─[./example.html]
    │
∙ 1 │   <div class="bg-[rgb(0,0,0),rgb(255,255,255)]"></div>
    ·                       ──┬── ┬    ─────┬─────
    ·                         │   │         ╰─────── Guarded by parens
    ·                         │   ╰───────────────── We will split here
    ·                         ╰───────────────────── Guarded by parens
    │
    └─
```

We properly split by `,` not inside a `()`. However, this RegEx fails
the moment you have deeply nested RegEx values.

Visually, this is what's happening:
```
    ┌─[./example.html]
    │
∙ 1 │   <div class="bg-[rgba(0,0,0,var(--alpha))]"></div>
    ·                         ┬ ┬ ┬
    ·                         ╰─┴─┴── We accidentally split here
    │
    └─
```
This is because on the right of the `,`, the first paren is an opening
paren `(` instead of a closing one `)`.

I'm not 100% sure how we can improve the RegEx to handle that case as
well, instead I wrote a small `splitBy` function that allows you to
split the string by a character (just like you could do before) but
ignores the ones inside the given exceptions. This keeps track of a
stack to know whether we are within parens or not.

Visually, the fix looks like this:
```
    ┌─[./example.html]
    │
∙ 1 │   <div class="bg-[rgba(0,0,0,var(--alpha)),rgb(255,255,255,var(--alpha))]"></div>
    ·                         ┬ ┬ ┬             ┬       ┬   ┬   ┬
    ·                         │ │ │             │       ╰───┴───┴── Guarded by parens
    ·                         │ │ │             ╰────────────────── We will split here
    ·                         ╰─┴─┴──────────────────────────────── Guarded by parens
    │
    └─
```

* use already existing `splitAtTopLevelOnly` function

* add faster implemetation for `splitAtTopLevelOnly`

However, the faster version can't handle separators with multiple
characters right now. So instead of using buggy code or only using the
"slower" code, we've added a fast path where we use the faster code
wherever we can.

* use `splitAtTopLevelOnly` directly

* make split go brrrrrrr

* update changelog

* remove unncessary array.from call

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2022-09-14 14:08:56 +02:00

73 lines
1.8 KiB
JavaScript

import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'
let KEYWORDS = new Set(['inset', 'inherit', 'initial', 'revert', 'unset'])
let SPACE = /\ +(?![^(]*\))/g // Similar to the one above, but with spaces instead.
let LENGTH = /^-?(\d+|\.\d+)(.*?)$/g
export function parseBoxShadowValue(input) {
let shadows = splitAtTopLevelOnly(input, ',')
return shadows.map((shadow) => {
let value = shadow.trim()
let result = { raw: value }
let parts = value.split(SPACE)
let seen = new Set()
for (let part of parts) {
// Reset index, since the regex is stateful.
LENGTH.lastIndex = 0
// Keyword
if (!seen.has('KEYWORD') && KEYWORDS.has(part)) {
result.keyword = part
seen.add('KEYWORD')
}
// Length value
else if (LENGTH.test(part)) {
if (!seen.has('X')) {
result.x = part
seen.add('X')
} else if (!seen.has('Y')) {
result.y = part
seen.add('Y')
} else if (!seen.has('BLUR')) {
result.blur = part
seen.add('BLUR')
} else if (!seen.has('SPREAD')) {
result.spread = part
seen.add('SPREAD')
}
}
// Color or unknown
else {
if (!result.color) {
result.color = part
} else {
if (!result.unknown) result.unknown = []
result.unknown.push(part)
}
}
}
// Check if valid
result.valid = result.x !== undefined && result.y !== undefined
return result
})
}
export function formatBoxShadowValue(shadows) {
return shadows
.map((shadow) => {
if (!shadow.valid) {
return shadow.raw
}
return [shadow.keyword, shadow.x, shadow.y, shadow.blur, shadow.spread, shadow.color]
.filter(Boolean)
.join(' ')
})
.join(', ')
}