import type { DesignSystem } from '../design-system'
import { ThemeOptions, type Theme, type ThemeKey } from '../theme'
import { withAlpha } from '../utilities'
import { DefaultMap } from '../utils/default-map'
import { unescape } from '../utils/escape'
import { toKeyPath } from '../utils/to-key-path'
import { keyPathToCssProperty } from './apply-config-to-theme'
import { deepMerge } from './config/deep-merge'
import type { UserConfig } from './config/types'
export function createThemeFn(
designSystem: DesignSystem,
configTheme: () => UserConfig['theme'],
resolveValue: (value: any) => any,
) {
return function theme(path: string, defaultValue?: unknown) {
let lastSlash = path.lastIndexOf('/')
let modifier: string | null = null
if (lastSlash !== -1) {
modifier = path.slice(lastSlash + 1).trim()
path = path.slice(0, lastSlash).trim()
}
let resolvedValue = (() => {
let keypath = toKeyPath(path)
let [cssValue, options] = readFromCss(designSystem.theme, keypath)
let configValue = resolveValue(get(configTheme() ?? {}, keypath) ?? null)
if (typeof configValue === 'string') {
configValue = configValue.replace('<alpha-value>', '1')
}
if (typeof cssValue !== 'object') {
if (typeof options !== 'object' && options & ThemeOptions.DEFAULT) {
return configValue ?? cssValue
}
return cssValue
}
if (configValue !== null && typeof configValue === 'object' && !Array.isArray(configValue)) {
let configValueCopy: Record<string, unknown> & { __CSS_VALUES__?: Record<string, number> } =
deepMerge({}, [configValue], (_, b) => b)
if (cssValue === null && Object.hasOwn(configValue, '__CSS_VALUES__')) {
let localCssValue: Record<string, unknown> = {}
for (let key in configValue.__CSS_VALUES__) {
localCssValue[key] = configValue[key]
delete configValueCopy[key]
}
cssValue = localCssValue
}
for (let key in cssValue) {
if (key === '__CSS_VALUES__') continue
if (
configValue?.__CSS_VALUES__?.[key] & ThemeOptions.DEFAULT &&
get(configValueCopy, key.split('-')) !== undefined
) {
continue
}
configValueCopy[unescape(key)] = cssValue[key]
}
return configValueCopy
}
if (Array.isArray(cssValue) && Array.isArray(options) && Array.isArray(configValue)) {
let base = cssValue[0]
let extra = cssValue[1]
if (options[0] & ThemeOptions.DEFAULT) {
base = configValue[0] ?? base
}
for (let key of Object.keys(extra)) {
if (options[1][key] & ThemeOptions.DEFAULT) {
extra[key] = configValue[1][key] ?? extra[key]
}
}
return [base, extra]
}
return cssValue ?? configValue
})()
if (modifier && typeof resolvedValue === 'string') {
resolvedValue = withAlpha(resolvedValue, modifier)
}
return resolvedValue ?? defaultValue
}
}
function readFromCss(
theme: Theme,
path: string[],
):
| [value: string | null | Record<string, unknown>, options: number]
| [value: Record<string, unknown>, options: Record<string, number>] {
if (path.length === 1 && path[0].startsWith('--')) {
return [theme.get([path[0] as ThemeKey]), theme.getOptions(path[0])] as const
}
type ThemeValue =
| string
| [main: string, extra: Record<string, string>]
let themeKey = keyPathToCssProperty(path)
let map = new Map<string | null, ThemeValue>()
let nested = new DefaultMap<string | null, Map<string, [value: string, options: number]>>(
() => new Map(),
)
let ns = theme.namespace(`--${themeKey}`)
if (ns.size === 0) {
return [null, ThemeOptions.NONE]
}
let options = new Map()
for (let [key, value] of ns) {
if (!key || !key.includes('--')) {
map.set(key, value)
options.set(key, theme.getOptions(!key ? `--${themeKey}` : `--${themeKey}-${key}`))
continue
}
let nestedIndex = key.indexOf('--')
let mainKey = key.slice(0, nestedIndex)
let nestedKey = key.slice(nestedIndex + 2)
nestedKey = nestedKey.replace(/-([a-z])/g, (_, a) => a.toUpperCase())
nested
.get(mainKey === '' ? null : mainKey)
.set(nestedKey, [value, theme.getOptions(`--${themeKey}${key}`)])
}
let baseOptions = theme.getOptions(`--${themeKey}`)
for (let [key, extra] of nested) {
let value = map.get(key)
if (typeof value !== 'string') continue
let extraObj: Record<string, string> = {}
let extraOptionsObj: Record<string, number> = {}
for (let [nestedKey, [nestedValue, nestedOptions]] of extra) {
extraObj[nestedKey] = nestedValue
extraOptionsObj[nestedKey] = nestedOptions
}
map.set(key, [value, extraObj])
options.set(key, [baseOptions, extraOptionsObj])
}
let obj: Record<string, unknown> = {}
let optionsObj: Record<string, number> = {}
for (let [key, value] of map) {
set(obj, [key ?? 'DEFAULT'], value)
}
for (let [key, value] of options) {
set(optionsObj, [key ?? 'DEFAULT'], value)
}
if (path[path.length - 1] === 'DEFAULT') {
return [(obj?.DEFAULT ?? null) as any, optionsObj.DEFAULT ?? ThemeOptions.NONE] as const
}
if ('DEFAULT' in obj && Object.keys(obj).length === 1) {
return [obj.DEFAULT as any, optionsObj.DEFAULT ?? ThemeOptions.NONE] as const
}
obj.__CSS_VALUES__ = optionsObj
return [obj, optionsObj] as const
}
function get(obj: any, path: string[]) {
for (let i = 0; i < path.length; ++i) {
let key = path[i]
if (obj?.[key] === undefined) {
if (path[i + 1] === undefined) {
return undefined
}
path[i + 1] = `${key}-${path[i + 1]}`
continue
}
obj = obj[key]
}
return obj
}
function set(obj: any, path: string[], value: any) {
for (let key of path.slice(0, -1)) {
if (obj[key] === undefined) {
obj[key] = {}
}
obj = obj[key]
}
obj[path[path.length - 1]] = value
}