aliaset
Version:
twind monorepo
255 lines (222 loc) • 6.99 kB
text/typescript
import type {
CSSObject,
Falsey,
Context,
TwindRule,
BaseTheme,
MaybeArray,
ColorValue,
} from '../types'
import type { ParsedRule } from '../parse'
import type { ConvertedRule } from './precedence'
import { Layer, moveToLayer } from './precedence'
import { mql, hash, asArray } from '../utils'
import { atRulePrecedence, declarationPropertyPrecedence, convert } from './precedence'
import { stringify } from './stringify'
import { translateWith } from './translate'
import { parse } from '../parse'
import { compareTwindRules } from './sorted-insertion-index'
import { toColorValue } from '../colors'
export function serialize<Theme extends BaseTheme = BaseTheme>(
style: CSSObject | Falsey,
rule: Partial<ParsedRule>,
context: Context<Theme>,
precedence: number,
conditions: string[] = [],
): TwindRule[] {
return serialize$(style, convert(rule, context, precedence, conditions), context)
}
function serialize$<Theme extends BaseTheme = BaseTheme>(
style: CSSObject | Falsey,
{ n: name, p: precedence, r: conditions = [], i: important }: ConvertedRule,
context: Context<Theme>,
): TwindRule[] {
const rules: TwindRule[] = []
// The generated declaration block eg body of the css rule
let declarations = ''
// This ensures that 'border-top-width' has a higher precedence than 'border-top'
let maxPropertyPrecedence = 0
// More specific utilities have less declarations and a higher precedence
let numberOfDeclarations = 0
for (let key in style || {}) {
const value = (style as Record<string, unknown>)[key]
if (key[0] == '@') {
// at rules: https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule
if (!value) continue
// @apply ...;
if (key[1] == 'a') {
rules.push(
...translateWith(
name as string,
precedence,
parse('' + value),
context,
precedence,
conditions,
important,
true /* useOrderOfRules */,
),
)
continue
}
// @layer <layer>
if (key[1] == 'l') {
for (const css of asArray(value as MaybeArray<CSSObject>)) {
rules.push(
...serialize$(
css,
{
n: name,
p: moveToLayer(precedence, Layer[key[7] as 'b']),
r: conditions,
i: important,
},
context,
),
)
}
continue
}
// @import
if (key[1] == 'i') {
rules.push(
...asArray(value).map((value) => ({
// before all layers
p: -1,
o: 0,
r: [],
d: key + ' ' + (value as string),
})),
)
continue
}
// @keyframes
if (key[1] == 'k') {
// Use defaults layer
rules.push({
p: Layer.d,
o: 0,
r: [key],
d: serialize$(value as CSSObject, { p: Layer.d }, context)
.map(stringify)
.join(''),
})
continue
}
// @font-face
// TODO @font-feature-values
if (key[1] == 'f') {
// Use defaults layer
rules.push(
...asArray(value).map((value) => ({
p: Layer.d,
o: 0,
r: [key],
d: serialize$(value as CSSObject, { p: Layer.d }, context)
.map(stringify)
.join(''),
})),
)
continue
}
// -> All other are handled below; same as selector
}
// @media
// @supports
// selector
if (typeof value == 'object' && !Array.isArray(value)) {
// at-rule or non-global selector
if (key[0] == '@' || key.includes('&')) {
let rulePrecedence = precedence
if (key[0] == '@') {
// Handle `@media screen(sm)` and `@media (screen(sm) or ...)`
key = key.replace(/\bscreen\(([^)]+)\)/g, (_, screenKey) => {
const screen = context.theme('screens', screenKey)
if (screen) {
rulePrecedence |= 1 << 26 /* Shifts.screens */
return mql(screen, '')
}
return _
})
rulePrecedence |= atRulePrecedence(key)
}
rules.push(
...serialize$(
value as CSSObject,
{
n: name,
p: rulePrecedence,
r: [...conditions, key],
i: important,
},
context,
),
)
} else {
// global selector
rules.push(
...serialize$(value as CSSObject, { p: precedence, r: [...conditions, key] }, context),
)
}
} else if (key == 'label' && value) {
name = (value as string) + hash(JSON.stringify([precedence, important, style]))
} else if (value || value === 0) {
// property -> hyphenate
key = key.replace(/[A-Z]/g, (_) => '-' + _.toLowerCase())
// Update precedence
numberOfDeclarations += 1
maxPropertyPrecedence = Math.max(maxPropertyPrecedence, declarationPropertyPrecedence(key))
declarations +=
(declarations ? ';' : '') +
asArray(value)
.map((value) =>
context.s(
key,
// support theme(...) function in values
// calc(100vh - theme('spacing.12'))
resolveThemeFunction('' + value, context.theme) + (important ? ' !important' : ''),
),
)
.join(';')
}
}
// PERF: prevent unshift using `rules = [{}]` above and then `rules[0] = {...}`
rules.unshift({
n: name,
p: precedence,
o:
// number of declarations (descending)
Math.max(0, 15 - numberOfDeclarations) +
// greatest precedence of properties
// if there is no property precedence this is most likely a custom property only declaration
// these have the highest precedence
Math.min(maxPropertyPrecedence || 15, 15) * 1.5,
r: conditions,
// stringified declarations
d: declarations,
})
return rules.sort(compareTwindRules)
}
export function resolveThemeFunction<Theme extends BaseTheme = BaseTheme>(
value: string,
theme: Context<Theme>['theme'],
): string {
// support theme(...) function in values
// calc(100vh - theme('spacing.12'))
// theme('borderColor.DEFAULT', 'currentColor')
// PERF: check for theme before running the regexp
// if (value.includes('theme')) {
return value.replace(
/theme\((["'`])?(.+?)\1(?:\s*,\s*(["'`])?(.+?)\3)?\)/g,
(_, __, key: string, ___, defaultValue: string | undefined) => {
const value = theme(key, defaultValue)
if (typeof value == 'function' && /color|fill|stroke/i.test(key)) {
return toColorValue(value as ColorValue)
}
// TODO: warn if not a string
return '' + value
},
)
// }
// return value
}