// Minimal server-side DOM for running solid-js/html + solid-js/web inside // goja. It implements only the surface Solid's client runtime actually // touches (enumerated from wwwroot/vendor/solid-js-web.js and // solid-js-html.js): linked-list tree mutation, template.innerHTML/.content, // element/text/comment creation, attributes, className/textContent, and // no-op event wiring. // // The tree is the source of truth as a doubly-linked list (firstChild, // nextSibling, ...) which is how the real DOM models it and what Solid's // clone-walk assumes. childNodes is a derived snapshot array so Solid's // `[...el.childNodes]` spreads work. // // HTML *parsing* (innerHTML setter) is the one genuinely hard operation, so // it is delegated to Go via __parseHTML (x/net/html) which returns a JSON // tree. Serialization back to a string is straightforward and lives here. // -mta (function (global) { "use strict"; var ELEMENT_NODE = 1, TEXT_NODE = 3, COMMENT_NODE = 8, FRAGMENT_NODE = 11; var VOID = { area: 1, base: 1, br: 1, col: 1, embed: 1, hr: 1, img: 1, input: 1, keygen: 1, link: 1, meta: 1, param: 1, source: 1, track: 1, wbr: 1, }; var SVG_NS = "http://www.w3.org/2000/svg"; function escapeText(s) { return String(s).replace(/&/g, "&").replace(//g, ">"); } function escapeAttr(s) { return String(s).replace(/&/g, "&").replace(/"/g, """); } // ---- Node ---------------------------------------------------------- class Node { constructor(type) { this.nodeType = type; this.parentNode = null; this.firstChild = null; this.lastChild = null; this.previousSibling = null; this.nextSibling = null; this._$host = null; this.host = null; } get childNodes() { var out = [], n = this.firstChild; while (n) { out.push(n); n = n.nextSibling; } return out; } appendChild(child) { detach(child); child.parentNode = this; child.previousSibling = this.lastChild; child.nextSibling = null; if (this.lastChild) this.lastChild.nextSibling = child; else this.firstChild = child; this.lastChild = child; return child; } insertBefore(child, ref) { if (ref == null) return this.appendChild(child); if (ref.parentNode !== this) throw new Error("insertBefore: ref not a child"); detach(child); child.parentNode = this; child.nextSibling = ref; child.previousSibling = ref.previousSibling; if (ref.previousSibling) ref.previousSibling.nextSibling = child; else this.firstChild = child; ref.previousSibling = child; return child; } removeChild(child) { if (child.parentNode !== this) throw new Error("removeChild: not a child"); detach(child); return child; } replaceChild(newNode, oldNode) { this.insertBefore(newNode, oldNode); this.removeChild(oldNode); return oldNode; } remove() { detach(this); } replaceWith() { var args = Array.prototype.slice.call(arguments); var parent = this.parentNode, ref = this.nextSibling; if (!parent) return; detach(this); for (var i = 0; i < args.length; i++) { var a = args[i]; if (typeof a === "string") a = new Text(a); parent.insertBefore(a, ref); } } cloneNode(deep) { var copy = this._shallowClone(); if (deep) { var n = this.firstChild; while (n) { copy.appendChild(n.cloneNode(true)); n = n.nextSibling; } } return copy; } get textContent() { if (this.nodeType === TEXT_NODE || this.nodeType === COMMENT_NODE) return this.data; var out = "", n = this.firstChild; while (n) { if (n.nodeType !== COMMENT_NODE) out += n.textContent; n = n.nextSibling; } return out; } set textContent(value) { if (this.nodeType === TEXT_NODE || this.nodeType === COMMENT_NODE) { this.data = String(value); return; } while (this.firstChild) this.removeChild(this.firstChild); if (value !== "" && value != null) this.appendChild(new Text(String(value))); } querySelectorAll(sel) { return querySelectorAll(this, sel); } querySelector(sel) { var r = querySelectorAll(this, sel); return r.length ? r[0] : null; } } function detach(node) { var p = node.parentNode; if (!p) return; if (node.previousSibling) node.previousSibling.nextSibling = node.nextSibling; else p.firstChild = node.nextSibling; if (node.nextSibling) node.nextSibling.previousSibling = node.previousSibling; else p.lastChild = node.previousSibling; node.parentNode = null; node.previousSibling = null; node.nextSibling = null; } // ---- Text / Comment ------------------------------------------------ class Text extends Node { constructor(data) { super(TEXT_NODE); this.data = data == null ? "" : String(data); this.nodeName = "#text"; } _shallowClone() { return new Text(this.data); } } class Comment extends Node { constructor(data) { super(COMMENT_NODE); this.data = data == null ? "" : String(data); this.nodeName = "#comment"; } _shallowClone() { return new Comment(this.data); } } // ---- Element ------------------------------------------------------- class Element extends Node { constructor(tagName, ns) { super(ELEMENT_NODE); this.tagName = tagName; this.localName = String(tagName).toLowerCase(); this.nodeName = this.localName; this.namespaceURI = ns || null; this.attributes = {}; this._style = null; this._classList = null; if (this.localName === "template") this.content = new Fragment(); } _shallowClone() { var copy = new Element(this.tagName, this.namespaceURI); for (var k in this.attributes) copy.attributes[k] = this.attributes[k]; if (this.content) { var n = this.content.firstChild; while (n) { copy.content.appendChild(n.cloneNode(true)); n = n.nextSibling; } } return copy; } setAttribute(name, value) { this.attributes[name] = String(value); } setAttributeNS(_ns, name, value) { this.attributes[name] = String(value); } getAttribute(name) { return name in this.attributes ? this.attributes[name] : null; } hasAttribute(name) { return name in this.attributes; } removeAttribute(name) { delete this.attributes[name]; } removeAttributeNS(_ns, name) { delete this.attributes[name]; } get className() { return this.attributes["class"] || ""; } set className(v) { this.attributes["class"] = String(v); } get id() { return this.attributes["id"] || ""; } set id(v) { this.attributes["id"] = String(v); } get innerHTML() { return serializeChildren(this); } set innerHTML(htmlStr) { var target = this.content ? this.content : this; while (target.firstChild) target.removeChild(target.firstChild); var json = global.__parseHTML(String(htmlStr)); var nodes = buildNodes(JSON.parse(json)); for (var i = 0; i < nodes.length; i++) target.appendChild(nodes[i]); } get style() { if (!this._style) this._style = makeStyle(this); return this._style; } get classList() { if (!this._classList) this._classList = makeClassList(this); return this._classList; } // Event wiring is irrelevant to server output. addEventListener() {} removeEventListener() {} } class Fragment extends Node { constructor() { super(FRAGMENT_NODE); this.nodeName = "#document-fragment"; } _shallowClone() { return new Fragment(); } } // ---- style / classList shims -------------------------------------- function makeStyle(el) { return { setProperty: function (k, v) { var cur = parseStyle(el.attributes["style"] || ""); cur[k] = v; el.attributes["style"] = stringifyStyle(cur); }, removeProperty: function (k) { var cur = parseStyle(el.attributes["style"] || ""); delete cur[k]; el.attributes["style"] = stringifyStyle(cur); }, get cssText() { return el.attributes["style"] || ""; }, set cssText(v) { el.attributes["style"] = String(v); }, }; } function parseStyle(s) { var out = {}; s.split(";").forEach(function (decl) { var i = decl.indexOf(":"); if (i > -1) out[decl.slice(0, i).trim()] = decl.slice(i + 1).trim(); }); return out; } function stringifyStyle(o) { return Object.keys(o).map(function (k) { return k + ":" + o[k]; }).join(";"); } function makeClassList(el) { function read() { return (el.attributes["class"] || "").split(/\s+/).filter(Boolean); } function write(list) { el.attributes["class"] = list.join(" "); } return { add: function () { var l = read(); for (var i = 0; i < arguments.length; i++) if (l.indexOf(arguments[i]) < 0) l.push(arguments[i]); write(l); }, remove: function () { var l = read(), a = Array.prototype.slice.call(arguments); write(l.filter(function (c) { return a.indexOf(c) < 0; })); }, toggle: function (c, force) { var l = read(), has = l.indexOf(c) > -1; if (force === undefined ? has : !force) write(l.filter(function (x) { return x !== c; })); else if (!has) { l.push(c); write(l); } }, contains: function (c) { return read().indexOf(c) > -1; }, }; } // ---- build from parsed JSON --------------------------------------- function buildNodes(arr) { var out = []; for (var i = 0; i < arr.length; i++) out.push(buildNode(arr[i])); return out; } function buildNode(j) { if (j.t === "t") return new Text(j.d); if (j.t === "c") return new Comment(j.d); var el = new Element(j.n, j.ns === "svg" ? SVG_NS : null); if (j.a) for (var k in j.a) el.attributes[k] = j.a[k]; if (j.c) for (var i = 0; i < j.c.length; i++) el.appendChild(buildNode(j.c[i])); return el; } // ---- serialization ------------------------------------------------- function serializeChildren(node) { var out = "", n = node.firstChild; while (n) { out += serializeNode(n); n = n.nextSibling; } return out; } function serializeNode(node) { if (node.nodeType === TEXT_NODE) return escapeText(node.data); // "#" is solid-js/html's template insertion placeholder; any that // survive instantiation are framework artifacts, not page content. if (node.nodeType === COMMENT_NODE) return node.data === "#" ? "" : ""; if (node.nodeType === FRAGMENT_NODE) return serializeChildren(node); // Output the original-case tag (SVG is case-sensitive: viewBox, // linearGradient); use the lowercased localName only for lookups. var tag = node.tagName, lname = node.localName; var s = "<" + tag; for (var k in node.attributes) s += " " + k + '="' + escapeAttr(node.attributes[k]) + '"'; s += ">"; if (VOID[lname]) return s; if (lname === "template" && node.content) s += serializeChildren(node.content); else s += serializeChildren(node); return s + ""; } // ---- minimal querySelectorAll (only script,style and *[data-hk]) --- function querySelectorAll(root, sel) { var wantHk = /\[data-hk\]/.test(sel); var tags = sel.split(",").map(function (s) { return s.trim().replace(/\[.*\]/, "").replace("*", "").toLowerCase(); }).filter(Boolean); var out = []; (function walk(n) { var c = n.firstChild; while (c) { if (c.nodeType === ELEMENT_NODE) { if (wantHk && c.attributes["data-hk"] != null) out.push(c); else if (tags.indexOf(c.localName) > -1) out.push(c); walk(c); } c = c.nextSibling; } })(root.content || root); return out; } // ---- document ------------------------------------------------------ var document = { createElement: function (tag) { return new Element(tag, null); }, createElementNS: function (ns, tag) { return new Element(tag, ns); }, createTextNode: function (data) { return new Text(data); }, createComment: function (data) { return new Comment(data); }, createDocumentFragment: function () { return new Fragment(); }, importNode: function (node, deep) { return node.cloneNode(deep); }, addEventListener: function () {}, removeEventListener: function () {}, nodeType: 9, }; global.document = document; global.Node = Node; global.Element = Element; global.Text = Text; global.Comment = Comment; global.window = global; // Serialize a node's children (innerHTML) — the Go side calls this to // extract the rendered markup from the render root. global.__serialize = function (node) { return serializeChildren(node); }; })(globalThis);