160 lines
4.9 KiB
TypeScript
160 lines
4.9 KiB
TypeScript
|
//---------------------------------------------------------------------------
|
||
|
// @syndicate-lang/html, Browser-based UI for Syndicate
|
||
|
// Copyright (C) 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
||
|
//
|
||
|
// This program is free software: you can redistribute it and/or modify
|
||
|
// it under the terms of the GNU General Public License as published by
|
||
|
// the Free Software Foundation, either version 3 of the License, or
|
||
|
// (at your option) any later version.
|
||
|
//
|
||
|
// This program is distributed in the hope that it will be useful,
|
||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
// GNU General Public License for more details.
|
||
|
//
|
||
|
// You should have received a copy of the GNU General Public License
|
||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
|
//---------------------------------------------------------------------------
|
||
|
|
||
|
let nextId = 1;
|
||
|
|
||
|
export function escape(s: string): string {
|
||
|
return s
|
||
|
.replace(/&/g, "&")
|
||
|
.replace(/</g, "<")
|
||
|
.replace(/>/g, ">")
|
||
|
.replace(/"/g, """)
|
||
|
.replace(/'/g, "'");
|
||
|
}
|
||
|
|
||
|
export type PlaceholderNodeMap = { [id: string]: Node };
|
||
|
|
||
|
export type HtmlFragment = string | number | Array<HtmlFragment> | Node | FlattenInto;
|
||
|
|
||
|
export interface FlattenInto {
|
||
|
flattenInto(acc: Array<HtmlFragment>, 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<HtmlFragment>;
|
||
|
|
||
|
constructor(pieces: Array<HtmlFragment> = []) {
|
||
|
this.pieces = pieces;
|
||
|
}
|
||
|
|
||
|
appendTo(n: ParentNode): Array<Node> {
|
||
|
const ns = [... this.nodes()];
|
||
|
n.append(... ns);
|
||
|
return ns;
|
||
|
}
|
||
|
|
||
|
replaceContentOf(n: Element) {
|
||
|
n.innerHTML = '';
|
||
|
return this.appendTo(n);
|
||
|
}
|
||
|
|
||
|
node(): Node {
|
||
|
return this.nodes()[0];
|
||
|
}
|
||
|
|
||
|
nodes(): Array<Node> {
|
||
|
let n = document.createElement('div');
|
||
|
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.childNodes);
|
||
|
}
|
||
|
|
||
|
toString(nodeMap?: PlaceholderNodeMap) {
|
||
|
const allPieces: Array<string> = [];
|
||
|
this.flattenInto(allPieces, { nodeMap });
|
||
|
return allPieces.join('');
|
||
|
}
|
||
|
|
||
|
flattenInto(acc: Array<string>, options: FlattenIntoOptions) {
|
||
|
flattenInto(acc, this.pieces, { ... options, escapeStrings: false });
|
||
|
}
|
||
|
|
||
|
join(pieces: Array<HtmlFragment>): Array<HtmlFragment> {
|
||
|
return join(pieces, this);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export function flattenInto(acc: Array<HtmlFragment>,
|
||
|
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(`<placeholder id="${id}"></placeholder>`);
|
||
|
} else {
|
||
|
acc.push(p);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
default:
|
||
|
((_n: never) => {})(p);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export function join(pieces: Array<HtmlFragment>, separator: HtmlFragment): Array<HtmlFragment> {
|
||
|
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<HtmlFragment>): HtmlFragments
|
||
|
{
|
||
|
const pieces: Array<HtmlFragment> = [];
|
||
|
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;
|