export function flatPlan(elementIds, tags, textIds, textElements) {
return {
elements: new Map(elementIds.map((id, index) => [id, index])),
tags,
texts: new Map(textIds.map((id, index) => [id, index])),
elementIds,
textIds,
textElements,
textHosts: new Map(Array.from({ length: textElements.length / 3 }, (_, i) =>
[textElements[i * 3], i * 3])),
};
}
function mismatch(message) {
throw `fusor: template mismatch: ${message}`;
}
export function resolveFlat(plan, root) {
const textOffset = plan.elementIds.length;
const hostOffset = textOffset + plan.textIds.length * 3;
const elements = new Array(hostOffset + plan.textElements.length / 3 * 2);
const walker = root.ownerDocument.createTreeWalker(root);
let node = root;
do {
if (node.nodeType === 1) {
const id = node.getAttribute('data-rf-node');
const text = node.getAttribute('data-rf-text');
if (text !== null) {
const offset = plan.textHosts.get(text);
if (offset === undefined) mismatch('unexpected text element');
const host = plan.textElements[offset + 1], tag = plan.textElements[offset + 2];
if (host !== null && host !== id) mismatch('mismatched text host');
if (node.localName !== tag || node.namespaceURI !== 'http://www.w3.org/1999/xhtml') mismatch(`text element ${text} must be <${tag}>`);
const slot = hostOffset + offset / 3 * 2;
if (elements[slot]) mismatch(`duplicate text element ${text}`);
elements[slot] = node;
}
if (id !== null) {
const index = plan.elements.get(id);
if (index === undefined) mismatch('unexpected element identifiers');
if (elements[index]) mismatch(`duplicate element ${id}`);
if (node.localName !== plan.tags[index] || node.namespaceURI !== 'http://www.w3.org/1999/xhtml') {
mismatch(`element ${id} must be <${plan.tags[index]}>, found <${node.localName}>`);
}
elements[index] = node;
}
} else if (node.nodeType === 8) {
const value = node.nodeValue;
let id, end;
if (value.startsWith('rf:')) {
id = value.slice(3); end = false;
} else if (value.startsWith('/rf:')) {
id = value.slice(4); end = true;
} else continue;
const index = plan.texts.get(id);
if (index === undefined) mismatch('unexpected text or component identifiers');
const slot = textOffset + index * 3 + (end ? 1 : 0);
if (elements[slot]) mismatch(`duplicate text ${end ? 'end' : 'start'} ${id}`);
elements[slot] = node;
}
} while ((node = walker.nextNode()));
return validateSlots(plan, elements);
}
function validateSlots(plan, elements) {
const textOffset = plan.elementIds.length;
const hostOffset = textOffset + plan.textIds.length * 3;
for (let index = 0; index < plan.elementIds.length; index++) {
if (!elements[index]) mismatch(`missing element ${plan.elementIds[index]}`);
}
for (let index = 0; index < plan.textIds.length; index++) {
const slot = textOffset + index * 3;
const id = plan.textIds[index], start = elements[slot], end = elements[slot + 1];
if (!start) mismatch(`missing text start ${id}`);
if (!end) mismatch(`missing text end ${id}`);
const next = start.nextSibling;
if (!next) mismatch(`unpaired text slot ${id}`);
let text = null;
if (next !== end) {
if (next.nextSibling !== end) mismatch(`unexpected nodes in text slot ${id}`);
if (next.nodeType !== 3) mismatch(`expected a text node in slot ${id}`);
text = next;
}
elements[slot + 2] = text;
}
for (let index = 0; index < plan.textElements.length; index += 3) {
const slot = hostOffset + index / 3 * 2;
const id = plan.textElements[index], host = elements[slot];
if (!host) mismatch(`missing or mismatched text element ${id}`);
const text = host.firstChild;
if (text && text.nextSibling) mismatch(`unexpected nodes in text element ${id}`);
if (text && text.nodeType !== 3) mismatch(`expected a text node in text element ${id}`);
elements[slot + 1] = text;
}
return elements;
}
function checked(value, constructor) {
let valid = false;
try {
switch (constructor) {
case 'Element': valid = value instanceof Element; break;
case 'HTMLInputElement': valid = value instanceof HTMLInputElement; break;
case 'Node': valid = value instanceof Node; break;
case 'Text': valid = value instanceof Text; break;
}
} catch (_) {}
if (!valid) throw value;
}
function validateBundleTypes(plan, nodes) {
const textOffset = plan.elementIds.length;
const hostOffset = textOffset + plan.textIds.length * 3;
for (let i = 0; i < textOffset; i++) {
checked(nodes[i], plan.tags[i] === 'input' ? 'HTMLInputElement' : 'Element');
}
for (let i = 0; i < plan.textIds.length; i++) {
const slot = textOffset + i * 3;
checked(nodes[slot], 'Node');
checked(nodes[slot + 1], 'Node');
if (nodes[slot + 2] !== null) checked(nodes[slot + 2], 'Text');
}
for (let i = 0; i < plan.textElements.length / 3; i++) {
const slot = hostOffset + i * 2;
checked(nodes[slot], 'Element');
if (nodes[slot + 1] !== null) checked(nodes[slot + 1], 'Text');
}
}
function pathFrom(root, target) {
const path = [];
let node = target;
while (node !== root) {
let sibling = node.previousSibling, index = 0;
while (sibling) { index++; sibling = sibling.previousSibling; }
path.push(index);
node = node.parentNode;
if (!node) mismatch('detached cached handle');
}
return path.reverse();
}
function followPath(root, path) {
let node = root;
for (const index of path) {
node = node.childNodes.item(index);
if (!node) mismatch('template changed during resolution');
}
return node;
}
function rememberBundle(plan, root, nodes) {
const textOffset = plan.elementIds.length;
const hostOffset = textOffset + plan.textIds.length * 3;
const wrapper = root.ownerDocument.createElement('template');
const inert = wrapper.content.ownerDocument;
const pristine = inert.importNode(root, true);
const paths = nodes.map((node, index) => {
const existingText = index >= hostOffset
? (index - hostOffset) % 2 === 1
: index >= textOffset && (index - textOffset) % 3 === 2;
return existingText ? null : pathFrom(root, node);
});
plan.bindingCache = { pristine, paths };
}
export function resolveBindings(plan, root, cached) {
let nodes;
const certificate = cached && plan.bindingCache;
const cacheHit = certificate && root.isEqualNode(certificate.pristine);
if (cacheHit) {
nodes = certificate.paths.map(path => path === null ? null : followPath(root, path));
validateSlots(plan, nodes);
} else {
nodes = resolveFlat(plan, root);
}
validateBundleTypes(plan, nodes);
if (cached && !cacheHit) {
try { rememberBundle(plan, root, nodes); } catch (_) {}
}
const textOffset = plan.elementIds.length;
const hostOffset = textOffset + plan.textIds.length * 3;
for (let i = 0; i < plan.textIds.length; i++) {
const slot = textOffset + i * 3;
const start = nodes[slot], end = nodes[slot + 1];
let text = nodes[slot + 2];
if (text === null) {
text = document.createTextNode('');
const parent = start.parentNode;
if (!parent) mismatch('detached text anchor');
parent.insertBefore(text, end);
}
nodes[textOffset + i] = text;
}
for (let i = 0; i < plan.textElements.length / 3; i++) {
const slot = hostOffset + i * 2, host = nodes[slot];
let text = nodes[slot + 1];
if (text === null) { text = document.createTextNode(''); host.appendChild(text); }
nodes[textOffset + plan.textIds.length + i] = text;
}
nodes.length = textOffset + plan.textIds.length + plan.textElements.length / 3;
return nodes;
}
export function bindingText(nodes, index, value) {
const text = nodes[index];
if (text.data !== value) text.data = value;
}
export function bindingIntegerText(nodes, index, number) {
const text = nodes[index], value = "" + number;
if (text.data !== value) text.data = value;
}
export function bindingSetAttribute(nodes, index, name, value) {
nodes[index].setAttribute(name, value);
}
export function bindingRemoveAttribute(nodes, index, name) {
nodes[index].removeAttribute(name);
}
export function bindingElement(nodes, index) { return nodes[index]; }