/** * Raw values. * * A raw value is html that is already rendered, it is never escaped. The * wrapper marks the values that come from a trusted source, e.g. the html of * a markdown collection. */ export class Raw { constructor(readonly html: string) {} toString(): string { return this.html; } } /** Marks html as pre-rendered, it is never escaped. */ export function raw(html: string): Raw { return new Raw(html); } /** `true` when the value is a raw html value. */ export function isRaw(value: unknown): value is Raw { return value instanceof Raw; } /** Properties that do not take a unit. */ const UNITLESS = new Set([ "animation-iteration-count", "border-image-outset", "border-image-slice", "border-image-width", "box-flex", "box-flex-group", "box-ordinal-group", "column-count", "columns", "flex", "flex-grow", "flex-positive", "flex-shrink", "flex-negative", "flex-order", "font-weight", "grid-area", "grid-column", "grid-column-end", "grid-column-start", "grid-row", "grid-row-end", "grid-row-start", "line-clamp", "line-height", "opacity", "order", "orphans", "scale", "tab-size", "widows", "z-index", "zoom", ]); /** Converts a camel case property into its css name. */ export function cssPropertyName(property: string): string { if (property.startsWith("--")) return property; return property.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`); } /** * Transforms an object into the value of a style attribute. * * The keys become css properties, the numbers become pixels unless the * property does not take a unit. */ export function styleValue(value: unknown): string { if (value === null || value === undefined || value === false) return ""; if (typeof value === "string" || typeof value === "number") return String(value); if (isRaw(value)) return value.html; if (typeof value !== "object") return ""; const parts: string[] = []; for (const [property, entry] of Object.entries( value as Record, )) { if (entry === null || entry === undefined || entry === false) continue; const name = cssPropertyName(property); if (typeof entry === "number") { parts.push(`${name}: ${UNITLESS.has(name) ? entry : `${entry}px`}`); continue; } if (typeof entry === "object") { parts.push(`${name}: ${styleValue(entry)}`); continue; } parts.push(`${name}: ${String(entry)}`); } return parts.join("; "); }