aliaset
Version:
twind monorepo
244 lines (201 loc) • 6.82 kB
text/typescript
import type {
BaseTheme,
Context,
RuleResult,
TwindConfig,
CSSProperties,
MatchResult,
MaybeArray,
RuleResolver,
MatchConverter,
Rule,
CSSObject,
Variant,
VariantResult,
VariantResolver,
Falsey,
} from '../types'
import { DEV } from 'distilt/env'
import { makeThemeFunction } from './theme'
import { asArray, escape, hash as defaultHash, identity } from '../utils'
import { fromMatch } from '../rules'
import { warn } from './warn'
type ResolveFunction<Theme extends BaseTheme = BaseTheme> = (
className: string,
context: Context<Theme>,
isDark?: boolean,
) => RuleResult
type VariantFunction<Theme extends BaseTheme = BaseTheme> = (
variant: string,
context: Context<Theme>,
) => VariantResult
export function createContext<Theme extends BaseTheme = BaseTheme>({
theme,
darkMode,
darkColor,
variants,
rules,
hash,
stringify,
ignorelist,
}: TwindConfig<Theme>): Context<Theme> {
// Used to cache resolved rule values
const variantCache = new Map<string, MaybeArray<string>>()
// lazy created resolve functions
const variantResolvers = new Map<Variant<Theme>, VariantFunction<Theme>>()
// Used to cache resolved rule values
const ruleCache = new Map<string, RuleResult>()
// lazy created resolve functions
const ruleResolvers = new Map<Rule<Theme>, ResolveFunction<Theme>>()
const ignored = createRegExpExecutor(ignorelist, (value, condition) => condition.test(value))
const reportedUnknownClasses = new Set<string>()
// add dark as last variant to allow user to override it
// we can modify variants as it has been passed through defineConfig which already made a copy
variants.push([
'dark',
Array.isArray(darkMode) || darkMode == 'class'
? `${asArray(darkMode)[1] || '.dark'} &`
: typeof darkMode == 'string' && darkMode != 'media'
? darkMode // a custom selector
: '@media (prefers-color-scheme:dark)',
])
const h =
typeof hash == 'function'
? (value: string) => hash(value, defaultHash)
: hash
? defaultHash
: identity
return {
theme: makeThemeFunction(theme),
e: escape,
h,
s(property, value) {
// Hash/Tag tailwind custom properties during serialization
return stringify(hashVars(property, h), hashVars(value, h), this)
},
d(section, key, color) {
return darkColor?.(section, key, this, color)
},
v(value) {
if (!variantCache.has(value)) {
variantCache.set(
value,
find(value, variants, variantResolvers, getVariantResolver, this) || '&:' + value,
)
}
return variantCache.get(value) as string
},
r(className, isDark) {
const key = JSON.stringify([className, isDark])
if (!ruleCache.has(key)) {
ruleCache.set(
key,
!ignored(className, this) &&
find(className, rules, ruleResolvers, getRuleResolver, this, isDark),
)
if (DEV) {
const rule = ruleCache.get(key)
if (rule == null && !reportedUnknownClasses.has(className)) {
reportedUnknownClasses.add(className)
warn(
`Unknown class ${JSON.stringify(className)} found.`,
'TWIND_INVALID_CLASS',
className,
)
}
}
}
return ruleCache.get(key)
},
}
}
function find<Value, Config, Result, Theme extends BaseTheme = BaseTheme>(
value: Value,
list: Config[],
cache: Map<Config, (value: Value, context: Context<Theme>, isDark?: boolean) => Result>,
getResolver: (
item: Config,
) => (value: Value, context: Context<Theme>, isDark?: boolean) => Result,
context: Context<Theme>,
isDark?: boolean,
) {
for (const item of list) {
let resolver = cache.get(item)
if (!resolver) {
cache.set(item, (resolver = getResolver(item)))
}
const resolved = resolver(value, context, isDark)
if (resolved) return resolved
}
}
function getVariantResolver<Theme extends BaseTheme = BaseTheme>(
variant: Variant<Theme>,
): VariantFunction<Theme> {
return createVariantFunction(variant[0], variant[1])
}
function getRuleResolver<Theme extends BaseTheme = BaseTheme>(
rule: Rule<Theme>,
): ResolveFunction<Theme> {
if (Array.isArray(rule)) {
return createResolveFunction(rule[0], rule[1], rule[2])
}
return createResolveFunction(rule)
}
function createVariantFunction<Theme extends BaseTheme = BaseTheme>(
patterns: MaybeArray<string | RegExp>,
resolve: string | VariantResolver<Theme>,
): VariantFunction<Theme> {
return createResolve(patterns, typeof resolve == 'function' ? resolve : () => resolve)
}
function createResolveFunction<Theme extends BaseTheme = BaseTheme>(
patterns: MaybeArray<string | RegExp>,
resolve?: keyof CSSProperties | string | CSSObject | RuleResolver<Theme>,
convert?: MatchConverter<Theme>,
): ResolveFunction<Theme> {
return createResolve(patterns, fromMatch(resolve as keyof CSSProperties, convert))
}
function createResolve<Result, Theme extends BaseTheme = BaseTheme>(
patterns: MaybeArray<string | RegExp>,
resolve: (match: MatchResult, context: Context<Theme>) => Result,
): (value: string, context: Context<Theme>, isDark?: boolean) => Result | undefined {
return createRegExpExecutor(patterns, (value, condition, context, isDark?: boolean) => {
const match = condition.exec(value) as MatchResult | Falsey
if (match) {
// MATCH.$_ = value
match.$$ = value.slice(match[0].length)
match.dark = isDark
return resolve(match, context)
}
})
}
function createRegExpExecutor<Result, Theme extends BaseTheme = any>(
patterns: MaybeArray<string | RegExp>,
run: (value: string, condition: RegExp, context: Context<Theme>, isDark?: boolean) => Result,
): (value: string, context: Context<Theme>, isDark?: boolean) => Result | undefined {
const conditions = asArray(patterns).map(toCondition)
return (value, context, isDark) => {
for (const condition of conditions) {
const result = run(value, condition, context, isDark)
if (result) return result
}
}
}
export function toCondition(value: string | RegExp): RegExp {
// "visible" -> /^visible$/
// "(float)-(left|right|none)" -> /^(float)-(left|right|none)$/
// "auto-rows-" -> /^auto-rows-/
// "gap(-|$)" -> /^gap(-|$)/
return typeof value == 'string'
? new RegExp('^' + value + (value.includes('$') || value.slice(-1) == '-' ? '' : '$'))
: value
}
function hashVars(value: string, h: Context['h']): string {
// PERF: check for --tw before running the regexp
// if (value.includes('--tw')) {
return value.replace(
/--(tw(?:-[\w-]+)?)\b/g,
(_: string, property: string) => '--' + h(property).replace('#', ''),
)
// }
// return value
}