pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/WebReflection/uhtml/pull/161.patch

= `<${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 ./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", 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/node.js b/src/dom/node.js new file mode 100644 index 0000000..c776479 --- /dev/null +++ b/src/dom/node.js @@ -0,0 +1,79 @@ +import DEBUG from '../debug.js'; +import errors from '../errors.js'; + +import resolve from './resolve.js'; +import set from './process.js'; +import props from './props.js'; +import { children } from './ish.js'; +import { set as setRefs } from './ref.js'; +import { ARRAY, COMMENT, COMPONENT, KEY, REF } from './update.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; + 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 (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]; + node.replaceWith(value(obj, {})); + } + else update(obj, value); + } + else if (type !== KEY) { + 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`); + const { childNodes } = root; + length = childNodes.length; + return length === 1 ? childNodes[0] : 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..bf9aaeb --- /dev/null +++ b/src/dom/process.js @@ -0,0 +1,36 @@ +import DEBUG from '../debug.js'; + +import { + Comment, + DocumentType, + Text, + Fragment, + Element, + Component, +} from './ish.js'; + +import parser from '../parser/index.js'; +import { isKeyed, fragment, update, pdt } from './update.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'); + 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..a5044c4 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 './update.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/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/update.js b/src/dom/update.js index 3116f99..c701114 100644 --- a/src/dom/update.js +++ b/src/dom/update.js @@ -6,12 +6,11 @@ import { COMMENT as TEMPLATE_COMMENT, COMPONENT as TEMPLATE_COMPONENT, 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 { PersistentFragment, diffFragment } from './persistent-fragment.js'; +import { ref } from './ref.js'; import creator from './creator.js'; import diff from './diff.js'; @@ -30,114 +29,94 @@ 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; + +// ARRAY flags const EVENT_ARRAY = EVENT | ARRAY; const COMMENT_ARRAY = COMMENT | ARRAY; 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); +// /** +// * @param {number[]} path +// * @param {unknown} detail +// * @param {typeof COMPONENT | typeof COMMENT_ARRAY | typeof UNSAFE | typeof COMMENT | typeof TEXT | typeof EVENT_ARRAY | typeof EVENT | typeof TOGGLE | typeof COMPONENT_DOTS | typeof DOTS | typeof COMPONENT_DIRECT | typeof DIRECT | typeof COMPONENT_PROP | typeof ARIA | typeof DATA | typeof KEY | typeof REF | typeof ATTRIBUTE} type +// * @returns +// */ +export const pdt = (path, detail, type) => ({ p: path, d: detail, t: type }); + +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 +128,91 @@ 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); } } } } }; + +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(node, curr) { + 'use strict'; + node[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 pf = PersistentFragment(fragment(curr, xml)); + (wm.get(node) ?? node).replaceWith(pf); + wm.set(node, pf); + return curr; +} diff --git a/src/parser/index.js b/src/parser/index.js index 4b1fb9b..79712b1 100644 --- a/src/parser/index.js +++ b/src/parser/index.js @@ -38,7 +38,7 @@ const ATTRS = /([^\s/>=]+)(?:=(\x00|(?:(['"])[\s\S]*?\3)))?/g; /** @typedef {Element | Component} Container */ /** @type {update} */ -const defaultUpdate = (_, type, path, name, hint) => [type, path, name]; +const defaultUpdate = (...args) => args; /** * @param {Node} node diff --git a/test/base.html b/test/base.html index c600db1..8d4f6d6 100644 --- a/test/base.html +++ b/test/base.html @@ -1,8 +1,25 @@ + + 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/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/update.d.ts b/types/dom/update.d.ts index e3c9159..57c0830 100644 --- a/types/dom/update.d.ts +++ b/types/dom/update.d.ts @@ -13,8 +13,15 @@ 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 pdt(path: any, detail: any, type: any): { + p: any; + d: any; + t: any; +}; 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; +}; From 416fdca7b122e5e2ed9833066e072743fc59a451 Mon Sep 17 00:00:00 2001 From: webreflection Date: Thu, 6 Nov 2025 10:48:11 +0100 Subject: [PATCH 2/6] 0.1.2 --- package-lock.json | 17 ++++++++++++----- package.json | 5 +++-- src/dom/node.js | 9 ++++++++- src/dom/process.js | 6 +++++- src/dom/templates.js | 3 +++ src/json/index.js | 2 ++ src/json/update.js | 2 ++ test/base.html | 34 ++++++++++++++++++++++++++-------- types/dom/templates.d.ts | 2 ++ 9 files changed, 63 insertions(+), 17 deletions(-) create mode 100644 src/dom/templates.js create mode 100644 types/dom/templates.d.ts diff --git a/package-lock.json b/package-lock.json index 5d3cb51..b957f27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { - "name": "uhtml", - "version": "5.0.9", + "name": "@webreflection/uhtml", + "version": "0.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "uhtml", - "version": "5.0.9", + "name": "@webreflection/uhtml", + "version": "0.1.2", "license": "MIT", "dependencies": { - "@webreflection/alien-signals": "^0.3.2" + "@webreflection/alien-signals": "^0.3.2", + "dom-cue": "^0.2.8" }, "devDependencies": { "@rollup/plugin-node-resolve": "^16.0.1", @@ -755,6 +756,12 @@ "node": ">=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 39f81e4..c571f12 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@webreflection/uhtml", - "version": "0.1.1", + "version": "0.1.2", "type": "module", "scripts": { "build": "npm run types && npm run build:js", @@ -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/dom/node.js b/src/dom/node.js index c776479..5c1f363 100644 --- a/src/dom/node.js +++ b/src/dom/node.js @@ -4,6 +4,8 @@ import errors from '../errors.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 './update.js'; @@ -15,12 +17,14 @@ const create = ({ p: fragment, d: updates }, values) => { 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) { @@ -29,11 +33,14 @@ const create = ({ p: fragment, d: updates }, values) => { 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]; - node.replaceWith(value(obj, {})); + 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); diff --git a/src/dom/process.js b/src/dom/process.js index bf9aaeb..067cd4b 100644 --- a/src/dom/process.js +++ b/src/dom/process.js @@ -10,6 +10,7 @@ import { } from './ish.js'; import parser from '../parser/index.js'; +import templates from './templates.js'; import { isKeyed, fragment, update, pdt } from './update.js'; const parse = parser({ @@ -30,7 +31,10 @@ export default (xml, cache, template, values) => { console.time('creating fragment'); } const parsed = pdt(fragment(domish.toString(), xml), updates, isKeyed()); - if (DEBUG) console.timeEnd('creating fragment'); + if (DEBUG) { + console.timeEnd('creating fragment'); + templates.set(parsed.p, template); + } cache.set(template, parsed); return parsed; }; 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/json/index.js b/src/json/index.js index ffda6a6..306e7f7 100644 --- a/src/json/index.js +++ b/src/json/index.js @@ -1,3 +1,5 @@ +// TODO align with the new parser/updates expectations + import DEBUG from '../debug.js'; import errors from '../errors.js'; import { assign } from '../utils.js'; diff --git a/src/json/update.js b/src/json/update.js index 57e2e85..76f6c98 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, diff --git a/test/base.html b/test/base.html index 8d4f6d6..d6f9509 100644 --- a/test/base.html +++ b/test/base.html @@ -1,24 +1,42 @@ + From 53925e634e225d0adb1ed10aeb03edb1e440460a Mon Sep 17 00:00:00 2001 From: webreflection Date: Tue, 11 Nov 2025 16:35:42 +0100 Subject: [PATCH 4/6] added holed comments + made unsafe tag compatible --- src/dom/ish.js | 1 + src/dom/node.js | 6 ++---- src/dom/update.js | 12 ++++++++---- src/parser/index.js | 8 +++++++- src/utils.js | 11 ++++++++++- test/base.html | 38 ++------------------------------------ types/dom/ish.d.ts | 1 + types/parser/index.d.ts | 3 ++- types/utils.d.ts | 3 ++- 9 files changed, 35 insertions(+), 48 deletions(-) diff --git a/src/dom/ish.js b/src/dom/ish.js index 5b74fa7..4d65b6b 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; diff --git a/src/dom/node.js b/src/dom/node.js index 5c1f363..4375052 100644 --- a/src/dom/node.js +++ b/src/dom/node.js @@ -1,6 +1,6 @@ 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'; @@ -51,9 +51,7 @@ const create = ({ p: fragment, d: updates }, values) => { if (refs) setRefs(refs); if (DEBUG && values.length) console.timeEnd(`mapping ${values.length} updates`); - const { childNodes } = root; - length = childNodes.length; - return length === 1 ? childNodes[0] : root; + return reduce(root); }; const tag = (xml, cache = new WeakMap) => diff --git a/src/dom/update.js b/src/dom/update.js index c701114..5b78a81 100644 --- a/src/dom/update.js +++ b/src/dom/update.js @@ -5,10 +5,11 @@ import { ATTRIBUTE as TEMPLATE_ATTRIBUTE, COMMENT as TEMPLATE_COMMENT, COMPONENT as TEMPLATE_COMPONENT, + DATA as TEMPLATE_DATA, TEXT as TEMPLATE_TEXT, } from './ish.js'; -import { Unsafe, assign, entries, isArray } from '../utils.js'; +import { Unsafe, assign, entries, reduce, isArray } from '../utils.js'; import { PersistentFragment, diffFragment } from './persistent-fragment.js'; import { ref } from './ref.js'; import creator from './creator.js'; @@ -167,6 +168,7 @@ export const update = (node, type, path, name, hint) => { } } } + case TEMPLATE_DATA: return pdt(path, directFor('data'), TEXT); } }; @@ -211,8 +213,10 @@ function toggle(node, curr) { function unsafe(node, curr) { const [wm, xml] = this; - const pf = PersistentFragment(fragment(curr, xml)); - (wm.get(node) ?? node).replaceWith(pf); - wm.set(node, pf); + 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/parser/index.js b/src/parser/index.js index 79712b1..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,7 +35,7 @@ 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} */ @@ -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..b5e61ff 100644 --- a/src/utils.js +++ b/src/utils.js @@ -34,7 +34,16 @@ 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); /* c8 ignore stop */ diff --git a/test/base.html b/test/base.html index ee5226d..4b2a4f0 100644 --- a/test/base.html +++ b/test/base.html @@ -12,41 +12,7 @@ diff --git a/types/dom/ish.d.ts b/types/dom/ish.d.ts index 33d07fe..d955888 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; 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..e2f867e 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -5,7 +5,8 @@ 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 const assign: { (target: T, source: U): T & U; From c830d27f9e427b10fa98efed09c871f0bfc28275 Mon Sep 17 00:00:00 2001 From: webreflection Date: Tue, 11 Nov 2025 16:35:54 +0100 Subject: [PATCH 5/6] 0.1.4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 78a0fe6..27368bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@webreflection/uhtml", - "version": "0.1.3", + "version": "0.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@webreflection/uhtml", - "version": "0.1.3", + "version": "0.1.4", "license": "MIT", "dependencies": { "@webreflection/alien-signals": "^0.3.2", diff --git a/package.json b/package.json index 6fc8738..a062ac4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@webreflection/uhtml", - "version": "0.1.3", + "version": "0.1.4", "type": "module", "scripts": { "build": "npm run types && npm run build:js", From 176aa4f08cb23375c8c963ebddefdd3cdbe1477c Mon Sep 17 00:00:00 2001 From: webreflection Date: Wed, 12 Nov 2025 13:50:04 +0100 Subject: [PATCH 6/6] working on JSON variant --- src/constants.js | 24 ++++++++ src/dom/ish.js | 16 ++++++ src/dom/node.js | 9 ++- src/dom/process.js | 3 +- src/dom/rabbit.js | 2 +- src/dom/resolve.js | 4 +- src/dom/update.js | 58 ++++++++------------ src/json/index.js | 118 ++++++++++++++++++++++------------------ src/json/process.js | 41 ++++++++++++++ src/json/resolve.js | 4 +- src/json/update.js | 7 +++ src/utils.js | 2 + types/constants.d.ts | 20 +++++++ types/dom/ish.d.ts | 2 + types/dom/update.d.ts | 20 ------- types/json/index.d.ts | 16 +++++- types/json/process.d.ts | 6 ++ types/json/update.d.ts | 1 + types/utils.d.ts | 5 ++ 19 files changed, 241 insertions(+), 117 deletions(-) create mode 100644 src/constants.js create mode 100644 src/json/process.js create mode 100644 types/constants.d.ts create mode 100644 types/json/process.d.ts 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/ish.js b/src/dom/ish.js index 4d65b6b..f5475d9 100644 --- a/src/dom/ish.js +++ b/src/dom/ish.js @@ -54,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 index 4375052..c6827ed 100644 --- a/src/dom/node.js +++ b/src/dom/node.js @@ -8,7 +8,14 @@ import props from './props.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 './update.js'; + +import { + ARRAY, + COMMENT, + COMPONENT, + KEY, + REF, +} from '../constants.js'; /** @typedef {globalThis.Element | globalThis.HTMLElement | globalThis.SVGSVGElement | globalThis.DocumentFragment} Container */ diff --git a/src/dom/process.js b/src/dom/process.js index 067cd4b..f2b557a 100644 --- a/src/dom/process.js +++ b/src/dom/process.js @@ -11,7 +11,8 @@ import { import parser from '../parser/index.js'; import templates from './templates.js'; -import { isKeyed, fragment, update, pdt } from './update.js'; +import { isKeyed, fragment, update } from './update.js'; +import { pdt } from '../utils.js'; const parse = parser({ Comment, diff --git a/src/dom/rabbit.js b/src/dom/rabbit.js index a5044c4..3a46868 100644 --- a/src/dom/rabbit.js +++ b/src/dom/rabbit.js @@ -7,7 +7,7 @@ import resolve from './resolve.js'; import { children } from './ish.js'; import { isArray } from '../utils.js'; import { PersistentFragment, diffFragment, nodes } from './persistent-fragment.js'; -import { ARRAY, COMMENT, COMPONENT, EVENT, KEY, REF } from './update.js'; +import { ARRAY, COMMENT, COMPONENT, EVENT, KEY, REF } from '../constants.js'; import { _get as getDirect, _set as setDirect } from './direct.js'; 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/update.js b/src/dom/update.js index 5b78a81..bb1b37b 100644 --- a/src/dom/update.js +++ b/src/dom/update.js @@ -9,47 +9,35 @@ import { TEXT as TEMPLATE_TEXT, } from './ish.js'; -import { Unsafe, assign, entries, reduce, isArray } from '../utils.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; - -// COMPONENT flags -const COMPONENT_DIRECT = COMPONENT | DIRECT; -const COMPONENT_DOTS = COMPONENT | DOTS; -const COMPONENT_PROP = COMPONENT | PROP; - -// ARRAY flags -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); -// /** -// * @param {number[]} path -// * @param {unknown} detail -// * @param {typeof COMPONENT | typeof COMMENT_ARRAY | typeof UNSAFE | typeof COMMENT | typeof TEXT | typeof EVENT_ARRAY | typeof EVENT | typeof TOGGLE | typeof COMPONENT_DOTS | typeof DOTS | typeof COMPONENT_DIRECT | typeof DIRECT | typeof COMPONENT_PROP | typeof ARIA | typeof DATA | typeof KEY | typeof REF | typeof ATTRIBUTE} type -// * @returns -// */ -export const pdt = (path, detail, type) => ({ p: path, d: detail, t: type }); - const aria = (node, curr, prev) => { if (prev !== curr) { for (const [key, value] of entries(curr)) @@ -185,9 +173,9 @@ function attribute(node, curr) { return curr; } -function direct(node, curr) { +function direct(ref, curr) { 'use strict'; - node[this] = curr; + ref[this] = curr; return curr; } diff --git a/src/json/index.js b/src/json/index.js index 306e7f7..6045868 100644 --- a/src/json/index.js +++ b/src/json/index.js @@ -3,68 +3,80 @@ 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'; +import { ARRAY, COMMENT, COMPONENT, KEY, REF } from '../constants.js'; -const textParser = parser({ - Comment, - DocumentType, - Text, - Fragment, - Element, - Component, - update, -}); - -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 (DEBUG) throw errors.invalid_template(); - return root; - }; + 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) 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 76f6c98..0194650 100644 --- a/src/json/update.js +++ b/src/json/update.js @@ -108,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/utils.js b/src/utils.js index b5e61ff..77fcf2a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -46,4 +46,6 @@ export const unsafe = (template, ...values) => new Unsafe( ); 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/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/ish.d.ts b/types/dom/ish.d.ts index d955888..30b819e 100644 --- a/types/dom/ish.d.ts +++ b/types/dom/ish.d.ts @@ -12,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/update.d.ts b/types/dom/update.d.ts index 57c0830..e588a6a 100644 --- a/types/dom/update.d.ts +++ b/types/dom/update.d.ts @@ -1,24 +1,4 @@ -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 fragment: (content: string, xml?: boolean) => DocumentFragment; -export function pdt(path: any, detail: any, type: any): { - p: any; - d: any; - t: any; -}; export function isKeyed(): boolean; export function update(node: any, type: any, path: any, name: any, hint: any): { p: 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/utils.d.ts b/types/utils.d.ts index e2f867e..2ccbfea 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -8,6 +8,11 @@ export class 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; pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy