96 lines
2.4 KiB
TypeScript
96 lines
2.4 KiB
TypeScript
/**
|
|
* 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<string, unknown>)) {
|
|
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("; ");
|
|
}
|