Skip to content

Commit

Permalink
feat(preset-mini): support for defining css property with the `theme(…
Browse files Browse the repository at this point in the history
…)` function (#3204)

Co-authored-by: chris <hizyyv@gmail.com>
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
Co-authored-by: Chris <1633711653@qq.com>
  • Loading branch information
4 people committed Oct 23, 2023
1 parent c15a29d commit da5ec25
Show file tree
Hide file tree
Showing 13 changed files with 88 additions and 100 deletions.
2 changes: 1 addition & 1 deletion packages/extractor-arbitrary-variants/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function splitCodeWithArbitraryVariants(code: string): string[] {
const result: string[] = []

for (const match of code.matchAll(arbitraryPropertyRE)) {
if (!code[match.index! - 1]?.match(/^[\s'"`]/))
if (match.index !== 0 && !code[match.index! - 1]?.match(/^[\s'"`]/))
continue

result.push(match[0])
Expand Down
8 changes: 4 additions & 4 deletions packages/postcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import type { Result, Root } from 'postcss'
import postcss from 'postcss'
import { createGenerator } from '@unocss/core'
import { loadConfig } from '@unocss/config'
import { hasThemeFn } from '@unocss/rule-utils'
import { defaultFilesystemGlobs } from '../../shared-integration/src/defaults'
import { parseApply } from './apply'
import { parseTheme, themeFnRE } from './theme'
import { parseTheme } from './theme'
import { parseScreen } from './screen'
import type { UnoPostcssPluginOptions } from './types'

Expand Down Expand Up @@ -66,9 +67,8 @@ function unocss(options: UnoPostcssPluginOptions = {}) {
})

if (!isTarget) {
const themeFn = themeFnRE(directiveMap.theme)
root.walkDecls((decl) => {
if (themeFn.test(decl.value)) {
if (hasThemeFn(decl.value)) {
isTarget = true
return false
}
Expand Down Expand Up @@ -114,7 +114,7 @@ function unocss(options: UnoPostcssPluginOptions = {}) {
}) as unknown as { path: string; mtimeMs: number }[]

await parseApply(root, uno, directiveMap.apply)
await parseTheme(root, uno, directiveMap.theme)
await parseTheme(root, uno)
await parseScreen(root, uno, directiveMap.screen)

promises.push(
Expand Down
48 changes: 3 additions & 45 deletions packages/postcss/src/theme.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,9 @@
import type { UnoGenerator } from '@unocss/core'
import { colorToString, parseCssColor } from '@unocss/rule-utils'
import { transformThemeFn } from '@unocss/rule-utils'
import type { Root } from 'postcss'
import MagicString from 'magic-string'

export function themeFnRE(directiveName: string) {
return new RegExp(`${directiveName}\\((.*?)\\)`, 'g')
}
export async function parseTheme(root: Root, uno: UnoGenerator, directiveName: string) {
export async function parseTheme(root: Root, uno: UnoGenerator) {
root.walkDecls((decl) => {
const matches = Array.from(decl.value.matchAll(themeFnRE(directiveName)))

if (!matches.length)
return

for (const match of matches) {
const rawArg = match[1].trim()
if (!rawArg)
throw new Error(`${directiveName}() expect exact one argument, but got 0`)

const [rawKey, alpha] = rawArg.slice(1, -1).split('/') as [string, string?]
let value: any = uno.config.theme
const keys = rawKey.trim().split('.')

keys.every((key) => {
if (value[key] != null)
value = value[key]
else if (value[+key] != null)
value = value[+key]
else
return false
return true
})

if (typeof value === 'string') {
if (alpha) {
const color = parseCssColor(value)
if (color)
value = colorToString(color, alpha)
}
const code = new MagicString(decl.value)
code.overwrite(
match.index!,
match.index! + match[0].length,
value,
)
decl.value = code.toString()
}
}
decl.value = transformThemeFn(decl.value, uno.config.theme)
})
}
14 changes: 11 additions & 3 deletions packages/preset-mini/src/_rules/variables.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Rule } from '@unocss/core'
import { h } from '../utils'
import { h, hasThemeFn, transformThemeFn } from '../utils'

const variablesAbbrMap: Record<string, string> = {
backface: 'backface-visibility',
Expand All @@ -26,14 +26,22 @@ export const cssVariables: Rule[] = [
]

export const cssProperty: Rule[] = [
[/^\[(.*)\]$/, ([_, body]) => {
[/^\[(.*)\]$/, ([_, body], { theme }) => {
if (!body.includes(':'))
return

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

if (!isURI(body) && prop.match(/^[a-z-]+$/) && isValidCSSBody(value)) {
const parsed = h.bracket(`[${value}]`)
let parsed

if (hasThemeFn(value))
parsed = transformThemeFn(value, theme)

if (!parsed || parsed === value)
parsed = h.bracket(`[${value}]`)

if (parsed)
return { [prop]: parsed }
}
Expand Down
1 change: 0 additions & 1 deletion packages/preset-mini/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ export const presetMini = definePreset((options: PresetMiniOptions = {}) => {
options.attributifyPseudo = options.attributifyPseudo ?? false
options.preflight = options.preflight ?? true
options.variablePrefix = options.variablePrefix ?? 'un-'

return {
name: '@unocss/preset-mini',
theme,
Expand Down
3 changes: 2 additions & 1 deletion packages/rule-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"stub": "unbuild --stub"
},
"dependencies": {
"@unocss/core": "workspace:^"
"@unocss/core": "workspace:^",
"magic-string": "^0.30.3"
}
}
1 change: 1 addition & 0 deletions packages/rule-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './colors'
export * from './utilities'
export * from './handlers'
export * from './variants'
export * from './themeFn'
46 changes: 46 additions & 0 deletions packages/rule-utils/src/themeFn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import MagicString from 'magic-string'
import { colorToString, parseCssColor } from './colors'

export const themeFnRE = /theme\(\s*['"]?(.*?)['"]?\s*\)/g

export function hasThemeFn(str: string) {
return str.includes('theme(') && str.includes(')')
}

export function transformThemeFn(code: string, theme: Record<string, any>, throwOnMissing = true) {
const matches = Array.from(code.toString().matchAll(themeFnRE))

if (!matches.length)
return code

const s = new MagicString(code)

for (const match of matches) {
const rawArg = match[1]
if (!rawArg)
throw new Error('theme() expect exact one argument, but got 0')

const [rawKey, alpha] = rawArg.split('/') as [string, string?]
const keys = rawKey.trim().split('.')
let value = keys.reduce((t, k) => t?.[k], theme) as unknown as string | undefined

if (typeof value === 'string') {
if (alpha) {
const color = parseCssColor(value)
if (color)
value = colorToString(color, alpha)
}

s.overwrite(
match.index!,
match.index! + match[0].length,
value,
)
}
else if (throwOnMissing) {
throw new Error(`theme of "${rawArg}" did not found`)
}
}

return s.toString()
}
5 changes: 3 additions & 2 deletions packages/transformer-directives/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import type { SourceCodeTransformer, UnoGenerator } from '@unocss/core'
import type { CssNode, List, ListItem } from 'css-tree'
import { parse, walk } from 'css-tree'
import type MagicString from 'magic-string'
import { handleThemeFn, themeFnRE } from './theme'
import { hasThemeFn as hasThemeFunction } from '@unocss/rule-utils'
import { handleThemeFn } from './theme'
import { handleScreen } from './screen'
import { handleApply } from './apply'

Expand Down Expand Up @@ -76,7 +77,7 @@ export async function transformDirectives(

const hasApply = code.original.includes('@apply') || applyVariable.some(s => code.original.includes(s))
const hasScreen = code.original.includes('@screen')
const hasThemeFn = code.original.match(themeFnRE)
const hasThemeFn = hasThemeFunction(code.original)

if (!hasApply && !hasThemeFn && !hasScreen)
return
Expand Down
45 changes: 2 additions & 43 deletions packages/transformer-directives/src/theme.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,12 @@
import type { Declaration } from 'css-tree'
import { colorToString, parseCssColor } from '@unocss/rule-utils'
import { transformThemeFn } from '@unocss/rule-utils'
import type { TransformerDirectivesContext } from '.'

export const themeFnRE = /theme\((.*?)\)/g

export function handleThemeFn({ code, uno, options }: TransformerDirectivesContext, node: Declaration) {
const { throwOnMissing = true } = options

const offset = node.value.loc!.start.offset
const str = code.original.slice(offset, node.value.loc!.end.offset)
const matches = Array.from(str.matchAll(themeFnRE))

if (!matches.length)
return

for (const match of matches) {
const rawArg = match[1].trim()
if (!rawArg)
throw new Error('theme() expect exact one argument, but got 0')

const [rawKey, alpha] = rawArg.slice(1, -1).split('/') as [string, string?]
let value: any = uno.config.theme
const keys = rawKey.trim().split('.')

keys.every((key) => {
if (value[key] != null)
value = value[key]
else if (value[+key] != null)
value = value[+key]
else
return false
return true
})

if (typeof value === 'string') {
if (alpha) {
const color = parseCssColor(value)
if (color)
value = colorToString(color, alpha)
}

code.overwrite(
offset + match.index!,
offset + match.index! + match[0].length,
value,
)
}
else if (throwOnMissing) {
throw new Error(`theme of "${rawArg.slice(1, -1)}" did not found`)
}
}
code.overwrite(offset, node.value.loc!.end.offset, transformThemeFn(str, uno.config.theme, throwOnMissing))
}
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions test/assets/output/preset-mini-targets.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@
.fw-\$variable{font-weight:var(--variable);}
.items-\$size{align-items:var(--size);}
.ws-\$variable{white-space:var(--variable);}
.\[--css-spacing\:theme\(spacing\.sm\)\]{--css-spacing:0.875rem;}
.\[--css-variable-color\:theme\(colors\.green\.500\)\,theme\(colors\.blue\.500\)\]{--css-variable-color:#22c55e,#3b82f6;}
.\[--css-variable-color\:theme\(colors\.red\.500\)\]{--css-variable-color:#ef4444;}
.\[--css-variable-color\:theme\(colors\.red\.500\/50\%\)\]{--css-variable-color:rgba(239,68,68,50%);}
.\[--css-variable\:\"wght\"_400\,_\"opsz\"_14\]{--css-variable:"wght" 400, "opsz" 14;}
.\[a\:b\]{a:b;}
.\[background-image\:url\(star_transparent\.gif\)\,_url\(cat_front\.png\)\]{background-image:url(star_transparent.gif), url(cat_front.png);}
.\[color\:theme\(colors\.blue\.300\/40\%\)\]{color:rgba(147,197,253,40%);}
.\[content\:attr\(attr_content\)\]{content:attr(attr content);}
.\[content\:attr\(attr\\_content\)\]{content:attr(attr_content);}
.\[font-family\:\'Inter\'\,_sans-serif\]{font-family:'Inter', sans-serif;}
.\[font-family\:var\(--font-family\)\]{font-family:var(--font-family);}
.\[font-feature-settings\:\'cv02\'\,\'cv03\'\,\'cv04\'\,\'cv11\'\]{font-feature-settings:'cv02','cv03','cv04','cv11';}
.\[font-variation-settings\:\"wght\"_400\,_\"opsz\"_14\]{font-variation-settings:"wght" 400, "opsz" 14;}
.\[margin\:logical_1rem_2rem_3rem\]{margin:logical 1rem 2rem 3rem;}
.\[padding\:theme\(spacing\.xl\)\]{padding:1.25rem;}
.all-\[\.target\]-\[combinator\:test-2\] .target,
.children-\[\.target\]-\[combinator\:test-2\]>.target,
.next-\[\.target\]-\[combinator\:test-2\]+.target{combinator:test-2;}
Expand Down
6 changes: 6 additions & 0 deletions test/assets/preset-mini-targets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -926,7 +926,13 @@ export const presetMiniTargets: string[] = [
'[font-family:\'Inter\',_sans-serif]',
'[font-feature-settings:\'cv02\',\'cv03\',\'cv04\',\'cv11\']',
'[font-variation-settings:"wght"_400,_"opsz"_14]',
'[padding:theme(spacing.xl)]',
'[color:theme(colors.blue.300/40%)]',
'[--css-variable:"wght"_400,_"opsz"_14]',
'[--css-variable-color:theme(colors.red.500)]',
'[--css-variable-color:theme(colors.red.500/50%)]',
'[--css-variable-color:theme(colors.green.500),theme(colors.blue.500)]',
'[--css-spacing:theme(spacing.sm)]',

// variants
'active:scale-4',
Expand Down

0 comments on commit da5ec25

Please sign in to comment.