/** * sx-platform.js — Thin JS platform layer for the OCaml SX WASM engine. * * This file provides browser-native primitives (DOM, fetch, timers, etc.) * to the WASM-compiled OCaml CEK machine. It: * 1. Loads the WASM module (SxKernel) * 2. Registers ~80 native browser functions via registerNative * 3. Loads web adapters (.sx files) into the engine * 4. Exports the public Sx API * * Both wasm_of_ocaml and js_of_ocaml targets bind to this same layer. */ (function(global) { "use strict"; function initPlatform() { var K = global.SxKernel; if (!K) { // WASM loader is async — wait and retry setTimeout(initPlatform, 20); return; } var _hasDom = typeof document !== "undefined"; var NIL = null; var SVG_NS = "http://www.w3.org/2000/svg"; // ========================================================================= // Helper: wrap SX lambda for use as JS callback // ========================================================================= function wrapLambda(fn) { // For now, SX lambdas from registerNative are opaque — we can't call them // directly from JS. They need to go through the engine. // TODO: add callLambda API to SxKernel return fn; } // ========================================================================= // 1. DOM Creation & Manipulation // ========================================================================= K.registerNative("dom-create-element", function(args) { if (!_hasDom) return NIL; var tag = args[0], ns = args[1]; if (ns && ns !== NIL) return document.createElementNS(ns, tag); return document.createElement(tag); }); K.registerNative("create-text-node", function(args) { return _hasDom ? document.createTextNode(args[0] || "") : NIL; }); K.registerNative("create-comment", function(args) { return _hasDom ? document.createComment(args[0] || "") : NIL; }); K.registerNative("create-fragment", function(_args) { return _hasDom ? document.createDocumentFragment() : NIL; }); K.registerNative("dom-clone", function(args) { var node = args[0]; return node && node.cloneNode ? node.cloneNode(true) : node; }); K.registerNative("dom-parse-html", function(args) { if (!_hasDom) return NIL; var tpl = document.createElement("template"); tpl.innerHTML = args[0] || ""; return tpl.content; }); K.registerNative("dom-parse-html-document", function(args) { if (!_hasDom) return NIL; var parser = new DOMParser(); return parser.parseFromString(args[0] || "", "text/html"); }); // ========================================================================= // 2. DOM Queries // ========================================================================= K.registerNative("dom-query", function(args) { return _hasDom ? document.querySelector(args[0]) || NIL : NIL; }); K.registerNative("dom-query-all", function(args) { var root = args[0] || (_hasDom ? document : null); if (!root || !root.querySelectorAll) return []; return Array.prototype.slice.call(root.querySelectorAll(args[1] || args[0])); }); K.registerNative("dom-query-by-id", function(args) { return _hasDom ? document.getElementById(args[0]) || NIL : NIL; }); K.registerNative("dom-body", function(_args) { return _hasDom ? document.body : NIL; }); K.registerNative("dom-ensure-element", function(args) { if (!_hasDom) return NIL; var sel = args[0]; var el = document.querySelector(sel); if (el) return el; if (sel.charAt(0) === "#") { el = document.createElement("div"); el.id = sel.slice(1); document.body.appendChild(el); return el; } return NIL; }); // ========================================================================= // 3. DOM Attributes // ========================================================================= K.registerNative("dom-get-attr", function(args) { var el = args[0], name = args[1]; if (!el || !el.getAttribute) return NIL; var v = el.getAttribute(name); return v === null ? NIL : v; }); K.registerNative("dom-set-attr", function(args) { var el = args[0], name = args[1], val = args[2]; if (el && el.setAttribute) el.setAttribute(name, val); return NIL; }); K.registerNative("dom-remove-attr", function(args) { if (args[0] && args[0].removeAttribute) args[0].removeAttribute(args[1]); return NIL; }); K.registerNative("dom-has-attr?", function(args) { return !!(args[0] && args[0].hasAttribute && args[0].hasAttribute(args[1])); }); K.registerNative("dom-attr-list", function(args) { var el = args[0]; if (!el || !el.attributes) return []; var r = []; for (var i = 0; i < el.attributes.length; i++) { r.push([el.attributes[i].name, el.attributes[i].value]); } return r; }); // ========================================================================= // 4. DOM Content // ========================================================================= K.registerNative("dom-text-content", function(args) { var el = args[0]; return el ? el.textContent || el.nodeValue || "" : ""; }); K.registerNative("dom-set-text-content", function(args) { var el = args[0], s = args[1]; if (el) { if (el.nodeType === 3 || el.nodeType === 8) el.nodeValue = s; else el.textContent = s; } return NIL; }); K.registerNative("dom-inner-html", function(args) { return args[0] && args[0].innerHTML != null ? args[0].innerHTML : ""; }); K.registerNative("dom-set-inner-html", function(args) { if (args[0]) args[0].innerHTML = args[1] || ""; return NIL; }); K.registerNative("dom-insert-adjacent-html", function(args) { var el = args[0], pos = args[1], html = args[2]; if (el && el.insertAdjacentHTML) el.insertAdjacentHTML(pos, html); return NIL; }); K.registerNative("dom-body-inner-html", function(args) { var doc = args[0]; return doc && doc.body ? doc.body.innerHTML : ""; }); // ========================================================================= // 5. DOM Structure & Navigation // ========================================================================= K.registerNative("dom-parent", function(args) { return args[0] ? args[0].parentNode || NIL : NIL; }); K.registerNative("dom-first-child", function(args) { return args[0] ? args[0].firstChild || NIL : NIL; }); K.registerNative("dom-next-sibling", function(args) { return args[0] ? args[0].nextSibling || NIL : NIL; }); K.registerNative("dom-id", function(args) { return args[0] && args[0].id ? args[0].id : NIL; }); K.registerNative("dom-node-type", function(args) { return args[0] ? args[0].nodeType : 0; }); K.registerNative("dom-node-name", function(args) { return args[0] ? args[0].nodeName : ""; }); K.registerNative("dom-tag-name", function(args) { return args[0] && args[0].tagName ? args[0].tagName : ""; }); K.registerNative("dom-child-list", function(args) { var el = args[0]; if (!el || !el.childNodes) return []; return Array.prototype.slice.call(el.childNodes); }); K.registerNative("dom-child-nodes", function(args) { var el = args[0]; if (!el || !el.childNodes) return []; return Array.prototype.slice.call(el.childNodes); }); // ========================================================================= // 6. DOM Insertion & Removal // ========================================================================= K.registerNative("dom-append", function(args) { var parent = args[0], child = args[1]; if (parent && child) parent.appendChild(child); return NIL; }); K.registerNative("dom-prepend", function(args) { var parent = args[0], child = args[1]; if (parent && child) parent.insertBefore(child, parent.firstChild); return NIL; }); K.registerNative("dom-insert-before", function(args) { var parent = args[0], node = args[1], ref = args[2]; if (parent && node) parent.insertBefore(node, ref || null); return NIL; }); K.registerNative("dom-insert-after", function(args) { var ref = args[0], node = args[1]; if (ref && ref.parentNode && node) { ref.parentNode.insertBefore(node, ref.nextSibling); } return NIL; }); K.registerNative("dom-remove", function(args) { var node = args[0]; if (node && node.parentNode) node.parentNode.removeChild(node); return NIL; }); K.registerNative("dom-remove-child", function(args) { var parent = args[0], child = args[1]; if (parent && child && child.parentNode === parent) parent.removeChild(child); return NIL; }); K.registerNative("dom-replace-child", function(args) { var parent = args[0], newC = args[1], oldC = args[2]; if (parent && newC && oldC) parent.replaceChild(newC, oldC); return NIL; }); K.registerNative("dom-remove-children-after", function(args) { var marker = args[0]; if (!marker || !marker.parentNode) return NIL; var parent = marker.parentNode; while (marker.nextSibling) parent.removeChild(marker.nextSibling); return NIL; }); K.registerNative("dom-append-to-head", function(args) { if (_hasDom && args[0]) document.head.appendChild(args[0]); return NIL; }); // ========================================================================= // 7. DOM Type Checks // ========================================================================= K.registerNative("dom-is-fragment?", function(args) { return args[0] ? args[0].nodeType === 11 : false; }); K.registerNative("dom-is-child-of?", function(args) { return !!(args[1] && args[0] && args[0].parentNode === args[1]); }); K.registerNative("dom-is-active-element?", function(args) { return _hasDom && args[0] === document.activeElement; }); K.registerNative("dom-is-input-element?", function(args) { if (!args[0] || !args[0].tagName) return false; var t = args[0].tagName; return t === "INPUT" || t === "TEXTAREA" || t === "SELECT"; }); // ========================================================================= // 8. DOM Styles & Classes // ========================================================================= K.registerNative("dom-get-style", function(args) { return args[0] && args[0].style ? args[0].style[args[1]] || "" : ""; }); K.registerNative("dom-set-style", function(args) { if (args[0] && args[0].style) args[0].style[args[1]] = args[2]; return NIL; }); K.registerNative("dom-add-class", function(args) { if (args[0] && args[0].classList) args[0].classList.add(args[1]); return NIL; }); K.registerNative("dom-remove-class", function(args) { if (args[0] && args[0].classList) args[0].classList.remove(args[1]); return NIL; }); K.registerNative("dom-has-class?", function(args) { return !!(args[0] && args[0].classList && args[0].classList.contains(args[1])); }); // ========================================================================= // 9. DOM Properties & Data // ========================================================================= K.registerNative("dom-get-prop", function(args) { return args[0] ? args[0][args[1]] : NIL; }); K.registerNative("dom-set-prop", function(args) { if (args[0]) args[0][args[1]] = args[2]; return NIL; }); K.registerNative("dom-set-data", function(args) { var el = args[0], key = args[1], val = args[2]; if (el) { if (!el._sxData) el._sxData = {}; el._sxData[key] = val; } return NIL; }); K.registerNative("dom-get-data", function(args) { var el = args[0], key = args[1]; return (el && el._sxData) ? (el._sxData[key] != null ? el._sxData[key] : NIL) : NIL; }); K.registerNative("dom-call-method", function(args) { var obj = args[0], method = args[1]; var callArgs = args.slice(2); if (obj && typeof obj[method] === "function") { try { return obj[method].apply(obj, callArgs); } catch(e) { return NIL; } } return NIL; }); // ========================================================================= // 10. DOM Events // ========================================================================= K.registerNative("dom-listen", function(args) { var el = args[0], name = args[1], handler = args[2]; if (!_hasDom || !el) return function() {}; // handler is a wrapped SX lambda (JS function with __sx_handle). // Wrap it to: // - Pass the event object as arg (or no args for 0-arity handlers) // - Catch errors from the CEK machine var arity = K.fnArity(handler); var wrapped; if (arity === 0) { wrapped = function(_e) { try { K.callFn(handler, []); } catch(err) { console.error("[sx] event handler error:", name, err); } }; } else { wrapped = function(e) { try { K.callFn(handler, [e]); } catch(err) { console.error("[sx] event handler error:", name, err); } }; } el.addEventListener(name, wrapped); return function() { el.removeEventListener(name, wrapped); }; }); K.registerNative("dom-dispatch", function(args) { if (!_hasDom || !args[0]) return false; var evt = new CustomEvent(args[1], { bubbles: true, cancelable: true, detail: args[2] || {} }); return args[0].dispatchEvent(evt); }); K.registerNative("event-detail", function(args) { return (args[0] && args[0].detail != null) ? args[0].detail : NIL; }); // ========================================================================= // 11. Browser Navigation & History // ========================================================================= K.registerNative("browser-location-href", function(_args) { return typeof location !== "undefined" ? location.href : ""; }); K.registerNative("browser-same-origin?", function(args) { try { return new URL(args[0], location.href).origin === location.origin; } catch (e) { return true; } }); K.registerNative("browser-push-state", function(args) { if (typeof history !== "undefined") { try { history.pushState({ sxUrl: args[0], scrollY: typeof window !== "undefined" ? window.scrollY : 0 }, "", args[0]); } catch (e) {} } return NIL; }); K.registerNative("browser-replace-state", function(args) { if (typeof history !== "undefined") { try { history.replaceState({ sxUrl: args[0], scrollY: typeof window !== "undefined" ? window.scrollY : 0 }, "", args[0]); } catch (e) {} } return NIL; }); K.registerNative("browser-navigate", function(args) { if (typeof location !== "undefined") location.assign(args[0]); return NIL; }); K.registerNative("browser-reload", function(_args) { if (typeof location !== "undefined") location.reload(); return NIL; }); K.registerNative("browser-scroll-to", function(args) { if (typeof window !== "undefined") window.scrollTo(args[0] || 0, args[1] || 0); return NIL; }); K.registerNative("browser-media-matches?", function(args) { if (typeof window === "undefined") return false; return window.matchMedia(args[0]).matches; }); K.registerNative("browser-confirm", function(args) { if (typeof window === "undefined") return false; return window.confirm(args[0]); }); K.registerNative("browser-prompt", function(args) { if (typeof window === "undefined") return NIL; var r = window.prompt(args[0]); return r === null ? NIL : r; }); // ========================================================================= // 12. Timers // ========================================================================= K.registerNative("set-timeout", function(args) { var fn = args[0], ms = args[1] || 0; var cb = (typeof fn === "function" && fn.__sx_handle != null) ? function() { try { K.callFn(fn, []); } catch(e) { console.error("[sx] timeout error:", e); } } : fn; return setTimeout(cb, ms); }); K.registerNative("set-interval", function(args) { var fn = args[0], ms = args[1] || 1000; var cb = (typeof fn === "function" && fn.__sx_handle != null) ? function() { try { K.callFn(fn, []); } catch(e) { console.error("[sx] interval error:", e); } } : fn; return setInterval(cb, ms); }); K.registerNative("clear-timeout", function(args) { clearTimeout(args[0]); return NIL; }); K.registerNative("clear-interval", function(args) { clearInterval(args[0]); return NIL; }); K.registerNative("now-ms", function(_args) { return (typeof performance !== "undefined") ? performance.now() : Date.now(); }); K.registerNative("request-animation-frame", function(args) { var fn = args[0]; var cb = (typeof fn === "function" && fn.__sx_handle != null) ? function() { try { K.callFn(fn, []); } catch(e) { console.error("[sx] raf error:", e); } } : fn; if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(cb); else setTimeout(cb, 16); return NIL; }); // ========================================================================= // 13. Promises // ========================================================================= K.registerNative("promise-resolve", function(args) { return Promise.resolve(args[0]); }); K.registerNative("promise-then", function(args) { var p = args[0]; if (!p || !p.then) return p; var onResolve = function(v) { return K.callFn(args[1], [v]); }; var onReject = args[2] ? function(e) { return K.callFn(args[2], [e]); } : undefined; return onReject ? p.then(onResolve, onReject) : p.then(onResolve); }); K.registerNative("promise-catch", function(args) { if (!args[0] || !args[0].catch) return args[0]; return args[0].catch(function(e) { return K.callFn(args[1], [e]); }); }); K.registerNative("promise-delayed", function(args) { return new Promise(function(resolve) { setTimeout(function() { resolve(args[1]); }, args[0]); }); }); // ========================================================================= // 14. Abort Controllers // ========================================================================= var _controllers = typeof WeakMap !== "undefined" ? new WeakMap() : null; var _targetControllers = typeof WeakMap !== "undefined" ? new WeakMap() : null; K.registerNative("new-abort-controller", function(_args) { return typeof AbortController !== "undefined" ? new AbortController() : { signal: null, abort: function() {} }; }); K.registerNative("abort-previous", function(args) { if (_controllers) { var prev = _controllers.get(args[0]); if (prev) prev.abort(); } return NIL; }); K.registerNative("track-controller", function(args) { if (_controllers) _controllers.set(args[0], args[1]); return NIL; }); K.registerNative("abort-previous-target", function(args) { if (_targetControllers) { var prev = _targetControllers.get(args[0]); if (prev) prev.abort(); } return NIL; }); K.registerNative("track-controller-target", function(args) { if (_targetControllers) _targetControllers.set(args[0], args[1]); return NIL; }); K.registerNative("controller-signal", function(args) { return args[0] ? args[0].signal : NIL; }); K.registerNative("is-abort-error", function(args) { return args[0] && args[0].name === "AbortError"; }); // ========================================================================= // 15. Fetch // ========================================================================= K.registerNative("fetch-request", function(args) { var config = args[0], successFn = args[1], errorFn = args[2]; var opts = { method: config.method, headers: config.headers }; if (config.signal) opts.signal = config.signal; if (config.body && config.method !== "GET") opts.body = config.body; if (config["cross-origin"]) opts.credentials = "include"; return fetch(config.url, opts).then(function(resp) { return resp.text().then(function(text) { var getHeader = function(name) { var v = resp.headers.get(name); return v === null ? NIL : v; }; return K.callFn(successFn, [resp.ok, resp.status, getHeader, text]); }); }).catch(function(err) { return K.callFn(errorFn, [err]); }); }); K.registerNative("csrf-token", function(_args) { if (!_hasDom) return NIL; var m = document.querySelector('meta[name="csrf-token"]'); return m ? m.getAttribute("content") : NIL; }); K.registerNative("is-cross-origin", function(args) { try { var h = new URL(args[0], location.href).hostname; return h !== location.hostname && (h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0); } catch (e) { return false; } }); // ========================================================================= // 16. localStorage // ========================================================================= K.registerNative("local-storage-get", function(args) { try { var v = localStorage.getItem(args[0]); return v === null ? NIL : v; } catch(e) { return NIL; } }); K.registerNative("local-storage-set", function(args) { try { localStorage.setItem(args[0], args[1]); } catch(e) {} return NIL; }); K.registerNative("local-storage-remove", function(args) { try { localStorage.removeItem(args[0]); } catch(e) {} return NIL; }); // ========================================================================= // 17. Document Head & Title // ========================================================================= K.registerNative("set-document-title", function(args) { if (_hasDom) document.title = args[0] || ""; return NIL; }); K.registerNative("remove-head-element", function(args) { if (_hasDom) { var el = document.head.querySelector(args[0]); if (el) el.remove(); } return NIL; }); // ========================================================================= // 18. Logging // ========================================================================= K.registerNative("log-info", function(args) { console.log("[sx]", args[0]); return NIL; }); K.registerNative("log-warn", function(args) { console.warn("[sx]", args[0]); return NIL; }); K.registerNative("log-error", function(args) { console.error("[sx]", args[0]); return NIL; }); // ========================================================================= // 19. JSON // ========================================================================= K.registerNative("json-parse", function(args) { try { return JSON.parse(args[0]); } catch(e) { return {}; } }); K.registerNative("try-parse-json", function(args) { try { return JSON.parse(args[0]); } catch(e) { return NIL; } }); // ========================================================================= // 20. Processing markers // ========================================================================= K.registerNative("mark-processed!", function(args) { var el = args[0], key = args[1] || "sx"; if (el) { if (!el._sxProcessed) el._sxProcessed = {}; el._sxProcessed[key] = true; } return NIL; }); K.registerNative("is-processed?", function(args) { var el = args[0], key = args[1] || "sx"; return !!(el && el._sxProcessed && el._sxProcessed[key]); }); // ========================================================================= // Public Sx API (wraps SxKernel for compatibility with existing code) // ========================================================================= var Sx = { // Core (delegated to WASM engine) parse: K.parse, eval: K.eval, evalExpr: K.evalExpr, load: K.load, loadSource: K.loadSource, renderToHtml: K.renderToHtml, typeOf: K.typeOf, inspect: K.inspect, engine: K.engine, // Will be populated after web adapters load: // mount, hydrate, processElements, etc. }; global.Sx = Sx; global.SxKernel = K; // Keep kernel available for direct access console.log("[sx-platform] registered, engine:", K.engine()); } // end initPlatform initPlatform(); })(typeof globalThis !== "undefined" ? globalThis : this);