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.diff

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