diff --git a/core/node.ts b/core/node.ts index d3f1933..9ceb2f5 100644 --- a/core/node.ts +++ b/core/node.ts @@ -1,6 +1,7 @@ import { Nilable } from "./utils.ts"; export type Attrs = Record; +export type AttrEntry = [key: string, value: string]; export type AnyNode = Fragment | FragmentNode; export function isTextNode(val: unknown): val is TextNode { diff --git a/ren/str.ts b/ren/str.ts index 7a56b10..2322c53 100644 --- a/ren/str.ts +++ b/ren/str.ts @@ -1,5 +1,6 @@ import { AnyNode, + AttrEntry, Attrs, Elem, Fragment, @@ -15,17 +16,31 @@ interface StrRendererOpts { doctype?: string; forceRenderDoctype?: boolean; wrapNode?: (node: AnyNode) => AnyNode; + onVisitAttr?: (entry: AttrEntry, params: OnVisitAttrParams) => AttrEntry; +} + +interface StrRendererHooks { + onVisitAttr: (entry: AttrEntry, params: OnVisitAttrParams) => AttrEntry; +} + +export interface OnVisitAttrParams { + readonly tagName: string; + readonly attrs: Attrs; } export class StrRenderer implements Renderer { #doctype: string; #forceRenderDoctype: boolean; #wrapNode: (node: AnyNode) => AnyNode; + #hooks: StrRendererHooks; constructor(opts?: StrRendererOpts) { this.#doctype = opts?.doctype ?? "html"; this.#forceRenderDoctype = opts?.forceRenderDoctype ?? false; this.#wrapNode = opts?.wrapNode ?? identity; + this.#hooks = { + onVisitAttr: opts?.onVisitAttr ?? identity, + }; } render(node: AnyNode): string { @@ -34,7 +49,7 @@ export class StrRenderer implements Renderer { (isElem(wrappedNode) && wrappedNode.tagName === "html"); return concat([ shouldRenderDoctype && encodeDoctype(this.#doctype), - encodeAnyNode(wrappedNode), + encodeAnyNode(wrappedNode, this.#hooks), ]); } } @@ -47,36 +62,44 @@ function encodeDoctype(value: string): string { return ``; } -function encodeAnyNode(node: AnyNode): string { +function encodeAnyNode(node: AnyNode, hooks: StrRendererHooks): string { return isTextNode(node) ? encodeTextNode(node) : isFragment(node) - ? encodeHtmlFragment(node) - : encodeHtmlElement(node); + ? encodeHtmlFragment(node, hooks) + : encodeHtmlElement(node, hooks); } function encodeTextNode(node: TextNode): string { return node.innerText; } -function encodeHtmlFragment(node: Fragment): string { - return concat(node.children.map(encodeAnyNode)); +function encodeHtmlFragment(node: Fragment, hooks: StrRendererHooks): string { + return concat(node.children.map((ch) => encodeAnyNode(ch, hooks))); } function encodeHtmlElement( { tagName, attrs, children }: Elem, + hooks: StrRendererHooks, ): string { - const open = `<${join(" ", [tagName, encodeAttrs(attrs)])}>`; + const open = `<${join(" ", [tagName, encodeAttrs(tagName, attrs, hooks)])}>`; if (isSelfClosedTagName(tagName)) return open; - const encodedChildren = children.map(encodeAnyNode); + const encodedChildren = children.map((ch) => encodeAnyNode(ch, hooks)); return `${open}${concat(encodedChildren)}`; } -function encodeAttrs(attrs: Attrs): string { +function encodeAttrs( + tagName: string, + attrs: Attrs, + hooks: StrRendererHooks, +): string { return join( " ", - Object.entries(attrs).map(([key, value]) => encodeAttr(key, value)), + Object.entries(attrs).map((entry) => { + const [key, value] = hooks.onVisitAttr(entry, { tagName, attrs }); + return encodeAttr(key, value); + }), ); }