import { filterXSS } from 'xss'; // import { aInterleaveFlattenSecond, map } from './iter'; type Repeatable = T | T[]; type Awaitable = T | Promise; type Callable = T | (() => T); type Primitive = undefined | boolean | number | string | BigInt | Symbol; type Renderable = null | Primitive | HTML | UnsafeHTML | Fallback; type HTMLContentStatic = Repeatable>>; export type HTMLContent = Callable; async function* unpackContent(content: HTMLContentStatic): AsyncIterableIterator { const x = await content; if (Array.isArray(x)) for (const xi of x) yield* unpackContent(xi); else if (x instanceof Unpackable) yield* x; else yield filterXSS(x as string); // relying on string coercion for primitives within the xss module here } async function* unpack(content: HTMLContent): AsyncIterableIterator { try { yield* unpackContent(typeof content === 'function' ? content() : content); } catch (err) { if (err instanceof HTML) yield* err; else throw err; } } abstract class Unpackable { abstract [Symbol.asyncIterator](): AsyncIterableIterator; } export class HTML extends Unpackable { strings: TemplateStringsArray; args: HTMLContent[]; constructor(strings: TemplateStringsArray, args: HTMLContent[]) { super(); this.strings = strings; this.args = 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(); if (stringDone) break; else yield string; const { done: argDone, value: arg } = argsIt.next(); if (argDone) break; else yield* unpack(arg); } } // async *[Symbol.asyncIterator]() { // return aInterleaveFlattenSecond(this.strings, map(unpack)(this.args)); // } } export class UnsafeHTML extends Unpackable { 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 Unpackable { 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[]) { 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)) { return new Fallback(content, fallback); } export function unsafeHTML(content: string) { return new UnsafeHTML(content); }