feat(node): add fragment
feat(node): add async nodes
This commit is contained in:
parent
0978aa8754
commit
a0df5e48fa
3 changed files with 85 additions and 55 deletions
|
@ -1,29 +1,75 @@
|
||||||
import { isNil, Nilable } from "./lang.mjs";
|
import { isNil, Nilable } from "./lang.mjs";
|
||||||
|
|
||||||
export type AnyNode = TextNode | Node;
|
export type AnyNode = AnySyncNode | AnyAsyncNode;
|
||||||
|
export type AnyAsyncNode = Promise<AnySyncNode>;
|
||||||
|
export type AnySyncNode = TextNode | Elem | Frag;
|
||||||
|
|
||||||
export class TextNode {
|
export class TextNode extends String {}
|
||||||
#text: string;
|
|
||||||
|
|
||||||
constructor(text: string) {
|
export function F(children: AnyNode[]): Frag {
|
||||||
this.#text = text;
|
return new Frag().withChildren(children);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Frag {
|
||||||
|
#children: Nilable<AnyNode[]>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.#children = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
get text(): string {
|
get children(): Nilable<AnyNode[]> {
|
||||||
return this.#text;
|
return this.#children;
|
||||||
|
}
|
||||||
|
|
||||||
|
withText(text: string): this {
|
||||||
|
this.addText(text);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addText(text: string): void {
|
||||||
|
this.addChild(new TextNode(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeWithChildren(nodes?: Nilable<AnyNode[]>): 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<AnyNode[]>
|
||||||
|
): Elem {
|
||||||
|
return new Elem(tagName).withAttrs(attrs).maybeWithChildren(children);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ElemAttrs = Record<string, unknown>;
|
||||||
|
|
||||||
|
export class Elem extends Frag {
|
||||||
#tagName: string;
|
#tagName: string;
|
||||||
#attrs: Record<string, unknown>;
|
#attrs: ElemAttrs;
|
||||||
#children: Nilable<AnyNode[]>;
|
|
||||||
#isSelfClosed: boolean;
|
#isSelfClosed: boolean;
|
||||||
|
|
||||||
constructor(tagName: string) {
|
constructor(tagName: string) {
|
||||||
|
super();
|
||||||
this.#tagName = tagName;
|
this.#tagName = tagName;
|
||||||
this.#attrs = {};
|
this.#attrs = {};
|
||||||
this.#children = undefined;
|
|
||||||
this.#isSelfClosed = selfClosedTagNames.has(tagName);
|
this.#isSelfClosed = selfClosedTagNames.has(tagName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,16 +81,12 @@ export class Node {
|
||||||
return this.#attrs;
|
return this.#attrs;
|
||||||
}
|
}
|
||||||
|
|
||||||
get children(): Nilable<AnyNode[]> {
|
withAttrs(attrs: Record<string, unknown>): Elem {
|
||||||
return this.#children;
|
|
||||||
}
|
|
||||||
|
|
||||||
withAttrs(attrs: Record<string, unknown>): Node {
|
|
||||||
Object.entries(attrs).forEach(([key, value]) => this.addAttr(key, value));
|
Object.entries(attrs).forEach(([key, value]) => this.addAttr(key, value));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
withAttr(name: string, value: unknown): Node {
|
withAttr(name: string, value: unknown): Elem {
|
||||||
this.addAttr(name, value);
|
this.addAttr(name, value);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -53,30 +95,10 @@ export class Node {
|
||||||
this.#attrs[name] = value;
|
this.#attrs[name] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
withText(text: string): Node {
|
addChild(node: AnySyncNode): void {
|
||||||
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 {
|
|
||||||
if (this.#isSelfClosed)
|
if (this.#isSelfClosed)
|
||||||
throw new Error("You cannot add child to self closed element");
|
throw new Error("You cannot add child to self closed element");
|
||||||
if (isNil(this.#children)) this.#children = [];
|
super.addChild(node);
|
||||||
this.#children.push(node);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
36
src/str.mts
36
src/str.mts
|
@ -1,30 +1,38 @@
|
||||||
import { Renderer } from "./types.mjs";
|
import { Renderer } from "./types.mjs";
|
||||||
import { AnyNode, Node, TextNode } from "./node.mjs";
|
|
||||||
import { isBool, isNil, Nullable } from "./lang.mjs";
|
import { isBool, isNil, Nullable } from "./lang.mjs";
|
||||||
|
import { AnyNode, Elem, Frag, TextNode } from "./nodes.mjs";
|
||||||
|
|
||||||
export class StrRenderer implements Renderer<string> {
|
export class StrRenderer implements Renderer<string> {
|
||||||
async render(node: Node): Promise<string> {
|
async render(node: Elem | Promise<Elem>): Promise<string> {
|
||||||
return encodeNode(node);
|
return encodeNode(await node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function encodeAnyNode(node: AnyNode): string {
|
async function encodeAnyNode(node: AnyNode): Promise<string> {
|
||||||
return node instanceof TextNode ? encodeTextNode(node) : encodeNode(node);
|
const syncNode = await node;
|
||||||
|
return syncNode instanceof TextNode
|
||||||
|
? encodeTextNode(syncNode)
|
||||||
|
: encodeNode(syncNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
function encodeTextNode(node: TextNode): string {
|
function encodeTextNode(node: TextNode): string {
|
||||||
return node.text;
|
return String(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
function encodeNode(node: Node): string {
|
async function encodeNode(node: Elem | Frag): Promise<string> {
|
||||||
return encodeHtml(
|
const encodedChildren = isNil(node.children)
|
||||||
node.tagName,
|
? undefined
|
||||||
node.attrs,
|
: await Promise.all(node.children.map(encodeAnyNode));
|
||||||
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,
|
tagName: string,
|
||||||
attrs?: Record<string, unknown>,
|
attrs?: Record<string, unknown>,
|
||||||
children?: string[]
|
children?: string[]
|
||||||
|
@ -34,7 +42,7 @@ function encodeHtml(
|
||||||
return `${open}${children.join("")}</${tagName}>`;
|
return `${open}${children.join("")}</${tagName}>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function encodeAttrs(attrs?: Record<string, unknown>): string {
|
function encodeAttrs(attrs?: Record<string, unknown>): Nullable<string> {
|
||||||
if (!attrs) return "";
|
if (!attrs) return "";
|
||||||
|
|
||||||
return Object.entries(attrs)
|
return Object.entries(attrs)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Node } from "./node.mjs";
|
import { Elem } from "./nodes.mjs";
|
||||||
|
|
||||||
export interface Renderer<T> {
|
export interface Renderer<T> {
|
||||||
render(node: Node): Promise<T>;
|
render(node: Elem | Promise<Elem>): Promise<T>;
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue