From 4dd7f8ccd9921d60a7ddb9e4b9f7166ddbdf09ea Mon Sep 17 00:00:00 2001 From: Sander Hahn Date: Sun, 12 Jan 2025 16:18:20 +0100 Subject: [PATCH 1/9] chore: add TS compilation into dist in the CI workflow --- .github/workflows/actions.yaml | 6 ++++++ .gitignore | 2 ++ deno.json | 9 ++++++--- deno.lock | 12 ++++++++++-- tsconfig.json | 27 +++++++++++++++++++++++++++ 5 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 tsconfig.json diff --git a/.github/workflows/actions.yaml b/.github/workflows/actions.yaml index 69047a5..dd04d53 100644 --- a/.github/workflows/actions.yaml +++ b/.github/workflows/actions.yaml @@ -46,5 +46,11 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - run: deno task tsc + - name: Publish package run: npx jsr publish diff --git a/.gitignore b/.gitignore index f6f5c18..554bea6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /.vscode /coverage /docs +/node_modules +/dist diff --git a/deno.json b/deno.json index 483f83b..a370d02 100644 --- a/deno.json +++ b/deno.json @@ -9,12 +9,15 @@ "doc": "deno doc --html html.ts", "coverage": "rm -Rf ./coverage && deno test --coverage && deno coverage ./coverage", "coverage:html": "rm -Rf ./coverage && deno test --coverage && deno coverage ./coverage --html", - "embed:example": "deno run -A embed_example.ts" + "embed:example": "deno run -A embed_example.ts", + "tsc": "deno run -A npm:typescript/tsc --project tsconfig.json && deno fmt dist" }, "authors": [ "Sander Hahn " ], "imports": { - "@std/assert": "jsr:@std/assert@^1.0.10" - } + "@std/assert": "jsr:@std/assert@^1.0.10", + "typescript": "npm:typescript@^5.7.3" + }, + "nodeModulesDir": "auto" } diff --git a/deno.lock b/deno.lock index f2401c9..a14a83d 100644 --- a/deno.lock +++ b/deno.lock @@ -3,7 +3,9 @@ "specifiers": { "jsr:@std/assert@*": "1.0.10", "jsr:@std/assert@^1.0.10": "1.0.10", - "jsr:@std/internal@^1.0.5": "1.0.5" + "jsr:@std/internal@^1.0.5": "1.0.5", + "npm:typescript@*": "5.7.3", + "npm:typescript@^5.7.3": "5.7.3" }, "jsr": { "@std/assert@1.0.10": { @@ -16,9 +18,15 @@ "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" } }, + "npm": { + "typescript@5.7.3": { + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==" + } + }, "workspace": { "dependencies": [ - "jsr:@std/assert@^1.0.10" + "jsr:@std/assert@^1.0.10", + "npm:typescript@^5.7.3" ] } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6ddfa63 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "strict": true, + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "target": "ES2021", + "lib": [ + "ES2021", + "ES2016.Array.Include", + "DOM", + ], + "module": "ESNext", + "outDir": "./dist", + "allowSyntheticDefaultImports": true, + }, + "include": [ + "**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "mod.ts", + "**/*_test.ts", + "**/*example.ts", + ], +} From c9b6ba1834b63ae9ad18e3fc2dcf10761b30b77a Mon Sep 17 00:00:00 2001 From: Sander Hahn Date: Sun, 12 Jan 2025 16:19:52 +0100 Subject: [PATCH 2/9] chore: fix formatting in tsconfig.json --- tsconfig.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 6ddfa63..353694d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,11 +8,11 @@ "lib": [ "ES2021", "ES2016.Array.Include", - "DOM", + "DOM" ], "module": "ESNext", "outDir": "./dist", - "allowSyntheticDefaultImports": true, + "allowSyntheticDefaultImports": true }, "include": [ "**/*.ts" @@ -22,6 +22,6 @@ "dist", "mod.ts", "**/*_test.ts", - "**/*example.ts", - ], + "**/*example.ts" + ] } From 9dca980e194a025f0f9433844380a19d65fc9ce9 Mon Sep 17 00:00:00 2001 From: Sander Hahn Date: Sun, 12 Jan 2025 16:26:55 +0100 Subject: [PATCH 3/9] chore: bump version to 0.1.7 in deno.json --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index a370d02..0ccf2eb 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@sander/html", - "version": "0.1.6", + "version": "0.1.7", "license": "MIT", "exports": "./mod.ts", "tasks": { From e67befee7728a55cf01c87540ecdd79be71c3583 Mon Sep 17 00:00:00 2001 From: Sander Hahn Date: Sun, 12 Jan 2025 16:48:41 +0100 Subject: [PATCH 4/9] chore: add dist output to the repo --- .gitignore | 1 - dist/html.d.ts | 183 +++++++++++++++++++++++++++ dist/html.d.ts.map | 1 + dist/html.js | 300 +++++++++++++++++++++++++++++++++++++++++++++ dist/html.js.map | 1 + 5 files changed, 485 insertions(+), 1 deletion(-) create mode 100644 dist/html.d.ts create mode 100644 dist/html.d.ts.map create mode 100644 dist/html.js create mode 100644 dist/html.js.map diff --git a/.gitignore b/.gitignore index 554bea6..7d520c2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,3 @@ /coverage /docs /node_modules -/dist diff --git a/dist/html.d.ts b/dist/html.d.ts new file mode 100644 index 0000000..081ae09 --- /dev/null +++ b/dist/html.d.ts @@ -0,0 +1,183 @@ +/** + * This module provides a simple way to generate html source code. + */ +/** Html represent a tree of html tags */ +export type HtmlNode = + | HtmlTag + | HtmlNode[] + | string + | null + | undefined + | ToHtml + | RawHtml; +type Attrs = { + [attr: string]: AttrValue; +}; +type AttrValue = string | number | boolean | null | undefined; +type HtmlTag = { + tag: string; + attrs?: Attrs; + children: HtmlNode; +}; +/** + * Escape text into html source + * @param text - the text to escape + * @returns the escaped text + */ +export declare function escapeHtml(text: string): string; +/** + * Create an html tag + * @param tag - the tag name + * @param children - the children + * @returns the html tag + */ +export declare function tag(tag: string, children?: HtmlNode): HtmlTag; +/** + * Create an html tag + * @param tag - the tag name + * @param attrs - the attributes + * @param children - the children + * @returns the html tag + */ +export declare function tag( + tag: string, + attrs?: Attrs, + ...children: HtmlNode[] +): HtmlTag; +declare class RawHtml { + html: string; + constructor(html: string); +} +/** + * Create a raw html node + * @param html raw html to insert + * @returns + */ +export declare function raw(html: string): HtmlNode; +export declare function comment(text: string): HtmlNode; +/** Options for the `html` function */ +interface HtmlOptions { + /** indentation level (defaults to 0) */ + indentLevel?: number; + /** raw text indicator (defaults to false) */ + rawText?: boolean; + /** text used for indent (defaults to " ") */ + indentText?: string; + /** insert new line at the end of each html item (defaults to true) */ + insertNewLines?: boolean; + /** doctype declaration */ + doctype?: string; +} +/** + * Convert a html tree into an html source + * + * Features: + * - void elements are not closed + * - boolean attributes are ommited if false + * - raw text elements are not escaped + * - empty nodes are ignored + * + * # Example + * + * filename: `example.ts` + * ```ts + * import { html, type HtmlNode, tag } from "@sander/html"; + * import { assertEquals } from "@std/assert"; + * + * const title = "Cool Projects"; + * + * interface Project { + * title: string; + * url: string; + * } + * + * const projects: Project[] = [{ + * title: "Deno", + * url: "https://deno.com/", + * }, { + * title: "TypeScript", + * url: "https://www.typescriptlang.org/", + * }]; + * + * const cssRules = [ + * "* { --ts-blue: #3178c6; }", + * "body { font-family: sans-serif; line-height: 1.6; }", + * "li > a { color: var(--ts-blue); text-decoration: none; }", + * ]; + * + * function list(items: HtmlNode[]) { + * return tag("ul", items.map((item) => tag("li", item))); + * } + * + * function link({ title, url }: Project) { + * return tag("a", { href: url }, title); + * } + * + * const result = html( + * tag("html", { lang: "en" }, [ + * tag("head", [ + * tag("meta", { charset: "utf-8" }), + * tag("title", title), + * tag("style", cssRules), + * ]), + * tag("body", [ + * tag("h1", title), + * list(projects.map(link)), + * ]), + * ]), + * { + * doctype: "html", + * }, + * ); + * + * assertEquals( + * result, + * `\ + * + * + * + * + * + * Cool Projects + * + * + * + * + *

+ * Cool Projects + *

+ * + * + * + * `, + * ); + * ``` + * + * @param content - the html tree + * @param options - the options for the html source + * @returns the html source + */ +export declare function html(content: HtmlNode, options?: HtmlOptions): string; +/** + * Interface for objects that can be converted to html + */ +export interface ToHtml { + toHtml(): HtmlNode; +} +export {}; +//# sourceMappingURL=html.d.ts.map diff --git a/dist/html.d.ts.map b/dist/html.d.ts.map new file mode 100644 index 0000000..ea0db88 --- /dev/null +++ b/dist/html.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../html.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,yCAAyC;AACzC,MAAM,MAAM,QAAQ,GAChB,OAAO,GACP,QAAQ,EAAE,GACV,MAAM,GACN,IAAI,GACJ,SAAS,GACT,MAAM,GACN,OAAO,CAAC;AACZ,KAAK,KAAK,GAAG;IAAE,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAC3C,KAAK,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC;AAC9D,KAAK,OAAO,GAAG;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,QAAQ,EAAE,QAAQ,CAAC;CACpB,CAAC;AAkBF;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAe/C;AAqED;;;;;GAKG;AACH,wBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;AAC/D;;;;;;GAMG;AACH,wBAAgB,GAAG,CACjB,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,KAAK,EACb,GAAG,QAAQ,EAAE,QAAQ,EAAE,GACtB,OAAO,CAAC;AAmBX,cAAM,OAAO;IACQ,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;CAChC;AAED;;;;GAIG;AACH,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAE1C;AAWD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAK9C;AAED,sCAAsC;AACtC,UAAU,WAAW;IACnB,wCAAwC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,8CAA8C;IAC9C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,0BAA0B;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAiED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuGG;AACH,wBAAgB,IAAI,CAClB,OAAO,EAAE,QAAQ,EACjB,OAAO,CAAC,EAAE,WAAW,GACpB,MAAM,CAGR;AAED;;GAEG;AACH,MAAM,WAAW,MAAM;IACrB,MAAM,IAAI,QAAQ,CAAC;CACpB"} \ No newline at end of file diff --git a/dist/html.js b/dist/html.js new file mode 100644 index 0000000..9ed3b11 --- /dev/null +++ b/dist/html.js @@ -0,0 +1,300 @@ +/** + * This module provides a simple way to generate html source code. + */ +const voidElements = + "area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr" + .split(", "); +const rawTextElements = "script, style, title, textarea".split(", "); +function isHtmlNode(value) { + return (value instanceof Tag || + value instanceof RawHtml || + Array.isArray(value) || + typeof value === "string" || + value === null || + value === undefined); +} +/** + * Escape text into html source + * @param text - the text to escape + * @returns the escaped text + */ +export function escapeHtml(text) { + return text.replaceAll(/[<>&"]/g, (c) => { + switch (c) { + case "<": + return "<"; + case ">": + return ">"; + case "&": + return "&"; + case '"': + return """; + default: + throw new Error("unreachable"); + } + }); +} +function attr([key, value]) { + if (value === false || value === null || value === undefined) { + return []; + } + if (value === true) { + return [" ", key]; + } + return [" ", key, '="', escapeHtml(`${value}`), '"']; +} +function emptyChildren(children) { + return children === undefined || + children === null || + children === "" || + (Array.isArray(children) && children.length === 0); +} +class Tag { + constructor(tag, attrs = {}, children = []) { + this.tag = tag; + this.attrs = attrs; + this.children = children; + } +} +/* + * XML tag names are case-sensitive, while HTML tag names are case-insensitive. + * In XML, colons (:) are allowed for namespaces, but they are rarely used in HTML. + */ +function validTagName(name) { + return /^[a-zA-Z_:][a-zA-Z0-9:_.-]*$/i.test(name); +} +function validAttrName(name) { + return /^[a-zA-Z_:][a-zA-Z0-9:_.-]*$/i.test(name); +} +// Only allow primitive values for attributes +// Ignore the structural type `toString` and `valueOf` interfaces +function parseAttrValue(value) { + if ( + typeof value === "string" || + typeof value === "number" || + typeof value === "boolean" || + value === null || + value === undefined + ) { + return value; + } + throw new Error(`Invalid attribute value: ${value}`); +} +function parseAttrs(attrs) { + if (isHtmlNode(attrs)) { + throw new Error("unreachable"); + } + const parsed = {}; + for (const [key, value] of Object.entries(attrs)) { + if (!validAttrName(key)) { + throw new Error(`Invalid attribute name: ${key}`); + } + parsed[key] = parseAttrValue(value); + } + return parsed; +} +export function tag(tag, attrs, ...children) { + if (!validTagName(tag)) { + throw new Error(`Invalid tag name: ${tag}`); + } + if (children.length > 0) { + return new Tag(tag, parseAttrs(attrs), children); + } + if (isHtmlNode(attrs)) { + const children = attrs; + return new Tag(tag, {}, children); + } + return new Tag(tag, parseAttrs(attrs), []); +} +class RawHtml { + constructor(html) { + this.html = html; + } +} +/** + * Create a raw html node + * @param html raw html to insert + * @returns + */ +export function raw(html) { + return new RawHtml(html); +} +// https://html.spec.whatwg.org/multipage/syntax.html#comments +function isValidComment(text) { + return !text.startsWith(">") && + !text.startsWith("->") && + !text.includes("") && + !text.includes("--!>"); +} +export function comment(text) { + if (!isValidComment(text)) { + throw new Error(`Invalid comment text: ${text}`); + } + return raw(``); +} +/** + * InnerHtml builds an array of strings that can be joined more efficiently + * than direct string concatenation. + */ +function innerHtml(content, options) { + const indentLevel = options?.indentLevel ?? 0; + const rawText = options?.rawText ?? false; + const indentText = options?.indentText ?? " "; + const insertNewLines = options?.insertNewLines ?? true; + const nl = insertNewLines ? "\n" : ""; + const indent = indentText.repeat(indentLevel); + if (content === undefined || content === null) { + return []; + } + if (typeof content === "string") { + if (rawText || rawTextElements.includes(content)) { + return [indent, content, nl]; + } + return [indent, escapeHtml(content), nl]; + } + if (Array.isArray(content)) { + return content.map((content) => innerHtml(content, options)).flat(); + } + if (content instanceof RawHtml) { + return [indent, content.html, nl]; + } + if (content instanceof Tag) { + const { tag, attrs, children } = content; + const quotedAttrs = Object.entries(attrs) + .flatMap(([key, value]) => attr([key, value])); + const prefix = [indent, "<", tag, ...quotedAttrs]; + const endTag = [""]; + if (emptyChildren(children)) { + if (voidElements.includes(tag)) { + return [...prefix, ">", nl]; + } + return [...prefix, ">", ...endTag, nl]; + } + const childrenStr = innerHtml(children, { + indentLevel: indentLevel + 1, + rawText: rawTextElements.includes(tag), + indentText, + insertNewLines, + }); + return [...prefix, ">", nl, ...childrenStr, indent, ...endTag, nl]; + } + if (isToHtml(content)) { + return innerHtml(content.toHtml(), options); + } + throw new Error("Cannot convert object to HTML"); +} +/** + * Convert a html tree into an html source + * + * Features: + * - void elements are not closed + * - boolean attributes are ommited if false + * - raw text elements are not escaped + * - empty nodes are ignored + * + * # Example + * + * filename: `example.ts` + * ```ts + * import { html, type HtmlNode, tag } from "@sander/html"; + * import { assertEquals } from "@std/assert"; + * + * const title = "Cool Projects"; + * + * interface Project { + * title: string; + * url: string; + * } + * + * const projects: Project[] = [{ + * title: "Deno", + * url: "https://deno.com/", + * }, { + * title: "TypeScript", + * url: "https://www.typescriptlang.org/", + * }]; + * + * const cssRules = [ + * "* { --ts-blue: #3178c6; }", + * "body { font-family: sans-serif; line-height: 1.6; }", + * "li > a { color: var(--ts-blue); text-decoration: none; }", + * ]; + * + * function list(items: HtmlNode[]) { + * return tag("ul", items.map((item) => tag("li", item))); + * } + * + * function link({ title, url }: Project) { + * return tag("a", { href: url }, title); + * } + * + * const result = html( + * tag("html", { lang: "en" }, [ + * tag("head", [ + * tag("meta", { charset: "utf-8" }), + * tag("title", title), + * tag("style", cssRules), + * ]), + * tag("body", [ + * tag("h1", title), + * list(projects.map(link)), + * ]), + * ]), + * { + * doctype: "html", + * }, + * ); + * + * assertEquals( + * result, + * `\ + * + * + * + * + * + * Cool Projects + * + * + * + * + *

+ * Cool Projects + *

+ * + * + * + * `, + * ); + * ``` + * + * @param content - the html tree + * @param options - the options for the html source + * @returns the html source + */ +export function html(content, options) { + const firstLine = options?.doctype ? `\n` : ""; + return firstLine + innerHtml(content, options).join(""); +} +function isToHtml(value) { + return value !== undefined && + typeof value === "object" && + typeof value.toHtml === "function"; +} +//# sourceMappingURL=html.js.map diff --git a/dist/html.js.map b/dist/html.js.map new file mode 100644 index 0000000..e25edcf --- /dev/null +++ b/dist/html.js.map @@ -0,0 +1 @@ +{"version":3,"file":"html.js","sourceRoot":"","sources":["../html.ts"],"names":[],"mappings":"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"} \ No newline at end of file From 4a68eb846b3982e0b8af7c597afee871de673fc0 Mon Sep 17 00:00:00 2001 From: Sander Hahn Date: Sun, 12 Jan 2025 17:30:33 +0100 Subject: [PATCH 5/9] chore: add pre-commit hook for formatting, linting, and testing --- .hooks/pre-commit | 6 ++++++ deno.json | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100755 .hooks/pre-commit diff --git a/.hooks/pre-commit b/.hooks/pre-commit new file mode 100755 index 0000000..b5576b8 --- /dev/null +++ b/.hooks/pre-commit @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/hook.sh" + +deno fmt --check +deno lint +deno test --allow-all --doc diff --git a/deno.json b/deno.json index 0ccf2eb..44984a8 100644 --- a/deno.json +++ b/deno.json @@ -10,7 +10,8 @@ "coverage": "rm -Rf ./coverage && deno test --coverage && deno coverage ./coverage", "coverage:html": "rm -Rf ./coverage && deno test --coverage && deno coverage ./coverage --html", "embed:example": "deno run -A embed_example.ts", - "tsc": "deno run -A npm:typescript/tsc --project tsconfig.json && deno fmt dist" + "tsc": "deno run -A npm:typescript/tsc --project tsconfig.json && deno fmt dist", + "hook": "deno run --allow-read --allow-run --allow-write https://deno.land/x/deno_hooks@0.1.1/mod.ts" }, "authors": [ "Sander Hahn " From bb5f464fd8f01cb6a1bdb286aea39b221faeac31 Mon Sep 17 00:00:00 2001 From: Sander Hahn Date: Thu, 16 Jan 2025 21:16:54 +0100 Subject: [PATCH 6/9] chore: bump version to 0.1.8 and add package.json for module exports --- deno.json | 2 +- package.json | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 package.json diff --git a/deno.json b/deno.json index 44984a8..4eacb50 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@sander/html", - "version": "0.1.7", + "version": "0.1.8", "license": "MIT", "exports": "./mod.ts", "tasks": { diff --git a/package.json b/package.json new file mode 100644 index 0000000..b9accf1 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "@sander/html", + "type": "module", + "main": "./dist/html.js", + "exports": { + ".": { + "import": "./dist/html.js", + "require": "./dist/html.js", + "types": "./dist/html.d.ts" + } + } +} From c91f141fb58d34af0cb8232acd7f769f8f635f29 Mon Sep 17 00:00:00 2001 From: Sander Hahn Date: Thu, 16 Jan 2025 21:20:41 +0100 Subject: [PATCH 7/9] chore: bump version to 0.1.9 and update .gitignore to exclude dist directory --- .gitignore | 1 + deno.json | 2 +- dist/html.d.ts | 183 --------------------------- dist/html.d.ts.map | 1 - dist/html.js | 300 --------------------------------------------- dist/html.js.map | 1 - 6 files changed, 2 insertions(+), 486 deletions(-) delete mode 100644 dist/html.d.ts delete mode 100644 dist/html.d.ts.map delete mode 100644 dist/html.js delete mode 100644 dist/html.js.map diff --git a/.gitignore b/.gitignore index 7d520c2..554bea6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /coverage /docs /node_modules +/dist diff --git a/deno.json b/deno.json index 4eacb50..8522b8e 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@sander/html", - "version": "0.1.8", + "version": "0.1.9", "license": "MIT", "exports": "./mod.ts", "tasks": { diff --git a/dist/html.d.ts b/dist/html.d.ts deleted file mode 100644 index 081ae09..0000000 --- a/dist/html.d.ts +++ /dev/null @@ -1,183 +0,0 @@ -/** - * This module provides a simple way to generate html source code. - */ -/** Html represent a tree of html tags */ -export type HtmlNode = - | HtmlTag - | HtmlNode[] - | string - | null - | undefined - | ToHtml - | RawHtml; -type Attrs = { - [attr: string]: AttrValue; -}; -type AttrValue = string | number | boolean | null | undefined; -type HtmlTag = { - tag: string; - attrs?: Attrs; - children: HtmlNode; -}; -/** - * Escape text into html source - * @param text - the text to escape - * @returns the escaped text - */ -export declare function escapeHtml(text: string): string; -/** - * Create an html tag - * @param tag - the tag name - * @param children - the children - * @returns the html tag - */ -export declare function tag(tag: string, children?: HtmlNode): HtmlTag; -/** - * Create an html tag - * @param tag - the tag name - * @param attrs - the attributes - * @param children - the children - * @returns the html tag - */ -export declare function tag( - tag: string, - attrs?: Attrs, - ...children: HtmlNode[] -): HtmlTag; -declare class RawHtml { - html: string; - constructor(html: string); -} -/** - * Create a raw html node - * @param html raw html to insert - * @returns - */ -export declare function raw(html: string): HtmlNode; -export declare function comment(text: string): HtmlNode; -/** Options for the `html` function */ -interface HtmlOptions { - /** indentation level (defaults to 0) */ - indentLevel?: number; - /** raw text indicator (defaults to false) */ - rawText?: boolean; - /** text used for indent (defaults to " ") */ - indentText?: string; - /** insert new line at the end of each html item (defaults to true) */ - insertNewLines?: boolean; - /** doctype declaration */ - doctype?: string; -} -/** - * Convert a html tree into an html source - * - * Features: - * - void elements are not closed - * - boolean attributes are ommited if false - * - raw text elements are not escaped - * - empty nodes are ignored - * - * # Example - * - * filename: `example.ts` - * ```ts - * import { html, type HtmlNode, tag } from "@sander/html"; - * import { assertEquals } from "@std/assert"; - * - * const title = "Cool Projects"; - * - * interface Project { - * title: string; - * url: string; - * } - * - * const projects: Project[] = [{ - * title: "Deno", - * url: "https://deno.com/", - * }, { - * title: "TypeScript", - * url: "https://www.typescriptlang.org/", - * }]; - * - * const cssRules = [ - * "* { --ts-blue: #3178c6; }", - * "body { font-family: sans-serif; line-height: 1.6; }", - * "li > a { color: var(--ts-blue); text-decoration: none; }", - * ]; - * - * function list(items: HtmlNode[]) { - * return tag("ul", items.map((item) => tag("li", item))); - * } - * - * function link({ title, url }: Project) { - * return tag("a", { href: url }, title); - * } - * - * const result = html( - * tag("html", { lang: "en" }, [ - * tag("head", [ - * tag("meta", { charset: "utf-8" }), - * tag("title", title), - * tag("style", cssRules), - * ]), - * tag("body", [ - * tag("h1", title), - * list(projects.map(link)), - * ]), - * ]), - * { - * doctype: "html", - * }, - * ); - * - * assertEquals( - * result, - * `\ - * - * - * - * - * - * Cool Projects - * - * - * - * - *

- * Cool Projects - *

- * - * - * - * `, - * ); - * ``` - * - * @param content - the html tree - * @param options - the options for the html source - * @returns the html source - */ -export declare function html(content: HtmlNode, options?: HtmlOptions): string; -/** - * Interface for objects that can be converted to html - */ -export interface ToHtml { - toHtml(): HtmlNode; -} -export {}; -//# sourceMappingURL=html.d.ts.map diff --git a/dist/html.d.ts.map b/dist/html.d.ts.map deleted file mode 100644 index ea0db88..0000000 --- a/dist/html.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../html.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,yCAAyC;AACzC,MAAM,MAAM,QAAQ,GAChB,OAAO,GACP,QAAQ,EAAE,GACV,MAAM,GACN,IAAI,GACJ,SAAS,GACT,MAAM,GACN,OAAO,CAAC;AACZ,KAAK,KAAK,GAAG;IAAE,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAC3C,KAAK,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC;AAC9D,KAAK,OAAO,GAAG;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,QAAQ,EAAE,QAAQ,CAAC;CACpB,CAAC;AAkBF;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAe/C;AAqED;;;;;GAKG;AACH,wBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;AAC/D;;;;;;GAMG;AACH,wBAAgB,GAAG,CACjB,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,KAAK,EACb,GAAG,QAAQ,EAAE,QAAQ,EAAE,GACtB,OAAO,CAAC;AAmBX,cAAM,OAAO;IACQ,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;CAChC;AAED;;;;GAIG;AACH,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAE1C;AAWD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAK9C;AAED,sCAAsC;AACtC,UAAU,WAAW;IACnB,wCAAwC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,8CAA8C;IAC9C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,0BAA0B;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAiED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuGG;AACH,wBAAgB,IAAI,CAClB,OAAO,EAAE,QAAQ,EACjB,OAAO,CAAC,EAAE,WAAW,GACpB,MAAM,CAGR;AAED;;GAEG;AACH,MAAM,WAAW,MAAM;IACrB,MAAM,IAAI,QAAQ,CAAC;CACpB"} \ No newline at end of file diff --git a/dist/html.js b/dist/html.js deleted file mode 100644 index 9ed3b11..0000000 --- a/dist/html.js +++ /dev/null @@ -1,300 +0,0 @@ -/** - * This module provides a simple way to generate html source code. - */ -const voidElements = - "area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr" - .split(", "); -const rawTextElements = "script, style, title, textarea".split(", "); -function isHtmlNode(value) { - return (value instanceof Tag || - value instanceof RawHtml || - Array.isArray(value) || - typeof value === "string" || - value === null || - value === undefined); -} -/** - * Escape text into html source - * @param text - the text to escape - * @returns the escaped text - */ -export function escapeHtml(text) { - return text.replaceAll(/[<>&"]/g, (c) => { - switch (c) { - case "<": - return "<"; - case ">": - return ">"; - case "&": - return "&"; - case '"': - return """; - default: - throw new Error("unreachable"); - } - }); -} -function attr([key, value]) { - if (value === false || value === null || value === undefined) { - return []; - } - if (value === true) { - return [" ", key]; - } - return [" ", key, '="', escapeHtml(`${value}`), '"']; -} -function emptyChildren(children) { - return children === undefined || - children === null || - children === "" || - (Array.isArray(children) && children.length === 0); -} -class Tag { - constructor(tag, attrs = {}, children = []) { - this.tag = tag; - this.attrs = attrs; - this.children = children; - } -} -/* - * XML tag names are case-sensitive, while HTML tag names are case-insensitive. - * In XML, colons (:) are allowed for namespaces, but they are rarely used in HTML. - */ -function validTagName(name) { - return /^[a-zA-Z_:][a-zA-Z0-9:_.-]*$/i.test(name); -} -function validAttrName(name) { - return /^[a-zA-Z_:][a-zA-Z0-9:_.-]*$/i.test(name); -} -// Only allow primitive values for attributes -// Ignore the structural type `toString` and `valueOf` interfaces -function parseAttrValue(value) { - if ( - typeof value === "string" || - typeof value === "number" || - typeof value === "boolean" || - value === null || - value === undefined - ) { - return value; - } - throw new Error(`Invalid attribute value: ${value}`); -} -function parseAttrs(attrs) { - if (isHtmlNode(attrs)) { - throw new Error("unreachable"); - } - const parsed = {}; - for (const [key, value] of Object.entries(attrs)) { - if (!validAttrName(key)) { - throw new Error(`Invalid attribute name: ${key}`); - } - parsed[key] = parseAttrValue(value); - } - return parsed; -} -export function tag(tag, attrs, ...children) { - if (!validTagName(tag)) { - throw new Error(`Invalid tag name: ${tag}`); - } - if (children.length > 0) { - return new Tag(tag, parseAttrs(attrs), children); - } - if (isHtmlNode(attrs)) { - const children = attrs; - return new Tag(tag, {}, children); - } - return new Tag(tag, parseAttrs(attrs), []); -} -class RawHtml { - constructor(html) { - this.html = html; - } -} -/** - * Create a raw html node - * @param html raw html to insert - * @returns - */ -export function raw(html) { - return new RawHtml(html); -} -// https://html.spec.whatwg.org/multipage/syntax.html#comments -function isValidComment(text) { - return !text.startsWith(">") && - !text.startsWith("->") && - !text.includes("") && - !text.includes("--!>"); -} -export function comment(text) { - if (!isValidComment(text)) { - throw new Error(`Invalid comment text: ${text}`); - } - return raw(``); -} -/** - * InnerHtml builds an array of strings that can be joined more efficiently - * than direct string concatenation. - */ -function innerHtml(content, options) { - const indentLevel = options?.indentLevel ?? 0; - const rawText = options?.rawText ?? false; - const indentText = options?.indentText ?? " "; - const insertNewLines = options?.insertNewLines ?? true; - const nl = insertNewLines ? "\n" : ""; - const indent = indentText.repeat(indentLevel); - if (content === undefined || content === null) { - return []; - } - if (typeof content === "string") { - if (rawText || rawTextElements.includes(content)) { - return [indent, content, nl]; - } - return [indent, escapeHtml(content), nl]; - } - if (Array.isArray(content)) { - return content.map((content) => innerHtml(content, options)).flat(); - } - if (content instanceof RawHtml) { - return [indent, content.html, nl]; - } - if (content instanceof Tag) { - const { tag, attrs, children } = content; - const quotedAttrs = Object.entries(attrs) - .flatMap(([key, value]) => attr([key, value])); - const prefix = [indent, "<", tag, ...quotedAttrs]; - const endTag = [""]; - if (emptyChildren(children)) { - if (voidElements.includes(tag)) { - return [...prefix, ">", nl]; - } - return [...prefix, ">", ...endTag, nl]; - } - const childrenStr = innerHtml(children, { - indentLevel: indentLevel + 1, - rawText: rawTextElements.includes(tag), - indentText, - insertNewLines, - }); - return [...prefix, ">", nl, ...childrenStr, indent, ...endTag, nl]; - } - if (isToHtml(content)) { - return innerHtml(content.toHtml(), options); - } - throw new Error("Cannot convert object to HTML"); -} -/** - * Convert a html tree into an html source - * - * Features: - * - void elements are not closed - * - boolean attributes are ommited if false - * - raw text elements are not escaped - * - empty nodes are ignored - * - * # Example - * - * filename: `example.ts` - * ```ts - * import { html, type HtmlNode, tag } from "@sander/html"; - * import { assertEquals } from "@std/assert"; - * - * const title = "Cool Projects"; - * - * interface Project { - * title: string; - * url: string; - * } - * - * const projects: Project[] = [{ - * title: "Deno", - * url: "https://deno.com/", - * }, { - * title: "TypeScript", - * url: "https://www.typescriptlang.org/", - * }]; - * - * const cssRules = [ - * "* { --ts-blue: #3178c6; }", - * "body { font-family: sans-serif; line-height: 1.6; }", - * "li > a { color: var(--ts-blue); text-decoration: none; }", - * ]; - * - * function list(items: HtmlNode[]) { - * return tag("ul", items.map((item) => tag("li", item))); - * } - * - * function link({ title, url }: Project) { - * return tag("a", { href: url }, title); - * } - * - * const result = html( - * tag("html", { lang: "en" }, [ - * tag("head", [ - * tag("meta", { charset: "utf-8" }), - * tag("title", title), - * tag("style", cssRules), - * ]), - * tag("body", [ - * tag("h1", title), - * list(projects.map(link)), - * ]), - * ]), - * { - * doctype: "html", - * }, - * ); - * - * assertEquals( - * result, - * `\ - * - * - * - * - * - * Cool Projects - * - * - * - * - *

- * Cool Projects - *

- * - * - * - * `, - * ); - * ``` - * - * @param content - the html tree - * @param options - the options for the html source - * @returns the html source - */ -export function html(content, options) { - const firstLine = options?.doctype ? `\n` : ""; - return firstLine + innerHtml(content, options).join(""); -} -function isToHtml(value) { - return value !== undefined && - typeof value === "object" && - typeof value.toHtml === "function"; -} -//# sourceMappingURL=html.js.map diff --git a/dist/html.js.map b/dist/html.js.map deleted file mode 100644 index e25edcf..0000000 --- a/dist/html.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"html.js","sourceRoot":"","sources":["../html.ts"],"names":[],"mappings":"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"} \ No newline at end of file From b53d3967a6765e9a50b4d5de3c59d22a4b9bb312 Mon Sep 17 00:00:00 2001 From: Sander Hahn Date: Thu, 16 Jan 2025 21:26:13 +0100 Subject: [PATCH 8/9] feat: add additional exports for comment, escapeHtml, and raw from html.ts --- mod.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mod.ts b/mod.ts index 0fdd3c2..4903686 100644 --- a/mod.ts +++ b/mod.ts @@ -1 +1,9 @@ -export { html, type HtmlNode, tag, type ToHtml } from "./html.ts"; +export { + comment, + escapeHtml, + html, + type HtmlNode, + raw, + tag, + type ToHtml, +} from "./html.ts"; From 232dbf6c52b2e539e8860de706f0be4d29d772e0 Mon Sep 17 00:00:00 2001 From: Sander Hahn Date: Thu, 16 Jan 2025 21:27:12 +0100 Subject: [PATCH 9/9] chore: update deno.json and publish action --- .github/workflows/actions.yaml | 2 +- deno.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/actions.yaml b/.github/workflows/actions.yaml index dd04d53..0120a52 100644 --- a/.github/workflows/actions.yaml +++ b/.github/workflows/actions.yaml @@ -53,4 +53,4 @@ jobs: - run: deno task tsc - name: Publish package - run: npx jsr publish + run: deno publish diff --git a/deno.json b/deno.json index 8522b8e..6dc4724 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@sander/html", - "version": "0.1.9", + "version": "0.1.10", "license": "MIT", "exports": "./mod.ts", "tasks": {