@thi.ng/hiccup
Version:
HTML/SVG/XML serialization of nested data structures, iterables & closures
115 lines (114 loc) • 4.88 kB
JavaScript
import { deref, isDeref } from "@thi.ng/api/deref";
import { implementsFunction } from "@thi.ng/checks/implements-function";
import { isArray } from "@thi.ng/checks/is-array";
import { isFunction } from "@thi.ng/checks/is-function";
import { isNotStringAndIterable } from "@thi.ng/checks/is-not-string-iterable";
import { isPlainObject } from "@thi.ng/checks/is-plain-object";
import { isString } from "@thi.ng/checks/is-string";
import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
import { escapeEntitiesNum } from "@thi.ng/strings/entities";
import {
ATTRIB_JOIN_DELIMS,
CDATA,
COMMENT,
INLINE,
NO_CLOSE_EMPTY,
NO_SPANS,
PROC_TAGS,
VOID_TAGS
} from "./api.js";
import { css } from "./css.js";
import { normalize } from "./normalize.js";
import { formatPrefixes } from "./prefix.js";
const serialize = (tree, opts, path = [0]) => {
const $opts = {
escape: false,
escapeFn: escapeEntitiesNum,
keys: false,
span: false,
xml: false,
...opts
};
if (opts?.keys == null && $opts.span) $opts.keys = true;
return __serialize(tree, $opts, path);
};
const __serialize = (tree, opts, path) => tree == null ? "" : Array.isArray(tree) ? __serializeElement(tree, opts, path) : isFunction(tree) ? __serialize(tree(opts.ctx), opts, path) : implementsFunction(tree, "toHiccup") ? __serialize(tree.toHiccup(opts.ctx), opts, path) : isDeref(tree) ? __serialize(tree.deref(), opts, path) : isNotStringAndIterable(tree) ? __serializeIter(tree, opts, path) : (tree = __escape(String(tree), opts), opts.span) ? `<span${opts.keys ? ` key="${path.join("-")}"` : ""}>${tree}</span>` : tree;
const __serializeElement = (tree, opts, path) => {
let tag = tree[0];
return !tree.length ? "" : isFunction(tag) ? __serialize(tag.apply(null, [opts.ctx, ...tree.slice(1)]), opts, path) : implementsFunction(tag, "render") ? __serialize(
tag.render.apply(null, [opts.ctx, ...tree.slice(1)]),
opts,
path
) : tag === COMMENT ? __serializeComment(tree) : tag === INLINE ? __serializeInline(tree) : tag == CDATA ? __serializeCData(tree) : isString(tag) ? __serializeTag(tree, opts, path) : isNotStringAndIterable(tree) ? __serializeIter(tree, opts, path) : illegalArgs(`invalid tree node: ${tree}`);
};
const __serializeTag = (tree, opts, path) => {
tree = normalize(tree);
const attribs = tree[1];
if (attribs.__skip || attribs.__serialize === false) return "";
opts.keys && attribs.key === void 0 && (attribs.key = path.join("-"));
if (attribs.__escape != null) opts = { ...opts, escape: attribs.__escape };
const tag = tree[0];
const body = tree[2] ? __serializeBody(tag, tree[2], opts, path) : !VOID_TAGS[tag] && !NO_CLOSE_EMPTY[tag] ? `></${tag}>` : PROC_TAGS[tag] || "/>";
return `<${tag}${__serializeAttribs(attribs, opts)}${body}`;
};
const __serializeAttribs = (attribs, opts) => {
let res = "";
for (let a in attribs) {
if (a.startsWith("__")) continue;
const v = __serializeAttrib(attribs, a, deref(attribs[a]), opts);
v != null && (res += v);
}
return res;
};
const __serializeAttrib = (attribs, a, v, opts) => {
return v == null ? null : isFunction(v) && (/^on\w+/.test(a) || (v = v(attribs)) == null) ? null : v === true ? " " + a + (opts.xml ? `=""` : "") : v === false ? null : a === "data" ? __serializeDataAttribs(v, opts) : __attribPair(a, v, opts);
};
const __attribPair = (a, v, opts) => {
v = a === "style" && isPlainObject(v) ? css(v) : a === "prefix" && isPlainObject(v) ? formatPrefixes(v) : isArray(v) ? v.join(ATTRIB_JOIN_DELIMS[a] || " ") : v.toString();
return v.length ? ` ${a}="${__escape(v, opts)}"` : null;
};
const __serializeDataAttribs = (data, opts) => {
let res = "";
for (let id in data) {
let v = deref(data[id]);
isFunction(v) && (v = v(data));
v != null && (res += ` data-${id}="${__escape(v, opts)}"`);
}
return res;
};
const __serializeBody = (tag, body, opts, path) => {
if (VOID_TAGS[tag]) {
illegalArgs(`No body allowed in tag: ${tag}`);
}
const proc = PROC_TAGS[tag];
let res = proc ? " " : ">";
if (opts.span && !proc && !NO_SPANS[tag]) opts = { ...opts, span: true };
for (let i = 0, n = body.length; i < n; i++) {
res += __serialize(body[i], opts, [...path, i]);
}
return res + (proc || `</${tag}>`);
};
const __serializeComment = (tree) => tree.length > 2 ? `
<!--
${tree.slice(1).map((x) => " " + x).join("\n")}
-->
` : `
<!-- ${tree[1]} -->
`;
const __serializeInline = (tree) => tree.slice(1).join("");
const __serializeCData = (tree) => `<![CDATA[
${tree.slice(1).join("\n")}
]]>`;
const __serializeIter = (iter, opts, path) => {
const res = [];
const p = path.slice(0, path.length - 1);
let k = 0;
for (let i of iter) {
res.push(__serialize(i, opts, [...p, k++]));
}
return res.join("");
};
const __escape = (x, opts) => opts.escape ? opts.escapeFn(x) : x;
export {
serialize
};