diff --git a/src/node.mts b/src/nodes.mts similarity index 57% rename from src/node.mts rename to src/nodes.mts index 770ea3f..ec5ff8b 100644 --- a/src/node.mts +++ b/src/nodes.mts @@ -1,29 +1,75 @@ import { isNil, Nilable } from "./lang.mjs"; -export type AnyNode = TextNode | Node; +export type AnyNode = AnySyncNode | AnyAsyncNode; +export type AnyAsyncNode = Promise; +export type AnySyncNode = TextNode | Elem | Frag; -export class TextNode { - #text: string; +export class TextNode extends String {} - constructor(text: string) { - this.#text = text; +export function F(children: AnyNode[]): Frag { + return new Frag().withChildren(children); +} + +export class Frag { + #children: Nilable; + + constructor() { + this.#children = undefined; } - get text(): string { - return this.#text; + get children(): Nilable { + return this.#children; + } + + withText(text: string): this { + this.addText(text); + return this; + } + + addText(text: string): void { + this.addChild(new TextNode(text)); + } + + maybeWithChildren(nodes?: Nilable): this { + if (isNil(nodes)) return this; + return this.withChildren(nodes); + } + + withChildren(nodes: AnyNode[]): this { + nodes.forEach((n) => this.addChild(n)); + return this; + } + + withChild(node: AnyNode): this { + this.addChild(node); + return this; + } + + addChild(node: AnyNode): void { + if (isNil(this.#children)) this.#children = []; + this.#children.push(node); } } -export class Node { +export function E( + tagName: string, + attrs: ElemAttrs, + children?: Nilable +): Elem { + return new Elem(tagName).withAttrs(attrs).maybeWithChildren(children); +} + +export type ElemAttrs = Record; + +export class Elem extends Frag { #tagName: string; - #attrs: Record; - #children: Nilable; + #attrs: ElemAttrs; #isSelfClosed: boolean; constructor(tagName: string) { + super(); this.#tagName = tagName; this.#attrs = {}; - this.#children = undefined; this.#isSelfClosed = selfClosedTagNames.has(tagName); } @@ -35,16 +81,12 @@ export class Node { return this.#attrs; } - get children(): Nilable { - return this.#children; - } - - withAttrs(attrs: Record): Node { + withAttrs(attrs: Record): Elem { Object.entries(attrs).forEach(([key, value]) => this.addAttr(key, value)); return this; } - withAttr(name: string, value: unknown): Node { + withAttr(name: string, value: unknown): Elem { this.addAttr(name, value); return this; } @@ -53,30 +95,10 @@ export class Node { this.#attrs[name] = value; } - withText(text: string): Node { - this.addText(text); - return this; - } - - addText(text: string): void { - this.addChild(new TextNode(text)); - } - - withChildren(nodes: AnyNode[]): Node { - nodes.forEach((n) => this.addChild(n)); - return this; - } - - withChild(node: AnyNode): Node { - this.addChild(node); - return this; - } - - addChild(node: AnyNode): void { + addChild(node: AnySyncNode): void { if (this.#isSelfClosed) throw new Error("You cannot add child to self closed element"); - if (isNil(this.#children)) this.#children = []; - this.#children.push(node); + super.addChild(node); } } diff --git a/src/str.mts b/src/str.mts index d0d9048..dd0a1c4 100644 --- a/src/str.mts +++ b/src/str.mts @@ -1,30 +1,38 @@ import { Renderer } from "./types.mjs"; -import { AnyNode, Node, TextNode } from "./node.mjs"; import { isBool, isNil, Nullable } from "./lang.mjs"; +import { AnyNode, Elem, Frag, TextNode } from "./nodes.mjs"; export class StrRenderer implements Renderer { - async render(node: Node): Promise { - return encodeNode(node); + async render(node: Elem | Promise): Promise { + return encodeNode(await node); } } -function encodeAnyNode(node: AnyNode): string { - return node instanceof TextNode ? encodeTextNode(node) : encodeNode(node); +async function encodeAnyNode(node: AnyNode): Promise { + const syncNode = await node; + return syncNode instanceof TextNode + ? encodeTextNode(syncNode) + : encodeNode(syncNode); } function encodeTextNode(node: TextNode): string { - return node.text; + return String(node); } -function encodeNode(node: Node): string { - return encodeHtml( - node.tagName, - node.attrs, - node.children?.map(encodeAnyNode) - ); +async function encodeNode(node: Elem | Frag): Promise { + const encodedChildren = isNil(node.children) + ? undefined + : await Promise.all(node.children.map(encodeAnyNode)); + return node instanceof Elem + ? encodeHtmlElement(node.tagName, node.attrs, encodedChildren) + : encodeHtmlFragment(encodedChildren); } -function encodeHtml( +function encodeHtmlFragment(children?: string[]): string { + return children?.join("") ?? ""; +} + +function encodeHtmlElement( tagName: string, attrs?: Record, children?: string[] @@ -34,7 +42,7 @@ function encodeHtml( return `${open}${children.join("")}`; } -function encodeAttrs(attrs?: Record): string { +function encodeAttrs(attrs?: Record): Nullable { if (!attrs) return ""; return Object.entries(attrs) diff --git a/src/types.mts b/src/types.mts index bd8e256..3e04a75 100644 --- a/src/types.mts +++ b/src/types.mts @@ -1,5 +1,5 @@ -import { Node } from "./node.mjs"; +import { Elem } from "./nodes.mjs"; export interface Renderer { - render(node: Node): Promise; + render(node: Elem | Promise): Promise; }