uhtml
Version:
A micro HTML/SVG render
122 lines (109 loc) • 3.69 kB
JavaScript
import { COMMENT_NODE, ELEMENT_NODE } from 'domconstants/constants';
import { TEXT_ELEMENTS } from 'domconstants/re';
import parser from '@webreflection/uparser';
import { empty, isArray, set } from './utils.js';
import { abc } from './literals.js';
import { array, attribute, hole, text, removeAttribute } from './handler.js';
import createContent from './create-content.js';
/** @typedef {import("./literals.js").Entry} Entry */
/**
* @typedef {Object} Resolved
* @param {DocumentFragment} f content retrieved from the template
* @param {Entry[]} e entries per each hole in the template
* @param {boolean} d direct node to handle
*/
/**
* @param {Element} node
* @returns {number[]}
*/
const createPath = node => {
const path = [];
let parentNode;
while ((parentNode = node.parentNode)) {
path.push(path.indexOf.call(parentNode.childNodes, node));
node = parentNode;
}
return path;
};
const textNode = () => document.createTextNode('');
/**
* @param {TemplateStringsArray} template
* @param {boolean} xml
* @returns {Resolved}
*/
const resolve = (template, values, xml) => {
const content = createContent(parser(template, prefix, xml), xml);
const { length } = template;
let entries = empty;
if (length > 1) {
const replace = [];
const tw = document.createTreeWalker(content, 1 | 128);
let i = 0, search = `${prefix}${i++}`;
entries = [];
while (i < length) {
const node = tw.nextNode();
// these are holes or arrays
if (node.nodeType === COMMENT_NODE) {
if (node.data === search) {
// ⚠️ once array, always array!
const update = isArray(values[i - 1]) ? array : hole;
if (update === hole) replace.push(node);
entries.push(abc(createPath(node), update, null));
search = `${prefix}${i++}`;
}
}
else {
let path;
// these are attributes
while (node.hasAttribute(search)) {
if (!path) path = createPath(node);
const name = node.getAttribute(search);
entries.push(abc(path, attribute(node, name, xml), name));
removeAttribute(node, search);
search = `${prefix}${i++}`;
}
// these are special text-only nodes
if (
!xml &&
TEXT_ELEMENTS.test(node.localName) &&
node.textContent.trim() === `<!--${search}-->`
) {
entries.push(abc(path || createPath(node), text, null));
search = `${prefix}${i++}`;
}
}
}
// can't replace holes on the fly or the tree walker fails
for (i = 0; i < replace.length; i++)
replace[i].replaceWith(textNode());
}
// need to decide if there should be a persistent fragment
const { childNodes } = content;
let { length: len } = childNodes;
// html`` or svg`` to signal an empty content
// these nodes can be passed directly as never mutated
if (len < 1) {
len = 1;
content.appendChild(textNode());
}
// html`${'b'}` or svg`${[]}` cases
else if (
len === 1 &&
// ignore html`static` or svg`static` because
// these nodes can be passed directly as never mutated
length !== 1 &&
childNodes[0].nodeType !== ELEMENT_NODE
) {
// use a persistent fragment for these cases too
len = 0;
}
return set(cache, template, abc(content, entries, len === 1));
};
/** @type {WeakMap<TemplateStringsArray, Resolved>} */
const cache = new WeakMap;
const prefix = 'isµ';
/**
* @param {boolean} xml
* @returns {(template: TemplateStringsArray, values: any[]) => Resolved}
*/
export default xml => (template, values) => cache.get(template) || resolve(template, values, xml);