@@ -0,0 +1,507 @@
4+ * Provides fake DOM objects that translate standard DOM API calls
5+ * (createElement, appendChild, setAttribute, etc.) to LynxJS PAPI calls.
6+ * This allows Elm's compiled JS to work unmodified on LynxJS's PrimJS runtime.
11+function camelToKebab(str) {
12+ return str.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
15+function kebabToCamel(str) {
16+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
21+// --- LynxTextNode ---
23+export class LynxTextNode {
26+ this.nodeValue = text;
27+ this.parentNode = null;
28+ this.nextSibling = null;
29+ this.previousSibling = null;
30+ this._papiElement = null;
34+ return this.nodeValue;
42+ /** Called by Elm's VirtualDom for text updates */
43+ replaceData(offset, length, data) {
44+ const before = this.nodeValue.substring(0, offset);
45+ const after = this.nodeValue.substring(offset + length);
46+ this.nodeValue = before + data + after;
51+ if (this._papiElement) {
52+ __SetAttribute(this._papiElement, "value", this.nodeValue);
56+ _ensurePapi(parentPapi) {
57+ if (!this._papiElement) {
58+ this._papiElement = __CreateTextElement("text", this.nodeValue);
60+ __AppendElement(parentPapi, this._papiElement);
66+// --- Style Proxy ---
68+function createStyleProxy(node) {
72+ set(target, prop, value) {
73+ target[prop] = value;
74+ const cssProp = camelToKebab(prop);
75+ node._setStyleProperty(cssProp, value);
79+ if (prop === "cssText") {
80+ return node.getAttribute("style") || "";
82+ if (prop === "setProperty") {
84+ target[kebabToCamel(p)] = v;
85+ node._setStyleProperty(p, v);
88+ if (prop === "removeProperty") {
90+ const camel = kebabToCamel(p);
91+ const old = target[camel];
92+ delete target[camel];
93+ node._setStyleProperty(p, "");
97+ if (prop === "getPropertyValue") {
98+ return (p) => target[kebabToCamel(p)] || "";
100+ return target[prop];
108+export class LynxNode {
109+ constructor(tagName) {
111+ this.tagName = tagName.toUpperCase();
112+ this.nodeName = this.tagName;
113+ this._tagLower = tagName.toLowerCase();
114+ this._id = nextNodeId++;
115+ this._papiElement = null;
117+ this._children = [];
118+ this._eventListeners = {};
119+ this._styleMap = {};
120+ this.parentNode = null;
121+ this.nextSibling = null;
122+ this.previousSibling = null;
123+ this.style = createStyleProxy(this);
126+ // --- Child access ---
129+ return this._children;
133+ return this._children[0] || null;
137+ return this._children[this._children.length - 1] || null;
140+ // --- className ---
143+ return this._attrs["class"] || "";
147+ this.setAttribute("class", v);
150+ // --- namespaceURI (Elm checks this) ---
152+ get namespaceURI() {
153+ return "http://www.w3.org/1999/xhtml";
156+ // --- Attribute methods ---
158+ setAttribute(name, value) {
159+ this._attrs[name] = String(value);
160+ if (name === "style") {
161+ this._parseAndApplyStyleString(value);
162+ } else if (this._papiElement) {
163+ __SetAttribute(this._papiElement, name, String(value));
167+ getAttribute(name) {
168+ return this._attrs[name] !== undefined ? this._attrs[name] : null;
171+ removeAttribute(name) {
172+ delete this._attrs[name];
173+ if (this._papiElement) {
174+ __SetAttribute(this._papiElement, name, "");
178+ // --- Style handling ---
180+ _parseAndApplyStyleString(cssText) {
181+ if (!this._papiElement) return;
182+ const pairs = cssText.split(";");
183+ for (const pair of pairs) {
184+ const colon = pair.indexOf(":");
185+ if (colon === -1) continue;
186+ const prop = pair.substring(0, colon).trim();
187+ const val = pair.substring(colon + 1).trim();
189+ this._setStyleProperty(prop, val);
194+ _setStyleProperty(prop, value) {
195+ this._styleMap[prop] = value;
196+ if (this._papiElement) {
197+ __SetAttribute(this._papiElement, prop, value);
201+ // --- Child mutation ---
203+ appendChild(child) {
204+ if (child.nodeType === 11) {
206+ const kids = child._children.slice();
207+ for (const k of kids) {
208+ this.appendChild(k);
213+ this._removeFromParent(child);
214+ this._children.push(child);
215+ child.parentNode = this;
216+ this._updateSiblings();
218+ if (this._papiElement) {
219+ this._attachChild(child);
225+ insertBefore(newChild, refChild) {
226+ if (newChild.nodeType === 11) {
227+ const kids = newChild._children.slice();
228+ for (const k of kids) {
229+ this.insertBefore(k, refChild);
234+ this._removeFromParent(newChild);
237+ return this.appendChild(newChild);
240+ const idx = this._children.indexOf(refChild);
242+ return this.appendChild(newChild);
245+ this._children.splice(idx, 0, newChild);
246+ newChild.parentNode = this;
247+ this._updateSiblings();
249+ if (this._papiElement) {
250+ this._attachChildBefore(newChild, refChild);
256+ removeChild(child) {
257+ const idx = this._children.indexOf(child);
258+ if (idx === -1) return child;
260+ this._children.splice(idx, 1);
261+ child.parentNode = null;
262+ this._updateSiblings();
264+ if (this._papiElement && child._papiElement) {
265+ __RemoveElement(this._papiElement, child._papiElement);
271+ replaceChild(newChild, oldChild) {
272+ if (newChild.nodeType === 11) {
273+ const kids = newChild._children.slice();
274+ const idx = this._children.indexOf(oldChild);
275+ if (idx === -1) return oldChild;
277+ // Insert all fragment children in place of oldChild
278+ this._children.splice(idx, 1, ...kids);
279+ for (const k of kids) {
280+ k.parentNode = this;
282+ oldChild.parentNode = null;
283+ this._updateSiblings();
285+ if (this._papiElement) {
286+ for (const k of kids) {
287+ this._attachChildBefore(k, oldChild.nextSibling);
289+ if (oldChild._papiElement) {
290+ __RemoveElement(this._papiElement, oldChild._papiElement);
297+ const idx = this._children.indexOf(oldChild);
298+ if (idx === -1) return oldChild;
300+ this._removeFromParent(newChild);
301+ this._children[idx] = newChild;
302+ newChild.parentNode = this;
303+ oldChild.parentNode = null;
304+ this._updateSiblings();
306+ if (this._papiElement) {
307+ this._attachChildBefore(newChild, oldChild.nextSibling);
308+ if (oldChild._papiElement) {
309+ __RemoveElement(this._papiElement, oldChild._papiElement);
318+ addEventListener(type, handler, options) {
319+ if (!this._eventListeners[type]) {
320+ this._eventListeners[type] = [];
322+ this._eventListeners[type].push(handler);
324+ if (this._papiElement) {
325+ __AddEvent(this._papiElement, "bind", type, (papiEvent) => {
329+ currentTarget: this,
330+ detail: papiEvent?.detail || papiEvent || {},
331+ preventDefault() {},
332+ stopPropagation() {},
339+ removeEventListener(type, handler, options) {
340+ const listeners = this._eventListeners[type];
341+ if (!listeners) return;
342+ const idx = listeners.indexOf(handler);
344+ listeners.splice(idx, 1);
346+ if (this._papiElement) {
347+ __RemoveEvent(this._papiElement, "bind", type);
351+ // --- Internal helpers ---
353+ _removeFromParent(child) {
354+ if (child.parentNode) {
355+ child.parentNode.removeChild(child);
360+ for (let i = 0; i < this._children.length; i++) {
361+ const child = this._children[i];
362+ child.previousSibling = this._children[i - 1] || null;
363+ child.nextSibling = this._children[i + 1] || null;
368+ if (!this._papiElement) {
369+ this._papiElement = __CreateElement(this._tagLower);
371+ // Apply buffered attributes
372+ for (const [k, v] of Object.entries(this._attrs)) {
373+ if (k === "style") {
374+ this._parseAndApplyStyleString(v);
376+ __SetAttribute(this._papiElement, k, v);
380+ // Apply buffered style properties
381+ for (const [prop, val] of Object.entries(this._styleMap)) {
382+ __SetAttribute(this._papiElement, prop, val);
386+ for (const [type, handlers] of Object.entries(this._eventListeners)) {
387+ for (const handler of handlers) {
388+ __AddEvent(this._papiElement, "bind", type, (papiEvent) => {
392+ currentTarget: this,
393+ detail: papiEvent?.detail || papiEvent || {},
394+ preventDefault() {},
395+ stopPropagation() {},
404+ _attachChild(child) {
405+ if (child.nodeType === 3) {
406+ child._ensurePapi(this._papiElement);
408+ child._ensurePapi();
409+ if (child._papiElement) {
410+ __AppendElement(this._papiElement, child._papiElement);
413+ // Recursively attach grandchildren
414+ if (child._children) {
415+ for (const grandchild of child._children) {
416+ child._attachChild(grandchild);
421+ _attachChildBefore(newChild, refChild) {
422+ if (newChild.nodeType === 3) {
423+ newChild._ensurePapi(this._papiElement);
425+ newChild._ensurePapi();
428+ if (!newChild._papiElement) return;
430+ if (refChild && refChild._papiElement) {
431+ __InsertBefore(this._papiElement, newChild._papiElement, refChild._papiElement);
433+ __AppendElement(this._papiElement, newChild._papiElement);
436+ // Recursively attach grandchildren
437+ if (newChild._children) {
438+ for (const grandchild of newChild._children) {
439+ newChild._attachChild(grandchild);
445+// --- LynxDocumentFragment ---
447+export class LynxDocumentFragment {
450+ this._children = [];
451+ this.parentNode = null;
452+ this.nextSibling = null;
453+ this.previousSibling = null;
457+ return this._children;
461+ return this._children[0] || null;
464+ appendChild(child) {
465+ if (child.parentNode) {
466+ child.parentNode.removeChild(child);
468+ this._children.push(child);
469+ child.parentNode = this;
474+// --- LynxDocument ---
476+export class LynxDocument {
478+ this.body = new LynxNode("body");
479+ this.body._papiElement = __CreatePage("page", 0);
483+ createElement(tagName) {
484+ return new LynxNode(tagName);
487+ createTextNode(text) {
488+ return new LynxTextNode(text);
491+ createDocumentFragment() {
492+ return new LynxDocumentFragment();
495+ createComment(_text) {
496+ // Comments are no-ops in LynxJS; return a minimal object
497+ return new LynxTextNode("");
500+ getElementById(_id) {
504+ createElementNS(_ns, tagName) {
505+ return this.createElement(tagName);