refac to deno
This commit is contained in:
parent
1b21c2d796
commit
b326d14676
33 changed files with 544 additions and 3347 deletions
|
@ -1,2 +0,0 @@
|
||||||
/*
|
|
||||||
!/src
|
|
|
@ -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
16
.gitignore
vendored
|
@ -8,17 +8,9 @@
|
||||||
|
|
||||||
# config
|
# config
|
||||||
!/.gitignore
|
!/.gitignore
|
||||||
!/.eslintignore
|
!/deno.json
|
||||||
!/.eslintrc.yml
|
!/import_map.json
|
||||||
!/tsconfig.json
|
|
||||||
|
|
||||||
# node modules
|
!/core
|
||||||
!/package.json
|
!/ren
|
||||||
!/package-lock.json
|
|
||||||
|
|
||||||
# sources
|
|
||||||
!/src
|
|
||||||
!/scripts
|
|
||||||
|
|
||||||
# builded
|
|
||||||
!/lib
|
|
||||||
|
|
68
core/node.test.ts
Normal file
68
core/node.test.ts
Normal 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
112
core/node.ts
Normal 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
29
core/utils.test.ts
Normal 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
43
core/utils.ts
Normal 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
6
deno.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["deno.ns", "dom"]
|
||||||
|
},
|
||||||
|
"importMap": "./import_map.json"
|
||||||
|
}
|
5
import_map.json
Normal file
5
import_map.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"testing/": "https://deno.land/std@0.139.0/testing/"
|
||||||
|
}
|
||||||
|
}
|
3
lib/index.d.ts
vendored
3
lib/index.d.ts
vendored
|
@ -1,3 +0,0 @@
|
||||||
export * from "./str.js";
|
|
||||||
export * from "./nodes.js";
|
|
||||||
export * from "./types.js";
|
|
|
@ -1,3 +0,0 @@
|
||||||
export * from "./str.mjs";
|
|
||||||
export * from "./nodes.mjs";
|
|
||||||
export * from "./types.mjs";
|
|
7
lib/lang.d.ts
vendored
7
lib/lang.d.ts
vendored
|
@ -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[];
|
|
12
lib/lang.mjs
12
lib/lang.mjs
|
@ -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
28
lib/nodes.d.ts
vendored
|
@ -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;
|
|
||||||
}
|
|
|
@ -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
5
lib/str.d.ts
vendored
|
@ -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;
|
|
||||||
}
|
|
48
lib/str.mjs
48
lib/str.mjs
|
@ -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
4
lib/types.d.ts
vendored
|
@ -1,4 +0,0 @@
|
||||||
import { Elem } from "./nodes.js";
|
|
||||||
export interface Renderer<T> {
|
|
||||||
render(node: Elem): T;
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export {};
|
|
2821
package-lock.json
generated
2821
package-lock.json
generated
File diff suppressed because it is too large
Load diff
32
package.json
32
package.json
|
@ -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
33
ren/attrs.test.ts
Normal 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
7
ren/attrs.ts
Normal 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
133
ren/str.test.ts
Normal 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
99
ren/str.ts
Normal 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
5
ren/types.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { AnyNode } from "../core/node.ts";
|
||||||
|
|
||||||
|
export interface Renderer<T> {
|
||||||
|
render(node: AnyNode): T;
|
||||||
|
}
|
|
@ -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
|
|
|
@ -1,3 +0,0 @@
|
||||||
export * from "./str.mjs";
|
|
||||||
export * from "./nodes.mjs";
|
|
||||||
export * from "./types.mjs";
|
|
21
src/lang.mts
21
src/lang.mts
|
@ -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];
|
|
||||||
}
|
|
125
src/nodes.mts
125
src/nodes.mts
|
@ -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",
|
|
||||||
]);
|
|
63
src/str.mts
63
src/str.mts
|
@ -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);
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
import { Elem } from "./nodes.mjs";
|
|
||||||
|
|
||||||
export interface Renderer<T> {
|
|
||||||
render(node: Elem): T;
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
],
|
|
||||||
}
|
|
Reference in a new issue