uhtml
Version:
A minimalistic library to create fast and reactive Web pages
1,699 lines (1,568 loc) • 56.3 kB
JavaScript
var ReactiveFlags;
(function (ReactiveFlags) {
ReactiveFlags[ReactiveFlags["None"] = 0] = "None";
ReactiveFlags[ReactiveFlags["Mutable"] = 1] = "Mutable";
ReactiveFlags[ReactiveFlags["Watching"] = 2] = "Watching";
ReactiveFlags[ReactiveFlags["RecursedCheck"] = 4] = "RecursedCheck";
ReactiveFlags[ReactiveFlags["Recursed"] = 8] = "Recursed";
ReactiveFlags[ReactiveFlags["Dirty"] = 16] = "Dirty";
ReactiveFlags[ReactiveFlags["Pending"] = 32] = "Pending";
})(ReactiveFlags || (ReactiveFlags = {}));
function createReactiveSystem({ update, notify, unwatched, }) {
let version = 0;
return {
link,
unlink,
propagate,
checkDirty,
endTracking,
startTracking,
shallowPropagate,
};
function link(dep, sub) {
const prevDep = sub.depsTail;
if (prevDep !== undefined && prevDep.dep === dep) {
return;
}
let nextDep;
if (sub.flags & 4) {
nextDep = prevDep !== undefined ? prevDep.nextDep : sub.deps;
if (nextDep !== undefined && nextDep.dep === dep) {
nextDep.version = version;
sub.depsTail = nextDep;
return;
}
}
const prevSub = dep.subsTail;
if (prevSub !== undefined && prevSub.version === version && prevSub.sub === sub) {
return;
}
const newLink = sub.depsTail
= dep.subsTail
= {
version,
dep,
sub,
prevDep,
nextDep,
prevSub,
nextSub: undefined,
};
if (nextDep !== undefined) {
nextDep.prevDep = newLink;
}
if (prevDep !== undefined) {
prevDep.nextDep = newLink;
}
else {
sub.deps = newLink;
}
if (prevSub !== undefined) {
prevSub.nextSub = newLink;
}
else {
dep.subs = newLink;
}
}
function unlink(link, sub = link.sub) {
const dep = link.dep;
const prevDep = link.prevDep;
const nextDep = link.nextDep;
const nextSub = link.nextSub;
const prevSub = link.prevSub;
if (nextDep !== undefined) {
nextDep.prevDep = prevDep;
}
else {
sub.depsTail = prevDep;
}
if (prevDep !== undefined) {
prevDep.nextDep = nextDep;
}
else {
sub.deps = nextDep;
}
if (nextSub !== undefined) {
nextSub.prevSub = prevSub;
}
else {
dep.subsTail = prevSub;
}
if (prevSub !== undefined) {
prevSub.nextSub = nextSub;
}
else if ((dep.subs = nextSub) === undefined) {
unwatched(dep);
}
return nextDep;
}
function propagate(link) {
let next = link.nextSub;
let stack;
top: do {
const sub = link.sub;
let flags = sub.flags;
if (flags & 3) {
if (!(flags & 60)) {
sub.flags = flags | 32;
}
else if (!(flags & 12)) {
flags = 0;
}
else if (!(flags & 4)) {
sub.flags = (flags & -9) | 32;
}
else if (!(flags & 48) && isValidLink(link, sub)) {
sub.flags = flags | 40;
flags &= 1;
}
else {
flags = 0;
}
if (flags & 2) {
notify(sub);
}
if (flags & 1) {
const subSubs = sub.subs;
if (subSubs !== undefined) {
link = subSubs;
if (subSubs.nextSub !== undefined) {
stack = { value: next, prev: stack };
next = link.nextSub;
}
continue;
}
}
}
if ((link = next) !== undefined) {
next = link.nextSub;
continue;
}
while (stack !== undefined) {
link = stack.value;
stack = stack.prev;
if (link !== undefined) {
next = link.nextSub;
continue top;
}
}
break;
} while (true);
}
function startTracking(sub) {
++version;
sub.depsTail = undefined;
sub.flags = (sub.flags & -57) | 4;
}
function endTracking(sub) {
const depsTail = sub.depsTail;
let toRemove = depsTail !== undefined ? depsTail.nextDep : sub.deps;
while (toRemove !== undefined) {
toRemove = unlink(toRemove, sub);
}
sub.flags &= -5;
}
function checkDirty(link, sub) {
let stack;
let checkDepth = 0;
top: do {
const dep = link.dep;
const depFlags = dep.flags;
let dirty = false;
if (sub.flags & 16) {
dirty = true;
}
else if ((depFlags & 17) === 17) {
if (update(dep)) {
const subs = dep.subs;
if (subs.nextSub !== undefined) {
shallowPropagate(subs);
}
dirty = true;
}
}
else if ((depFlags & 33) === 33) {
if (link.nextSub !== undefined || link.prevSub !== undefined) {
stack = { value: link, prev: stack };
}
link = dep.deps;
sub = dep;
++checkDepth;
continue;
}
if (!dirty && link.nextDep !== undefined) {
link = link.nextDep;
continue;
}
while (checkDepth) {
--checkDepth;
const firstSub = sub.subs;
const hasMultipleSubs = firstSub.nextSub !== undefined;
if (hasMultipleSubs) {
link = stack.value;
stack = stack.prev;
}
else {
link = firstSub;
}
if (dirty) {
if (update(sub)) {
if (hasMultipleSubs) {
shallowPropagate(firstSub);
}
sub = link.sub;
continue;
}
}
else {
sub.flags &= -33;
}
sub = link.sub;
if (link.nextDep !== undefined) {
link = link.nextDep;
continue top;
}
dirty = false;
}
return dirty;
} while (true);
}
function shallowPropagate(link) {
do {
const sub = link.sub;
const nextSub = link.nextSub;
const subFlags = sub.flags;
if ((subFlags & 48) === 32) {
sub.flags = subFlags | 16;
if (subFlags & 2) {
notify(sub);
}
}
link = nextSub;
} while (link !== undefined);
}
function isValidLink(checkLink, sub) {
const depsTail = sub.depsTail;
if (depsTail !== undefined) {
let link = sub.deps;
do {
if (link === checkLink) {
return true;
}
if (link === depsTail) {
break;
}
link = link.nextDep;
} while (link !== undefined);
}
return false;
}
}
const pauseStack = [];
const queuedEffects = [];
const { link, unlink, propagate, checkDirty, endTracking, startTracking, shallowPropagate, } = createReactiveSystem({
update(signal) {
if ('getter' in signal) {
return updateComputed(signal);
}
else {
return updateSignal(signal, signal.value);
}
},
notify,
unwatched(node) {
if ('getter' in node) {
let toRemove = node.deps;
if (toRemove !== undefined) {
node.flags = 17;
do {
toRemove = unlink(toRemove, node);
} while (toRemove !== undefined);
}
}
else if (!('previousValue' in node)) {
effectOper.call(node);
}
},
});
let batchDepth = 0;
let notifyIndex = 0;
let queuedEffectsLength = 0;
let activeSub;
let activeScope;
function setCurrentSub(sub) {
const prevSub = activeSub;
activeSub = sub;
return prevSub;
}
function setCurrentScope(scope) {
const prevScope = activeScope;
activeScope = scope;
return prevScope;
}
function startBatch() {
++batchDepth;
}
function endBatch() {
if (!--batchDepth) {
flush();
}
}
function pauseTracking() {
pauseStack.push(setCurrentSub(undefined));
}
function resumeTracking() {
setCurrentSub(pauseStack.pop());
}
function signal$2(initialValue) {
return signalOper.bind({
previousValue: initialValue,
value: initialValue,
subs: undefined,
subsTail: undefined,
flags: 1,
});
}
function computed$1(getter) {
return computedOper.bind({
value: undefined,
subs: undefined,
subsTail: undefined,
deps: undefined,
depsTail: undefined,
flags: 17,
getter: getter,
});
}
function effect(fn) {
const e = {
fn,
subs: undefined,
subsTail: undefined,
deps: undefined,
depsTail: undefined,
flags: 2,
};
if (activeSub !== undefined) {
link(e, activeSub);
}
else if (activeScope !== undefined) {
link(e, activeScope);
}
const prev = setCurrentSub(e);
try {
e.fn();
}
finally {
setCurrentSub(prev);
}
return effectOper.bind(e);
}
function effectScope(fn) {
const e = {
deps: undefined,
depsTail: undefined,
subs: undefined,
subsTail: undefined,
flags: 0,
};
if (activeScope !== undefined) {
link(e, activeScope);
}
const prevSub = setCurrentSub(undefined);
const prevScope = setCurrentScope(e);
try {
fn();
}
finally {
setCurrentScope(prevScope);
setCurrentSub(prevSub);
}
return effectOper.bind(e);
}
function updateComputed(c) {
const prevSub = setCurrentSub(c);
startTracking(c);
try {
const oldValue = c.value;
return oldValue !== (c.value = c.getter(oldValue));
}
finally {
setCurrentSub(prevSub);
endTracking(c);
}
}
function updateSignal(s, value) {
s.flags = 1;
return s.previousValue !== (s.previousValue = value);
}
function notify(e) {
const flags = e.flags;
if (!(flags & 64)) {
e.flags = flags | 64;
const subs = e.subs;
if (subs !== undefined) {
notify(subs.sub);
}
else {
queuedEffects[queuedEffectsLength++] = e;
}
}
}
function run(e, flags) {
if (flags & 16
|| (flags & 32 && checkDirty(e.deps, e))) {
const prev = setCurrentSub(e);
startTracking(e);
try {
e.fn();
}
finally {
setCurrentSub(prev);
endTracking(e);
}
return;
}
else if (flags & 32) {
e.flags = flags & -33;
}
let link = e.deps;
while (link !== undefined) {
const dep = link.dep;
const depFlags = dep.flags;
if (depFlags & 64) {
run(dep, dep.flags = depFlags & -65);
}
link = link.nextDep;
}
}
function flush() {
while (notifyIndex < queuedEffectsLength) {
const effect = queuedEffects[notifyIndex];
queuedEffects[notifyIndex++] = undefined;
run(effect, effect.flags &= -65);
}
notifyIndex = 0;
queuedEffectsLength = 0;
}
function computedOper() {
const flags = this.flags;
if (flags & 16
|| (flags & 32 && checkDirty(this.deps, this))) {
if (updateComputed(this)) {
const subs = this.subs;
if (subs !== undefined) {
shallowPropagate(subs);
}
}
}
else if (flags & 32) {
this.flags = flags & -33;
}
if (activeSub !== undefined) {
link(this, activeSub);
}
else if (activeScope !== undefined) {
link(this, activeScope);
}
return this.value;
}
function signalOper(...value) {
if (value.length) {
const newValue = value[0];
if (this.value !== (this.value = newValue)) {
this.flags = 17;
const subs = this.subs;
if (subs !== undefined) {
propagate(subs);
if (!batchDepth) {
flush();
}
}
}
}
else {
const value = this.value;
if (this.flags & 16) {
if (updateSignal(this, value)) {
const subs = this.subs;
if (subs !== undefined) {
shallowPropagate(subs);
}
}
}
if (activeSub !== undefined) {
link(this, activeSub);
}
return value;
}
}
function effectOper() {
let dep = this.deps;
while (dep !== undefined) {
dep = unlink(dep, this);
}
const sub = this.subs;
if (sub !== undefined) {
unlink(sub);
}
this.flags = 0;
}
const defaults = { greedy: false };
const computed = value => new Computed(value);
const signal$1 = (value, { greedy = false } = defaults) => greedy ? new Greedy(value) : new Signal(signal$2, value);
/**
* @template T
* @param {function(): T} fn
* @returns {T}
*/
const untracked = fn => {
pauseTracking();
try { return fn() }
finally { resumeTracking(); }
};
/**
* @template T
*/
class Signal {
/**
* @param {(value: T) => T} fn
* @param {T} value
*/
constructor(fn, value) {
this._ = fn(value);
}
/** @returns {T} */
get value() {
return this._();
}
/** @param {T} value */
set value(value) {
this._(value);
}
/** @returns {T} */
peek() {
return untracked(this._);
}
/** @returns {T} */
valueOf() {
return this.value;
}
}
/**
* @template T
*/
class Computed extends Signal {
/** @param {T} value */
constructor(value) { super(computed$1, value); }
/** @returns {T} */
get value() { return this._() }
set value(_) { throw new Error('Computed values are read-only') }
}
class Greedy extends Signal {
constructor(value) { super(signal$2, [value]); }
get value() { return super.value[0] }
set value(value) { super.value = [value]; }
peek() { return super.peek()[0] }
}
const batch = fn => {
startBatch();
try { return fn() }
finally { endBatch(); }
};
let $ = signal$1;
function signal() {
return $.apply(null, arguments);
}
const _get$1 = () => $;
const _set$1 = fn => { $ = fn; };
const { isArray } = Array;
const { assign, defineProperties, entries, freeze} = Object;
class Unsafe {
#data;
constructor(data) {
this.#data = data;
}
valueOf() {
return this.#data;
}
toString() {
return String(this.#data);
}
}
const unsafe = data => new Unsafe(data);
const createComment = value => document.createComment(value);
/* c8 ignore stop */
// this is an essential ad-hoc DOM facade
const ELEMENT = 1;
const ATTRIBUTE$1 = 2;
const TEXT$1 = 3;
const COMMENT$1 = 8;
const DOCUMENT_TYPE = 10;
const FRAGMENT = 11;
const COMPONENT$1 = 42;
const TEXT_ELEMENTS = new Set([
'plaintext',
'script',
'style',
'textarea',
'title',
'xmp',
]);
const VOID_ELEMENTS = new Set([
'area',
'base',
'br',
'col',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'menuitem',
'meta',
'param',
'source',
'track',
'wbr',
]);
const props$1 = freeze({});
const children = freeze([]);
const append = (node, child) => {
if (node.children === children) node.children = [];
node.children.push(child);
child.parent = node;
return child;
};
const prop = (node, name, value) => {
if (node.props === props$1) node.props = {};
node.props[name] = value;
};
const addJSON = (value, comp, json) => {
if (value !== comp) json.push(value);
};
class Node {
constructor(type) {
this.type = type;
this.parent = null;
}
toJSON() {
//@ts-ignore
return [this.type, this.data];
}
}
class Comment extends Node {
constructor(data) {
super(COMMENT$1);
this.data = data;
}
toString() {
return `<!--${this.data}-->`;
}
}
class DocumentType extends Node {
constructor(data) {
super(DOCUMENT_TYPE);
this.data = data;
}
toString() {
return `<!${this.data}>`;
}
}
class Text extends Node {
constructor(data) {
super(TEXT$1);
this.data = data;
}
toString() {
return this.data;
}
}
class Component extends Node {
constructor() {
super(COMPONENT$1);
this.name = 'template';
this.props = props$1;
this.children = children;
}
toJSON() {
const json = [COMPONENT$1];
addJSON(this.props, props$1, json);
addJSON(this.children, children, json);
return json;
}
toString() {
let attrs = '';
for (const key in this.props) {
const value = this.props[key];
if (value != null) {
/* c8 ignore start */
if (typeof value === 'boolean') {
if (value) attrs += ` ${key}`;
}
else attrs += ` ${key}="${value}"`;
/* c8 ignore stop */
}
}
return `<template${attrs}>${this.children.join('')}</template>`;
}
}
class Element extends Node {
constructor(name, xml = false) {
super(ELEMENT);
this.name = name;
this.xml = xml;
this.props = props$1;
this.children = children;
}
toJSON() {
const json = [ELEMENT, this.name, +this.xml];
addJSON(this.props, props$1, json);
addJSON(this.children, children, json);
return json;
}
toString() {
const { xml, name, props, children } = this;
const { length } = children;
let html = `<${name}`;
for (const key in props) {
const value = props[key];
if (value != null) {
if (typeof value === 'boolean') {
if (value) html += xml ? ` ${key}=""` : ` ${key}`;
}
else html += ` ${key}="${value}"`;
}
}
if (length) {
html += '>';
for (let text = !xml && TEXT_ELEMENTS.has(name), i = 0; i < length; i++)
html += text ? children[i].data : children[i];
html += `</${name}>`;
}
else if (xml) html += ' />';
else html += VOID_ELEMENTS.has(name) ? '>' : `></${name}>`;
return html;
}
}
class Fragment extends Node {
constructor() {
super(FRAGMENT);
this.name = '#fragment';
this.children = children;
}
toJSON() {
const json = [FRAGMENT];
addJSON(this.children, children, json);
return json;
}
toString() {
return this.children.join('');
}
}
/* c8 ignore start */
const asTemplate = template => (template?.raw || template)?.join?.(',') || 'unknown';
/* c8 ignore stop */
var errors = {
text: (template, tag, value) => new SyntaxError(`Mixed text and interpolations found in text only <${tag}> element ${JSON.stringify(String(value))} in template ${asTemplate(template)}`),
unclosed: (template, tag) => new SyntaxError(`The text only <${tag}> element requires explicit </${tag}> closing tag in template ${asTemplate(template)}`),
unclosed_element: (template, tag) => new SyntaxError(`Unclosed element <${tag}> found in template ${asTemplate(template)}`),
invalid_content: template => new SyntaxError(`Invalid content "<!" found in template: ${asTemplate(template)}`),
invalid_closing: template => new SyntaxError(`Invalid closing tag: </... found in template: ${asTemplate(template)}`),
invalid_nul: template => new SyntaxError(`Invalid content: NUL char \\x00 found in template: ${asTemplate(template)}`),
invalid_comment: template => new SyntaxError(`Invalid comment: no closing --> found in template ${asTemplate(template)}`),
invalid_layout: template => new SyntaxError(`Too many closing tags found in template ${asTemplate(template)}`),
invalid_doctype: (template, value) => new SyntaxError(`Invalid doctype: ${value} found in template ${asTemplate(template)}`),
// DOM ONLY
/* c8 ignore start */
invalid_template: template => new SyntaxError(`Invalid template - the amount of values does not match the amount of updates: ${asTemplate(template)}`),
invalid_path: (template, path) => new SyntaxError(`Invalid path - unreachable node at the path [${path.join(', ')}] found in template ${asTemplate(template)}`),
invalid_attribute: (template, kind) => new SyntaxError(`Invalid ${kind} attribute in template definition\n${asTemplate(template)}`),
invalid_interpolation: (template, value) => new SyntaxError(`Invalid interpolation - expected hole or array: ${String(value)} found in template ${asTemplate(template)}`),
invalid_hole: value => new SyntaxError(`Invalid interpolation - expected hole: ${String(value)}`),
invalid_key: value => new SyntaxError(`Invalid key attribute or position in template: ${String(value)}`),
invalid_array: value => new SyntaxError(`Invalid array - expected html/svg but found something else: ${String(value)}`),
invalid_component: value => new SyntaxError(`Invalid component: ${String(value)}`),
};
//@ts-check
const NUL = '\x00';
const DOUBLE_QUOTED_NUL = `"${NUL}"`;
const SINGLE_QUOTED_NUL = `'${NUL}'`;
const NEXT = /\x00|<[^><\s]+/g;
const ATTRS = /([^\s/>=]+)(?:=(\x00|(?:(['"])[\s\S]*?\3)))?/g;
// // YAGNI: NUL char in the wild is a shenanigan
// // usage: template.map(safe).join(NUL).trim()
// const NUL_RE = /\x00/g;
// const safe = s => s.replace(NUL_RE, '�');
/** @typedef {import('../dom/ish.js').Node} Node */
/** @typedef {import('../dom/ish.js').Element} Element */
/** @typedef {import('../dom/ish.js').Component} Component */
/** @typedef {(node: import('../dom/ish.js').Node, type: typeof ATTRIBUTE | typeof TEXT | typeof COMMENT | typeof COMPONENT, path: number[], name: string, hint: unknown) => unknown} update */
/** @typedef {Element | Component} Container */
/** @type {update} */
const defaultUpdate = (_, type, path, name, hint) => [type, path, name];
/**
* @param {Node} node
* @returns {number[]}
*/
const path = node => {
const insideout = [];
while (node.parent) {
switch (node.type) {
/* c8 ignore start */
case COMPONENT$1:
// fallthrough
/* c8 ignore stop */
case ELEMENT: {
if (/** @type {Container} */(node).name === 'template') insideout.push(-1);
break;
}
}
insideout.push(node.parent.children.indexOf(node));
node = node.parent;
}
return insideout;
};
/**
* @param {Node} node
* @param {Set<Node>} ignore
* @returns {Node}
*/
const parent = (node, ignore) => {
do { node = node.parent; } while (ignore.has(node));
return node;
};
var parser = ({
Comment: Comment$1 = Comment,
DocumentType: DocumentType$1 = DocumentType,
Text: Text$1 = Text,
Fragment: Fragment$1 = Fragment,
Element: Element$1 = Element,
Component: Component$1 = Component,
update = defaultUpdate,
}) =>
/**
* Parse a template string into a crawable JS literal tree and provide a list of updates.
* @param {TemplateStringsArray|string[]} template
* @param {unknown[]} holes
* @param {boolean} xml
* @returns {[Node, unknown[]]}
*/
(template, holes, xml) => {
if (template.some(chunk => chunk.includes(NUL))) throw errors.invalid_nul(template);
const content = template.join(NUL).trim();
if (content.replace(/(\S+)=(['"])([\S\s]+?)\2/g, (...a) => /^[^\x00]+\x00|\x00[^\x00]+$/.test(a[3]) ? (xml = a[1]) : a[0]) !== content) throw errors.invalid_attribute(template, xml);
const ignore = new Set;
const values = [];
let node = new Fragment$1, pos = 0, skip = 0, hole = 0, resolvedPath = children;
for (const match of content.matchAll(NEXT)) {
// already handled via attributes or text content nodes
if (0 < skip) {
skip--;
continue;
}
const chunk = match[0];
const index = match.index;
// prepend previous content, if any
if (pos < index)
append(node, new Text$1(content.slice(pos, index)));
// holes
if (chunk === NUL) {
if (node.name === 'table') {
node = append(node, new Element$1('tbody', xml));
ignore.add(node);
}
const comment = append(node, new Comment$1('◦'));
values.push(update(comment, COMMENT$1, path(comment), '', holes[hole++]));
pos = index + 1;
}
// comments or doctype
else if (chunk.startsWith('<!')) {
const i = content.indexOf('>', index + 2);
if (i < 0) throw errors.invalid_content(template);
if (content.slice(i - 2, i + 1) === '-->') {
if ((i - index) < 6) throw errors.invalid_comment(template);
const data = content.slice(index + 4, i - 2);
if (data[0] === '!') append(node, new Comment$1(data.slice(1).replace(/!$/, '')));
}
else {
if (!content.slice(index + 2, i).toLowerCase().startsWith('doctype')) throw errors.invalid_doctype(template, content.slice(index + 2, i));
append(node, new DocumentType$1(content.slice(index + 2, i)));
}
pos = i + 1;
}
// closing tag </> or </name>
else if (chunk.startsWith('</')) {
const i = content.indexOf('>', index + 2);
if (i < 0) throw errors.invalid_closing(template);
if (xml && node.name === 'svg') xml = false;
node = /** @type {Container} */(parent(node, ignore));
if (!node) throw errors.invalid_layout(template);
pos = i + 1;
}
// opening tag <name> or <name />
else {
const i = index + chunk.length;
const j = content.indexOf('>', i);
const name = chunk.slice(1);
if (j < 0) throw errors.unclosed_element(template, name);
let tag = name;
// <${Component} ... />
if (name === NUL) {
tag = 'template';
node = append(node, new Component$1);
resolvedPath = path(node).slice(1);
//@ts-ignore
values.push(update(node, COMPONENT$1, resolvedPath, '', holes[hole++]));
}
// any other element
else {
if (!xml) {
tag = tag.toLowerCase();
// patch automatic elements insertion with <table>
// or path will fail once live on the DOM
if (node.name === 'table' && (tag === 'tr' || tag === 'td')) {
node = append(node, new Element$1('tbody', xml));
ignore.add(node);
}
if (node.name === 'tbody' && tag === 'td') {
node = append(node, new Element$1('tr', xml));
ignore.add(node);
}
}
node = append(node, new Element$1(tag, xml ? tag !== 'svg' : false));
resolvedPath = children;
}
// attributes
if (i < j) {
let dot = false;
for (const [_, name, value] of content.slice(i, j).matchAll(ATTRS)) {
if (value === NUL || value === DOUBLE_QUOTED_NUL || value === SINGLE_QUOTED_NUL || (dot = name.endsWith(NUL))) {
const p = resolvedPath === children ? (resolvedPath = path(node)) : resolvedPath;
//@ts-ignore
values.push(update(node, ATTRIBUTE$1, p, dot ? name.slice(0, -1) : name, holes[hole++]));
dot = false;
skip++;
}
else prop(node, name, value ? value.slice(1, -1) : true);
}
resolvedPath = children;
}
pos = j + 1;
// to handle self-closing tags
const closed = 0 < j && content[j - 1] === '/';
if (xml) {
if (closed) {
node = node.parent;
/* c8 ignore start unable to reproduce, still worth a guard */
if (!node) throw errors.invalid_layout(template);
/* c8 ignore stop */
}
}
else if (closed || VOID_ELEMENTS.has(tag)) {
// void elements are never td or tr
node = closed ? parent(node, ignore) : node.parent;
/* c8 ignore start unable to reproduce, still worth a guard */
if (!node) throw errors.invalid_layout();
/* c8 ignore stop */
}
// <svg> switches to xml mode
else if (tag === 'svg') xml = true;
// text content / data elements content handling
else if (TEXT_ELEMENTS.has(tag)) {
const index = content.indexOf(`</${name}>`, pos);
if (index < 0) throw errors.unclosed(template, tag);
const value = content.slice(pos, index);
// interpolation as text
if (value.trim() === NUL) {
skip++;
values.push(update(node, TEXT$1, path(node), '', holes[hole++]));
}
else if (value.includes(NUL)) throw errors.text(template, tag, value);
else append(node, new Text$1(value));
// text elements are never td or tr
node = node.parent;
/* c8 ignore start unable to reproduce, still worth a guard */
if (!node) throw errors.invalid_layout(template);
/* c8 ignore stop */
pos = index + name.length + 3;
// ignore the closing tag regardless of the content
skip++;
continue;
}
}
}
if (pos < content.length)
append(node, new Text$1(content.slice(pos)));
/* c8 ignore start */
if (hole < holes.length) throw errors.invalid_template(template);
/* c8 ignore stop */
return [node, values];
};
const tree = ((node, i) => i < 0 ? node?.content : node?.childNodes?.[i])
;
var resolve = (root, path) => path.reduceRight(tree, root);
//@ts-check
let checkType = false, range;
/**
* @param {DocumentFragment} fragment
* @returns {Node | Element}
*/
const drop = ({ firstChild, lastChild }) => {
const r = range || (range = document.createRange());
r.setStartAfter(firstChild);
r.setEndAfter(lastChild);
r.deleteContents();
//@ts-ignore
return firstChild;
};
/**
* @param {Node} node
* @param {1 | 0 | -0 | -1} operation
* @returns {Node}
*/
const diffFragment = (node, operation) => (
checkType && node.nodeType === 11 ?
((1 / operation) < 0 ?
//@ts-ignore
(operation ? drop(node) : node.lastChild) :
//@ts-ignore
(operation ? node.valueOf() : node.firstChild)) :
node
);
const nodes = Symbol('nodes');
const parentNode = { get() { return this.firstChild.parentNode } };
//@ts-ignore
const replaceWith = { value(node) { drop(this).replaceWith(node); } };
//@ts-ignore
const remove = { value() { drop(this).remove(); } };
const valueOf = {
value() {
const { parentNode } = this;
if (parentNode === this) {
if (this[nodes] === children)
this[nodes] = [...this.childNodes];
}
else {
// TODO: verify fragments in lists don't call this twice
if (parentNode) {
let { firstChild, lastChild } = this;
this[nodes] = [firstChild];
while (firstChild !== lastChild)
this[nodes].push((firstChild = firstChild.nextSibling));
}
this.replaceChildren(...this[nodes]);
}
return this;
}
};
/**
* @param {DocumentFragment} fragment
* @returns {DocumentFragment}
*/
function PersistentFragment(fragment) {
const firstChild = createComment('<>'), lastChild = createComment('</>');
//@ts-ignore
fragment.replaceChildren(firstChild, ...fragment.childNodes, lastChild);
checkType = true;
return defineProperties(fragment, {
[nodes]: { writable: true, value: children },
firstChild: { value: firstChild },
lastChild: { value: lastChild },
parentNode,
valueOf,
replaceWith,
remove,
});
}
PersistentFragment.prototype = DocumentFragment.prototype;
// @ts-check
/**
* @param {Document} document
* @returns
*/
var creator = (document = /** @type {Document} */(globalThis.document)) => {
let tpl = document.createElement('template'), range;
/**
* @param {string} content
* @param {boolean} [xml=false]
* @returns {DocumentFragment}
*/
return (content, xml = false) => {
if (xml) {
if (!range) {
range = document.createRange();
range.selectNodeContents(
document.createElementNS('http://www.w3.org/2000/svg', 'svg')
);
}
return range.createContextualFragment(content);
}
tpl.innerHTML = content;
const fragment = tpl.content;
tpl = /** @type {HTMLTemplateElement} */(tpl.cloneNode(false));
return fragment;
};
};
// @see https://github.com/WebReflection/udomdiff
/**
* ISC License
*
* Copyright (c) 2020, Andrea Giammarchi, @WebReflection
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
/**
* @param {Node[]} a The list of current/live children
* @param {Node[]} b The list of future children
* @param {(entry: Node, action: number) => Node} get
* The callback invoked per each entry related DOM operation.
* @param {Node} [before] The optional node used as anchor to insert before.
* @returns {Node[]} The same list of future children.
*/
var diff = (a, b, get, before) => {
const parentNode = before.parentNode;
const bLength = b.length;
let aEnd = a.length;
let bEnd = bLength;
let aStart = 0;
let bStart = 0;
let map = null;
while (aStart < aEnd || bStart < bEnd) {
// append head, tail, or nodes in between: fast path
if (aEnd === aStart) {
// we could be in a situation where the rest of nodes that
// need to be added are not at the end, and in such case
// the node to `insertBefore`, if the index is more than 0
// must be retrieved, otherwise it's gonna be the first item.
const node = bEnd < bLength ?
(bStart ?
(get(b[bStart - 1], -0).nextSibling) :
get(b[bEnd], 0)) :
before;
while (bStart < bEnd)
parentNode.insertBefore(get(b[bStart++], 1), node);
}
// remove head or tail: fast path
else if (bEnd === bStart) {
while (aStart < aEnd) {
// remove the node only if it's unknown or not live
if (!map || !map.has(a[aStart]))
//@ts-ignore
get(a[aStart], -1).remove();
aStart++;
}
}
// same node: fast path
else if (a[aStart] === b[bStart]) {
aStart++;
bStart++;
}
// same tail: fast path
else if (a[aEnd - 1] === b[bEnd - 1]) {
aEnd--;
bEnd--;
}
// The once here single last swap "fast path" has been removed in v1.1.0
// https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85
// reverse swap: also fast path
else if (
a[aStart] === b[bEnd - 1] &&
b[bStart] === a[aEnd - 1]
) {
// this is a "shrink" operation that could happen in these cases:
// [1, 2, 3, 4, 5]
// [1, 4, 3, 2, 5]
// or asymmetric too
// [1, 2, 3, 4, 5]
// [1, 2, 3, 5, 6, 4]
const node = get(a[--aEnd], -0).nextSibling;
parentNode.insertBefore(
get(b[bStart++], 1),
get(a[aStart++], -0).nextSibling
);
parentNode.insertBefore(get(b[--bEnd], 1), node);
// mark the future index as identical (yeah, it's dirty, but cheap 👍)
// The main reason to do this, is that when a[aEnd] will be reached,
// the loop will likely be on the fast path, as identical to b[bEnd].
// In the best case scenario, the next loop will skip the tail,
// but in the worst one, this node will be considered as already
// processed, bailing out pretty quickly from the map index check
a[aEnd] = b[bEnd];
}
// map based fallback, "slow" path
else {
// the map requires an O(bEnd - bStart) operation once
// to store all future nodes indexes for later purposes.
// In the worst case scenario, this is a full O(N) cost,
// and such scenario happens at least when all nodes are different,
// but also if both first and last items of the lists are different
if (!map) {
map = new Map;
let i = bStart;
while (i < bEnd)
map.set(b[i], i++);
}
const index = map.get(a[aStart]) ?? -1;
// this node has no meaning in the future list, so it's more than safe
// to remove it, and check the next live node out instead, meaning
// that only the live list index should be forwarded
//@ts-ignore
if (index < 0) get(a[aStart++], -1).remove();
// it's a future node, hence it needs some handling
else {
// if it's not already processed, look on demand for the next LCS
if (bStart < index && index < bEnd) {
let i = aStart;
// counts the amount of nodes that are the same in the future
let sequence = 1;
while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence))
sequence++;
// effort decision here: if the sequence is longer than replaces
// needed to reach such sequence, which would brings again this loop
// to the fast path, prepend the difference before a sequence,
// and move only the future list index forward, so that aStart
// and bStart will be aligned again, hence on the fast path.
// An example considering aStart and bStart are both 0:
// a: [1, 2, 3, 4]
// b: [7, 1, 2, 3, 6]
// this would place 7 before 1 and, from that time on, 1, 2, and 3
// will be processed at zero cost
if (sequence > (index - bStart)) {
const node = get(a[aStart], 0);
while (bStart < index)
parentNode.insertBefore(get(b[bStart++], 1), node);
}
// if the effort wasn't good enough, fallback to a replace,
// moving both source and target indexes forward, hoping that some
// similar node will be found later on, to go back to the fast path
else {
// TODO: benchmark replaceWith instead
parentNode.replaceChild(
get(b[bStart++], 1),
get(a[aStart++], -1)
);
}
}
// otherwise move the source forward, 'cause there's nothing to do
else
aStart++;
}
}
}
return b;
};
const ARRAY = 1 << 0;
const ARIA = 1 << 1;
const ATTRIBUTE = 1 << 2;
const COMMENT = 1 << 3;
const COMPONENT = 1 << 4;
const DATA = 1 << 5;
const DIRECT = 1 << 6;
const DOTS = 1 << 7;
const EVENT = 1 << 8;
const KEY = 1 << 9;
const PROP = 1 << 10;
const TEXT = 1 << 11;
const TOGGLE = 1 << 12;
const UNSAFE = 1 << 13;
const REF = 1 << 14;
const SIGNAL = 1 << 15;
// COMPONENT flags
const COMPONENT_DIRECT = COMPONENT | DIRECT;
const COMPONENT_DOTS = COMPONENT | DOTS;
const COMPONENT_PROP = COMPONENT | PROP;
const EVENT_ARRAY = EVENT | ARRAY;
const COMMENT_ARRAY = COMMENT | ARRAY;
const fragment = creator(document);
const ref = Symbol('ref');
const aria = (node, values) => {
for (const [key, value] of entries(values)) {
const name = key === 'role' ? key : `aria-${key.toLowerCase()}`;
if (value == null) node.removeAttribute(name);
else node.setAttribute(name, value);
}
};
const attribute = name => (node, value) => {
if (value == null) node.removeAttribute(name);
else node.setAttribute(name, value);
};
const comment_array = (node, value) => {
node[nodes] = diff(
node[nodes] || children,
value,
diffFragment,
node
);
};
const text = new WeakMap;
const getText = (ref, value) => {
let node = text.get(ref);
if (node) node.data = value;
else text.set(ref, (node = document.createTextNode(value)));
return node;
};
const comment_hole = (node, value) => {
const current = typeof value === 'object' ? (value ?? node) : getText(node, value);
const prev = node[nodes] ?? node;
if (current !== prev)
prev.replaceWith(diffFragment(node[nodes] = current, 1));
};
const comment_unsafe = xml => (node, value) => {
const prev = node[ref] ?? (node[ref] = {});
if (prev.v !== value) {
prev.f = PersistentFragment(fragment(value, xml));
prev.v = value;
}
comment_hole(node, prev.f);
};
const comment_signal = (node, value) => {
comment_hole(node, value instanceof Signal ? value.value : value);
};
const data = ({ dataset }, values) => {
for (const [key, value] of entries(values)) {
if (value == null) delete dataset[key];
else dataset[key] = value;
}
};
/** @type {Map<string|Symbol, Function>} */
const directRefs = new Map;
/**
* @param {string|Symbol} name
* @returns {Function}
*/
const directFor = name => {
let fn = directRefs.get(name);
if (!fn) directRefs.set(name, (fn = direct$1(name)));
return fn;
};
const direct$1 = name => (node, value) => {
node[name] = value;
};
const dots = (node, values) => {
for (const [name, value] of entries(values))
attribute(name)(node, value);
};
const event = (type, at, array) => array ?
((node, value) => {
const prev = node[at];
if (prev?.length) node.removeEventListener(type, ...prev);
if (value) node.addEventListener(type, ...value);
node[at] = value;
}) :
((node, value) => {
const prev = node[at];
if (prev) node.removeEventListener(type, prev);
if (value) node.addEventListener(type, value);
node[at] = value;
})
;
const toggle = name => (node, value) => {
node.toggleAttribute(name, !!value);
};
let k = false;
const isKeyed = () => {
const wasKeyed = k;
k = false;
return wasKeyed;
};
const update = (node, type, path, name, hint) => {
switch (type) {
case COMPONENT$1: return [path, hint, COMPONENT];
case COMMENT$1: {
if (isArray(hint)) return [path, comment_array, COMMENT_ARRAY];
if (hint instanceof Unsafe) return [path, comment_unsafe(node.xml), UNSAFE];
if (hint instanceof Signal) return [path, comment_signal, COMMENT | SIGNAL];
return [path, comment_hole, COMMENT];
}
case TEXT$1: return [path, directFor('textContent'), TEXT];
case ATTRIBUTE$1: {
const isComponent = node.type === COMPONENT$1;
switch (name.at(0)) {
case '@': {
if (isComponent) throw errors.invalid_attribute([], name);
const array = isArray(hint);
return [path, event(name.slice(1), Symbol(name), array), array ? EVENT_ARRAY : EVENT];
}
case '?':
if (isComponent) throw errors.invalid_attribute([], name);
return [path, toggle(name.slice(1)), TOGGLE];
case '.': {
return name === '...' ?
[path, isComponent ? assign : dots, isComponent ? COMPONENT_DOTS : DOTS] :
[path, direct$1(name.slice(1)), isComponent ? COMPONENT_DIRECT : DIRECT]
;
}
default: {
if (isComponent) return [path, direct$1(name), COMPONENT_PROP];
if (name === 'aria') return [path, aria, ARIA];
if (name === 'data' && !/^object$/i.test(node.name)) return [path, data, DATA];
if (name === 'key') {
if (1 < path.length) throw errors.invalid_key(hint);
return [path, (k = true), KEY];
} if (name === 'ref') return [path, directFor(ref), REF];
if (name.startsWith('on')) return [path, directFor(name.toLowerCase()), DIRECT];
return [path, attribute(name), ATTRIBUTE];
}
}
}
}
};
let direct = true;
/** @param {boolean} value */
const _set = value => {
direct = value;
};
/** @returns {boolean} */
const _get = () => direct;
//@ts-nocheck
/**
* @param {Hole} hole
* @returns
*/
const dom = hole => diffFragment(hole.n ? hole.update(hole) : hole.valueOf(false), 1);
const holed = (prev, current) => {
const changes = [], h = prev.length, l = current.length;
for (let c, p, j = 0, i = 0; i < l; i++) {
c = current[i];
changes[i] = j < h && (p = prev[j++]).t === c.t ? (current[i] = p).update(c) : c.valueOf(false);
}
return changes;
};
/**
* @param {Hole} hole
* @param {unknown} value
* @returns {Node}
*/
const keyed$1 = (hole, value) => /** @type {import('./keyed.js').Keyed} */(hole.t[2]).get(value)?.update(hole) ?? hole.valueOf(false);
/**
*
* @param {Function} Component
* @param {Object} obj
* @param {unknown[]} signals
* @returns {Hole}
*/
const component = (Component, obj, signals) => {
const signal = _get$1();
const length = signals.length;
let i = 0;
_set$1(/** @param {unknown} value */ value => i < length ? signals[i++] : (signals[i++] = value instanceof Signal ? value : signal(value)));
const wasDirect = _get();
if (wasDirect) _set(!wasDirect);
try { return Component(obj, global); }
finally {
if (wasDirect) _set(wasDirect);
_set$1(signal);
}
};
/**
* @param {Hole} hole
* @param {Hole} value
* @returns {Hole}
*/
const getHole = (hole, value) => {
if (hole.t === value.t) {
hole.update(value);
}
else {
hole.n.replaceWith(dom(value));
hole = value;
}
return hole;
};
const createEffect = (node, value, obj) => {
let signals = [], entry = [COMPONENT, null, obj], bootstrap = true, hole;
effect(() => {
if (bootstrap) {
bootstrap = false;
hole = component(value, obj, signals);
if (!signals.length) signals = children;
if (hole) {
node.replaceWith(dom(hole));
entry[1] = hole;
}
else node.remove();
}
else {
const result = component(value, obj, signals);
if (hole) {
if (!(result instanceof Hole)) throw errors.invalid_component(value);
if (getHole(hole, /** @type {Hole} */(result)) === result) entry[2] = (hole = result);
}
}
});
return entry;
};
const updateRefs = refs => {
for (const node of refs) {
const value = node[ref];
if (typeof value === 'function')
value(node);
else if (value instanceof Signal)
value.value = node;
else if (value)
value.current = node;
}
};
const props = Symbol();
const global = {};
class Hole {
/**
* @param {[DocumentFragment, unknown[], import('./keyed.js').Keyed?]} template
* @param {unknown[]} values
*/
constructor(template, values) {
this.t = template;
this.v = values;
this.n = null;
this.k = -1;
}
/**
* @param {boolean} [direct]
* @returns {Node}
*/
valueOf(direct = _get()) {
const [fragment, updates, keys] = this.t;
const root = document.importNode(fragment, true);
const values = this.v;
let length = values.length;
let changes = children;
let node, prev, refs;
if (length !== updates.length) throw errors.invalid_interpolation(this.t[3], values);
if (0 < length) {
changes = updates.slice(0);
while (length--) {
const [path, update, type] = updates[length];
const value = values[length];
if (prev !== path) {
node = resolve(root, path);
prev = path;
if (!node) throw errors.invalid_path(this.t[3], path);
}
if (type & COMPONENT) {
const obj = node[props] || (node[props] = {});
if (type === COMPONENT) {
for (const { name, value } of node.attributes) obj[name] ??= value;
obj.children ??= [...node.content.childNodes];
changes[length] = createEffect(node, value, obj);
}
else {
update(obj, value);
changes[length] = [type, update, obj];
}
}
else {
let commit = true;
if ((type & ARRAY) && !isArray(value)) throw errors.invalid_interpolation(this.t[3], value);
if (!direct && (type & COMMENT) && !(type & SI