// Inspirado en https://gitea.nulo.in/Nulo/html.lua/src/commit/cb7e35dcca0e45b397baf628f5e1a162a2269638/html.lua import { escape } from "html-escaper"; export interface VirtualElement { __element_type: string; things: Thing[]; } export interface RawHtml { __raw: string; } interface Attributes { [key: string]: string | number; } type Thing = VirtualElement | RawHtml | Attributes | string; export type Renderable = VirtualElement | RawHtml | string; export const basicElement = (__element_type: string) => (...things: Thing[]): VirtualElement => ({ __element_type, things }); export const raw = (html: string): RawHtml => ({ __raw: html }); export const doctype = basicElement("!doctype html"); export const head = basicElement("head"); export const body = basicElement("body"); export const meta = basicElement("meta"); export const title = basicElement("title"); export const link = basicElement("link"); export const script = basicElement("script"); export const metaUtf8 = meta({ charset: "utf-8" }); export const h1 = basicElement("h1"); export const h2 = basicElement("h2"); export const h3 = basicElement("h3"); export const h4 = basicElement("h4"); export const h5 = basicElement("h5"); export const h6 = basicElement("h6"); export const p = basicElement("p"); export const ul = basicElement("ul"); export const ol = basicElement("ol"); export const li = basicElement("li"); export const a = basicElement("a"); export const img = basicElement("img"); export const span = basicElement("span"); export const picture = basicElement("picture"); export const source = basicElement("source"); export const figure = basicElement("figure"); export const figcaption = basicElement("figcaption"); export const article = basicElement("article"); export const nav = basicElement("nav"); export const header = basicElement("header"); export const main = basicElement("main"); export const section = basicElement("section"); export const time = basicElement("time"); // TODO: actually escape const escapeHTML = (string: string) => escape(string); export const render = (...elements: Renderable[]): string => { if (elements.length > 1) return elements.map((e) => render(e)).join(""); if (typeof elements[0] == "string") return escapeHTML(elements[0]); if ("__raw" in elements[0]) return elements[0].__raw; const { __element_type, things } = elements[0]; const attributes = things.filter( (t) => !(typeof t == "string") && !("__element_type" in t) && !("__raw" in t) ); const children = things.filter( (t) => typeof t == "string" || ("__element_type" in t && "things" in t) || "__raw" in t ) as Renderable[]; const selfClosing = ["meta", "img", "source", "link", "br"].includes(__element_type) || __element_type.startsWith("!"); // doctype // TODO: escape attribute values return `<${__element_type}${attributes.length ? " " : ""}${attributes .flatMap((t) => Object.entries(t).map(([name, value]) => `${name}="${value}"`) ) .join("")}${ selfClosing ? ">" : `>${children.map((c) => render(c)).join("")}` }`; };