Add js web stuff to landing page + documentation
This commit is contained in:
352
go/jsbundler/js/ssr/dom.js
Normal file
352
go/jsbundler/js/ssr/dom.js
Normal file
@@ -0,0 +1,352 @@
|
||||
// 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, "<").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 === "#" ? "" : "<!--" + 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);
|
||||
Reference in New Issue
Block a user