@@ -0,0 +1,507 @@
1+/**
2+ * DOM Shim for LynxJS
3+ *
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.
7+ */
8+
9+// --- Helpers ---
10+
11+function camelToKebab(str) {
12+ return str.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
13+}
14+
15+function kebabToCamel(str) {
16+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
17+}
18+
19+let nextNodeId = 1;
20+
21+// --- LynxTextNode ---
22+
23+export class LynxTextNode {
24+ constructor(text) {
25+ this.nodeType = 3;
26+ this.nodeValue = text;
27+ this.parentNode = null;
28+ this.nextSibling = null;
29+ this.previousSibling = null;
30+ this._papiElement = null;
31+ }
32+
33+ get textContent() {
34+ return this.nodeValue;
35+ }
36+
37+ set textContent(v) {
38+ this.nodeValue = v;
39+ this._syncText();
40+ }
41+
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;
47+ this._syncText();
48+ }
49+
50+ _syncText() {
51+ if (this._papiElement) {
52+ __SetAttribute(this._papiElement, "value", this.nodeValue);
53+ }
54+ }
55+
56+ _ensurePapi(parentPapi) {
57+ if (!this._papiElement) {
58+ this._papiElement = __CreateTextElement("text", this.nodeValue);
59+ if (parentPapi) {
60+ __AppendElement(parentPapi, this._papiElement);
61+ }
62+ }
63+ }
64+}
65+
66+// --- Style Proxy ---
67+
68+function createStyleProxy(node) {
69+ return new Proxy(
70+ {},
71+ {
72+ set(target, prop, value) {
73+ target[prop] = value;
74+ const cssProp = camelToKebab(prop);
75+ node._setStyleProperty(cssProp, value);
76+ return true;
77+ },
78+ get(target, prop) {
79+ if (prop === "cssText") {
80+ return node.getAttribute("style") || "";
81+ }
82+ if (prop === "setProperty") {
83+ return (p, v) => {
84+ target[kebabToCamel(p)] = v;
85+ node._setStyleProperty(p, v);
86+ };
87+ }
88+ if (prop === "removeProperty") {
89+ return (p) => {
90+ const camel = kebabToCamel(p);
91+ const old = target[camel];
92+ delete target[camel];
93+ node._setStyleProperty(p, "");
94+ return old || "";
95+ };
96+ }
97+ if (prop === "getPropertyValue") {
98+ return (p) => target[kebabToCamel(p)] || "";
99+ }
100+ return target[prop];
101+ },
102+ }
103+ );
104+}
105+
106+// --- LynxNode ---
107+
108+export class LynxNode {
109+ constructor(tagName) {
110+ this.nodeType = 1;
111+ this.tagName = tagName.toUpperCase();
112+ this.nodeName = this.tagName;
113+ this._tagLower = tagName.toLowerCase();
114+ this._id = nextNodeId++;
115+ this._papiElement = null;
116+ this._attrs = {};
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);
124+ }
125+
126+ // --- Child access ---
127+
128+ get childNodes() {
129+ return this._children;
130+ }
131+
132+ get firstChild() {
133+ return this._children[0] || null;
134+ }
135+
136+ get lastChild() {
137+ return this._children[this._children.length - 1] || null;
138+ }
139+
140+ // --- className ---
141+
142+ get className() {
143+ return this._attrs["class"] || "";
144+ }
145+
146+ set className(v) {
147+ this.setAttribute("class", v);
148+ }
149+
150+ // --- namespaceURI (Elm checks this) ---
151+
152+ get namespaceURI() {
153+ return "http://www.w3.org/1999/xhtml";
154+ }
155+
156+ // --- Attribute methods ---
157+
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));
164+ }
165+ }
166+
167+ getAttribute(name) {
168+ return this._attrs[name] !== undefined ? this._attrs[name] : null;
169+ }
170+
171+ removeAttribute(name) {
172+ delete this._attrs[name];
173+ if (this._papiElement) {
174+ __SetAttribute(this._papiElement, name, "");
175+ }
176+ }
177+
178+ // --- Style handling ---
179+
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();
188+ if (prop) {
189+ this._setStyleProperty(prop, val);
190+ }
191+ }
192+ }
193+
194+ _setStyleProperty(prop, value) {
195+ this._styleMap[prop] = value;
196+ if (this._papiElement) {
197+ __SetAttribute(this._papiElement, prop, value);
198+ }
199+ }
200+
201+ // --- Child mutation ---
202+
203+ appendChild(child) {
204+ if (child.nodeType === 11) {
205+ // DocumentFragment
206+ const kids = child._children.slice();
207+ for (const k of kids) {
208+ this.appendChild(k);
209+ }
210+ return child;
211+ }
212+
213+ this._removeFromParent(child);
214+ this._children.push(child);
215+ child.parentNode = this;
216+ this._updateSiblings();
217+
218+ if (this._papiElement) {
219+ this._attachChild(child);
220+ }
221+
222+ return child;
223+ }
224+
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);
230+ }
231+ return newChild;
232+ }
233+
234+ this._removeFromParent(newChild);
235+
236+ if (!refChild) {
237+ return this.appendChild(newChild);
238+ }
239+
240+ const idx = this._children.indexOf(refChild);
241+ if (idx === -1) {
242+ return this.appendChild(newChild);
243+ }
244+
245+ this._children.splice(idx, 0, newChild);
246+ newChild.parentNode = this;
247+ this._updateSiblings();
248+
249+ if (this._papiElement) {
250+ this._attachChildBefore(newChild, refChild);
251+ }
252+
253+ return newChild;
254+ }
255+
256+ removeChild(child) {
257+ const idx = this._children.indexOf(child);
258+ if (idx === -1) return child;
259+
260+ this._children.splice(idx, 1);
261+ child.parentNode = null;
262+ this._updateSiblings();
263+
264+ if (this._papiElement && child._papiElement) {
265+ __RemoveElement(this._papiElement, child._papiElement);
266+ }
267+
268+ return child;
269+ }
270+
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;
276+
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;
281+ }
282+ oldChild.parentNode = null;
283+ this._updateSiblings();
284+
285+ if (this._papiElement) {
286+ for (const k of kids) {
287+ this._attachChildBefore(k, oldChild.nextSibling);
288+ }
289+ if (oldChild._papiElement) {
290+ __RemoveElement(this._papiElement, oldChild._papiElement);
291+ }
292+ }
293+
294+ return oldChild;
295+ }
296+
297+ const idx = this._children.indexOf(oldChild);
298+ if (idx === -1) return oldChild;
299+
300+ this._removeFromParent(newChild);
301+ this._children[idx] = newChild;
302+ newChild.parentNode = this;
303+ oldChild.parentNode = null;
304+ this._updateSiblings();
305+
306+ if (this._papiElement) {
307+ this._attachChildBefore(newChild, oldChild.nextSibling);
308+ if (oldChild._papiElement) {
309+ __RemoveElement(this._papiElement, oldChild._papiElement);
310+ }
311+ }
312+
313+ return oldChild;
314+ }
315+
316+ // --- Events ---
317+
318+ addEventListener(type, handler, options) {
319+ if (!this._eventListeners[type]) {
320+ this._eventListeners[type] = [];
321+ }
322+ this._eventListeners[type].push(handler);
323+
324+ if (this._papiElement) {
325+ __AddEvent(this._papiElement, "bind", type, (papiEvent) => {
326+ const domEvent = {
327+ type,
328+ target: this,
329+ currentTarget: this,
330+ detail: papiEvent?.detail || papiEvent || {},
331+ preventDefault() {},
332+ stopPropagation() {},
333+ };
334+ handler(domEvent);
335+ });
336+ }
337+ }
338+
339+ removeEventListener(type, handler, options) {
340+ const listeners = this._eventListeners[type];
341+ if (!listeners) return;
342+ const idx = listeners.indexOf(handler);
343+ if (idx !== -1) {
344+ listeners.splice(idx, 1);
345+ }
346+ if (this._papiElement) {
347+ __RemoveEvent(this._papiElement, "bind", type);
348+ }
349+ }
350+
351+ // --- Internal helpers ---
352+
353+ _removeFromParent(child) {
354+ if (child.parentNode) {
355+ child.parentNode.removeChild(child);
356+ }
357+ }
358+
359+ _updateSiblings() {
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;
364+ }
365+ }
366+
367+ _ensurePapi() {
368+ if (!this._papiElement) {
369+ this._papiElement = __CreateElement(this._tagLower);
370+
371+ // Apply buffered attributes
372+ for (const [k, v] of Object.entries(this._attrs)) {
373+ if (k === "style") {
374+ this._parseAndApplyStyleString(v);
375+ } else {
376+ __SetAttribute(this._papiElement, k, v);
377+ }
378+ }
379+
380+ // Apply buffered style properties
381+ for (const [prop, val] of Object.entries(this._styleMap)) {
382+ __SetAttribute(this._papiElement, prop, val);
383+ }
384+
385+ // Re-bind events
386+ for (const [type, handlers] of Object.entries(this._eventListeners)) {
387+ for (const handler of handlers) {
388+ __AddEvent(this._papiElement, "bind", type, (papiEvent) => {
389+ const domEvent = {
390+ type,
391+ target: this,
392+ currentTarget: this,
393+ detail: papiEvent?.detail || papiEvent || {},
394+ preventDefault() {},
395+ stopPropagation() {},
396+ };
397+ handler(domEvent);
398+ });
399+ }
400+ }
401+ }
402+ }
403+
404+ _attachChild(child) {
405+ if (child.nodeType === 3) {
406+ child._ensurePapi(this._papiElement);
407+ } else {
408+ child._ensurePapi();
409+ if (child._papiElement) {
410+ __AppendElement(this._papiElement, child._papiElement);
411+ }
412+ }
413+ // Recursively attach grandchildren
414+ if (child._children) {
415+ for (const grandchild of child._children) {
416+ child._attachChild(grandchild);
417+ }
418+ }
419+ }
420+
421+ _attachChildBefore(newChild, refChild) {
422+ if (newChild.nodeType === 3) {
423+ newChild._ensurePapi(this._papiElement);
424+ } else {
425+ newChild._ensurePapi();
426+ }
427+
428+ if (!newChild._papiElement) return;
429+
430+ if (refChild && refChild._papiElement) {
431+ __InsertBefore(this._papiElement, newChild._papiElement, refChild._papiElement);
432+ } else {
433+ __AppendElement(this._papiElement, newChild._papiElement);
434+ }
435+
436+ // Recursively attach grandchildren
437+ if (newChild._children) {
438+ for (const grandchild of newChild._children) {
439+ newChild._attachChild(grandchild);
440+ }
441+ }
442+ }
443+}
444+
445+// --- LynxDocumentFragment ---
446+
447+export class LynxDocumentFragment {
448+ constructor() {
449+ this.nodeType = 11;
450+ this._children = [];
451+ this.parentNode = null;
452+ this.nextSibling = null;
453+ this.previousSibling = null;
454+ }
455+
456+ get childNodes() {
457+ return this._children;
458+ }
459+
460+ get firstChild() {
461+ return this._children[0] || null;
462+ }
463+
464+ appendChild(child) {
465+ if (child.parentNode) {
466+ child.parentNode.removeChild(child);
467+ }
468+ this._children.push(child);
469+ child.parentNode = this;
470+ return child;
471+ }
472+}
473+
474+// --- LynxDocument ---
475+
476+export class LynxDocument {
477+ constructor() {
478+ this.body = new LynxNode("body");
479+ this.body._papiElement = __CreatePage("page", 0);
480+ this._nextId = 1;
481+ }
482+
483+ createElement(tagName) {
484+ return new LynxNode(tagName);
485+ }
486+
487+ createTextNode(text) {
488+ return new LynxTextNode(text);
489+ }
490+
491+ createDocumentFragment() {
492+ return new LynxDocumentFragment();
493+ }
494+
495+ createComment(_text) {
496+ // Comments are no-ops in LynxJS; return a minimal object
497+ return new LynxTextNode("");
498+ }
499+
500+ getElementById(_id) {
501+ return null;
502+ }
503+
504+ createElementNS(_ns, tagName) {
505+ return this.createElement(tagName);
506+ }
507+}