html.js/html.ts
2023-03-31 20:21:36 -03:00

88 lines
3.1 KiB
TypeScript

// 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("")}</${__element_type}>`
}`;
};