Skip to content

Commit caeb39c

Browse files
authored
feat(preset-wind4): support theme function in bracket (#4970)
1 parent 1d1ca31 commit caeb39c

File tree

9 files changed

+119
-36
lines changed

9 files changed

+119
-36
lines changed

packages-presets/preset-wind4/src/rules/variables.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,15 @@ export const cssVariables: Rule<Theme>[] = [
3939
]
4040

4141
export const cssProperty: Rule<Theme>[] = [
42-
[/^\[(.*)\]$/, ([_, body]) => {
42+
[/^\[(.*)\]$/, ([_, body], { theme }) => {
4343
if (!body.includes(':'))
4444
return
4545

4646
const [prop, ...rest] = body.split(':')
4747
const value = rest.join(':')
4848

4949
if (!isURI(body) && /^[\w-]+$/.test(prop) && isValidCSSBody(value)) {
50-
const parsed = h.bracket(`[${value}]`)
50+
const parsed = h.bracket(`[${value}]`, theme)
5151

5252
if (parsed)
5353
return { [prop]: parsed }

packages-presets/preset-wind4/src/utils/handlers/handlers.ts

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import type { Theme } from '../../theme'
12
import { escapeSelector } from '@unocss/core'
23
import { globalKeywords } from '../mappings'
4+
import { themeTracking } from '../track'
5+
import { getThemeByKey } from '../utilities'
36
import { bracketTypeRe, numberRE, numberWithUnitRE, unitOnlyMap, unitOnlyRE } from './regex'
47

58
// Not all, but covers most high frequency attributes
@@ -147,7 +150,23 @@ export function fraction(str: string) {
147150
}
148151
}
149152

150-
function bracketWithType(str: string, requiredType?: string) {
153+
function processThemeVariable(name: string, key: keyof Theme, paths: string[], theme: Theme) {
154+
const valOrObj = getThemeByKey(theme, key, paths)
155+
const hasDefault = typeof valOrObj === 'object' && 'DEFAULT' in valOrObj
156+
157+
if (hasDefault)
158+
paths.push('DEFAULT')
159+
160+
const val = hasDefault ? valOrObj.DEFAULT : valOrObj
161+
const varKey = hasDefault && key !== 'spacing' ? `${name}.DEFAULT` : name
162+
163+
if (val != null)
164+
themeTracking(key, paths.length ? paths : undefined)
165+
166+
return { val, varKey }
167+
}
168+
169+
function bracketWithType(str: string, requiredType?: string, theme?: Theme) {
151170
if (str && str.startsWith('[') && str.endsWith(']')) {
152171
let base: string | undefined
153172
let hintedType: string | undefined
@@ -175,8 +194,33 @@ function bracketWithType(str: string, requiredType?: string) {
175194
return
176195

177196
if (base.startsWith('--')) {
178-
const [name, defaultValue] = base.slice(2).split(',')
179-
base = `var(--${escapeSelector(name)}${defaultValue ? `, ${defaultValue}` : ''})`
197+
const calcMatch = base.match(/^--([\w.-]+)\(([^)]+)\)$/)
198+
if (calcMatch != null && theme) {
199+
// Handle theme function with calculation: --theme.key(factor)
200+
const [, name, factor] = calcMatch
201+
const [key, ...paths] = name.split('.') as [keyof Theme, ...string[]]
202+
const { val, varKey } = processThemeVariable(name, key, paths, theme)
203+
204+
if (val != null)
205+
base = `calc(var(--${escapeSelector(varKey.replaceAll('.', '-'))}) * ${factor})`
206+
}
207+
else {
208+
// Handle regular CSS variable: --name or --theme.key with optional default
209+
const [name, defaultValue] = base.slice(2).split(',')
210+
const suffix = defaultValue ? `, ${defaultValue}` : ''
211+
const escapedName = escapeSelector(name)
212+
213+
if (theme) {
214+
const [key, ...paths] = name.split('.') as [keyof Theme, ...string[]]
215+
const { val, varKey } = processThemeVariable(name, key, paths, theme)
216+
base = val != null
217+
? `var(--${escapeSelector(varKey.replaceAll('.', '-'))}${suffix})`
218+
: `var(--${escapedName}${suffix})`
219+
}
220+
else {
221+
base = `var(--${escapedName}${suffix})`
222+
}
223+
}
180224
}
181225

182226
let curly = 0
@@ -222,28 +266,28 @@ function bracketWithType(str: string, requiredType?: string) {
222266
}
223267
}
224268

225-
export function bracket(str: string) {
226-
return bracketWithType(str)
269+
export function bracket(str: string, theme?: Theme) {
270+
return bracketWithType(str, undefined, theme)
227271
}
228272

229-
export function bracketOfColor(str: string) {
230-
return bracketWithType(str, 'color')
273+
export function bracketOfColor(str: string, theme?: Theme) {
274+
return bracketWithType(str, 'color', theme)
231275
}
232276

233-
export function bracketOfLength(str: string) {
234-
return bracketWithType(str, 'length') || bracketWithType(str, 'size')
277+
export function bracketOfLength(str: string, theme?: Theme) {
278+
return bracketWithType(str, 'length', theme) || bracketWithType(str, 'size', theme)
235279
}
236280

237-
export function bracketOfPosition(str: string) {
238-
return bracketWithType(str, 'position')
281+
export function bracketOfPosition(str: string, theme?: Theme) {
282+
return bracketWithType(str, 'position', theme)
239283
}
240284

241-
export function bracketOfFamily(str: string) {
242-
return bracketWithType(str, 'family')
285+
export function bracketOfFamily(str: string, theme?: Theme) {
286+
return bracketWithType(str, 'family', theme)
243287
}
244288

245-
export function bracketOfNumber(str: string) {
246-
return bracketWithType(str, 'number')
289+
export function bracketOfNumber(str: string, theme?: Theme) {
290+
return bracketWithType(str, 'number', theme)
247291
}
248292

249293
export function cssvar(str: string) {
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import type { Theme } from '../../theme'
12
import { createValueHandler } from '@unocss/rule-utils'
23
import * as valueHandlers from './handlers'
34

4-
export const handler = createValueHandler(valueHandlers)
5+
export const handler = createValueHandler<string, Theme>(valueHandlers)
56
export const h = handler
67

78
export { valueHandlers }

packages-presets/preset-wind4/src/utils/utilities.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function directionSize(
5555
return map[direction].map(i => [formatter(property, i), isNegative ? `calc(var(--${escapeSelector(`spacing-${size}`)}) * -1)` : `var(--${escapeSelector(`spacing-${size}`)})`])
5656
}
5757

58-
v = h.bracket.cssvar.global.auto.fraction.rem(isNegative ? `-${size}` : size)
58+
v = h.bracket.cssvar.global.auto.fraction.rem(isNegative ? `-${size}` : size, theme)
5959

6060
if (v != null) {
6161
return map[direction].map(i => [formatter(property, i), v])
@@ -147,7 +147,7 @@ export function parseColor(body: string, theme: Theme) {
147147
let { no, keys, color } = parsed ?? {}
148148

149149
if (!color) {
150-
const bracket = h.bracketOfColor(main)
150+
const bracket = h.bracketOfColor(main, theme)
151151
const bracketOrMain = bracket || main
152152

153153
if (h.numberWithUnit(bracketOrMain))
@@ -213,10 +213,10 @@ export function parseThemeColor(theme: Theme, keys: string[]) {
213213
export function getThemeByKey(theme: Theme, themeKey: keyof Theme, keys: string[]) {
214214
const obj = theme[themeKey]
215215
function deepGet(current: any, path: string[]): any {
216-
if (!current || typeof current !== 'object')
217-
return undefined
218216
if (path.length === 0)
219217
return current
218+
if (!current || typeof current !== 'object')
219+
return undefined
220220
// First, check if the path is a flat key (e.g., foo-bar)
221221
for (let i = path.length; i > 0; i--) {
222222
const flatKey = path.slice(0, i).join('-')

packages-presets/preset-wind4/src/variants/pseudo.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@ import {
1111
import { getBracket, h, variantGetBracket } from '../utils'
1212

1313
export function variantPseudoClassesAndElements(): VariantObject<Theme>[] {
14-
const utils: PseudoVariantUtilities = { getBracket, h, variantGetBracket }
14+
const utils: PseudoVariantUtilities<Theme> = { getBracket, h: h as unknown as PseudoVariantUtilities<Theme>['h'], variantGetBracket }
1515
return createPseudoClassesAndElements<Theme>(utils)
1616
}
1717

1818
export function variantPseudoClassFunctions(): VariantObject<Theme> {
19-
const utils: PseudoVariantUtilities = { getBracket, h, variantGetBracket }
19+
const utils: PseudoVariantUtilities<Theme> = { getBracket, h: h as unknown as PseudoVariantUtilities<Theme>['h'], variantGetBracket }
2020
return createPseudoClassFunctions<Theme>(utils)
2121
}
2222

2323
export function variantTaggedPseudoClasses(options: PresetWind4Options = {}): VariantObject<Theme>[] {
24-
const utils: PseudoVariantUtilities = { getBracket, h, variantGetBracket }
24+
const utils: PseudoVariantUtilities<Theme> = { getBracket, h: h as unknown as PseudoVariantUtilities<Theme>['h'], variantGetBracket }
2525
return createTaggedPseudoClasses<Theme>(options, utils)
2626
}
2727

packages-presets/rule-utils/src/handlers.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
1-
export type ValueHandlerCallback = (str: string) => string | number | undefined
1+
export type ValueHandlerCallback<T extends object> = (str: string, theme?: T) => string | number | undefined
22

3-
export type ValueHandler<K extends string> = { [S in K]: ValueHandler<K> } & {
4-
(str: string): string | undefined
3+
export type ValueHandler<K extends string, T extends object> = { [S in K]: ValueHandler<K, T> } & {
4+
(str: string, theme?: T): string | undefined
55
__options: {
66
sequence: K[]
77
}
88
}
99

10-
export function createValueHandler<K extends string>(handlers: Record<K, ValueHandlerCallback>): ValueHandler<K> {
10+
export function createValueHandler<K extends string, T extends object>(handlers: Record<K, ValueHandlerCallback<T>>): ValueHandler<K, T> {
1111
const handler = function (
12-
this: ValueHandler<K>,
12+
this: ValueHandler<K, T>,
1313
str: string,
14+
theme?: T,
1415
): string | number | undefined {
1516
const s = this.__options?.sequence || []
1617
this.__options.sequence = []
1718
for (const n of s) {
18-
const res = handlers[n](str)
19+
const res = handlers[n](str, theme)
1920
if (res != null)
2021
return res
2122
}
22-
} as unknown as ValueHandler<K>
23+
} as unknown as ValueHandler<K, T>
2324

24-
function addProcessor(that: ValueHandler<K>, name: K) {
25+
function addProcessor(that: ValueHandler<K, T>, name: K) {
2526
if (!that.__options) {
2627
that.__options = {
2728
sequence: [],

packages-presets/rule-utils/src/pseudo.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,10 @@ export interface PseudoVariantOptions {
144144
prefix?: string | string[]
145145
}
146146

147-
export interface PseudoVariantUtilities {
147+
export interface PseudoVariantUtilities<Theme extends object = object> {
148148
getBracket: typeof getBracket
149149
h: {
150-
bracket: (s: string) => string | undefined
150+
bracket: (s: string, theme?: Theme) => string | undefined
151151
}
152152
variantGetBracket: typeof variantGetBracket
153153
}

packages-presets/rule-utils/test/pseudo.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const utils: PseudoVariantUtilities = {
2222
return undefined
2323
return [str.slice(startIndex + 1, endIndex), str.slice(endIndex + 1)]
2424
},
25-
h,
25+
h: h as unknown as PseudoVariantUtilities['h'],
2626
variantGetBracket,
2727
}
2828

test/preset-wind4.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,4 +683,41 @@ describe('important', () => {
683683
}"
684684
`)
685685
})
686+
687+
it('h.bracket new syntax', async () => {
688+
const uno = await createGenerator({
689+
presets: [
690+
presetWind4({ preflights: { reset: false } }),
691+
],
692+
theme: {
693+
bar: '10px',
694+
} as any,
695+
})
696+
697+
const cases = [
698+
'm-[--spacing]',
699+
'm-[--spacing(2)]',
700+
'px-[--spacing.sm(2.5)]',
701+
'text-[--colors.blue,#000]',
702+
'text-[--colors.red.200,#fff]',
703+
'[--foo:--bar(8)]',
704+
]
705+
706+
const { css } = await uno.generate(cases)
707+
708+
expect(css).toMatchInlineSnapshot(`
709+
"/* layer: properties */
710+
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*, ::before, ::after, ::backdrop{--un-text-opacity:100%;}}
711+
@property --un-text-opacity{syntax:"<percentage>";inherits:false;initial-value:100%;}
712+
/* layer: theme */
713+
:root, :host { --spacing: 0.25rem; --spacing-sm: 0.875rem; --colors-blue-DEFAULT: oklch(70.7% 0.165 254.624); --colors-red-200: oklch(88.5% 0.062 18.334); }
714+
/* layer: default */
715+
.text-\\[--colors\\.blue\\,\\#000\\]{color:color-mix(in oklab, var(--colors-blue-DEFAULT, #000) var(--un-text-opacity), transparent);}
716+
.text-\\[--colors\\.red\\.200\\,\\#fff\\]{color:color-mix(in oklab, var(--colors-red-200, #fff) var(--un-text-opacity), transparent);}
717+
.m-\\[--spacing\\(2\\)\\]{margin:calc(var(--spacing) * 2);}
718+
.m-\\[--spacing\\]{margin:var(--spacing);}
719+
.px-\\[--spacing\\.sm\\(2\\.5\\)\\]{padding-inline:calc(var(--spacing-sm) * 2.5);}
720+
.\\[--foo\\:--bar\\(8\\)\\]{--foo:calc(var(--bar) * 8);}"
721+
`)
722+
})
686723
})

0 commit comments

Comments
 (0)