/**
* 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("; ");
}