Archived
1
0
Fork 0
This repository has been archived on 2024-07-25. You can view files and clone it, but cannot push or open issues or pull requests.
paren/ren/html_str.ts

146 lines
3.3 KiB
TypeScript
Raw Normal View History

2022-05-22 00:03:48 +03:00
import {
AnyNode,
2022-05-28 23:28:31 +03:00
AttrEntry,
2022-05-22 00:03:48 +03:00
Attrs,
2022-05-29 02:02:55 +03:00
AttrVal,
2022-05-22 00:03:48 +03:00
Elem,
Fragment,
isElem,
isFragment,
isTextNode,
TextNode,
} from "../core/node.ts";
import { concat, join } from "../core/utils.ts";
import { Renderer } from "./types.ts";
interface HtmlStrRendererOpts {
2022-05-22 00:03:48 +03:00
doctype?: string;
forceRenderDoctype?: boolean;
2022-05-22 22:47:17 +03:00
wrapNode?: (node: AnyNode) => AnyNode;
2022-05-28 23:28:31 +03:00
onVisitAttr?: (entry: AttrEntry, params: OnVisitAttrParams) => AttrEntry;
}
interface HtmlStrRendererHooks {
2022-05-28 23:28:31 +03:00
onVisitAttr: (entry: AttrEntry, params: OnVisitAttrParams) => AttrEntry;
}
export interface OnVisitAttrParams {
readonly tagName: string;
readonly attrs: Attrs;
2022-05-22 00:03:48 +03:00
}
export class HtmlStrRenderer implements Renderer<string> {
2022-05-22 22:47:17 +03:00
#doctype: string;
#forceRenderDoctype: boolean;
#wrapNode: (node: AnyNode) => AnyNode;
#hooks: HtmlStrRendererHooks;
2022-05-22 00:03:48 +03:00
constructor(opts?: HtmlStrRendererOpts) {
2022-05-22 22:47:17 +03:00
this.#doctype = opts?.doctype ?? "html";
this.#forceRenderDoctype = opts?.forceRenderDoctype ?? false;
this.#wrapNode = opts?.wrapNode ?? identity;
2022-05-28 23:28:31 +03:00
this.#hooks = {
onVisitAttr: opts?.onVisitAttr ?? identity,
};
2022-05-22 00:03:48 +03:00
}
render(node: AnyNode): string {
2022-05-22 22:47:17 +03:00
const wrappedNode = this.#wrapNode(node);
const shouldRenderDoctype = this.#forceRenderDoctype ||
(isElem(wrappedNode) && wrappedNode.tagName === "html");
2022-05-22 00:03:48 +03:00
return concat([
2022-05-22 22:47:17 +03:00
shouldRenderDoctype && encodeDoctype(this.#doctype),
2022-05-28 23:28:31 +03:00
encodeAnyNode(wrappedNode, this.#hooks),
2022-05-22 00:03:48 +03:00
]);
}
}
2022-05-22 22:47:17 +03:00
function identity<T>(val: T): T {
return val;
}
function encodeDoctype(value: string): string {
return `<!doctype ${value}>`;
2022-05-22 00:03:48 +03:00
}
function encodeAnyNode(node: AnyNode, hooks: HtmlStrRendererHooks): string {
2022-05-22 00:03:48 +03:00
return isTextNode(node)
? encodeTextNode(node)
: isFragment(node)
2022-05-28 23:28:31 +03:00
? encodeHtmlFragment(node, hooks)
: encodeHtmlElement(node, hooks);
2022-05-22 00:03:48 +03:00
}
function encodeTextNode(node: TextNode): string {
return node.innerText;
}
function encodeHtmlFragment(
node: Fragment,
hooks: HtmlStrRendererHooks,
): string {
return concatEncodedNodes(
node.children.map((ch) => encodeAnyNode(ch, hooks)),
);
2022-05-22 00:03:48 +03:00
}
function encodeHtmlElement(
{ tagName, attrs, children }: Elem,
hooks: HtmlStrRendererHooks,
2022-05-22 00:03:48 +03:00
): string {
2022-05-28 23:28:31 +03:00
const open = `<${join(" ", [tagName, encodeAttrs(tagName, attrs, hooks)])}>`;
2022-05-22 00:03:48 +03:00
if (isSelfClosedTagName(tagName)) return open;
2022-05-28 23:28:31 +03:00
const encodedChildren = children.map((ch) => encodeAnyNode(ch, hooks));
return `${open}${concatEncodedNodes(encodedChildren)}</${tagName}>`;
}
function concatEncodedNodes(encodedChildren: string[]): string {
return join(" ", encodedChildren).replace(/>\s+?</g, "><");
2022-05-22 00:03:48 +03:00
}
2022-05-28 23:28:31 +03:00
function encodeAttrs(
tagName: string,
attrs: Attrs,
hooks: HtmlStrRendererHooks,
2022-05-28 23:28:31 +03:00
): string {
2022-05-22 00:03:48 +03:00
return join(
" ",
2022-05-28 23:28:31 +03:00
Object.entries(attrs).map((entry) => {
const [key, value] = hooks.onVisitAttr(entry, { tagName, attrs });
return encodeAttr(key, value);
}),
2022-05-22 00:03:48 +03:00
);
}
2022-05-29 02:02:55 +03:00
function encodeAttr(key: string, value: AttrVal): string {
if (typeof value === "boolean") {
return value ? key : "";
}
2022-05-22 00:03:48 +03:00
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",
];