diff --git a/CHANGELOG.md b/CHANGELOG.md index a3d2cf2bc40e..1af2180d1abb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index 558cd47ebfa3..116813a6d1ff 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -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) { diff --git a/src/util/dataTypes.js b/src/util/dataTypes.js index 1491cecd9601..d7f04f7044bc 100644 --- a/src/util/dataTypes.js +++ b/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) { @@ -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 diff --git a/src/util/parseBoxShadowValue.js b/src/util/parseBoxShadowValue.js index 16fc8eb1b03a..4be3efa04c7c 100644 --- a/src/util/parseBoxShadowValue.js +++ b/src/util/parseBoxShadowValue.js @@ -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 } diff --git a/src/util/splitAtTopLevelOnly.js b/src/util/splitAtTopLevelOnly.js index 8297a6feca4e..63fd767403ce 100644 --- a/src/util/splitAtTopLevelOnly.js +++ b/src/util/splitAtTopLevelOnly.js @@ -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') - - 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 } diff --git a/tests/arbitrary-values.test.css b/tests/arbitrary-values.test.css index 9cf6b8325898..df4e216c9d5e 100644 --- a/tests/arbitrary-values.test.css +++ b/tests/arbitrary-values.test.css @@ -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'); } diff --git a/tests/arbitrary-values.test.html b/tests/arbitrary-values.test.html index 142240b14590..eb011386437c 100644 --- a/tests/arbitrary-values.test.html +++ b/tests/arbitrary-values.test.html @@ -220,6 +220,7 @@
+