paren/ren/str.ts

100 lines
2.0 KiB
TypeScript

import {
AnyNode,
Attrs,
Elem,
Fragment,
isElem,
isFragment,
isTextNode,
TextNode,
} from "../core/node.ts";
import { concat, join } from "../core/utils.ts";
import { Renderer } from "./types.ts";
interface StrRendererOpts {
doctype?: string;
forceRenderDoctype?: boolean;
}
export class StrRenderer implements Renderer<string> {
#opts: StrRendererOpts;
constructor(opts?: StrRendererOpts) {
this.#opts = opts ?? {};
}
render(node: AnyNode): string {
const shouldRenderDoctype = this.#opts.forceRenderDoctype ||
(isElem(node) && node.tagName === "html");
return concat([
shouldRenderDoctype && encodeDoctype(this.#opts.doctype),
encodeAnyNode(node),
]);
}
}
function encodeDoctype(value?: string): string {
return `<!doctype ${value ?? "html"}>`;
}
function encodeAnyNode(node: AnyNode): string {
return isTextNode(node)
? encodeTextNode(node)
: isFragment(node)
? encodeHtmlFragment(node)
: encodeHtmlElement(node);
}
function encodeTextNode(node: TextNode): string {
return node.innerText;
}
function encodeHtmlFragment(node: Fragment): string {
return concat(node.children.map(encodeAnyNode));
}
function encodeHtmlElement(
{ tagName, attrs, children }: Elem,
): string {
const open = `<${join(" ", [tagName, encodeAttrs(attrs)])}>`;
if (isSelfClosedTagName(tagName)) return open;
const encodedChildren = children.map(encodeAnyNode);
return `${open}${concat(encodedChildren)}</${tagName}>`;
}
function encodeAttrs(attrs: Attrs): string {
return join(
" ",
Object.entries(attrs).map(([key, value]) => encodeAttr(key, value)),
);
}
function encodeAttr(key: string, value: string): string {
return `${key}="${value}"`;
}
function isSelfClosedTagName(tagName: string): boolean {
return SELF_CLOSED_HTML_TAG_NAMES.includes(tagName);
}
const SELF_CLOSED_HTML_TAG_NAMES = [
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"param",
"source",
"track",
"wbr",
"command",
"keygen",
"menuitem",
];