diff --git a/build/files.js b/build/files.js
index fd26bb4..7f3737d 100644
--- a/build/files.js
+++ b/build/files.js
@@ -7,6 +7,14 @@ export default (target, plugins) => [
file: `./dist/${target}/parser.js`,
}
},
+ {
+ plugins,
+ input: './src/dom/node.js',
+ output: {
+ esModule: true,
+ file: `./dist/${target}/node.js`,
+ }
+ },
{
plugins,
input: './src/dom/cdn.js',
diff --git a/dist/dev/cdn.js b/dist/dev/cdn.js
deleted file mode 100644
index 91f7de0..0000000
--- a/dist/dev/cdn.js
+++ /dev/null
@@ -1,16 +0,0 @@
-const resolve = ({ protocol, host, pathname }) => {
- const dev = /[?&](?:dev|debug)(?:=|$)/.test(location.search);
- let path = pathname.replace(/\+\S*?$/, '');
- path = path.replace(/\/(?:auto|cdn)(?:\/|\.js\S*)$/, '/');
- path = path.replace(/\/(?:dist\/)?(?:dev|prod)\//, '/');
- return `${protocol}//${host}${path}dist/${dev ? 'dev' : 'prod'}/dom.js`;
-};
-
-const uhtml = Symbol.for('µhtml');
-
-const {
- render, html, svg,
- computed, signal, batch, effect, untracked,
-} = globalThis[uhtml] || (globalThis[uhtml] = await import(/* webpackIgnore: true */resolve(new URL(import.meta.url))));
-
-export { batch, computed, effect, html, render, signal, svg, untracked };
diff --git a/dist/dev/creator.js b/dist/dev/creator.js
deleted file mode 100644
index ac47cd4..0000000
--- a/dist/dev/creator.js
+++ /dev/null
@@ -1,31 +0,0 @@
-// @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;
- };
-};
-
-export { creator as default };
diff --git a/dist/dev/ish.js b/dist/dev/ish.js
deleted file mode 100644
index 7b1d835..0000000
--- a/dist/dev/ish.js
+++ /dev/null
@@ -1,236 +0,0 @@
-const { isArray } = Array;
-const { assign, freeze} = Object;
-/* c8 ignore stop */
-
-// this is an essential ad-hoc DOM facade
-
-
-const ELEMENT = 1;
-const ATTRIBUTE = 2;
-const TEXT = 3;
-const COMMENT = 8;
-const DOCUMENT_TYPE = 10;
-const FRAGMENT = 11;
-const COMPONENT = 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 = 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) node.props = {};
- node.props[name] = value;
-};
-
-const addJSON = (value, comp, json) => {
- if (value !== comp) json.push(value);
-};
-
-const setChildren = (node, json) => {
- node.children = json.map(revive, node);
-};
-
-const setJSON = (node, json, index) => {
- switch (json.length) {
- case index: setChildren(node, json[index - 1]);
- case index - 1: {
- const value = json[index - 2];
- if (isArray(value)) setChildren(node, value);
- else node.props = assign({}, value);
- }
- }
- return node;
-};
-
-function revive(json) {
- const node = fromJSON(json);
- node.parent = this;
- return node;
-}
-
-const fromJSON = json => {
- switch (json[0]) {
- case COMMENT: return new Comment(json[1]);
- case DOCUMENT_TYPE: return new DocumentType(json[1]);
- case TEXT: return new Text(json[1]);
- case COMPONENT: return setJSON(new Component, json, 3);
- case ELEMENT: return setJSON(new Element(json[1], !!json[2]), json, 5);
- case FRAGMENT: {
- const node = new Fragment;
- if (1 < json.length) node.children = json[1].map(revive, node);
- return node;
- }
- }
-};
-
-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);
- this.data = data;
- }
-
- toString() {
- return ``;
- }
-}
-
-class DocumentType extends Node {
- constructor(data) {
- super(DOCUMENT_TYPE);
- this.data = data;
- }
-
- toString() {
- return ``;
- }
-}
-
-class Text extends Node {
- constructor(data) {
- super(TEXT);
- this.data = data;
- }
-
- toString() {
- return this.data;
- }
-}
-
-class Component extends Node {
- constructor() {
- super(COMPONENT);
- this.name = 'template';
- this.props = props;
- this.children = children;
- }
-
- toJSON() {
- const json = [COMPONENT];
- addJSON(this.props, props, 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 `${this.children.join('')}`;
- }
-}
-
-class Element extends Node {
- constructor(name, xml = false) {
- super(ELEMENT);
- this.name = name;
- this.xml = xml;
- this.props = props;
- this.children = children;
- }
-
- toJSON() {
- const json = [ELEMENT, this.name, +this.xml];
- addJSON(this.props, props, 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('');
- }
-}
-
-export { ATTRIBUTE, COMMENT, COMPONENT, Comment, Component, DOCUMENT_TYPE, DocumentType, ELEMENT, Element, FRAGMENT, Fragment, Node, TEXT, TEXT_ELEMENTS, Text, VOID_ELEMENTS, append, children, fromJSON, prop, props };
diff --git a/dist/dev/json.js b/dist/dev/json.js
deleted file mode 100644
index f55401d..0000000
--- a/dist/dev/json.js
+++ /dev/null
@@ -1,675 +0,0 @@
-/* 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 " new SyntaxError(`Invalid closing tag: 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)}`),
-};
-
-const { isArray } = Array;
-const { assign, freeze, keys } = Object;
-/* 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 = 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) node.props = {};
- node.props[name] = value;
-};
-
-const addJSON = (value, comp, json) => {
- if (value !== comp) json.push(value);
-};
-
-const setChildren = (node, json) => {
- node.children = json.map(revive, node);
-};
-
-const setJSON = (node, json, index) => {
- switch (json.length) {
- case index: setChildren(node, json[index - 1]);
- case index - 1: {
- const value = json[index - 2];
- if (isArray(value)) setChildren(node, value);
- else node.props = assign({}, value);
- }
- }
- return node;
-};
-
-function revive(json) {
- const node = fromJSON(json);
- node.parent = this;
- return node;
-}
-
-const fromJSON = json => {
- switch (json[0]) {
- case COMMENT$1: return new Comment(json[1]);
- case DOCUMENT_TYPE: return new DocumentType(json[1]);
- case TEXT$1: return new Text(json[1]);
- case COMPONENT$1: return setJSON(new Component, json, 3);
- case ELEMENT: return setJSON(new Element(json[1], !!json[2]), json, 5);
- case FRAGMENT: {
- const node = new Fragment;
- if (1 < json.length) node.children = json[1].map(revive, node);
- return node;
- }
- }
-};
-
-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 ``;
- }
-}
-
-class DocumentType extends Node {
- constructor(data) {
- super(DOCUMENT_TYPE);
- this.data = data;
- }
-
- toString() {
- return ``;
- }
-}
-
-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;
- this.children = children;
- }
-
- toJSON() {
- const json = [COMPONENT$1];
- addJSON(this.props, props, 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 `${this.children.join('')}`;
- }
-}
-
-class Element extends Node {
- constructor(name, xml = false) {
- super(ELEMENT);
- this.name = name;
- this.xml = xml;
- this.props = props;
- this.children = children;
- }
-
- toJSON() {
- const json = [ELEMENT, this.name, +this.xml];
- addJSON(this.props, props, 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('');
- }
-}
-
-//@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} 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('', 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
- 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 or
- 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
- // 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 */
- }
- //