UNPKG

4.63 kBPlain TextView Raw
1// deno-lint-ignore-file ban-types no-explicit-any
2
3import { escapeHtml } from './deps/deno.land/x/escape_html@1.0.0/mod.js';
4
5type Primitive = undefined | boolean | number | string | bigint | symbol;
6type Callable<T> = T | (() => T);
7
8export type Unpackable<T> =
9 | T
10 | Iterable<T>
11 | Iterable<Promise<T>> // isn't this the same as an async 0iterable?
12 | Promise<T>
13 | Promise<Iterable<T>>
14 | Promise<Iterable<Promise<T>>>
15 | AsyncIterable<T>
16 | AsyncIterable<Iterable<T>>
17 | AsyncIterable<Iterable<Promise<T>>>
18 | Promise<AsyncIterable<T>>
19 | Promise<AsyncIterable<Iterable<T>>>
20 | Promise<AsyncIterable<Iterable<Promise<T>>>>
21
22export type Renderable = null | Exclude<Primitive, symbol> | HTML | UnsafeHTML | Fallback;
23export type HTMLContentStatic = Unpackable<Renderable>;
24export type HTMLContent = Callable<HTMLContentStatic>;
25
26const isIterable = <T>(x?: unknown): x is (object & Iterable<T>) =>
27 typeof x === 'object' && x !== null && Symbol.iterator in x;
28
29const isAsyncIterable = <T>(x?: unknown): x is (object & AsyncIterable<T>) =>
30 typeof x === 'object' && x !== null && Symbol.asyncIterator in x;
31
32async function* unpackContent(content: HTMLContentStatic): AsyncIterableIterator<string> {
33 const x = await content;
34 if (x == null || x === '' || x === false) {
35 yield '';
36 } else if (x instanceof AbstractHTML) {
37 yield* x;
38 } else if (isIterable(x)) {
39 for (const xi of x) {
40 yield* unpackContent(xi);
41 }
42 } else if (isAsyncIterable(x)) {
43 for await (const xi of x) {
44 yield* unpackContent(xi);
45 }
46 } else {
47 yield escapeHtml(x as string);
48 }
49}
50
51async function* unpack(content: HTMLContent): AsyncIterableIterator<string> {
52 try {
53 yield* unpackContent(typeof content === 'function' ? content() : content);
54 } catch (err) {
55 if (err instanceof AbstractHTML) yield* err;
56 else throw err;
57 }
58}
59
60export abstract class AbstractHTML {
61 abstract [Symbol.asyncIterator](): AsyncIterableIterator<string>;
62}
63
64export class HTML extends AbstractHTML {
65 #strings: TemplateStringsArray;
66 #args: HTMLContent[];
67
68 constructor(strings: TemplateStringsArray, args: HTMLContent[]) {
69 super();
70 this.#strings = strings;
71 this.#args = args;
72 }
73
74 // async *[Symbol.asyncIterator]() {
75 // return aInterleaveFlattenSecond(this.strings, map(unpack)(this.args));
76 // }
77 async *[Symbol.asyncIterator](): AsyncIterableIterator<string> {
78 const stringsIt = this.#strings[Symbol.iterator]();
79 const argsIt = this.#args[Symbol.iterator]();
80 while (true) {
81 const { done: stringDone, value: string } = stringsIt.next() as IteratorYieldResult<string>;
82 if (stringDone) break;
83 else yield string;
84
85 const { done: argDone, value: arg } = argsIt.next() as IteratorYieldResult<HTMLContent>;
86 if (argDone) break;
87 else yield* unpack(arg);
88 }
89 const { done: stringDone, value: string } = stringsIt.next() as IteratorYieldResult<string>;
90 if (stringDone) return;
91 else yield string;
92 }
93}
94
95export class UnsafeHTML extends AbstractHTML {
96 #value: string;
97 constructor(value: string) { super(); this.#value = value || '' }
98 async *[Symbol.asyncIterator]() { yield this.#value }
99 toString() { return this.#value }
100 toJSON() { return this.#value }
101}
102
103export class Fallback extends AbstractHTML {
104 #content: HTMLContent;
105 #fallback: HTML | ((e: any) => HTML);
106
107 constructor(content: HTMLContent, fallback: HTML | ((e: any) => HTML)) {
108 super();
109 this.#content = content;
110 this.#fallback = fallback;
111 }
112
113 async *[Symbol.asyncIterator]() {
114 try {
115 yield* unpack(this.#content)
116 } catch (e) {
117 yield* typeof this.#fallback === 'function'
118 ? this.#fallback(e)
119 : this.#fallback
120 }
121 }
122}
123
124export function html(strings: TemplateStringsArray, ...args: HTMLContent[]): HTML;
125export function html(strings: TemplateStringsArray, ...args: any[]): HTML;
126export function html(strings: TemplateStringsArray, ...args: HTMLContent[]) {
127 return new HTML(strings, args);
128}
129
130// For the purpose of generating strings, there is no difference between html and css
131// so we can export this alias here to help with syntax highlighting and avoid confusion.
132export { html as css, html as js }
133
134export function fallback(content: HTMLContent, fallback: HTML | ((e: any) => HTML)): Fallback;
135export function fallback(content: any, fallback: HTML | ((e: any) => HTML)): Fallback;
136export function fallback(content: HTMLContent, fallback: HTML | ((e: any) => HTML)) {
137 return new Fallback(content, fallback);
138}
139
140export function unsafeHTML(content: string) {
141 return new UnsafeHTML(content);
142}
143
\No newline at end of file