diff --git a/package-lock.json b/package-lock.json index 9c95a98..c701161 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,25 @@ "requires": true, "packages": { "": { + "dependencies": { + "ren": "github:pleshevskiy/ren" + }, + "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" + } + }, + "../../sandbox/ren": { + "version": "0.0.1", + "extraneous": true, + "license": "MIT", "devDependencies": { "@types/node": "^17.0.21", "@typescript-eslint/eslint-plugin": "^5.14.0", @@ -1327,6 +1346,11 @@ "url": "https://github.com/sponsors/mysticatea" } }, + "node_modules/ren": { + "version": "0.0.1", + "resolved": "git+ssh://git@github.com/pleshevskiy/ren.git#9a783eba321b32fdbbb385fee3277fc08e80cb92", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2597,6 +2621,10 @@ "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true }, + "ren": { + "version": "git+ssh://git@github.com/pleshevskiy/ren.git#9a783eba321b32fdbbb385fee3277fc08e80cb92", + "from": "ren@git+ssh://git@github.com:pleshevskiy/ren.git" + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", diff --git a/package.json b/package.json index 1c54e83..9357e0e 100644 --- a/package.json +++ b/package.json @@ -9,5 +9,8 @@ "prettier": "^2.5.1", "tsc-watch": "^4.6.0", "typescript": "^4.6.2" + }, + "dependencies": { + "ren": "github:pleshevskiy/ren" } } diff --git a/src/components/layout.mts b/src/components/layout.mts index 2a26868..bdeb1bc 100644 --- a/src/components/layout.mts +++ b/src/components/layout.mts @@ -1,4 +1,4 @@ -import { AnyNode, Elem } from "../ren/nodes.mjs"; +import { AnyNode, Elem } from "ren"; export async function Layout(page: AnyNode): Promise { return new Elem("html") diff --git a/src/components/page_layout.mts b/src/components/page_layout.mts index cda9ad9..4dde09d 100644 --- a/src/components/page_layout.mts +++ b/src/components/page_layout.mts @@ -1,4 +1,4 @@ -import { AnyNode, Elem, Frag } from "../ren/nodes.mjs"; +import { AnyNode, Elem, Frag } from "ren"; export async function PageLayout(children: AnyNode[]): Promise { return new Frag() diff --git a/src/ren/nodes.mts b/src/ren/nodes.mts deleted file mode 100644 index d576063..0000000 --- a/src/ren/nodes.mts +++ /dev/null @@ -1,120 +0,0 @@ -import { isNil, Nilable } from "../lang.mjs"; - -export type AnyNode = AnySyncNode | AnyAsyncNode; -export type AnyAsyncNode = Promise; -export type AnySyncNode = TextNode | Elem | Frag; - -export class TextNode extends String {} - -export function F(children: AnyNode[]): Frag { - return new Frag().withChildren(children); -} - -export class Frag { - #children: Nilable; - - constructor() { - this.#children = undefined; - } - - get children(): Nilable { - return this.#children; - } - - withText(text: string): this { - this.addText(text); - return this; - } - - addText(text: string): void { - this.addChild(new TextNode(text)); - } - - maybeWithChildren(nodes?: Nilable): 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 function E( - tagName: string, - attrs: ElemAttrs, - children?: Nilable -): Elem { - return new Elem(tagName).withAttrs(attrs).maybeWithChildren(children); -} - -export type ElemAttrs = Record; - -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 { - return this.#attrs; - } - - withAttrs(attrs: Record): Elem { - Object.entries(attrs).forEach(([key, value]) => this.addAttr(key, value)); - return this; - } - - withAttr(name: string, value: unknown): Elem { - this.addAttr(name, value); - return this; - } - - addAttr(name: string, value: unknown): void { - this.#attrs[name] = value; - } - - addChild(node: AnySyncNode): 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", -]); diff --git a/src/ren/str.mts b/src/ren/str.mts deleted file mode 100644 index d02447c..0000000 --- a/src/ren/str.mts +++ /dev/null @@ -1,58 +0,0 @@ -import { Renderer } from "./types.mjs"; -import { isBool, isNil, Nullable } from "../lang.mjs"; -import { AnyNode, Elem, Frag, TextNode } from "./nodes.mjs"; - -export class StrRenderer implements Renderer { - async render(node: Elem | Promise): Promise { - return encodeNode(await node); - } -} - -async function encodeAnyNode(node: AnyNode): Promise { - const syncNode = await node; - return syncNode instanceof TextNode - ? encodeTextNode(syncNode) - : encodeNode(syncNode); -} - -function encodeTextNode(node: TextNode): string { - return String(node); -} - -async function encodeNode(node: Elem | Frag): Promise { - const encodedChildren = isNil(node.children) - ? undefined - : await Promise.all(node.children.map(encodeAnyNode)); - return node instanceof Elem - ? encodeHtmlElement(node.tagName, node.attrs, encodedChildren) - : encodeHtmlFragment(encodedChildren); -} - -function encodeHtmlFragment(children?: string[]): string { - return children?.join("") ?? ""; -} - -function encodeHtmlElement( - tagName: string, - attrs?: Record, - children?: string[] -): string { - const open = `<${tagName} ${encodeAttrs(attrs)}>`; - if (isNil(children)) return open; - return `${open}${children.join("")}`; -} - -function encodeAttrs(attrs?: Record): Nullable { - if (!attrs) return ""; - - return Object.entries(attrs) - .map(([key, value]) => encodeAttr(key, value)) - .filter(Boolean) - .join(" "); -} - -function encodeAttr(key: string, value: unknown): Nullable { - if (isNil(value)) return null; - if (isBool(value)) return value ? key : null; - return `${key}="${value}"`; -} diff --git a/src/ren/types.mts b/src/ren/types.mts deleted file mode 100644 index 3e04a75..0000000 --- a/src/ren/types.mts +++ /dev/null @@ -1,5 +0,0 @@ -import { Elem } from "./nodes.mjs"; - -export interface Renderer { - render(node: Elem | Promise): Promise; -} diff --git a/src/server.mts b/src/server.mts index 72b5484..a2d99e3 100644 --- a/src/server.mts +++ b/src/server.mts @@ -2,7 +2,7 @@ import * as http from "http"; import { Layout } from "./components/layout.mjs"; import { ServerConfig } from "./config.mjs"; import { debug, info } from "./log.mjs"; -import { StrRenderer } from "./ren/str.mjs"; +import { StrRenderer } from "ren"; import { AboutPage } from "./views/about.mjs"; import { E404 } from "./views/e404.mjs"; import { WorksPage } from "./views/works.mjs"; diff --git a/src/views/about.mts b/src/views/about.mts index 4653e56..0f6cfa2 100644 --- a/src/views/about.mts +++ b/src/views/about.mts @@ -1,5 +1,5 @@ import { PageLayout } from "../components/page_layout.mjs"; -import { AnyAsyncNode, Elem } from "../ren/nodes.mjs"; +import { AnyAsyncNode, Elem } from "ren"; export async function AboutPage(): AnyAsyncNode { return PageLayout([new Elem("p").withText("Привет мир")]); diff --git a/src/views/e404.mts b/src/views/e404.mts index 31cd664..54208e3 100644 --- a/src/views/e404.mts +++ b/src/views/e404.mts @@ -1,5 +1,5 @@ import { PageLayout } from "../components/page_layout.mjs"; -import { AnyAsyncNode, Elem } from "../ren/nodes.mjs"; +import { AnyAsyncNode, Elem } from "ren"; export async function E404(): AnyAsyncNode { return PageLayout([new Elem("p").withText("Page not found")]); diff --git a/src/views/works.mts b/src/views/works.mts index 7c75229..b799284 100644 --- a/src/views/works.mts +++ b/src/views/works.mts @@ -1,5 +1,5 @@ import { PageLayout } from "../components/page_layout.mjs"; -import { AnyAsyncNode, Elem } from "../ren/nodes.mjs"; +import { AnyAsyncNode, Elem } from "ren"; export async function WorksPage(): AnyAsyncNode { return PageLayout([new Elem("p").withText("Works")]);