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 = ( className: string, context: Context, isDark?: boolean, ) => RuleResult type VariantFunction = ( variant: string, context: Context, ) => VariantResult export function createContext({ theme, darkMode, darkColor, variants, rules, hash, stringify, ignorelist, }: TwindConfig): Context { // Used to cache resolved rule values const variantCache = new Map>() // lazy created resolve functions const variantResolvers = new Map, VariantFunction>() // Used to cache resolved rule values const ruleCache = new Map() // lazy created resolve functions const ruleResolvers = new Map, ResolveFunction>() const ignored = createRegExpExecutor(ignorelist, (value, condition) => condition.test(value)) const reportedUnknownClasses = new Set() // 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: Value, list: Config[], cache: Map, isDark?: boolean) => Result>, getResolver: ( item: Config, ) => (value: Value, context: Context, isDark?: boolean) => Result, context: Context, 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( variant: Variant, ): VariantFunction { return createVariantFunction(variant[0], variant[1]) } function getRuleResolver( rule: Rule, ): ResolveFunction { if (Array.isArray(rule)) { return createResolveFunction(rule[0], rule[1], rule[2]) } return createResolveFunction(rule) } function createVariantFunction( patterns: MaybeArray, resolve: string | VariantResolver, ): VariantFunction { return createResolve(patterns, typeof resolve == 'function' ? resolve : () => resolve) } function createResolveFunction( patterns: MaybeArray, resolve?: keyof CSSProperties | string | CSSObject | RuleResolver, convert?: MatchConverter, ): ResolveFunction { return createResolve(patterns, fromMatch(resolve as keyof CSSProperties, convert)) } function createResolve( patterns: MaybeArray, resolve: (match: MatchResult, context: Context) => Result, ): (value: string, context: Context, 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( patterns: MaybeArray, run: (value: string, condition: RegExp, context: Context, isDark?: boolean) => Result, ): (value: string, context: Context, 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 }