uhtml
Version:
A micro HTML/SVG render
123 lines (110 loc) • 3.83 kB
JavaScript
;
const { COMMENT_NODE, ELEMENT_NODE } = require('domconstants/constants');
const { TEXT_ELEMENTS } = require('domconstants/re');
const parser = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('@webreflection/uparser'));
const { empty, isArray, set } = require('./utils.js');
const { abc } = require('./literals.js');
const { array, attribute, hole, text, removeAttribute } = require('./handler.js');
const createContent = (require('./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}
*/
module.exports = xml => (template, values) => cache.get(template) || resolve(template, values, xml);