.
This commit is contained in:
commit
0978aa8754
15 changed files with 3206 additions and 0 deletions
28
.eslintrc.yml
Normal file
28
.eslintrc.yml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
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
|
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
|
||||||
|
# editors
|
||||||
|
!/.vscode
|
||||||
|
|
||||||
|
# git
|
||||||
|
!/.gitignore
|
||||||
|
|
||||||
|
# makefile
|
||||||
|
!/makefile
|
||||||
|
|
||||||
|
# config
|
||||||
|
!/.eslintrc.yml
|
||||||
|
!/tsconfig.json
|
||||||
|
|
||||||
|
# node modules
|
||||||
|
!/package.json
|
||||||
|
!/package-lock.json
|
||||||
|
|
||||||
|
# sources
|
||||||
|
!/src
|
||||||
|
|
||||||
|
# builded
|
||||||
|
!/lib
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"editor.tabSize": 2
|
||||||
|
}
|
6
lib/lang.mjs
Normal file
6
lib/lang.mjs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export function isNil(v) {
|
||||||
|
return v == null;
|
||||||
|
}
|
||||||
|
export function isBool(v) {
|
||||||
|
return typeof v === "boolean";
|
||||||
|
}
|
80
lib/node.mjs
Normal file
80
lib/node.mjs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import { isNil } from "./lang.mjs";
|
||||||
|
export class TextNode {
|
||||||
|
#text;
|
||||||
|
constructor(text) {
|
||||||
|
this.#text = text;
|
||||||
|
}
|
||||||
|
get text() {
|
||||||
|
return this.#text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class Node {
|
||||||
|
#tagName;
|
||||||
|
#attrs;
|
||||||
|
#children;
|
||||||
|
#isSelfClosed;
|
||||||
|
constructor(tagName) {
|
||||||
|
this.#tagName = tagName;
|
||||||
|
this.#attrs = {};
|
||||||
|
this.#children = undefined;
|
||||||
|
this.#isSelfClosed = selfClosedTagNames.has(tagName);
|
||||||
|
}
|
||||||
|
get tagName() {
|
||||||
|
return this.#tagName;
|
||||||
|
}
|
||||||
|
get attrs() {
|
||||||
|
return this.#attrs;
|
||||||
|
}
|
||||||
|
get children() {
|
||||||
|
return this.#children;
|
||||||
|
}
|
||||||
|
withAttrs(attrs) {
|
||||||
|
Object.entries(attrs).forEach(([key, value]) => this.addAttr(key, value));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
withAttr(name, value) {
|
||||||
|
this.addAttr(name, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
addAttr(name, value) {
|
||||||
|
this.#attrs[name] = value;
|
||||||
|
}
|
||||||
|
withText(text) {
|
||||||
|
this.addText(text);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
addText(text) {
|
||||||
|
this.addChild(new TextNode(text));
|
||||||
|
}
|
||||||
|
withChildren(nodes) {
|
||||||
|
nodes.forEach((n) => this.addChild(n));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
withChild(node) {
|
||||||
|
this.addChild(node);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
addChild(node) {
|
||||||
|
if (this.#isSelfClosed)
|
||||||
|
throw new Error("You cannot add child to self closed element");
|
||||||
|
if (isNil(this.#children))
|
||||||
|
this.#children = [];
|
||||||
|
this.#children.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const selfClosedTagNames = new Set([
|
||||||
|
"area",
|
||||||
|
"base",
|
||||||
|
"br",
|
||||||
|
"col",
|
||||||
|
"embed",
|
||||||
|
"hr",
|
||||||
|
"img",
|
||||||
|
"input",
|
||||||
|
"link",
|
||||||
|
"meta",
|
||||||
|
"param",
|
||||||
|
"source",
|
||||||
|
"track",
|
||||||
|
"wbr",
|
||||||
|
]);
|
37
lib/str.mjs
Normal file
37
lib/str.mjs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { TextNode } from "./node.mjs";
|
||||||
|
import { isBool, isNil } from "./lang.mjs";
|
||||||
|
export class StrRenderer {
|
||||||
|
async render(node) {
|
||||||
|
return encodeNode(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function encodeAnyNode(node) {
|
||||||
|
return node instanceof TextNode ? encodeTextNode(node) : encodeNode(node);
|
||||||
|
}
|
||||||
|
function encodeTextNode(node) {
|
||||||
|
return node.text;
|
||||||
|
}
|
||||||
|
function encodeNode(node) {
|
||||||
|
return encodeHtml(node.tagName, node.attrs, node.children?.map(encodeAnyNode));
|
||||||
|
}
|
||||||
|
function encodeHtml(tagName, attrs, children) {
|
||||||
|
const open = `<${tagName} ${encodeAttrs(attrs)}>`;
|
||||||
|
if (isNil(children))
|
||||||
|
return open;
|
||||||
|
return `${open}${children.join("")}</${tagName}>`;
|
||||||
|
}
|
||||||
|
function encodeAttrs(attrs) {
|
||||||
|
if (!attrs)
|
||||||
|
return "";
|
||||||
|
return Object.entries(attrs)
|
||||||
|
.map(([key, value]) => encodeAttr(key, value))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
function encodeAttr(key, value) {
|
||||||
|
if (isNil(value))
|
||||||
|
return null;
|
||||||
|
if (isBool(value))
|
||||||
|
return value ? key : null;
|
||||||
|
return `${key}="${value}"`;
|
||||||
|
}
|
1
lib/types.mjs
Normal file
1
lib/types.mjs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
6
makefile
Normal file
6
makefile
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
ts-w:
|
||||||
|
npx tsc-watch
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf target
|
2821
package-lock.json
generated
Normal file
2821
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
13
package.json
Normal file
13
package.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
13
src/lang.mts
Normal file
13
src/lang.mts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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";
|
||||||
|
}
|
98
src/node.mts
Normal file
98
src/node.mts
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import { isNil, Nilable } from "./lang.mjs";
|
||||||
|
|
||||||
|
export type AnyNode = TextNode | Node;
|
||||||
|
|
||||||
|
export class TextNode {
|
||||||
|
#text: string;
|
||||||
|
|
||||||
|
constructor(text: string) {
|
||||||
|
this.#text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
get text(): string {
|
||||||
|
return this.#text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Node {
|
||||||
|
#tagName: string;
|
||||||
|
#attrs: Record<string, unknown>;
|
||||||
|
#children: Nilable<AnyNode[]>;
|
||||||
|
#isSelfClosed: boolean;
|
||||||
|
|
||||||
|
constructor(tagName: string) {
|
||||||
|
this.#tagName = tagName;
|
||||||
|
this.#attrs = {};
|
||||||
|
this.#children = undefined;
|
||||||
|
this.#isSelfClosed = selfClosedTagNames.has(tagName);
|
||||||
|
}
|
||||||
|
|
||||||
|
get tagName(): string {
|
||||||
|
return this.#tagName;
|
||||||
|
}
|
||||||
|
|
||||||
|
get attrs(): Record<string, unknown> {
|
||||||
|
return this.#attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
get children(): Nilable<AnyNode[]> {
|
||||||
|
return this.#children;
|
||||||
|
}
|
||||||
|
|
||||||
|
withAttrs(attrs: Record<string, unknown>): Node {
|
||||||
|
Object.entries(attrs).forEach(([key, value]) => this.addAttr(key, value));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
withAttr(name: string, value: unknown): Node {
|
||||||
|
this.addAttr(name, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addAttr(name: string, value: unknown): void {
|
||||||
|
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 {
|
||||||
|
if (this.#isSelfClosed)
|
||||||
|
throw new Error("You cannot add child to self closed element");
|
||||||
|
if (isNil(this.#children)) this.#children = [];
|
||||||
|
this.#children.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selfClosedTagNames = new Set([
|
||||||
|
"area",
|
||||||
|
"base",
|
||||||
|
"br",
|
||||||
|
"col",
|
||||||
|
"embed",
|
||||||
|
"hr",
|
||||||
|
"img",
|
||||||
|
"input",
|
||||||
|
"link",
|
||||||
|
"meta",
|
||||||
|
"param",
|
||||||
|
"source",
|
||||||
|
"track",
|
||||||
|
"wbr",
|
||||||
|
]);
|
50
src/str.mts
Normal file
50
src/str.mts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { Renderer } from "./types.mjs";
|
||||||
|
import { AnyNode, Node, TextNode } from "./node.mjs";
|
||||||
|
import { isBool, isNil, Nullable } from "./lang.mjs";
|
||||||
|
|
||||||
|
export class StrRenderer implements Renderer<string> {
|
||||||
|
async render(node: Node): Promise<string> {
|
||||||
|
return encodeNode(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeAnyNode(node: AnyNode): string {
|
||||||
|
return node instanceof TextNode ? encodeTextNode(node) : encodeNode(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeTextNode(node: TextNode): string {
|
||||||
|
return node.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeNode(node: Node): string {
|
||||||
|
return encodeHtml(
|
||||||
|
node.tagName,
|
||||||
|
node.attrs,
|
||||||
|
node.children?.map(encodeAnyNode)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeHtml(
|
||||||
|
tagName: string,
|
||||||
|
attrs?: Record<string, unknown>,
|
||||||
|
children?: string[]
|
||||||
|
): string {
|
||||||
|
const open = `<${tagName} ${encodeAttrs(attrs)}>`;
|
||||||
|
if (isNil(children)) return open;
|
||||||
|
return `${open}${children.join("")}</${tagName}>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeAttrs(attrs?: Record<string, unknown>): string {
|
||||||
|
if (!attrs) return "";
|
||||||
|
|
||||||
|
return Object.entries(attrs)
|
||||||
|
.map(([key, value]) => encodeAttr(key, value))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeAttr(key: string, value: unknown): Nullable<string> {
|
||||||
|
if (isNil(value)) return null;
|
||||||
|
if (isBool(value)) return value ? key : null;
|
||||||
|
return `${key}="${value}"`;
|
||||||
|
}
|
5
src/types.mts
Normal file
5
src/types.mts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { Node } from "./node.mjs";
|
||||||
|
|
||||||
|
export interface Renderer<T> {
|
||||||
|
render(node: Node): Promise<T>;
|
||||||
|
}
|
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "lib",
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["dom", "esNext"],
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"rootDir": "src",
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"suppressImplicitAnyIndexErrors": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"noImplicitAny": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
],
|
||||||
|
}
|
Reference in a new issue