diff --git a/packages/tailwindcss/package.json b/packages/tailwindcss/package.json index 6d6c67c7f237..05907861abd8 100644 --- a/packages/tailwindcss/package.json +++ b/packages/tailwindcss/package.json @@ -130,6 +130,7 @@ "@jridgewell/remapping": "^2.3.5", "@tailwindcss/oxide": "workspace:^", "@types/node": "catalog:", + "brace-expansion": "^5.0.4", "dedent": "1.7.1", "lightningcss": "catalog:", "magic-string": "^0.30.21", diff --git a/packages/tailwindcss/src/utils/brace-expansion.test.ts b/packages/tailwindcss/src/utils/brace-expansion.test.ts index bab93ac434a8..7cda2eeb9f59 100644 --- a/packages/tailwindcss/src/utils/brace-expansion.test.ts +++ b/packages/tailwindcss/src/utils/brace-expansion.test.ts @@ -19,9 +19,12 @@ describe('expand(…)', () => { ['a/{10..0..5}/b', ['a/10/b', 'a/5/b', 'a/0/b']], ['a/{10..0..-5}/b', ['a/0/b', 'a/5/b', 'a/10/b']], - // Numeric range with padding (we do not support padding) - ['a/{00..05}/b', ['a/0/b', 'a/1/b', 'a/2/b', 'a/3/b', 'a/4/b', 'a/5/b']], - ['a{001..9}b', ['a1b', 'a2b', 'a3b', 'a4b', 'a5b', 'a6b', 'a7b', 'a8b', 'a9b']], + // Numeric range with zero-padding + ['a/{00..05}/b', ['a/00/b', 'a/01/b', 'a/02/b', 'a/03/b', 'a/04/b', 'a/05/b']], + [ + 'a{001..9}b', + ['a001b', 'a002b', 'a003b', 'a004b', 'a005b', 'a006b', 'a007b', 'a008b', 'a009b'], + ], // Numeric range with step ['a/{0..5..2}/b', ['a/0/b', 'a/2/b', 'a/4/b']], @@ -61,15 +64,15 @@ describe('expand(…)', () => { ], ], - // Should not try to expand ranges with decimals - ['{1.1..2.2}', ['1.1..2.2']], + // Decimal ranges are not expanded; braces are preserved as a literal + ['{1.1..2.2}', ['{1.1..2.2}']], ])('should expand %s (%#)', (input, expected) => { expect(expand(input).sort()).toEqual(expected.sort()) }) - test('throws on unbalanced braces', () => { - expect(() => expand('a{b,c{d,e},{f,g}h}x{y,z')).toThrowErrorMatchingInlineSnapshot( - `[Error: The pattern \`x{y,z\` is not balanced.]`, + test('gracefully handles unbalanced braces', () => { + expect(expand('a{b,c{d,e},{f,g}h}x{y,z').sort()).toEqual( + ['abx{y,z', 'acdx{y,z', 'acex{y,z', 'afhx{y,z', 'aghx{y,z'].sort(), ) }) diff --git a/packages/tailwindcss/src/utils/brace-expansion.ts b/packages/tailwindcss/src/utils/brace-expansion.ts index ee382cd82a29..3db5b9e7f6cc 100644 --- a/packages/tailwindcss/src/utils/brace-expansion.ts +++ b/packages/tailwindcss/src/utils/brace-expansion.ts @@ -1,91 +1,10 @@ -import { segment } from './segment' +import { expand as braceExpand } from 'brace-expansion' -const NUMERICAL_RANGE = /^(-?\d+)\.\.(-?\d+)(?:\.\.(-?\d+))?$/ +const ZERO_STEP = /\{-?\d+\.\.-?\d+\.\.0+\}/ export function expand(pattern: string): string[] { - let index = pattern.indexOf('{') - if (index === -1) return [pattern] - - let result: string[] = [] - let pre = pattern.slice(0, index) - let rest = pattern.slice(index) - - // Find the matching closing brace - let depth = 0 - let endIndex = rest.lastIndexOf('}') - for (let i = 0; i < rest.length; i++) { - let char = rest[i] - if (char === '{') { - depth++ - } else if (char === '}') { - depth-- - if (depth === 0) { - endIndex = i - break - } - } - } - - if (endIndex === -1) { - throw new Error(`The pattern \`${pattern}\` is not balanced.`) - } - - let inside = rest.slice(1, endIndex) - let post = rest.slice(endIndex + 1) - let parts: string[] - - if (isSequence(inside)) { - parts = expandSequence(inside) - } else { - parts = segment(inside, ',') - } - - parts = parts.flatMap((part) => expand(part)) - - let expandedTail = expand(post) - - for (let tail of expandedTail) { - for (let part of parts) { - result.push(pre + part + tail) - } - } - return result -} - -function isSequence(str: string): boolean { - return NUMERICAL_RANGE.test(str) -} - -/** - * Expands a sequence string like "01..20" (optionally with a step). - */ -function expandSequence(seq: string): string[] { - let seqMatch = seq.match(NUMERICAL_RANGE) - if (!seqMatch) { - return [seq] - } - let [, start, end, stepStr] = seqMatch - let step = stepStr ? parseInt(stepStr, 10) : undefined - let result: string[] = [] - - if (/^-?\d+$/.test(start) && /^-?\d+$/.test(end)) { - let startNum = parseInt(start, 10) - let endNum = parseInt(end, 10) - - if (step === undefined) { - step = startNum <= endNum ? 1 : -1 - } - if (step === 0) { - throw new Error('Step cannot be zero in sequence expansion.') - } - - let increasing = startNum < endNum - if (increasing && step < 0) step = -step - if (!increasing && step > 0) step = -step - - for (let i = startNum; increasing ? i <= endNum : i >= endNum; i += step) { - result.push(i.toString()) - } + if (ZERO_STEP.test(pattern)) { + throw new Error('Step cannot be zero in sequence expansion.') } - return result + return braceExpand(pattern) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef88e3b9d1bc..0c88730114e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -477,6 +477,9 @@ importers: '@types/node': specifier: 'catalog:' version: 20.19.1 + brace-expansion: + specifier: ^5.0.4 + version: 5.0.4 dedent: specifier: 1.7.1 version: 1.7.1 @@ -2855,6 +2858,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + baseline-browser-mapping@2.8.29: resolution: {integrity: sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==} hasBin: true @@ -2876,6 +2883,10 @@ packages: brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -6954,6 +6965,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + baseline-browser-mapping@2.8.29: {} baseline-browser-mapping@2.9.11: {} @@ -6971,6 +6984,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1