/// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2016-2023 Tony Garnock-Jones let nextId = 1; export function escape(s: string): string { return s .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } export type PlaceholderNodeMap = { [id: string]: Node }; export type HtmlFragment = string | number | Array | Node | FlattenInto; export interface FlattenInto { flattenInto(acc: Array, options?: FlattenIntoOptions): void; } export interface FlattenIntoOptions { nodeMap?: PlaceholderNodeMap; escapeStrings?: boolean; } export function isFlattenInto(x: any): x is FlattenInto { return typeof x === 'object' && x !== null && typeof x.flattenInto === 'function'; } export class HtmlFragments implements FlattenInto { readonly contextElementName: string | undefined; readonly pieces: Array; constructor(pieces: Array = [], contextElementName?: string) { this.contextElementName = contextElementName; this.pieces = pieces; } appendTo(n: ParentNode): Array { const ns = [... this.nodes()]; n.append(... ns); return ns; } replaceContentOf(n: Element) { n.innerHTML = ''; return this.appendTo(n); } node(): ChildNode { return this.nodes()[0]; } nodes(): Array { let n = document.createElement('template'); const nodeMap: PlaceholderNodeMap = {}; const source = this.toString(nodeMap); if (this.contextElementName !== void 0) { n.innerHTML = `<${this.contextElementName}>${source}`; } else { n.innerHTML = source; } for (const p of Array.from(n.content.querySelectorAll('placeholder'))) { const e = nodeMap[p.id]; if (e) { p.parentNode!.insertBefore(e, p); p.remove(); } } if (this.contextElementName !== void 0) { return Array.from(n.content.firstElementChild!.childNodes); } else { return Array.from(n.content.childNodes); } } toString(nodeMap?: PlaceholderNodeMap) { const allPieces: Array = []; this.flattenInto(allPieces, { nodeMap }); return allPieces.join(''); } flattenInto(acc: Array, options: FlattenIntoOptions) { flattenInto(acc, this.pieces, { ... options, escapeStrings: false }); } join(pieces: Array): Array { return join(pieces, this); } } export function flattenInto(acc: Array, p: HtmlFragment, options: FlattenIntoOptions = {}) { switch (typeof p) { case 'string': acc.push((options.escapeStrings ?? true) ? escape(p) : p); break; case 'number': acc.push('' + p); break; case 'object': if (isFlattenInto(p)) { p.flattenInto(acc, { nodeMap: options.nodeMap }); } else if (Array.isArray(p)) { p.forEach(q => flattenInto(acc, q, options)); } else if (typeof Node !== 'undefined' && p instanceof Node) { if (options.nodeMap !== void 0) { const id = `__SYNDICATE__html__${nextId++}`; options.nodeMap[id] = p; acc.push(``); } else { acc.push(p); } } break; default: ((_n: never) => {})(p); } } export function join(pieces: Array, separator: HtmlFragment): Array { if (pieces.length <= 1) { return []; } else { const result = [pieces[0]]; for (let i = 1; i < pieces.length; i++) { result.push(separator); result.push(pieces[i]); } return result; } } export type HtmlFragmentsTemplate = (constantParts: TemplateStringsArray, ... variableParts: Array) => HtmlFragments; export type NodeTemplate = (constantParts: TemplateStringsArray, ... variableParts: Array) => N; export interface TagTemplate { tag(): NodeTemplate; tag(c: { new (...args: any): N }): NodeTemplate; }; export function makeTemplate( contextElementName?: string, ): HtmlFragmentsTemplate & TagTemplate { const templater = (constantParts: TemplateStringsArray, ... variableParts: Array) => { const pieces: Array = []; function pushConst(i: number) { const r0 = constantParts.raw[i]; const r = i === 0 ? r0.trimLeft() : r0; if (r) pieces.push(r); } pushConst(0); variableParts.forEach((vp, vpIndex) => { flattenInto(pieces, vp, { escapeStrings: true }); pushConst(vpIndex + 1); }); return new HtmlFragments(pieces, contextElementName); }; templater.tag = (c?: { new (...args: any): N }) => (constantParts: TemplateStringsArray, ... variableParts: Array): any => { const n = templater(constantParts, ... variableParts).node(); if (n instanceof (c ?? Element)) { return n; } else { throw new TypeError(`Template generated ${n}, but instance of ${c} was expected`); } }; return templater; } export const template = makeTemplate(); export function raw(str: string, contextElementName?: string) { return new HtmlFragments([str], contextElementName); } export default template;