// deno-lint-ignore-file ban-types no-explicit-any import { escapeHtml } from './deps/deno.land/x/escape_html@1.0.0/mod.js'; type Primitive = undefined | boolean | number | string | bigint | symbol; type Callable = T | (() => T); export type Unpackable = | T | Iterable | Iterable> // isn't this the same as an async 0iterable? | Promise | Promise> | Promise>> | AsyncIterable | AsyncIterable> | AsyncIterable>> | Promise> | Promise>> | Promise>>> export type Renderable = null | Exclude | HTML | UnsafeHTML | Fallback; export type HTMLContentStatic = Unpackable; export type HTMLContent = Callable; const isIterable = (x?: unknown): x is (object & Iterable) => typeof x === 'object' && x !== null && Symbol.iterator in x; const isAsyncIterable = (x?: unknown): x is (object & AsyncIterable) => typeof x === 'object' && x !== null && Symbol.asyncIterator in x; async function* unpackContent(content: HTMLContentStatic): AsyncIterableIterator { const x = await content; if (x == null || x === '' || x === false) { yield ''; } else if (x instanceof AbstractHTML) { yield* x; } else if (isIterable(x)) { for (const xi of x) { yield* unpackContent(xi); } } else if (isAsyncIterable(x)) { for await (const xi of x) { yield* unpackContent(xi); } } else { yield escapeHtml(x as string); } } async function* unpack(content: HTMLContent): AsyncIterableIterator { try { yield* unpackContent(typeof content === 'function' ? content() : content); } catch (err) { if (err instanceof AbstractHTML) yield* err; else throw err; } } export abstract class AbstractHTML { abstract [Symbol.asyncIterator](): AsyncIterableIterator; } export class HTML extends AbstractHTML { #strings: TemplateStringsArray; #args: HTMLContent[]; constructor(strings: TemplateStringsArray, args: HTMLContent[]) { super(); this.#strings = strings; this.#args = args; } // async *[Symbol.asyncIterator]() { // return aInterleaveFlattenSecond(this.strings, map(unpack)(this.args)); // } async *[Symbol.asyncIterator](): AsyncIterableIterator { const stringsIt = this.#strings[Symbol.iterator](); const argsIt = this.#args[Symbol.iterator](); while (true) { const { done: stringDone, value: string } = stringsIt.next() as IteratorYieldResult; if (stringDone) break; else yield string; const { done: argDone, value: arg } = argsIt.next() as IteratorYieldResult; if (argDone) break; else yield* unpack(arg); } const { done: stringDone, value: string } = stringsIt.next() as IteratorYieldResult; if (stringDone) return; else yield string; } } export class UnsafeHTML extends AbstractHTML { #value: string; constructor(value: string) { super(); this.#value = value || '' } async *[Symbol.asyncIterator]() { yield this.#value } toString() { return this.#value } toJSON() { return this.#value } } export class Fallback extends AbstractHTML { #content: HTMLContent; #fallback: HTML | ((e: any) => HTML); constructor(content: HTMLContent, fallback: HTML | ((e: any) => HTML)) { super(); this.#content = content; this.#fallback = fallback; } async *[Symbol.asyncIterator]() { try { yield* unpack(this.#content) } catch (e) { yield* typeof this.#fallback === 'function' ? this.#fallback(e) : this.#fallback } } } export function html(strings: TemplateStringsArray, ...args: HTMLContent[]): HTML; export function html(strings: TemplateStringsArray, ...args: any[]): HTML; export function html(strings: TemplateStringsArray, ...args: HTMLContent[]) { return new HTML(strings, args); } // For the purpose of generating strings, there is no difference between html and css // so we can export this alias here to help with syntax highlighting and avoid confusion. export { html as css, html as js } export function fallback(content: HTMLContent, fallback: HTML | ((e: any) => HTML)): Fallback; export function fallback(content: any, fallback: HTML | ((e: any) => HTML)): Fallback; export function fallback(content: HTMLContent, fallback: HTML | ((e: any) => HTML)) { return new Fallback(content, fallback); } export function unsafeHTML(content: string) { return new UnsafeHTML(content); }