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 += ``; - } - else if (xml) html += ' />'; - else html += VOID_ELEMENTS.has(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 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 += ``; - } - else if (xml) html += ' />'; - else html += VOID_ELEMENTS.has(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('', 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 */ - } - // 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(``, 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 : node?.children?.[i]) -; - -var resolve = (root, path) => path.reduceRight(tree, root); - -const get = node => { - if (node.props === props) node.props = {}; - return node.props; -}; - -const set = (props, name, value) => { - if (value == null) delete props[name]; - else props[name] = value; -}; - -const ARIA = 0; -const aria = (node, values) => { - const props = get(node); - for (const key in values) { - const name = key === 'role' ? key : `aria-${key}`; - const value = values[key]; - set(props, name, value); - } - if (keys(props).length === 0) node.props = props; -}; - -const ATTRIBUTE = 1; -const attribute = name => (node, value) => { - const props = get(node); - set(props, name, value); - if (keys(props).length === 0) node.props = props; -}; - -const COMMENT = 2; -const comment = (node, value) => { - const { children } = node.parent; - const i = children.indexOf(node); - if (isArray(value)) { - const fragment = new Fragment; - fragment.children = value; - value = fragment; - } - else if (!(value instanceof Node)) value = new Text(value == null ? '' : value); - children[i] = value; -}; - -const COMPONENT = 3; -const component = (node, value) => [node, value]; - -const DATA = 4; -const data = (node, values) => { - const props = get(node); - for (const key in values) { - const name = `data-${key}`; - const value = values[key]; - set(props, name, value); - } - if (keys(props).length === 0) node.props = props; -}; - -const DIRECT = 5; -const direct = name => (node, value) => { - const props = get(node); - set(props, name, value); - if (keys(props).length === 0) node.props = props; -}; - -const DOTS = 6; -const dots = isComponent => (node, value) => { -}; - -const EVENT = 7; -const event = at => (node, value) => { - const props = get(node); - if (value == null) delete props[at]; - else props[at] = value; -}; - -const KEY = 8; - -const TEXT = 9; -const text = (node, value) => { - if (value == null) node.children = children; - else node.children = [new Text(value)]; -}; - -const TOGGLE = 10; -const toggle = name => (node, value) => { - const props = get(node); - if (!value) { - delete props[name]; - if (keys(props).length === 0) node.props = props; - } - else props[name] = !!value; -}; - -const update = (node, type, path, name) => { - switch (type) { - case COMPONENT$1: { - return [path, component, COMPONENT]; - } - case COMMENT$1: { - return [path, comment, COMMENT]; - } - case ATTRIBUTE$1: { - switch (name.at(0)) { - case '@': return [path, event(Symbol(name)), EVENT]; - case '?': return [path, toggle(name.slice(1)), TOGGLE]; - case '.': return name === '...' ? - [path, dots(node.type === COMPONENT$1), DOTS] : - [path, direct(name.slice(1)), DIRECT] - ; - case 'a': if (name === 'aria') return [path, aria, ARIA]; - case 'd': if (name === 'data') return [path, data, DATA]; - case 'k': if (name === 'key') return [path, Object, KEY]; - default: return [path, attribute(name), ATTRIBUTE]; - } - } - case TEXT$1: return [path, text, TEXT]; - } -}; - -const textParser = parser({ - Comment, - DocumentType, - Text, - Fragment, - Element, - Component, - update, -}); - -const { parse, stringify } = JSON; - -const create = xml => { - const twm = new WeakMap; - const cache = (template, values) => { - const parsed = textParser(template, values, xml); - parsed[0] = parse(stringify(parsed[0])); - twm.set(template, parsed); - return parsed; - }; - return (template, ...values) => { - const [json, updates] = twm.get(template) || cache(template, values); - const root = fromJSON(json); - const length = values.length; - if (length === updates.length) { - const components = []; - for (let node, prev, i = 0; i < length; i++) { - const [path, update, type] = updates[i]; - const value = values[i]; - if (prev !== path) { - node = resolve(root, path); - prev = path; - if (!node) throw errors.invalid_path(path); - } - if (type === KEY) continue; - if (type === COMPONENT) components.push(update(node, value)); - else update(node, value); - } - for (const [node, Component] of components) { - const props = assign({ children: node.children }, node.props); - comment(node, Component(props)); - } - } - else throw errors.invalid_template(); - return root; - }; -}; - -const html = create(false); -const svg = create(true); - -export { html, svg }; diff --git a/dist/dev/parser.js b/dist/dev/parser.js deleted file mode 100644 index 4163343..0000000 --- a/dist/dev/parser.js +++ /dev/null @@ -1,464 +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 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 { 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); -}; - -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 += ``; - } - else if (xml) html += ' />'; - else html += VOID_ELEMENTS.has(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: - // 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 index = ({ - 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, 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('', 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, 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, 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 */ - } - // 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(``, 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, 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]; -}; - -export { index as default }; diff --git a/dist/prod/cdn.js b/dist/prod/cdn.js deleted file mode 100644 index 70a57ea..0000000 --- a/dist/prod/cdn.js +++ /dev/null @@ -1 +0,0 @@ -const e=Symbol.for("µhtml"),{render:t,html:o,svg:r,computed:a,signal:l,batch:c,effect:d,untracked:s}=globalThis[e]||(globalThis[e]=await import((({protocol:e,host:t,pathname:o})=>{const r=/[?&](?:dev|debug)(?:=|$)/.test(location.search);let a=o.replace(/\+\S*?$/,"");return a=a.replace(/\/(?:auto|cdn)(?:\/|\.js\S*)$/,"/"),a=a.replace(/\/(?:dist\/)?(?:dev|prod)\//,"/"),`${e}//${t}${a}dist/${r?"dev":"prod"}/dom.js`})(new URL(import.meta.url))));export{c as batch,a as computed,d as effect,o as html,t as render,l as signal,r as svg,s as untracked}; diff --git a/dist/prod/creator.js b/dist/prod/creator.js deleted file mode 100644 index 8b1d1df..0000000 --- a/dist/prod/creator.js +++ /dev/null @@ -1 +0,0 @@ -var e=(e=globalThis.document)=>{let t,n=e.createElement("template");return(r,a=!1)=>{if(a)return t||(t=e.createRange(),t.selectNodeContents(e.createElementNS("http://www.w3.org/2000/svg","svg"))),t.createContextualFragment(r);n.innerHTML=r;const o=n.content;return n=n.cloneNode(!1),o}};export{e as default}; diff --git a/dist/prod/ish.js b/dist/prod/ish.js deleted file mode 100644 index c5b6fac..0000000 --- a/dist/prod/ish.js +++ /dev/null @@ -1 +0,0 @@ -const{isArray:t}=Array,{assign:e,freeze:s}=Object,r=1,n=2,i=3,c=8,o=10,a=11,h=42,p=new Set(["plaintext","script","style","textarea","title","xmp"]),l=new Set(["area","base","br","col","embed","hr","img","input","keygen","link","menuitem","meta","param","source","track","wbr"]),u=s({}),d=s([]),m=(t,e)=>(t.children===d&&(t.children=[]),t.children.push(e),e.parent=t,e),x=(t,e,s)=>{t.props===u&&(t.props={}),t.props[e]=s},$=(t,e,s)=>{t!==e&&s.push(t)},g=(t,e)=>{t.children=e.map(w,t)},S=(s,r,n)=>{switch(r.length){case n:g(s,r[n-1]);case n-1:{const i=r[n-2];t(i)?g(s,i):s.props=e({},i)}}return s};function w(t){const e=f(t);return e.parent=this,e}const f=t=>{switch(t[0]){case 8:return new b(t[1]);case 10:return new O(t[1]);case 3:return new J(t[1]);case 42:return S(new N,t,3);case 1:return S(new j(t[1],!!t[2]),t,5);case 11:{const e=new k;return 1`}}class J extends y{constructor(t){super(3),this.data=t}toString(){return this.data}}class N extends y{constructor(){super(42),this.name="template",this.props=u,this.children=d}toJSON(){const t=[42];return $(this.props,u,t),$(this.children,d,t),t}toString(){let t="";for(const e in this.props){const s=this.props[e];null!=s&&("boolean"==typeof s?s&&(t+=` ${e}`):t+=` ${e}="${s}"`)}return`${this.children.join("")}`}}class j extends y{constructor(t,e=!1){super(1),this.name=t,this.xml=e,this.props=u,this.children=d}toJSON(){const t=[1,this.name,+this.xml];return $(this.props,u,t),$(this.children,d,t),t}toString(){const{xml:t,name:e,props:s,children:r}=this,{length:n}=r;let i=`<${e}`;for(const e in s){const r=s[e];null!=r&&("boolean"==typeof r?r&&(i+=t?` ${e}=""`:` ${e}`):i+=` ${e}="${r}"`)}if(n){i+=">";for(let s=!t&&p.has(e),c=0;c`}else i+=t?" />":l.has(e)?">":`>`;return i}}class k extends y{constructor(){super(11),this.name="#fragment",this.children=d}toJSON(){const t=[11];return $(this.children,d,t),t}toString(){return this.children.join("")}}export{n as ATTRIBUTE,c as COMMENT,h as COMPONENT,b as Comment,N as Component,o as DOCUMENT_TYPE,O as DocumentType,r as ELEMENT,j as Element,a as FRAGMENT,k as Fragment,y as Node,i as TEXT,p as TEXT_ELEMENTS,J as Text,l as VOID_ELEMENTS,m as append,d as children,f as fromJSON,x as prop,u as props}; diff --git a/dist/prod/json.js b/dist/prod/json.js deleted file mode 100644 index 809dc67..0000000 --- a/dist/prod/json.js +++ /dev/null @@ -1 +0,0 @@ -const{isArray:e}=Array,{assign:t,freeze:n,keys:s}=Object,r=42,c=new Set(["plaintext","script","style","textarea","title","xmp"]),o=new Set(["area","base","br","col","embed","hr","img","input","keygen","link","menuitem","meta","param","source","track","wbr"]),i=n({}),a=n([]),l=(e,t)=>(e.children===a&&(e.children=[]),e.children.push(t),t.parent=e,t),p=(e,t,n)=>{e.props===i&&(e.props={}),e.props[t]=n},h=(e,t,n)=>{e!==t&&n.push(e)},u=(e,t)=>{e.children=t.map(m,e)},d=(n,s,r)=>{switch(s.length){case r:u(n,s[r-1]);case r-1:{const c=s[r-2];e(c)?u(n,c):n.props=t({},c)}}return n};function m(e){const t=f(e);return t.parent=this,t}const f=e=>{switch(e[0]){case 8:return new w(e[1]);case 10:return new x(e[1]);case 3:return new $(e[1]);case r:return d(new y,e,3);case 1:return d(new S(e[1],!!e[2]),e,5);case 11:{const t=new b;return 1`}}class $ extends g{constructor(e){super(3),this.data=e}toString(){return this.data}}class y extends g{constructor(){super(r),this.name="template",this.props=i,this.children=a}toJSON(){const e=[r];return h(this.props,i,e),h(this.children,a,e),e}toString(){let e="";for(const t in this.props){const n=this.props[t];null!=n&&("boolean"==typeof n?n&&(e+=` ${t}`):e+=` ${t}="${n}"`)}return`${this.children.join("")}`}}class S extends g{constructor(e,t=!1){super(1),this.name=e,this.xml=t,this.props=i,this.children=a}toJSON(){const e=[1,this.name,+this.xml];return h(this.props,i,e),h(this.children,a,e),e}toString(){const{xml:e,name:t,props:n,children:s}=this,{length:r}=s;let i=`<${t}`;for(const t in n){const s=n[t];null!=s&&("boolean"==typeof s?s&&(i+=e?` ${t}=""`:` ${t}`):i+=` ${t}="${s}"`)}if(r){i+=">";for(let n=!e&&c.has(t),o=0;o`}else i+=e?" />":o.has(t)?">":`>`;return i}}class b extends g{constructor(){super(11),this.name="#fragment",this.children=a}toJSON(){const e=[11];return h(this.children,a,e),e}toString(){return this.children.join("")}}const O="\0",k=`"${O}"`,j=`'${O}'`,v=/\x00|<[^><\s]+/g,C=/([^\s/>=]+)(?:=(\x00|(?:(['"])[\s\S]*?\3)))?/g,J=(e,t,n,s,r)=>[t,n,s],N=e=>{const t=[];for(;e.parent;){switch(e.type){case r:case 1:"template"===e.name&&t.push(-1)}t.push(e.parent.children.indexOf(e)),e=e.parent}return t},A=(e,t)=>{do{e=e.parent}while(t.has(e));return e};const T=(e,t)=>t<0?e:e.children[t];var W=(e,t)=>t.reduceRight(T,e);const D=e=>(e.props===i&&(e.props={}),e.props),E=(e,t,n)=>{null==n?delete e[t]:e[t]=n},F=(e,t)=>{const n=D(e);for(const e in t){const s="role"===e?e:`aria-${e}`,r=t[e];E(n,s,r)}0===s(n).length&&(e.props=n)},z=e=>(t,n)=>{const r=D(t);E(r,e,n),0===s(r).length&&(t.props=r)},L=(t,n)=>{const{children:s}=t.parent,r=s.indexOf(t);if(e(n)){const e=new b;e.children=n,n=e}else n instanceof g||(n=new $(null==n?"":n));s[r]=n},M=(e,t)=>[e,t],R=(e,t)=>{const n=D(e);for(const e in t){const s=`data-${e}`,r=t[e];E(n,s,r)}0===s(n).length&&(e.props=n)},q=e=>(t,n)=>{const r=D(t);E(r,e,n),0===s(r).length&&(t.props=r)},B=(e,t)=>{e.children=null==t?a:[new $(t)]},G=e=>(t,n)=>{const r=D(t);n?r[e]=!!n:(delete r[e],0===s(r).length&&(t.props=r))},H=(({Comment:e=w,DocumentType:t=x,Text:n=$,Fragment:s=b,Element:i=S,Component:h=y,update:u=J})=>(d,m,f)=>{const g=d.join(O).trim(),w=new Set,x=[];let $=new s,y=0,S=0,b=0,J=a;for(const s of g.matchAll(v)){if(0",v+2);if("--\x3e"===g.slice(n-2,n+1)){const t=g.slice(v+4,n-2);"!"===t[0]&&l($,new e(t.slice(1).replace(/!$/,"")))}else l($,new t(g.slice(v+2,n)));y=n+1}else if(d.startsWith("",v+2);f&&"svg"===$.name&&(f=!1),$=A($,w),y=e+1}else{const e=v+d.length,t=g.indexOf(">",e),s=d.slice(1);let T=s;if(s===O?(T="template",$=l($,new h),J=N($).slice(1),x.push(u($,r,J,"",m[b++]))):(f||(T=T.toLowerCase(),"table"!==$.name||"tr"!==T&&"td"!==T||($=l($,new i("tbody",f)),w.add($)),"tbody"===$.name&&"td"===T&&($=l($,new i("tr",f)),w.add($))),$=l($,new i(T,!!f&&"svg"!==T)),J=a),e`,y),t=g.slice(y,e);t.trim()===O?(S++,x.push(u($,3,N($),"",m[b++]))):l($,new n(t)),$=$.parent,y=e+s.length+3,S++;continue}}}return y{switch(t){case r:return[n,M,3];case 8:return[n,L,2];case 2:switch(s.at(0)){case"@":return[n,(c=Symbol(s),(e,t)=>{const n=D(e);null==t?delete n[c]:n[c]=t}),7];case"?":return[n,G(s.slice(1)),10];case".":return"..."===s?[n,(e.type,(e,t)=>{}),6]:[n,q(s.slice(1)),5];case"a":if("aria"===s)return[n,F,0];case"d":if("data"===s)return[n,R,4];case"k":if("key"===s)return[n,Object,8];default:return[n,z(s),1]}case 3:return[n,B,9]}var c}}),{parse:I,stringify:K}=JSON,P=e=>{const n=new WeakMap;return(s,...r)=>{const[c,o]=n.get(s)||((t,s)=>{const r=H(t,s,e);return r[0]=I(K(r[0])),n.set(t,r),r})(s,r),i=f(c),a=r.length;if(a===o.length){const e=[];for(let t,n,s=0;s(t.children===r&&(t.children=[]),t.children.push(e),e.parent=t,e),o=(t,e,s)=>{t.props===n&&(t.props={}),t.props[e]=s},c=(t,e,s)=>{t!==e&&s.push(t)};class a{constructor(t){this.type=t,this.parent=null}toJSON(){return[this.type,this.data]}}class l extends a{constructor(t){super(8),this.data=t}toString(){return`\x3c!--${this.data}--\x3e`}}class h extends a{constructor(t){super(10),this.data=t}toString(){return``}}class p extends a{constructor(t){super(3),this.data=t}toString(){return this.data}}class d extends a{constructor(){super(42),this.name="template",this.props=n,this.children=r}toJSON(){const t=[42];return c(this.props,n,t),c(this.children,r,t),t}toString(){let t="";for(const e in this.props){const s=this.props[e];null!=s&&("boolean"==typeof s?s&&(t+=` ${e}`):t+=` ${e}="${s}"`)}return`${this.children.join("")}`}}class u extends a{constructor(t,e=!1){super(1),this.name=t,this.xml=e,this.props=n,this.children=r}toJSON(){const t=[1,this.name,+this.xml];return c(this.props,n,t),c(this.children,r,t),t}toString(){const{xml:t,name:n,props:r,children:i}=this,{length:o}=i;let c=`<${n}`;for(const e in r){const s=r[e];null!=s&&("boolean"==typeof s?s&&(c+=t?` ${e}=""`:` ${e}`):c+=` ${e}="${s}"`)}if(o){c+=">";for(let s=!t&&e.has(n),r=0;r`}else c+=t?" />":s.has(n)?">":`>`;return c}}class m extends a{constructor(){super(11),this.name="#fragment",this.children=r}toJSON(){const t=[11];return c(this.children,r,t),t}toString(){return this.children.join("")}}const f="\0",x=`"${f}"`,g=`'${f}'`,w=/\x00|<[^><\s]+/g,$=/([^\s/>=]+)(?:=(\x00|(?:(['"])[\s\S]*?\3)))?/g,S=(t,e,s,n,r)=>[e,s,n],b=t=>{const e=[];for(;t.parent;){switch(t.type){case 42:case 1:"template"===t.name&&e.push(-1)}e.push(t.parent.children.indexOf(t)),t=t.parent}return e},y=(t,e)=>{do{t=t.parent}while(e.has(t));return t};var O=({Comment:t=l,DocumentType:n=h,Text:c=p,Fragment:a=m,Element:O=u,Component:j=d,update:v=S})=>(l,h,p)=>{const d=l.join(f).trim(),u=new Set,m=[];let S=new a,J=0,N=0,k=0,C=r;for(const a of d.matchAll(w)){if(0",w+2);if("--\x3e"===d.slice(e-2,e+1)){const s=d.slice(w+4,e-2);"!"===s[0]&&i(S,new t(s.slice(1).replace(/!$/,"")))}else i(S,new n(d.slice(w+2,e)));J=e+1}else if(l.startsWith("",w+2);p&&"svg"===S.name&&(p=!1),S=y(S,u),J=t+1}else{const t=w+l.length,n=d.indexOf(">",t),a=l.slice(1);let W=a;if(a===f?(W="template",S=i(S,new j),C=b(S).slice(1),m.push(v(S,42,C,"",h[k++]))):(p||(W=W.toLowerCase(),"table"!==S.name||"tr"!==W&&"td"!==W||(S=i(S,new O("tbody",p)),u.add(S)),"tbody"===S.name&&"td"===W&&(S=i(S,new O("tr",p)),u.add(S))),S=i(S,new O(W,!!p&&"svg"!==W)),C=r),t`,J),e=d.slice(J,t);e.trim()===f?(N++,m.push(v(S,3,b(S),"",h[k++]))):i(S,new c(e)),S=S.parent,J=t+a.length+3,N++;continue}}}return J=0.10.0" } }, + "node_modules/dom-cue": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/dom-cue/-/dom-cue-0.2.8.tgz", + "integrity": "sha512-GKk92iCOC2KsfpK8piWN+7gkMWI4ifMYNOV/tQYYGZdBh8aim2XP3//fpoj29RlDyMMKq+NuBHHS0GqV719qRg==", + "license": "MIT" + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", diff --git a/package.json b/package.json index 381149e..a062ac4 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { - "name": "uhtml", - "version": "5.0.9", + "name": "@webreflection/uhtml", + "version": "0.1.4", "type": "module", "scripts": { "build": "npm run types && npm run build:js", - "build:js": "rm -rf dist && npm test && npm run build:prod && npm run build:dev && npm run size", + "build:js": "rm -rf dist && npm run build:prod && npm run build:dev && npm run size", "build:dev": "sed -i 's/false/true/' src/debug.js && rollup -c build/dev.js", "build:prod": "sed -i 's/true/false/' src/debug.js && rollup -c build/prod.js", "coverage": "mkdir -p ./coverage; c8 report --reporter=text-lcov > ./coverage/lcov.info", - "size": "echo \"dom\t\t$(cat dist/prod/dom.js | brotli | wc -c)\"; echo \"json\t\t$(cat dist/prod/json.js | brotli | wc -c)\"; echo \"parser\t\t$(cat dist/prod/parser.js | brotli | wc -c)\"", + "size": "echo \"node\t\t$(cat dist/prod/node.js | brotli | wc -c)\"; echo \"dom\t\t$(cat dist/prod/dom.js | brotli | wc -c)\"; echo \"json\t\t$(cat dist/prod/json.js | brotli | wc -c)\"; echo \"parser\t\t$(cat dist/prod/parser.js | brotli | wc -c)\"", "test": "c8 node test/parser.js", "test:json": "node test/json.js", "test:all": "npm run test:json && npm run test", @@ -106,6 +106,7 @@ }, "homepage": "https://github.com/WebReflection/uhtml#readme", "dependencies": { - "@webreflection/alien-signals": "^0.3.2" + "@webreflection/alien-signals": "^0.3.2", + "dom-cue": "^0.2.8" } } diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..7786453 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,24 @@ +export const ARRAY = 1 << 0; +export const ARIA = 1 << 1; +export const ATTRIBUTE = 1 << 2; +export const COMMENT = 1 << 3; +export const COMPONENT = 1 << 4; +export const DATA = 1 << 5; +export const DIRECT = 1 << 6; +export const DOTS = 1 << 7; +export const EVENT = 1 << 8; +export const KEY = 1 << 9; +export const PROP = 1 << 10; +export const TEXT = 1 << 11; +export const TOGGLE = 1 << 12; +export const UNSAFE = 1 << 13; +export const REF = 1 << 14; + +// COMPONENT flags +export const COMPONENT_DIRECT = COMPONENT | DIRECT; +export const COMPONENT_DOTS = COMPONENT | DOTS; +export const COMPONENT_PROP = COMPONENT | PROP; + +// ARRAY flags +export const EVENT_ARRAY = EVENT | ARRAY; +export const COMMENT_ARRAY = COMMENT | ARRAY; diff --git a/src/dom/cdn.js b/src/dom/cdn.js index 413245d..9c21a1f 100644 --- a/src/dom/cdn.js +++ b/src/dom/cdn.js @@ -9,11 +9,9 @@ const resolve = ({ protocol, host, pathname }) => { const uhtml = Symbol.for('µhtml'); const { - render, html, svg, - computed, signal, batch, effect, untracked, + render, html, svg, unsafe, } = globalThis[uhtml] || (globalThis[uhtml] = await import(/* webpackIgnore: true */resolve(new URL(import.meta.url)))); export { - render, html, svg, - computed, signal, batch, effect, untracked, + render, html, svg, unsafe, }; diff --git a/src/dom/creator.js b/src/dom/creator.js index 1d4efad..ee9ac64 100644 --- a/src/dom/creator.js +++ b/src/dom/creator.js @@ -12,18 +12,18 @@ export default (document = /** @type {Document} */(globalThis.document)) => { * @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); + if (!xml) { + tpl.innerHTML = content; + const fragment = tpl.content; + tpl = /** @type {HTMLTemplateElement} */(tpl.cloneNode(false)); + return fragment; } - tpl.innerHTML = content; - const fragment = tpl.content; - tpl = /** @type {HTMLTemplateElement} */(tpl.cloneNode(false)); - return fragment; + if (!range) { + range = document.createRange(); + range.selectNodeContents( + document.createElementNS('http://www.w3.org/2000/svg', 'svg') + ); + } + return range.createContextualFragment(content); }; }; diff --git a/src/dom/index.js b/src/dom/index.js index 99cdfab..b72064a 100644 --- a/src/dom/index.js +++ b/src/dom/index.js @@ -2,10 +2,6 @@ import DEBUG from '../debug.js'; -//@ts-ignore -import { effectScope } from '@webreflection/alien-signals'; -export { signal, computed, effect, untracked, batch } from './signals.js'; - import { Comment, DocumentType, @@ -38,6 +34,25 @@ const parse = parser({ update, }); +/** + * @param {boolean} xml + * @param {WeakMap} twm + * @param {TemplateStringsArray | string[]} template + * @param {unknown[]} values + * @returns + */ +const set = (xml, twm, template, values) => { + const parsed = parse(template, values, xml); + //@ts-ignore + parsed.push(isKeyed() ? new Keyed : null); + //@ts-ignore + if (DEBUG) parsed.push(template); + //@ts-ignore + parsed[0] = fragment(parsed[0].toString(), xml); + twm.set(template, parsed); + return parsed; +}; + /** * @param {boolean} xml * @param {WeakMap} twm @@ -47,45 +62,22 @@ const create = (xml, twm = new WeakMap) => /** * @param {TemplateStringsArray | string[]} template * @param {unknown[]} values - * @returns {Hole} + * @returns {Node | HTMLElement | SVGSVGElement | Hole} */ (template, ...values) => { - let parsed = twm.get(template); - if (!parsed) { - parsed = parse(template, values, xml); - parsed.push(isKeyed() ? new Keyed : null); - if (DEBUG) parsed.push(template); - parsed[0] = fragment(parsed[0].toString(), xml); - twm.set(template, parsed); - } - return new Hole(parsed, values); - }; + const hole = new Hole( + twm.get(template) ?? set(xml, twm, template, values), + values, + ); + return getDirect() ? hole.valueOf(true) : hole; + } +; -const htmlHole = create(false); -const svgHole = create(true); +export const html = create(false); +export const svg = create(true); const rendered = new WeakMap; -/** - * @param {TemplateStringsArray | string[]} template - * @param {any[]} values - * @returns {Node | HTMLElement | Hole} - */ -export function html(template, ...values) { - const hole = htmlHole.apply(null, arguments); - return getDirect() ? hole.valueOf(true) : hole; -} - -/** - * @param {TemplateStringsArray | string[]} template - * @param {any[]} values - * @returns {Node | SVGSVGElement | Hole} - */ -export function svg(template, ...values) { - const hole = svgHole.apply(null, arguments); - return getDirect() ? hole.valueOf(true) : hole; -} - /** * @param {Container} where * @param {Function | Node | Container} what @@ -93,19 +85,14 @@ export function svg(template, ...values) { */ export const render = (where, what) => { const known = rendered.get(where); - if (known) known[0](); if (typeof what === 'function') { setDirect(false); - let hole; - const scope = effectScope(() => { hole = what() }); - //@ts-ignore - if (!known || known[1].t !== hole.t) { - //@ts-ignore - const d = hole.valueOf(false); - where.replaceChildren(d); + let hole = what(); + if (known?.t !== hole.t) { + where.replaceChildren(hole.valueOf(false)); + rendered.set(where, hole); } - else known[1].update(hole); - rendered.set(where, [scope, hole]); + else known.update(hole); } else { setDirect(true); diff --git a/src/dom/ish.js b/src/dom/ish.js index 5b74fa7..f5475d9 100644 --- a/src/dom/ish.js +++ b/src/dom/ish.js @@ -5,6 +5,7 @@ import { assign, freeze, isArray } from '../utils.js'; export const ELEMENT = 1; export const ATTRIBUTE = 2; export const TEXT = 3; +export const DATA = 4; export const COMMENT = 8; export const DOCUMENT_TYPE = 10; export const FRAGMENT = 11; @@ -53,6 +54,22 @@ export const prop = (node, name, value) => { node.props[name] = value; }; +export const replaceWith = (source, target) => { + const { children } = source.parent; + children[children.indexOf(source)] = target; + target.parent = source.parent; + source.parent = null; +}; + +export const remove = node => { + const { parent } = node; + if (parent) { + const { children } = parent; + children.splice(children.indexOf(node), 1); + node.parent = null; + } +}; + const addJSON = (value, comp, json) => { if (value !== comp) json.push(value); }; diff --git a/src/dom/node.js b/src/dom/node.js new file mode 100644 index 0000000..c6827ed --- /dev/null +++ b/src/dom/node.js @@ -0,0 +1,91 @@ +import DEBUG from '../debug.js'; +import errors from '../errors.js'; +import { reduce } from '../utils.js'; +import resolve from './resolve.js'; +import set from './process.js'; +import props from './props.js'; +// import templates from './templates.js'; +// import { isArray } from '../utils.js'; +import { children } from './ish.js'; +import { set as setRefs } from './ref.js'; + +import { + ARRAY, + COMMENT, + COMPONENT, + KEY, + REF, +} from '../constants.js'; + +/** @typedef {globalThis.Element | globalThis.HTMLElement | globalThis.SVGSVGElement | globalThis.DocumentFragment} Container */ + +const create = ({ p: fragment, d: updates }, values) => { + if (DEBUG && values.length) console.time(`mapping ${values.length} updates`); + const root = document.importNode(fragment, true); + let length = values.length; + let node, prev, refs; + // if (DEBUG && length !== updates.length) throw errors.invalid_interpolation(templates.get(fragment), values); + while (length--) { + const { p: path, d: update, t: type } = updates[length]; + const value = values[length]; + if (prev !== path) { + node = resolve(root, path); + prev = path; + // if (DEBUG && !node) throw errors.invalid_path(templates.get(fragment), path); + } + + if (type & COMPONENT) { + const obj = props(node); + if (type === COMPONENT) { + if (DEBUG && typeof value !== 'function') throw errors.invalid_component(value); + for (const { name, value } of node.attributes) obj[name] ??= value; + obj.children ??= [...node.content.childNodes]; + const result = value(obj, {}); + if (result) node.replaceWith(result); + else node.remove(); + } + else update(obj, value); + } + else if (type !== KEY) { + // if (DEBUG && (type & ARRAY) && !isArray(value)) throw errors.invalid_interpolation(templates.get(fragment), value); + if (type === REF) (refs ??= []).push(node); + const prev = type === COMMENT ? node : (type & ARRAY ? children : null); + update(node, value, prev); + } + if (type & COMMENT) node.remove(); + } + + if (refs) setRefs(refs); + + if (DEBUG && values.length) console.timeEnd(`mapping ${values.length} updates`); + return reduce(root); +}; + +const tag = (xml, cache = new WeakMap) => + /** + * @param {TemplateStringsArray | string[]} template + * @param {unknown[]} values + * @returns {Container | Node} + */ + (template, ...values) => create( + cache.get(template) ?? set(xml, cache, template, values), + values, + ); +; + +export const html = tag(false); +export const svg = tag(true); + +/** + * @param {Container} where + * @param {Function | Container | Node} what + * @returns + */ +export const render = (where, what) => { + const node = typeof what === 'function' ? what() : what; + where.replaceChildren(node); + // where.normalize(); can be performed, arbitrarily, after + return where; +}; + +export { unsafe } from '../utils.js'; diff --git a/src/dom/process.js b/src/dom/process.js new file mode 100644 index 0000000..f2b557a --- /dev/null +++ b/src/dom/process.js @@ -0,0 +1,41 @@ +import DEBUG from '../debug.js'; + +import { + Comment, + DocumentType, + Text, + Fragment, + Element, + Component, +} from './ish.js'; + +import parser from '../parser/index.js'; +import templates from './templates.js'; +import { isKeyed, fragment, update } from './update.js'; +import { pdt } from '../utils.js'; + +const parse = parser({ + Comment, + DocumentType, + Text, + Fragment, + Element, + Component, + update, +}); + +export default (xml, cache, template, values) => { + if (DEBUG) console.time(`parsing ${values.length} holes`); + const [domish, updates] = parse(template, values, xml); + if (DEBUG) { + console.timeEnd(`parsing ${values.length} holes`); + console.time('creating fragment'); + } + const parsed = pdt(fragment(domish.toString(), xml), updates, isKeyed()); + if (DEBUG) { + console.timeEnd('creating fragment'); + templates.set(parsed.p, template); + } + cache.set(template, parsed); + return parsed; +}; diff --git a/src/dom/props.js b/src/dom/props.js new file mode 100644 index 0000000..2efbc97 --- /dev/null +++ b/src/dom/props.js @@ -0,0 +1,7 @@ +const props = new WeakMap; + +export default node => { + let obj = props.get(node); + if (!obj) props.set(node, (obj = {})); + return obj; +}; diff --git a/src/dom/rabbit.js b/src/dom/rabbit.js index be704ec..3a46868 100644 --- a/src/dom/rabbit.js +++ b/src/dom/rabbit.js @@ -5,13 +5,11 @@ import errors from '../errors.js'; import resolve from './resolve.js'; import { children } from './ish.js'; -import { effect } from './signals.js'; import { isArray } from '../utils.js'; import { PersistentFragment, diffFragment, nodes } from './persistent-fragment.js'; -import { ARRAY, COMMENT, COMPONENT, EVENT, KEY, REF, SIGNAL, ref } from './update.js'; +import { ARRAY, COMMENT, COMPONENT, EVENT, KEY, REF } from '../constants.js'; import { _get as getDirect, _set as setDirect } from './direct.js'; -import { Signal, _get as getSignal, _set as setSignal } from './signals.js'; /** * @param {Hole} hole @@ -43,17 +41,17 @@ const keyed = (hole, value) => /** @type {import('./keyed.js').Keyed} */(hole.t[ * @returns {Hole} */ const component = (Component, obj, signals) => { - const signal = getSignal(); - const length = signals.length; - let i = 0; - setSignal(/** @param {unknown} value */ value => i < length ? signals[i++] : (signals[i++] = signal(value))); - const wasDirect = getDirect(); - if (wasDirect) setDirect(!wasDirect); - try { return Component(obj, global); } - finally { - if (wasDirect) setDirect(wasDirect); - setSignal(signal); - } + // const signal = getSignal(); + // const length = signals.length; + // let i = 0; + // setSignal(/** @param {unknown} value */ value => i < length ? signals[i++] : (signals[i++] = signal(value))); + // const wasDirect = getDirect(); + // if (wasDirect) setDirect(!wasDirect); + // try { return Component(obj, global); } + // finally { + // if (wasDirect) setDirect(wasDirect); + // setSignal(signal); + // } }; /** @@ -99,12 +97,14 @@ const createEffect = (node, value, obj) => { 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; + switch (typeof value) { + case 'function': + value(node); + break; + case 'object': + if (value) value.current = node; + break; + } } }; @@ -160,11 +160,12 @@ export class Hole { else { let commit = true; if (DEBUG && (type & ARRAY) && !isArray(value)) throw errors.invalid_interpolation(this.t[3], value); - if (!direct && (type & COMMENT) && !(type & SIGNAL)) { + if (!direct && (type & COMMENT)) { if (type & ARRAY) { commit = false; - if (value.length) - update(node, value[0] instanceof Hole ? holed(children, value) : value); + if (value.length) { + changes[length] = update(node, children, value[0] instanceof Hole ? holed(children, value) : value); + } } else if (value instanceof Hole) { commit = false; @@ -242,13 +243,7 @@ export class Hole { else if ((type & EVENT) && (value[0] === prev[0])) continue; } else if (type & COMMENT) { - if (type & SIGNAL) { - if (value === prev) { - update(entry[3], change); - continue; - } - } - else if (prev instanceof Hole) { + if (prev instanceof Hole) { if (DEBUG && !(value instanceof Hole)) throw errors.invalid_interpolation([], value); value = getHole(prev, /** @type {Hole} */(value)); change = value.n; diff --git a/src/dom/ref.js b/src/dom/ref.js new file mode 100644 index 0000000..bcf4dd2 --- /dev/null +++ b/src/dom/ref.js @@ -0,0 +1,16 @@ +const refs = new WeakMap; + +export const set = list => { + for (let node, value, type, i = 0; i < list.length; i++) { + node = list[i]; + value = refs.get(node); + type = typeof value; + if (type === 'function') value(node); + else if (type === 'object' && value) value.current = node; + } +}; + +export const ref = (node, curr) => { + refs.set(node, curr); + return curr; +}; diff --git a/src/dom/resolve.js b/src/dom/resolve.js index 3e9cc76..cf548e9 100644 --- a/src/dom/resolve.js +++ b/src/dom/resolve.js @@ -1,8 +1,8 @@ import DEBUG from '../debug.js'; const tree = DEBUG ? - ((node, i) => i < 0 ? node?.content : node?.childNodes?.[i]) : - ((node, i) => i < 0 ? node.content : node.childNodes[i]) + ((node, i) => node?.childNodes?.[i]) : + ((node, i) => node.childNodes[i]) ; export default (root, path) => path.reduceRight(tree, root); diff --git a/src/dom/signals.js b/src/dom/signals.js deleted file mode 100644 index 7869501..0000000 --- a/src/dom/signals.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Signal, signal as _signal, computed, effect, untracked, startBatch, endBatch } from '@webreflection/alien-signals'; - -const batch = fn => { - startBatch(); - try { return fn() } - finally { endBatch() } -}; - -let $ = _signal; - -export function signal() { - return $.apply(null, arguments); -} - -export const _get = () => $; -export const _set = fn => { $ = fn }; - -export { Signal, computed, effect, untracked, batch }; diff --git a/src/dom/templates.js b/src/dom/templates.js new file mode 100644 index 0000000..5e9bc4a --- /dev/null +++ b/src/dom/templates.js @@ -0,0 +1,3 @@ +import DEBUG from '../debug.js'; + +export default DEBUG ? new WeakMap : null; diff --git a/src/dom/update.js b/src/dom/update.js index 3116f99..bb1b37b 100644 --- a/src/dom/update.js +++ b/src/dom/update.js @@ -5,139 +5,107 @@ import { ATTRIBUTE as TEMPLATE_ATTRIBUTE, COMMENT as TEMPLATE_COMMENT, COMPONENT as TEMPLATE_COMPONENT, + DATA as TEMPLATE_DATA, TEXT as TEMPLATE_TEXT, - children, } from './ish.js'; -import { Signal } from './signals.js'; -import { Unsafe, assign, entries, isArray } from '../utils.js'; -import { PersistentFragment, diffFragment, nodes } from './persistent-fragment.js'; +import { Unsafe, assign, entries, pdt, reduce, isArray } from '../utils.js'; +import { PersistentFragment, diffFragment } from './persistent-fragment.js'; +import { ref } from './ref.js'; import creator from './creator.js'; import diff from './diff.js'; -export const ARRAY = 1 << 0; -export const ARIA = 1 << 1; -export const ATTRIBUTE = 1 << 2; -export const COMMENT = 1 << 3; -export const COMPONENT = 1 << 4; -export const DATA = 1 << 5; -export const DIRECT = 1 << 6; -export const DOTS = 1 << 7; -export const EVENT = 1 << 8; -export const KEY = 1 << 9; -export const PROP = 1 << 10; -export const TEXT = 1 << 11; -export const TOGGLE = 1 << 12; -export const UNSAFE = 1 << 13; -export const REF = 1 << 14; -export 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; +import { + ARIA, + ATTRIBUTE, + COMMENT, + COMPONENT, + DATA, + DIRECT, + DOTS, + EVENT, + KEY, + TEXT, + TOGGLE, + UNSAFE, + REF, + COMPONENT_DIRECT, + COMPONENT_DOTS, + COMPONENT_PROP, + EVENT_ARRAY, + COMMENT_ARRAY, +} from '../constants.js'; export const fragment = creator(document); -export 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 aria = (node, curr, prev) => { + if (prev !== curr) { + for (const [key, value] of entries(curr)) + attribute.call(key === 'role' ? key : `aria-${key.toLowerCase()}`, node, value); } + return curr; }; -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 commentArray = (node, curr, prev) => diff( + prev, + curr, + diffFragment, + node +); -const comment_hole = (node, value) => { - const current = typeof value === 'object' ? (value ?? node) : getText(node, value); - const prev = node[nodes] ?? node; +const commentHole = (node, curr, prev) => { + const current = typeof curr === 'object' ? (curr ?? node) : getText(node, curr); 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); + prev.replaceWith(diffFragment(current, 1)); + return current; }; -const data = ({ dataset }, values) => { - for (const [key, value] of entries(values)) { +const data = ({ dataset }, curr) => { + for (const [key, value] of entries(curr)) { if (value == null) delete dataset[key]; else dataset[key] = value; } + return curr; }; -/** @type {Map} */ -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(name))); - return fn; -}; - -const direct = name => (node, value) => { - node[name] = value; -}; - -const dots = (node, values) => { - for (const [name, value] of entries(values)) - attribute(name)(node, value); +const dots = (node, curr) => { + for (const [name, value] of entries(curr)) + attribute.call(name, node, value); + return curr; }; -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); +const [ + attributeFor, + directFor, + eventArrayFor, + eventFor, + toggleFor, +] = [ + attribute, + direct, + eventArray, + event, + toggle, +].map( + fn => createFor.bind(fn, new Map) +); + +const [ + unsafeSVG, + unsafeHTML, +] = [ + unsafe, + unsafe, +].map( + (fn, i) => fn.bind([new WeakMap, !i]) +); + +const getTextWM = new WeakMap; +const getText = (node, text) => { + let dom = getTextWM.get(node); + if (dom) dom.data = text; + else getTextWM.set(node, (dom = document.createTextNode(text))); + return dom; }; let k = false; @@ -149,44 +117,94 @@ export const isKeyed = () => { export const update = (node, type, path, name, hint) => { switch (type) { - case TEMPLATE_COMPONENT: return [path, hint, COMPONENT]; + case TEMPLATE_COMPONENT: return pdt(path, hint, COMPONENT); case TEMPLATE_COMMENT: { - 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]; + if (isArray(hint)) return pdt(path, commentArray, COMMENT_ARRAY); + if (hint instanceof Unsafe) return pdt(path, node.xml ? unsafeSVG : unsafeHTML, UNSAFE); + return pdt(path, commentHole, COMMENT); } - case TEMPLATE_TEXT: return [path, directFor('textContent'), TEXT]; + case TEMPLATE_TEXT: return pdt(path, directFor('textContent'), TEXT); case TEMPLATE_ATTRIBUTE: { const isComponent = node.type === TEMPLATE_COMPONENT; switch (name.at(0)) { case '@': { if (DEBUG && isComponent) throw errors.invalid_attribute([], name); const array = isArray(hint); - return [path, event(name.slice(1), Symbol(name), array), array ? EVENT_ARRAY : EVENT]; + const cb = array ? eventArrayFor : eventFor; + return pdt(path, cb(name.slice(1)), array ? EVENT_ARRAY : EVENT); } case '?': if (DEBUG && isComponent) throw errors.invalid_attribute([], name); - return [path, toggle(name.slice(1)), TOGGLE]; + return pdt(path, toggleFor(name.slice(1)), TOGGLE); case '.': { return name === '...' ? - [path, isComponent ? assign : dots, isComponent ? COMPONENT_DOTS : DOTS] : - [path, direct(name.slice(1)), isComponent ? COMPONENT_DIRECT : DIRECT] + pdt(path, isComponent ? assign : dots, isComponent ? COMPONENT_DOTS : DOTS) : + pdt(path, directFor(name.slice(1)), isComponent ? COMPONENT_DIRECT : DIRECT) ; } default: { - if (isComponent) return [path, direct(name), COMPONENT_PROP]; - if (name === 'aria') return [path, aria, ARIA]; - if (name === 'data' && !/^object$/i.test(node.name)) return [path, data, DATA]; + if (isComponent) return pdt(path, directFor(name), COMPONENT_PROP); + if (name === 'aria') return pdt(path, aria, ARIA); + if (name === 'data' && !/^object$/i.test(node.name)) return pdt(path, data, DATA); if (name === 'key') { if (DEBUG && 1 < path.length) throw errors.invalid_key(hint); - return [path, (k = true), KEY]; + return pdt(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]; + if (name === 'ref') return pdt(path, ref, REF); + if (name.startsWith('on')) return pdt(path, directFor(name.toLowerCase()), DIRECT); + return pdt(path, attributeFor(name), ATTRIBUTE); } } } + case TEMPLATE_DATA: return pdt(path, directFor('data'), TEXT); } }; + +function createFor(map, name) { + let cb = map.get(name); + if (!cb) map.set(name, (cb = this.bind(name))); + return cb; +} + +function attribute(node, curr) { + 'use strict'; + if (curr == null) node.removeAttribute(this); + else node.setAttribute(this, curr); + return curr; +} + +function direct(ref, curr) { + 'use strict'; + ref[this] = curr; + return curr; +} + +function eventArray(node, curr, prev) { + 'use strict'; + if (prev.length) node.removeEventListener(this, ...prev); + if (curr) node.addEventListener(this, ...curr); + return curr; +} + +function event(node, curr, prev) { + 'use strict'; + if (prev) node.removeEventListener(this, prev); + if (curr) node.addEventListener(this, curr); + return curr; +} + +function toggle(node, curr) { + 'use strict'; + node.toggleAttribute(this, !!curr); + return curr; +} + +function unsafe(node, curr) { + const [wm, xml] = this; + const f = fragment(curr, xml); + const u = reduce(f); + const n = u === f ? PersistentFragment(u) : u; + (wm.get(node) ?? node).replaceWith(n); + wm.set(node, n); + return curr; +} diff --git a/src/json/index.js b/src/json/index.js index ffda6a6..6045868 100644 --- a/src/json/index.js +++ b/src/json/index.js @@ -1,68 +1,82 @@ +// TODO align with the new parser/updates expectations + import DEBUG from '../debug.js'; import errors from '../errors.js'; import { assign } from '../utils.js'; +import set from './process.js'; +import props from '../dom/props.js'; +import { set as setRefs } from '../dom/ref.js'; import { - Comment, - DocumentType, - Text, - Fragment, - Element, - Component, fromJSON, + replaceWith, + remove, + children, } from '../dom/ish.js'; -import parser from '../parser/index.js'; import resolve from './resolve.js'; -import { COMPONENT, KEY, comment, update } from './update.js'; - -const textParser = parser({ - Comment, - DocumentType, - Text, - Fragment, - Element, - Component, - update, -}); +import { ARRAY, COMMENT, COMPONENT, KEY, REF } from '../constants.js'; -const { parse, stringify } = JSON; +const create = ({ p: json, d: updates }, values) => { + if (DEBUG && values.length) console.time(`mapping ${values.length} updates`); + const root = fromJSON(json); + let length = values.length; + let node, prev, refs; + // if (DEBUG && length !== updates.length) throw errors.invalid_interpolation(templates.get(fragment), values); + while (length--) { + const { p: path, d: update, t: type } = updates[length]; + const value = values[length]; + if (prev !== path) { + node = resolve(root, path); + prev = path; + // if (DEBUG && !node) throw errors.invalid_path(templates.get(fragment), path); + } -const create = xml => { - const twm = new WeakMap; - const cache = (template, values) => { - const parsed = textParser(template, values, xml); - parsed[0] = parse(stringify(parsed[0])); - twm.set(template, parsed); - return parsed; - }; - return (template, ...values) => { - const [json, updates] = twm.get(template) || cache(template, values); - const root = fromJSON(json); - const length = values.length; - if (length === updates.length) { - const components = []; - for (let node, prev, i = 0; i < length; i++) { - const [path, update, type] = updates[i]; - const value = values[i]; - if (prev !== path) { - node = resolve(root, path); - prev = path; - if (DEBUG && !node) throw errors.invalid_path(path); - } - if (type === KEY) continue; - if (type === COMPONENT) components.push(update(node, value)); - else update(node, value); - } - for (const [node, Component] of components) { - const props = assign({ children: node.children }, node.props); - comment(node, Component(props)); + if (type & COMPONENT) { + const obj = props(node); + if (type === COMPONENT) { + if (DEBUG && typeof value !== 'function') throw errors.invalid_component(value); + const result = value(assign({}, node.props, { children: node.children }, obj), {}); + if (result) replaceWith(node, result); + else remove(node); } + else update(obj, value); + } + else if (type !== KEY) { + // if (DEBUG && (type & ARRAY) && !isArray(value)) throw errors.invalid_interpolation(templates.get(fragment), value); + if (type === REF) (refs ??= []).push(node); + const prev = type === COMMENT ? node : (type & ARRAY ? children : null); + update(node, value, prev); } - else if (DEBUG) throw errors.invalid_template(); - return root; - }; + if (type & COMMENT) remove(node); + } + + if (refs) setRefs(refs); + + if (DEBUG && values.length) console.timeEnd(`mapping ${values.length} updates`); + return root.children.length === 1 ? root.children[0] : root; +}; + +const tag = (xml, cache = new WeakMap) => + /** + * @param {TemplateStringsArray | string[]} template + * @param {unknown[]} values + * @returns {import("../dom/ish.js").Node} + */ + (template, ...values) => create( + cache.get(template) ?? set(xml, cache, template, values), + values, + ); +; + +export const html = tag(false); +export const svg = tag(true); + +export const render = (where, what) => { + const content = (typeof what === 'function' ? what() : what).toString(); + if (!where.write) return where(content); + where.write(content); + return where; }; -export const html = create(false); -export const svg = create(true); +export { unsafe } from '../utils.js'; diff --git a/src/json/process.js b/src/json/process.js new file mode 100644 index 0000000..8ec6508 --- /dev/null +++ b/src/json/process.js @@ -0,0 +1,41 @@ +import DEBUG from '../debug.js'; + +import { + Comment, + DocumentType, + Text, + Fragment, + Element, + Component, +} from '../dom/ish.js'; + +import parser from '../parser/index.js'; +import templates from '../dom/templates.js'; +import { isKeyed, update } from './update.js'; +import { pdt } from '../utils.js'; + +const parse = parser({ + Comment, + DocumentType, + Text, + Fragment, + Element, + Component, + update, +}); + +export default (xml, cache, template, values) => { + if (DEBUG) console.time(`parsing ${values.length} holes`); + const [domish, updates] = parse(template, values, xml); + if (DEBUG) { + console.timeEnd(`parsing ${values.length} holes`); + console.time('creating fragment'); + } + const parsed = pdt(domish.toJSON(), updates, isKeyed()); + if (DEBUG) { + console.timeEnd('creating fragment'); + templates.set(parsed.p, template); + } + cache.set(template, parsed); + return parsed; +}; diff --git a/src/json/resolve.js b/src/json/resolve.js index d295081..694867c 100644 --- a/src/json/resolve.js +++ b/src/json/resolve.js @@ -1,8 +1,8 @@ import DEBUG from '../debug.js'; const tree = DEBUG ? - ((node, i) => i < 0 ? node : node?.children?.[i]) : - ((node, i) => i < 0 ? node : node.children[i]) + ((node, i) => node?.children?.[i]) : + ((node, i) => node.children[i]) ; export default (root, path) => path.reduceRight(tree, root); diff --git a/src/json/update.js b/src/json/update.js index 57e2e85..0194650 100644 --- a/src/json/update.js +++ b/src/json/update.js @@ -1,3 +1,5 @@ +// TODO align with the new parser/updates expectations + import { ATTRIBUTE as TEMPLATE_ATTRIBUTE, COMMENT as TEMPLATE_COMMENT, @@ -106,6 +108,13 @@ const toggle = name => (node, value) => { else props[name] = !!value; }; +let k = false; +export const isKeyed = () => { + const wasKeyed = k; + k = false; + return wasKeyed; +}; + export const update = (node, type, path, name) => { switch (type) { case TEMPLATE_COMPONENT: { diff --git a/src/parser/index.js b/src/parser/index.js index 4b1fb9b..1abe97b 100644 --- a/src/parser/index.js +++ b/src/parser/index.js @@ -7,6 +7,7 @@ import { ATTRIBUTE, COMMENT, COMPONENT, + DATA, ELEMENT, TEXT, TEXT_ELEMENTS, @@ -34,11 +35,11 @@ const ATTRS = /([^\s/>=]+)(?:=(\x00|(?:(['"])[\s\S]*?\3)))?/g; /** @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 {(node: import('../dom/ish.js').Node, type: typeof ATTRIBUTE | typeof DATA | 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]; +const defaultUpdate = (...args) => args; /** * @param {Node} node @@ -130,6 +131,11 @@ export default ({ if (DEBUG && (i - index) < 6) throw errors.invalid_comment(template); const data = content.slice(index + 4, i - 2); if (data[0] === '!') append(node, new Comment(data.slice(1).replace(/!$/, ''))); + else if (data === NUL) { + const comment = append(node, new Comment('◦')); + values.push(update(comment, DATA, path(comment), '', holes[hole++])); + pos = i + 1; + } } else { if (DEBUG && !content.slice(index + 2, i).toLowerCase().startsWith('doctype')) throw errors.invalid_doctype(template, content.slice(index + 2, i)); diff --git a/src/utils.js b/src/utils.js index 9cfc365..77fcf2a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -34,7 +34,18 @@ export class Unsafe { } } -export const unsafe = data => new Unsafe(data); +export const reduce = node => { + const { childNodes } = node; + return childNodes.length === 1 ? childNodes[0] : node; +}; + +export const unsafe = (template, ...values) => new Unsafe( + typeof template === 'string' ? + template : + [template[0], ...values.map((v, i) => v + template[i + 1])].join('') +); export const createComment = value => document.createComment(value); + +export const pdt = (path, detail, type) => ({ p: path, d: detail, t: type }); /* c8 ignore stop */ diff --git a/test/base.html b/test/base.html index c600db1..4b2a4f0 100644 --- a/test/base.html +++ b/test/base.html @@ -1,8 +1,18 @@ + + + diff --git a/types/constants.d.ts b/types/constants.d.ts new file mode 100644 index 0000000..4db027c --- /dev/null +++ b/types/constants.d.ts @@ -0,0 +1,20 @@ +export const ARRAY: number; +export const ARIA: number; +export const ATTRIBUTE: number; +export const COMMENT: number; +export const COMPONENT: number; +export const DATA: number; +export const DIRECT: number; +export const DOTS: number; +export const EVENT: number; +export const KEY: number; +export const PROP: number; +export const TEXT: number; +export const TOGGLE: number; +export const UNSAFE: number; +export const REF: number; +export const COMPONENT_DIRECT: number; +export const COMPONENT_DOTS: number; +export const COMPONENT_PROP: number; +export const EVENT_ARRAY: number; +export const COMMENT_ARRAY: number; diff --git a/types/dom/cdn.d.ts b/types/dom/cdn.d.ts index 3aa5c95..26b7444 100644 --- a/types/dom/cdn.d.ts +++ b/types/dom/cdn.d.ts @@ -1,8 +1,4 @@ export const render: any; export const html: any; export const svg: any; -export const computed: any; -export const signal: any; -export const batch: any; -export const effect: any; -export const untracked: any; +export const unsafe: any; diff --git a/types/dom/index.d.ts b/types/dom/index.d.ts index 21ee583..a4b7a0e 100644 --- a/types/dom/index.d.ts +++ b/types/dom/index.d.ts @@ -1,19 +1,18 @@ /** * @param {TemplateStringsArray | string[]} template - * @param {any[]} values - * @returns {Node | HTMLElement | Hole} + * @param {unknown[]} values + * @returns {Node | HTMLElement | SVGSVGElement | Hole} */ -export function html(template: TemplateStringsArray | string[], ...values: any[]): Node | HTMLElement | Hole; +export function html(template: TemplateStringsArray | string[], ...values: unknown[]): Node | HTMLElement | SVGSVGElement | Hole; /** * @param {TemplateStringsArray | string[]} template - * @param {any[]} values - * @returns {Node | SVGSVGElement | Hole} + * @param {unknown[]} values + * @returns {Node | HTMLElement | SVGSVGElement | Hole} */ -export function svg(template: TemplateStringsArray | string[], ...values: any[]): Node | SVGSVGElement | Hole; +export function svg(template: TemplateStringsArray | string[], ...values: unknown[]): Node | HTMLElement | SVGSVGElement | Hole; export function render(where: Container, what: Function | Node | Container): Container; export type Container = globalThis.Element | globalThis.HTMLElement | globalThis.SVGSVGElement | globalThis.DocumentFragment; import { Hole } from './rabbit.js'; import { fragment } from './update.js'; import { unsafe } from '../utils.js'; export { Hole, fragment, unsafe }; -export { signal, computed, effect, untracked, batch } from "./signals.js"; diff --git a/types/dom/ish.d.ts b/types/dom/ish.d.ts index 33d07fe..30b819e 100644 --- a/types/dom/ish.d.ts +++ b/types/dom/ish.d.ts @@ -1,6 +1,7 @@ export const ELEMENT: 1; export const ATTRIBUTE: 2; export const TEXT: 3; +export const DATA: 4; export const COMMENT: 8; export const DOCUMENT_TYPE: 10; export const FRAGMENT: 11; @@ -11,6 +12,8 @@ export const props: Readonly<{}>; export const children: readonly any[]; export function append(node: any, child: any): any; export function prop(node: any, name: any, value: any): void; +export function replaceWith(source: any, target: any): void; +export function remove(node: any): void; export function fromJSON(json: any): any; export class Node { constructor(type: any); diff --git a/types/dom/node.d.ts b/types/dom/node.d.ts new file mode 100644 index 0000000..0eea281 --- /dev/null +++ b/types/dom/node.d.ts @@ -0,0 +1,15 @@ +/** + * @param {TemplateStringsArray | string[]} template + * @param {unknown[]} values + * @returns {Container | Node} + */ +export function html(template: TemplateStringsArray | string[], ...values: unknown[]): Container | Node; +/** + * @param {TemplateStringsArray | string[]} template + * @param {unknown[]} values + * @returns {Container | Node} + */ +export function svg(template: TemplateStringsArray | string[], ...values: unknown[]): Container | Node; +export function render(where: Container, what: Function | Container | Node): Container; +export { unsafe } from "../utils.js"; +export type Container = globalThis.Element | globalThis.HTMLElement | globalThis.SVGSVGElement | globalThis.DocumentFragment; diff --git a/types/dom/process.d.ts b/types/dom/process.d.ts new file mode 100644 index 0000000..015a9a0 --- /dev/null +++ b/types/dom/process.d.ts @@ -0,0 +1,6 @@ +declare function _default(xml: any, cache: any, template: any, values: any): { + p: any; + d: any; + t: any; +}; +export default _default; diff --git a/types/dom/props.d.ts b/types/dom/props.d.ts new file mode 100644 index 0000000..77edc4b --- /dev/null +++ b/types/dom/props.d.ts @@ -0,0 +1,2 @@ +declare function _default(node: any): any; +export default _default; diff --git a/types/dom/ref.d.ts b/types/dom/ref.d.ts new file mode 100644 index 0000000..4d122f4 --- /dev/null +++ b/types/dom/ref.d.ts @@ -0,0 +1,2 @@ +export function set(list: any): void; +export function ref(node: any, curr: any): any; diff --git a/types/dom/signals.d.ts b/types/dom/signals.d.ts deleted file mode 100644 index 2f5c607..0000000 --- a/types/dom/signals.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function signal(...args: any[]): any; -export function _get(): any; -export function _set(fn: any): void; -export function batch(fn: any): any; -export { Signal, computed, effect, untracked }; diff --git a/types/dom/templates.d.ts b/types/dom/templates.d.ts new file mode 100644 index 0000000..eca5f36 --- /dev/null +++ b/types/dom/templates.d.ts @@ -0,0 +1,2 @@ +declare const _default: WeakMap; +export default _default; diff --git a/types/dom/update.d.ts b/types/dom/update.d.ts index e3c9159..e588a6a 100644 --- a/types/dom/update.d.ts +++ b/types/dom/update.d.ts @@ -1,20 +1,7 @@ -export const ARRAY: number; -export const ARIA: number; -export const ATTRIBUTE: number; -export const COMMENT: number; -export const COMPONENT: number; -export const DATA: number; -export const DIRECT: number; -export const DOTS: number; -export const EVENT: number; -export const KEY: number; -export const PROP: number; -export const TEXT: number; -export const TOGGLE: number; -export const UNSAFE: number; -export const REF: number; -export const SIGNAL: number; export const fragment: (content: string, xml?: boolean) => DocumentFragment; -export const ref: unique symbol; export function isKeyed(): boolean; -export function update(node: any, type: any, path: any, name: any, hint: any): any[]; +export function update(node: any, type: any, path: any, name: any, hint: any): { + p: any; + d: any; + t: any; +}; diff --git a/types/json/index.d.ts b/types/json/index.d.ts index c15d58d..c263b12 100644 --- a/types/json/index.d.ts +++ b/types/json/index.d.ts @@ -1,2 +1,14 @@ -export function html(template: any, ...values: any[]): any; -export function svg(template: any, ...values: any[]): any; +/** + * @param {TemplateStringsArray | string[]} template + * @param {unknown[]} values + * @returns {import("../dom/ish.js").Node} + */ +export function html(template: TemplateStringsArray | string[], ...values: unknown[]): import("../dom/ish.js").Node; +/** + * @param {TemplateStringsArray | string[]} template + * @param {unknown[]} values + * @returns {import("../dom/ish.js").Node} + */ +export function svg(template: TemplateStringsArray | string[], ...values: unknown[]): import("../dom/ish.js").Node; +export function render(where: any, what: any): any; +export { unsafe } from "../utils.js"; diff --git a/types/json/process.d.ts b/types/json/process.d.ts new file mode 100644 index 0000000..015a9a0 --- /dev/null +++ b/types/json/process.d.ts @@ -0,0 +1,6 @@ +declare function _default(xml: any, cache: any, template: any, values: any): { + p: any; + d: any; + t: any; +}; +export default _default; diff --git a/types/json/update.d.ts b/types/json/update.d.ts index 71eba26..6c30201 100644 --- a/types/json/update.d.ts +++ b/types/json/update.d.ts @@ -10,4 +10,5 @@ export const EVENT: 7; export const KEY: 8; export const TEXT: 9; export const TOGGLE: 10; +export function isKeyed(): boolean; export function update(node: any, type: any, path: any, name: any): any[]; diff --git a/types/parser/index.d.ts b/types/parser/index.d.ts index 45bcd6e..7ddfaea 100644 --- a/types/parser/index.d.ts +++ b/types/parser/index.d.ts @@ -11,7 +11,7 @@ export default _default; export type Node = import("../dom/ish.js").Node; export type Element = import("../dom/ish.js").Element; export type Component = import("../dom/ish.js").Component; -export type update = (node: import("../dom/ish.js").Node, type: typeof ATTRIBUTE | typeof TEXT | typeof COMMENT | typeof COMPONENT, path: number[], name: string, hint: unknown) => unknown; +export type update = (node: import("../dom/ish.js").Node, type: typeof ATTRIBUTE | typeof DATA | typeof TEXT | typeof COMMENT | typeof COMPONENT, path: number[], name: string, hint: unknown) => unknown; export type Container = Element | Component; import { Comment as DOMComment } from '../dom/ish.js'; import { DocumentType as DOMDocumentType } from '../dom/ish.js'; @@ -20,6 +20,7 @@ import { Fragment as DOMFragment } from '../dom/ish.js'; import { Element as DOMElement } from '../dom/ish.js'; import { Component as DOMComponent } from '../dom/ish.js'; import { ATTRIBUTE } from '../dom/ish.js'; +import { DATA } from '../dom/ish.js'; import { TEXT } from '../dom/ish.js'; import { COMMENT } from '../dom/ish.js'; import { COMPONENT } from '../dom/ish.js'; diff --git a/types/utils.d.ts b/types/utils.d.ts index f613117..2ccbfea 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -5,8 +5,14 @@ export class Unsafe { toString(): string; #private; } -export function unsafe(data: any): Unsafe; +export function reduce(node: any): any; +export function unsafe(template: any, ...values: any[]): Unsafe; export function createComment(value: any): Comment; +export function pdt(path: any, detail: any, type: any): { + p: any; + d: any; + t: any; +}; export const assign: { (target: T, source: U): T & U; (target: T, source1: U, source2: V): T & U & V;