htmx demos working: activation, fetch, swap, OOB filtering, test runner page

- htmx-boot-subtree! wired into process-elements for auto-activation
- Fixed cond compilation bug in hx-verb-info (Clojure-style flat cond)
- Platform io-fetch upgraded: method/body/headers support, full response dict
- Replaced perform IO ops with browser primitives (set-timeout, browser-confirm, etc)
- SX→HTML rendering in hx-do-swap with OOB section filtering
- hx-collect-params: collects input name/value for all methods
- Handler naming: ex-{slug} convention, removed perform IO dependencies
- Test runner page at (test.(applications.(htmx))) with iframe-based runner
- Header "test" link on every page linking to test URL
- Page file restructure: 285 files moved to URL-matching paths (a/b/c/index.sx)
- page-functions.sx: ~100 component name references updated
- _test added to skip_dirs, test- file prefix convention for test files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 11:56:15 +00:00
parent 4f02f82f4e
commit 4aa49e42e8
16 changed files with 3201 additions and 1562 deletions

View File

@@ -2846,13 +2846,9 @@ let http_inject_shell_statics env static_dir sx_sxc =
(* Hash each .sxbc module individually and add to the hash index. (* Hash each .sxbc module individually and add to the hash index.
Each module's content is stored by hash; exported symbols map to the module hash. *) Each module's content is stored by hash; exported symbols map to the module hash. *)
let sxbc_dir = static_dir ^ "/wasm/sx" in let sxbc_dir = static_dir ^ "/wasm/sx" in
let module_manifest_path = sxbc_dir ^ "/module-manifest.json" in let module_manifest_path = sxbc_dir ^ "/module-manifest.sx" in
let module_hashes : (string, string) Hashtbl.t = Hashtbl.create 32 in (* module key → hash *) let module_hashes : (string, string) Hashtbl.t = Hashtbl.create 32 in (* module key → hash *)
(if Sys.file_exists module_manifest_path then begin (if Sys.file_exists module_manifest_path then begin
let manifest_src = In_channel.with_open_text module_manifest_path In_channel.input_all in
(* Simple JSON parse — extract "key": { "file": "...", "exports": [...] } *)
let exprs = try Sx_parser.parse_all ("(" ^ manifest_src ^ ")") with _ -> [] in
ignore exprs; (* The manifest is JSON, not SX — parse it manually *)
(* Read each .sxbc file, hash it, store in hash_to_def *) (* Read each .sxbc file, hash it, store in hash_to_def *)
if Sys.file_exists sxbc_dir && Sys.is_directory sxbc_dir then begin if Sys.file_exists sxbc_dir && Sys.is_directory sxbc_dir then begin
let files = Array.to_list (Sys.readdir sxbc_dir) in let files = Array.to_list (Sys.readdir sxbc_dir) in
@@ -3485,7 +3481,7 @@ let http_mode port =
(* Files to skip — declarative metadata, not needed for rendering *) (* Files to skip — declarative metadata, not needed for rendering *)
let skip_files = ["primitives.sx"; "types.sx"; "boundary.sx"; let skip_files = ["primitives.sx"; "types.sx"; "boundary.sx";
"harness.sx"; "eval-rules.sx"; "vm-inline.sx"] in "harness.sx"; "eval-rules.sx"; "vm-inline.sx"] in
let skip_dirs = ["tests"; "test"; "plans"; "essays"; "spec"; "client-libs"] in let skip_dirs = ["tests"; "test"; "plans"; "essays"; "spec"; "client-libs"; "_test"] in
let rec load_dir ?(base="") dir = let rec load_dir ?(base="") dir =
if Sys.file_exists dir && Sys.is_directory dir then begin if Sys.file_exists dir && Sys.is_directory dir then begin
let entries = Sys.readdir dir in let entries = Sys.readdir dir in

View File

@@ -98,8 +98,33 @@
try { driveAsync(result.resume(null)); } catch(e) { console.error("[sx] driveAsync:", e.message); } try { driveAsync(result.resume(null)); } catch(e) { console.error("[sx] driveAsync:", e.message); }
}, typeof arg === "number" ? arg : 0); }, typeof arg === "number" ? arg : 0);
} else if (opName === "io-fetch") { } else if (opName === "io-fetch") {
fetch(typeof arg === "string" ? arg : "").then(function(r) { return r.text(); }).then(function(t) { var fetchUrl = typeof arg === "string" ? arg : "";
try { driveAsync(result.resume({ok: true, text: t})); } catch(e) { console.error("[sx] driveAsync:", e.message); } var fetchMethod = (items && items[2]) || "GET";
var fetchBody = items && items[3];
var fetchHeaders = items && items[4];
var fetchOpts = { method: typeof fetchMethod === "string" ? fetchMethod : "GET" };
if (fetchBody && typeof fetchBody !== "boolean") {
fetchOpts.body = typeof fetchBody === "string" ? fetchBody : JSON.stringify(fetchBody);
}
if (fetchHeaders && typeof fetchHeaders === "object") {
var h = {};
var keys = fetchHeaders._keys || Object.keys(fetchHeaders);
for (var fi = 0; fi < keys.length; fi++) {
var k = keys[fi], v = fetchHeaders[k];
if (typeof k === "string" && typeof v === "string") h[k] = v;
}
fetchOpts.headers = h;
}
fetch(fetchUrl, fetchOpts).then(function(r) {
var hdrs = {};
try { r.headers.forEach(function(v, k) { hdrs[k] = v; }); } catch(e) {}
return r.text().then(function(t) {
return { status: r.status, body: t, headers: hdrs, ok: r.ok };
});
}).then(function(resp) {
try { driveAsync(result.resume(resp)); } catch(e) { console.error("[sx] driveAsync:", e.message); }
}).catch(function(e) {
try { driveAsync(result.resume({status: 0, body: "", headers: {}, ok: false})); } catch(e2) { console.error("[sx] driveAsync:", e2.message); }
}); });
} else if (opName === "io-navigate") { } else if (opName === "io-navigate") {
// navigation — don't resume // navigation — don't resume
@@ -273,7 +298,7 @@
} }
} }
// Content-addressed boot: script loaded from /sx/h/{hash}, not /static/wasm/. // Content-addressed boot: script loaded from /sx/h/{hash}, not /static/wasm/.
// Fall back to /static/wasm/ base URL for module-manifest.json and .sx sources. // Fall back to /static/wasm/ base URL for module-manifest.sx and .sx sources.
if (!_baseUrl || _baseUrl.indexOf("/sx/h/") !== -1) { if (!_baseUrl || _baseUrl.indexOf("/sx/h/") !== -1) {
_baseUrl = "/static/wasm/"; _baseUrl = "/static/wasm/";
} }
@@ -522,6 +547,22 @@
var _manifest = null; var _manifest = null;
var _loadedLibs = {}; var _loadedLibs = {};
/**
* Convert K.parse output (tagged {_type, ...} objects) to plain JS.
* SX nil (from empty lists `()`) becomes [].
*/
function sxDataToJs(v) {
if (v === null || v === undefined) return [];
if (typeof v !== "object") return v;
if (v._type === "list") return (v.items || []).map(sxDataToJs);
if (v._type === "dict") {
var out = {};
for (var k in v) if (k !== "_type") out[k] = sxDataToJs(v[k]);
return out;
}
return v;
}
/** /**
* Fetch and parse the module manifest (library deps + file paths). * Fetch and parse the module manifest (library deps + file paths).
*/ */
@@ -529,11 +570,14 @@
if (_manifest) return _manifest; if (_manifest) return _manifest;
try { try {
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open("GET", _baseUrl + "sx/module-manifest.json" + _cacheBust, false); xhr.open("GET", _baseUrl + "sx/module-manifest.sx" + _cacheBust, false);
xhr.send(); xhr.send();
if (xhr.status === 200) { if (xhr.status === 200) {
_manifest = JSON.parse(xhr.responseText); var parsed = K.parse(xhr.responseText);
return _manifest; if (parsed && parsed.length > 0) {
_manifest = sxDataToJs(parsed[0]);
return _manifest;
}
} }
} catch(e) {} } catch(e) {}
console.warn("[sx-platform] No manifest found, falling back to full load"); console.warn("[sx-platform] No manifest found, falling back to full load");
@@ -699,6 +743,32 @@
} }
} }
// Merge definitions from a new page's manifest (called during navigation)
function mergeManifest(el) {
if (!el) return;
try {
var incoming = JSON.parse(el.textContent);
var newDefs = incoming.defs || {};
// Ensure base manifest is loaded
if (!_pageManifest) loadPageManifest();
if (!_pageManifest) _pageManifest = { defs: {} };
if (!_pageManifest.defs) _pageManifest.defs = {};
for (var name in newDefs) {
_pageManifest.defs[name] = newDefs[name];
_hashToName[newDefs[name]] = name;
}
// Merge hash store entries
if (incoming.store) {
if (!_pageManifest.store) _pageManifest.store = {};
for (var h in incoming.store) {
_pageManifest.store[h] = incoming.store[h];
}
}
} catch(e) {
console.warn("[sx] Failed to merge manifest:", e);
}
}
function resolveHash(hash) { function resolveHash(hash) {
// 1. In-memory cache // 1. In-memory cache
if (_hashCache[hash]) return _hashCache[hash]; if (_hashCache[hash]) return _hashCache[hash];
@@ -833,6 +903,7 @@
renderToHtml: function(expr) { return K.renderToHtml(expr); }, renderToHtml: function(expr) { return K.renderToHtml(expr); },
callFn: function(fn, args) { return K.callFn(fn, args); }, callFn: function(fn, args) { return K.callFn(fn, args); },
engine: function() { return K.engine(); }, engine: function() { return K.engine(); },
mergeManifest: function(el) { return mergeManifest(el); },
// Boot entry point (called by auto-init or manually) // Boot entry point (called by auto-init or manually)
init: function() { init: function() {
if (typeof K.eval === "function") { if (typeof K.eval === "function") {

1211
lib/hyperscript/htmx.sx Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,257 +1,223 @@
// sx-test-runner.js — Run SX test specs in the browser using sx-browser.js. /**
// Supports both legacy (monolithic test.sx) and modular (per-spec) modes. * SX Test Runner — drives tests in an iframe, reports results.
* Updates [data-test] elements in-place with pass/fail icons.
* Expects: #run-btn, #test-status, #test-summary, #test-iframe, [data-test]
*/
(function() { (function() {
var NIL = Sx.NIL; "use strict";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function deepEqual(a, b) { var TESTS = [
if (a === b) return true; { name: "click-to-load", actions: [
if (isNil(a) && isNil(b)) return true; { type: "click", selector: "button[hx-get]" },
if (typeof a !== typeof b) return false; { type: "wait", ms: 2000 },
if (Array.isArray(a) && Array.isArray(b)) { { type: "assert-text", selector: "#click-result", contains: "Content loaded!" }
if (a.length !== b.length) return false; ]},
for (var i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false; { name: "click-no-oob-leak", actions: [
return true; { type: "click", selector: "button[hx-get]" },
{ type: "wait", ms: 2000 },
{ type: "assert-text", selector: "#click-result", notContains: "defcomp" }
]},
{ name: "search-debounce", actions: [
{ type: "fill", selector: "input[hx-get]", value: "hx-get" },
{ type: "wait", ms: 1500 },
{ type: "assert-text", selector: "#search-results", contains: "GET request" }
]},
{ name: "search-no-results", actions: [
{ type: "fill", selector: "input[hx-get]", value: "xyznonexistent" },
{ type: "wait", ms: 1500 },
{ type: "assert-text", selector: "#search-results", contains: "No results" }
]},
{ name: "tab-overview", actions: [
{ type: "click", selector: "button[hx-get*='tab=overview']" },
{ type: "wait", ms: 2000 },
{ type: "assert-text", selector: "#htmx-tab-content", contains: "htmx gives you access" }
]},
{ name: "tab-features", actions: [
{ type: "click", selector: "button[hx-get*='tab=features']" },
{ type: "wait", ms: 2000 },
{ type: "assert-text", selector: "#htmx-tab-content", contains: "Any element" }
]},
{ name: "append-item", actions: [
{ type: "click", selector: "button[hx-post*='api.append']" },
{ type: "wait", ms: 2000 },
{ type: "assert-count", selector: "#item-list > *", gte: 1 }
]},
{ name: "form-submit", actions: [
{ type: "fill", selector: "form[hx-post] input[name='name']", value: "Alice" },
{ type: "fill", selector: "form[hx-post] input[name='email']", value: "alice@test.com" },
{ type: "click", selector: "form[hx-post] button[type='submit']" },
{ type: "wait", ms: 2000 },
{ type: "assert-text", selector: "#form-result", contains: "Alice" }
]}
];
if (window.__sxTests) TESTS = window.__sxTests;
var runBtn, statusEl, summaryEl, iframe;
function init() {
runBtn = document.getElementById("run-btn");
statusEl = document.getElementById("test-status");
summaryEl = document.getElementById("test-summary");
iframe = document.getElementById("test-iframe");
if (runBtn) {
console.log("[sx-test] Runner initialized, " + TESTS.length + " tests");
runBtn.addEventListener("click", function() {
console.log("[sx-test] Run clicked");
runAll();
});
} else {
console.warn("[sx-test] #run-btn not found");
} }
if (a && typeof a === "object" && b && typeof b === "object") {
var ka = Object.keys(a), kb = Object.keys(b);
if (ka.length !== kb.length) return false;
for (var j = 0; j < ka.length; j++) if (!deepEqual(a[ka[j]], b[ka[j]])) return false;
return true;
}
return false;
} }
// --- Platform functions shared across all specs --- function sleep(ms) { return new Promise(function(r) { setTimeout(r, ms); }); }
function makeEnv() {
var stack = [], passed = 0, failed = 0, num = 0, lines = []; function waitForReady(doc, timeout) {
var env = { return new Promise(function(resolve, reject) {
"try-call": function(thunk) { var start = Date.now();
try { (function check() {
Sx.eval([thunk], env); try { if (doc.documentElement.getAttribute("data-sx-ready") === "true") return resolve(); } catch(e) {}
return { ok: true }; if (Date.now() - start > timeout) return reject(new Error("Timeout"));
} catch(e) { setTimeout(check, 200);
return { ok: false, error: e.message || String(e) }; })();
});
}
function reloadIframe() {
return new Promise(function(resolve) {
iframe.addEventListener("load", function onLoad() {
iframe.removeEventListener("load", onLoad);
resolve();
});
iframe.contentWindow.location.reload();
});
}
function updateTestItem(name, state, error) {
var item = document.querySelector('[data-test="' + name + '"]');
if (!item) return;
var icon = item.querySelector('[data-role="test-icon"]');
if (!icon) return;
if (state === "running") {
icon.textContent = "⟳";
icon.style.color = "#7c3aed";
item.style.borderColor = "#c4b5fd";
} else if (state === "pass") {
icon.textContent = "✓";
icon.style.color = "#16a34a";
item.style.borderColor = "#bbf7d0";
item.style.backgroundColor = "#f0fdf4";
} else if (state === "fail") {
icon.textContent = "✗";
icon.style.color = "#dc2626";
item.style.borderColor = "#fecaca";
item.style.backgroundColor = "#fef2f2";
if (error) {
var errEl = item.querySelector('[data-role="test-error"]');
if (!errEl) {
errEl = document.createElement("div");
errEl.setAttribute("data-role", "test-error");
errEl.style.cssText = "padding:8px 16px;font-size:12px;color:#dc2626;border-top:1px solid #fecaca;background:#fef2f2";
item.querySelector("summary").after(errEl);
} }
}, errEl.textContent = error;
"report-pass": function(name) { }
num++; passed++;
lines.push("ok " + num + " - " + stack.concat([name]).join(" > "));
},
"report-fail": function(name, error) {
num++; failed++;
lines.push("not ok " + num + " - " + stack.concat([name]).join(" > "));
lines.push(" # " + error);
},
"push-suite": function(name) { stack.push(name); },
"pop-suite": function() { stack.pop(); },
// Primitives that sx-browser.js may not expose in env
"equal?": function(a, b) { return deepEqual(a, b); },
"eq?": function(a, b) { return a === b; },
"boolean?": function(x) { return typeof x === "boolean"; },
"string-length": function(s) { return String(s).length; },
"substring": function(s, start, end) { return String(s).slice(start, end); },
"string-contains?": function(s, n) { return String(s).indexOf(n) !== -1; },
"upcase": function(s) { return String(s).toUpperCase(); },
"downcase": function(s) { return String(s).toLowerCase(); },
"reverse": function(c) { return c ? c.slice().reverse() : []; },
"flatten": function(c) {
var r = [];
for (var i = 0; i < (c||[]).length; i++) {
if (Array.isArray(c[i])) for (var j = 0; j < c[i].length; j++) r.push(c[i][j]);
else r.push(c[i]);
}
return r;
},
"has-key?": function(d, k) { return d && typeof d === "object" && k in d; },
"append": function(c, x) { return Array.isArray(x) ? (c||[]).concat(x) : (c||[]).concat([x]); },
"for-each-indexed": function(f, coll) {
for (var i = 0; i < (coll||[]).length; i++) Sx.eval([f, i, coll[i]], env);
},
"for-each": function(f, coll) {
for (var i = 0; i < (coll||[]).length; i++) Sx.eval([f, coll[i]], env);
},
"dict-set!": function(d, k, v) { if (d) d[k] = v; },
"dict-has?": function(d, k) { return d && typeof d === "object" && k in d; },
"dict-get": function(d, k) { return d ? d[k] : undefined; },
"starts-with?": function(s, prefix) { return String(s).indexOf(prefix) === 0; },
"ends-with?": function(s, suffix) { var str = String(s); return str.indexOf(suffix) === str.length - suffix.length; },
"slice": function(s, start, end) { return end !== undefined ? s.slice(start, end) : s.slice(start); },
"inc": function(n) { return n + 1; },
"append!": function(arr, item) { if (Array.isArray(arr)) arr.push(item); },
"dict": function() { return {}; },
// --- Parser platform functions ---
"sx-parse": function(source) { return Sx.parseAll(source); },
"sx-serialize": function(val) {
if (val === NIL || val === null || val === undefined) return "nil";
if (typeof val === "boolean") return val ? "true" : "false";
if (typeof val === "number") return String(val);
if (typeof val === "string") return '"' + val.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
if (val && (val._sym || val._sx_symbol)) return val.name;
if (val && (val._kw || val._sx_keyword)) return ":" + val.name;
if (Array.isArray(val)) return "(" + val.map(function(x) { return env["sx-serialize"](x); }).join(" ") + ")";
if (val && typeof val === "object") {
var parts = [];
Object.keys(val).forEach(function(k) { parts.push(":" + k); parts.push(env["sx-serialize"](val[k])); });
return "{" + parts.join(" ") + "}";
}
return String(val);
},
"make-symbol": function(name) { return Sx.sym ? Sx.sym(name) : { _sx_symbol: true, name: name, toString: function() { return name; } }; },
"make-keyword": function(name) { return Sx.kw ? Sx.kw(name) : { _sx_keyword: true, name: name, toString: function() { return name; } }; },
"symbol-name": function(s) { return s && s.name ? s.name : String(s); },
"keyword-name": function(k) { return k && k.name ? k.name : String(k); },
// --- Render platform function ---
"render-html": function(sxSource) {
if (!Sx.renderToHtml) throw new Error("render-to-html not available");
var exprs = Sx.parseAll(sxSource);
var result = "";
for (var i = 0; i < exprs.length; i++) result += Sx.renderToHtml(exprs[i], env);
return result;
},
};
return { env: env, getResults: function() { return { passed: passed, failed: failed, num: num, lines: lines }; } };
}
function evalSource(src, env) {
var exprs = Sx.parseAll(src);
for (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env);
}
function loadRouterFromBootstrap(env) {
if (Sx.splitPathSegments) {
env["split-path-segments"] = Sx.splitPathSegments;
env["parse-route-pattern"] = Sx.parseRoutePattern;
env["match-route-segments"] = Sx.matchRouteSegments;
env["match-route"] = Sx.matchRoute;
env["find-matching-route"] = Sx.findMatchingRoute;
env["make-route-segment"] = Sx.makeRouteSegment;
} }
} }
function loadDepsFromBootstrap(env) { function resetAllItems() {
if (Sx.scanRefs) { var items = document.querySelectorAll("[data-test]");
env["scan-refs"] = Sx.scanRefs; for (var i = 0; i < items.length; i++) {
env["scan-components-from-source"] = Sx.scanComponentsFromSource; var icon = items[i].querySelector('[data-role="test-icon"]');
env["transitive-deps"] = Sx.transitiveDeps; if (icon) { icon.textContent = "○"; icon.style.color = ""; }
env["compute-all-deps"] = Sx.computeAllDeps; items[i].style.borderColor = "";
env["components-needed"] = Sx.componentsNeeded; items[i].style.backgroundColor = "";
env["page-component-bundle"] = Sx.pageComponentBundle; var err = items[i].querySelector('[data-role="test-error"]');
env["page-css-classes"] = Sx.pageCssClasses; if (err) err.remove();
env["scan-io-refs"] = Sx.scanIoRefs;
env["transitive-io-refs"] = Sx.transitiveIoRefs;
env["compute-all-io-refs"] = Sx.computeAllIoRefs;
env["component-pure?"] = Sx.componentPure_p;
env["test-env"] = function() { return env; };
} }
} }
function loadEngineFromBootstrap(env) { async function runOneTest(test) {
if (Sx.parseTime) { var doc = iframe.contentDocument;
env["parse-time"] = Sx.parseTime; for (var j = 0; j < test.actions.length; j++) {
env["parse-trigger-spec"] = Sx.parseTriggerSpec; var a = test.actions[j];
env["default-trigger"] = Sx.defaultTrigger; if (a.type === "click") {
env["parse-swap-spec"] = Sx.parseSwapSpec; var el = doc.querySelector(a.selector);
env["parse-retry-spec"] = Sx.parseRetrySpec; if (!el) throw new Error("Not found: " + a.selector);
env["next-retry-ms"] = function(cur, cap) { return Math.min(cur * 2, cap); }; el.click();
env["filter-params"] = Sx.filterParams; } else if (a.type === "fill") {
var el = doc.querySelector(a.selector);
if (!el) throw new Error("Not found: " + a.selector);
el.focus();
el.value = "";
el.value = a.value;
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
} else if (a.type === "wait") {
await sleep(a.ms);
} else if (a.type === "assert-text") {
var el = doc.querySelector(a.selector);
if (!el) throw new Error("Not found: " + a.selector);
var txt = el.textContent || "";
if (a.contains && txt.indexOf(a.contains) === -1)
throw new Error("Expected '" + a.contains + "' in '" + txt.substring(0, 60) + "'");
if (a.notContains && txt.indexOf(a.notContains) !== -1)
throw new Error("Unexpected '" + a.notContains + "'");
} else if (a.type === "assert-count") {
var els = doc.querySelectorAll(a.selector);
if (a.gte !== undefined && els.length < a.gte)
throw new Error("Expected >=" + a.gte + ", got " + els.length);
}
} }
} }
// --- Legacy runner (monolithic test.sx) --- async function runAll() {
window.sxRunTests = function(srcId, outId, btnId) { resetAllItems();
var src = document.getElementById(srcId).textContent; summaryEl.innerHTML = "";
var out = document.getElementById(outId); runBtn.disabled = true;
var btn = document.getElementById(btnId); runBtn.style.opacity = "0.5";
var ctx = makeEnv(); var passed = 0, failed = 0;
try { for (var i = 0; i < TESTS.length; i++) {
var t0 = performance.now(); var test = TESTS[i];
evalSource(src, ctx.env); statusEl.textContent = test.name + " (" + (i+1) + "/" + TESTS.length + ")";
var elapsed = Math.round(performance.now() - t0); updateTestItem(test.name, "running");
var r = ctx.getResults();
r.lines.push("");
r.lines.push("1.." + r.num);
r.lines.push("# tests " + (r.passed + r.failed));
r.lines.push("# pass " + r.passed);
if (r.failed > 0) r.lines.push("# fail " + r.failed);
r.lines.push("# time " + elapsed + "ms");
} catch(e) {
var r = ctx.getResults();
r.lines.push("");
r.lines.push("FATAL: " + (e.message || String(e)));
}
out.textContent = r.lines.join("\n"); if (i > 0) await reloadIframe();
out.style.display = "block";
btn.textContent = r.passed + "/" + (r.passed + r.failed) + " passed" + (r.failed === 0 ? "" : " (" + r.failed + " failed)");
btn.className = r.failed > 0
? "px-4 py-2 rounded-md bg-red-600 text-white font-medium text-sm cursor-default"
: "px-4 py-2 rounded-md bg-green-600 text-white font-medium text-sm cursor-default";
};
// --- Modular runner (per-spec or all) --- try {
var SPECS = { await waitForReady(iframe.contentDocument, 15000);
"eval": { needs: [] }, await sleep(500);
"parser": { needs: ["sx-parse"] }, } catch(e) {
"router": { needs: [] }, updateTestItem(test.name, "fail", "Page load timeout");
"render": { needs: ["render-html"] }, failed++;
"deps": { needs: [] }, continue;
"engine": { needs: [] },
};
window.sxRunModularTests = function(specName, outId, btnId) {
var out = document.getElementById(outId);
var btn = document.getElementById(btnId);
var ctx = makeEnv();
var specs = specName === "all" ? Object.keys(SPECS) : [specName];
try {
var t0 = performance.now();
// Load framework
var fwEl = document.getElementById("test-framework-source");
if (fwEl) {
evalSource(fwEl.textContent, ctx.env);
} }
for (var si = 0; si < specs.length; si++) { try {
var sn = specs[si]; await runOneTest(test);
if (!SPECS[sn]) continue; updateTestItem(test.name, "pass");
passed++;
// Load module functions from bootstrap } catch(err) {
if (sn === "router") loadRouterFromBootstrap(ctx.env); updateTestItem(test.name, "fail", err.message);
if (sn === "deps") loadDepsFromBootstrap(ctx.env); failed++;
if (sn === "engine") loadEngineFromBootstrap(ctx.env);
// Find spec source — either per-spec textarea or embedded in overview
var specEl = document.getElementById("test-spec-" + sn);
if (specEl) {
evalSource(specEl.textContent, ctx.env);
}
} }
var elapsed = Math.round(performance.now() - t0);
var r = ctx.getResults();
r.lines.push("");
r.lines.push("1.." + r.num);
r.lines.push("# tests " + (r.passed + r.failed));
r.lines.push("# pass " + r.passed);
if (r.failed > 0) r.lines.push("# fail " + r.failed);
r.lines.push("# time " + elapsed + "ms");
} catch(e) {
var r = ctx.getResults();
r.lines.push("");
r.lines.push("FATAL: " + (e.message || String(e)));
} }
out.textContent = r.lines.join("\n"); runBtn.disabled = false;
out.style.display = "block"; runBtn.style.opacity = "1";
btn.textContent = r.passed + "/" + (r.passed + r.failed) + " passed" + (r.failed === 0 ? "" : " (" + r.failed + " failed)"); statusEl.textContent = "Done";
btn.className = r.failed > 0 var total = passed + failed;
? "px-4 py-2 rounded-md bg-red-600 text-white font-medium text-sm cursor-default" summaryEl.innerHTML = '<div style="padding:12px;border-radius:8px;font-weight:600;' +
: "px-4 py-2 rounded-md bg-green-600 text-white font-medium text-sm cursor-default"; (failed === 0 ? 'background:#f0fdf4;color:#166534' : 'background:#fef2f2;color:#991b1b') +
}; '">' + passed + '/' + total + ' tests passed</div>';
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})(); })();

View File

@@ -98,8 +98,33 @@
try { driveAsync(result.resume(null)); } catch(e) { console.error("[sx] driveAsync:", e.message); } try { driveAsync(result.resume(null)); } catch(e) { console.error("[sx] driveAsync:", e.message); }
}, typeof arg === "number" ? arg : 0); }, typeof arg === "number" ? arg : 0);
} else if (opName === "io-fetch") { } else if (opName === "io-fetch") {
fetch(typeof arg === "string" ? arg : "").then(function(r) { return r.text(); }).then(function(t) { var fetchUrl = typeof arg === "string" ? arg : "";
try { driveAsync(result.resume({ok: true, text: t})); } catch(e) { console.error("[sx] driveAsync:", e.message); } var fetchMethod = (items && items[2]) || "GET";
var fetchBody = items && items[3];
var fetchHeaders = items && items[4];
var fetchOpts = { method: typeof fetchMethod === "string" ? fetchMethod : "GET" };
if (fetchBody && typeof fetchBody !== "boolean") {
fetchOpts.body = typeof fetchBody === "string" ? fetchBody : JSON.stringify(fetchBody);
}
if (fetchHeaders && typeof fetchHeaders === "object") {
var h = {};
var keys = fetchHeaders._keys || Object.keys(fetchHeaders);
for (var fi = 0; fi < keys.length; fi++) {
var k = keys[fi], v = fetchHeaders[k];
if (typeof k === "string" && typeof v === "string") h[k] = v;
}
fetchOpts.headers = h;
}
fetch(fetchUrl, fetchOpts).then(function(r) {
var hdrs = {};
try { r.headers.forEach(function(v, k) { hdrs[k] = v; }); } catch(e) {}
return r.text().then(function(t) {
return { status: r.status, body: t, headers: hdrs, ok: r.ok };
});
}).then(function(resp) {
try { driveAsync(result.resume(resp)); } catch(e) { console.error("[sx] driveAsync:", e.message); }
}).catch(function(e) {
try { driveAsync(result.resume({status: 0, body: "", headers: {}, ok: false})); } catch(e2) { console.error("[sx] driveAsync:", e2.message); }
}); });
} else if (opName === "io-navigate") { } else if (opName === "io-navigate") {
// navigation — don't resume // navigation — don't resume
@@ -273,7 +298,7 @@
} }
} }
// Content-addressed boot: script loaded from /sx/h/{hash}, not /static/wasm/. // Content-addressed boot: script loaded from /sx/h/{hash}, not /static/wasm/.
// Fall back to /static/wasm/ base URL for module-manifest.json and .sx sources. // Fall back to /static/wasm/ base URL for module-manifest.sx and .sx sources.
if (!_baseUrl || _baseUrl.indexOf("/sx/h/") !== -1) { if (!_baseUrl || _baseUrl.indexOf("/sx/h/") !== -1) {
_baseUrl = "/static/wasm/"; _baseUrl = "/static/wasm/";
} }
@@ -522,6 +547,22 @@
var _manifest = null; var _manifest = null;
var _loadedLibs = {}; var _loadedLibs = {};
/**
* Convert K.parse output (tagged {_type, ...} objects) to plain JS.
* SX nil (from empty lists `()`) becomes [].
*/
function sxDataToJs(v) {
if (v === null || v === undefined) return [];
if (typeof v !== "object") return v;
if (v._type === "list") return (v.items || []).map(sxDataToJs);
if (v._type === "dict") {
var out = {};
for (var k in v) if (k !== "_type") out[k] = sxDataToJs(v[k]);
return out;
}
return v;
}
/** /**
* Fetch and parse the module manifest (library deps + file paths). * Fetch and parse the module manifest (library deps + file paths).
*/ */
@@ -529,11 +570,14 @@
if (_manifest) return _manifest; if (_manifest) return _manifest;
try { try {
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open("GET", _baseUrl + "sx/module-manifest.json" + _cacheBust, false); xhr.open("GET", _baseUrl + "sx/module-manifest.sx" + _cacheBust, false);
xhr.send(); xhr.send();
if (xhr.status === 200) { if (xhr.status === 200) {
_manifest = JSON.parse(xhr.responseText); var parsed = K.parse(xhr.responseText);
return _manifest; if (parsed && parsed.length > 0) {
_manifest = sxDataToJs(parsed[0]);
return _manifest;
}
} }
} catch(e) {} } catch(e) {}
console.warn("[sx-platform] No manifest found, falling back to full load"); console.warn("[sx-platform] No manifest found, falling back to full load");
@@ -699,6 +743,32 @@
} }
} }
// Merge definitions from a new page's manifest (called during navigation)
function mergeManifest(el) {
if (!el) return;
try {
var incoming = JSON.parse(el.textContent);
var newDefs = incoming.defs || {};
// Ensure base manifest is loaded
if (!_pageManifest) loadPageManifest();
if (!_pageManifest) _pageManifest = { defs: {} };
if (!_pageManifest.defs) _pageManifest.defs = {};
for (var name in newDefs) {
_pageManifest.defs[name] = newDefs[name];
_hashToName[newDefs[name]] = name;
}
// Merge hash store entries
if (incoming.store) {
if (!_pageManifest.store) _pageManifest.store = {};
for (var h in incoming.store) {
_pageManifest.store[h] = incoming.store[h];
}
}
} catch(e) {
console.warn("[sx] Failed to merge manifest:", e);
}
}
function resolveHash(hash) { function resolveHash(hash) {
// 1. In-memory cache // 1. In-memory cache
if (_hashCache[hash]) return _hashCache[hash]; if (_hashCache[hash]) return _hashCache[hash];
@@ -833,6 +903,7 @@
renderToHtml: function(expr) { return K.renderToHtml(expr); }, renderToHtml: function(expr) { return K.renderToHtml(expr); },
callFn: function(fn, args) { return K.callFn(fn, args); }, callFn: function(fn, args) { return K.callFn(fn, args); },
engine: function() { return K.engine(); }, engine: function() { return K.engine(); },
mergeManifest: function(el) { return mergeManifest(el); },
// Boot entry point (called by auto-init or manually) // Boot entry point (called by auto-init or manually)
init: function() { init: function() {
if (typeof K.eval === "function") { if (typeof K.eval === "function") {

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -415,6 +415,14 @@
(swap-dom-nodes t oob s) (swap-dom-nodes t oob s)
(post-swap t))) (post-swap t)))
(hoist-head-elements container) (hoist-head-elements container)
(let
((manifest-el (dom-query-in container "script[data-sx-manifest]")))
(when
manifest-el
(host-call
(host-global "Sx")
"mergeManifest"
manifest-el)))
(let (let
((html (select-from-container container select-sel))) ((html (select-from-container container select-sel)))
(with-transition (with-transition
@@ -1547,7 +1555,8 @@
(process-sse root) (process-sse root)
(bind-inline-handlers root) (bind-inline-handlers root)
(process-emit-elements root) (process-emit-elements root)
(hs-boot-subtree! (or root (host-global "document"))))) (hs-boot-subtree! (or root (host-global "document")))
(htmx-boot-subtree! (or root (host-global "document")))))
(define (define
process-one process-one
:effects (mutation io) :effects (mutation io)
@@ -1622,9 +1631,11 @@
(pathname (url-pathname url))) (pathname (url-pathname url)))
(when (when
target target
(let (when
((headers (dict "SX-Request" "true"))) (not (try-client-route pathname target-sel))
(fetch-and-restore target url headers scrollY)))))) (let
((headers (build-request-headers target (loaded-component-names))))
(fetch-and-restore target url headers scrollY)))))))
(define (define
engine-init engine-init
:effects (mutation io) :effects (mutation io)

File diff suppressed because one or more lines are too long

View File

@@ -1792,7 +1792,7 @@
blake2_js_for_wasm_create: blake2_js_for_wasm_create}; blake2_js_for_wasm_create: blake2_js_for_wasm_create};
} }
(globalThis)) (globalThis))
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["re-9a0de245",[2]],["sx-80a20737",[2,3]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,5]],["dune__exe__Sx_browser-653fa705",[2,4,6]],["std_exit-10fb8830",[2]],["start-f808dbe1",0]],"generated":(b=>{var ({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["re-9a0de245",[2]],["sx-faa4f9b6",[2,3]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,5]],["dune__exe__Sx_browser-efe9c27c",[2,4,6]],["std_exit-10fb8830",[2]],["start-f808dbe1",0]],"generated":(b=>{var
c=b,a=b?.module?.export||b;return{"env":{"caml_ba_kind_of_typed_array":()=>{throw new c=b,a=b?.module?.export||b;return{"env":{"caml_ba_kind_of_typed_array":()=>{throw new
Error("caml_ba_kind_of_typed_array not implemented")},"caml_exn_with_js_backtrace":()=>{throw new Error("caml_ba_kind_of_typed_array not implemented")},"caml_exn_with_js_backtrace":()=>{throw new
Error("caml_exn_with_js_backtrace not implemented")},"caml_int64_create_lo_mi_hi":()=>{throw new Error("caml_exn_with_js_backtrace not implemented")},"caml_int64_create_lo_mi_hi":()=>{throw new
@@ -1818,4 +1818,4 @@ a()},"Js_of_ocaml__Json.fragments":{"get_JSON":a=>a.JSON,"get_constructor":a=>a.
a(b)},"Js_of_ocaml__Dom_svg.fragments":{"get_SVGElement":a=>a.SVGElement,"get_document":a=>a.document,"get_tagName":a=>a.tagName,"meth_call_0_toLowerCase":a=>a.toLowerCase(),"meth_call_1_getElementById":(a,b)=>a.getElementById(b),"meth_call_2_createElementNS":(a,b,c)=>a.createElementNS(b,c)},"Js_of_ocaml__EventSource.fragments":{"get_EventSource":a=>a.EventSource,"obj_9":()=>({}),"set_withCredentials":(a,b)=>a.withCredentials=b},"Js_of_ocaml__Geolocation.fragments":{"get_geolocation":a=>a.geolocation,"get_navigator":a=>a.navigator,"obj_10":()=>({})},"Js_of_ocaml__IntersectionObserver.fragments":{"get_IntersectionObserver":a=>a.IntersectionObserver,"obj_11":()=>({})},"Js_of_ocaml__Intl.fragments":{"get_Collator":a=>a.Collator,"get_DateTimeFormat":a=>a.DateTimeFormat,"get_Intl":a=>a.Intl,"get_NumberFormat":a=>a.NumberFormat,"get_PluralRules":a=>a.PluralRules,"obj_12":a=>({localeMatcher:a}),"obj_13":(a,b,c,d,e,f)=>({localeMatcher:a,usage:b,sensitivity:c,ignorePunctuation:d,numeric:e,caseFirst:f}),"obj_14":(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t)=>({dateStyle:a,timeStyle:b,calendar:c,dayPeriod:d,numberingSystem:e,localeMatcher:f,timeZone:g,hour12:h,hourCycle:i,formatMatcher:j,weekday:k,era:l,year:m,month:n,day:o,hour:p,minute:q,second:r,fractionalSecondDigits:s,timeZoneName:t}),"obj_15":(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u)=>({compactDisplay:a,currency:b,currencyDisplay:c,currencySign:d,localeMatcher:e,notation:f,numberingSystem:g,signDisplay:h,style:i,unit:j,unitDisplay:k,useGrouping:l,roundingMode:m,roundingPriority:n,roundingIncrement:o,trailingZeroDisplay:p,minimumIntegerDigits:q,minimumFractionDigits:r,maximumFractionDigits:s,minimumSignificantDigits:t,maximumSignificantDigits:u}),"obj_16":(a,b)=>({localeMatcher:a,type:b})},"Dune__exe__Sx_browser.fragments":{"fun_call_1":(a,b)=>a(b),"fun_call_3":(a,b,c,d)=>a(b,c,d),"get_Array":a=>a.Array,"get_Object":a=>a.Object,"get___sx_handle":a=>a.__sx_handle,"get__driveAsync":a=>a._driveAsync,"get__type":a=>a._type,"get_console":a=>a.console,"get_items":a=>a.items,"get_length":a=>a.length,"get_name":a=>a.name,"js_expr_10d25c5c":()=>function(a){return function(){b.__sxR=undefined;var a(b)},"Js_of_ocaml__Dom_svg.fragments":{"get_SVGElement":a=>a.SVGElement,"get_document":a=>a.document,"get_tagName":a=>a.tagName,"meth_call_0_toLowerCase":a=>a.toLowerCase(),"meth_call_1_getElementById":(a,b)=>a.getElementById(b),"meth_call_2_createElementNS":(a,b,c)=>a.createElementNS(b,c)},"Js_of_ocaml__EventSource.fragments":{"get_EventSource":a=>a.EventSource,"obj_9":()=>({}),"set_withCredentials":(a,b)=>a.withCredentials=b},"Js_of_ocaml__Geolocation.fragments":{"get_geolocation":a=>a.geolocation,"get_navigator":a=>a.navigator,"obj_10":()=>({})},"Js_of_ocaml__IntersectionObserver.fragments":{"get_IntersectionObserver":a=>a.IntersectionObserver,"obj_11":()=>({})},"Js_of_ocaml__Intl.fragments":{"get_Collator":a=>a.Collator,"get_DateTimeFormat":a=>a.DateTimeFormat,"get_Intl":a=>a.Intl,"get_NumberFormat":a=>a.NumberFormat,"get_PluralRules":a=>a.PluralRules,"obj_12":a=>({localeMatcher:a}),"obj_13":(a,b,c,d,e,f)=>({localeMatcher:a,usage:b,sensitivity:c,ignorePunctuation:d,numeric:e,caseFirst:f}),"obj_14":(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t)=>({dateStyle:a,timeStyle:b,calendar:c,dayPeriod:d,numberingSystem:e,localeMatcher:f,timeZone:g,hour12:h,hourCycle:i,formatMatcher:j,weekday:k,era:l,year:m,month:n,day:o,hour:p,minute:q,second:r,fractionalSecondDigits:s,timeZoneName:t}),"obj_15":(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u)=>({compactDisplay:a,currency:b,currencyDisplay:c,currencySign:d,localeMatcher:e,notation:f,numberingSystem:g,signDisplay:h,style:i,unit:j,unitDisplay:k,useGrouping:l,roundingMode:m,roundingPriority:n,roundingIncrement:o,trailingZeroDisplay:p,minimumIntegerDigits:q,minimumFractionDigits:r,maximumFractionDigits:s,minimumSignificantDigits:t,maximumSignificantDigits:u}),"obj_16":(a,b)=>({localeMatcher:a,type:b})},"Dune__exe__Sx_browser.fragments":{"fun_call_1":(a,b)=>a(b),"fun_call_3":(a,b,c,d)=>a(b,c,d),"get_Array":a=>a.Array,"get_Object":a=>a.Object,"get___sx_handle":a=>a.__sx_handle,"get__driveAsync":a=>a._driveAsync,"get__type":a=>a._type,"get_console":a=>a.console,"get_items":a=>a.items,"get_length":a=>a.length,"get_name":a=>a.name,"js_expr_10d25c5c":()=>function(a){return function(){b.__sxR=undefined;var
c=a.apply(null,arguments);return b.__sxR!==undefined?b.__sxR:c}},"js_expr_1ab4fffb":()=>function(){var c=a.apply(null,arguments);return b.__sxR!==undefined?b.__sxR:c}},"js_expr_1ab4fffb":()=>function(){var
b={},d=0;return{put:function(a){var b={},d=0;return{put:function(a){var
c=d++;b[c]=a;return c},get:function(a){return b[a]}}}(),"js_expr_36506fc1":()=>function(a,b,c){a.__sx_handle=b;a._type=c;return a},"meth_call_1_error":(a,b)=>a.error(b),"meth_call_1_get":(a,b)=>a.get(b),"meth_call_1_isArray":(a,b)=>a.isArray(b),"meth_call_1_keys":(a,b)=>a.keys(b),"meth_call_1_put":(a,b)=>a.put(b),"obj_0":()=>({}),"obj_1":()=>({}),"obj_2":(a,b)=>({_type:a,items:b}),"obj_3":(a,b)=>({_type:a,name:b}),"obj_4":(a,b)=>({_type:a,name:b}),"obj_5":(a,b)=>({_type:a,__sx_handle:b}),"obj_6":()=>({}),"obj_7":()=>({}),"obj_8":()=>({}),"set_SxKernel":(a,b)=>a.SxKernel=b,"set___sxR":(a,b)=>a.__sxR=b,"set__type":(a,b)=>a._type=b,"set_beginModuleLoad":(a,b)=>a.beginModuleLoad=b,"set_callFn":(a,b)=>a.callFn=b,"set_compileModule":(a,b)=>a.compileModule=b,"set_debugEnv":(a,b)=>a.debugEnv=b,"set_endModuleLoad":(a,b)=>a.endModuleLoad=b,"set_engine":(a,b)=>a.engine=b,"set_eval":(a,b)=>a.eval=b,"set_evalExpr":(a,b)=>a.evalExpr=b,"set_evalVM":(a,b)=>a.evalVM=b,"set_fnArity":(a,b)=>a.fnArity=b,"set_inspect":(a,b)=>a.inspect=b,"set_isCallable":(a,b)=>a.isCallable=b,"set_load":(a,b)=>a.load=b,"set_loadModule":(a,b)=>a.loadModule=b,"set_loadSource":(a,b)=>a.loadSource=b,"set_op":(a,b)=>a.op=b,"set_parse":(a,b)=>a.parse=b,"set_registerNative":(a,b)=>a.registerNative=b,"set_renderToHtml":(a,b)=>a.renderToHtml=b,"set_request":(a,b)=>a.request=b,"set_resume":(a,b)=>a.resume=b,"set_scopeTraceDrain":(a,b)=>a.scopeTraceDrain=b,"set_scopeTraceOff":(a,b)=>a.scopeTraceOff=b,"set_scopeTraceOn":(a,b)=>a.scopeTraceOn=b,"set_stringify":(a,b)=>a.stringify=b,"set_suspended":(a,b)=>a.suspended=b,"set_typeOf":(a,b)=>a.typeOf=b}}})(globalThis),"src":"sx_browser.bc.wasm.assets"}); c=d++;b[c]=a;return c},get:function(a){return b[a]}}}(),"js_expr_36506fc1":()=>function(a,b,c){a.__sx_handle=b;a._type=c;return a},"meth_call_1_error":(a,b)=>a.error(b),"meth_call_1_get":(a,b)=>a.get(b),"meth_call_1_isArray":(a,b)=>a.isArray(b),"meth_call_1_keys":(a,b)=>a.keys(b),"meth_call_1_put":(a,b)=>a.put(b),"obj_0":()=>({}),"obj_1":()=>({}),"obj_10":(a,b)=>({__sx_error:a,message:b}),"obj_2":(a,b)=>({_type:a,items:b}),"obj_3":(a,b)=>({_type:a,name:b}),"obj_4":(a,b)=>({_type:a,name:b}),"obj_5":(a,b)=>({_type:a,__sx_handle:b}),"obj_6":()=>({}),"obj_7":()=>({}),"obj_8":()=>({}),"obj_9":(a,b)=>({__sx_error:a,message:b}),"set_SxKernel":(a,b)=>a.SxKernel=b,"set___sxR":(a,b)=>a.__sxR=b,"set__type":(a,b)=>a._type=b,"set_beginModuleLoad":(a,b)=>a.beginModuleLoad=b,"set_callFn":(a,b)=>a.callFn=b,"set_compileModule":(a,b)=>a.compileModule=b,"set_debugEnv":(a,b)=>a.debugEnv=b,"set_endModuleLoad":(a,b)=>a.endModuleLoad=b,"set_engine":(a,b)=>a.engine=b,"set_eval":(a,b)=>a.eval=b,"set_evalExpr":(a,b)=>a.evalExpr=b,"set_evalVM":(a,b)=>a.evalVM=b,"set_fnArity":(a,b)=>a.fnArity=b,"set_inspect":(a,b)=>a.inspect=b,"set_isCallable":(a,b)=>a.isCallable=b,"set_load":(a,b)=>a.load=b,"set_loadModule":(a,b)=>a.loadModule=b,"set_loadSource":(a,b)=>a.loadSource=b,"set_op":(a,b)=>a.op=b,"set_parse":(a,b)=>a.parse=b,"set_registerNative":(a,b)=>a.registerNative=b,"set_renderToHtml":(a,b)=>a.renderToHtml=b,"set_request":(a,b)=>a.request=b,"set_resetStepCount":(a,b)=>a.resetStepCount=b,"set_resume":(a,b)=>a.resume=b,"set_scopeTraceDrain":(a,b)=>a.scopeTraceDrain=b,"set_scopeTraceOff":(a,b)=>a.scopeTraceOff=b,"set_scopeTraceOn":(a,b)=>a.scopeTraceOn=b,"set_setStepLimit":(a,b)=>a.setStepLimit=b,"set_stringify":(a,b)=>a.stringify=b,"set_suspended":(a,b)=>a.suspended=b,"set_typeOf":(a,b)=>a.typeOf=b}}})(globalThis),"src":"sx_browser.bc.wasm.assets"});

View File

@@ -0,0 +1,176 @@
;; Test runner page for the htmx demo
;; URL: /sx/(test.(applications.(htmx)))
(defcomp
()
(~docs/page
:title "Test: htmx demos"
(p
(~tw :tokens "text-stone-500 mb-4")
"Running tests against "
(a
:href "/sx/(applications.(htmx))"
(~tw :tokens "text-violet-600 underline")
"/sx/(applications.(htmx))"))
(div
(~tw :tokens "flex items-center gap-4 mb-4")
(button
:id "run-btn"
(~tw
:tokens "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors")
"Run All Tests")
(span :id "test-status" (~tw :tokens "text-sm text-stone-500") "Ready"))
(div :id "test-summary" (~tw :tokens "mb-4"))
(div
:id "test-list"
(~tw :tokens "space-y-2 mb-6")
(details
:class "sx-test-item"
:data-test "click-to-load"
(~tw :tokens "border border-stone-200 rounded-lg overflow-hidden")
(summary
(~tw
:tokens "px-4 py-2 cursor-pointer hover:bg-stone-50 flex items-center gap-2")
(span :data-role "test-icon" (~tw :tokens "font-bold") "○")
(span (~tw :tokens "font-medium") "click-to-load")
(span
(~tw :tokens "text-sm text-stone-400 ml-auto")
"Click button, verify content loads"))
(div
(~tw :tokens "px-4 py-3 bg-stone-50 border-t border-stone-200")
(pre
(~tw
:tokens "text-xs font-mono whitespace-pre-wrap text-stone-600")
"(deftest click-to-load\n :runner :playwright\n :url \"/sx/(applications.(htmx))\"\n (click \"button[hx-get]\")\n (wait 2000)\n (assert-text \"#click-result\" :contains \"Content loaded!\"))")))
(details
:class "sx-test-item"
:data-test "click-no-oob-leak"
(~tw :tokens "border border-stone-200 rounded-lg overflow-hidden")
(summary
(~tw
:tokens "px-4 py-2 cursor-pointer hover:bg-stone-50 flex items-center gap-2")
(span :data-role "test-icon" (~tw :tokens "font-bold") "○")
(span (~tw :tokens "font-medium") "click-no-oob-leak")
(span
(~tw :tokens "text-sm text-stone-400 ml-auto")
"OOB swap sections filtered from content"))
(div
(~tw :tokens "px-4 py-3 bg-stone-50 border-t border-stone-200")
(pre
(~tw
:tokens "text-xs font-mono whitespace-pre-wrap text-stone-600")
"(deftest click-no-oob-leak\n :runner :playwright\n :url \"/sx/(applications.(htmx))\"\n (click \"button[hx-get]\")\n (wait 2000)\n (assert-text \"#click-result\" :not-contains \"defcomp\"))")))
(details
:class "sx-test-item"
:data-test "search-debounce"
(~tw :tokens "border border-stone-200 rounded-lg overflow-hidden")
(summary
(~tw
:tokens "px-4 py-2 cursor-pointer hover:bg-stone-50 flex items-center gap-2")
(span :data-role "test-icon" (~tw :tokens "font-bold") "○")
(span (~tw :tokens "font-medium") "search-debounce")
(span
(~tw :tokens "text-sm text-stone-400 ml-auto")
"Type in search, results appear after debounce"))
(div
(~tw :tokens "px-4 py-3 bg-stone-50 border-t border-stone-200")
(pre
(~tw
:tokens "text-xs font-mono whitespace-pre-wrap text-stone-600")
"(deftest search-debounce\n :runner :playwright\n :url \"/sx/(applications.(htmx))\"\n (fill \"input[hx-get]\" \"hx-get\")\n (wait 1500)\n (assert-text \"#search-results\" :contains \"GET request\"))")))
(details
:class "sx-test-item"
:data-test "search-no-results"
(~tw :tokens "border border-stone-200 rounded-lg overflow-hidden")
(summary
(~tw
:tokens "px-4 py-2 cursor-pointer hover:bg-stone-50 flex items-center gap-2")
(span :data-role "test-icon" (~tw :tokens "font-bold") "○")
(span (~tw :tokens "font-medium") "search-no-results")
(span
(~tw :tokens "text-sm text-stone-400 ml-auto")
"Non-matching query shows no results"))
(div
(~tw :tokens "px-4 py-3 bg-stone-50 border-t border-stone-200")
(pre
(~tw
:tokens "text-xs font-mono whitespace-pre-wrap text-stone-600")
"(deftest search-no-results\n :runner :playwright\n :url \"/sx/(applications.(htmx))\"\n (fill \"input[hx-get]\" \"xyznonexistent\")\n (wait 1500)\n (assert-text \"#search-results\" :contains \"No results\"))")))
(details
:class "sx-test-item"
:data-test "tab-overview"
(~tw :tokens "border border-stone-200 rounded-lg overflow-hidden")
(summary
(~tw
:tokens "px-4 py-2 cursor-pointer hover:bg-stone-50 flex items-center gap-2")
(span :data-role "test-icon" (~tw :tokens "font-bold") "○")
(span (~tw :tokens "font-medium") "tab-overview")
(span
(~tw :tokens "text-sm text-stone-400 ml-auto")
"Click overview tab, verify content"))
(div
(~tw :tokens "px-4 py-3 bg-stone-50 border-t border-stone-200")
(pre
(~tw
:tokens "text-xs font-mono whitespace-pre-wrap text-stone-600")
"(deftest tab-overview\n :runner :playwright\n :url \"/sx/(applications.(htmx))\"\n (click \"button[hx-get*='tab=overview']\")\n (wait 2000)\n (assert-text \"#htmx-tab-content\" :contains \"htmx gives you access\"))")))
(details
:class "sx-test-item"
:data-test "tab-features"
(~tw :tokens "border border-stone-200 rounded-lg overflow-hidden")
(summary
(~tw
:tokens "px-4 py-2 cursor-pointer hover:bg-stone-50 flex items-center gap-2")
(span :data-role "test-icon" (~tw :tokens "font-bold") "○")
(span (~tw :tokens "font-medium") "tab-features")
(span
(~tw :tokens "text-sm text-stone-400 ml-auto")
"Click features tab, verify list"))
(div
(~tw :tokens "px-4 py-3 bg-stone-50 border-t border-stone-200")
(pre
(~tw
:tokens "text-xs font-mono whitespace-pre-wrap text-stone-600")
"(deftest tab-features\n :runner :playwright\n :url \"/sx/(applications.(htmx))\"\n (click \"button[hx-get*='tab=features']\")\n (wait 2000)\n (assert-text \"#htmx-tab-content\" :contains \"Any element\"))")))
(details
:class "sx-test-item"
:data-test "append-item"
(~tw :tokens "border border-stone-200 rounded-lg overflow-hidden")
(summary
(~tw
:tokens "px-4 py-2 cursor-pointer hover:bg-stone-50 flex items-center gap-2")
(span :data-role "test-icon" (~tw :tokens "font-bold") "○")
(span (~tw :tokens "font-medium") "append-item")
(span
(~tw :tokens "text-sm text-stone-400 ml-auto")
"POST appends new item to list"))
(div
(~tw :tokens "px-4 py-3 bg-stone-50 border-t border-stone-200")
(pre
(~tw
:tokens "text-xs font-mono whitespace-pre-wrap text-stone-600")
"(deftest append-item\n :runner :playwright\n :url \"/sx/(applications.(htmx))\"\n (click \"button[hx-post*='api.append']\")\n (wait 2000)\n (assert-count \"#item-list > *\" :gte 1))")))
(details
:class "sx-test-item"
:data-test "form-submit"
(~tw :tokens "border border-stone-200 rounded-lg overflow-hidden")
(summary
(~tw
:tokens "px-4 py-2 cursor-pointer hover:bg-stone-50 flex items-center gap-2")
(span :data-role "test-icon" (~tw :tokens "font-bold") "○")
(span (~tw :tokens "font-medium") "form-submit")
(span
(~tw :tokens "text-sm text-stone-400 ml-auto")
"POST form data, verify name in response"))
(div
(~tw :tokens "px-4 py-3 bg-stone-50 border-t border-stone-200")
(pre
(~tw
:tokens "text-xs font-mono whitespace-pre-wrap text-stone-600")
"(deftest form-submit\n :runner :playwright\n :url \"/sx/(applications.(htmx))\"\n (fill \"form[hx-post] input[name='name']\" \"Alice\")\n (fill \"form[hx-post] input[name='email']\" \"alice@test.com\")\n (click \"form[hx-post] button[type='submit']\")\n (wait 2000)\n (assert-text \"#form-result\" :contains \"Alice\"))"))))
(iframe
:id "test-iframe"
:src "/sx/(applications.(htmx))"
(~tw :tokens "w-full border border-stone-200 rounded-lg")
:style "height:600px")
(script :src "/static/scripts/sx-test-runner.js?v=7")))

View File

@@ -53,4 +53,11 @@
(span (span
(~tw :tokens "text-stone-300 text-xs") (~tw :tokens "text-stone-300 text-xs")
:style "margin-left:0.5em;" :style "margin-left:0.5em;"
(a
:href (str "/sx/(test." (slice path 4) ")")
:data-sx-no-boost "true"
:target "_self"
(~tw :tokens "text-violet-400 text-xs hover:text-violet-600")
:style "text-decoration:none;margin-right:0.4em;"
"test")
(str "· " path)))))))) (str "· " path))))))))

View File

@@ -18,11 +18,7 @@
(list (make-symbol default-name)) (list (make-symbol default-name))
(list (slug->component slug prefix infix suffix)))))) (list (slug->component slug prefix infix suffix))))))
(define (define home (fn (content) (if (nil? content) (quote (~home)) content)))
home
(fn
(content)
(if (nil? content) (quote (~docs-content/home-content)) content)))
(define language (fn (content) (if (nil? content) (quote (<>)) content))) (define language (fn (content) (if (nil? content) (quote (<>)) content)))
@@ -42,18 +38,13 @@
(define (define
reactive reactive
(fn (fn (content) (if (nil? content) (quote (~geography/reactive)) content)))
(content)
(if
(nil? content)
(quote (~reactive-islands/index/reactive-islands-index-content))
content)))
(define (define
examples examples
(make-page-fn (make-page-fn
"~reactive-islands/demo/reactive-islands-demo-content" "~geography/reactive/examples"
"~reactive-islands/demo/example-" "~geography/reactive/examples/"
nil nil
"")) ""))
@@ -63,34 +54,28 @@
(slug) (slug)
(if (if
(nil? slug) (nil? slug)
(quote (~geography/cek/cek-content)) (quote (~geography/cek))
(case (case
slug slug
"demo" "demo"
(quote (~geography/cek/cek-demo-content)) (quote (~geography/cek/demo))
"freeze" "freeze"
(quote (~geography/cek/cek-freeze-content)) (quote (~geography/cek/freeze))
"content" "content"
(quote (~geography/cek/cek-content-address-content)) (quote (~geography/cek/content))
:else (quote (~geography/cek/cek-content)))))) :else (quote (~geography/cek))))))
(define (define
provide provide
(fn (fn (content) (if (nil? content) (quote (~geography/provide)) content)))
(content)
(if (nil? content) (quote (~geography/provide-content)) content)))
(define (define
scopes scopes
(fn (fn (content) (if (nil? content) (quote (~geography/scopes)) content)))
(content)
(if (nil? content) (quote (~geography/scopes-content)) content)))
(define (define
spreads spreads
(fn (fn (content) (if (nil? content) (quote (~geography/spreads)) content)))
(content)
(if (nil? content) (quote (~geography/spreads-content)) content)))
(define (define
marshes marshes
@@ -98,20 +83,20 @@
(slug) (slug)
(if (if
(nil? slug) (nil? slug)
(quote (~reactive-islands/marshes/reactive-islands-marshes-content)) (quote (~geography/marshes))
(case (case
slug slug
"hypermedia-feeds" "hypermedia-feeds"
(quote (~reactive-islands/marshes/example-hypermedia-feeds)) (quote (~geography/marshes/hypermedia-feeds))
"server-signals" "server-signals"
(quote (~reactive-islands/marshes/example-server-signals)) (quote (~geography/marshes/server-signals))
"on-settle" "on-settle"
(quote (~reactive-islands/marshes/example-on-settle)) (quote (~geography/marshes/on-settle))
"signal-triggers" "signal-triggers"
(quote (~reactive-islands/marshes/example-signal-triggers)) (quote (~geography/marshes/signal-triggers))
"view-transform" "view-transform"
(quote (~reactive-islands/marshes/example-view-transform)) (quote (~geography/marshes/view-transform))
:else (quote (~reactive-islands/marshes/reactive-islands-marshes-content)))))) :else (quote (~geography/marshes))))))
(define (define
isomorphism isomorphism
@@ -119,14 +104,14 @@
(slug) (slug)
(if (if
(nil? slug) (nil? slug)
(quote (~plans/isomorphic/plan-isomorphic-content)) (quote (~geography/isomorphism))
(case (case
slug slug
"bundle-analyzer" "bundle-analyzer"
(let (let
((data (helper "bundle-analyzer-data"))) ((data (helper "bundle-analyzer-data")))
(quasiquote (quasiquote
(~analyzer/bundle-analyzer-content (~geography/isomorphism/bundle-analyzer
:pages (unquote (get data "pages")) :pages (unquote (get data "pages"))
:total-components (unquote (get data "total-components")) :total-components (unquote (get data "total-components"))
:total-macros (unquote (get data "total-macros")) :total-macros (unquote (get data "total-macros"))
@@ -136,7 +121,7 @@
(let (let
((data (helper "routing-analyzer-data"))) ((data (helper "routing-analyzer-data")))
(quasiquote (quasiquote
(~routing-analyzer/content (~geography/isomorphism/routing-analyzer
:pages (unquote (get data "pages")) :pages (unquote (get data "pages"))
:total-pages (unquote (get data "total-pages")) :total-pages (unquote (get data "total-pages"))
:client-count (unquote (get data "client-count")) :client-count (unquote (get data "client-count"))
@@ -146,35 +131,35 @@
(let (let
((data (helper "data-test-data"))) ((data (helper "data-test-data")))
(quasiquote (quasiquote
(~data-test/content (~geography/isomorphism/data-test
:server-time (unquote (get data "server-time")) :server-time (unquote (get data "server-time"))
:items (unquote (get data "items")) :items (unquote (get data "items"))
:phase (unquote (get data "phase")) :phase (unquote (get data "phase"))
:transport (unquote (get data "transport"))))) :transport (unquote (get data "transport")))))
"async-io" "async-io"
(quote (~async-io-demo/content)) (quote (~geography/isomorphism/async-io))
"affinity" "affinity"
(let (let
((data (helper "affinity-demo-data"))) ((data (helper "affinity-demo-data")))
(quasiquote (quasiquote
(~affinity-demo/content (~geography/isomorphism/affinity
:components (unquote (get data "components")) :components (unquote (get data "components"))
:page-plans (unquote (get data "page-plans"))))) :page-plans (unquote (get data "page-plans")))))
"optimistic" "optimistic"
(let (let
((data (helper "optimistic-demo-data"))) ((data (helper "optimistic-demo-data")))
(quasiquote (quasiquote
(~optimistic-demo/content (~geography/isomorphism/optimistic
:items (unquote (get data "items")) :items (unquote (get data "items"))
:server-time (unquote (get data "server-time"))))) :server-time (unquote (get data "server-time")))))
"offline" "offline"
(let (let
((data (helper "offline-demo-data"))) ((data (helper "offline-demo-data")))
(quasiquote (quasiquote
(~offline-demo/content (~geography/isomorphism/offline
:notes (unquote (get data "notes")) :notes (unquote (get data "notes"))
:server-time (unquote (get data "server-time"))))) :server-time (unquote (get data "server-time")))))
:else (quote (~plans/isomorphic/plan-isomorphic-content)))))) :else (quote (~geography/isomorphism))))))
(define (define
doc doc
@@ -182,32 +167,34 @@
(slug) (slug)
(if (if
(nil? slug) (nil? slug)
(quote (~docs-content/docs-introduction-content)) (quote (~language/doc/introduction))
(case (case
slug slug
"introduction" "introduction"
(quote (~docs-content/docs-introduction-content)) (quote (~language/doc/introduction))
"getting-started" "getting-started"
(quote (~docs-content/docs-getting-started-content)) (quote (~language/doc/getting-started))
"components" "components"
(quote (~docs-content/docs-components-content)) (quote (~language/doc/components))
"evaluator" "evaluator"
(quote (~docs-content/docs-evaluator-content)) (quote (~language/doc/evaluator))
"primitives" "primitives"
(let (let
((data (helper "primitives-data"))) ((data (helper "primitives-data")))
(quasiquote (quasiquote
(~docs-content/docs-primitives-content (~language/doc/primitives
:prims (~docs/primitives-tables :primitives (unquote data))))) :prims (~language/doc/_shared/primitives-tables
:primitives (unquote data)))))
"special-forms" "special-forms"
(let (let
((data (helper "special-forms-data"))) ((data (helper "special-forms-data")))
(quasiquote (quasiquote
(~docs-content/docs-special-forms-content (~language/doc/special-forms
:forms (~docs/special-forms-tables :forms (unquote data))))) :forms (~language/doc/_shared/special-forms-tables
:forms (unquote data)))))
"server-rendering" "server-rendering"
(quote (~docs-content/docs-server-rendering-content)) (quote (~language/doc/server-rendering))
:else (quote (~docs-content/docs-introduction-content)))))) :else (quote (~language/doc/introduction))))))
(define (define
spec spec
@@ -215,7 +202,7 @@
(slug) (slug)
(cond (cond
(nil? slug) (nil? slug)
(quote (~specs/architecture-content)) (quote (~language/spec))
(not (= (type-of slug) "string")) (not (= (type-of slug) "string"))
slug slug
:else (case :else (case
@@ -224,42 +211,42 @@
(let (let
((files (make-spec-files core-spec-items))) ((files (make-spec-files core-spec-items)))
(quasiquote (quasiquote
(~specs/overview-content (~language/spec/_shared/overview-content
:spec-title "Core Language" :spec-title "Core Language"
:spec-files (unquote files)))) :spec-files (unquote files))))
"adapters" "adapters"
(let (let
((files (make-spec-files adapter-spec-items))) ((files (make-spec-files adapter-spec-items)))
(quasiquote (quasiquote
(~specs/overview-content (~language/spec/_shared/overview-content
:spec-title "Adapters" :spec-title "Adapters"
:spec-files (unquote files)))) :spec-files (unquote files))))
"browser" "browser"
(let (let
((files (make-spec-files browser-spec-items))) ((files (make-spec-files browser-spec-items)))
(quasiquote (quasiquote
(~specs/overview-content (~language/spec/_shared/overview-content
:spec-title "Browser Runtime" :spec-title "Browser Runtime"
:spec-files (unquote files)))) :spec-files (unquote files))))
"reactive" "reactive"
(let (let
((files (make-spec-files reactive-spec-items))) ((files (make-spec-files reactive-spec-items)))
(quasiquote (quasiquote
(~specs/overview-content (~language/spec/_shared/overview-content
:spec-title "Reactive System" :spec-title "Reactive System"
:spec-files (unquote files)))) :spec-files (unquote files))))
"host" "host"
(let (let
((files (make-spec-files host-spec-items))) ((files (make-spec-files host-spec-items)))
(quasiquote (quasiquote
(~specs/overview-content (~language/spec/_shared/overview-content
:spec-title "Host Interface" :spec-title "Host Interface"
:spec-files (unquote files)))) :spec-files (unquote files))))
"extensions" "extensions"
(let (let
((files (make-spec-files extension-spec-items))) ((files (make-spec-files extension-spec-items)))
(quasiquote (quasiquote
(~specs/overview-content (~language/spec/_shared/overview-content
:spec-title "Extensions" :spec-title "Extensions"
:spec-files (unquote files)))) :spec-files (unquote files))))
:else (let :else (let
@@ -269,13 +256,14 @@
(let (let
((src (helper "read-spec-file" (get found-spec "filename")))) ((src (helper "read-spec-file" (get found-spec "filename"))))
(quasiquote (quasiquote
(~specs/detail-content (~language/spec/_shared/detail-content
:spec-title (unquote (get found-spec "title")) :spec-title (unquote (get found-spec "title"))
:spec-desc (unquote (get found-spec "desc")) :spec-desc (unquote (get found-spec "desc"))
:spec-filename (unquote (get found-spec "filename")) :spec-filename (unquote (get found-spec "filename"))
:spec-source (unquote src) :spec-source (unquote src)
:spec-prose (unquote (get found-spec "prose"))))) :spec-prose (unquote (get found-spec "prose")))))
(quasiquote (~specs/not-found :slug (unquote slug))))))))) (quasiquote
(~language/spec/_shared/not-found :slug (unquote slug)))))))))
(define (define
explore explore
@@ -332,17 +320,18 @@
(slug) (slug)
(if (if
(nil? slug) (nil? slug)
(quote (~specs/bootstrappers-index-content)) (quote (~language/bootstrapper))
(let (let
((data (helper "bootstrapper-data" slug))) ((data (helper "bootstrapper-data" slug)))
(if (if
(get data "bootstrapper-not-found") (get data "bootstrapper-not-found")
(quasiquote (~specs/not-found :slug (unquote slug))) (quasiquote
(~language/bootstrapper/_shared/not-found :slug (unquote slug)))
(case (case
slug slug
"self-hosting" "self-hosting"
(quasiquote (quasiquote
(~specs/bootstrapper-self-hosting-content (~language/bootstrapper/self-hosting
:py-sx-source (unquote (get data "py-sx-source")) :py-sx-source (unquote (get data "py-sx-source"))
:g0-output (unquote (get data "g0-output")) :g0-output (unquote (get data "g0-output"))
:g1-output (unquote (get data "g1-output")) :g1-output (unquote (get data "g1-output"))
@@ -353,7 +342,7 @@
:verification-status (unquote (get data "verification-status")))) :verification-status (unquote (get data "verification-status"))))
"self-hosting-js" "self-hosting-js"
(quasiquote (quasiquote
(~specs/bootstrapper-self-hosting-js-content (~language/bootstrapper/self-hosting-js
:js-sx-source (unquote (get data "js-sx-source")) :js-sx-source (unquote (get data "js-sx-source"))
:defines-matched (unquote (get data "defines-matched")) :defines-matched (unquote (get data "defines-matched"))
:defines-total (unquote (get data "defines-total")) :defines-total (unquote (get data "defines-total"))
@@ -361,14 +350,14 @@
:verification-status (unquote (get data "verification-status")))) :verification-status (unquote (get data "verification-status"))))
"python" "python"
(quasiquote (quasiquote
(~specs/bootstrapper-py-content (~language/bootstrapper/python
:bootstrapper-source (unquote (get data "bootstrapper-source")) :bootstrapper-source (unquote (get data "bootstrapper-source"))
:bootstrapped-output (unquote (get data "bootstrapped-output")))) :bootstrapped-output (unquote (get data "bootstrapped-output"))))
"page-helpers" "page-helpers"
(let (let
((ph-data (helper "page-helpers-demo-data"))) ((ph-data (helper "page-helpers-demo-data")))
(quasiquote (quasiquote
(~page-helpers-demo/content (~language/bootstrapper/page-helpers
:sf-categories (unquote (get ph-data "sf-categories")) :sf-categories (unquote (get ph-data "sf-categories"))
:sf-total (unquote (get ph-data "sf-total")) :sf-total (unquote (get ph-data "sf-total"))
:sf-ms (unquote (get ph-data "sf-ms")) :sf-ms (unquote (get ph-data "sf-ms"))
@@ -386,7 +375,7 @@
:req-attrs (unquote (get ph-data "req-attrs")) :req-attrs (unquote (get ph-data "req-attrs"))
:attr-keys (unquote (get ph-data "attr-keys"))))) :attr-keys (unquote (get ph-data "attr-keys")))))
:else (quasiquote :else (quasiquote
(~specs/bootstrapper-js-content (~language/bootstrapper/javascript
:bootstrapper-source (unquote (get data "bootstrapper-source")) :bootstrapper-source (unquote (get data "bootstrapper-source"))
:bootstrapped-output (unquote (get data "bootstrapped-output")))))))))) :bootstrapped-output (unquote (get data "bootstrapped-output"))))))))))
@@ -397,9 +386,9 @@
(if (if
(nil? slug) (nil? slug)
(let (let
((data (helper "run-modular-tests" "all"))) ((data (perform (list (quote io-test-data)))))
(quasiquote (quasiquote
(~testing/overview-content (~language/test
:server-results (unquote (get data "server-results")) :server-results (unquote (get data "server-results"))
:framework-source (unquote (get data "framework-source")) :framework-source (unquote (get data "framework-source"))
:eval-source (unquote (get data "eval-source")) :eval-source (unquote (get data "eval-source"))
@@ -408,80 +397,85 @@
:render-source (unquote (get data "render-source")) :render-source (unquote (get data "render-source"))
:deps-source (unquote (get data "deps-source")) :deps-source (unquote (get data "deps-source"))
:engine-source (unquote (get data "engine-source"))))) :engine-source (unquote (get data "engine-source")))))
(case (if
slug (not (string? slug))
"runners" (quote (~applications/htmx/runner))
(quote (~testing/runners-content)) (case
:else (let slug
((data (helper "run-modular-tests" slug))) "runners"
(case (quote (~language/test/runners))
slug "applications"
"eval" (quote (~applications/htmx/runner))
(quasiquote :else (let
(~testing/spec-content ((data (perform (list (quote io-test-data) slug))))
:spec-name "eval" (case
:spec-title "Evaluator Tests" slug
:spec-desc "81 tests covering the core evaluator and all primitives." "eval"
:spec-source (unquote (get data "spec-source")) (quasiquote
:framework-source (unquote (get data "framework-source")) (~language/test/_shared/spec-content
:server-results (unquote (get data "server-results")))) :spec-name "eval"
"parser" :spec-title "Evaluator Tests"
(quasiquote :spec-desc "81 tests covering the core evaluator."
(~testing/spec-content :spec-source (unquote (get data "spec-source"))
:spec-name "parser" :framework-source (unquote (get data "framework-source"))
:spec-title "Parser Tests" :server-results (unquote (get data "server-results"))))
:spec-desc "39 tests covering tokenization and parsing." "parser"
:spec-source (unquote (get data "spec-source")) (quasiquote
:framework-source (unquote (get data "framework-source")) (~language/test/_shared/spec-content
:server-results (unquote (get data "server-results")))) :spec-name "parser"
"router" :spec-title "Parser Tests"
(quasiquote :spec-desc "39 tests covering tokenization and parsing."
(~testing/spec-content :spec-source (unquote (get data "spec-source"))
:spec-name "router" :framework-source (unquote (get data "framework-source"))
:spec-title "Router Tests" :server-results (unquote (get data "server-results"))))
:spec-desc "18 tests covering client-side route matching." "router"
:spec-source (unquote (get data "spec-source")) (quasiquote
:framework-source (unquote (get data "framework-source")) (~language/test/_shared/spec-content
:server-results (unquote (get data "server-results")))) :spec-name "router"
"render" :spec-title "Router Tests"
(quasiquote :spec-desc "18 tests covering client-side route matching."
(~testing/spec-content :spec-source (unquote (get data "spec-source"))
:spec-name "render" :framework-source (unquote (get data "framework-source"))
:spec-title "Renderer Tests" :server-results (unquote (get data "server-results"))))
:spec-desc "23 tests covering HTML rendering." "render"
:spec-source (unquote (get data "spec-source")) (quasiquote
:framework-source (unquote (get data "framework-source")) (~language/test/_shared/spec-content
:server-results (unquote (get data "server-results")))) :spec-name "render"
"deps" :spec-title "Renderer Tests"
(quasiquote :spec-desc "23 tests covering HTML rendering."
(~testing/spec-content :spec-source (unquote (get data "spec-source"))
:spec-name "deps" :framework-source (unquote (get data "framework-source"))
:spec-title "Dependency Analysis Tests" :server-results (unquote (get data "server-results"))))
:spec-desc "33 tests covering component dependency analysis." "deps"
:spec-source (unquote (get data "spec-source")) (quasiquote
:framework-source (unquote (get data "framework-source")) (~language/test/_shared/spec-content
:server-results (unquote (get data "server-results")))) :spec-name "deps"
"engine" :spec-title "Dependency Analysis Tests"
(quasiquote :spec-desc "33 tests covering component dependency analysis."
(~testing/spec-content :spec-source (unquote (get data "spec-source"))
:spec-name "engine" :framework-source (unquote (get data "framework-source"))
:spec-title "Engine Tests" :server-results (unquote (get data "server-results"))))
:spec-desc "37 tests covering engine pure functions." "engine"
:spec-source (unquote (get data "spec-source")) (quasiquote
:framework-source (unquote (get data "framework-source")) (~language/test/_shared/spec-content
:server-results (unquote (get data "server-results")))) :spec-name "engine"
"orchestration" :spec-title "Engine Tests"
(quasiquote :spec-desc "37 tests covering engine pure functions."
(~testing/spec-content :spec-source (unquote (get data "spec-source"))
:spec-name "orchestration" :framework-source (unquote (get data "framework-source"))
:spec-title "Orchestration Tests" :server-results (unquote (get data "server-results"))))
:spec-desc "17 tests covering orchestration." "orchestration"
:spec-source (unquote (get data "spec-source")) (quasiquote
:framework-source (unquote (get data "framework-source")) (~language/test/_shared/spec-content
:server-results (unquote (get data "server-results")))) :spec-name "orchestration"
:else (quasiquote :spec-title "Orchestration Tests"
(~testing/overview-content :spec-desc "17 tests covering orchestration."
:server-results (unquote (get data "server-results")))))))))) :spec-source (unquote (get data "spec-source"))
:framework-source (unquote (get data "framework-source"))
:server-results (unquote (get data "server-results"))))
:else (quasiquote
(~language/test
:server-results (unquote (get data "server-results")))))))))))
(define (define
reference reference
@@ -489,57 +483,57 @@
(slug) (slug)
(if (if
(nil? slug) (nil? slug)
(quote (~examples/reference-index-content)) (quote (~geography/hypermedia/reference))
(let (let
((data (helper "reference-data" slug))) ((data (helper "reference-data" slug)))
(case (case
slug slug
"attributes" "attributes"
(quasiquote (quasiquote
(~reference/attrs-content (~geography/hypermedia/reference/attributes
:req-table (~docs/attr-table-from-data :req-table (~geography/hypermedia/reference/_shared/attr-table-from-data
:title "Request Attributes" :title "Request Attributes"
:attrs (unquote (get data "req-attrs"))) :attrs (unquote (get data "req-attrs")))
:beh-table (~docs/attr-table-from-data :beh-table (~geography/hypermedia/reference/_shared/attr-table-from-data
:title "Behavior Attributes" :title "Behavior Attributes"
:attrs (unquote (get data "beh-attrs"))) :attrs (unquote (get data "beh-attrs")))
:uniq-table (~docs/attr-table-from-data :uniq-table (~geography/hypermedia/reference/_shared/attr-table-from-data
:title "Unique to sx" :title "Unique to sx"
:attrs (unquote (get data "uniq-attrs"))))) :attrs (unquote (get data "uniq-attrs")))))
"headers" "headers"
(quasiquote (quasiquote
(~reference/headers-content (~geography/hypermedia/reference/headers
:req-table (~docs/headers-table-from-data :req-table (~geography/hypermedia/reference/_shared/headers-table-from-data
:title "Request Headers" :title "Request Headers"
:headers (unquote (get data "req-headers"))) :headers (unquote (get data "req-headers")))
:resp-table (~docs/headers-table-from-data :resp-table (~geography/hypermedia/reference/_shared/headers-table-from-data
:title "Response Headers" :title "Response Headers"
:headers (unquote (get data "resp-headers"))))) :headers (unquote (get data "resp-headers")))))
"events" "events"
(quasiquote (quasiquote
(~reference/events-content (~geography/hypermedia/reference/events
:table (~docs/two-col-table-from-data :table (~geography/hypermedia/reference/_shared/two-col-table-from-data
:intro "sx fires custom DOM events at various points in the request lifecycle." :intro "sx fires custom DOM events at various points in the request lifecycle."
:col1 "Event" :col1 "Event"
:col2 "Description" :col2 "Description"
:items (unquote (get data "events-list"))))) :items (unquote (get data "events-list")))))
"js-api" "js-api"
(quasiquote (quasiquote
(~reference/js-api-content (~geography/hypermedia/reference/js-api
:table (~docs/two-col-table-from-data :table (~geography/hypermedia/reference/_shared/two-col-table-from-data
:intro "The client-side sx.js library exposes a public API for programmatic use." :intro "The client-side sx.js library exposes a public API for programmatic use."
:col1 "Method" :col1 "Method"
:col2 "Description" :col2 "Description"
:items (unquote (get data "js-api-list"))))) :items (unquote (get data "js-api-list")))))
:else (quasiquote :else (quasiquote
(~reference/attrs-content (~geography/hypermedia/reference/attributes
:req-table (~docs/attr-table-from-data :req-table (~geography/hypermedia/reference/_shared/attr-table-from-data
:title "Request Attributes" :title "Request Attributes"
:attrs (unquote (get data "req-attrs"))) :attrs (unquote (get data "req-attrs")))
:beh-table (~docs/attr-table-from-data :beh-table (~geography/hypermedia/reference/_shared/attr-table-from-data
:title "Behavior Attributes" :title "Behavior Attributes"
:attrs (unquote (get data "beh-attrs"))) :attrs (unquote (get data "beh-attrs")))
:uniq-table (~docs/attr-table-from-data :uniq-table (~geography/hypermedia/reference/_shared/attr-table-from-data
:title "Unique to sx" :title "Unique to sx"
:attrs (unquote (get data "uniq-attrs")))))))))) :attrs (unquote (get data "uniq-attrs"))))))))))
@@ -557,9 +551,11 @@
((data (helper "attr-detail-data" slug))) ((data (helper "attr-detail-data" slug)))
(if (if
(get data "attr-not-found") (get data "attr-not-found")
(quasiquote (~reference/attr-not-found :slug (unquote slug)))
(quasiquote (quasiquote
(~reference/attr-detail-content (~geography/hypermedia/reference/_shared/attr-not-found
:slug (unquote slug)))
(quasiquote
(~geography/hypermedia/reference/_shared/attr-detail-content
:title (unquote (get data "attr-title")) :title (unquote (get data "attr-title"))
:description (unquote (get data "attr-description")) :description (unquote (get data "attr-description"))
:demo (unquote (get data "attr-demo")) :demo (unquote (get data "attr-demo"))
@@ -571,9 +567,11 @@
((data (helper "header-detail-data" slug))) ((data (helper "header-detail-data" slug)))
(if (if
(get data "header-not-found") (get data "header-not-found")
(quasiquote (~reference/attr-not-found :slug (unquote slug)))
(quasiquote (quasiquote
(~reference/header-detail-content (~geography/hypermedia/reference/_shared/attr-not-found
:slug (unquote slug)))
(quasiquote
(~geography/hypermedia/reference/_shared/header-detail-content
:title (unquote (get data "header-title")) :title (unquote (get data "header-title"))
:direction (unquote (get data "header-direction")) :direction (unquote (get data "header-direction"))
:description (unquote (get data "header-description")) :description (unquote (get data "header-description"))
@@ -584,9 +582,11 @@
((data (helper "event-detail-data" slug))) ((data (helper "event-detail-data" slug)))
(if (if
(get data "event-not-found") (get data "event-not-found")
(quasiquote (~reference/attr-not-found :slug (unquote slug)))
(quasiquote (quasiquote
(~reference/event-detail-content (~geography/hypermedia/reference/_shared/attr-not-found
:slug (unquote slug)))
(quasiquote
(~geography/hypermedia/reference/_shared/event-detail-content
:title (unquote (get data "event-title")) :title (unquote (get data "event-title"))
:description (unquote (get data "event-description")) :description (unquote (get data "event-description"))
:example-code (unquote (get data "event-example")) :example-code (unquote (get data "event-example"))
@@ -600,26 +600,26 @@
(if (if
(nil? slug) (nil? slug)
"" ""
(list (slug->component slug "~examples-content/example-" nil ""))))) (list (slug->component slug "~geography/hypermedia/example/" nil "")))))
(define sx-urls (fn (slug) (quote (~sx-urls/urls-content)))) (define sx-urls (fn (slug) (quote (~applications/sx-urls))))
(define cssx (make-page-fn "~cssx/overview-content" "~cssx/" nil "-content")) (define cssx (make-page-fn "~applications/cssx" "~applications/cssx/" nil ""))
(define (define
protocol protocol
(make-page-fn "~protocols/wire-format-content" "~protocols/" nil "-content")) (make-page-fn "~applications/protocol" "~applications/protocol/" nil ""))
(define (define
sx-pub sx-pub
(fn (slug) (if (nil? slug) (quote (~sx-pub/overview-content)) nil))) (fn (slug) (if (nil? slug) (quote (~applications/sx-pub)) nil)))
(define (define
sx-tools sx-tools
(fn (fn
(&key title &rest args) (&key title &rest args)
(quasiquote (quasiquote
(~sx-tools/overview-content (~tools/sx-tools
:title (unquote (or title "SX Tools")) :title (unquote (or title "SX Tools"))
(splice-unquote args))))) (splice-unquote args)))))
@@ -630,7 +630,7 @@
(fn (fn
(&key title &rest args) (&key title &rest args)
(quasiquote (quasiquote
(~playground/content (~tools/playground
:title (unquote (or title "Playground")) :title (unquote (or title "Playground"))
(splice-unquote args))))) (splice-unquote args)))))
@@ -639,29 +639,27 @@
(fn (fn
(&key title &rest args) (&key title &rest args)
(quasiquote (quasiquote
(~services-tools/overview-content (~tools/services
:title (unquote (or title "Services")) :title (unquote (or title "Services"))
(splice-unquote args))))) (splice-unquote args)))))
(define (define
reactive-runtime reactive-runtime
(make-page-fn (make-page-fn
"~reactive-runtime/overview-content" "~geography/reactive-runtime"
"~reactive-runtime/" "~geography/reactive-runtime/"
nil nil
"-content")) ""))
(define (define
native-browser native-browser
(make-page-fn (make-page-fn
"~applications/native-browser/content" "~applications/native-browser"
"~applications/native-browser/" "~applications/native-browser/"
nil nil
"-content")) ""))
(define (define essay (make-page-fn "~etc/essay" "~etc/essay/" nil ""))
essay
(make-page-fn "~essays/index/essays-index-content" "~essays/" "/essay-" ""))
(define (define
philosophy philosophy
@@ -669,67 +667,53 @@
(slug) (slug)
(if (if
(nil? slug) (nil? slug)
(quote (~essays/philosophy-index/content)) (quote (~etc/philosophy))
(case (case
slug slug
"sx-manifesto" "sx-manifesto"
(quote (~essay-sx-manifesto)) (quote (~etc/philosophy/sx-manifesto))
"godel-escher-bach" "godel-escher-bach"
(quote (~essays/godel-escher-bach/essay-godel-escher-bach)) (quote (~etc/philosophy/godel-escher-bach))
"wittgenstein" "wittgenstein"
(quote (~essays/sx-and-wittgenstein/essay-sx-and-wittgenstein)) (quote (~etc/philosophy/wittgenstein))
"dennett" "dennett"
(quote (~essays/sx-and-dennett/essay-sx-and-dennett)) (quote (~etc/philosophy/dennett))
"existentialism" "existentialism"
(quote (~essays/s-existentialism/essay-s-existentialism)) (quote (~etc/philosophy/existentialism))
"platonic-sx" "platonic-sx"
(quote (~essays/platonic-sx/essay-platonic-sx)) (quote (~etc/philosophy/platonic-sx))
:else (quote (~essays/philosophy-index/content)))))) :else (quote (~etc/philosophy))))))
(define (define plan (make-page-fn "~etc/plan" "~etc/plan/" nil ""))
plan
(make-page-fn
"~plans/index/plans-index-content"
"~plans/"
"/plan-"
"-content"))
(define (define
capabilities capabilities
(fn (fn (&key title &rest args) (quasiquote (~geography/capabilities))))
(&key title &rest args)
(quasiquote (~geography/capabilities-content))))
(define (define
modules modules
(fn (&key title &rest args) (quasiquote (~geography/modules-content)))) (fn (&key title &rest args) (quasiquote (~geography/modules))))
(define (define
eval-rules eval-rules
(fn (&key title &rest args) (quasiquote (~geography/eval-rules-content)))) (fn (&key title &rest args) (quasiquote (~geography/eval-rules))))
(define (define
hyperscript hyperscript
(make-page-fn (make-page-fn
"~hyperscript/playground-content" "~applications/hyperscript"
"~hyperscript/" "~applications/hyperscript/"
nil nil
"-content")) ""))
(define htmx (make-page-fn "~htmx/demo-content" "~htmx/" nil "-content")) (define htmx (make-page-fn "~applications/htmx" "~applications/htmx/" nil ""))
(define (define sxtp (make-page-fn "~applications/sxtp" "~applications/sxtp/" nil ""))
sxtp
(make-page-fn
"~applications/sxtp/content"
"~applications/sxtp/"
nil
"-content"))
(define (define
graphql graphql
(make-page-fn "~graphql/demo-content" "~graphql/" nil "-content")) (make-page-fn "~applications/graphql" "~applications/graphql/" nil ""))
(define (define
pretext pretext
(make-page-fn "~pretext-demo/content" "~pretext-demo/" nil "-content")) (make-page-fn "~applications/pretext" "~applications/pretext/" nil ""))

View File

@@ -415,6 +415,14 @@
(swap-dom-nodes t oob s) (swap-dom-nodes t oob s)
(post-swap t))) (post-swap t)))
(hoist-head-elements container) (hoist-head-elements container)
(let
((manifest-el (dom-query-in container "script[data-sx-manifest]")))
(when
manifest-el
(host-call
(host-global "Sx")
"mergeManifest"
manifest-el)))
(let (let
((html (select-from-container container select-sel))) ((html (select-from-container container select-sel)))
(with-transition (with-transition
@@ -1547,7 +1555,8 @@
(process-sse root) (process-sse root)
(bind-inline-handlers root) (bind-inline-handlers root)
(process-emit-elements root) (process-emit-elements root)
(hs-boot-subtree! (or root (host-global "document"))))) (hs-boot-subtree! (or root (host-global "document")))
(htmx-boot-subtree! (or root (host-global "document")))))
(define (define
process-one process-one
:effects (mutation io) :effects (mutation io)
@@ -1622,9 +1631,11 @@
(pathname (url-pathname url))) (pathname (url-pathname url)))
(when (when
target target
(let (when
((headers (dict "SX-Request" "true"))) (not (try-client-route pathname target-sel))
(fetch-and-restore target url headers scrollY)))))) (let
((headers (build-request-headers target (loaded-component-names))))
(fetch-and-restore target url headers scrollY)))))))
(define (define
engine-init engine-init
:effects (mutation io) :effects (mutation io)