160 lines
4 KiB
TypeScript
160 lines
4 KiB
TypeScript
export type Prop = string | number | boolean;
|
|
export type Attributes = Record<string, Prop>;
|
|
export type Html = string | NonText;
|
|
export type NonText = {
|
|
tag: string;
|
|
attributes: Attributes;
|
|
children: Html[];
|
|
};
|
|
|
|
export const Fragment = "Fragment";
|
|
|
|
function is_string(elem: Html): elem is string {
|
|
return typeof elem == "string";
|
|
}
|
|
|
|
export function fr(...children: Html[]): Html {
|
|
return { tag: Fragment, attributes: {}, children };
|
|
}
|
|
|
|
function div(...children: Html[]): Html {
|
|
return { tag: "div", attributes: {}, children };
|
|
}
|
|
|
|
function expand_fragments_in_list(children: Html[]): Html[] {
|
|
let out = [];
|
|
|
|
for (const child of children) {
|
|
if (is_string(child)) {
|
|
out.push(child);
|
|
} else if (child.tag == Fragment) {
|
|
out.push(...child.children.map(expand_fragments));
|
|
} else {
|
|
out.push(child);
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
// NOTE: if you pass a fragment to this, it won't be expanded (which makes sense if you think about it)
|
|
function expand_fragments(elem: Html): Html {
|
|
if (is_string(elem)) {
|
|
return elem;
|
|
} else {
|
|
const children = expand_fragments_in_list(elem.children);
|
|
return { ...elem, children };
|
|
}
|
|
}
|
|
|
|
const attr = (attrs: Attributes) => (elem: Html): Html => {
|
|
if (is_string(elem)) {
|
|
return elem;
|
|
} else {
|
|
const { tag, attributes, children } = elem;
|
|
return { tag, attributes: { ...attributes, ...attrs }, children };
|
|
}
|
|
};
|
|
|
|
const escape = (unsafe: string): string => {
|
|
return unsafe
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
};
|
|
|
|
function render_attributes(attrs: Attributes): string {
|
|
return (
|
|
(Object.keys(attrs).length > 0 ? " " : "") +
|
|
Object.entries(attrs)
|
|
.filter(([_attr, val]) => typeof val != "undefined")
|
|
.map(([attr, val]) => `${attr}="${escape((val ?? "").toString())}"`)
|
|
.join(" ")
|
|
);
|
|
}
|
|
|
|
function indent(str: string, amount = 4, char = " "): string {
|
|
const ind = char.repeat(4);
|
|
return str
|
|
.split("\n")
|
|
.map((l) => ind + l)
|
|
.join("\n");
|
|
}
|
|
|
|
// incomplete, obviously.
|
|
const NON_SELF_CLOSING = ["div", "p", "a"];
|
|
|
|
function render_elem(
|
|
{ tag, attributes, children }: NonText,
|
|
mini = false,
|
|
): string {
|
|
if (children.length == 0) {
|
|
if (!NON_SELF_CLOSING.includes(tag)) {
|
|
return (
|
|
`<${tag}${render_attributes(attributes)}` + (mini ? "/>" : " />")
|
|
);
|
|
} else {
|
|
return `<${tag}${render_attributes(attributes)}></${tag}>`;
|
|
}
|
|
} else {
|
|
let inner = "";
|
|
let string_last = false;
|
|
for (let i = 0; i < children.length; i++) {
|
|
let child = children[i];
|
|
const rendered = render(child, mini ? "mini" : undefined);
|
|
|
|
if (is_string(child)) {
|
|
mini && string_last && (inner += " ");
|
|
inner += rendered;
|
|
|
|
string_last = true;
|
|
} else {
|
|
inner += rendered;
|
|
string_last = false;
|
|
}
|
|
|
|
if (!mini && i + 1 < children.length) inner += "\n";
|
|
}
|
|
|
|
return [
|
|
`<${tag}${render_attributes(attributes)}>`,
|
|
mini ? inner : indent(inner),
|
|
`</${tag}>`,
|
|
].join(mini ? "" : "\n");
|
|
}
|
|
}
|
|
// old:
|
|
// joining with " " is inefficient, but necessary for correct string behavior
|
|
// i've decided that joining with "" is worth the size savings
|
|
// THIS MEANS MINIFICATION IS SEMANTICALLY DIFFERENT
|
|
// JK I DID IT THE HARD WAY :]
|
|
|
|
export function render(elem: Html, mini?: "mini"): string {
|
|
if (is_string(elem)) {
|
|
return escape(elem);
|
|
} else if (elem.tag == Fragment) {
|
|
// mimics react's behavior with fragments, ehhh nvm different aims
|
|
return elem.children.map((elem) => render(elem, mini)).join("\n");
|
|
} else {
|
|
const { tag, attributes } = elem;
|
|
const expanded = expand_fragments(elem);
|
|
|
|
if (is_string(expanded)) {
|
|
throw "impossible";
|
|
}
|
|
|
|
return render_elem(
|
|
{ tag, attributes, children: expanded.children },
|
|
mini == "mini",
|
|
);
|
|
}
|
|
}
|
|
|
|
export function debug_render(elem: Html) {
|
|
console.log(elem);
|
|
console.log(render(elem));
|
|
console.log(render(elem, "mini"));
|
|
}
|