Skip to content

Commit 527031d

Browse files
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>
1 parent 3e6b8ac commit 527031d

File tree

7 files changed

+37
-60
lines changed

7 files changed

+37
-60
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3131
- Don't mutate shared config objects ([#9294](https://github.com/tailwindlabs/tailwindcss/pull/9294))
3232
- Fix ordering of parallel variants ([#9282](https://github.com/tailwindlabs/tailwindcss/pull/9282))
3333
- Handle variants in utility selectors using `:where()` and `:has()` ([#9309](https://github.com/tailwindlabs/tailwindcss/pull/9309))
34+
- Improve data type analyses for arbitrary values ([#9320](https://github.com/tailwindlabs/tailwindcss/pull/9320))
3435

3536
## [3.1.8] - 2022-08-05
3637

src/lib/generateRules.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ function splitWithSeparator(input, separator) {
461461
return [sharedState.NOT_ON_DEMAND]
462462
}
463463

464-
return Array.from(splitAtTopLevelOnly(input, separator))
464+
return splitAtTopLevelOnly(input, separator)
465465
}
466466

467467
function* recordCandidates(matches, classCandidate) {

src/util/dataTypes.js

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import { parseColor } from './color'
22
import { parseBoxShadowValue } from './parseBoxShadowValue'
3+
import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'
34

45
let cssFunctions = ['min', 'max', 'clamp', 'calc']
56

67
// Ref: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Types
78

8-
let COMMA = /,(?![^(]*\))/g // Comma separator that is not located between brackets. E.g.: `cubiz-bezier(a, b, c)` these don't count.
9-
let UNDERSCORE = /_(?![^(]*\))/g // Underscore separator that is not located between brackets. E.g.: `rgba(255,_255,_255)_black` these don't count.
10-
119
// This is not a data type, but rather a function that can normalize the
1210
// correct values.
1311
export function normalize(value, isRoot = true) {
@@ -61,7 +59,7 @@ export function number(value) {
6159
}
6260

6361
export function percentage(value) {
64-
return value.split(UNDERSCORE).every((part) => {
62+
return splitAtTopLevelOnly(value, '_').every((part) => {
6563
return /%$/g.test(part) || cssFunctions.some((fn) => new RegExp(`^${fn}\\(.+?%`).test(part))
6664
})
6765
}
@@ -86,7 +84,7 @@ let lengthUnits = [
8684
]
8785
let lengthUnitsPattern = `(?:${lengthUnits.join('|')})`
8886
export function length(value) {
89-
return value.split(UNDERSCORE).every((part) => {
87+
return splitAtTopLevelOnly(value, '_').every((part) => {
9088
return (
9189
part === '0' ||
9290
new RegExp(`${lengthUnitsPattern}$`).test(part) ||
@@ -115,7 +113,7 @@ export function shadow(value) {
115113
export function color(value) {
116114
let colors = 0
117115

118-
let result = value.split(UNDERSCORE).every((part) => {
116+
let result = splitAtTopLevelOnly(value, '_').every((part) => {
119117
part = normalize(part)
120118

121119
if (part.startsWith('var(')) return true
@@ -130,7 +128,7 @@ export function color(value) {
130128

131129
export function image(value) {
132130
let images = 0
133-
let result = value.split(COMMA).every((part) => {
131+
let result = splitAtTopLevelOnly(value, ',').every((part) => {
134132
part = normalize(part)
135133

136134
if (part.startsWith('var(')) return true
@@ -171,7 +169,7 @@ export function gradient(value) {
171169
let validPositions = new Set(['center', 'top', 'right', 'bottom', 'left'])
172170
export function position(value) {
173171
let positions = 0
174-
let result = value.split(UNDERSCORE).every((part) => {
172+
let result = splitAtTopLevelOnly(value, '_').every((part) => {
175173
part = normalize(part)
176174

177175
if (part.startsWith('var(')) return true
@@ -189,7 +187,7 @@ export function position(value) {
189187

190188
export function familyName(value) {
191189
let fonts = 0
192-
let result = value.split(COMMA).every((part) => {
190+
let result = splitAtTopLevelOnly(value, ',').every((part) => {
193191
part = normalize(part)
194192

195193
if (part.startsWith('var(')) return true

src/util/parseBoxShadowValue.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ let SPACE = /\ +(?![^(]*\))/g // Similar to the one above, but with spaces inste
55
let LENGTH = /^-?(\d+|\.\d+)(.*?)$/g
66

77
export function parseBoxShadowValue(input) {
8-
let shadows = Array.from(splitAtTopLevelOnly(input, ','))
8+
let shadows = splitAtTopLevelOnly(input, ',')
99
return shadows.map((shadow) => {
1010
let value = shadow.trim()
1111
let result = { raw: value }

src/util/splitAtTopLevelOnly.js

Lines changed: 23 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import * as regex from '../lib/regex'
2-
31
/**
42
* This splits a string on a top-level character.
53
*
@@ -15,57 +13,33 @@ import * as regex from '../lib/regex'
1513
* @param {string} input
1614
* @param {string} separator
1715
*/
18-
export function* splitAtTopLevelOnly(input, separator) {
19-
let SPECIALS = new RegExp(`[(){}\\[\\]${regex.escape(separator)}]`, 'g')
20-
21-
let depth = 0
22-
let lastIndex = 0
23-
let found = false
24-
let separatorIndex = 0
25-
let separatorStart = 0
26-
let separatorLength = separator.length
27-
28-
// Find all paren-like things & character
29-
// And only split on commas if they're top-level
30-
for (let match of input.matchAll(SPECIALS)) {
31-
let matchesSeparator = match[0] === separator[separatorIndex]
32-
let atEndOfSeparator = separatorIndex === separatorLength - 1
33-
let matchesFullSeparator = matchesSeparator && atEndOfSeparator
34-
35-
if (match[0] === '(') depth++
36-
if (match[0] === ')') depth--
37-
if (match[0] === '[') depth++
38-
if (match[0] === ']') depth--
39-
if (match[0] === '{') depth++
40-
if (match[0] === '}') depth--
41-
42-
if (matchesSeparator && depth === 0) {
43-
if (separatorStart === 0) {
44-
separatorStart = match.index
16+
export function splitAtTopLevelOnly(input, separator) {
17+
let stack = []
18+
let parts = []
19+
let lastPos = 0
20+
21+
for (let idx = 0; idx < input.length; idx++) {
22+
let char = input[idx]
23+
24+
if (stack.length === 0 && char === separator[0]) {
25+
if (separator.length === 1 || input.slice(idx, idx + separator.length) === separator) {
26+
parts.push(input.slice(lastPos, idx))
27+
lastPos = idx + separator.length
4528
}
46-
47-
separatorIndex++
4829
}
4930

50-
if (matchesFullSeparator && depth === 0) {
51-
found = true
52-
53-
yield input.substring(lastIndex, separatorStart)
54-
lastIndex = separatorStart + separatorLength
55-
}
56-
57-
if (separatorIndex === separatorLength) {
58-
separatorIndex = 0
59-
separatorStart = 0
31+
if (char === '(' || char === '[' || char === '{') {
32+
stack.push(char)
33+
} else if (
34+
(char === ')' && stack[stack.length - 1] === '(') ||
35+
(char === ']' && stack[stack.length - 1] === '[') ||
36+
(char === '}' && stack[stack.length - 1] === '{')
37+
) {
38+
stack.pop()
6039
}
6140
}
6241

63-
// Provide the last segment of the string if available
64-
// Otherwise the whole string since no `char`s were found
65-
// This mirrors the behavior of string.split()
66-
if (found) {
67-
yield input.substring(lastIndex)
68-
} else {
69-
yield input
70-
}
42+
parts.push(input.slice(lastPos))
43+
44+
return parts
7145
}

tests/arbitrary-values.test.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,9 @@
661661
.bg-opacity-\[var\(--value\)\] {
662662
--tw-bg-opacity: var(--value);
663663
}
664+
.bg-\[linear-gradient\(to_left\2c rgb\(var\(--green\)\)\2c blue\)\] {
665+
background-image: linear-gradient(to left, rgb(var(--green)), blue);
666+
}
664667
.bg-\[url\(\'\/path-to-image\.png\'\)\] {
665668
background-image: url('/path-to-image.png');
666669
}

tests/arbitrary-values.test.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@
220220
<div class="bg-[#0f0_var(--value)]"></div>
221221
<div class="bg-[var(--value1)_var(--value2)]"></div>
222222
<div class="bg-[color:var(--value1)_var(--value2)]"></div>
223+
<div class="bg-[linear-gradient(to_left,rgb(var(--green)),blue)]"></div>
223224

224225
<div class="bg-[url('/path-to-image.png')] bg-[url:var(--url)]"></div>
225226
<div class="bg-[linear-gradient(#eee,#fff)]"></div>

0 commit comments

Comments
 (0)