Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve data type analysis for arbitrary values #9320

Merged
merged 7 commits into from Sep 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/lib/generateRules.js
Expand Up @@ -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) {
Expand Down
16 changes: 7 additions & 9 deletions src/util/dataTypes.js
@@ -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) {
Expand Down Expand Up @@ -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))
})
}
Expand All @@ -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) ||
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/util/parseBoxShadowValue.js
Expand Up @@ -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 }
Expand Down
72 changes: 23 additions & 49 deletions src/util/splitAtTopLevelOnly.js
@@ -1,5 +1,3 @@
import * as regex from '../lib/regex'

/**
* This splits a string on a top-level character.
*
Expand All @@ -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')

let depth = 0
let lastIndex = 0
let found = false
let separatorIndex = 0
let separatorStart = 0
let separatorLength = separator.length

// 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
export function splitAtTopLevelOnly(input, separator) {
let stack = []
let parts = []
let lastPos = 0

for (let idx = 0; idx < input.length; idx++) {
let char = input[idx]

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
}
3 changes: 3 additions & 0 deletions tests/arbitrary-values.test.css
Expand Up @@ -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');
}
Expand Down
1 change: 1 addition & 0 deletions tests/arbitrary-values.test.html
Expand Up @@ -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>
Expand Down