@mindees/renderer 0.22.4 → 0.22.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dom.js +5 -0
- package/dist/dom.js.map +1 -1
- package/dist/headless.d.ts.map +1 -1
- package/dist/headless.js +7 -5
- package/dist/headless.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/dom.js
CHANGED
|
@@ -102,6 +102,11 @@ function createDomBackend(doc) {
|
|
|
102
102
|
}
|
|
103
103
|
if (key === "style") {
|
|
104
104
|
const style = el.style;
|
|
105
|
+
if (typeof value === "string") {
|
|
106
|
+
style.cssText = value;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (typeof prev === "string") style.cssText = "";
|
|
105
110
|
const next = value && typeof value === "object" ? value : null;
|
|
106
111
|
const prevObj = prev && typeof prev === "object" ? prev : null;
|
|
107
112
|
if (prevObj) {
|
package/dist/dom.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dom.js","names":[],"sources":["../src/dom.ts"],"sourcesContent":["/**\n * DOM host backend — renders to real `Node`s in a browser (or any DOM, such as\n * happy-dom/jsdom in tests).\n *\n * MindeesNative element tags map to DOM via a small alias table so the same app\n * tree (`view`, `text`, …) renders to sensible HTML; unknown tags pass through\n * as custom elements. Props become attributes, except `onX` event props (added\n * as listeners) and `style` objects (applied to `style`).\n *\n * @module\n */\n\nimport type { HostBackend } from './backend'\nimport { styleValue } from './css'\n\n/** A minimal structural view of the DOM we use (so types don't require `lib.dom`). */\ninterface DomDocument {\n createElement(tag: string): DomElement\n createTextNode(data: string): DomText\n}\ninterface DomNode {\n parentNode: DomNode | null\n nextSibling: DomNode | null\n nodeType: number\n removeChild(child: DomNode): DomNode\n insertBefore(node: DomNode, ref: DomNode | null): DomNode\n}\ninterface DomElement extends DomNode {\n setAttribute(name: string, value: string): void\n removeAttribute(name: string): void\n addEventListener(type: string, listener: (e: unknown) => void): void\n removeEventListener(type: string, listener: (e: unknown) => void): void\n style: Record<string, string> & { cssText: string }\n}\ninterface DomText extends DomNode {\n data: string\n}\n\nconst TEXT_NODE = 3\n\n/** Tag aliases: MindeesNative semantic tags → HTML elements on the web target. */\nconst TAG_ALIASES: Record<string, string> = {\n view: 'div',\n text: 'span',\n image: 'img',\n scrollview: 'div',\n horizontalscrollview: 'div',\n textinput: 'input',\n button: 'button',\n}\n\n/** Map a MindeesNative tag to its DOM tag. Unknown tags pass through. */\nexport function domTagFor(type: string): string {\n return TAG_ALIASES[type] ?? type\n}\n\n/**\n * Inject the keyframes the `activityindicator` spinner needs, once per document. Uses a\n * fuller DOM shape than {@link DomDocument}; on minimal/headless documents (no `head`) it's a\n * no-op (tests don't need the animation, and the element still renders).\n */\nfunction ensureSpinnerKeyframes(document: DomDocument): void {\n const doc = document as unknown as {\n getElementById?: (id: string) => unknown\n head?: { appendChild: (node: unknown) => void }\n createElement: (tag: string) => { id: string; textContent: string }\n }\n if (!doc.head || typeof doc.getElementById !== 'function') return\n if (doc.getElementById('mindees-keyframes')) return\n const style = doc.createElement('style')\n style.id = 'mindees-keyframes'\n style.textContent = '@keyframes mindees-spin{to{transform:rotate(360deg)}}'\n doc.head.appendChild(style)\n}\n\n/**\n * Build the web `activityindicator`: a CSS border-spinner. Size comes from the element's\n * `width`/`height` style and the active arc from its `color` (`currentColor`), both applied\n * normally afterwards — so the backend owns only the animation + ring shape.\n */\nfunction createSpinner(document: DomDocument): DomElement {\n ensureSpinnerKeyframes(document)\n const el = document.createElement('div')\n const s = el.style\n s.boxSizing = 'border-box'\n s.display = 'inline-block'\n s.borderStyle = 'solid'\n s.borderWidth = '3px'\n s.borderColor = 'rgba(127, 127, 127, 0.30)'\n s.borderTopColor = 'currentColor'\n s.borderRadius = '50%'\n s.animation = 'mindees-spin 0.8s linear infinite'\n return el\n}\n\nfunction isEventProp(key: string): boolean {\n return (\n key.length > 2 && key[0] === 'o' && key[1] === 'n' && key[2] === (key[2] ?? '').toUpperCase()\n )\n}\n\n/**\n * Form-control props that must be written as the live DOM **property** (not an attribute). The\n * `value`/`checked` *attribute* only seeds the DEFAULT; once the user edits, the property and\n * attribute diverge, so a controlled update must set the property to change what's shown.\n */\nconst FORM_PROPERTIES = new Set(['value', 'checked', 'selected', 'indeterminate'])\n\n/** `onClick` → `click`, `onPointerDown` → `pointerdown`. */\nfunction eventNameFor(key: string): string {\n return key.slice(2).toLowerCase()\n}\n\n/** Listeners we've attached, so reactive updates can swap them cleanly. */\nconst listeners = new WeakMap<object, Map<string, (e: unknown) => void>>()\n\n/**\n * Create a {@link HostBackend} that renders to real DOM nodes.\n *\n * @param doc - The document to create nodes with. Defaults to the global\n * `document`; pass a happy-dom/jsdom document for headless tests.\n */\nexport function createDomBackend(doc?: DomDocument): HostBackend<DomNode> {\n const documentRef = doc ?? (globalThis as unknown as { document?: DomDocument }).document\n if (!documentRef) {\n throw new Error(\n 'createDomBackend: no document available (pass one explicitly outside a browser)',\n )\n }\n const document = documentRef\n let overlay: DomElement | null = null // lazily-created portal overlay layer (see overlayRoot)\n\n return {\n createElement: (type) =>\n type === 'activityindicator'\n ? createSpinner(document)\n : document.createElement(domTagFor(type)),\n createText: (value) => document.createTextNode(value),\n\n setProp(node, key, value, prev): void {\n const el = node as DomElement\n if (isEventProp(key)) {\n const event = eventNameFor(key)\n let map = listeners.get(el)\n if (!map) {\n map = new Map()\n listeners.set(el, map)\n }\n const old = map.get(event)\n if (old) el.removeEventListener(event, old)\n if (typeof value === 'function') {\n const fn = value as (e: unknown) => void\n el.addEventListener(event, fn)\n map.set(event, fn)\n } else {\n map.delete(event)\n }\n return\n }\n if (key === 'style') {\n const style = el.style\n const next = value && typeof value === 'object' ? (value as Record<string, unknown>) : null\n const prevObj = prev && typeof prev === 'object' ? (prev as Record<string, unknown>) : null\n // Clear keys present in the previous style object but absent from the\n // new one, so stale inline styles don't persist across reactive updates.\n if (prevObj) {\n for (const prop of Object.keys(prevObj)) {\n if (!next || !(prop in next)) style[prop] = ''\n }\n }\n if (next) {\n for (const [prop, v] of Object.entries(next)) {\n // Nullish → unset (don't write the literal \"undefined\"/\"null\").\n style[prop] = v === null || v === undefined ? '' : styleValue(prop, v)\n }\n }\n return\n }\n if (FORM_PROPERTIES.has(key)) {\n // Live property, so a controlled `value`/`checked` updates what the user sees.\n ;(el as unknown as Record<string, unknown>)[key] =\n value === null || value === undefined ? '' : value\n return\n }\n if (value === false || value === null || value === undefined) {\n el.removeAttribute(key)\n } else {\n el.setAttribute(key, value === true ? '' : String(value))\n }\n void prev\n },\n\n setText(node, value): void {\n ;(node as DomText).data = value\n },\n\n insert(parent, node, anchor): void {\n ;(parent as DomNode).insertBefore(node, anchor)\n },\n\n remove(parent, node): void {\n ;(parent as DomNode).removeChild(node)\n },\n\n parentOf: (node) => node.parentNode,\n nextSibling: (node) => node.nextSibling,\n isText: (node) => node.nodeType === TEXT_NODE,\n\n overlayRoot(): DomNode | null {\n if (overlay) return overlay\n // Lazily create ONE overlay layer on <body> (fallback <html>) — zero-config portals.\n const host = document as unknown as {\n body?: { appendChild(n: DomNode): void }\n documentElement?: { appendChild(n: DomNode): void }\n }\n const mount = host.body ?? host.documentElement\n if (!mount) return null // no DOM host (e.g. a bare document) → portal falls back in place\n const layer = document.createElement('div')\n layer.setAttribute('data-mindees-overlay', '')\n mount.appendChild(layer)\n overlay = layer\n return overlay\n },\n }\n}\n\nexport type { DomDocument, DomElement, DomNode, DomText }\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"dom.js","names":[],"sources":["../src/dom.ts"],"sourcesContent":["/**\n * DOM host backend — renders to real `Node`s in a browser (or any DOM, such as\n * happy-dom/jsdom in tests).\n *\n * MindeesNative element tags map to DOM via a small alias table so the same app\n * tree (`view`, `text`, …) renders to sensible HTML; unknown tags pass through\n * as custom elements. Props become attributes, except `onX` event props (added\n * as listeners) and `style` objects (applied to `style`).\n *\n * @module\n */\n\nimport type { HostBackend } from './backend'\nimport { styleValue } from './css'\n\n/** A minimal structural view of the DOM we use (so types don't require `lib.dom`). */\ninterface DomDocument {\n createElement(tag: string): DomElement\n createTextNode(data: string): DomText\n}\ninterface DomNode {\n parentNode: DomNode | null\n nextSibling: DomNode | null\n nodeType: number\n removeChild(child: DomNode): DomNode\n insertBefore(node: DomNode, ref: DomNode | null): DomNode\n}\ninterface DomElement extends DomNode {\n setAttribute(name: string, value: string): void\n removeAttribute(name: string): void\n addEventListener(type: string, listener: (e: unknown) => void): void\n removeEventListener(type: string, listener: (e: unknown) => void): void\n style: Record<string, string> & { cssText: string }\n}\ninterface DomText extends DomNode {\n data: string\n}\n\nconst TEXT_NODE = 3\n\n/** Tag aliases: MindeesNative semantic tags → HTML elements on the web target. */\nconst TAG_ALIASES: Record<string, string> = {\n view: 'div',\n text: 'span',\n image: 'img',\n scrollview: 'div',\n horizontalscrollview: 'div',\n textinput: 'input',\n button: 'button',\n}\n\n/** Map a MindeesNative tag to its DOM tag. Unknown tags pass through. */\nexport function domTagFor(type: string): string {\n return TAG_ALIASES[type] ?? type\n}\n\n/**\n * Inject the keyframes the `activityindicator` spinner needs, once per document. Uses a\n * fuller DOM shape than {@link DomDocument}; on minimal/headless documents (no `head`) it's a\n * no-op (tests don't need the animation, and the element still renders).\n */\nfunction ensureSpinnerKeyframes(document: DomDocument): void {\n const doc = document as unknown as {\n getElementById?: (id: string) => unknown\n head?: { appendChild: (node: unknown) => void }\n createElement: (tag: string) => { id: string; textContent: string }\n }\n if (!doc.head || typeof doc.getElementById !== 'function') return\n if (doc.getElementById('mindees-keyframes')) return\n const style = doc.createElement('style')\n style.id = 'mindees-keyframes'\n style.textContent = '@keyframes mindees-spin{to{transform:rotate(360deg)}}'\n doc.head.appendChild(style)\n}\n\n/**\n * Build the web `activityindicator`: a CSS border-spinner. Size comes from the element's\n * `width`/`height` style and the active arc from its `color` (`currentColor`), both applied\n * normally afterwards — so the backend owns only the animation + ring shape.\n */\nfunction createSpinner(document: DomDocument): DomElement {\n ensureSpinnerKeyframes(document)\n const el = document.createElement('div')\n const s = el.style\n s.boxSizing = 'border-box'\n s.display = 'inline-block'\n s.borderStyle = 'solid'\n s.borderWidth = '3px'\n s.borderColor = 'rgba(127, 127, 127, 0.30)'\n s.borderTopColor = 'currentColor'\n s.borderRadius = '50%'\n s.animation = 'mindees-spin 0.8s linear infinite'\n return el\n}\n\nfunction isEventProp(key: string): boolean {\n return (\n key.length > 2 && key[0] === 'o' && key[1] === 'n' && key[2] === (key[2] ?? '').toUpperCase()\n )\n}\n\n/**\n * Form-control props that must be written as the live DOM **property** (not an attribute). The\n * `value`/`checked` *attribute* only seeds the DEFAULT; once the user edits, the property and\n * attribute diverge, so a controlled update must set the property to change what's shown.\n */\nconst FORM_PROPERTIES = new Set(['value', 'checked', 'selected', 'indeterminate'])\n\n/** `onClick` → `click`, `onPointerDown` → `pointerdown`. */\nfunction eventNameFor(key: string): string {\n return key.slice(2).toLowerCase()\n}\n\n/** Listeners we've attached, so reactive updates can swap them cleanly. */\nconst listeners = new WeakMap<object, Map<string, (e: unknown) => void>>()\n\n/**\n * Create a {@link HostBackend} that renders to real DOM nodes.\n *\n * @param doc - The document to create nodes with. Defaults to the global\n * `document`; pass a happy-dom/jsdom document for headless tests.\n */\nexport function createDomBackend(doc?: DomDocument): HostBackend<DomNode> {\n const documentRef = doc ?? (globalThis as unknown as { document?: DomDocument }).document\n if (!documentRef) {\n throw new Error(\n 'createDomBackend: no document available (pass one explicitly outside a browser)',\n )\n }\n const document = documentRef\n let overlay: DomElement | null = null // lazily-created portal overlay layer (see overlayRoot)\n\n return {\n createElement: (type) =>\n type === 'activityindicator'\n ? createSpinner(document)\n : document.createElement(domTagFor(type)),\n createText: (value) => document.createTextNode(value),\n\n setProp(node, key, value, prev): void {\n const el = node as DomElement\n if (isEventProp(key)) {\n const event = eventNameFor(key)\n let map = listeners.get(el)\n if (!map) {\n map = new Map()\n listeners.set(el, map)\n }\n const old = map.get(event)\n if (old) el.removeEventListener(event, old)\n if (typeof value === 'function') {\n const fn = value as (e: unknown) => void\n el.addEventListener(event, fn)\n map.set(event, fn)\n } else {\n map.delete(event)\n }\n return\n }\n if (key === 'style') {\n const style = el.style\n // A STRING style (`style=\"color:red\"`) → set cssText wholesale, symmetric with SSR which emits\n // the string verbatim (was silently dropped before, breaking styling + hydration parity).\n if (typeof value === 'string') {\n style.cssText = value\n return\n }\n // Switching away from a previous string style: clear it wholesale before the object diff.\n if (typeof prev === 'string') style.cssText = ''\n const next = value && typeof value === 'object' ? (value as Record<string, unknown>) : null\n const prevObj = prev && typeof prev === 'object' ? (prev as Record<string, unknown>) : null\n // Clear keys present in the previous style object but absent from the\n // new one, so stale inline styles don't persist across reactive updates.\n if (prevObj) {\n for (const prop of Object.keys(prevObj)) {\n if (!next || !(prop in next)) style[prop] = ''\n }\n }\n if (next) {\n for (const [prop, v] of Object.entries(next)) {\n // Nullish → unset (don't write the literal \"undefined\"/\"null\").\n style[prop] = v === null || v === undefined ? '' : styleValue(prop, v)\n }\n }\n return\n }\n if (FORM_PROPERTIES.has(key)) {\n // Live property, so a controlled `value`/`checked` updates what the user sees.\n ;(el as unknown as Record<string, unknown>)[key] =\n value === null || value === undefined ? '' : value\n return\n }\n if (value === false || value === null || value === undefined) {\n el.removeAttribute(key)\n } else {\n el.setAttribute(key, value === true ? '' : String(value))\n }\n void prev\n },\n\n setText(node, value): void {\n ;(node as DomText).data = value\n },\n\n insert(parent, node, anchor): void {\n ;(parent as DomNode).insertBefore(node, anchor)\n },\n\n remove(parent, node): void {\n ;(parent as DomNode).removeChild(node)\n },\n\n parentOf: (node) => node.parentNode,\n nextSibling: (node) => node.nextSibling,\n isText: (node) => node.nodeType === TEXT_NODE,\n\n overlayRoot(): DomNode | null {\n if (overlay) return overlay\n // Lazily create ONE overlay layer on <body> (fallback <html>) — zero-config portals.\n const host = document as unknown as {\n body?: { appendChild(n: DomNode): void }\n documentElement?: { appendChild(n: DomNode): void }\n }\n const mount = host.body ?? host.documentElement\n if (!mount) return null // no DOM host (e.g. a bare document) → portal falls back in place\n const layer = document.createElement('div')\n layer.setAttribute('data-mindees-overlay', '')\n mount.appendChild(layer)\n overlay = layer\n return overlay\n },\n }\n}\n\nexport type { DomDocument, DomElement, DomNode, DomText }\n"],"mappings":"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"}
|
package/dist/headless.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headless.d.ts","names":[],"sources":["../src/headless.ts"],"mappings":";;;;UAciB,YAAA;EAIf;EAFA,IAAA;EAIA;EAFA,KAAA,EAAO,MAAA;EAIG;EAFV,IAAA;EAIQ;EAFR,QAAA,EAAU,YAAA;EAEU;EAApB,MAAA,EAAQ,YAAA;AAAA;;
|
|
1
|
+
{"version":3,"file":"headless.d.ts","names":[],"sources":["../src/headless.ts"],"mappings":";;;;UAciB,YAAA;EAIf;EAFA,IAAA;EAIA;EAFA,KAAA,EAAO,MAAA;EAIG;EAFV,IAAA;EAIQ;EAFR,QAAA,EAAU,YAAA;EAEU;EAApB,MAAA,EAAQ,YAAA;AAAA;;UA4FO,sBAAA;EAMoB;AAIrC;;;;EAJqC,SAA1B,WAAA,GAAc,YAAY;AAAA;;iBAIrB,qBAAA,CACd,OAAA,GAAS,sBAAA,GACR,mBAAA,CAAoB,YAAA;;iBAwEP,WAAA,CAAY,GAAW;;iBAOvB,kBAAA,CAAmB,IAAA,YAAgB,YAAY"}
|
package/dist/headless.js
CHANGED
|
@@ -8,11 +8,13 @@ function escapeText(value) {
|
|
|
8
8
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
* Render an attribute value
|
|
12
|
-
* the DOM backend
|
|
11
|
+
* Render an attribute value. ONLY the `style` attribute runs the CSS serializer (kebab-case + `px`),
|
|
12
|
+
* matching the DOM backend so SSR markup equals the hydrated DOM. Gating on the NAME (not the value
|
|
13
|
+
* type) is essential: a non-`style` object prop (e.g. `data-config={{…}}`) must serialize the same way
|
|
14
|
+
* the DOM backend does (`String(value)`), not get CSS-mangled — otherwise SSR/DOM hydration diverges.
|
|
13
15
|
*/
|
|
14
|
-
function serializeAttrValue(value) {
|
|
15
|
-
if (value && typeof value === "object") return serializeStyle(value);
|
|
16
|
+
function serializeAttrValue(key, value) {
|
|
17
|
+
if (key === "style" && value && typeof value === "object") return serializeStyle(value);
|
|
16
18
|
return String(value);
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
@@ -37,7 +39,7 @@ function serializeHeadless(node, options) {
|
|
|
37
39
|
if (node.type === TEXT) return escapeText(node.text);
|
|
38
40
|
const tag = (options?.mapTag ?? ((t) => t))(node.type);
|
|
39
41
|
if (!isValidAttrName(tag)) throw new Error(`refusing to serialize unsafe element tag: ${JSON.stringify(tag)}`);
|
|
40
|
-
const attrs = Object.entries(node.props).filter(([key]) => !isEventProp(key) && isValidAttrName(key)).map(([key, value]) => value === true ? ` ${key}=""` : ` ${key}="${escapeAttr(serializeAttrValue(value))}"`).join("");
|
|
42
|
+
const attrs = Object.entries(node.props).filter(([key]) => !isEventProp(key) && isValidAttrName(key)).map(([key, value]) => value === true ? ` ${key}=""` : ` ${key}="${escapeAttr(serializeAttrValue(key, value))}"`).join("");
|
|
41
43
|
if (VOID_ELEMENTS.has(tag)) return `<${tag}${attrs}>`;
|
|
42
44
|
return `<${tag}${attrs}>${node.children.map((c) => serializeHeadless(c, options)).join("")}</${tag}>`;
|
|
43
45
|
}
|
package/dist/headless.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headless.js","names":[],"sources":["../src/headless.ts"],"sourcesContent":["/**\n * Headless host backend — an in-memory host tree, no browser required.\n *\n * This is the **reference backend**: it implements the full {@link HostBackend}\n * (plus {@link SerializableBackend}) so the entire reconciler can be exercised\n * in CI without a DOM. It's also handy for snapshot-testing rendered output.\n *\n * @module\n */\n\nimport type { SerializableBackend, SerializeOptions } from './backend'\nimport { serializeStyle } from './css'\n\n/** A headless host node: an element (with tag/props/children) or a text node. */\nexport interface HeadlessNode {\n /** `\"#text\"` for text nodes, otherwise the element tag. */\n type: string\n /** Applied props (elements only). */\n props: Record<string, unknown>\n /** Text content (text nodes only). */\n text: string\n /** Child nodes (elements only). */\n children: HeadlessNode[]\n /** Back-pointer to the parent, or `null` when detached. */\n parent: HeadlessNode | null\n}\n\nconst TEXT = '#text'\n\nfunction escapeAttr(value: string): string {\n return value.replace(/&/g, '&').replace(/\"/g, '"').replace(/</g, '<')\n}\n\nfunction escapeText(value: string): string {\n return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\n}\n\n/**\n * Render an attribute value: a `style` object becomes a CSS string via the SAME serializer\n * the DOM backend uses (kebab-case names + `px` units), so SSR markup matches the hydrated DOM.\n */\nfunction serializeAttrValue(value: unknown): string {\n if (value && typeof value === 'object') {\n return serializeStyle(value as Record<string, unknown>)\n }\n return String(value)\n}\n\n/**\n * Whether `key` is a safe HTML attribute name. Attribute NAMES are interpolated\n * into markup unescaped, so a name containing `>`, whitespace, quotes, `=`, `/`,\n * etc. could break out of the tag and inject markup (stored XSS when props are\n * built from user/server data). We emit only names that match the HTML name\n * grammar — matching what the DOM's `setAttribute` would accept — and drop the\n * rest, exactly as an invalid name would never reach the DOM either.\n */\nfunction isValidAttrName(key: string): boolean {\n return /^[A-Za-z_:][\\w:.-]*$/.test(key)\n}\n\n/**\n * Serialize a headless node (and subtree) to HTML. A standalone function, not an\n * object method: the public {@link SerializableBackend.serialize} is typed as a\n * plain function member, so a consumer may legally detach it\n * (`const { serialize } = backend`). Recursing through this lexical helper rather\n * than `this.serialize` keeps it binding-independent.\n */\nfunction serializeHeadless(node: HeadlessNode, options?: SerializeOptions): string {\n if (node.type === TEXT) return escapeText(node.text)\n const mapTag = options?.mapTag ?? ((t: string) => t)\n const tag = mapTag(node.type)\n // The tag is interpolated into `<tag>`/`</tag>` unescaped, so a tag containing `>`,\n // whitespace, etc. would break out of the element and inject markup. Reject any tag\n // that isn't a valid name (same grammar as attribute names) — fail closed.\n if (!isValidAttrName(tag)) {\n throw new Error(`refusing to serialize unsafe element tag: ${JSON.stringify(tag)}`)\n }\n const attrs = Object.entries(node.props)\n .filter(([key]) => !isEventProp(key) && isValidAttrName(key))\n .map(([key, value]) =>\n // Boolean `true` → a valueless attribute (`disabled=\"\"`), matching the DOM\n // backend (dom.ts) so SSR markup equals hydrated markup.\n value === true ? ` ${key}=\"\"` : ` ${key}=\"${escapeAttr(serializeAttrValue(value))}\"`,\n )\n .join('')\n // HTML void elements (e.g. `img`, `input` — what `image`/`textinput` map to) have NO closing tag\n // and NO children: emitting `<img>...</img>` is malformed and the browser reparents the children as\n // siblings, diverging from the reconciler's tree. Emit a self-contained start tag only.\n if (VOID_ELEMENTS.has(tag)) {\n return `<${tag}${attrs}>`\n }\n const inner = node.children.map((c) => serializeHeadless(c, options)).join('')\n return `<${tag}${attrs}>${inner}</${tag}>`\n}\n\n/** HTML void elements — serialized with no closing tag and no children (post-`mapTag` names). */\nconst VOID_ELEMENTS = new Set([\n 'area',\n 'base',\n 'br',\n 'col',\n 'embed',\n 'hr',\n 'img',\n 'input',\n 'link',\n 'meta',\n 'param',\n 'source',\n 'track',\n 'wbr',\n])\n\n/** Options for {@link createHeadlessBackend}. */\nexport interface HeadlessBackendOptions {\n /**\n * A designated overlay node for portals. Omit (the default) and `overlayRoot` is unimplemented,\n * so portals mount IN PLACE — the SSR-correct behavior (`renderToString` only serializes the\n * root's own children). Pass a node to test relocated portal placement.\n */\n readonly overlayRoot?: HeadlessNode\n}\n\n/** Create a {@link SerializableBackend} backed by an in-memory tree. */\nexport function createHeadlessBackend(\n options: HeadlessBackendOptions = {},\n): SerializableBackend<HeadlessNode> {\n const backend: SerializableBackend<HeadlessNode> = {\n createElement(type: string): HeadlessNode {\n return { type, props: {}, text: '', children: [], parent: null }\n },\n\n createText(value: string): HeadlessNode {\n return { type: TEXT, props: {}, text: value, children: [], parent: null }\n },\n\n setProp(node, key, value): void {\n // Event handlers and falsy values are tracked but not serialized as attrs.\n if (value === undefined || value === null || value === false) {\n delete node.props[key]\n } else {\n node.props[key] = value\n }\n },\n\n setText(node, value): void {\n node.text = value\n },\n\n insert(parent, node, anchor): void {\n if (node.parent) {\n const prevSiblings = node.parent.children\n const at = prevSiblings.indexOf(node)\n if (at >= 0) prevSiblings.splice(at, 1)\n }\n node.parent = parent\n if (anchor === null) {\n parent.children.push(node)\n } else {\n const idx = parent.children.indexOf(anchor)\n parent.children.splice(idx < 0 ? parent.children.length : idx, 0, node)\n }\n },\n\n remove(parent, node): void {\n const idx = parent.children.indexOf(node)\n if (idx >= 0) parent.children.splice(idx, 1)\n node.parent = null\n },\n\n parentOf(node): HeadlessNode | null {\n return node.parent\n },\n\n nextSibling(node): HeadlessNode | null {\n const parent = node.parent\n if (!parent) return null\n const idx = parent.children.indexOf(node)\n return idx >= 0 && idx + 1 < parent.children.length\n ? (parent.children[idx + 1] ?? null)\n : null\n },\n\n isText(node): boolean {\n return node.type === TEXT\n },\n\n serialize: serializeHeadless,\n }\n // Only expose overlayRoot when a target was provided, so the default stays in-place (SSR-correct).\n if (options.overlayRoot) {\n const target = options.overlayRoot\n backend.overlayRoot = () => target\n }\n return backend\n}\n\n/** Whether a prop key is an event handler (`onClick`, `onPress`, …). */\nexport function isEventProp(key: string): boolean {\n return (\n key.length > 2 && key[0] === 'o' && key[1] === 'n' && key[2] === (key[2] ?? '').toUpperCase()\n )\n}\n\n/** Convenience: create a detached headless root element (default tag `\"root\"`). */\nexport function createHeadlessRoot(type = 'root'): HeadlessNode {\n return { type, props: {}, text: '', children: [], parent: null }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"headless.js","names":[],"sources":["../src/headless.ts"],"sourcesContent":["/**\n * Headless host backend — an in-memory host tree, no browser required.\n *\n * This is the **reference backend**: it implements the full {@link HostBackend}\n * (plus {@link SerializableBackend}) so the entire reconciler can be exercised\n * in CI without a DOM. It's also handy for snapshot-testing rendered output.\n *\n * @module\n */\n\nimport type { SerializableBackend, SerializeOptions } from './backend'\nimport { serializeStyle } from './css'\n\n/** A headless host node: an element (with tag/props/children) or a text node. */\nexport interface HeadlessNode {\n /** `\"#text\"` for text nodes, otherwise the element tag. */\n type: string\n /** Applied props (elements only). */\n props: Record<string, unknown>\n /** Text content (text nodes only). */\n text: string\n /** Child nodes (elements only). */\n children: HeadlessNode[]\n /** Back-pointer to the parent, or `null` when detached. */\n parent: HeadlessNode | null\n}\n\nconst TEXT = '#text'\n\nfunction escapeAttr(value: string): string {\n return value.replace(/&/g, '&').replace(/\"/g, '"').replace(/</g, '<')\n}\n\nfunction escapeText(value: string): string {\n return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\n}\n\n/**\n * Render an attribute value. ONLY the `style` attribute runs the CSS serializer (kebab-case + `px`),\n * matching the DOM backend so SSR markup equals the hydrated DOM. Gating on the NAME (not the value\n * type) is essential: a non-`style` object prop (e.g. `data-config={{…}}`) must serialize the same way\n * the DOM backend does (`String(value)`), not get CSS-mangled — otherwise SSR/DOM hydration diverges.\n */\nfunction serializeAttrValue(key: string, value: unknown): string {\n if (key === 'style' && value && typeof value === 'object') {\n return serializeStyle(value as Record<string, unknown>)\n }\n return String(value)\n}\n\n/**\n * Whether `key` is a safe HTML attribute name. Attribute NAMES are interpolated\n * into markup unescaped, so a name containing `>`, whitespace, quotes, `=`, `/`,\n * etc. could break out of the tag and inject markup (stored XSS when props are\n * built from user/server data). We emit only names that match the HTML name\n * grammar — matching what the DOM's `setAttribute` would accept — and drop the\n * rest, exactly as an invalid name would never reach the DOM either.\n */\nfunction isValidAttrName(key: string): boolean {\n return /^[A-Za-z_:][\\w:.-]*$/.test(key)\n}\n\n/**\n * Serialize a headless node (and subtree) to HTML. A standalone function, not an\n * object method: the public {@link SerializableBackend.serialize} is typed as a\n * plain function member, so a consumer may legally detach it\n * (`const { serialize } = backend`). Recursing through this lexical helper rather\n * than `this.serialize` keeps it binding-independent.\n */\nfunction serializeHeadless(node: HeadlessNode, options?: SerializeOptions): string {\n if (node.type === TEXT) return escapeText(node.text)\n const mapTag = options?.mapTag ?? ((t: string) => t)\n const tag = mapTag(node.type)\n // The tag is interpolated into `<tag>`/`</tag>` unescaped, so a tag containing `>`,\n // whitespace, etc. would break out of the element and inject markup. Reject any tag\n // that isn't a valid name (same grammar as attribute names) — fail closed.\n if (!isValidAttrName(tag)) {\n throw new Error(`refusing to serialize unsafe element tag: ${JSON.stringify(tag)}`)\n }\n const attrs = Object.entries(node.props)\n .filter(([key]) => !isEventProp(key) && isValidAttrName(key))\n .map(([key, value]) =>\n // Boolean `true` → a valueless attribute (`disabled=\"\"`), matching the DOM\n // backend (dom.ts) so SSR markup equals hydrated markup.\n value === true ? ` ${key}=\"\"` : ` ${key}=\"${escapeAttr(serializeAttrValue(key, value))}\"`,\n )\n .join('')\n // HTML void elements (e.g. `img`, `input` — what `image`/`textinput` map to) have NO closing tag\n // and NO children: emitting `<img>...</img>` is malformed and the browser reparents the children as\n // siblings, diverging from the reconciler's tree. Emit a self-contained start tag only.\n if (VOID_ELEMENTS.has(tag)) {\n return `<${tag}${attrs}>`\n }\n const inner = node.children.map((c) => serializeHeadless(c, options)).join('')\n return `<${tag}${attrs}>${inner}</${tag}>`\n}\n\n/** HTML void elements — serialized with no closing tag and no children (post-`mapTag` names). */\nconst VOID_ELEMENTS = new Set([\n 'area',\n 'base',\n 'br',\n 'col',\n 'embed',\n 'hr',\n 'img',\n 'input',\n 'link',\n 'meta',\n 'param',\n 'source',\n 'track',\n 'wbr',\n])\n\n/** Options for {@link createHeadlessBackend}. */\nexport interface HeadlessBackendOptions {\n /**\n * A designated overlay node for portals. Omit (the default) and `overlayRoot` is unimplemented,\n * so portals mount IN PLACE — the SSR-correct behavior (`renderToString` only serializes the\n * root's own children). Pass a node to test relocated portal placement.\n */\n readonly overlayRoot?: HeadlessNode\n}\n\n/** Create a {@link SerializableBackend} backed by an in-memory tree. */\nexport function createHeadlessBackend(\n options: HeadlessBackendOptions = {},\n): SerializableBackend<HeadlessNode> {\n const backend: SerializableBackend<HeadlessNode> = {\n createElement(type: string): HeadlessNode {\n return { type, props: {}, text: '', children: [], parent: null }\n },\n\n createText(value: string): HeadlessNode {\n return { type: TEXT, props: {}, text: value, children: [], parent: null }\n },\n\n setProp(node, key, value): void {\n // Event handlers and falsy values are tracked but not serialized as attrs.\n if (value === undefined || value === null || value === false) {\n delete node.props[key]\n } else {\n node.props[key] = value\n }\n },\n\n setText(node, value): void {\n node.text = value\n },\n\n insert(parent, node, anchor): void {\n if (node.parent) {\n const prevSiblings = node.parent.children\n const at = prevSiblings.indexOf(node)\n if (at >= 0) prevSiblings.splice(at, 1)\n }\n node.parent = parent\n if (anchor === null) {\n parent.children.push(node)\n } else {\n const idx = parent.children.indexOf(anchor)\n parent.children.splice(idx < 0 ? parent.children.length : idx, 0, node)\n }\n },\n\n remove(parent, node): void {\n const idx = parent.children.indexOf(node)\n if (idx >= 0) parent.children.splice(idx, 1)\n node.parent = null\n },\n\n parentOf(node): HeadlessNode | null {\n return node.parent\n },\n\n nextSibling(node): HeadlessNode | null {\n const parent = node.parent\n if (!parent) return null\n const idx = parent.children.indexOf(node)\n return idx >= 0 && idx + 1 < parent.children.length\n ? (parent.children[idx + 1] ?? null)\n : null\n },\n\n isText(node): boolean {\n return node.type === TEXT\n },\n\n serialize: serializeHeadless,\n }\n // Only expose overlayRoot when a target was provided, so the default stays in-place (SSR-correct).\n if (options.overlayRoot) {\n const target = options.overlayRoot\n backend.overlayRoot = () => target\n }\n return backend\n}\n\n/** Whether a prop key is an event handler (`onClick`, `onPress`, …). */\nexport function isEventProp(key: string): boolean {\n return (\n key.length > 2 && key[0] === 'o' && key[1] === 'n' && key[2] === (key[2] ?? '').toUpperCase()\n )\n}\n\n/** Convenience: create a detached headless root element (default tag `\"root\"`). */\nexport function createHeadlessRoot(type = 'root'): HeadlessNode {\n return { type, props: {}, text: '', children: [], parent: null }\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { Maturity, NotImplementedError, PackageInfo, notImplemented } from "@min
|
|
|
17
17
|
/** The npm package name. */
|
|
18
18
|
declare const name = "@mindees/renderer";
|
|
19
19
|
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
20
|
-
declare const VERSION = "0.22.
|
|
20
|
+
declare const VERSION = "0.22.5";
|
|
21
21
|
/**
|
|
22
22
|
* Current maturity. The Helix **web/DOM** renderer (reconciler, DOM backend,
|
|
23
23
|
* headless backend, SSR + hydration) is implemented and tested. Native
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { NotImplementedError, notImplemented } from "@mindees/core";
|
|
|
16
16
|
/** The npm package name. */
|
|
17
17
|
const name = "@mindees/renderer";
|
|
18
18
|
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
19
|
-
const VERSION = "0.22.
|
|
19
|
+
const VERSION = "0.22.5";
|
|
20
20
|
/**
|
|
21
21
|
* Current maturity. The Helix **web/DOM** renderer (reconciler, DOM backend,
|
|
22
22
|
* headless backend, SSR + hydration) is implemented and tested. Native
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** Host-backend contract + capability detection. */\nexport {\n type HostBackend,\n isSerializable,\n type SerializableBackend,\n} from './backend'\n/** Helix Canvas strand — a 2D scene graph driven by the reconciler, painted to a 2D context (§6.2). */\nexport {\n type Canvas2DBackend,\n createCanvas2DBackend,\n type Scene2DContext,\n type SceneNode,\n} from './canvas'\n/** DOM (web) backend. */\nexport {\n createDomBackend,\n type DomDocument,\n type DomElement,\n type DomNode,\n type DomText,\n domTagFor,\n} from './dom'\n/** Keyed list reconciliation (the renderer side of core's KeyedRegion). */\nexport { bindKeyedChild } from './for'\n/** Headless (in-memory) backend — the reference/test target. */\nexport {\n createHeadlessBackend,\n createHeadlessRoot,\n type HeadlessNode,\n isEventProp,\n} from './headless'\n/**\n * Native backends. `createNativeCommandBackend` is implemented (emits a native\n * command stream); `createNativeBackend`/`createCanvasBackend` are research\n * tracks that throw `NotImplementedError`.\n */\nexport {\n type CanvasBackend,\n createCanvasBackend,\n createNativeBackend,\n createNativeCommandBackend,\n type NativeBackend,\n type NativeCommandBackend,\n type NativeCommandBackendOptions,\n type NativeCommandNode,\n} from './native'\n/** One-call native app entry — wires the command backend + host contract. */\nexport {\n type CreateNativeAppOptions,\n createNativeApp,\n type NativeApp,\n} from './native-app'\n/**\n * The strict reference native host — applies a command stream to a model tree and\n * validates it (the executable conformance contract real native hosts implement).\n */\nexport {\n createReferenceHost,\n NativeHostError,\n type ReferenceHost,\n type ReferenceHostNode,\n} from './native-host'\n/** The native command protocol: command types + serialization-safe helpers. */\nexport {\n type CreateNodeCommand,\n type CreateTextCommand,\n createNativeNodeIdFactory,\n type DisposeNodeCommand,\n type InsertChildCommand,\n isNativeCommand,\n isNativePropValue,\n type NativeCommand,\n type NativeNodeId,\n type NativePropValue,\n normalizeNativeProp,\n type RegisterEventCommand,\n type RemoveChildCommand,\n type RemovePropCommand,\n type SetPropCommand,\n type UnregisterEventCommand,\n type UpdateTextCommand,\n} from './native-protocol'\n/** Portal reconciliation (the renderer side of core's PortalRegion). */\nexport { bindPortalChild } from './portal'\n/** The fine-grained reactive reconciler. */\nexport { type Mounted, mountNode, render } from './render'\n/** Server-side rendering + hydration (web). */\nexport { hydrate, renderToString } from './ssr'\n\n/** The npm package name. */\nexport const name = '@mindees/renderer'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.22.
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** Host-backend contract + capability detection. */\nexport {\n type HostBackend,\n isSerializable,\n type SerializableBackend,\n} from './backend'\n/** Helix Canvas strand — a 2D scene graph driven by the reconciler, painted to a 2D context (§6.2). */\nexport {\n type Canvas2DBackend,\n createCanvas2DBackend,\n type Scene2DContext,\n type SceneNode,\n} from './canvas'\n/** DOM (web) backend. */\nexport {\n createDomBackend,\n type DomDocument,\n type DomElement,\n type DomNode,\n type DomText,\n domTagFor,\n} from './dom'\n/** Keyed list reconciliation (the renderer side of core's KeyedRegion). */\nexport { bindKeyedChild } from './for'\n/** Headless (in-memory) backend — the reference/test target. */\nexport {\n createHeadlessBackend,\n createHeadlessRoot,\n type HeadlessNode,\n isEventProp,\n} from './headless'\n/**\n * Native backends. `createNativeCommandBackend` is implemented (emits a native\n * command stream); `createNativeBackend`/`createCanvasBackend` are research\n * tracks that throw `NotImplementedError`.\n */\nexport {\n type CanvasBackend,\n createCanvasBackend,\n createNativeBackend,\n createNativeCommandBackend,\n type NativeBackend,\n type NativeCommandBackend,\n type NativeCommandBackendOptions,\n type NativeCommandNode,\n} from './native'\n/** One-call native app entry — wires the command backend + host contract. */\nexport {\n type CreateNativeAppOptions,\n createNativeApp,\n type NativeApp,\n} from './native-app'\n/**\n * The strict reference native host — applies a command stream to a model tree and\n * validates it (the executable conformance contract real native hosts implement).\n */\nexport {\n createReferenceHost,\n NativeHostError,\n type ReferenceHost,\n type ReferenceHostNode,\n} from './native-host'\n/** The native command protocol: command types + serialization-safe helpers. */\nexport {\n type CreateNodeCommand,\n type CreateTextCommand,\n createNativeNodeIdFactory,\n type DisposeNodeCommand,\n type InsertChildCommand,\n isNativeCommand,\n isNativePropValue,\n type NativeCommand,\n type NativeNodeId,\n type NativePropValue,\n normalizeNativeProp,\n type RegisterEventCommand,\n type RemoveChildCommand,\n type RemovePropCommand,\n type SetPropCommand,\n type UnregisterEventCommand,\n type UpdateTextCommand,\n} from './native-protocol'\n/** Portal reconciliation (the renderer side of core's PortalRegion). */\nexport { bindPortalChild } from './portal'\n/** The fine-grained reactive reconciler. */\nexport { type Mounted, mountNode, render } from './render'\n/** Server-side rendering + hydration (web). */\nexport { hydrate, renderToString } from './ssr'\n\n/** The npm package name. */\nexport const name = '@mindees/renderer'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.22.5'\n\n/**\n * Current maturity. The Helix **web/DOM** renderer (reconciler, DOM backend,\n * headless backend, SSR + hydration) is implemented and tested. Native\n * (iOS/Android) and the GPU canvas are research tracks (throw\n * `NotImplementedError`). See the repository `STATUS.md`.\n */\nexport const maturity: Maturity = 'experimental'\n\n/**\n * Static identity + maturity metadata for this package. Frozen so the\n * self-reported identity tooling introspects cannot be mutated at runtime,\n * matching the `readonly` fields of {@link PackageInfo}.\n */\nexport const info: PackageInfo = Object.freeze({ name, version: VERSION, maturity })\n\nexport type { Maturity, PackageInfo }\nexport { NotImplementedError, notImplemented }\n"],"mappings":";;;;;;;;;;;;;;;;AA6FA,MAAa,OAAO;;AAGpB,MAAa,UAAU;;;;;;;AAQvB,MAAa,WAAqB;;;;;;AAOlC,MAAa,OAAoB,OAAO,OAAO;CAAE;CAAM,SAAS;CAAS;AAAS,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindees/renderer",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.5",
|
|
4
4
|
"description": "MindeesNative Helix — fine-grained reactive renderer with a web/DOM backend, SSR + hydration, and a headless test backend. Native and GPU-canvas backends are research tracks.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"directory": "packages/renderer"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@mindees/core": "0.22.
|
|
26
|
+
"@mindees/core": "0.22.5"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"happy-dom": "20.9.0"
|