diff --git a/src/util/parseBoxShadowValue.js b/src/util/parseBoxShadowValue.js index ceafe650918d..0806ec699b88 100644 --- a/src/util/parseBoxShadowValue.js +++ b/src/util/parseBoxShadowValue.js @@ -1,10 +1,58 @@ let KEYWORDS = new Set(['inset', 'inherit', 'initial', 'revert', 'unset']) -let COMMA = /\,(?![^(]*\))/g // Comma separator that is not located between brackets. E.g.: `cubiz-bezier(a, b, c)` these don't count. let SPACE = /\ +(?![^(]*\))/g // Similar to the one above, but with spaces instead. let LENGTH = /^-?(\d+|\.\d+)(.*?)$/g +let SPECIALS = /[(),]/g + +/** + * This splits a string on top-level commas. + * + * Regex doesn't support recursion (at least not the JS-flavored version). + * So we have to use a tiny state machine to keep track of paren vs comma + * placement. Before we'd only exclude commas from the inner-most nested + * set of parens rather than any commas that were not contained in parens + * at all which is the intended behavior here. + * + * Expected behavior: + * var(--a, 0 0 1px rgb(0, 0, 0)), 0 0 1px rgb(0, 0, 0) + * ─┬─ ┬ ┬ ┬ + * x x x ╰──────── Split because top-level + * ╰──────────────┴──┴───────────── Ignored b/c inside >= 1 levels of parens + * + * @param {string} input + */ +function* splitByTopLevelCommas(input) { + SPECIALS.lastIndex = -1 + + let depth = 0 + let lastIndex = 0 + let found = false + + // Find all parens & commas + // And only split on commas if they're top-level + for (let match of input.matchAll(SPECIALS)) { + if (match[0] === '(') depth++ + if (match[0] === ')') depth-- + if (match[0] === ',' && depth === 0) { + found = true + + yield input.substring(lastIndex, match.index) + lastIndex = match.index + match[0].length + } + } + + // Provide the last segment of the string if available + // Otherwise the whole string since no commas were found + // This mirrors the behavior of string.split() + if (found) { + yield input.substring(lastIndex) + } else { + yield input + } +} + export function parseBoxShadowValue(input) { - let shadows = input.split(COMMA) + let shadows = Array.from(splitByTopLevelCommas(input)) return shadows.map((shadow) => { let value = shadow.trim() let result = { raw: value } diff --git a/tests/basic-usage.test.js b/tests/basic-usage.test.js index a26796ccc15a..ba2646ec6347 100644 --- a/tests/basic-usage.test.js +++ b/tests/basic-usage.test.js @@ -249,3 +249,31 @@ it('does not produce duplicate output when seeing variants preceding a wildcard `) }) }) + +it('it can parse box shadows with variables', () => { + let config = { + content: [{ raw: html`
` }], + theme: { + boxShadow: { + lg: 'var(-a, 0 35px 60px -15px rgba(0, 0, 0)), 0 0 1px rgb(0, 0, 0)', + }, + }, + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .shadow-lg { + --tw-shadow: var(-a, 0 35px 60px -15px rgba(0, 0, 0)), 0 0 1px rgb(0, 0, 0); + --tw-shadow-colored: 0 35px 60px -15px var(--tw-shadow-color), + 0 0 1px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), + var(--tw-shadow); + } + `) + }) +})