refac to deno

This commit is contained in:
Dmitriy Pleshevskiy 2022-05-22 00:03:48 +03:00
parent 1b21c2d796
commit b326d14676
33 changed files with 544 additions and 3347 deletions

View File

@ -1,2 +0,0 @@
/*
!/src

View File

@ -1,28 +0,0 @@
parser: "@typescript-eslint/parser"
env:
es6: true
node: true
parserOptions:
ecmaVersion: 2020
sourceType: "module"
extends:
- prettier
- plugin:prettier/recommended
- "plugin:@typescript-eslint/recommended"
rules:
"@typescript-eslint/no-unused-vars":
- error
- vars: all
args: after-used
argsIgnorePattern: ^_
varsIgnorePattern: ^_
ignoreRestSiblings: true
"@typescript-eslint/no-empty-interface": off
"@typescript-eslint/no-explicit-any": off
"@typescript-eslint/explicit-function-return-type":
- warn
- allowExpressions: false
allowTypedFunctionExpressions: true
allowHigherOrderFunctions: true
"@typescript-eslint/camelcase": off
"@typescript-eslint/no-use-before-define": off

16
.gitignore vendored
View File

@ -8,17 +8,9 @@
# config
!/.gitignore
!/.eslintignore
!/.eslintrc.yml
!/tsconfig.json
!/deno.json
!/import_map.json
# node modules
!/package.json
!/package-lock.json
!/core
!/ren
# sources
!/src
!/scripts
# builded
!/lib

68
core/node.test.ts Normal file
View File

@ -0,0 +1,68 @@
import { assertEquals, assertInstanceOf } from "testing/asserts.ts";
import { E, Elem, F, Fragment, TextNode } from "./node.ts";
Deno.test({
name: "should create text node from string",
fn: () => {
const sourceText = "hello world";
const tn = new TextNode(sourceText);
assertInstanceOf(tn, TextNode);
assertEquals(tn.innerText, sourceText);
},
});
Deno.test({
name: "should create fragment from array",
fn: () => {
const hello = new TextNode("hello");
const world = new TextNode("world");
const frag = new Fragment([hello, world]);
assertInstanceOf(frag, Fragment);
assertEquals(frag.children, [hello, world]);
},
});
Deno.test({
name: "should create fragment via util",
fn: () => {
const el = E("p", [], "hello world");
const innerFrag = F(["inner"]);
const frag = F(["hello", innerFrag, "world", el]);
assertInstanceOf(frag, Fragment);
assertEquals(frag.children, [
new TextNode("hello"),
new TextNode("inner"),
new TextNode("world"),
el,
]);
},
});
Deno.test({
name: "should create element",
fn: () => {
const child = new Elem("p", {}, [new TextNode("hello world")]);
const el = new Elem("div", { class: "hello" }, [child]);
assertEquals(el.tagName, "div");
assertEquals(el.attrs, { class: "hello" });
assertEquals(el.children, [child]);
},
});
Deno.test({
name: "should create element via util",
fn: () => {
const child = E("p", [], "hello world");
const el = E("div", { class: "hello" }, [child]);
assertEquals(el.tagName, "div");
assertEquals(el.attrs, { class: "hello" });
assertEquals(el.children, [child]);
},
});

112
core/node.ts Normal file
View File

@ -0,0 +1,112 @@
import { isNotSkip, isNum, isStr, Nilable, Skipable, toArr } from "./utils.ts";
export type Attrs = Record<string, string>;
export type AnyNode = Fragment | FragmentNode;
export function isTextNode(val: unknown): val is TextNode {
return val instanceof TextNode;
}
export class TextNode {
#innerText: string;
get innerText(): string {
return this.#innerText;
}
constructor(text: string) {
this.#innerText = text;
}
}
export function isFragment(val: unknown): val is Fragment {
return val instanceof Fragment;
}
export type FragmentNode = Elem | TextNode;
export class Fragment {
#children: FragmentNode[];
get children(): FragmentNode[] {
return this.#children;
}
constructor(children: FragmentNode[]) {
this.#children = children;
}
}
export function isElem(val: unknown): val is Elem {
return val instanceof Elem;
}
export type ElemTagName =
| keyof HTMLElementTagNameMap
| keyof SVGElementTagNameMap;
export class Elem {
#tagName: ElemTagName;
#attrs: Attrs;
#children: AnyNode[];
get tagName(): ElemTagName {
return this.#tagName;
}
get attrs(): Attrs {
return this.#attrs;
}
get children(): AnyNode[] {
return this.#children;
}
constructor(
tagName: ElemTagName,
attrs?: Nilable<Attrs>,
children?: Nilable<AnyNode[]>,
) {
this.#tagName = tagName;
this.#attrs = attrs ?? {};
this.#children = children ?? [];
}
}
export function F(children: Skipable<EChild>[]): Fragment {
return new Fragment(
children.filter(isNotSkip).flatMap((c) =>
normFragmentChild(normElemChild(c))
),
);
}
type EAttrs = Attrs | Attrs[];
type ETextNode = string | number | TextNode;
type EChild = ETextNode | Fragment | Elem;
export function E(
tagName: ElemTagName,
attrs: EAttrs,
children?: ETextNode | Skipable<EChild>[],
): Elem {
return new Elem(
tagName,
mergeAttrs(attrs ?? []),
toArr(children ?? []).filter(isNotSkip).map(normElemChild),
);
}
function mergeAttrs(attrs: EAttrs): Attrs {
return !Array.isArray(attrs)
? attrs
: attrs.reduce((acc, attrs) => ({ ...acc, ...attrs }), {} as Attrs);
}
function normFragmentChild(node: AnyNode): FragmentNode | FragmentNode[] {
return isFragment(node) ? node.children : node;
}
function normElemChild(node: EChild): AnyNode {
return isStr(node) || isNum(node) ? new TextNode(String(node)) : node;
}

29
core/utils.test.ts Normal file
View File

@ -0,0 +1,29 @@
import { assertEquals } from "testing/asserts.ts";
import { isNil, isSkip } from "./utils.ts";
Deno.test({
name: "should check value on nil",
fn: () => {
assertEquals(isNil(null), true);
assertEquals(isNil(undefined), true);
assertEquals(isNil(0), false);
assertEquals(isNil(""), false);
assertEquals(isNil(false), false);
assertEquals(isNil({}), false);
assertEquals(isNil([]), false);
},
});
Deno.test({
name: "should check value on skip",
fn: () => {
assertEquals(isSkip(null), true);
assertEquals(isSkip(undefined), true);
assertEquals(isSkip(0), false);
assertEquals(isSkip(""), false);
assertEquals(isSkip(false), true);
assertEquals(isSkip({}), false);
assertEquals(isSkip([]), false);
},
});

43
core/utils.ts Normal file
View File

@ -0,0 +1,43 @@
export function concat(arr: Skipable<string>[]): string {
return join("", arr);
}
export function join(sep: string, arr: Skipable<string>[]): string {
return arr.filter((item) => !(isSkip(item) || isEmptyStr(item))).join(sep);
}
export type Skipable<T> = T | Skip;
export type Skip = Nil | false;
export function isNotSkip<T>(val: Skipable<T>): val is T {
return !isSkip(val);
}
export function isSkip(val: unknown): val is Skip {
return isNil(val) || val === false;
}
export type Nilable<T> = T | Nil;
export type Nil = undefined | null;
export function isNil(val: unknown): val is Nil {
return val == null;
}
export function isEmptyStr(val: unknown): val is "" {
return isStr(val) && val.length === 0;
}
export function isStr(val: unknown): val is string {
return typeof val === "string";
}
export function isNum(val: unknown): val is number {
return typeof val === "number";
}
export function toArr<T>(val: T | T[]): T[] {
return Array.isArray(val) ? val : [val];
}

6
deno.json Normal file
View File

@ -0,0 +1,6 @@
{
"compilerOptions": {
"lib": ["deno.ns", "dom"]
},
"importMap": "./import_map.json"
}

5
import_map.json Normal file
View File

@ -0,0 +1,5 @@
{
"imports": {
"testing/": "https://deno.land/std@0.139.0/testing/"
}
}

3
lib/index.d.ts vendored
View File

@ -1,3 +0,0 @@
export * from "./str.js";
export * from "./nodes.js";
export * from "./types.js";

View File

@ -1,3 +0,0 @@
export * from "./str.mjs";
export * from "./nodes.mjs";
export * from "./types.mjs";

7
lib/lang.d.ts vendored
View File

@ -1,7 +0,0 @@
export declare function isNil<T>(v: Nilable<T>): v is Nil;
export declare type Nullable<T> = T | null;
export declare type Nilable<T> = T | Nil;
export declare type Nil = null | undefined;
export declare function isBool(v: unknown): v is boolean;
export declare function isStr(v: unknown): v is string;
export declare function intoArr<T>(v: T | T[]): T[];

View File

@ -1,12 +0,0 @@
export function isNil(v) {
return v == null;
}
export function isBool(v) {
return typeof v === "boolean";
}
export function isStr(v) {
return typeof v === "string";
}
export function intoArr(v) {
return Array.isArray(v) ? v : [v];
}

28
lib/nodes.d.ts vendored
View File

@ -1,28 +0,0 @@
import { Nil, Nilable } from "./lang.js";
export declare function Et(tagName: string, ...texts: string[]): Elem;
export declare function Ea(tagName: string, attrs?: ElemAttrs, children?: AnyNode | AnyNode[]): Elem;
export declare function E(tagName: string, children: AnyNode | AnyNode[]): Elem;
export declare function F(...children: AnyNode[]): Frag;
export declare type AnyNode = TextNode | Elem | Frag | Nil | false;
export declare type TextNode = string;
export declare class Frag {
#private;
constructor();
get children(): Nilable<AnyNode[]>;
withText(texts: TextNode | TextNode[]): this;
addText(texts: TextNode | TextNode[]): void;
withChildren(nodes: AnyNode | AnyNode[]): this;
addChildren(nodes: AnyNode | AnyNode[]): void;
addChild(node: AnyNode): void;
}
export declare type ElemAttrs = Record<string, unknown>;
export declare class Elem extends Frag {
#private;
constructor(tagName: string);
get tagName(): string;
get attrs(): Record<string, unknown>;
withAttrs(attrs: ElemAttrs): Elem;
addAttrs(attrs: ElemAttrs): void;
addAttr(name: string, value: unknown): void;
addChild(node: AnyNode): void;
}

View File

@ -1,96 +0,0 @@
import { intoArr, isNil } from "./lang.mjs";
export function Et(tagName, ...texts) {
return new Elem(tagName).withText(texts);
}
export function Ea(tagName, attrs, children) {
const el = new Elem(tagName);
if (attrs)
el.addAttrs(attrs);
if (children)
el.addChildren(children);
return el;
}
export function E(tagName, children) {
return new Elem(tagName).withChildren(children);
}
export function F(...children) {
return new Frag().withChildren(children);
}
export class Frag {
#children;
constructor() {
this.#children = undefined;
}
get children() {
return this.#children;
}
withText(texts) {
this.addText(texts);
return this;
}
addText(texts) {
this.addChildren(intoArr(texts)
.map((t) => t.trim())
.join(" "));
}
withChildren(nodes) {
this.addChildren(nodes);
return this;
}
addChildren(nodes) {
intoArr(nodes).forEach((n) => this.addChild(n));
}
addChild(node) {
if (isNil(this.#children))
this.#children = [];
this.#children.push(node);
}
}
export class Elem extends Frag {
#tagName;
#attrs;
#isSelfClosed;
constructor(tagName) {
super();
this.#tagName = tagName;
this.#attrs = {};
this.#isSelfClosed = selfClosedTagNames.has(tagName);
}
get tagName() {
return this.#tagName;
}
get attrs() {
return this.#attrs;
}
withAttrs(attrs) {
this.addAttrs(attrs);
return this;
}
addAttrs(attrs) {
Object.entries(attrs).forEach(([key, value]) => this.addAttr(key, value));
}
addAttr(name, value) {
this.#attrs[name] = value;
}
addChild(node) {
if (this.#isSelfClosed)
throw new Error("You cannot add child to self closed element");
super.addChild(node);
}
}
const selfClosedTagNames = new Set([
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"param",
"source",
"track",
"wbr",
]);

5
lib/str.d.ts vendored
View File

@ -1,5 +0,0 @@
import { Renderer } from "./types.js";
import { Elem } from "./nodes.js";
export declare class StrRenderer implements Renderer<string> {
render(node: Elem): string;
}

View File

@ -1,48 +0,0 @@
import { isBool, isNil, isStr } from "./lang.mjs";
import { Elem } from "./nodes.mjs";
export class StrRenderer {
render(node) {
return encodeNode(node);
}
}
function encodeAnyNode(node) {
return !node ? null : isStr(node) ? encodeTextNode(node) : encodeNode(node);
}
function encodeTextNode(node) {
return String(node);
}
function encodeNode(node) {
const encodedChildren = isNil(node.children)
? undefined
: node.children.map(encodeAnyNode);
return node instanceof Elem
? encodeHtmlElement(node.tagName, node.attrs, encodedChildren)
: encodeHtmlFragment(encodedChildren);
}
function encodeHtmlFragment(children) {
return concat(children ?? []);
}
function encodeHtmlElement(tagName, attrs, children) {
const open = `<${join(" ", [tagName, encodeAttrs(attrs)])}>`;
if (isNil(children))
return open;
return `${open}${concat(children)}</${tagName}>`;
}
function encodeAttrs(attrs) {
if (!attrs)
return null;
return join(" ", Object.entries(attrs).map(([key, value]) => encodeAttr(key, value)));
}
function encodeAttr(key, value) {
if (isNil(value))
return null;
if (isBool(value))
return value ? key : null;
return `${key}="${value}"`;
}
function concat(arr) {
return join("", arr);
}
function join(sep, arr) {
return arr.filter(Boolean).join(sep);
}

4
lib/types.d.ts vendored
View File

@ -1,4 +0,0 @@
import { Elem } from "./nodes.js";
export interface Renderer<T> {
render(node: Elem): T;
}

View File

@ -1 +0,0 @@
export {};

2821
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +0,0 @@
{
"name": "ren",
"description": "",
"version": "0.0.1",
"main": "lib/index.mjs",
"module": "lib/index.mjs",
"types": "lib/index.d.ts",
"directories": {
"lib": "lib"
},
"devDependencies": {
"@types/node": "^17.0.21",
"@typescript-eslint/eslint-plugin": "^5.14.0",
"@typescript-eslint/parser": "^5.14.0",
"eslint": "^8.10.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"prettier": "^2.5.1",
"tsc-watch": "^4.6.0",
"typescript": "^4.6.2"
},
"repository": {
"type": "git",
"url": "git+https://github.com/pleshevskiy/ren.git"
},
"author": "Dmitriy Pleshevskiy <dmitriy@ideascup.me>",
"license": "MIT",
"bugs": {
"url": "https://github.com/pleshevskiy/ren/issues"
},
"homepage": "https://github.com/pleshevskiy/ren#readme"
}

33
ren/attrs.test.ts Normal file
View File

@ -0,0 +1,33 @@
import { assertEquals } from "testing/asserts.ts";
import * as a from "./attrs.ts";
Deno.test({
name: "should return empty attrs object",
fn: () => {
assertEquals(a.classNames([]), {});
assertEquals(a.classNames([false, null, undefined]), {});
},
});
Deno.test({
name: "should return class attr",
fn: () => {
assertEquals(a.classNames(["hello"]), { class: "hello" });
assertEquals(a.classNames(["hello", "world"]), { class: "hello world" });
},
});
Deno.test({
name: "should return filter skipable and return class attr",
fn: () => {
assertEquals(
a.classNames([
null && "my",
undefined && "name",
"hello",
false && "world",
]),
{ class: "hello" },
);
},
});

7
ren/attrs.ts Normal file
View File

@ -0,0 +1,7 @@
import { Attrs } from "../core/node.ts";
import { isNotSkip, join, Skipable } from "../core/utils.ts";
export function classNames(vals: Skipable<string>[]): Attrs {
const val = join(" ", vals.filter(isNotSkip));
return !val.length ? {} : { class: val };
}

133
ren/str.test.ts Normal file
View File

@ -0,0 +1,133 @@
import { assertEquals } from "testing/asserts.ts";
import { E, F, TextNode } from "../core/node.ts";
import { StrRenderer } from "./str.ts";
Deno.test({
name: "should render text node",
fn: () => {
const tn = new TextNode("hello world");
const ren = new StrRenderer();
const res = ren.render(tn);
assertEquals(res, "hello world");
},
});
Deno.test({
name: "should render element",
fn: () => {
const el = E("p", [], "hello world");
const ren = new StrRenderer();
const res = ren.render(el);
assertEquals(res, "<p>hello world</p>");
},
});
Deno.test({
name: "should render empty fragment as empty string",
fn: () => {
const frag = F([]);
const ren = new StrRenderer();
const res = ren.render(frag);
assertEquals(res, "");
},
});
Deno.test({
name: "should render fragment",
fn: () => {
const frag = F([
"hello world",
E("div", { class: "hello" }),
E("p", [], "world"),
]);
const ren = new StrRenderer();
const res = ren.render(frag);
assertEquals(res, 'hello world<div class="hello"></div><p>world</p>');
},
});
Deno.test({
name: "should recursive render elements",
fn: () => {
const layout = E("body", [], [
E("div", { id: "root" }, [
E("p", [], "hello world"),
]),
]);
const ren = new StrRenderer();
const res = ren.render(layout);
assertEquals(res, '<body><div id="root"><p>hello world</p></div></body>');
},
});
Deno.test({
name: "should render attributes",
fn: () => {
const layout = E("body", { class: "body-lock" }, [
E("div", { id: "root" }, [
E("p", { class: "first" }, "hello world"),
]),
]);
const ren = new StrRenderer();
const res = ren.render(layout);
assertEquals(
res,
'<body class="body-lock"><div id="root"><p class="first">hello world</p></div></body>',
);
},
});
Deno.test({
name: "should render default doctype if root node is html",
fn: () => {
const layout = E("html", []);
const ren = new StrRenderer();
const res = ren.render(layout);
assertEquals(res, "<!doctype html><html></html>");
},
});
Deno.test({
name: "should render custom doctype if root node is html",
fn: () => {
const layout = E("html", []);
const ren = new StrRenderer({
doctype:
'html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"',
});
const res = ren.render(layout);
assertEquals(
res,
'<!doctype html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"><html></html>',
);
},
});
Deno.test({
name: "should force render doctype if root nod is not html",
fn: () => {
const layout = E("body", [], []);
const ren = new StrRenderer({ forceRenderDoctype: true });
const res = ren.render(layout);
assertEquals(res, "<!doctype html><body></body>");
},
});

99
ren/str.ts Normal file
View File

@ -0,0 +1,99 @@
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",
];

5
ren/types.ts Normal file
View File

@ -0,0 +1,5 @@
import { AnyNode } from "../core/node.ts";
export interface Renderer<T> {
render(node: AnyNode): T;
}

View File

@ -1,6 +0,0 @@
#!/bin/bash
for f in lib/*.d.mts;
do
sed -i 's/.mjs"/.js"/' "$f"
mv "$f" "${f//\.d\.mts/.d.ts}"
done

View File

@ -1,3 +0,0 @@
export * from "./str.mjs";
export * from "./nodes.mjs";
export * from "./types.mjs";

View File

@ -1,21 +0,0 @@
export function isNil<T>(v: Nilable<T>): v is Nil {
return v == null;
}
export type Nullable<T> = T | null;
export type Nilable<T> = T | Nil;
export type Nil = null | undefined;
export function isBool(v: unknown): v is boolean {
return typeof v === "boolean";
}
export function isStr(v: unknown): v is string {
return typeof v === "string";
}
export function intoArr<T>(v: T | T[]): T[] {
return Array.isArray(v) ? v : [v];
}

View File

@ -1,125 +0,0 @@
import { intoArr, isNil, Nil, Nilable } from "./lang.mjs";
export function Et(tagName: string, ...texts: string[]): Elem {
return new Elem(tagName).withText(texts);
}
export function Ea(
tagName: string,
attrs?: ElemAttrs,
children?: AnyNode | AnyNode[]
): Elem {
const el = new Elem(tagName);
if (attrs) el.addAttrs(attrs);
if (children) el.addChildren(children);
return el;
}
export function E(tagName: string, children: AnyNode | AnyNode[]): Elem {
return new Elem(tagName).withChildren(children);
}
export function F(...children: AnyNode[]): Frag {
return new Frag().withChildren(children);
}
export type AnyNode = TextNode | Elem | Frag | Nil | false;
export type TextNode = string;
export class Frag {
#children: Nilable<AnyNode[]>;
constructor() {
this.#children = undefined;
}
get children(): Nilable<AnyNode[]> {
return this.#children;
}
withText(texts: TextNode | TextNode[]): this {
this.addText(texts);
return this;
}
addText(texts: TextNode | TextNode[]): void {
this.addChildren(
intoArr(texts)
.map((t) => t.trim())
.join(" ")
);
}
withChildren(nodes: AnyNode | AnyNode[]): this {
this.addChildren(nodes);
return this;
}
addChildren(nodes: AnyNode | AnyNode[]): void {
intoArr(nodes).forEach((n) => this.addChild(n));
}
addChild(node: AnyNode): void {
if (isNil(this.#children)) this.#children = [];
this.#children.push(node);
}
}
export type ElemAttrs = Record<string, unknown>;
export class Elem extends Frag {
#tagName: string;
#attrs: ElemAttrs;
#isSelfClosed: boolean;
constructor(tagName: string) {
super();
this.#tagName = tagName;
this.#attrs = {};
this.#isSelfClosed = selfClosedTagNames.has(tagName);
}
get tagName(): string {
return this.#tagName;
}
get attrs(): Record<string, unknown> {
return this.#attrs;
}
withAttrs(attrs: ElemAttrs): Elem {
this.addAttrs(attrs);
return this;
}
addAttrs(attrs: ElemAttrs): void {
Object.entries(attrs).forEach(([key, value]) => this.addAttr(key, value));
}
addAttr(name: string, value: unknown): void {
this.#attrs[name] = value;
}
addChild(node: AnyNode): void {
if (this.#isSelfClosed)
throw new Error("You cannot add child to self closed element");
super.addChild(node);
}
}
const selfClosedTagNames = new Set([
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"param",
"source",
"track",
"wbr",
]);

View File

@ -1,63 +0,0 @@
import { Renderer } from "./types.mjs";
import { isBool, isNil, isStr, Nilable, Nullable } from "./lang.mjs";
import { AnyNode, Elem, ElemAttrs, Frag, TextNode } from "./nodes.mjs";
export class StrRenderer implements Renderer<string> {
render(node: Elem): string {
return encodeNode(node);
}
}
function encodeAnyNode(node: AnyNode): Nullable<string> {
return !node ? null : isStr(node) ? encodeTextNode(node) : encodeNode(node);
}
function encodeTextNode(node: TextNode): string {
return String(node);
}
function encodeNode(node: Elem | Frag): string {
const encodedChildren = isNil(node.children)
? undefined
: node.children.map(encodeAnyNode);
return node instanceof Elem
? encodeHtmlElement(node.tagName, node.attrs, encodedChildren)
: encodeHtmlFragment(encodedChildren);
}
function encodeHtmlFragment(children?: Nilable<string>[]): string {
return concat(children ?? []);
}
function encodeHtmlElement(
tagName: string,
attrs?: ElemAttrs,
children?: Nilable<string>[]
): string {
const open = `<${join(" ", [tagName, encodeAttrs(attrs)])}>`;
if (isNil(children)) return open;
return `${open}${concat(children)}</${tagName}>`;
}
function encodeAttrs(attrs?: ElemAttrs): Nullable<string> {
if (!attrs) return null;
return join(
" ",
Object.entries(attrs).map(([key, value]) => encodeAttr(key, value))
);
}
function encodeAttr(key: string, value: unknown): Nullable<string> {
if (isNil(value)) return null;
if (isBool(value)) return value ? key : null;
return `${key}="${value}"`;
}
function concat(arr: Nilable<string>[]): string {
return join("", arr);
}
function join(sep: string, arr: Nilable<string>[]): string {
return arr.filter(Boolean).join(sep);
}

View File

@ -1,5 +0,0 @@
import { Elem } from "./nodes.mjs";
export interface Renderer<T> {
render(node: Elem): T;
}

View File

@ -1,22 +0,0 @@
{
"compilerOptions": {
"outDir": "lib",
"target": "esnext",
"module": "esnext",
"lib": [],
"moduleResolution": "node",
"declaration": true,
"rootDir": "src",
"forceConsistentCasingInFileNames": true,
"suppressImplicitAnyIndexErrors": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true
},
"include": [
"src/**/*.mts"
],
}