Files
kjol/go/bundler/js/ssr/dom.js

353 lines
12 KiB
JavaScript

// 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
function escapeAttr(s) {
return String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;");
}
// ---- 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 === "#" ? "" : "<!--" + 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 + "</" + tag + ">";
}
// ---- 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);