1 |
|
2 |
|
3 | import { escapeHtml } from './deps/deno.land/x/escape_html@1.0.0/mod.js';
|
4 |
|
5 | type Primitive = undefined | boolean | number | string | bigint | symbol;
|
6 | type Callable<T> = T | (() => T);
|
7 |
|
8 | export 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 |
|
22 | export type Renderable = null | Exclude<Primitive, symbol> | HTML | UnsafeHTML | Fallback;
|
23 | export type HTMLContentStatic = Unpackable<Renderable>;
|
24 | export type HTMLContent = Callable<HTMLContentStatic>;
|
25 |
|
26 | const isIterable = <T>(x?: unknown): x is (object & Iterable<T>) =>
|
27 | typeof x === 'object' && x !== null && Symbol.iterator in x;
|
28 |
|
29 | const isAsyncIterable = <T>(x?: unknown): x is (object & AsyncIterable<T>) =>
|
30 | typeof x === 'object' && x !== null && Symbol.asyncIterator in x;
|
31 |
|
32 | async 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 |
|
51 | async 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 |
|
60 | export abstract class AbstractHTML {
|
61 | abstract [Symbol.asyncIterator](): AsyncIterableIterator<string>;
|
62 | }
|
63 |
|
64 | export 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 |
|
75 |
|
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 |
|
95 | export 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 |
|
103 | export 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 |
|
124 | export function html(strings: TemplateStringsArray, ...args: HTMLContent[]): HTML;
|
125 | export function html(strings: TemplateStringsArray, ...args: any[]): HTML;
|
126 | export 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.
|
132 | export { html as css, html as js }
|
133 |
|
134 | export function fallback(content: HTMLContent, fallback: HTML | ((e: any) => HTML)): Fallback;
|
135 | export function fallback(content: any, fallback: HTML | ((e: any) => HTML)): Fallback;
|
136 | export function fallback(content: HTMLContent, fallback: HTML | ((e: any) => HTML)) {
|
137 | return new Fallback(content, fallback);
|
138 | }
|
139 |
|
140 | export function unsafeHTML(content: string) {
|
141 | return new UnsafeHTML(content);
|
142 | }
|
143 |
|
\ | No newline at end of file |