This commit is contained in:
Dmitriy Pleshevskiy 2022-03-14 10:32:45 +03:00
commit 0978aa8754
15 changed files with 3206 additions and 0 deletions

28
.eslintrc.yml Normal file
View 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
View 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
View File

@ -0,0 +1,3 @@
{
"editor.tabSize": 2
}

6
lib/lang.mjs Normal file
View 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
View 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
View 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
View File

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

6
makefile Normal file
View File

@ -0,0 +1,6 @@
ts-w:
npx tsc-watch
clean:
rm -rf target

2821
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

13
package.json Normal file
View 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
View 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
View 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
View 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
View File

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

21
tsconfig.json Normal file
View 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/**/*"
],
}