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>
This commit is contained in:
parent
3e6b8ac6e5
commit
527031d5f6
7 changed files with 35 additions and 58 deletions
|
|
@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Don't mutate shared config objects ([#9294](https://github.com/tailwindlabs/tailwindcss/pull/9294))
|
||||
- Fix ordering of parallel variants ([#9282](https://github.com/tailwindlabs/tailwindcss/pull/9282))
|
||||
- Handle variants in utility selectors using `:where()` and `:has()` ([#9309](https://github.com/tailwindlabs/tailwindcss/pull/9309))
|
||||
- Improve data type analyses for arbitrary values ([#9320](https://github.com/tailwindlabs/tailwindcss/pull/9320))
|
||||
|
||||
## [3.1.8] - 2022-08-05
|
||||
|
||||
|
|
|
|||
|
|
@ -461,7 +461,7 @@ function splitWithSeparator(input, separator) {
|
|||
return [sharedState.NOT_ON_DEMAND]
|
||||
}
|
||||
|
||||
return Array.from(splitAtTopLevelOnly(input, separator))
|
||||
return splitAtTopLevelOnly(input, separator)
|
||||
}
|
||||
|
||||
function* recordCandidates(matches, classCandidate) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import { parseColor } from './color'
|
||||
import { parseBoxShadowValue } from './parseBoxShadowValue'
|
||||
import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'
|
||||
|
||||
let cssFunctions = ['min', 'max', 'clamp', 'calc']
|
||||
|
||||
// Ref: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Types
|
||||
|
||||
let COMMA = /,(?![^(]*\))/g // Comma separator that is not located between brackets. E.g.: `cubiz-bezier(a, b, c)` these don't count.
|
||||
let UNDERSCORE = /_(?![^(]*\))/g // Underscore separator that is not located between brackets. E.g.: `rgba(255,_255,_255)_black` these don't count.
|
||||
|
||||
// This is not a data type, but rather a function that can normalize the
|
||||
// correct values.
|
||||
export function normalize(value, isRoot = true) {
|
||||
|
|
@ -61,7 +59,7 @@ export function number(value) {
|
|||
}
|
||||
|
||||
export function percentage(value) {
|
||||
return value.split(UNDERSCORE).every((part) => {
|
||||
return splitAtTopLevelOnly(value, '_').every((part) => {
|
||||
return /%$/g.test(part) || cssFunctions.some((fn) => new RegExp(`^${fn}\\(.+?%`).test(part))
|
||||
})
|
||||
}
|
||||
|
|
@ -86,7 +84,7 @@ let lengthUnits = [
|
|||
]
|
||||
let lengthUnitsPattern = `(?:${lengthUnits.join('|')})`
|
||||
export function length(value) {
|
||||
return value.split(UNDERSCORE).every((part) => {
|
||||
return splitAtTopLevelOnly(value, '_').every((part) => {
|
||||
return (
|
||||
part === '0' ||
|
||||
new RegExp(`${lengthUnitsPattern}$`).test(part) ||
|
||||
|
|
@ -115,7 +113,7 @@ export function shadow(value) {
|
|||
export function color(value) {
|
||||
let colors = 0
|
||||
|
||||
let result = value.split(UNDERSCORE).every((part) => {
|
||||
let result = splitAtTopLevelOnly(value, '_').every((part) => {
|
||||
part = normalize(part)
|
||||
|
||||
if (part.startsWith('var(')) return true
|
||||
|
|
@ -130,7 +128,7 @@ export function color(value) {
|
|||
|
||||
export function image(value) {
|
||||
let images = 0
|
||||
let result = value.split(COMMA).every((part) => {
|
||||
let result = splitAtTopLevelOnly(value, ',').every((part) => {
|
||||
part = normalize(part)
|
||||
|
||||
if (part.startsWith('var(')) return true
|
||||
|
|
@ -171,7 +169,7 @@ export function gradient(value) {
|
|||
let validPositions = new Set(['center', 'top', 'right', 'bottom', 'left'])
|
||||
export function position(value) {
|
||||
let positions = 0
|
||||
let result = value.split(UNDERSCORE).every((part) => {
|
||||
let result = splitAtTopLevelOnly(value, '_').every((part) => {
|
||||
part = normalize(part)
|
||||
|
||||
if (part.startsWith('var(')) return true
|
||||
|
|
@ -189,7 +187,7 @@ export function position(value) {
|
|||
|
||||
export function familyName(value) {
|
||||
let fonts = 0
|
||||
let result = value.split(COMMA).every((part) => {
|
||||
let result = splitAtTopLevelOnly(value, ',').every((part) => {
|
||||
part = normalize(part)
|
||||
|
||||
if (part.startsWith('var(')) return true
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ let SPACE = /\ +(?![^(]*\))/g // Similar to the one above, but with spaces inste
|
|||
let LENGTH = /^-?(\d+|\.\d+)(.*?)$/g
|
||||
|
||||
export function parseBoxShadowValue(input) {
|
||||
let shadows = Array.from(splitAtTopLevelOnly(input, ','))
|
||||
let shadows = splitAtTopLevelOnly(input, ',')
|
||||
return shadows.map((shadow) => {
|
||||
let value = shadow.trim()
|
||||
let result = { raw: value }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import * as regex from '../lib/regex'
|
||||
|
||||
/**
|
||||
* This splits a string on a top-level character.
|
||||
*
|
||||
|
|
@ -15,57 +13,33 @@ import * as regex from '../lib/regex'
|
|||
* @param {string} input
|
||||
* @param {string} separator
|
||||
*/
|
||||
export function* splitAtTopLevelOnly(input, separator) {
|
||||
let SPECIALS = new RegExp(`[(){}\\[\\]${regex.escape(separator)}]`, 'g')
|
||||
export function splitAtTopLevelOnly(input, separator) {
|
||||
let stack = []
|
||||
let parts = []
|
||||
let lastPos = 0
|
||||
|
||||
let depth = 0
|
||||
let lastIndex = 0
|
||||
let found = false
|
||||
let separatorIndex = 0
|
||||
let separatorStart = 0
|
||||
let separatorLength = separator.length
|
||||
for (let idx = 0; idx < input.length; idx++) {
|
||||
let char = input[idx]
|
||||
|
||||
// Find all paren-like things & character
|
||||
// And only split on commas if they're top-level
|
||||
for (let match of input.matchAll(SPECIALS)) {
|
||||
let matchesSeparator = match[0] === separator[separatorIndex]
|
||||
let atEndOfSeparator = separatorIndex === separatorLength - 1
|
||||
let matchesFullSeparator = matchesSeparator && atEndOfSeparator
|
||||
|
||||
if (match[0] === '(') depth++
|
||||
if (match[0] === ')') depth--
|
||||
if (match[0] === '[') depth++
|
||||
if (match[0] === ']') depth--
|
||||
if (match[0] === '{') depth++
|
||||
if (match[0] === '}') depth--
|
||||
|
||||
if (matchesSeparator && depth === 0) {
|
||||
if (separatorStart === 0) {
|
||||
separatorStart = match.index
|
||||
if (stack.length === 0 && char === separator[0]) {
|
||||
if (separator.length === 1 || input.slice(idx, idx + separator.length) === separator) {
|
||||
parts.push(input.slice(lastPos, idx))
|
||||
lastPos = idx + separator.length
|
||||
}
|
||||
|
||||
separatorIndex++
|
||||
}
|
||||
|
||||
if (matchesFullSeparator && depth === 0) {
|
||||
found = true
|
||||
|
||||
yield input.substring(lastIndex, separatorStart)
|
||||
lastIndex = separatorStart + separatorLength
|
||||
}
|
||||
|
||||
if (separatorIndex === separatorLength) {
|
||||
separatorIndex = 0
|
||||
separatorStart = 0
|
||||
if (char === '(' || char === '[' || char === '{') {
|
||||
stack.push(char)
|
||||
} else if (
|
||||
(char === ')' && stack[stack.length - 1] === '(') ||
|
||||
(char === ']' && stack[stack.length - 1] === '[') ||
|
||||
(char === '}' && stack[stack.length - 1] === '{')
|
||||
) {
|
||||
stack.pop()
|
||||
}
|
||||
}
|
||||
|
||||
// Provide the last segment of the string if available
|
||||
// Otherwise the whole string since no `char`s were found
|
||||
// This mirrors the behavior of string.split()
|
||||
if (found) {
|
||||
yield input.substring(lastIndex)
|
||||
} else {
|
||||
yield input
|
||||
}
|
||||
parts.push(input.slice(lastPos))
|
||||
|
||||
return parts
|
||||
}
|
||||
|
|
|
|||
|
|
@ -661,6 +661,9 @@
|
|||
.bg-opacity-\[var\(--value\)\] {
|
||||
--tw-bg-opacity: var(--value);
|
||||
}
|
||||
.bg-\[linear-gradient\(to_left\2c rgb\(var\(--green\)\)\2c blue\)\] {
|
||||
background-image: linear-gradient(to left, rgb(var(--green)), blue);
|
||||
}
|
||||
.bg-\[url\(\'\/path-to-image\.png\'\)\] {
|
||||
background-image: url('/path-to-image.png');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -220,6 +220,7 @@
|
|||
<div class="bg-[#0f0_var(--value)]"></div>
|
||||
<div class="bg-[var(--value1)_var(--value2)]"></div>
|
||||
<div class="bg-[color:var(--value1)_var(--value2)]"></div>
|
||||
<div class="bg-[linear-gradient(to_left,rgb(var(--green)),blue)]"></div>
|
||||
|
||||
<div class="bg-[url('/path-to-image.png')] bg-[url:var(--url)]"></div>
|
||||
<div class="bg-[linear-gradient(#eee,#fff)]"></div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue