/// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2016-2021 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 pieces: Array; constructor(pieces: Array = []) { 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 = {}; n.innerHTML = this.toString(nodeMap); for (const p of Array.from(n.querySelectorAll('placeholder'))) { const e = nodeMap[p.id]; if (e) { p.parentNode!.insertBefore(e, p); p.remove(); } } 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 function template( constantParts: TemplateStringsArray, ... variableParts: Array): HtmlFragments { const pieces: Array = []; function pushConst(i: number) { const r = constantParts.raw[i].trimLeft(); if (r) pieces.push(r); } pushConst(0); variableParts.forEach((vp, vpIndex) => { flattenInto(pieces, vp, { escapeStrings: true }); pushConst(vpIndex + 1); }); return new HtmlFragments(pieces); }; export function raw(str: string) { return new HtmlFragments([str]); } export default template;