let nodeSerial = 1;
const NODE_TYPE = { element: 1, text: 3, comment: 8, fragment: 11 };
class Node {
get nextSibling() {
const siblings = this.parentNode ? this.parentNode.childNodes : null;
if (!siblings) return null;
const index = siblings.indexOf(this);
return index >= 0 && index + 1 < siblings.length ? siblings[index + 1] : null;
}
get firstChild() {
return this.childNodes.length > 0 ? this.childNodes[0] : null;
}
get lastChild() {
return this.childNodes.length > 0 ? this.childNodes[this.childNodes.length - 1] : null;
}
get nodeType() {
return NODE_TYPE[this.kind];
}
}
function baseNode(kind) {
return Object.assign(new Node(), {
__id: nodeSerial++,
kind,
parentNode: null,
childNodes: [],
appendChild(child) {
return this.insertBefore(child, null);
},
insertBefore(child, reference) {
if (child.kind === 'fragment') {
for (const grandchild of [...child.childNodes]) {
this.insertBefore(grandchild, reference);
}
child.childNodes.length = 0;
return child;
}
if (child.parentNode) child.parentNode.removeChild(child);
const index = reference === null ? this.childNodes.length : this.childNodes.indexOf(reference);
if (index < 0) throw new Error('insertBefore: reference node is not a child');
this.childNodes.splice(index, 0, child);
child.parentNode = this;
return child;
},
removeChild(child) {
const index = this.childNodes.indexOf(child);
if (index < 0) throw new Error('removeChild: node is not a child');
this.childNodes.splice(index, 1);
child.parentNode = null;
return child;
},
remove() {
if (this.parentNode) this.parentNode.removeChild(this);
},
replaceChildren(...nodes) {
for (const child of [...this.childNodes]) this.removeChild(child);
for (const node of nodes) this.appendChild(node);
},
cloneNode(deep = false) {
let copy;
if (this.kind === 'element') {
copy = createElement(this.tagName);
for (const [name, value] of Object.entries(this.attributes)) {
copy.setAttribute(name, value);
}
} else if (this.kind === 'text') {
copy = createTextNode(this.nodeValue);
} else if (this.kind === 'comment') {
copy = createComment(this.nodeValue);
} else {
copy = createDocumentFragment();
}
if (deep) {
for (const child of this.childNodes) copy.appendChild(child.cloneNode(true));
}
return copy;
},
isEqualNode(other) {
if (!other || this.kind !== other.kind) return false;
if (this.kind === 'text' || this.kind === 'comment') {
return this.nodeValue === other.nodeValue;
}
if (this.kind === 'element') {
if (this.tagName !== other.tagName) return false;
const names = Object.keys(this.attributes).sort();
const otherNames = Object.keys(other.attributes).sort();
if (names.length !== otherNames.length) return false;
for (let i = 0; i < names.length; i += 1) {
if (names[i] !== otherNames[i]) return false;
if (this.attributes[names[i]] !== other.attributes[names[i]]) return false;
}
}
if (this.childNodes.length !== other.childNodes.length) return false;
for (let i = 0; i < this.childNodes.length; i += 1) {
if (!this.childNodes[i].isEqualNode(other.childNodes[i])) return false;
}
return true;
},
});
}
const TEMPLATE_INNER_HTML = {
get() {
return serialize(this.content);
},
set(value) {
this.content = parseHtml(String(value));
},
};
const ELEMENT_INNER_HTML = {
get() {
return serialize(this);
},
set(value) {
const parsed = parseHtml(String(value));
for (const child of this.childNodes) child.parentNode = null;
this.childNodes = [];
for (const child of [...parsed.childNodes]) {
child.parentNode = this;
this.childNodes.push(child);
}
},
};
const VALUE_AS_NUMBER = {
get() {
if (this.attributes.type === 'date') {
const day = /^(\d{4})-(\d{2})-(\d{2})$/.exec(this.value);
return day ? Date.UTC(Number(day[1]), Number(day[2]) - 1, Number(day[3])) : NaN;
}
return this.value.trim() === '' ? NaN : Number(this.value);
},
set(number) {
if (Number.isNaN(number)) {
this.value = '';
} else if (this.attributes.type === 'date') {
this.value = new Date(number).toISOString().slice(0, 10);
} else {
this.value = String(number);
}
},
configurable: true,
};
function createElement(tag) {
const node = baseNode('element');
node.tagName = tag;
node.attributes = {};
node.listeners = {};
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
node.value = '';
node.checked = false;
}
if (tag === 'input') {
Object.defineProperty(node, 'valueAsNumber', VALUE_AS_NUMBER);
}
node.style = {
properties: {},
setProperty(name, value) {
this.properties[name] = value;
},
};
node.setAttribute = function (name, value) {
this.attributes[name] = String(value);
};
node.removeAttribute = function (name) {
delete this.attributes[name];
};
node.addEventListener = function (event, handler) {
(this.listeners[event] ??= []).push(handler);
};
node.fire = function (event, payload = {}) {
for (const handler of this.listeners[event] ?? []) {
handler({ target: this, ...payload });
}
};
if (tag === 'template') {
node.content = createDocumentFragment();
Object.defineProperty(node, 'innerHTML', TEMPLATE_INNER_HTML);
} else {
Object.defineProperty(node, 'innerHTML', ELEMENT_INNER_HTML);
}
return node;
}
const VOID_ELEMENTS = new Set([
'area', 'base', 'br', 'col', 'embed', 'hr', 'img',
'input', 'link', 'meta', 'param', 'source', 'track', 'wbr',
]);
function decodeEntities(text) {
return text
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/&/g, '&');
}
function parseStartTag(source, start) {
let i = start + 1;
let name = '';
while (i < source.length && /[A-Za-z0-9-]/.test(source[i])) name += source[i++];
if (name === '') throw new Error(`template HTML: expected a tag name at ${start}`);
const attributes = {};
for (;;) {
while (i < source.length && /\s/.test(source[i])) i += 1;
if (i >= source.length) throw new Error('template HTML: unterminated start tag');
if (source[i] === '/') {
i += 1;
continue;
}
if (source[i] === '>') {
i += 1;
break;
}
let attribute = '';
while (i < source.length && !/[\s=>/]/.test(source[i])) attribute += source[i++];
if (attribute === '') throw new Error(`template HTML: expected an attribute name at ${i}`);
let value = '';
while (i < source.length && /\s/.test(source[i])) i += 1;
if (source[i] === '=') {
i += 1;
while (i < source.length && /\s/.test(source[i])) i += 1;
const quote = source[i];
if (quote === '"' || quote === "'") {
i += 1;
const close = source.indexOf(quote, i);
if (close < 0) throw new Error('template HTML: unterminated attribute value');
value = decodeEntities(source.slice(i, close));
i = close + 1;
} else {
while (i < source.length && !/[\s>]/.test(source[i])) value += source[i++];
value = decodeEntities(value);
}
}
attributes[attribute] = value;
}
return { name, attributes, end: i };
}
function parseHtml(source) {
const root = createDocumentFragment();
const stack = [root];
const top = () => stack[stack.length - 1];
let i = 0;
const addText = (raw) => {
if (raw.length > 0) top().appendChild(createTextNode(decodeEntities(raw)));
};
while (i < source.length) {
const lt = source.indexOf('<', i);
if (lt < 0) {
addText(source.slice(i));
break;
}
addText(source.slice(i, lt));
if (source.startsWith('<!--', lt)) {
const close = source.indexOf('-->', lt + 4);
if (close < 0) throw new Error('template HTML: unterminated comment');
top().appendChild(createComment(source.slice(lt + 4, close)));
i = close + 3;
continue;
}
if (source.startsWith('</', lt)) {
const close = source.indexOf('>', lt);
if (close < 0) throw new Error('template HTML: unterminated end tag');
if (stack.length === 1) throw new Error('template HTML: end tag with no open element');
stack.pop();
i = close + 1;
continue;
}
const tag = parseStartTag(source, lt);
const element = createElement(tag.name);
for (const [name, value] of Object.entries(tag.attributes)) {
element.setAttribute(name, value);
}
top().appendChild(element);
if (!VOID_ELEMENTS.has(tag.name)) stack.push(element);
i = tag.end;
}
if (stack.length !== 1) throw new Error('template HTML: unclosed element');
return root;
}
function createTextNode(value) {
const node = baseNode('text');
node.nodeValue = String(value);
return node;
}
function createComment(value) {
const node = baseNode('comment');
node.nodeValue = String(value);
return node;
}
function createDocumentFragment() {
const node = baseNode('fragment');
node.append = function (...children) {
for (const child of children) this.appendChild(child);
};
return node;
}
const documentListeners = {};
const document = {
createElement,
createTextNode,
createComment,
createDocumentFragment,
addEventListener(event, handler) {
(documentListeners[event] ??= []).push(handler);
},
removeEventListener(event, handler) {
const registered = documentListeners[event];
if (!registered) return;
const at = registered.indexOf(handler);
if (at !== -1) registered.splice(at, 1);
},
listenerCount(event) {
return (documentListeners[event] ?? []).length;
},
fire(event, payload = {}) {
for (const handler of (documentListeners[event] ?? []).slice()) {
handler({ type: event, target: null, ...payload });
}
},
};
function html(node) {
if (node.kind === 'text') return node.nodeValue;
if (node.kind === 'comment') return '';
const inner = node.childNodes.map(html).join('');
if (node.kind === 'fragment') return inner;
const attrs = Object.entries(node.attributes)
.map(([k, v]) => (v === '' ? ` ${k}` : ` ${k}="${v}"`))
.join('');
return `<${node.tagName}${attrs}>${inner}</${node.tagName}>`;
}
function serialize(node) {
if (node.kind === 'text') return node.nodeValue;
if (node.kind === 'comment') return `<!--${node.nodeValue}-->`;
const inner = node.childNodes.map(serialize).join('');
if (node.kind === 'fragment') return inner;
const attrs = Object.entries(node.attributes)
.map(([k, v]) => (v === '' ? ` ${k}` : ` ${k}="${v}"`))
.join('');
let state = '';
if ('value' in node) state += ` .value="${node.value}"`;
if ('checked' in node && node.checked) state += ' .checked';
return `<${node.tagName}${attrs}${state}>${inner}</${node.tagName}>`;
}
function walk(node, out = []) {
if (node.kind === 'element') out.push(node);
for (const child of node.childNodes) walk(child, out);
return out;
}
function findTag(node, tagName) {
return walk(node).find((n) => n.tagName === tagName) ?? null;
}
var reported = [];
var reportError = function (error) {
reported.push(error);
};