mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Add @source inline(…) (#17147)
This PR adds a new experimental feature that can be used to force-inline
utilities based on an input string. The idea is that all utilities
matching the source string will be included in your CSS:
```css
/* input.css */
@source inline('underline');
/* output.css */
.underline {
text-decoration: underline;
}
```
Additionally, the source input is brace-expanded, meaning you can use
one line to inline a whole namespace easily:
```css
/* input.css */
@source inline('{hover:,}bg-red-{50,{100..900..100},950}');
/* output.css */
.bg-red-50 {
background-color: var(--color-red-50);
}
.bg-red-100 {
background-color: var(--color-red-100);
}
.bg-red-200 {
background-color: var(--color-red-200);
}
.bg-red-300 {
background-color: var(--color-red-300);
}
.bg-red-400 {
background-color: var(--color-red-400);
}
.bg-red-500 {
background-color: var(--color-red-500);
}
.bg-red-600 {
background-color: var(--color-red-600);
}
.bg-red-700 {
background-color: var(--color-red-700);
}
.bg-red-800 {
background-color: var(--color-red-800);
}
.bg-red-900 {
background-color: var(--color-red-900);
}
.bg-red-950 {
background-color: var(--color-red-950);
}
@media (hover: hover) {
.hover\\:bg-red-50:hover {
background-color: var(--color-red-50);
}
.hover\\:bg-red-100:hover {
background-color: var(--color-red-100);
}
.hover\\:bg-red-200:hover {
background-color: var(--color-red-200);
}
.hover\\:bg-red-300:hover {
background-color: var(--color-red-300);
}
.hover\\:bg-red-400:hover {
background-color: var(--color-red-400);
}
.hover\\:bg-red-500:hover {
background-color: var(--color-red-500);
}
.hover\\:bg-red-600:hover {
background-color: var(--color-red-600);
}
.hover\\:bg-red-700:hover {
background-color: var(--color-red-700);
}
.hover\\:bg-red-800:hover {
background-color: var(--color-red-800);
}
.hover\\:bg-red-900:hover {
background-color: var(--color-red-900);
}
.hover\\:bg-red-950:hover {
background-color: var(--color-red-950);
}
}
```
This feature is also compatible with the `not` keyword that we're about
to add to `@source "…"` in a follow-up PR. This can be used to set up an
ignore list purely in CSS. The following code snippet, for example, will
ensure that the `.container` utility is never created:
```css
@theme {
--breakpoint-sm: 40rem;
--breakpoint-md: 48rem;
--breakpoint-lg: 64rem;
--breakpoint-xl: 80rem;
--breakpoint-2xl: 96rem;
}
@source not inline("container");
@tailwind utilities;
```
## Test plan
- See added unit tests
- The new brace expansion library was also benchmarked against the
popular `braces` library:
```
clk: ~3.96 GHz
cpu: Apple M4 Max
runtime: bun 1.1.34 (arm64-darwin)
benchmark avg (min … max) p75 / p99 (min … top 1%)
-------------------------------------------
-------------------------------
braces 31.05 ms/iter 32.35 ms █ █
(28.14 ms … 36.35 ms) 35.14 ms ██ █
( 0.00 b … 116.45 mb) 18.71 mb ██████▁▁▁██▁█▁██▁█▁▁█
./brace-expansion 19.34 ms/iter 21.69 ms █
(12.53 ms … 26.63 ms) 25.53 ms ▅ ▅ █ █
( 0.00 b … 114.13 mb) 11.86 mb █▁▅▁██▁▅█▅█▅▁█▅█▅▅▁▅█
┌ ┐
╷┌────┬─┐ ╷
braces ├┤ │ ├─────┤
╵└────┴─┘ ╵
╷ ┌────┬───┐ ╷
./brace-expansion ├────────┤ │ ├───────┤
╵ └────┴───┘ ╵
└ ┘
12.53 ms 23.84 ms 35.14 ms
```
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
parent
ca408d0612
commit
215f4f348b
@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- _Experimental_: Add `any-pointer-none`, `any-pointer-coarse`, and `any-pointer-fine` variants ([#16941](https://github.com/tailwindlabs/tailwindcss/pull/16941))
|
||||
- _Experimental_: Add `user-valid` and `user-invalid` variants ([#12370](https://github.com/tailwindlabs/tailwindcss/pull/12370))
|
||||
- _Experimental_: Add `wrap-anywhere`, `wrap-break-word`, and `wrap-normal` utilities ([#12128](https://github.com/tailwindlabs/tailwindcss/pull/12128))
|
||||
- _Experimental_: Add `@source inline(…)` ([#17147](https://github.com/tailwindlabs/tailwindcss/pull/17147))
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@ -2,5 +2,6 @@ export const enableDetailsContent = process.env.FEATURES_ENV !== 'stable'
|
||||
export const enableInvertedColors = process.env.FEATURES_ENV !== 'stable'
|
||||
export const enablePointerVariants = process.env.FEATURES_ENV !== 'stable'
|
||||
export const enableScripting = process.env.FEATURES_ENV !== 'stable'
|
||||
export const enableSourceInline = process.env.FEATURES_ENV !== 'stable'
|
||||
export const enableUserValid = process.env.FEATURES_ENV !== 'stable'
|
||||
export const enableWrapAnywhere = process.env.FEATURES_ENV !== 'stable'
|
||||
|
||||
@ -3194,6 +3194,185 @@ describe('@source', () => {
|
||||
{ pattern: './php/secr3t/smarty.php', base: '/root' },
|
||||
])
|
||||
})
|
||||
|
||||
describe('@source inline(…)', () => {
|
||||
test('always includes the candidate', async () => {
|
||||
let { build } = await compile(
|
||||
css`
|
||||
@source inline("underline");
|
||||
@tailwind utilities;
|
||||
`,
|
||||
{ base: '/root' },
|
||||
)
|
||||
|
||||
expect(build([])).toMatchInlineSnapshot(`
|
||||
".underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('applies brace expansion', async () => {
|
||||
let { build } = await compile(
|
||||
css`
|
||||
@theme {
|
||||
--color-red-50: oklch(0.971 0.013 17.38);
|
||||
--color-red-100: oklch(0.936 0.032 17.717);
|
||||
--color-red-200: oklch(0.885 0.062 18.334);
|
||||
--color-red-300: oklch(0.808 0.114 19.571);
|
||||
--color-red-400: oklch(0.704 0.191 22.216);
|
||||
--color-red-500: oklch(0.637 0.237 25.331);
|
||||
--color-red-600: oklch(0.577 0.245 27.325);
|
||||
--color-red-700: oklch(0.505 0.213 27.518);
|
||||
--color-red-800: oklch(0.444 0.177 26.899);
|
||||
--color-red-900: oklch(0.396 0.141 25.723);
|
||||
--color-red-950: oklch(0.258 0.092 26.042);
|
||||
}
|
||||
@source inline("bg-red-{50,{100..900..100},950}");
|
||||
@tailwind utilities;
|
||||
`,
|
||||
{ base: '/root' },
|
||||
)
|
||||
|
||||
expect(build([])).toMatchInlineSnapshot(`
|
||||
":root, :host {
|
||||
--color-red-50: oklch(0.971 0.013 17.38);
|
||||
--color-red-100: oklch(0.936 0.032 17.717);
|
||||
--color-red-200: oklch(0.885 0.062 18.334);
|
||||
--color-red-300: oklch(0.808 0.114 19.571);
|
||||
--color-red-400: oklch(0.704 0.191 22.216);
|
||||
--color-red-500: oklch(0.637 0.237 25.331);
|
||||
--color-red-600: oklch(0.577 0.245 27.325);
|
||||
--color-red-700: oklch(0.505 0.213 27.518);
|
||||
--color-red-800: oklch(0.444 0.177 26.899);
|
||||
--color-red-900: oklch(0.396 0.141 25.723);
|
||||
--color-red-950: oklch(0.258 0.092 26.042);
|
||||
}
|
||||
.bg-red-50 {
|
||||
background-color: var(--color-red-50);
|
||||
}
|
||||
.bg-red-100 {
|
||||
background-color: var(--color-red-100);
|
||||
}
|
||||
.bg-red-200 {
|
||||
background-color: var(--color-red-200);
|
||||
}
|
||||
.bg-red-300 {
|
||||
background-color: var(--color-red-300);
|
||||
}
|
||||
.bg-red-400 {
|
||||
background-color: var(--color-red-400);
|
||||
}
|
||||
.bg-red-500 {
|
||||
background-color: var(--color-red-500);
|
||||
}
|
||||
.bg-red-600 {
|
||||
background-color: var(--color-red-600);
|
||||
}
|
||||
.bg-red-700 {
|
||||
background-color: var(--color-red-700);
|
||||
}
|
||||
.bg-red-800 {
|
||||
background-color: var(--color-red-800);
|
||||
}
|
||||
.bg-red-900 {
|
||||
background-color: var(--color-red-900);
|
||||
}
|
||||
.bg-red-950 {
|
||||
background-color: var(--color-red-950);
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('adds multiple inline sources separated by spaces', async () => {
|
||||
let { build } = await compile(
|
||||
css`
|
||||
@theme {
|
||||
--color-red-100: oklch(0.936 0.032 17.717);
|
||||
--color-red-200: oklch(0.885 0.062 18.334);
|
||||
}
|
||||
@source inline("block bg-red-{100..200..100}");
|
||||
@tailwind utilities;
|
||||
`,
|
||||
{ base: '/root' },
|
||||
)
|
||||
|
||||
expect(build([])).toMatchInlineSnapshot(`
|
||||
":root, :host {
|
||||
--color-red-100: oklch(0.936 0.032 17.717);
|
||||
--color-red-200: oklch(0.885 0.062 18.334);
|
||||
}
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
.bg-red-100 {
|
||||
background-color: var(--color-red-100);
|
||||
}
|
||||
.bg-red-200 {
|
||||
background-color: var(--color-red-200);
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('ignores invalid inline candidates', async () => {
|
||||
let { build } = await compile(
|
||||
css`
|
||||
@source inline("my-cucumber");
|
||||
@tailwind utilities;
|
||||
`,
|
||||
{ base: '/root' },
|
||||
)
|
||||
|
||||
expect(build([])).toMatchInlineSnapshot(`""`)
|
||||
})
|
||||
|
||||
test('can be negated', async () => {
|
||||
let { build } = await compile(
|
||||
css`
|
||||
@theme {
|
||||
--breakpoint-sm: 40rem;
|
||||
--breakpoint-md: 48rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-2xl: 96rem;
|
||||
}
|
||||
@source not inline("container");
|
||||
@tailwind utilities;
|
||||
`,
|
||||
{ base: '/root' },
|
||||
)
|
||||
|
||||
expect(build(['container'])).toMatchInlineSnapshot(`""`)
|
||||
})
|
||||
|
||||
test('applies brace expansion to negated sources', async () => {
|
||||
let { build } = await compile(
|
||||
css`
|
||||
@theme {
|
||||
--color-red-50: oklch(0.971 0.013 17.38);
|
||||
--color-red-100: oklch(0.936 0.032 17.717);
|
||||
--color-red-200: oklch(0.885 0.062 18.334);
|
||||
--color-red-300: oklch(0.808 0.114 19.571);
|
||||
--color-red-400: oklch(0.704 0.191 22.216);
|
||||
--color-red-500: oklch(0.637 0.237 25.331);
|
||||
--color-red-600: oklch(0.577 0.245 27.325);
|
||||
--color-red-700: oklch(0.505 0.213 27.518);
|
||||
--color-red-800: oklch(0.444 0.177 26.899);
|
||||
--color-red-900: oklch(0.396 0.141 25.723);
|
||||
--color-red-950: oklch(0.258 0.092 26.042);
|
||||
}
|
||||
@source not inline("bg-red-{50,{100..900..100},950}");
|
||||
@tailwind utilities;
|
||||
`,
|
||||
{ base: '/root' },
|
||||
)
|
||||
|
||||
expect(build(['bg-red-500', 'bg-red-700'])).toMatchInlineSnapshot(`""`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('@custom-variant', () => {
|
||||
|
||||
@ -26,8 +26,10 @@ import { applyVariant, compileCandidates } from './compile'
|
||||
import { substituteFunctions } from './css-functions'
|
||||
import * as CSS from './css-parser'
|
||||
import { buildDesignSystem, type DesignSystem } from './design-system'
|
||||
import { enableSourceInline } from './feature-flags'
|
||||
import { Theme, ThemeOptions } from './theme'
|
||||
import { createCssUtility } from './utilities'
|
||||
import { expand } from './utils/brace-expansion'
|
||||
import { escape, unescape } from './utils/escape'
|
||||
import { segment } from './utils/segment'
|
||||
import { compoundsForSelectors, IS_VALID_VARIANT_NAME } from './variants'
|
||||
@ -127,6 +129,8 @@ async function parseCss(
|
||||
let utilitiesNode = null as AtRule | null
|
||||
let variantNodes: AtRule[] = []
|
||||
let globs: { base: string; pattern: string }[] = []
|
||||
let inlineCandidates: string[] = []
|
||||
let ignoredCandidates: string[] = []
|
||||
let root = null as Root
|
||||
|
||||
// Handle at-rules
|
||||
@ -208,7 +212,22 @@ async function parseCss(
|
||||
throw new Error('`@source` cannot be nested.')
|
||||
}
|
||||
|
||||
let not = false
|
||||
let inline = false
|
||||
let path = node.params
|
||||
|
||||
if (enableSourceInline) {
|
||||
if (path[0] === 'n' && path.startsWith('not ')) {
|
||||
not = true
|
||||
path = path.slice(4)
|
||||
}
|
||||
|
||||
if (path[0] === 'i' && path.startsWith('inline(')) {
|
||||
inline = true
|
||||
path = path.slice(7, -1)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(path[0] === '"' && path[path.length - 1] !== '"') ||
|
||||
(path[0] === "'" && path[path.length - 1] !== "'") ||
|
||||
@ -216,7 +235,20 @@ async function parseCss(
|
||||
) {
|
||||
throw new Error('`@source` paths must be quoted.')
|
||||
}
|
||||
globs.push({ base: context.base as string, pattern: path.slice(1, -1) })
|
||||
|
||||
let source = path.slice(1, -1)
|
||||
|
||||
if (enableSourceInline && inline) {
|
||||
let destination = not ? ignoredCandidates : inlineCandidates
|
||||
let sources = segment(source, ' ')
|
||||
for (let source of sources) {
|
||||
for (let candidate of expand(source)) {
|
||||
destination.push(candidate)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
globs.push({ base: context.base as string, pattern: source })
|
||||
}
|
||||
replaceWith([])
|
||||
return
|
||||
}
|
||||
@ -505,6 +537,12 @@ async function parseCss(
|
||||
designSystem.important = important
|
||||
}
|
||||
|
||||
if (ignoredCandidates.length > 0) {
|
||||
for (let candidate of ignoredCandidates) {
|
||||
designSystem.invalidCandidates.add(candidate)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply hooks from backwards compatibility layer. This function takes a lot
|
||||
// of random arguments because it really just needs access to "the world" to
|
||||
// do whatever ungodly things it needs to do to make things backwards
|
||||
@ -603,6 +641,7 @@ async function parseCss(
|
||||
root,
|
||||
utilitiesNode,
|
||||
features,
|
||||
inlineCandidates,
|
||||
}
|
||||
}
|
||||
|
||||
@ -615,7 +654,8 @@ export async function compileAst(
|
||||
features: Features
|
||||
build(candidates: string[]): AstNode[]
|
||||
}> {
|
||||
let { designSystem, ast, globs, root, utilitiesNode, features } = await parseCss(input, opts)
|
||||
let { designSystem, ast, globs, root, utilitiesNode, features, inlineCandidates } =
|
||||
await parseCss(input, opts)
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
ast.unshift(comment(`! tailwindcss v${version} | MIT License | https://tailwindcss.com `))
|
||||
@ -632,6 +672,14 @@ export async function compileAst(
|
||||
let allValidCandidates = new Set<string>()
|
||||
let compiled = null as AstNode[] | null
|
||||
let previousAstNodeCount = 0
|
||||
let defaultDidChange = false
|
||||
|
||||
for (let candidate of inlineCandidates) {
|
||||
if (!designSystem.invalidCandidates.has(candidate)) {
|
||||
allValidCandidates.add(candidate)
|
||||
defaultDidChange = true
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
globs,
|
||||
@ -647,7 +695,8 @@ export async function compileAst(
|
||||
return compiled
|
||||
}
|
||||
|
||||
let didChange = false
|
||||
let didChange = defaultDidChange
|
||||
defaultDidChange = false
|
||||
|
||||
// Add all new candidates unless we know that they are invalid.
|
||||
let prevSize = allValidCandidates.size
|
||||
|
||||
14
packages/tailwindcss/src/utils/brace-expansion.bench.ts
Normal file
14
packages/tailwindcss/src/utils/brace-expansion.bench.ts
Normal file
@ -0,0 +1,14 @@
|
||||
// import braces from 'braces'
|
||||
import { bench } from 'vitest'
|
||||
import { expand } from './brace-expansion'
|
||||
|
||||
const PATTERN =
|
||||
'{{xs,sm,md,lg}:,}{border-{x,y,t,r,b,l,s,e},bg,text,cursor,accent}-{{red,orange,amber,yellow,lime,green,emerald,teal,cyan,sky,blue,indigo,violet,purple,fuchsia,pink,rose,slate,gray,zinc,neutral,stone}-{50,{100..900..100},950},black,white}{,/{0..100}}'
|
||||
|
||||
// bench('braces', () => {
|
||||
// void braces.expand(PATTERN)
|
||||
// })
|
||||
|
||||
bench('./brace-expansion', () => {
|
||||
void expand(PATTERN)
|
||||
})
|
||||
80
packages/tailwindcss/src/utils/brace-expansion.test.ts
Normal file
80
packages/tailwindcss/src/utils/brace-expansion.test.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import { expand } from './brace-expansion'
|
||||
|
||||
describe('expand(…)', () => {
|
||||
test.each([
|
||||
['a/b/c', ['a/b/c']],
|
||||
|
||||
// Groups
|
||||
['a/{x,y,z}/b', ['a/x/b', 'a/y/b', 'a/z/b']],
|
||||
['{a,b}/{x,y}', ['a/x', 'a/y', 'b/x', 'b/y']],
|
||||
['{{xs,sm,md,lg}:,}hidden', ['xs:hidden', 'sm:hidden', 'md:hidden', 'lg:hidden', 'hidden']],
|
||||
|
||||
// Numeric ranges
|
||||
['a/{0..5}/b', ['a/0/b', 'a/1/b', 'a/2/b', 'a/3/b', 'a/4/b', 'a/5/b']],
|
||||
['a/{-5..0}/b', ['a/-5/b', 'a/-4/b', 'a/-3/b', 'a/-2/b', 'a/-1/b', 'a/0/b']],
|
||||
['a/{0..-5}/b', ['a/0/b', 'a/-1/b', 'a/-2/b', 'a/-3/b', 'a/-4/b', 'a/-5/b']],
|
||||
|
||||
// Numeric range with 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']],
|
||||
[
|
||||
'bg-red-{100..900..100}',
|
||||
[
|
||||
'bg-red-100',
|
||||
'bg-red-200',
|
||||
'bg-red-300',
|
||||
'bg-red-400',
|
||||
'bg-red-500',
|
||||
'bg-red-600',
|
||||
'bg-red-700',
|
||||
'bg-red-800',
|
||||
'bg-red-900',
|
||||
],
|
||||
],
|
||||
|
||||
// Nested braces
|
||||
['a{b,c,/{x,y}}/e', ['ab/e', 'ac/e', 'a/x/e', 'a/y/e']],
|
||||
['a{b,c,/{x,y},{z,w}}/e', ['ab/e', 'ac/e', 'a/x/e', 'a/y/e', 'az/e', 'aw/e']],
|
||||
['a{b,c,/{x,y},{0..2}}/e', ['ab/e', 'ac/e', 'a/x/e', 'a/y/e', 'a0/e', 'a1/e', 'a2/e']],
|
||||
[
|
||||
'bg-red-{50,{100..900..100},950}',
|
||||
[
|
||||
'bg-red-50',
|
||||
'bg-red-100',
|
||||
'bg-red-200',
|
||||
'bg-red-300',
|
||||
'bg-red-400',
|
||||
'bg-red-500',
|
||||
'bg-red-600',
|
||||
'bg-red-700',
|
||||
'bg-red-800',
|
||||
'bg-red-900',
|
||||
'bg-red-950',
|
||||
],
|
||||
],
|
||||
|
||||
// Should not try to expand ranges with decimals
|
||||
['{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('throws when step is set to zero', () => {
|
||||
expect(() => expand('a{0..5..0}/b')).toThrowErrorMatchingInlineSnapshot(
|
||||
`[Error: Step cannot be zero in sequence expansion.]`,
|
||||
)
|
||||
})
|
||||
})
|
||||
103
packages/tailwindcss/src/utils/brace-expansion.ts
Normal file
103
packages/tailwindcss/src/utils/brace-expansion.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { segment } from './segment'
|
||||
|
||||
const NUMERICAL_RANGE = /^(-?\d+)\.\.(-?\d+)(?:\.\.(-?\d+))?$/
|
||||
|
||||
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)
|
||||
|
||||
// Determine padding length (if any) but don't count the sign as length
|
||||
let padLength = Math.max(start.replace(/^-/, '').length, end.replace(/^-/, '').length)
|
||||
|
||||
if (step === undefined) {
|
||||
step = startNum <= endNum ? 1 : -1
|
||||
}
|
||||
if (step === 0) {
|
||||
throw new Error('Step cannot be zero in sequence expansion.')
|
||||
}
|
||||
if (step > 0) {
|
||||
for (let i = startNum; i <= endNum; i += step) {
|
||||
let numStr = i.toString()
|
||||
if (numStr.length < padLength) {
|
||||
numStr = numStr.padStart(padLength, '0')
|
||||
}
|
||||
result.push(numStr)
|
||||
}
|
||||
} else {
|
||||
for (let i = startNum; i >= endNum; i += step) {
|
||||
let numStr = i.toString()
|
||||
if (numStr.length < padLength) {
|
||||
numStr = numStr.padStart(padLength, '0')
|
||||
}
|
||||
result.push(numStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user