sx-tools: WASM kernel updates, TW/CSSX rework, content refresh, new debugging tools

Build tooling: updated OCaml bootstrapper, compile-modules, bundle.sh, sx-build-all.
WASM browser: rebuilt sx_browser.bc.js/wasm, sx-platform-2.js, .sxbc bytecode files.
CSSX/Tailwind: reworked cssx.sx templates and tw-layout, added tw-type support.
Content: refreshed essays, plans, geography, reactive islands, docs, demos, handlers.
New tools: bisect_sxbc.sh, test-spa.js, render-trace.sx, morph playwright spec.
Tests: added test-match.sx, test-examples.sx, updated test-tw.sx and web tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 11:31:57 +00:00
parent 9ed1100ef6
commit d40a9c6796
178 changed files with 13591 additions and 9110 deletions

View File

@@ -24,7 +24,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-31T23:22:57Z";
var SX_VERSION = "2026-04-01T20:24:51Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -462,6 +462,8 @@
PRIMITIVES["dict-set!"] = function(d, k, v) { d[k] = v; return v; };
PRIMITIVES["has-key?"] = function(d, k) { return d !== null && d !== undefined && k in d; };
PRIMITIVES["into"] = function(target, coll) {
if (target === "list") return Array.isArray(coll) ? coll.slice() : Object.entries(coll).map(function(e) { return [e[0], e[1]]; });
if (target === "dict") { var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; } return r; }
if (Array.isArray(target)) return Array.isArray(coll) ? coll.slice() : Object.entries(coll);
var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; }
return r;
@@ -2864,10 +2866,20 @@ PRIMITIVES["render-html-form?"] = isRenderHtmlForm;
return (isSxTruthy(!isSxTruthy(sxEq(typeOf(head), "symbol"))) ? join("", map(function(x) { return renderValueToHtml(x, env); }, expr)) : (function() {
var name = symbolName(head);
var args = rest(expr);
return (isSxTruthy(sxEq(name, "<>")) ? join("", map(function(x) { return renderToHtml(x, env); }, args)) : (isSxTruthy(sxEq(name, "raw!")) ? join("", map(function(x) { return (String(trampoline(evalExpr(x, env)))); }, args)) : (isSxTruthy(sxEq(name, "lake")) ? renderHtmlLake(args, env) : (isSxTruthy(sxEq(name, "marsh")) ? renderHtmlMarsh(args, env) : (isSxTruthy(sxOr(sxEq(name, "portal"), sxEq(name, "error-boundary"), sxEq(name, "promise-delayed"))) ? join("", map(function(x) { return renderToHtml(x, env); }, args)) : (isSxTruthy(contains(HTML_TAGS, name)) ? renderHtmlElement(name, args, env) : (isSxTruthy((isSxTruthy(startsWith(name, "~")) && isSxTruthy(envHas(env, name)) && isIsland(envGet(env, name)))) ? renderHtmlIsland(envGet(env, name), args, env) : (isSxTruthy(startsWith(name, "~")) ? (function() {
return (isSxTruthy(sxEq(name, "<>")) ? join("", map(function(x) { return renderToHtml(x, env); }, args)) : (isSxTruthy(sxEq(name, "raw!")) ? join("", map(function(x) { return (String(trampoline(evalExpr(x, env)))); }, args)) : (isSxTruthy(sxEq(name, "lake")) ? renderHtmlLake(args, env) : (isSxTruthy(sxEq(name, "marsh")) ? renderHtmlMarsh(args, env) : (isSxTruthy(sxEq(name, "error-boundary")) ? (function() {
var hasFallback = (len(args) > 1);
return (function() {
var bodyExprs = (isSxTruthy(hasFallback) ? rest(args) : args);
var fallbackExpr = (isSxTruthy(hasFallback) ? first(args) : NIL);
return (String("<div data-sx-boundary=\"true\">") + String(tryCatch(function() { return join("", map(function(x) { return renderToHtml(x, env); }, bodyExprs)); }, function(err) { return (function() {
var safeErr = replace_(replace_((String(err)), "<", "&lt;"), ">", "&gt;");
return (isSxTruthy((isSxTruthy(fallbackExpr) && !isSxTruthy(isNil(fallbackExpr)))) ? tryCatch(function() { return renderToHtml([trampoline(evalExpr(fallbackExpr, env)), err, NIL], env); }, function(e2) { return (String("<div class=\"sx-render-error\" style=\"color:red;font-size:0.875rem;padding:0.5rem;border:1px solid red;border-radius:0.25rem;margin:0.5rem 0;\">Render error: ") + String(safeErr) + String("</div>")); }) : (String("<div class=\"sx-render-error\" style=\"color:red;font-size:0.875rem;padding:0.5rem;border:1px solid red;border-radius:0.25rem;margin:0.5rem 0;\">Render error: ") + String(safeErr) + String("</div>")));
})(); })) + String("</div>"));
})();
})() : (isSxTruthy(sxOr(sxEq(name, "portal"), sxEq(name, "promise-delayed"))) ? join("", map(function(x) { return renderToHtml(x, env); }, args)) : (isSxTruthy(contains(HTML_TAGS, name)) ? renderHtmlElement(name, args, env) : (isSxTruthy((isSxTruthy(startsWith(name, "~")) && isSxTruthy(envHas(env, name)) && isIsland(envGet(env, name)))) ? renderHtmlIsland(envGet(env, name), args, env) : (isSxTruthy(startsWith(name, "~")) ? (function() {
var val = envGet(env, name);
return (isSxTruthy(isComponent(val)) ? renderHtmlComponent(val, args, env) : (isSxTruthy(isMacro(val)) ? renderToHtml(expandMacro(val, args, env), env) : (String("<!-- unknown component: ") + String(name) + String(" -->"))));
})() : (isSxTruthy(isRenderHtmlForm(name)) ? dispatchHtmlForm(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? renderToHtml(expandMacro(envGet(env, name), args, env), env) : renderValueToHtml(trampoline(evalExpr(expr, env)), env)))))))))));
})() : (isSxTruthy(isRenderHtmlForm(name)) ? dispatchHtmlForm(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? renderToHtml(expandMacro(envGet(env, name), args, env), env) : renderValueToHtml(trampoline(evalExpr(expr, env)), env))))))))))));
})());
})()); };
PRIMITIVES["render-list-to-html"] = renderListToHtml;
@@ -3099,11 +3111,25 @@ PRIMITIVES["aser"] = aser;
var comp = (isSxTruthy(envHas(env, name)) ? envGet(env, name) : NIL);
var expandAll = (isSxTruthy(envHas(env, "expand-components?")) ? expandComponents_p() : false);
return (isSxTruthy((isSxTruthy(comp) && isMacro(comp))) ? aser(expandMacro(comp, args, env), env) : (isSxTruthy((isSxTruthy(comp) && isSxTruthy(isComponent(comp)) && isSxTruthy(!isSxTruthy(isIsland(comp))) && isSxTruthy(sxOr(expandAll, sxEq(componentAffinity(comp), "server"))) && !isSxTruthy(sxEq(componentAffinity(comp), "client")))) ? aserExpandComponent(comp, args, env) : aserCall(name, args, env)));
})() : (isSxTruthy(sxEq(name, "lake")) ? aserCall(name, args, env) : (isSxTruthy(sxEq(name, "marsh")) ? aserCall(name, args, env) : (isSxTruthy(contains(HTML_TAGS, name)) ? aserCall(name, args, env) : (isSxTruthy(sxOr(isSpecialForm(name), isHoForm(name))) ? aserSpecial(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? aser(expandMacro(envGet(env, name), args, env), env) : (function() {
})() : (isSxTruthy(sxEq(name, "lake")) ? aserCall(name, args, env) : (isSxTruthy(sxEq(name, "marsh")) ? aserCall(name, args, env) : (isSxTruthy(sxEq(name, "error-boundary")) ? (function() {
var hasFallback = (len(args) > 1);
return (function() {
var bodyExprs = (isSxTruthy(hasFallback) ? rest(args) : args);
var errStr = NIL;
return (function() {
var rendered = tryCatch(function() { return join("", map(function(x) { return (function() {
var v = aser(x, env);
return (isSxTruthy(sxEq(typeOf(v), "sx-expr")) ? sxExprSource(v) : (isSxTruthy(isNil(v)) ? "" : serialize(v)));
})(); }, bodyExprs)); }, function(err) { errStr = (String(err));
return NIL; });
return (isSxTruthy(rendered) ? makeSxExpr((String("(error-boundary ") + String(rendered) + String(")"))) : makeSxExpr((String("(div :data-sx-boundary \"true\" ") + String("(div :class \"sx-render-error\" ") + String(":style \"color:red;font-size:0.875rem;padding:0.5rem;border:1px solid red;border-radius:0.25rem;margin:0.5rem 0;\" ") + String("\"Render error: ") + String(replace_(replace_(errStr, "\"", "'"), "\\", "\\\\")) + String("\"))"))));
})();
})();
})() : (isSxTruthy(contains(HTML_TAGS, name)) ? aserCall(name, args, env) : (isSxTruthy(sxOr(isSpecialForm(name), isHoForm(name))) ? aserSpecial(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? aser(expandMacro(envGet(env, name), args, env), env) : (function() {
var f = trampoline(evalExpr(head, env));
var evaledArgs = map(function(a) { return trampoline(evalExpr(a, env)); }, args);
return (isSxTruthy((isSxTruthy(isCallable(f)) && isSxTruthy(!isSxTruthy(isLambda(f))) && isSxTruthy(!isSxTruthy(isComponent(f))) && !isSxTruthy(isIsland(f)))) ? apply(f, evaledArgs) : (isSxTruthy(isLambda(f)) ? trampoline(callLambda(f, evaledArgs, env)) : (isSxTruthy(isComponent(f)) ? aserCall((String("~") + String(componentName(f))), args, env) : (isSxTruthy(isIsland(f)) ? aserCall((String("~") + String(componentName(f))), args, env) : error((String("Not callable: ") + String(inspect(f))))))));
})()))))))));
})())))))))));
})());
})(); };
PRIMITIVES["aser-list"] = aserList;
@@ -4522,8 +4548,8 @@ PRIMITIVES["render-dom-portal"] = renderDomPortal;
// render-dom-error-boundary
var renderDomErrorBoundary = function(args, env, ns) { return (function() {
var fallbackExpr = first(args);
var bodyExprs = rest(args);
var fallbackExpr = (isSxTruthy((len(args) > 1)) ? first(args) : NIL);
var bodyExprs = (isSxTruthy((len(args) > 1)) ? rest(args) : args);
var container = domCreateElement("div", NIL);
var retryVersion = signal(0);
domSetAttr(container, "data-sx-boundary", "true");
@@ -4540,7 +4566,13 @@ return (function() {
var fallbackFn = trampoline(evalExpr(fallbackExpr, env));
var retryFn = function() { return swap_b(retryVersion, function(n) { return (n + 1); }); };
return (function() {
var fallbackDom = (isSxTruthy(isLambda(fallbackFn)) ? renderLambdaDom(fallbackFn, [err, retryFn], env, ns) : renderToDom(apply(fallbackFn, [err, retryFn]), env, ns));
var fallbackDom = (isSxTruthy(isNil(fallbackFn)) ? (function() {
var el = domCreateElement("div", NIL);
domSetAttr(el, "class", "sx-render-error");
domSetAttr(el, "style", "color:red;font-size:0.875rem;padding:0.5rem;border:1px solid red;border-radius:0.25rem;margin:0.5rem 0;");
domSetTextContent(el, (String("Render error: ") + String(err)));
return el;
})() : (isSxTruthy(isLambda(fallbackFn)) ? renderLambdaDom(fallbackFn, [err, retryFn], env, ns) : renderToDom(apply(fallbackFn, [err, retryFn]), env, ns)));
return domAppend(container, fallbackDom);
})();
})(); }); });
@@ -4578,7 +4610,7 @@ PRIMITIVES["parse-time"] = parseTime;
PRIMITIVES["parse-trigger-spec"] = parseTriggerSpec;
// default-trigger
var defaultTrigger = function(tagName) { return (isSxTruthy(sxEq(tagName, "FORM")) ? [{["event"]: "submit", ["modifiers"]: {}}] : (isSxTruthy(sxOr(sxEq(tagName, "INPUT"), sxEq(tagName, "SELECT"), sxEq(tagName, "TEXTAREA"))) ? [{["event"]: "change", ["modifiers"]: {}}] : [{["event"]: "click", ["modifiers"]: {}}])); };
var defaultTrigger = function(tagName) { return (isSxTruthy(sxEq(tagName, "form")) ? [{["event"]: "submit", ["modifiers"]: {}}] : (isSxTruthy(sxOr(sxEq(tagName, "input"), sxEq(tagName, "select"), sxEq(tagName, "textarea"))) ? [{["event"]: "change", ["modifiers"]: {}}] : [{["event"]: "click", ["modifiers"]: {}}])); };
PRIMITIVES["default-trigger"] = defaultTrigger;
// get-verb-info
@@ -5053,11 +5085,11 @@ PRIMITIVES["handle-fetch-success"] = handleFetchSuccess;
var container = domCreateElement("div", NIL);
domAppend(container, rendered);
processOobSwaps(container, function(t, oob, s) { disposeIslandsIn(t);
swapDomNodes(t, oob, s);
sxHydrate(t);
return processElements(t); });
swapDomNodes(t, (isSxTruthy(sxEq(s, "innerHTML")) ? childrenToFragment(oob) : oob), s);
return postSwap(t); });
return (function() {
var selectSel = domGetAttr(el, "sx-select");
return (function() {
var content = (isSxTruthy(selectSel) ? selectFromContainer(container, selectSel) : childrenToFragment(container));
disposeIslandsIn(target);
return withTransition(useTransition, function() { return (function() {
@@ -5065,6 +5097,7 @@ return processElements(t); });
return postSwap((isSxTruthy(sxEq(swapStyle, "outerHTML")) ? domParent(sxOr(swapResult, target)) : sxOr(swapResult, target)));
})(); });
})();
})();
})() : NIL);
})();
})();
@@ -5078,12 +5111,20 @@ PRIMITIVES["handle-sx-response"] = handleSxResponse;
var selectSel = domGetAttr(el, "sx-select");
disposeIslandsIn(target);
return (isSxTruthy(selectSel) ? (function() {
var html = selectHtmlFromDoc(doc, selectSel);
var container = domCreateElement("div", NIL);
domSetInnerHtml(container, domBodyInnerHtml(doc));
processOobSwaps(container, function(t, oob, s) { disposeIslandsIn(t);
swapDomNodes(t, oob, s);
return postSwap(t); });
hoistHeadElements(container);
return (function() {
var html = selectFromContainer(container, selectSel);
return withTransition(useTransition, function() { return (function() {
var swapRoot = swapHtmlString(target, html, swapStyle);
var swapRoot = swapDomNodes(target, html, swapStyle);
logInfo((String("swap-root: ") + String((isSxTruthy(swapRoot) ? domTagName(swapRoot) : "nil")) + String(" target: ") + String(domTagName(target))));
return postSwap(sxOr(swapRoot, target));
})(); });
})();
})() : (function() {
var container = domCreateElement("div", NIL);
domSetInnerHtml(container, domBodyInnerHtml(doc));
@@ -5119,7 +5160,10 @@ PRIMITIVES["handle-retry"] = handleRetry;
return forEach(function(trigger) { return (function() {
var kind = classifyTrigger(trigger);
var mods = get(trigger, "modifiers");
return (isSxTruthy(sxEq(kind, "poll")) ? setInterval_(function() { return executeRequest(el, NIL, NIL); }, get(mods, "interval")) : (isSxTruthy(sxEq(kind, "intersect")) ? observeIntersection(el, function() { return executeRequest(el, NIL, NIL); }, false, get(mods, "delay")) : (isSxTruthy(sxEq(kind, "load")) ? setTimeout_(function() { return executeRequest(el, NIL, NIL); }, sxOr(get(mods, "delay"), 0)) : (isSxTruthy(sxEq(kind, "revealed")) ? observeIntersection(el, function() { return executeRequest(el, NIL, NIL); }, true, get(mods, "delay")) : (isSxTruthy(sxEq(kind, "event")) ? bindEvent(el, get(trigger, "event"), mods, verbInfo) : NIL)))));
return (isSxTruthy(sxEq(kind, "poll")) ? (function() {
var intervalId = NIL;
return (intervalId = setInterval_(function() { return (isSxTruthy(hostGet(el, "isConnected")) ? executeRequest(el, NIL, NIL) : (clearInterval_(intervalId), logInfo("poll stopped: element removed"))); }, get(mods, "interval")));
})() : (isSxTruthy(sxEq(kind, "intersect")) ? observeIntersection(el, function() { return executeRequest(el, NIL, NIL); }, false, get(mods, "delay")) : (isSxTruthy(sxEq(kind, "load")) ? setTimeout_(function() { return executeRequest(el, NIL, NIL); }, sxOr(get(mods, "delay"), 0)) : (isSxTruthy(sxEq(kind, "revealed")) ? observeIntersection(el, function() { return executeRequest(el, NIL, NIL); }, true, get(mods, "delay")) : (isSxTruthy(sxEq(kind, "event")) ? bindEvent(el, get(trigger, "event"), mods, verbInfo) : NIL)))));
})(); }, triggers);
})(); };
PRIMITIVES["bind-triggers"] = bindTriggers;
@@ -5421,7 +5465,32 @@ PRIMITIVES["offline-aware-mutation"] = offlineAwareMutation;
PRIMITIVES["current-page-layout"] = currentPageLayout;
// swap-rendered-content
var swapRenderedContent = function(target, rendered, pathname) { return (disposeIslandsIn(target), domSetTextContent(target, ""), domAppend(target, rendered), hoistHeadElementsFull(target), processElements(target), sxHydrateElements(target), sxHydrateIslands(target), runPostRenderHooks(), domDispatch(target, "sx:clientRoute", {["pathname"]: pathname}), logInfo((String("sx:route client ") + String(pathname)))); };
var swapRenderedContent = function(target, rendered, pathname) { return (function() {
var container = domCreateElement("div", NIL);
domAppend(container, rendered);
processOobSwaps(container, function(t, oob, s) { disposeIslandsIn(t);
swapDomNodes(t, (isSxTruthy(sxEq(s, "innerHTML")) ? childrenToFragment(oob) : oob), s);
return postSwap(t); });
return (function() {
var targetId = domGetAttr(target, "id");
return (function() {
var inner = (isSxTruthy(targetId) ? domQuery(container, (String("#") + String(targetId))) : NIL);
return (function() {
var content = (isSxTruthy(inner) ? childrenToFragment(inner) : childrenToFragment(container));
disposeIslandsIn(target);
domSetTextContent(target, "");
domAppend(target, content);
hoistHeadElementsFull(target);
processElements(target);
sxHydrateElements(target);
sxHydrateIslands(target);
runPostRenderHooks();
domDispatch(target, "sx:clientRoute", {["pathname"]: pathname});
return logInfo((String("sx:route client ") + String(pathname)));
})();
})();
})();
})(); };
PRIMITIVES["swap-rendered-content"] = swapRenderedContent;
// resolve-route-target

View File

@@ -166,6 +166,22 @@
document.cookie = args[0] + "=" + encodeURIComponent(args[1] || "") + ";path=/;max-age=31536000;SameSite=Lax";
});
// IntersectionObserver — native JS to avoid bytecode callback issues
K.registerNative("observe-intersection", function(args) {
var el = args[0], callback = args[1], once = args[2], delay = args[3];
var obs = new IntersectionObserver(function(entries) {
for (var i = 0; i < entries.length; i++) {
if (entries[i].isIntersecting) {
var d = (delay && delay !== null) ? delay : 0;
setTimeout(function() { K.callFn(callback, []); }, d);
if (once) obs.unobserve(el);
}
}
});
obs.observe(el);
return obs;
});
// ================================================================
// Load SX web libraries and adapters
// ================================================================
@@ -398,68 +414,6 @@
"children:", islands[j].children.length);
}
console.log("[sx] boot done");
// sx-on: inline event handlers — bind from JS because the WASM
// CSS selector [sx-on\:] doesn't match. Uses MutationObserver to
// also catch elements added after boot (e.g. from swaps).
function _bindSxOn(root) {
var all = (root || document).querySelectorAll('*');
for (var k = 0; k < all.length; k++) {
var el = all[k];
if (el._sxOnBound) continue;
var attrs = el.attributes;
var hasSxOn = false;
for (var a = 0; a < attrs.length; a++) {
var aname = attrs[a].name;
if (aname.indexOf('sx-on:') === 0) {
hasSxOn = true;
var evtName = aname.slice(6);
// HTML lowercases attrs: afterSwap → afterswap.
// Engine dispatches camelCase: sx:afterSwap.
// Listen for both forms.
var evtName2 = null;
if (evtName.indexOf('after') === 0 || evtName.indexOf('before') === 0) {
evtName2 = 'sx:' + evtName; // lowercase form
// Also try camelCase form
var camel = evtName.replace(/swap|request|settle/gi, function(m) {
return m.charAt(0).toUpperCase() + m.slice(1);
});
evtName = 'sx:' + camel;
}
(function(el2, evt, evt2, code) {
var handler = function(e) {
try { new Function('event', code).call(el2, e); }
catch(err) { console.warn('[sx] sx-on:' + evt + ' error:', err); }
};
el2.addEventListener(evt, handler);
if (evt2) el2.addEventListener(evt2, handler);
})(el, evtName, evtName2, attrs[a].value);
}
}
if (hasSxOn) el._sxOnBound = true;
}
}
_bindSxOn(document);
// Re-bind after swaps
document.addEventListener('sx:afterSwap', function(e) {
if (e.target) _bindSxOn(e.target);
});
// Global keyboard shortcut dispatch — WASM host-callbacks on
// document/body don't fire, so handle from:body keyboard
// triggers in JS and call execute-request via the SX engine.
document.addEventListener("keyup", function(e) {
if (e.target && e.target.matches && e.target.matches("input,textarea,select")) return;
var sel = '[sx-trigger*="key==\'' + e.key + '\'"]';
var els = document.querySelectorAll(sel);
for (var i = 0; i < els.length; i++) {
var el = els[i];
if (!el.id) el.id = "_sx_kbd_" + Math.random().toString(36).slice(2);
try {
K.eval('(execute-request (dom-query-by-id "' + el.id + '") nil nil)');
} catch(err) { console.warn("[sx] keyboard dispatch error:", err); }
}
});
}
}
};

View File

@@ -0,0 +1,452 @@
/**
* sx-platform.js — Browser platform layer for the SX WASM kernel.
*
* Registers the 8 FFI host primitives and loads web adapter .sx files.
* This is the only JS needed beyond the WASM kernel itself.
*
* Usage:
* <script src="sx_browser.bc.wasm.js"></script>
* <script src="sx-platform.js"></script>
*
* Or for js_of_ocaml mode:
* <script src="sx_browser.bc.js"></script>
* <script src="sx-platform.js"></script>
*/
(function() {
"use strict";
function boot(K) {
// ================================================================
// 8 FFI Host Primitives
// ================================================================
K.registerNative("host-global", function(args) {
var name = args[0];
if (typeof globalThis !== "undefined" && name in globalThis) return globalThis[name];
if (typeof window !== "undefined" && name in window) return window[name];
return null;
});
K.registerNative("host-get", function(args) {
var obj = args[0], prop = args[1];
if (obj == null) return null;
var v = obj[prop];
return v === undefined ? null : v;
});
K.registerNative("host-set!", function(args) {
var obj = args[0], prop = args[1], val = args[2];
if (obj != null) obj[prop] = val;
});
K.registerNative("host-call", function(args) {
var obj = args[0], method = args[1];
var callArgs = [];
for (var i = 2; i < args.length; i++) callArgs.push(args[i]);
if (obj == null) {
// Global function call
var fn = typeof globalThis !== "undefined" ? globalThis[method] : window[method];
if (typeof fn === "function") return fn.apply(null, callArgs);
return null;
}
if (typeof obj[method] === "function") {
try { return obj[method].apply(obj, callArgs); }
catch(e) { console.error("[sx] host-call error:", e); return null; }
}
return null;
});
K.registerNative("host-new", function(args) {
var name = args[0];
var cArgs = args.slice(1);
var Ctor = typeof globalThis !== "undefined" ? globalThis[name] : window[name];
if (typeof Ctor !== "function") return null;
switch (cArgs.length) {
case 0: return new Ctor();
case 1: return new Ctor(cArgs[0]);
case 2: return new Ctor(cArgs[0], cArgs[1]);
case 3: return new Ctor(cArgs[0], cArgs[1], cArgs[2]);
default: return new Ctor(cArgs[0], cArgs[1], cArgs[2], cArgs[3]);
}
});
K.registerNative("host-callback", function(args) {
var fn = args[0];
// Native JS function — pass through
if (typeof fn === "function") return fn;
// SX callable (has __sx_handle) — wrap as JS function
if (fn && fn.__sx_handle !== undefined) {
return function() {
var a = Array.prototype.slice.call(arguments);
return K.callFn(fn, a);
};
}
return function() {};
});
K.registerNative("host-typeof", function(args) {
var obj = args[0];
if (obj == null) return "nil";
if (obj instanceof Element) return "element";
if (obj instanceof Text) return "text";
if (obj instanceof DocumentFragment) return "fragment";
if (obj instanceof Document) return "document";
if (obj instanceof Event) return "event";
if (obj instanceof Promise) return "promise";
if (obj instanceof AbortController) return "abort-controller";
return typeof obj;
});
K.registerNative("host-await", function(args) {
var promise = args[0], callback = args[1];
if (promise && typeof promise.then === "function") {
var cb;
if (typeof callback === "function") cb = callback;
else if (callback && callback.__sx_handle !== undefined)
cb = function(v) { return K.callFn(callback, [v]); };
else cb = function() {};
promise.then(cb);
}
});
// ================================================================
// Constants expected by .sx files
// ================================================================
K.eval('(define SX_VERSION "wasm-1.0")');
K.eval('(define SX_ENGINE "ocaml-vm-wasm")');
K.eval('(define parse sx-parse)');
K.eval('(define serialize sx-serialize)');
// ================================================================
// DOM query helpers used by boot.sx / orchestration.sx
// (These are JS-native in the transpiled bundle; here via FFI.)
// ================================================================
K.registerNative("query-sx-scripts", function(args) {
var root = (args[0] && args[0] !== null) ? args[0] : document;
if (typeof root.querySelectorAll !== "function") root = document;
return Array.prototype.slice.call(root.querySelectorAll('script[type="text/sx"]'));
});
K.registerNative("query-page-scripts", function(args) {
return Array.prototype.slice.call(document.querySelectorAll('script[type="text/sx-pages"]'));
});
K.registerNative("query-component-scripts", function(args) {
var root = (args[0] && args[0] !== null) ? args[0] : document;
if (typeof root.querySelectorAll !== "function") root = document;
return Array.prototype.slice.call(root.querySelectorAll('script[type="text/sx"][data-components]'));
});
// localStorage
K.registerNative("local-storage-get", function(args) {
try { var v = localStorage.getItem(args[0]); return v === null ? null : v; }
catch(e) { return null; }
});
K.registerNative("local-storage-set", function(args) {
try { localStorage.setItem(args[0], args[1]); } catch(e) {}
});
K.registerNative("local-storage-remove", function(args) {
try { localStorage.removeItem(args[0]); } catch(e) {}
});
// log-info/log-warn defined in browser.sx; log-error as native fallback
K.registerNative("log-error", function(args) { console.error.apply(console, ["[sx]"].concat(args)); });
// Cookie access (browser-side)
K.registerNative("get-cookie", function(args) {
var name = args[0];
var match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '=([^;]*)'));
return match ? decodeURIComponent(match[1]) : null;
});
K.registerNative("set-cookie", function(args) {
document.cookie = args[0] + "=" + encodeURIComponent(args[1] || "") + ";path=/;max-age=31536000;SameSite=Lax";
});
// IntersectionObserver — native JS to avoid bytecode callback issues
K.registerNative("observe-intersection", function(args) {
var el = args[0], callback = args[1], once = args[2], delay = args[3];
var obs = new IntersectionObserver(function(entries) {
for (var i = 0; i < entries.length; i++) {
if (entries[i].isIntersecting) {
var d = (delay && delay !== null) ? delay : 0;
setTimeout(function() { K.callFn(callback, []); }, d);
if (once) obs.unobserve(el);
}
}
});
obs.observe(el);
return obs;
});
// ================================================================
// Load SX web libraries and adapters
// ================================================================
// Load order follows dependency graph:
// 1. Core spec files (parser, render, primitives already compiled into WASM kernel)
// 2. Spec modules: signals, deps, router, page-helpers
// 3. Bytecode compiler + VM (for JIT in browser)
// 4. Web libraries: dom.sx, browser.sx (built on 8 FFI primitives)
// 5. Web adapters: adapter-html, adapter-sx, adapter-dom
// 6. Web framework: engine, orchestration, boot
var _baseUrl = "";
// Detect base URL and cache-bust params from current script tag.
// _cacheBust comes from the script's own ?v= query string (used for .sx source fallback).
// _sxbcCacheBust comes from data-sxbc-hash attribute — a separate content hash
// covering all .sxbc files so each file gets its own correct cache buster.
var _cacheBust = "";
var _sxbcCacheBust = "";
(function() {
if (typeof document !== "undefined") {
var scripts = document.getElementsByTagName("script");
for (var i = scripts.length - 1; i >= 0; i--) {
var src = scripts[i].src || "";
if (src.indexOf("sx-platform") !== -1) {
_baseUrl = src.substring(0, src.lastIndexOf("/") + 1);
var qi = src.indexOf("?");
if (qi !== -1) _cacheBust = src.substring(qi);
var sxbcHash = scripts[i].getAttribute("data-sxbc-hash");
if (sxbcHash) _sxbcCacheBust = "?v=" + sxbcHash;
break;
}
}
}
})();
/**
* Deserialize type-tagged JSON constant back to JS value for loadModule.
*/
function deserializeConstant(c) {
if (!c || !c.t) return null;
switch (c.t) {
case 's': return c.v;
case 'n': return c.v;
case 'b': return c.v;
case 'nil': return null;
case 'sym': return { _type: 'symbol', name: c.v };
case 'kw': return { _type: 'keyword', name: c.v };
case 'list': return { _type: 'list', items: (c.v || []).map(deserializeConstant) };
case 'code': return {
_type: 'dict',
bytecode: { _type: 'list', items: c.v.bytecode },
constants: { _type: 'list', items: (c.v.constants || []).map(deserializeConstant) },
arity: c.v.arity || 0,
'upvalue-count': c.v['upvalue-count'] || 0,
locals: c.v.locals || 0,
};
case 'dict': {
var d = { _type: 'dict' };
for (var k in c.v) d[k] = deserializeConstant(c.v[k]);
return d;
}
default: return null;
}
}
/**
* Try loading a pre-compiled .sxbc bytecode module (SX text format).
* Returns true on success, null on failure (caller falls back to .sx source).
*/
function loadBytecodeFile(path) {
var sxbcPath = path.replace(/\.sx$/, '.sxbc');
var url = _baseUrl + sxbcPath + _sxbcCacheBust;
try {
var xhr = new XMLHttpRequest();
xhr.open("GET", url, false);
xhr.send();
if (xhr.status !== 200) return null;
window.__sxbcText = xhr.responseText;
var result = K.eval('(load-sxbc (first (parse (host-global "__sxbcText"))))');
delete window.__sxbcText;
if (typeof result === 'string' && result.indexOf('Error') === 0) {
console.warn("[sx-platform] bytecode FAIL " + path + ":", result);
return null;
}
return true;
} catch(e) {
delete window.__sxbcText;
return null;
}
}
/**
* Load an .sx file synchronously via XHR (boot-time only).
* Returns the number of expressions loaded, or an error string.
*/
function loadSxFile(path) {
var url = _baseUrl + path + _cacheBust;
try {
var xhr = new XMLHttpRequest();
xhr.open("GET", url, false); // synchronous
xhr.send();
if (xhr.status === 200) {
var result = K.load(xhr.responseText);
if (typeof result === "string" && result.indexOf("Error") === 0) {
console.error("[sx-platform] FAIL " + path + ":", result);
return 0;
}
console.log("[sx-platform] ok " + path + " (" + result + " exprs)");
return result;
} else {
console.error("[sx] Failed to fetch " + path + ": HTTP " + xhr.status);
return null;
}
} catch(e) {
console.error("[sx] Failed to load " + path + ":", e);
return null;
}
}
/**
* Load all web adapter .sx files in dependency order.
* Tries pre-compiled bytecode first, falls back to source.
*/
function loadWebStack() {
var files = [
// Spec modules
"sx/render.sx",
"sx/core-signals.sx",
"sx/signals.sx",
"sx/deps.sx",
"sx/router.sx",
"sx/page-helpers.sx",
// Freeze scope (signal persistence) + highlight (syntax coloring)
"sx/freeze.sx",
"sx/highlight.sx",
// Bytecode compiler + VM
"sx/bytecode.sx",
"sx/compiler.sx",
"sx/vm.sx",
// Web libraries (use 8 FFI primitives)
"sx/dom.sx",
"sx/browser.sx",
// Web adapters
"sx/adapter-html.sx",
"sx/adapter-sx.sx",
"sx/adapter-dom.sx",
// Boot helpers (platform functions in pure SX)
"sx/boot-helpers.sx",
"sx/hypersx.sx",
// Test harness (for inline test runners)
"sx/harness.sx",
"sx/harness-reactive.sx",
"sx/harness-web.sx",
// Web framework
"sx/engine.sx",
"sx/orchestration.sx",
"sx/boot.sx",
];
var loaded = 0, bcCount = 0, srcCount = 0;
if (K.beginModuleLoad) K.beginModuleLoad();
for (var i = 0; i < files.length; i++) {
var r = loadBytecodeFile(files[i]);
if (r) { bcCount++; continue; }
// Bytecode not available — end batch, load source, restart batch
if (K.endModuleLoad) K.endModuleLoad();
r = loadSxFile(files[i]);
if (typeof r === "number") { loaded += r; srcCount++; }
if (K.beginModuleLoad) K.beginModuleLoad();
}
if (K.endModuleLoad) K.endModuleLoad();
console.log("[sx-platform] Loaded " + files.length + " files (" + bcCount + " bytecode, " + srcCount + " source, " + loaded + " exprs)");
return loaded;
}
// ================================================================
// Compatibility shim — expose Sx global matching current JS API
// ================================================================
globalThis.Sx = {
VERSION: "wasm-1.0",
parse: function(src) { return K.parse(src); },
eval: function(src) { return K.eval(src); },
load: function(src) { return K.load(src); },
renderToHtml: function(expr) { return K.renderToHtml(expr); },
callFn: function(fn, args) { return K.callFn(fn, args); },
engine: function() { return K.engine(); },
// Boot entry point (called by auto-init or manually)
init: function() {
if (typeof K.eval === "function") {
// Check boot-init exists
// Step through boot manually
console.log("[sx] init-css-tracking...");
K.eval("(init-css-tracking)");
console.log("[sx] process-page-scripts...");
K.eval("(process-page-scripts)");
console.log("[sx] routes after pages:", K.eval("(len _page-routes)"));
console.log("[sx] process-sx-scripts...");
K.eval("(process-sx-scripts nil)");
console.log("[sx] sx-hydrate-elements...");
K.eval("(sx-hydrate-elements nil)");
console.log("[sx] sx-hydrate-islands...");
K.eval("(sx-hydrate-islands nil)");
console.log("[sx] process-elements...");
K.eval("(process-elements nil)");
// Debug islands
console.log("[sx] ~home/stepper defined?", K.eval("(type-of ~home/stepper)"));
console.log("[sx] ~layouts/header defined?", K.eval("(type-of ~layouts/header)"));
// Island count (JS-side, avoids VM overhead)
console.log("[sx] manual island query:", document.querySelectorAll("[data-sx-island]").length);
// Try hydrating again
console.log("[sx] retry hydrate-islands...");
K.eval("(sx-hydrate-islands nil)");
// Check if links are boosted
var links = document.querySelectorAll("a[href]");
var boosted = 0;
for (var i = 0; i < links.length; i++) {
if (links[i]._sxBoundboost) boosted++;
}
console.log("[sx] boosted links:", boosted, "/", links.length);
// Check island state
var islands = document.querySelectorAll("[data-sx-island]");
console.log("[sx] islands:", islands.length);
for (var j = 0; j < islands.length; j++) {
console.log("[sx] island:", islands[j].getAttribute("data-sx-island"),
"hydrated:", !!islands[j]._sxBoundislandhydrated || !!islands[j]["_sxBound" + "island-hydrated"],
"children:", islands[j].children.length);
}
console.log("[sx] boot done");
}
}
};
// ================================================================
// Auto-init: load web stack and boot on DOMContentLoaded
// ================================================================
if (typeof document !== "undefined") {
var _doInit = function() {
loadWebStack();
Sx.init();
// Enable JIT after all boot code has run
setTimeout(function() { K.eval('(enable-jit!)'); }, 0);
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", _doInit);
} else {
_doInit();
}
}
} // end boot
// SxKernel is available synchronously (js_of_ocaml) or after async
// WASM init. Poll briefly to handle both cases.
var K = globalThis.SxKernel;
if (K) { boot(K); return; }
var tries = 0;
var poll = setInterval(function() {
K = globalThis.SxKernel;
if (K) { clearInterval(poll); boot(K); }
else if (++tries > 100) { clearInterval(poll); console.error("[sx-platform] SxKernel not found after 5s"); }
}, 50);
})();

View File

@@ -437,17 +437,9 @@
(let
((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env))))
(map
(fn
(item)
(if
(lambda? f)
(let
((local (env-merge (lambda-closure f) env)))
(env-bind! local (first (lambda-params f)) item)
(aser (lambda-body f) local))
(cek-call f (list item))))
coll))
(let
((results (map (fn (item) (if (lambda? f) (let ((local (env-extend (lambda-closure f)))) (env-bind! local (first (lambda-params f)) item) (aser (lambda-body f) local)) (cek-call f (list item)))) coll)))
(aser-fragment results env)))
(= name "map-indexed")
(let
((f (trampoline (eval-expr (first args) env)))

File diff suppressed because one or more lines are too long

View File

@@ -430,17 +430,6 @@
(host-callback thunk))
(thunk))))
(define
observe-intersection
(fn
(el callback once? delay)
(let
((cb (host-callback (fn (entries) (for-each (fn (entry) (when (host-get entry "isIntersecting") (if delay (set-timeout (fn () (callback entry)) delay) (callback entry)) (when once? (host-call observer "unobserve" el)))) (host-call entries "forEach" (host-callback (fn (e) e))))))))
(let
((observer (host-new "IntersectionObserver" (host-callback (fn (entries) (let ((arr-len (host-get entries "length"))) (let loop ((i 0)) (when (< i arr-len) (let ((entry (host-call entries "item" i))) (when (and entry (host-get entry "isIntersecting")) (if delay (set-timeout (fn () (callback entry)) delay) (callback entry)) (when once? (host-call observer "unobserve" el)))) (loop (+ i 1))))))))))
(host-call observer "observe" el)
observer))))
(define
event-source-connect
(fn

File diff suppressed because one or more lines are too long

View File

@@ -1,17 +1,23 @@
;; Create raw signal dict with value, subs, deps fields
(define
make-signal
(fn
(value)
(dict "__signal" true "value" value "subscribers" (list) "deps" (list))))
;; Type predicate for signals
(define signal? (fn (x) (and (dict? x) (has-key? x "__signal"))))
;; Read current value from signal
(define signal-value (fn (s) (get s "value")))
;; Write value to signal (no notification)
(define signal-set-value! (fn (s v) (dict-set! s "value" v)))
;; List of subscriber functions
(define signal-subscribers (fn (s) (get s "subscribers")))
;; Add a subscriber function
(define
signal-add-sub!
(fn
@@ -20,6 +26,7 @@
(not (contains? (get s "subscribers") f))
(dict-set! s "subscribers" (append (get s "subscribers") (list f))))))
;; Remove a subscriber function
(define
signal-remove-sub!
(fn
@@ -29,15 +36,19 @@
"subscribers"
(filter (fn (sub) (not (identical? sub f))) (get s "subscribers")))))
;; List of upstream signal dependencies
(define signal-deps (fn (s) (get s "deps")))
;; Set upstream dependencies
(define signal-set-deps! (fn (s deps) (dict-set! s "deps" deps)))
;; Create a reactive signal (user-facing constructor)
(define
signal
:effects ()
(fn ((initial-value :as any)) (make-signal initial-value)))
;; Dereference a signal, returning its current value
(define
deref
:effects ()
@@ -58,6 +69,7 @@
(signal-add-sub! s notify-fn))))
(signal-value s)))))
;; Set signal to new value and notify subscribers
(define
reset!
:effects (mutation)
@@ -72,6 +84,7 @@
(signal-set-value! s value)
(notify-subscribers s))))))
;; Apply function to current value and reset
(define
swap!
:effects (mutation)
@@ -87,6 +100,7 @@
(signal-set-value! s new-val)
(notify-subscribers s))))))
;; Create a derived signal that auto-updates from dependencies
(define
computed
:effects (mutation)
@@ -100,6 +114,7 @@
(register-in-scope (fn () (dispose-computed s)))
s))))
;; Create a side-effect that runs when dependencies change
(define
effect
:effects (mutation)
@@ -115,10 +130,13 @@
(register-in-scope dispose-fn)
dispose-fn)))))
;; Nesting counter for batched updates
(define *batch-depth* 0)
;; Queued notifications during batch
(define *batch-queue* (list))
;; Batch multiple signal updates, notify once at end
(define
batch
:effects (mutation)
@@ -148,6 +166,7 @@
queue)
(for-each (fn ((sub :as lambda)) (sub)) pending))))))
;; Notify all subscribers of a signal change
(define
notify-subscribers
:effects (mutation)
@@ -158,6 +177,7 @@
(when (not (contains? *batch-queue* s)) (append! *batch-queue* s))
(flush-subscribers s))))
;; Process queued subscriber notifications
(define
flush-subscribers
:effects (mutation)
@@ -165,6 +185,7 @@
((s :as dict))
(for-each (fn (sub) (cek-call sub nil)) (signal-subscribers s))))
;; Tear down a computed signal, remove from deps
(define
dispose-computed
:effects (mutation)
@@ -177,6 +198,7 @@
(signal-deps s))
(signal-set-deps! s (list)))))
;; Evaluate body in an island disposal scope
(define
with-island-scope
:effects (mutation)
@@ -185,6 +207,7 @@
(scope-push! "sx-island-scope" scope-fn)
(let ((result (body-fn))) (scope-pop! "sx-island-scope") result)))
;; Register a disposable in the current island scope
(define
register-in-scope
:effects (mutation)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,41 +1,62 @@
;; Assert condition is truthy, error with message
(define assert (fn (condition msg) (when (not condition) (error (or msg "Assertion failed")))))
;; Assert two values are equal
(define assert= (fn (actual expected msg) (when (not (= actual expected)) (error (or msg (str "Expected " expected ", got " actual))))))
;; Dict of mock IO operations for testing
(define default-platform {:current-user (fn () nil) :csrf-token (fn () "test-csrf-token") :app-url (fn (service &rest path) "/mock-app-url") :frag (fn (service comp &rest args) "") :sleep (fn (ms) nil) :local-storage-set (fn (key val) nil) :set-cookie (fn (name val &rest opts) nil) :url-for (fn (endpoint &rest args) "/mock-url") :create-element (fn (tag) nil) :request-path (fn () "/") :config (fn (key) nil) :set-attr (fn (el name val) nil) :set-text (fn (el text) nil) :remove-child (fn (parent child) nil) :fetch (fn (url &rest opts) {:status 200 :body "" :ok true}) :query (fn (service name &rest args) (list)) :add-class (fn (el cls) nil) :get-element (fn (id) nil) :now (fn () 0) :abort (fn (code) nil) :action (fn (service name &rest args) {:ok true}) :remove-class (fn (el cls) nil) :append-child (fn (parent child) nil) :request-arg (fn (name) nil) :emit-dom (fn (op &rest args) nil) :local-storage-get (fn (key) nil) :get-cookie (fn (name) nil)})
;; Create a test session with mock IO platform
(define make-harness :effects () (fn (&key platform) (let ((merged (if (nil? platform) default-platform (merge default-platform platform)))) {:log (list) :platform merged :state {:cookies {} :storage {} :dom nil}})))
;; Clear IO log and state for a new test
(define harness-reset! :effects () (fn (session) (dict-set! session "log" (list)) (dict-set! session "state" {:cookies {} :storage {} :dom nil}) session))
;; Append an IO call record to session log
(define harness-log :effects () (fn (session &key op) (let ((log (get session "log"))) (if (nil? op) log (filter (fn (entry) (= (get entry "op") op)) log)))))
;; Read state value from session store
(define harness-get :effects () (fn (session key) (get (get session "state") key)))
;; Write state value to session store
(define harness-set! :effects () (fn (session key value) (dict-set! (get session "state") key value) nil))
;; Wrap a mock fn to record calls in the IO log
(define make-interceptor :effects () (fn (session op-name mock-fn) (fn (&rest args) (let ((result (if (empty? args) (mock-fn) (if (= 1 (len args)) (mock-fn (first args)) (if (= 2 (len args)) (mock-fn (first args) (nth args 1)) (if (= 3 (len args)) (mock-fn (first args) (nth args 1) (nth args 2)) (apply mock-fn args)))))) (log (get session "log"))) (append! log {:args args :result result :op op-name}) result))))
;; Bind all interceptors into the eval environment
(define install-interceptors :effects () (fn (session env) (for-each (fn (key) (let ((mock-fn (get (get session "platform") key)) (interceptor (make-interceptor session key mock-fn))) (env-bind! env key interceptor))) (keys (get session "platform"))) env))
;; Query IO log: all calls, or filtered by op name
(define io-calls :effects () (fn (session op-name) (filter (fn (entry) (= (get entry "op") op-name)) (get session "log"))))
;; Count IO calls, optionally filtered by op name
(define io-call-count :effects () (fn (session op-name) (len (io-calls session op-name))))
;; Get the nth IO call record
(define io-call-nth :effects () (fn (session op-name n) (let ((calls (io-calls session op-name))) (if (< n (len calls)) (nth calls n) nil))))
;; Get args from the nth call to an operation
(define io-call-args :effects () (fn (session op-name n) (let ((call (io-call-nth session op-name n))) (if (nil? call) nil (get call "args")))))
;; Get return value from the nth call to an operation
(define io-call-result :effects () (fn (session op-name n) (let ((call (io-call-nth session op-name n))) (if (nil? call) nil (get call "result")))))
;; Assert an IO operation was called at least once
(define assert-io-called :effects () (fn (session op-name) (assert (> (io-call-count session op-name) 0) (str "Expected IO operation " op-name " to be called but it was not"))))
;; Assert an IO operation was never called
(define assert-no-io :effects () (fn (session op-name) (assert (= (io-call-count session op-name) 0) (str "Expected IO operation " op-name " not to be called but it was called " (io-call-count session op-name) " time(s)"))))
;; Assert exact call count for an operation
(define assert-io-count :effects () (fn (session op-name expected) (let ((actual (io-call-count session op-name))) (assert (= actual expected) (str "Expected " op-name " to be called " expected " time(s) but was called " actual " time(s)")))))
;; Assert args of the nth call match expected
(define assert-io-args :effects () (fn (session op-name n expected-args) (let ((actual (io-call-args session op-name n))) (assert (equal? actual expected-args) (str "Expected call " n " to " op-name " with args " (str expected-args) " but got " (str actual))))))
;; Assert result of the nth call matches expected
(define assert-io-result :effects () (fn (session op-name n expected) (let ((actual (io-call-result session op-name n))) (assert (equal? actual expected) (str "Expected call " n " to " op-name " to return " (str expected) " but got " (str actual))))))
;; Assert a state key has the expected value
(define assert-state :effects () (fn (session key expected) (let ((actual (harness-get session key))) (assert (equal? actual expected) (str "Expected state " key " to be " (str expected) " but got " (str actual))))))

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,4 @@
;; Registry of all valid HTML tag names
(define
HTML_TAGS
(list
@@ -141,6 +142,7 @@
"dialog"
"menu"))
;; Self-closing tags (br, img, hr, etc.)
(define
VOID_ELEMENTS
(list
@@ -159,6 +161,7 @@
"track"
"wbr"))
;; Attrs that are true/false (checked, disabled, etc.)
(define
BOOLEAN_ATTRS
(list
@@ -186,8 +189,10 @@
"reversed"
"selected"))
;; Extensible list of forms treated as definitions
(define *definition-form-extensions* (list))
;; Check if a symbol names a definition form
(define
definition-form?
:effects ()
@@ -203,6 +208,7 @@
(= name "defeffect")
(contains? *definition-form-extensions* name))))
;; Parse keyword attrs and children from element arg list
(define
parse-element-args
:effects (render)
@@ -233,6 +239,7 @@
args)
(list attrs children))))
;; Render attr dict to HTML attribute string
(define
render-attrs
:effects ()
@@ -255,6 +262,7 @@
:else (str " " key "=\"" (escape-attr (str val)) "\""))))
(keys attrs)))))
;; Evaluate cond expression (dispatches to scheme/clojure style)
(define
eval-cond
:effects ()
@@ -265,6 +273,7 @@
(eval-cond-scheme clauses env)
(eval-cond-clojure clauses env))))
;; Scheme-style cond: ((test body) ...)
(define
eval-cond-scheme
:effects ()
@@ -285,6 +294,7 @@
body
(eval-cond-scheme (rest clauses) env)))))))
;; Clojure-style cond: (test body test body ...)
(define
eval-cond-clojure
:effects ()
@@ -303,6 +313,7 @@
body
(eval-cond-clojure (slice clauses 2) env)))))))
;; Evaluate let binding pairs, extend env
(define
process-bindings
:effects (mutation)
@@ -324,6 +335,7 @@
bindings)
local)))
;; Check if an expression should be rendered vs evaluated
(define
is-render-expr?
:effects ()
@@ -350,6 +362,7 @@
(> (len expr) 1)
(= (type-of (nth expr 1)) "keyword")))))))))
;; Merge spread child attrs into parent element attrs
(define
merge-spread-attrs
:effects (mutation)
@@ -385,6 +398,7 @@
(dict-set! target key val)))))
(keys spread-dict))))
;; Escape special chars for HTML text content
(define
escape-html
(fn
@@ -397,4 +411,5 @@
(set! r (replace r "\"" "&quot;"))
r)))
;; Escape special chars for HTML attribute values
(define escape-attr (fn (s) (escape-html s)))

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,414 @@
(define tw-spacing-props {:ml "margin-left:{v}" :mr "margin-right:{v}" :mt "margin-top:{v}" :mb "margin-bottom:{v}" :pl "padding-left:{v}" :gap-y "row-gap:{v}" :m "margin:{v}" :gap-x "column-gap:{v}" :my "margin-top:{v};margin-bottom:{v}" :px "padding-left:{v};padding-right:{v}" :pb "padding-bottom:{v}" :pr "padding-right:{v}" :p "padding:{v}" :gap "gap:{v}" :py "padding-top:{v};padding-bottom:{v}" :pt "padding-top:{v}" :mx "margin-left:{v};margin-right:{v}"})
(define tw-displays {:flex "flex" :table "table" :grid "grid" :inline-block "inline-block" :table-row "table-row" :inline "inline" :hidden "none" :block "block" :contents "contents" :inline-flex "inline-flex" :inline-grid "inline-grid" :table-cell "table-cell"})
(define tw-max-widths {:xs "20rem" :3xl "48rem" :7xl "80rem" :sm "24rem" :xl "36rem" :full "100%" :md "28rem" :6xl "72rem" :prose "65ch" :max "max-content" :5xl "64rem" :min "min-content" :lg "32rem" :2xl "42rem" :4xl "56rem" :none "none" :screen "100vw" :fit "fit-content"})
(define tw-min-widths {:full "100%" :0 "0px" :max "max-content" :min "min-content" :fit "fit-content"})
(define
tw-resolve-layout
(fn
(token)
(let
((parts (split token "-"))
(head (first parts))
(rest (slice parts 1)))
(cond
(and (= (len parts) 1) (not (nil? (get tw-displays head))))
(str "display:" (get tw-displays head))
(and (= (len parts) 2) (not (nil? (get tw-displays token))))
(str "display:" (get tw-displays token))
(and (get tw-spacing-props head) (= (len rest) 1))
(let
((tmpl (get tw-spacing-props head))
(v (tw-spacing-value (first rest))))
(if (nil? v) nil (tw-template tmpl v)))
(and
(= (len rest) 2)
(get tw-spacing-props (str head "-" (first rest))))
(let
((tmpl (get tw-spacing-props (str head "-" (first rest))))
(v (tw-spacing-value (last rest))))
(if (nil? v) nil (tw-template tmpl v)))
(and
(= head "space")
(= (len rest) 2)
(or (= (first rest) "x") (= (first rest) "y")))
(let
((v (tw-spacing-value (last rest))) (dir (first rest)))
(if (nil? v) nil (if (= dir "x") {:suffix ">*+*" :css (str "margin-left:" v)} {:suffix ">*+*" :css (str "margin-top:" v)})))
(and (= head "flex") (empty? rest))
"display:flex"
(and (= head "flex") (= (len rest) 1))
(case
(first rest)
"row"
"flex-direction:row"
"col"
"flex-direction:column"
"wrap"
"flex-wrap:wrap"
"nowrap"
"flex-wrap:nowrap"
"1"
"flex:1 1 0%"
"auto"
"flex:1 1 auto"
"initial"
"flex:0 1 auto"
"none"
"flex:none"
:else nil)
(and (= head "flex") (= (len rest) 2))
(case
(join "-" rest)
"row-reverse"
"flex-direction:row-reverse"
"col-reverse"
"flex-direction:column-reverse"
"wrap-reverse"
"flex-wrap:wrap-reverse"
:else nil)
(= head "grow")
(if
(empty? rest)
"flex-grow:1"
(if (= (first rest) "0") "flex-grow:0" nil))
(= head "shrink")
(if
(empty? rest)
"flex-shrink:1"
(if (= (first rest) "0") "flex-shrink:0" nil))
(and (= head "basis") (= (len rest) 1))
(let
((val (first rest)))
(cond
(= val "auto")
"flex-basis:auto"
(= val "full")
"flex-basis:100%"
(= val "0")
"flex-basis:0px"
(contains? val "/")
(let
((frac (split val "/")))
(if
(= (len frac) 2)
(let
((num (parse-int (first frac) nil))
(den (parse-int (nth frac 1) nil)))
(if
(or (nil? num) (nil? den))
nil
(str "flex-basis:" (* (/ num den) 100) "%")))
nil))
:else (let
((n (parse-int val nil)))
(if (nil? n) nil (str "flex-basis:" (* n 0.25) "rem")))))
(and (= head "justify") (= (len rest) 1))
(case
(first rest)
"start"
"justify-content:flex-start"
"end"
"justify-content:flex-end"
"center"
"justify-content:center"
"between"
"justify-content:space-between"
"around"
"justify-content:space-around"
"evenly"
"justify-content:space-evenly"
"stretch"
"justify-content:stretch"
:else nil)
(and (= head "items") (= (len rest) 1))
(case
(first rest)
"start"
"align-items:flex-start"
"end"
"align-items:flex-end"
"center"
"align-items:center"
"baseline"
"align-items:baseline"
"stretch"
"align-items:stretch"
:else nil)
(and (= head "self") (= (len rest) 1))
(case
(first rest)
"auto"
"align-self:auto"
"start"
"align-self:flex-start"
"end"
"align-self:flex-end"
"center"
"align-self:center"
"stretch"
"align-self:stretch"
"baseline"
"align-self:baseline"
:else nil)
(and (= head "content") (= (len rest) 1))
(case
(first rest)
"start"
"align-content:flex-start"
"end"
"align-content:flex-end"
"center"
"align-content:center"
"between"
"align-content:space-between"
"around"
"align-content:space-around"
"evenly"
"align-content:space-evenly"
"stretch"
"align-content:stretch"
:else nil)
(and (= head "order") (= (len rest) 1))
(let
((val (first rest)))
(cond
(= val "first")
"order:-9999"
(= val "last")
"order:9999"
(= val "none")
"order:0"
:else (let
((n (parse-int val nil)))
(if (nil? n) nil (str "order:" n)))))
(and (= head "grid") (empty? rest))
"display:grid"
(and (= head "grid") (>= (len rest) 2) (= (first rest) "cols"))
(let
((val (join "-" (slice rest 1))))
(cond
(= val "none")
"grid-template-columns:none"
(= val "subgrid")
"grid-template-columns:subgrid"
:else (let
((n (parse-int val nil)))
(if
(nil? n)
nil
(str "grid-template-columns:repeat(" n ",minmax(0,1fr))")))))
(and (= head "grid") (>= (len rest) 2) (= (first rest) "rows"))
(let
((val (join "-" (slice rest 1))))
(cond
(= val "none")
"grid-template-rows:none"
(= val "subgrid")
"grid-template-rows:subgrid"
:else (let
((n (parse-int val nil)))
(if
(nil? n)
nil
(str "grid-template-rows:repeat(" n ",minmax(0,1fr))")))))
(and (= head "grid") (>= (len rest) 2) (= (first rest) "flow"))
(case
(nth rest 1)
"row"
"grid-auto-flow:row"
"col"
"grid-auto-flow:column"
"dense"
"grid-auto-flow:dense"
:else nil)
(and (= head "col") (>= (len rest) 2))
(let
((sub (first rest)) (val (join "-" (slice rest 1))))
(cond
(and (= sub "span") (= val "full"))
"grid-column:1 / -1"
(= sub "span")
(let
((n (parse-int val nil)))
(if (nil? n) nil (str "grid-column:span " n " / span " n)))
(= sub "start")
(str "grid-column-start:" val)
(= sub "end")
(str "grid-column-end:" val)
:else nil))
(and (= head "row") (>= (len rest) 2))
(let
((sub (first rest)) (val (join "-" (slice rest 1))))
(cond
(and (= sub "span") (= val "full"))
"grid-row:1 / -1"
(= sub "span")
(let
((n (parse-int val nil)))
(if (nil? n) nil (str "grid-row:span " n " / span " n)))
(= sub "start")
(str "grid-row-start:" val)
(= sub "end")
(str "grid-row-end:" val)
:else nil))
(and (= head "auto") (>= (len rest) 2))
(let
((sub (first rest)) (val (join "-" (slice rest 1))))
(cond
(and (= sub "cols") (= val "auto"))
"grid-auto-columns:auto"
(and (= sub "cols") (= val "min"))
"grid-auto-columns:min-content"
(and (= sub "cols") (= val "max"))
"grid-auto-columns:max-content"
(and (= sub "cols") (= val "fr"))
"grid-auto-columns:minmax(0,1fr)"
(and (= sub "rows") (= val "auto"))
"grid-auto-rows:auto"
(and (= sub "rows") (= val "min"))
"grid-auto-rows:min-content"
(and (= sub "rows") (= val "max"))
"grid-auto-rows:max-content"
(and (= sub "rows") (= val "fr"))
"grid-auto-rows:minmax(0,1fr)"
:else nil))
(and
(= (len parts) 1)
(or
(= head "relative")
(= head "absolute")
(= head "fixed")
(= head "sticky")
(= head "static")))
(str "position:" head)
(and
(or
(= head "top")
(= head "right")
(= head "bottom")
(= head "left"))
(= (len rest) 1))
(let
((v (tw-spacing-value (first rest))))
(if (nil? v) nil (str head ":" v)))
(and (= head "inset") (= (len rest) 1))
(let
((v (tw-spacing-value (first rest))))
(if (nil? v) nil (str "inset:" v)))
(and (= head "inset") (= (len rest) 2))
(let
((dir (first rest)) (v (tw-spacing-value (nth rest 1))))
(if
(nil? v)
nil
(case
dir
"x"
(str "left:" v ";right:" v)
"y"
(str "top:" v ";bottom:" v)
:else nil)))
(and (= head "z") (= (len rest) 1))
(if
(= (first rest) "auto")
"z-index:auto"
(let
((n (parse-int (first rest) nil)))
(if (nil? n) nil (str "z-index:" n))))
(and (or (= head "w") (= head "h")) (= (len rest) 1))
(let
((prop (if (= head "w") "width" "height")) (val (first rest)))
(cond
(= val "full")
(str prop ":100%")
(= val "screen")
(str prop (if (= head "w") ":100vw" ":100vh"))
(= val "auto")
(str prop ":auto")
(= val "min")
(str prop ":min-content")
(= val "max")
(str prop ":max-content")
(= val "fit")
(str prop ":fit-content")
(contains? val "/")
(let
((frac (split val "/")))
(if
(= (len frac) 2)
(let
((num (parse-int (first frac) nil))
(den (parse-int (nth frac 1) nil)))
(if
(or (nil? num) (nil? den))
nil
(str prop ":" (* (/ num den) 100) "%")))
nil))
:else (let
((n (parse-int val nil)))
(if (nil? n) nil (str prop ":" (* n 0.25) "rem")))))
(and (= head "max") (>= (len rest) 2) (= (first rest) "w"))
(let
((val-name (join "-" (slice rest 1)))
(val (get tw-max-widths val-name)))
(if (nil? val) nil (str "max-width:" val)))
(and (= head "max") (>= (len rest) 2) (= (first rest) "h"))
(let
((val (first (slice rest 1))))
(cond
(= val "full")
"max-height:100%"
(= val "screen")
"max-height:100vh"
(= val "none")
"max-height:none"
:else (let
((n (parse-int val nil)))
(if (nil? n) nil (str "max-height:" (* n 0.25) "rem")))))
(and (= head "min") (>= (len rest) 2) (= (first rest) "w"))
(let
((val-name (join "-" (slice rest 1)))
(val (get tw-min-widths val-name)))
(if (nil? val) nil (str "min-width:" val)))
(and (= head "min") (>= (len rest) 2) (= (first rest) "h"))
(let
((val (first (slice rest 1))))
(cond
(= val "0")
"min-height:0px"
(= val "full")
"min-height:100%"
(= val "screen")
"min-height:100vh"
:else nil))
(and (= head "overflow") (= (len rest) 1))
(str "overflow:" (first rest))
(and (= head "overflow") (= (len rest) 2))
(str "overflow-" (first rest) ":" (nth rest 1))
(and (= head "aspect") (= (len rest) 1))
(case
(first rest)
"auto"
"aspect-ratio:auto"
"square"
"aspect-ratio:1 / 1"
"video"
"aspect-ratio:16 / 9"
:else nil)
(and (= head "object") (= (len rest) 1))
(str "object-fit:" (first rest))
(and (= (len parts) 1) (= head "visible"))
"visibility:visible"
(and (= (len parts) 1) (= head "invisible"))
"visibility:hidden"
(and (= (len parts) 1) (= head "collapse"))
"visibility:collapse"
(and (= (len parts) 1) (= head "container"))
"width:100%;max-width:100%"
(and (= (len parts) 1) (= head "isolate"))
"isolation:isolate"
:else nil))))

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,213 @@
(define tw-sizes {:xs "font-size:0.75rem;line-height:1rem" :3xl "font-size:1.875rem;line-height:2.25rem" :7xl "font-size:4.5rem;line-height:1" :sm "font-size:0.875rem;line-height:1.25rem" :8xl "font-size:6rem;line-height:1" :xl "font-size:1.25rem;line-height:1.75rem" :6xl "font-size:3.75rem;line-height:1" :9xl "font-size:8rem;line-height:1" :5xl "font-size:3rem;line-height:1" :lg "font-size:1.125rem;line-height:1.75rem" :2xl "font-size:1.5rem;line-height:2rem" :base "font-size:1rem;line-height:1.5rem" :4xl "font-size:2.25rem;line-height:2.5rem"})
(define tw-weights {:light "300" :semibold "600" :bold "700" :extrabold "800" :black "900" :extralight "200" :thin "100" :medium "500" :normal "400"})
(define tw-families {:mono "ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace" :sans "ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,\"Noto Sans\",sans-serif" :serif "ui-serif,Georgia,Cambria,\"Times New Roman\",Times,serif"})
(define tw-alignments {:center true :end true :left true :right true :start true :justify true})
(define tw-leading {:tight "1.25" :9 "2.25rem" :loose "2" :relaxed "1.625" :3 "0.75rem" :8 "2rem" :5 "1.25rem" :4 "1rem" :6 "1.5rem" :snug "1.375" :none "1" :normal "1.5" :7 "1.75rem" :10 "2.5rem"})
(define tw-tracking {:wide "0.025em" :tight "-0.025em" :tighter "-0.05em" :wider "0.05em" :widest "0.1em" :normal "0em"})
(define
tw-resolve-type
(fn
(token)
(let
((parts (split token "-"))
(head (first parts))
(rest (slice parts 1)))
(cond
(and
(= head "text")
(= (len rest) 1)
(not (nil? (get tw-sizes (first rest)))))
(get tw-sizes (first rest))
(and
(= head "text")
(= (len rest) 1)
(get tw-alignments (first rest)))
(str "text-align:" (first rest))
(and
(= (len parts) 1)
(or
(= head "uppercase")
(= head "lowercase")
(= head "capitalize")))
(str "text-transform:" head)
(and (= (len parts) 2) (= head "normal") (= (first rest) "case"))
"text-transform:none"
(and
(= head "font")
(= (len rest) 1)
(not (nil? (get tw-weights (first rest)))))
(str "font-weight:" (get tw-weights (first rest)))
(and
(= head "font")
(= (len rest) 1)
(not (nil? (get tw-families (first rest)))))
(str "font-family:" (get tw-families (first rest)))
(and (= (len parts) 1) (= head "italic"))
"font-style:italic"
(and (= (len parts) 2) (= head "not") (= (first rest) "italic"))
"font-style:normal"
(and (= head "leading") (= (len rest) 1))
(let
((val (get tw-leading (first rest))))
(if (nil? val) nil (str "line-height:" val)))
(and (= head "tracking") (= (len rest) 1))
(let
((val (get tw-tracking (first rest))))
(if (nil? val) nil (str "letter-spacing:" val)))
(and (= head "whitespace") (= (len rest) 1))
(case
(first rest)
"normal"
"white-space:normal"
"nowrap"
"white-space:nowrap"
"pre"
"white-space:pre"
"pre-line"
"white-space:pre-line"
"pre-wrap"
"white-space:pre-wrap"
"break-spaces"
"white-space:break-spaces"
:else nil)
(and (= head "whitespace") (= (len rest) 2))
(let
((val (join "-" rest)))
(case
val
"pre-line"
"white-space:pre-line"
"pre-wrap"
"white-space:pre-wrap"
"break-spaces"
"white-space:break-spaces"
:else nil))
(and (= head "break") (= (len rest) 1))
(case
(first rest)
"normal"
"overflow-wrap:normal;word-break:normal"
"words"
"overflow-wrap:break-word"
"all"
"word-break:break-all"
"keep"
"word-break:keep-all"
:else nil)
(and (= (len parts) 1) (= head "truncate"))
"overflow:hidden;text-overflow:ellipsis;white-space:nowrap"
(and (= head "line") (= (len rest) 2) (= (first rest) "clamp"))
(let
((val (nth rest 1)))
(if
(= val "none")
"overflow:visible;display:block;-webkit-line-clamp:unset"
(let
((n (parse-int val nil)))
(if
(nil? n)
nil
(str
"overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:"
n)))))
(and (= head "indent") (= (len rest) 1))
(let
((v (tw-spacing-value (first rest))))
(if (nil? v) nil (str "text-indent:" v)))
(and (= head "align") (= (len rest) 1))
(case
(first rest)
"baseline"
"vertical-align:baseline"
"top"
"vertical-align:top"
"middle"
"vertical-align:middle"
"bottom"
"vertical-align:bottom"
"text-top"
"vertical-align:text-top"
"text-bottom"
"vertical-align:text-bottom"
"sub"
"vertical-align:sub"
"super"
"vertical-align:super"
:else nil)
(and (= head "align") (= (len rest) 2))
(let
((val (join "-" rest)))
(case
val
"text-top"
"vertical-align:text-top"
"text-bottom"
"vertical-align:text-bottom"
:else nil))
(and (= head "list") (= (len rest) 1))
(case
(first rest)
"none"
"list-style-type:none"
"disc"
"list-style-type:disc"
"decimal"
"list-style-type:decimal"
"inside"
"list-style-position:inside"
"outside"
"list-style-position:outside"
:else nil)
(and
(= head "text")
(= (len rest) 1)
(or
(= (first rest) "wrap")
(= (first rest) "nowrap")
(= (first rest) "balance")
(= (first rest) "pretty")))
(str "text-wrap:" (first rest))
(and (= head "hyphens") (= (len rest) 1))
(str "hyphens:" (first rest))
(and (= head "content") (= (len rest) 1) (= (first rest) "none"))
"content:none"
(and (= (len parts) 1) (= head "antialiased"))
"-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale"
(and
(= (len parts) 2)
(= head "subpixel")
(= (first rest) "antialiased"))
"-webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto"
(and (= (len parts) 2) (= (first rest) "nums"))
(case
head
"tabular"
"font-variant-numeric:tabular-nums"
"proportional"
"font-variant-numeric:proportional-nums"
"lining"
"font-variant-numeric:lining-nums"
"oldstyle"
"font-variant-numeric:oldstyle-nums"
:else nil)
(and (= (len parts) 2) (= (first rest) "fractions"))
(case
head
"diagonal"
"font-variant-numeric:diagonal-fractions"
"stacked"
"font-variant-numeric:stacked-fractions"
:else nil)
(and (= (len parts) 2) (= head "normal") (= (first rest) "nums"))
"font-variant-numeric:normal"
(and (= (len parts) 1) (= head "ordinal"))
"font-variant-numeric:ordinal"
(and (= (len parts) 2) (= head "slashed") (= (first rest) "zero"))
"font-variant-numeric:slashed-zero"
:else nil))))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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};
}
(globalThis))
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["sx-319ce79b",[2]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,4]],["dune__exe__Sx_browser-8ae21d0a",[2,3,5]],["std_exit-10fb8830",[2]],["start-80fdb768",0]],"generated":(b=>{var
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["sx-bd388764",[2]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,4]],["dune__exe__Sx_browser-d5ae75e7",[2,3,5]],["std_exit-10fb8830",[2]],["start-80fdb768",0]],"generated":(b=>{var
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_exn_with_js_backtrace not implemented")},"caml_int64_create_lo_mi_hi":()=>{throw new

View File

@@ -0,0 +1,56 @@
(define
protocols-nav-items
(list
(dict
:label "Wire Format"
:href "/sx/(applications.(protocol.wire-format))")
(dict :label "Fragments" :href "/sx/(applications.(protocol.fragments))")
(dict
:label "Resolver I/O"
:href "/sx/(applications.(protocol.resolver-io))")
(dict
:label "Internal Services"
:href "/sx/(applications.(protocol.internal-services))")
(dict
:label "ActivityPub"
:href "/sx/(applications.(protocol.activitypub))")
(dict :label "Future" :href "/sx/(applications.(protocol.future))")))
(define
cssx-nav-items
(list
(dict :label "Overview" :href "/sx/(applications.(cssx))")
(dict :label "Patterns" :href "/sx/(applications.(cssx.patterns))")
(dict :label "Delivery" :href "/sx/(applications.(cssx.delivery))")
(dict :label "Async CSS" :href "/sx/(applications.(cssx.async))")
(dict :label "Live Styles" :href "/sx/(applications.(cssx.live))")
(dict :label "Comparisons" :href "/sx/(applications.(cssx.comparison))")
(dict :label "Philosophy" :href "/sx/(applications.(cssx.philosophy))")))
(define
reactive-runtime-nav-items
(list
(dict :label "Ref" :href "/sx/(applications.(reactive-runtime.ref))")
(dict
:label "Foreign FFI"
:href "/sx/(applications.(reactive-runtime.foreign))")
(dict
:label "State Machines"
:href "/sx/(applications.(reactive-runtime.machine))")
(dict
:label "Commands"
:href "/sx/(applications.(reactive-runtime.commands))")
(dict
:label "Render Loop"
:href "/sx/(applications.(reactive-runtime.loop))")
(dict
:label "Keyed Lists"
:href "/sx/(applications.(reactive-runtime.keyed-lists))")
(dict
:label "App Shell"
:href "/sx/(applications.(reactive-runtime.app-shell))")))
(define
native-browser-nav-items
(list
(dict :label "Native Browser" :href "/sx/(applications.(native-browser))")))

View File

@@ -0,0 +1,816 @@
(define
docs-nav-items
(list
(dict :label "Introduction" :href "/sx/(language.(doc.introduction))")
(dict
:label "Getting Started"
:href "/sx/(language.(doc.getting-started))")
(dict :label "Components" :href "/sx/(language.(doc.components))")
(dict :label "Evaluator" :href "/sx/(language.(doc.evaluator))")
(dict :label "Primitives" :href "/sx/(language.(doc.primitives))")
(dict :label "Special Forms" :href "/sx/(language.(doc.special-forms))")
(dict
:label "Server Rendering"
:href "/sx/(language.(doc.server-rendering))")))
(define
reference-nav-items
(list
(dict
:label "Attributes"
:href "/sx/(geography.(hypermedia.(reference.attributes)))")
(dict
:label "Headers"
:href "/sx/(geography.(hypermedia.(reference.headers)))")
(dict
:label "Events"
:href "/sx/(geography.(hypermedia.(reference.events)))")
(dict
:label "JS API"
:href "/sx/(geography.(hypermedia.(reference.js-api)))")))
(define
protocols-nav-items
(list
(dict
:label "Wire Format"
:href "/sx/(applications.(protocol.wire-format))")
(dict :label "Fragments" :href "/sx/(applications.(protocol.fragments))")
(dict
:label "Resolver I/O"
:href "/sx/(applications.(protocol.resolver-io))")
(dict
:label "Internal Services"
:href "/sx/(applications.(protocol.internal-services))")
(dict
:label "ActivityPub"
:href "/sx/(applications.(protocol.activitypub))")
(dict :label "Future" :href "/sx/(applications.(protocol.future))")))
(define
examples-nav-items
(list
(dict
:label "Click to Load"
:href "/sx/(geography.(hypermedia.(example.click-to-load)))")
(dict
:label "Form Submission"
:href "/sx/(geography.(hypermedia.(example.form-submission)))")
(dict
:label "Polling"
:href "/sx/(geography.(hypermedia.(example.polling)))")
(dict
:label "Delete Row"
:href "/sx/(geography.(hypermedia.(example.delete-row)))")
(dict
:label "Inline Edit"
:href "/sx/(geography.(hypermedia.(example.inline-edit)))")
(dict
:label "OOB Swaps"
:href "/sx/(geography.(hypermedia.(example.oob-swaps)))")
(dict
:label "Lazy Loading"
:href "/sx/(geography.(hypermedia.(example.lazy-loading)))")
(dict
:label "Infinite Scroll"
:href "/sx/(geography.(hypermedia.(example.infinite-scroll)))")
(dict
:label "Progress Bar"
:href "/sx/(geography.(hypermedia.(example.progress-bar)))")
(dict
:label "Active Search"
:href "/sx/(geography.(hypermedia.(example.active-search)))")
(dict
:label "Inline Validation"
:href "/sx/(geography.(hypermedia.(example.inline-validation)))")
(dict
:label "Value Select"
:href "/sx/(geography.(hypermedia.(example.value-select)))")
(dict
:label "Reset on Submit"
:href "/sx/(geography.(hypermedia.(example.reset-on-submit)))")
(dict
:label "Edit Row"
:href "/sx/(geography.(hypermedia.(example.edit-row)))")
(dict
:label "Bulk Update"
:href "/sx/(geography.(hypermedia.(example.bulk-update)))")
(dict
:label "Swap Positions"
:href "/sx/(geography.(hypermedia.(example.swap-positions)))")
(dict
:label "Select Filter"
:href "/sx/(geography.(hypermedia.(example.select-filter)))")
(dict :label "Tabs" :href "/sx/(geography.(hypermedia.(example.tabs)))")
(dict
:label "Animations"
:href "/sx/(geography.(hypermedia.(example.animations)))")
(dict
:label "Dialogs"
:href "/sx/(geography.(hypermedia.(example.dialogs)))")
(dict
:label "Keyboard Shortcuts"
:href "/sx/(geography.(hypermedia.(example.keyboard-shortcuts)))")
(dict
:label "PUT / PATCH"
:href "/sx/(geography.(hypermedia.(example.put-patch)))")
(dict
:label "JSON Encoding"
:href "/sx/(geography.(hypermedia.(example.json-encoding)))")
(dict
:label "Vals & Headers"
:href "/sx/(geography.(hypermedia.(example.vals-and-headers)))")
(dict
:label "Loading States"
:href "/sx/(geography.(hypermedia.(example.loading-states)))")
(dict
:label "Request Abort"
:href "/sx/(geography.(hypermedia.(example.sync-replace)))")
(dict :label "Retry" :href "/sx/(geography.(hypermedia.(example.retry)))")))
(define
cssx-nav-items
(list
(dict :label "Overview" :href "/sx/(applications.(cssx))")
(dict :label "Patterns" :href "/sx/(applications.(cssx.patterns))")
(dict :label "Delivery" :href "/sx/(applications.(cssx.delivery))")
(dict :label "Async CSS" :href "/sx/(applications.(cssx.async))")
(dict :label "Live Styles" :href "/sx/(applications.(cssx.live))")
(dict :label "Comparisons" :href "/sx/(applications.(cssx.comparison))")
(dict :label "Philosophy" :href "/sx/(applications.(cssx.philosophy))")))
(define
reactive-runtime-nav-items
(list
(dict :label "Ref" :href "/sx/(geography.(reactive-runtime.ref))")
(dict
:label "Foreign FFI"
:href "/sx/(geography.(reactive-runtime.foreign))")
(dict
:label "State Machines"
:href "/sx/(geography.(reactive-runtime.machine))")
(dict
:label "Commands"
:href "/sx/(geography.(reactive-runtime.commands))")
(dict
:label "Render Loop"
:href "/sx/(geography.(reactive-runtime.loop))")
(dict
:label "Keyed Lists"
:href "/sx/(geography.(reactive-runtime.keyed-lists))")
(dict
:label "App Shell"
:href "/sx/(geography.(reactive-runtime.app-shell))")))
(define
native-browser-nav-items
(list
(dict :label "Native Browser" :href "/sx/(applications.(native-browser))")))
(define
essays-nav-items
(list
(dict
:label "Why S-Expressions"
:href "/sx/(etc.(essay.why-sexps))"
:summary "Why SX uses s-expressions instead of HTML templates, JSX, or any other syntax.")
(dict
:label "The htmx/React Hybrid"
:href "/sx/(etc.(essay.htmx-react-hybrid))"
:summary "How SX combines the server-driven simplicity of htmx with the component model of React.")
(dict
:label "On-Demand CSS"
:href "/sx/(etc.(essay.on-demand-css))"
:summary "How SX delivers only the CSS each page needs — server scans rendered classes, sends the delta.")
(dict
:label "Client Reactivity"
:href "/sx/(etc.(essay.client-reactivity))"
:summary "Reactive UI updates without a virtual DOM, diffing library, or build step.")
(dict
:label "SX Native"
:href "/sx/(etc.(essay.sx-native))"
:summary "Extending SX beyond the browser — native desktop and mobile rendering from the same source.")
(dict
:label "Tail-Call Optimization"
:href "/sx/(etc.(essay.tail-call-optimization))"
:summary "How SX implements proper tail calls via trampolining in a language that doesn't have them.")
(dict
:label "Continuations"
:href "/sx/(etc.(essay.continuations))"
:summary "First-class continuations in a tree-walking evaluator — theory and implementation.")
(dict
:label "The Reflexive Web"
:href "/sx/(etc.(essay.reflexive-web))"
:summary "A web where pages can inspect, modify, and extend their own rendering pipeline.")
(dict
:label "Server Architecture"
:href "/sx/(etc.(essay.server-architecture))"
:summary "How SX enforces the boundary between host and embedded language, and what it looks like across targets.")
(dict
:label "Separate your Own Concerns"
:href "/sx/(etc.(essay.separation-of-concerns))"
:summary "The web's HTML/CSS/JS split separates the framework's concerns, not your application's. Real separation is domain-specific.")
(dict
:label "SX and AI"
:href "/sx/(etc.(essay.sx-and-ai))"
:summary "Why s-expressions are the most AI-friendly representation for web interfaces.")
(dict
:label "There Is No Alternative"
:href "/sx/(etc.(essay.no-alternative))"
:summary "Every attempt to escape s-expressions leads back to s-expressions. This is not an accident.")
(dict
:label "sx sucks"
:href "/sx/(etc.(essay.sx-sucks))"
:summary "An honest accounting of everything wrong with SX and why you probably shouldn't use it.")
(dict
:label "Tools for Fools"
:href "/sx/(etc.(essay.zero-tooling))"
:summary "SX was built without a code editor. No IDE, no build tools, no linters, no bundlers. What zero-tooling web development looks like.")
(dict
:label "React is Hypermedia"
:href "/sx/(etc.(essay.react-is-hypermedia))"
:summary "A React Island is a hypermedia control. Its behavior is specified in SX.")
(dict
:label "The Hegelian Synthesis"
:href "/sx/(etc.(essay.hegelian-synthesis))"
:summary "On the dialectical resolution of the hypertext/reactive contradiction. Thesis: the server renders. Antithesis: the client reacts. Synthesis: the island in the lake.")
(dict
:label "The Art Chain"
:href "/sx/(etc.(essay.the-art-chain))"
:summary "On making, self-making, and the chain of artifacts that produces itself. Ars, techne, content addressing, and why the spec is the art.")
(dict
:label "The True Hypermedium"
:href "/sx/(etc.(essay.self-defining-medium))"
:summary "The true hypermedium must define itself with itself. On ontological uniformity, the metacircular web, and why address and content should be the same stuff.")
(dict
:label "Hypermedia in the Age of AI"
:href "/sx/(etc.(essay.hypermedia-age-of-ai))"
:summary "JSON hypermedia, MCP, and why s-expressions are the format both humans and AI agents actually need.")))
(define
philosophy-nav-items
(list
(dict
:label "The SX Manifesto"
:href "/sx/(etc.(philosophy.sx-manifesto))"
:summary "The design principles behind SX: simplicity, self-hosting, and s-expressions all the way down.")
(dict
:label "Strange Loops"
:href "/sx/(etc.(philosophy.godel-escher-bach))"
:summary "Self-reference, and the tangled hierarchy of a language that defines itself.")
(dict
:label "SX and Wittgenstein"
:href "/sx/(etc.(philosophy.wittgenstein))"
:summary "The limits of my language are the limits of my world — Wittgenstein's philosophy and what it means for SX.")
(dict
:label "SX and Dennett"
:href "/sx/(etc.(philosophy.dennett))"
:summary "Real patterns, intentional stance, and multiple drafts — Dennett's philosophy of mind as a framework for understanding SX.")
(dict
:label "S-Existentialism"
:href "/sx/(etc.(philosophy.existentialism))"
:summary "Existence precedes essence — Sartre, Camus, and the absurd freedom of writing a Lisp for the web.")
(dict
:label "Platonic SX"
:href "/sx/(etc.(philosophy.platonic-sx))"
:summary "The allegory of the cave, the theory of Forms, and why a self-defining hypermedium participates in something Plato would have recognized.")))
(define
specs-nav-items
(list {:href "/sx/(language.(spec.core))" :children (list {:href "/sx/(language.(spec.parser))" :label "Parser"} {:href "/sx/(language.(spec.evaluator))" :label "Evaluator"} {:href "/sx/(language.(spec.primitives))" :label "Primitives"} {:href "/sx/(language.(spec.special-forms))" :label "Special Forms"} {:href "/sx/(language.(spec.renderer))" :label "Renderer"}) :label "Core"} {:href "/sx/(language.(spec.adapters))" :children (list {:href "/sx/(language.(spec.adapter-dom))" :label "DOM Adapter"} {:href "/sx/(language.(spec.adapter-html))" :label "HTML Adapter"} {:href "/sx/(language.(spec.adapter-sx))" :label "SX Wire Adapter"} {:href "/sx/(language.(spec.adapter-async))" :label "Async Adapter"}) :label "Adapters"} {:href "/sx/(language.(spec.browser))" :children (list {:href "/sx/(language.(spec.engine))" :label "SxEngine"} {:href "/sx/(language.(spec.orchestration))" :label "Orchestration"} {:href "/sx/(language.(spec.boot))" :label "Boot"} {:href "/sx/(language.(spec.router))" :label "Router"}) :label "Browser"} {:href "/sx/(language.(spec.reactive))" :children (list {:href "/sx/(language.(spec.signals))" :label "Signals"} {:href "/sx/(language.(spec.frames))" :label "CEK Frames"} {:href "/sx/(language.(spec.cek))" :label "CEK Machine"}) :label "Reactive"} {:href "/sx/(language.(spec.host))" :children (list {:href "/sx/(language.(spec.boundary))" :label "Boundary"} {:href "/sx/(language.(spec.forms))" :label "Forms"} {:href "/sx/(language.(spec.page-helpers))" :label "Page Helpers"}) :label "Host Interface"} {:href "/sx/(language.(spec.extensions))" :children (list {:href "/sx/(language.(spec.continuations))" :label "Continuations"} {:href "/sx/(language.(spec.callcc))" :label "call/cc"} {:href "/sx/(language.(spec.types))" :label "Types"} {:href "/sx/(language.(spec.deps))" :label "Deps"}) :label "Extensions"}))
(define
testing-nav-items
(list
(dict :label "Overview" :href "/sx/(language.(test))")
(dict :label "Evaluator" :href "/sx/(language.(test.eval))")
(dict :label "Parser" :href "/sx/(language.(test.parser))")
(dict :label "Router" :href "/sx/(language.(test.router))")
(dict :label "Renderer" :href "/sx/(language.(test.render))")
(dict :label "Dependencies" :href "/sx/(language.(test.deps))")
(dict :label "Engine" :href "/sx/(language.(test.engine))")
(dict :label "Orchestration" :href "/sx/(language.(test.orchestration))")
(dict :label "Runners" :href "/sx/(language.(test.runners))")))
(define
isomorphism-nav-items
(list
(dict :label "Roadmap" :href "/sx/(geography.(isomorphism))")
(dict
:label "Bundle Analyzer"
:href "/sx/(geography.(isomorphism.bundle-analyzer))")
(dict
:label "Routing Analyzer"
:href "/sx/(geography.(isomorphism.routing-analyzer))")
(dict :label "Data Test" :href "/sx/(geography.(isomorphism.data-test))")
(dict :label "Async IO" :href "/sx/(geography.(isomorphism.async-io))")
(dict :label "Streaming" :href "/sx/(geography.(isomorphism.streaming))")
(dict :label "Affinity" :href "/sx/(geography.(isomorphism.affinity))")
(dict :label "Optimistic" :href "/sx/(geography.(isomorphism.optimistic))")
(dict :label "Offline" :href "/sx/(geography.(isomorphism.offline))")))
(define
cek-nav-items
(list
(dict
:label "Overview"
:href "/sx/(geography.(cek))"
:summary "The CEK machine — explicit evaluator with Control, Environment, Kontinuation. Three registers, pure step function.")
(dict
:label "Demo"
:href "/sx/(geography.(cek.demo))"
:summary "Live islands evaluated by the CEK machine. Counter, computed chains, reactive attributes — all through explicit continuation frames.")
(dict
:label "Freeze / Thaw"
:href "/sx/(geography.(cek.freeze))"
:summary "Serialize a CEK state to s-expressions. Ship it, store it, content-address it. Thaw and resume anywhere.")
(dict
:label "Content Addressing"
:href "/sx/(geography.(cek.content))"
:summary "Hash frozen state to a CID. Same state = same address. Store, share, verify, reproduce.")))
(define
plans-nav-items
(list
(dict
:label "Status"
:href "/sx/(etc.(plan.status))"
:summary "Audit of all plans — what's done, what's in progress, and what remains.")
(dict
:label "Reader Macros"
:href "/sx/(etc.(plan.reader-macros))"
:summary "Extensible parse-time transformations via # dispatch — datum comments, raw strings, and quote shorthand.")
(dict
:label "Reader Macro Demo"
:href "/sx/(etc.(plan.reader-macro-demo))"
:summary "Live demo: #z3 translates SX spec declarations to SMT-LIB verification conditions.")
(dict
:label "Theorem Prover"
:href "/sx/(etc.(plan.theorem-prover))"
:summary "prove.sx — constraint solver and property prover for SX primitives, written in SX.")
(dict
:label "Self-Hosting Bootstrapper"
:href "/sx/(etc.(plan.self-hosting-bootstrapper))"
:summary "py.sx — an SX-to-Python translator written in SX. Complete: G0 == G1, 128/128 defines match.")
(dict
:label "JS Bootstrapper"
:href "/sx/(etc.(plan.js-bootstrapper))"
:summary "js.sx — SX-to-JavaScript translator + ahead-of-time component compiler. Zero-runtime static sites.")
(dict
:label "SX-Activity"
:href "/sx/(etc.(plan.sx-activity))"
:summary "A new web built on SX — executable content, shared components, parsers, and logic on IPFS, provenance on Bitcoin, all running within your own security context.")
(dict
:label "Predictive Prefetching"
:href "/sx/(etc.(plan.predictive-prefetch))"
:summary "Prefetch missing component definitions before the user clicks — hover a link, fetch its deps, navigate client-side.")
(dict
:label "Content-Addressed Components"
:href "/sx/(etc.(plan.content-addressed-components))"
:summary "Components identified by CID, stored on IPFS, fetched from anywhere. Canonical serialization, content verification, federated sharing.")
(dict
:label "Environment Images"
:href "/sx/(etc.(plan.environment-images))"
:summary "Serialize evaluated environments as content-addressed images. Spec CID → image CID → every endpoint is fully executable and verifiable.")
(dict
:label "Runtime Slicing"
:href "/sx/(etc.(plan.runtime-slicing))"
:summary "Tier the client runtime by need: L0 hypermedia (~5KB), L1 DOM ops (~8KB), L2 islands (~15KB), L3 full eval (~44KB). Sliced by slice.sx, translated by js.sx.")
(dict
:label "Typed SX"
:href "/sx/(etc.(plan.typed-sx))"
:summary "Gradual type system with static effect checking. Optional type annotations, deftype (aliases, unions, records), and effect declarations — checked at registration time, zero runtime cost. types.sx — specced, bootstrapped, catches composition and boundary errors.")
(dict
:label "Nav Redesign"
:href "/sx/(etc.(plan.nav-redesign))"
:summary "Replace menu bars with vertical breadcrumb navigation. Logo → section → page, arrows for siblings, children below. No dropdowns, no hamburger, infinite depth.")
(dict
:label "Fragment Protocol"
:href "/sx/(etc.(plan.fragment-protocol))"
:summary "Structured sexp request/response for cross-service component transfer.")
(dict
:label "Glue Decoupling"
:href "/sx/(etc.(plan.glue-decoupling))"
:summary "Eliminate all cross-app model imports via glue service layer.")
(dict
:label "Social Sharing"
:href "/sx/(etc.(plan.social-sharing))"
:summary "OAuth-based sharing to Facebook, Instagram, Threads, Twitter/X, LinkedIn, and Mastodon.")
(dict
:label "SX CI Pipeline"
:href "/sx/(etc.(plan.sx-ci))"
:summary "Build, test, and deploy in s-expressions — CI pipelines as SX components.")
(dict
:label "Live Streaming"
:href "/sx/(etc.(plan.live-streaming))"
:summary "SSE and WebSocket transports for re-resolving suspense slots after initial page load — live data, real-time collaboration.")
(dict
:label "sx-web Platform"
:href "/sx/(etc.(plan.sx-web-platform))"
:summary "sx-web.org as online development platform — embedded Claude Code, IPFS storage, sx-activity publishing, sx-ci testing. Author, stage, test, deploy from the browser.")
(dict
:label "sx-forge"
:href "/sx/(etc.(plan.sx-forge))"
:summary "Git forge in SX — repositories, issues, pull requests, CI, permissions, and federation. Configuration as macros, diffs as components.")
(dict
:label "sx-swarm"
:href "/sx/(etc.(plan.sx-swarm))"
:summary "Container orchestration in SX — service definitions, environment macros, deploy pipelines. Replace YAML with a real language.")
(dict
:label "sx-proxy"
:href "/sx/(etc.(plan.sx-proxy))"
:summary "Reverse proxy in SX — routes, TLS, middleware chains, load balancing. Macros generate config from the same service definitions as the orchestrator.")
(dict
:label "Async Eval Convergence"
:href "/sx/(etc.(plan.async-eval-convergence))"
:summary "Eliminate hand-written evaluators — bootstrap async_eval.py from the spec via an async adapter layer. One spec, one truth, zero divergence.")
(dict
:label "WASM Bytecode VM"
:href "/sx/(etc.(plan.wasm-bytecode-vm))"
:summary "Compile SX to bytecode, run in a Rust/WASM VM. Compact wire format, no parse overhead, near-native speed, DOM via JS bindings.")
(dict
:label "Generative SX"
:href "/sx/(etc.(plan.generative-sx))"
:summary "Programs that write themselves as they run — self-compiling specs, runtime self-extension, generative testing, seed networks.")
(dict
:label "Art DAG on SX"
:href "/sx/(etc.(plan.art-dag-sx))"
:summary "SX endpoints as portals into media processing environments — recipes as programs, split execution across GPU/cache/live boundaries, streaming AV output.")
(dict
:label "Spec Explorer"
:href "/sx/(etc.(plan.spec-explorer))"
:summary "The fifth ring — SX exploring itself. Per-function cards showing source, Python/JS/Z3 translations, platform dependencies, tests, proofs, and usage examples.")
(dict
:label "SX Protocol"
:href "/sx/(etc.(plan.sx-protocol))"
:summary "S-expressions as a universal protocol for networked hypermedia — replacing URLs, HTTP verbs, query languages, and rendering with one evaluable format.")
(dict
:label "Scoped Effects"
:href "/sx/(etc.(plan.scoped-effects))"
:summary "Algebraic effects as the unified foundation — spreads, islands, lakes, signals, and context are all instances of one primitive: a named scope with downward value, upward accumulation, and a propagation mode.")
(dict
:label "Foundations"
:href "/sx/(etc.(plan.foundations))"
:summary "The computational floor — from scoped effects through algebraic effects and delimited continuations to the CEK machine. Why three registers are irreducible, and the three-axis model (depth, topology, linearity).")
(dict
:label "Deref as Shift"
:href "/sx/(etc.(plan.cek-reactive))"
:summary "Phase B: replace explicit effect wrapping with implicit continuation capture. Deref inside reactive-reset performs shift, capturing the rest of the expression as the subscriber.")
(dict
:label "Rust/WASM Host"
:href "/sx/(etc.(plan.rust-wasm-host))"
:summary "Bootstrap the SX spec to Rust, compile to WASM, replace sx-browser.js. Shared platform layer for DOM, phased rollout from parse to full parity.")
(dict
:label "Isolated Evaluator"
:href "/sx/(etc.(plan.isolated-evaluator))"
:summary "Core/application split, shared sx-platform.js, isolated JS evaluator, Rust WASM via handle table. Only language-defining spec gets bootstrapped; everything else is runtime-evaluated .sx.")
(dict
:label "Mother Language"
:href "/sx/(etc.(plan.mother-language))"
:summary "SX as its own compiler. OCaml as substrate (closest to CEK), Koka as alternative (compile-time linearity), ultimately self-hosting. One language, every target.")
(dict
:label "sx-web"
:href "/sx/(etc.(plan.sx-web))"
:summary "Federated component web. Browser nodes via WebTransport, server nodes via IPFS, content-addressed SX verified by CID. In-browser editing, testing, publishing. AI composition over the federated graph.")
(dict
:label "sx-host"
:href "/sx/(etc.(plan.sx-host))"
:summary "Universal platform primitives.")))
(define
reactive-examples-nav-items
(list
{:href "/sx/(geography.(reactive.(examples.counter)))" :label "Counter"}
{:href "/sx/(geography.(reactive.(examples.temperature)))" :label "Temperature"}
{:href "/sx/(geography.(reactive.(examples.stopwatch)))" :label "Stopwatch"}
{:href "/sx/(geography.(reactive.(examples.imperative)))" :label "Imperative"}
{:href "/sx/(geography.(reactive.(examples.reactive-list)))" :label "Reactive List"}
{:href "/sx/(geography.(reactive.(examples.input-binding)))" :label "Input Binding"}
{:href "/sx/(geography.(reactive.(examples.portal)))" :label "Portals"}
{:href "/sx/(geography.(reactive.(examples.error-boundary)))" :label "Error Boundary"}
{:href "/sx/(geography.(reactive.(examples.refs)))" :label "Refs"}
{:href "/sx/(geography.(reactive.(examples.dynamic-class)))" :label "Dynamic Class"}
{:href "/sx/(geography.(reactive.(examples.resource)))" :label "Resource"}
{:href "/sx/(geography.(reactive.(examples.transition)))" :label "Transitions"}
{:href "/sx/(geography.(reactive.(examples.stores)))" :label "Stores"}
{:href "/sx/(geography.(reactive.(examples.event-bridge-demo)))" :label "Event Bridge"}
{:href "/sx/(geography.(reactive.(examples.defisland)))" :label "defisland"}
{:href "/sx/(geography.(reactive.(examples.tests)))" :label "Tests"}
{:href "/sx/(geography.(reactive.(examples.coverage)))" :label "Coverage"}
{:href "/sx/(geography.(reactive.(examples.cyst)))" :label "Cyst"}
{:href "/sx/(geography.(reactive.(examples.reactive-expressions)))" :label "Reactive Expressions"}))
(define
reactive-islands-nav-items
(list
(dict
:label "Examples"
:href "/sx/(geography.(reactive.(examples)))"
:summary "Live interactive islands — click the buttons, type in the inputs."
:children reactive-examples-nav-items)))
(define
marshes-examples-nav-items
(list {:href "/sx/(geography.(marshes.hypermedia-feeds))" :label "Hypermedia Feeds State"} {:href "/sx/(geography.(marshes.server-signals))" :label "Server Writes to Signals"} {:href "/sx/(geography.(marshes.on-settle))" :label "sx-on-settle"} {:href "/sx/(geography.(marshes.signal-triggers))" :label "Signal-Bound Triggers"} {:href "/sx/(geography.(marshes.view-transform))" :label "Reactive View Transform"}))
(define
bootstrappers-nav-items
(list
(dict :label "Overview" :href "/sx/(language.(bootstrapper))")
(dict :label "JavaScript" :href "/sx/(language.(bootstrapper.javascript))")
(dict :label "Python" :href "/sx/(language.(bootstrapper.python))")
(dict
:label "Self-Hosting (py.sx)"
:href "/sx/(language.(bootstrapper.self-hosting))")
(dict
:label "Self-Hosting JS (js.sx)"
:href "/sx/(language.(bootstrapper.self-hosting-js))")
(dict
:label "Page Helpers"
:href "/sx/(language.(bootstrapper.page-helpers))")))
(define
core-spec-items
(list
(dict
:slug "parser"
:filename "parser.sx"
:title "Parser"
:desc "Tokenization and parsing of SX source text into AST."
:prose "The parser converts SX source text into an abstract syntax tree. It tokenizes the input into atoms, strings, numbers, keywords, and delimiters, then assembles them into nested list structures. The parser is intentionally minimal — s-expressions need very little syntax to parse. Special reader macros handle quasiquote (\\`), unquote (~), splice (~@), and the quote (') shorthand. The output is a tree of plain lists, symbols, keywords, strings, and numbers that the evaluator can walk directly.")
(dict
:slug "evaluator"
:filename "evaluator.sx"
:title "Evaluator"
:desc "CEK machine evaluator."
:prose "The evaluator walks the AST produced by the parser and reduces it to values. It implements lexical scoping with closures, special forms (define, let, if, cond, fn, defcomp, defmacro, quasiquote, set!, do), and function application. Macros are expanded at eval time. Component definitions (defcomp) create callable component objects that participate in the rendering pipeline. The evaluator delegates rendering expressions — HTML tags, components, fragments — to whichever adapter is active, making the same source renderable to DOM nodes, HTML strings, or SX wire format.")
(dict
:slug "primitives"
:filename "primitives.sx"
:title "Primitives"
:desc "All built-in pure functions and their signatures."
:prose "Primitives are the built-in functions available in every SX environment. Each entry declares a name, parameter signature, and semantics. Bootstrap compilers implement these natively per target (JavaScript, Python, etc.). The registry covers arithmetic, comparison, string manipulation, list operations, dict operations, type predicates, and control flow helpers. All primitives are pure — they take values and return values with no side effects. Platform-specific operations (DOM access, HTTP, file I/O) are provided separately via platform bridge functions, not primitives.")
(dict
:slug "special-forms"
:filename "special-forms.sx"
:title "Special Forms"
:desc "All special forms — syntactic constructs with custom evaluation rules."
:prose "Special forms are the syntactic constructs whose arguments are NOT evaluated before dispatch. Each form has its own evaluation rules — unlike primitives, which receive pre-evaluated values. Together with primitives, special forms define the complete language surface. The registry covers control flow (if, when, cond, case, and, or), binding (let, letrec, define, set!), functions (lambda, defcomp, defmacro), sequencing (begin, do, thread-first), quoting (quote, quasiquote), continuations (reset, shift), guards (dynamic-wind), higher-order forms (map, filter, reduce), and domain-specific definitions (defstyle, defhandler, defpage, defquery, defaction).")
(dict
:slug "renderer"
:filename "render.sx"
:title "Renderer"
:desc "Shared rendering registries and utilities used by all adapters."
:prose "The renderer defines what is renderable and how arguments are parsed, but not the output format. It maintains registries of known HTML tags, SVG tags, void elements, and boolean attributes. It specifies how keyword arguments on elements become HTML attributes, how children are collected, and how special attributes (class, style, data-*) are handled. All three adapters (DOM, HTML, SX wire) share these definitions so they agree on what constitutes valid markup.")))
(define
adapter-spec-items
(list
(dict
:slug "adapter-dom"
:filename "adapter-dom.sx"
:title "DOM Adapter"
:desc "Renders SX expressions to live DOM nodes. Browser-only."
:prose "The DOM adapter renders evaluated SX expressions into live browser DOM nodes — Elements, Text nodes, and DocumentFragments. It mirrors the HTML adapter's logic but produces DOM objects instead of strings. This is the adapter used by the browser-side SX runtime for initial mount, hydration, and dynamic updates. It handles element creation, attribute setting (including event handlers and style objects), SVG namespace handling, and fragment composition.")
(dict
:slug "adapter-html"
:filename "adapter-html.sx"
:title "HTML Adapter"
:desc "Renders SX expressions to HTML strings. Server-side."
:prose "The HTML adapter renders evaluated SX expressions to HTML strings. It is used server-side to produce complete HTML pages and fragments. It handles void elements (self-closing tags like <br>, <img>), boolean attributes, style serialization, class merging, and proper escaping. The output is standard HTML5 that any browser can parse.")
(dict
:slug "adapter-sx"
:filename "adapter-sx.sx"
:title "SX Wire Adapter"
:desc "Serializes SX for client-side rendering. Component calls stay unexpanded."
:prose "The SX wire adapter serializes expressions as SX source text for transmission to the browser, where sx.js renders them client-side. Unlike the HTML adapter, component calls (~plans/content-addressed-components/name ...) are NOT expanded — they are sent to the client as-is, allowing the browser to render them with its local component registry. HTML tags ARE serialized as s-expression source. This is the format used for SX-over-HTTP responses and the page boot payload.")
(dict
:slug "adapter-async"
:filename "adapter-async.sx"
:title "Async Adapter"
:desc "Async versions of HTML and SX wire adapters for server-side rendering with I/O."
:prose "The async adapter provides async-aware versions of the HTML and SX wire rendering functions. It intercepts I/O operations (database queries, service calls, fragment fetches) during evaluation, awaiting them before continuing. Entry points: async-render (HTML output with awaited I/O), async-aser (SX wire format with awaited I/O). The bootstrapper emits async def and automatic await insertion for all define-async functions. This adapter is what makes server-side SX pages work with real data.")))
(define
browser-spec-items
(list
(dict
:slug "engine"
:filename "engine.sx"
:title "SxEngine"
:desc "Pure logic for fetch, swap, history, SSE, triggers, morph, and indicators."
:prose "The engine specifies the pure logic of the browser-side fetch/swap/history system. Like HTMX but native to SX. It defines trigger parsing (click, submit, intersect, poll, load, revealed), swap algorithms (innerHTML, outerHTML, morph, beforebegin, etc.), the morph/diff algorithm for patching existing DOM, history management (push-url, replace-url, popstate), out-of-band swap identification, Server-Sent Events parsing, retry logic with exponential backoff, request header building, response header processing, and optimistic UI updates. This file contains no browser API calls — all platform interaction is in orchestration.sx.")
(dict
:slug "orchestration"
:filename "orchestration.sx"
:title "Orchestration"
:desc "Browser wiring that binds engine logic to DOM events, fetch, and lifecycle."
:prose "Orchestration is the browser wiring layer. It binds the pure engine logic to actual browser APIs: DOM event listeners, fetch(), AbortController, setTimeout/setInterval, IntersectionObserver, history.pushState, and EventSource (SSE). It implements the full request lifecycle — from trigger through fetch through swap — including CSS tracking, response type detection (SX vs HTML), OOB swap processing, script activation, element boosting, and preload. Dependency is strictly one-way: orchestration depends on engine, never the reverse.")
(dict
:slug "boot"
:filename "boot.sx"
:title "Boot"
:desc "Browser startup lifecycle: mount, hydrate, script processing."
:prose "Boot handles the browser startup sequence and provides the public API for mounting SX content. On page load it: (1) initializes CSS tracking, (2) processes <script type=\"text/sx\"> tags (component definitions and mount directives), (3) hydrates [data-sx] elements, and (4) activates the engine on all elements. It also provides the public mount/hydrate/update/render-component API, and the head element hoisting logic that moves <meta>, <title>, and <link> tags from rendered content into <head>.")
(dict
:slug "router"
:filename "router.sx"
:title "Router"
:desc "Client-side route matching — Flask-style pattern parsing, segment matching, route table search."
:prose "The router module provides pure functions for matching URL paths against Flask-style route patterns (e.g. /docs/<slug>). Used by client-side routing to determine if a page can be rendered locally without a server roundtrip. split-path-segments breaks a path into segments, parse-route-pattern converts patterns into typed segment descriptors, match-route-segments tests a path against a parsed pattern returning extracted params, and find-matching-route searches a route table for the first match.")))
(define
reactive-spec-items
(list
(dict
:slug "signals"
:filename "signals.sx"
:title "Signals"
:desc "Fine-grained reactive primitives — signal, computed, effect, batch."
:prose "The signals module defines a fine-grained reactive system for client-side islands. Signals are containers for values that notify subscribers on change. Computed signals derive values lazily from other signals. Effects run side-effects when their dependencies change, with automatic cleanup. Batch coalesces multiple signal writes into a single notification pass. Island scope management ensures all signals, computeds, and effects are cleaned up when an island is removed from the DOM. The spec defines the reactive graph topology and update algorithm — each platform implements the actual signal/tracking types natively.")
(dict
:slug "frames"
:filename "frames.sx"
:title "CEK Frames"
:desc "Continuation frame types for the explicit CEK machine."
:prose "Frames define what to do next when a sub-evaluation completes. Each frame type is a dict with a type key and frame-specific data. IfFrame, WhenFrame, BeginFrame handle control flow. LetFrame, DefineFrame, SetFrame handle bindings. ArgFrame tracks function call argument evaluation. MapFrame, FilterFrame, ReduceFrame, ForEachFrame drive higher-order forms element by element through the CEK machine. ReactiveResetFrame and DerefFrame enable deref-as-shift — the core reactive mechanism where deref inside a reactive boundary captures the continuation as a signal subscriber.")
(dict
:slug "cek"
:filename "cek.sx"
:title "CEK Machine"
:desc "Explicit CEK machine evaluator — step function, run loop, reactive shift."
:prose "The CEK machine makes evaluation explicit. Every step is a pure function from state to state: Control (expression), Environment (bindings), Kontinuation (stack of frames). step-eval dispatches on expression type — literals pass through, symbols are looked up, lists dispatch on head (special forms, higher-order forms, macros, function calls). step-continue dispatches on the top frame type. Higher-order forms (map, filter, reduce, for-each, some, every?) step element by element, so deref-as-shift works inside callbacks. cek-call replaces invoke as the universal function dispatch — lambdas go through cek-run, native callables through apply.")))
(define
host-spec-items
(list
(dict
:slug "boundary"
:filename "boundary.sx"
:title "Boundary"
:desc "Language boundary contract — declares I/O primitives the host must provide."
:prose "The boundary defines the contract between SX and its host environment. Tier 1 declares pure primitives (from primitives.sx). Tier 2 declares async I/O primitives the host must implement: fetch, async-eval, call-action, send-activity, and other operations that require network or database access. Tier 3 declares page helpers: format, highlight, scan-css-classes, parse-datetime. This is the interface every host must satisfy to run SX — framework-agnostic, universal to all targets. Boundary enforcement validates at registration time that all declared primitives are provided.")
(dict
:slug "forms"
:filename "forms.sx"
:title "Forms"
:desc "Server-side definition forms — defhandler, defquery, defaction, defpage."
:prose "Forms defines the server-side definition macros that compose the application layer. defhandler registers an HTTP route handler. defquery defines a read-only data source. defaction defines a mutation (write). defpage declares a client-routable page with path, auth, layout, data dependencies, and content. Each form parses &key parameter lists and creates typed definition objects. Platform-specific constructors are provided by the host — these have different bindings on server (Python/Quart) vs client (route matching only).")
(dict
:slug "page-helpers"
:filename "page-helpers.sx"
:title "Page Helpers"
:desc "Pure data-transformation helpers for page rendering."
:prose "Page helpers are pure functions that assist page rendering: categorizing special forms by type, formatting numbers and dates, highlighting code, scanning CSS classes, constructing page titles and descriptions. Unlike boundary I/O primitives, these are pure — they take data and return data with no side effects. They run identically on server and client. The host registers native implementations that match these declarations.")))
(define
extension-spec-items
(list
(dict
:slug "continuations"
:filename "continuations.sx"
:title "Continuations"
:desc "Delimited continuations — shift/reset for suspendable rendering and cooperative scheduling."
:prose "Delimited continuations capture the rest of a computation up to a delimiter. shift captures the continuation to the nearest reset as a first-class callable value. Unlike full call/cc, delimited continuations are composable — invoking one returns a value. This covers the practical use cases: suspendable server rendering, cooperative scheduling, linear async flows, wizard-style multi-step UIs, and undo. Each bootstrapper target implements the mechanism differently — generators in Python/JS, native shift/reset in Scheme, ContT in Haskell, CPS transform in Rust — but the semantics are identical. Optional extension: code that doesn't use continuations pays zero cost.")
(dict
:slug "callcc"
:filename "callcc.sx"
:title "call/cc"
:desc "Full first-class continuations — call-with-current-continuation."
:prose "Full call/cc captures the entire remaining computation as a first-class function — not just up to a delimiter, but all the way to the top level. Invoking the continuation abandons the current computation entirely and resumes from where it was captured. Strictly more powerful than delimited continuations, but harder to implement in targets that don't support it natively. Recommended for Scheme and Haskell targets where it's natural. Python, JavaScript, and Rust targets should prefer delimited continuations (continuations.sx) unless full escape semantics are genuinely needed. Optional extension: the continuation type is shared with continuations.sx if both are loaded.")
(dict
:slug "types"
:filename "types.sx"
:title "Types"
:desc "Gradual type system — registration-time checking with zero runtime cost."
:prose "The types module defines a gradual type system for SX. Type annotations on function parameters and return values are checked at registration time (when defcomp or define is evaluated), not at every call site. Base types include number, string, boolean, nil, symbol, keyword, element, any, and never. Union types (string|nil), function types, and type narrowing through control flow are supported. The system catches composition errors and boundary mismatches at definition time without any runtime overhead — unannotated code is unaffected.")
(dict
:slug "deps"
:filename "deps.sx"
:title "Deps"
:desc "Component dependency analysis and IO detection — per-page bundling, transitive closure, CSS scoping."
:prose "The deps module analyzes component dependency graphs and classifies components as pure or IO-dependent. Phase 1 (bundling): walks component AST bodies to find transitive ~component references, computes the minimal set needed per page, and collects per-page CSS classes from only the used components. Phase 2 (IO detection): scans component ASTs for references to IO primitive names (from boundary.sx declarations), computes transitive IO refs through the component graph, and caches the result. Components with no transitive IO refs are pure — they can render anywhere without server data. IO-dependent components must expand server-side.")))
(define
all-spec-items
(concat
core-spec-items
(concat
adapter-spec-items
(concat
browser-spec-items
(concat
reactive-spec-items
(concat host-spec-items extension-spec-items))))))
(define
find-spec
(fn
(slug)
(some
(fn (item) (when (= (get item "slug") slug) item))
all-spec-items)))
(define
find-current
(fn
(items slug)
(when
slug
(some
(fn
(item)
(when
(ends-with? (get item "href") (str "." slug "))"))
(get item "label")))
items))))
(defcomp
~nav-data/section-nav
(&key items current)
(<>
(map
(fn
(item)
(~shared:layout/nav-link
:href (get item "href")
:label (get item "label")
:is-selected (when (= (get item "label") current) "true")
:select-colours "aria-selected:bg-violet-200 aria-selected:text-violet-900"))
items)))
(define sx-nav-tree {:href "/sx/" :children (list {:href "/sx/(geography)" :children (list {:href "/sx/(geography.(reactive))" :children reactive-islands-nav-items :label "Reactive Islands"} {:href "/sx/(geography.(hypermedia))" :children (list {:href "/sx/(geography.(hypermedia.(reference)))" :children reference-nav-items :label "Reference"} {:href "/sx/(geography.(hypermedia.(example)))" :children examples-nav-items :label "Examples"}) :label "Hypermedia Lakes"} {:href "/sx/(geography.(scopes))" :summary "The unified primitive beneath provide, collect!, spreads, and islands. Named scope with downward value, upward accumulation, and a dedup flag." :label "Scopes"} {:href "/sx/(geography.(provide))" :summary "Sugar for scope-with-value. Render-time dynamic scope — the substrate beneath spreads, CSSX, and script collection." :label "Provide / Emit!"} {:href "/sx/(geography.(spreads))" :summary "Child-to-parent communication across render boundaries — spread, collect!, reactive-spread, built on scopes." :label "Spreads"} {:href "/sx/(geography.(marshes))" :children marshes-examples-nav-items :summary "Where reactivity and hypermedia interpenetrate — server writes to signals, reactive transforms reshape server content, client state modifies how hypermedia is interpreted." :label "Marshes"} {:href "/sx/(geography.(isomorphism))" :children isomorphism-nav-items :label "Isomorphism"} {:href "/sx/(geography.(cek))" :children cek-nav-items :label "CEK Machine"}) :label "Geography"} {:href "/sx/(language)" :children (list {:href "/sx/(language.(doc))" :children docs-nav-items :label "Docs"} {:href "/sx/(language.(spec))" :children specs-nav-items :label "Specs"} {:href "/sx/(language.(spec.(explore.evaluator)))" :label "Spec Explorer"} {:href "/sx/(language.(bootstrapper))" :children bootstrappers-nav-items :label "Bootstrappers"} {:href "/sx/(language.(test))" :children testing-nav-items :label "Testing"}) :label "Language"} {:href "/sx/(applications)" :children (list {:href "/sx/(applications.(sx-urls))" :label "SX URLs"} {:href "/sx/(applications.(cssx))" :children cssx-nav-items :label "CSSX"} {:href "/sx/(applications.(protocol))" :children protocols-nav-items :label "Protocols"} {:href "/sx/(applications.(sx-pub))" :label "sx-pub"} {:href "/sx/(applications.(sx-tools))" :label "SX Tools"} {:href "/sx/(geography.(reactive-runtime))" :children reactive-runtime-nav-items :label "Reactive Runtime"}) :label "Applications"} {:href "/sx/(etc)" :children (list {:href "/sx/(etc.(essay))" :children essays-nav-items :label "Essays"} {:href "/sx/(etc.(philosophy))" :children philosophy-nav-items :label "Philosophy"} {:href "/sx/(etc.(plan))" :children plans-nav-items :label "Plans"}) :label "Etc"}) :label "sx"})
(define
has-descendant-href?
(fn
(node path)
(let
((children (get node "children")))
(when
children
(some
(fn
(child)
(or
(= (get child "href") path)
(has-descendant-href? child path)))
children)))))
(define
find-nav-match
(fn
(items path)
(or
(some (fn (item) (when (= (get item "href") path) item)) items)
(some (fn (item) (when (has-descendant-href? item path) item)) items))))
(define
resolve-nav-path
(fn
(tree path)
(let
((trail (list)))
(define
walk
(fn
(node)
(let
((children (get node "children")))
(when
children
(let
((match (find-nav-match children path)))
(when
match
(append! trail {:siblings children :node match})
(when (not (= (get match "href") path)) (walk match))))))))
(walk tree)
(let
((depth (len trail)))
(if
(= depth 0)
{:children (get tree "children") :depth 0 :trail trail}
(let ((deepest (nth trail (- depth 1)))) {:children (get (get deepest "node") "children") :depth depth :trail trail}))))))
(define
find-nav-index
(fn
(items node)
(let
((target-href (get node "href")) (count (len items)))
(define
find-loop
(fn
(i)
(if
(>= i count)
0
(if
(= (get (nth items i) "href") target-href)
i
(find-loop (+ i 1))))))
(find-loop 0))))
(define sxtp-nav-items
(list (dict :label "SXTP Protocol" :href "/sx/(applications.(sxtp))")))

View File

@@ -0,0 +1,3 @@
;; Navigation items for the Etc section (essays, philosophy, plans)
;; NOTE: All nav items are now defined in nav-data.sx
;; This file previously held duplicate definitions that would overwrite nav-data.sx

View File

@@ -0,0 +1,26 @@
;; Navigation items for the Geography section
(define reference-nav-items (list (dict :label "Attributes" :href "/sx/(geography.(hypermedia.(reference.attributes)))") (dict :label "Headers" :href "/sx/(geography.(hypermedia.(reference.headers)))") (dict :label "Events" :href "/sx/(geography.(hypermedia.(reference.events)))") (dict :label "JS API" :href "/sx/(geography.(hypermedia.(reference.js-api)))")))
(define examples-nav-items (list (dict :label "Click to Load" :href "/sx/(geography.(hypermedia.(example.click-to-load)))") (dict :label "Form Submission" :href "/sx/(geography.(hypermedia.(example.form-submission)))") (dict :label "Polling" :href "/sx/(geography.(hypermedia.(example.polling)))") (dict :label "Delete Row" :href "/sx/(geography.(hypermedia.(example.delete-row)))") (dict :label "Inline Edit" :href "/sx/(geography.(hypermedia.(example.inline-edit)))") (dict :label "OOB Swaps" :href "/sx/(geography.(hypermedia.(example.oob-swaps)))") (dict :label "Lazy Loading" :href "/sx/(geography.(hypermedia.(example.lazy-loading)))") (dict :label "Infinite Scroll" :href "/sx/(geography.(hypermedia.(example.infinite-scroll)))") (dict :label "Progress Bar" :href "/sx/(geography.(hypermedia.(example.progress-bar)))") (dict :label "Active Search" :href "/sx/(geography.(hypermedia.(example.active-search)))") (dict :label "Inline Validation" :href "/sx/(geography.(hypermedia.(example.inline-validation)))") (dict :label "Value Select" :href "/sx/(geography.(hypermedia.(example.value-select)))") (dict :label "Reset on Submit" :href "/sx/(geography.(hypermedia.(example.reset-on-submit)))") (dict :label "Edit Row" :href "/sx/(geography.(hypermedia.(example.edit-row)))") (dict :label "Bulk Update" :href "/sx/(geography.(hypermedia.(example.bulk-update)))") (dict :label "Swap Positions" :href "/sx/(geography.(hypermedia.(example.swap-positions)))") (dict :label "Select Filter" :href "/sx/(geography.(hypermedia.(example.select-filter)))") (dict :label "Tabs" :href "/sx/(geography.(hypermedia.(example.tabs)))") (dict :label "Animations" :href "/sx/(geography.(hypermedia.(example.animations)))") (dict :label "Dialogs" :href "/sx/(geography.(hypermedia.(example.dialogs)))") (dict :label "Keyboard Shortcuts" :href "/sx/(geography.(hypermedia.(example.keyboard-shortcuts)))") (dict :label "PUT / PATCH" :href "/sx/(geography.(hypermedia.(example.put-patch)))") (dict :label "JSON Encoding" :href "/sx/(geography.(hypermedia.(example.json-encoding)))") (dict :label "Vals & Headers" :href "/sx/(geography.(hypermedia.(example.vals-and-headers)))") (dict :label "Loading States" :href "/sx/(geography.(hypermedia.(example.loading-states)))") (dict :label "Request Abort" :href "/sx/(geography.(hypermedia.(example.sync-replace)))") (dict :label "Retry" :href "/sx/(geography.(hypermedia.(example.retry)))")))
(define isomorphism-nav-items (list (dict :label "Roadmap" :href "/sx/(geography.(isomorphism))") (dict :label "Bundle Analyzer" :href "/sx/(geography.(isomorphism.bundle-analyzer))") (dict :label "Routing Analyzer" :href "/sx/(geography.(isomorphism.routing-analyzer))") (dict :label "Data Test" :href "/sx/(geography.(isomorphism.data-test))") (dict :label "Async IO" :href "/sx/(geography.(isomorphism.async-io))") (dict :label "Streaming" :href "/sx/(geography.(isomorphism.streaming))") (dict :label "Affinity" :href "/sx/(geography.(isomorphism.affinity))") (dict :label "Optimistic" :href "/sx/(geography.(isomorphism.optimistic))") (dict :label "Offline" :href "/sx/(geography.(isomorphism.offline))")))
(define cek-nav-items (list (dict :label "Overview" :href "/sx/(geography.(cek))" :summary "The CEK machine — explicit evaluator with Control, Environment, Kontinuation. Three registers, pure step function.") (dict :label "Demo" :href "/sx/(geography.(cek.demo))" :summary "Live islands evaluated by the CEK machine. Counter, computed chains, reactive attributes — all through explicit continuation frames.") (dict :label "Freeze / Thaw" :href "/sx/(geography.(cek.freeze))" :summary "Serialize a CEK state to s-expressions. Ship it, store it, content-address it. Thaw and resume anywhere.") (dict :label "Content Addressing" :href "/sx/(geography.(cek.content))" :summary "Hash frozen state to a CID. Same state = same address. Store, share, verify, reproduce.")))
(define reactive-examples-nav-items (list {:href "/sx/(geography.(reactive.(examples.counter)))" :label "Counter"} {:href "/sx/(geography.(reactive.(examples.temperature)))" :label "Temperature"} {:href "/sx/(geography.(reactive.(examples.stopwatch)))" :label "Stopwatch"} {:href "/sx/(geography.(reactive.(examples.imperative)))" :label "Imperative"} {:href "/sx/(geography.(reactive.(examples.reactive-list)))" :label "Reactive List"} {:href "/sx/(geography.(reactive.(examples.input-binding)))" :label "Input Binding"} {:href "/sx/(geography.(reactive.(examples.portal)))" :label "Portals"} {:href "/sx/(geography.(reactive.(examples.error-boundary)))" :label "Error Boundary"} {:href "/sx/(geography.(reactive.(examples.refs)))" :label "Refs"} {:href "/sx/(geography.(reactive.(examples.dynamic-class)))" :label "Dynamic Class"} {:href "/sx/(geography.(reactive.(examples.resource)))" :label "Resource"} {:href "/sx/(geography.(reactive.(examples.transition)))" :label "Transitions"} {:href "/sx/(geography.(reactive.(examples.stores)))" :label "Stores"} {:href "/sx/(geography.(reactive.(examples.event-bridge-demo)))" :label "Event Bridge"} {:href "/sx/(geography.(reactive.(examples.defisland)))" :label "defisland"} {:href "/sx/(geography.(reactive.(examples.tests)))" :label "Tests"} {:href "/sx/(geography.(reactive.(examples.coverage)))" :label "Coverage"} {:href "/sx/(geography.(reactive.(examples.cyst)))" :label "Cyst"} {:href "/sx/(geography.(reactive.(examples.reactive-expressions)))" :label "Reactive Expressions"}))
(define reactive-islands-nav-items (list (dict :label "Examples" :href "/sx/(geography.(reactive.(examples)))" :summary "Live interactive islands — click the buttons, type in the inputs." :children reactive-examples-nav-items)))
(define marshes-examples-nav-items (list {:href "/sx/(geography.(marshes.hypermedia-feeds))" :label "Hypermedia Feeds State"} {:href "/sx/(geography.(marshes.server-signals))" :label "Server Writes to Signals"} {:href "/sx/(geography.(marshes.on-settle))" :label "sx-on-settle"} {:href "/sx/(geography.(marshes.signal-triggers))" :label "Signal-Bound Triggers"} {:href "/sx/(geography.(marshes.view-transform))" :label "Reactive View Transform"}))
(define semantics-nav-items
(list
(dict :label "Capabilities" :href "/sx/(geography.(capabilities))"
:summary "Abstract evaluation contexts — what an expression can do, not where it runs.")
(dict :label "Modules" :href "/sx/(geography.(modules))"
:summary "The (use) form — declaring dependencies for documentation and static analysis.")
(dict :label "Eval Rules" :href "/sx/(geography.(eval-rules))"
:summary "Machine-readable SX semantics — 35 rules as queryable data.")))

View File

@@ -0,0 +1,26 @@
;; Navigation items for the Language section (docs, specs, testing, bootstrappers)
(define docs-nav-items (list (dict :label "Introduction" :href "/sx/(language.(doc.introduction))") (dict :label "Getting Started" :href "/sx/(language.(doc.getting-started))") (dict :label "Components" :href "/sx/(language.(doc.components))") (dict :label "Evaluator" :href "/sx/(language.(doc.evaluator))") (dict :label "Primitives" :href "/sx/(language.(doc.primitives))") (dict :label "Special Forms" :href "/sx/(language.(doc.special-forms))") (dict :label "Server Rendering" :href "/sx/(language.(doc.server-rendering))")))
(define specs-nav-items (list {:href "/sx/(language.(spec.core))" :children (list {:href "/sx/(language.(spec.parser))" :label "Parser"} {:href "/sx/(language.(spec.evaluator))" :label "Evaluator"} {:href "/sx/(language.(spec.primitives))" :label "Primitives"} {:href "/sx/(language.(spec.special-forms))" :label "Special Forms"} {:href "/sx/(language.(spec.renderer))" :label "Renderer"}) :label "Core"} {:href "/sx/(language.(spec.adapters))" :children (list {:href "/sx/(language.(spec.adapter-dom))" :label "DOM Adapter"} {:href "/sx/(language.(spec.adapter-html))" :label "HTML Adapter"} {:href "/sx/(language.(spec.adapter-sx))" :label "SX Wire Adapter"} {:href "/sx/(language.(spec.adapter-async))" :label "Async Adapter"}) :label "Adapters"} {:href "/sx/(language.(spec.browser))" :children (list {:href "/sx/(language.(spec.engine))" :label "SxEngine"} {:href "/sx/(language.(spec.orchestration))" :label "Orchestration"} {:href "/sx/(language.(spec.boot))" :label "Boot"} {:href "/sx/(language.(spec.router))" :label "Router"}) :label "Browser"} {:href "/sx/(language.(spec.reactive))" :children (list {:href "/sx/(language.(spec.signals))" :label "Signals"} {:href "/sx/(language.(spec.frames))" :label "CEK Frames"} {:href "/sx/(language.(spec.cek))" :label "CEK Machine"}) :label "Reactive"} {:href "/sx/(language.(spec.host))" :children (list {:href "/sx/(language.(spec.boundary))" :label "Boundary"} {:href "/sx/(language.(spec.forms))" :label "Forms"} {:href "/sx/(language.(spec.page-helpers))" :label "Page Helpers"}) :label "Host Interface"} {:href "/sx/(language.(spec.extensions))" :children (list {:href "/sx/(language.(spec.continuations))" :label "Continuations"} {:href "/sx/(language.(spec.callcc))" :label "call/cc"} {:href "/sx/(language.(spec.types))" :label "Types"} {:href "/sx/(language.(spec.deps))" :label "Deps"}) :label "Extensions"}))
(define testing-nav-items (list (dict :label "Overview" :href "/sx/(language.(test))") (dict :label "Evaluator" :href "/sx/(language.(test.eval))") (dict :label "Parser" :href "/sx/(language.(test.parser))") (dict :label "Router" :href "/sx/(language.(test.router))") (dict :label "Renderer" :href "/sx/(language.(test.render))") (dict :label "Dependencies" :href "/sx/(language.(test.deps))") (dict :label "Engine" :href "/sx/(language.(test.engine))") (dict :label "Orchestration" :href "/sx/(language.(test.orchestration))") (dict :label "Runners" :href "/sx/(language.(test.runners))")))
(define bootstrappers-nav-items (list (dict :label "Overview" :href "/sx/(language.(bootstrapper))") (dict :label "JavaScript" :href "/sx/(language.(bootstrapper.javascript))") (dict :label "Python" :href "/sx/(language.(bootstrapper.python))") (dict :label "Self-Hosting (py.sx)" :href "/sx/(language.(bootstrapper.self-hosting))") (dict :label "Self-Hosting JS (js.sx)" :href "/sx/(language.(bootstrapper.self-hosting-js))") (dict :label "Page Helpers" :href "/sx/(language.(bootstrapper.page-helpers))")))
(define core-spec-items (list (dict :slug "parser" :filename "parser.sx" :title "Parser" :desc "Tokenization and parsing of SX source text into AST." :prose "The parser converts SX source text into an abstract syntax tree. It tokenizes the input into atoms, strings, numbers, keywords, and delimiters, then assembles them into nested list structures. The parser is intentionally minimal — s-expressions need very little syntax to parse. Special reader macros handle quasiquote (\\`), unquote (~), splice (~@), and the quote (') shorthand. The output is a tree of plain lists, symbols, keywords, strings, and numbers that the evaluator can walk directly.") (dict :slug "evaluator" :filename "evaluator.sx" :title "Evaluator" :desc "CEK machine evaluator." :prose "The evaluator walks the AST produced by the parser and reduces it to values. It implements lexical scoping with closures, special forms (define, let, if, cond, fn, defcomp, defmacro, quasiquote, set!, do), and function application. Macros are expanded at eval time. Component definitions (defcomp) create callable component objects that participate in the rendering pipeline. The evaluator delegates rendering expressions — HTML tags, components, fragments — to whichever adapter is active, making the same source renderable to DOM nodes, HTML strings, or SX wire format.") (dict :slug "primitives" :filename "primitives.sx" :title "Primitives" :desc "All built-in pure functions and their signatures." :prose "Primitives are the built-in functions available in every SX environment. Each entry declares a name, parameter signature, and semantics. Bootstrap compilers implement these natively per target (JavaScript, Python, etc.). The registry covers arithmetic, comparison, string manipulation, list operations, dict operations, type predicates, and control flow helpers. All primitives are pure — they take values and return values with no side effects. Platform-specific operations (DOM access, HTTP, file I/O) are provided separately via platform bridge functions, not primitives.") (dict :slug "special-forms" :filename "special-forms.sx" :title "Special Forms" :desc "All special forms — syntactic constructs with custom evaluation rules." :prose "Special forms are the syntactic constructs whose arguments are NOT evaluated before dispatch. Each form has its own evaluation rules — unlike primitives, which receive pre-evaluated values. Together with primitives, special forms define the complete language surface. The registry covers control flow (if, when, cond, case, and, or), binding (let, letrec, define, set!), functions (lambda, defcomp, defmacro), sequencing (begin, do, thread-first), quoting (quote, quasiquote), continuations (reset, shift), guards (dynamic-wind), higher-order forms (map, filter, reduce), and domain-specific definitions (defstyle, defhandler, defpage, defquery, defaction).") (dict :slug "renderer" :filename "render.sx" :title "Renderer" :desc "Shared rendering registries and utilities used by all adapters." :prose "The renderer defines what is renderable and how arguments are parsed, but not the output format. It maintains registries of known HTML tags, SVG tags, void elements, and boolean attributes. It specifies how keyword arguments on elements become HTML attributes, how children are collected, and how special attributes (class, style, data-*) are handled. All three adapters (DOM, HTML, SX wire) share these definitions so they agree on what constitutes valid markup.")))
(define adapter-spec-items (list (dict :slug "adapter-dom" :filename "adapter-dom.sx" :title "DOM Adapter" :desc "Renders SX expressions to live DOM nodes. Browser-only." :prose "The DOM adapter renders evaluated SX expressions into live browser DOM nodes — Elements, Text nodes, and DocumentFragments. It mirrors the HTML adapter's logic but produces DOM objects instead of strings. This is the adapter used by the browser-side SX runtime for initial mount, hydration, and dynamic updates. It handles element creation, attribute setting (including event handlers and style objects), SVG namespace handling, and fragment composition.") (dict :slug "adapter-html" :filename "adapter-html.sx" :title "HTML Adapter" :desc "Renders SX expressions to HTML strings. Server-side." :prose "The HTML adapter renders evaluated SX expressions to HTML strings. It is used server-side to produce complete HTML pages and fragments. It handles void elements (self-closing tags like <br>, <img>), boolean attributes, style serialization, class merging, and proper escaping. The output is standard HTML5 that any browser can parse.") (dict :slug "adapter-sx" :filename "adapter-sx.sx" :title "SX Wire Adapter" :desc "Serializes SX for client-side rendering. Component calls stay unexpanded." :prose "The SX wire adapter serializes expressions as SX source text for transmission to the browser, where sx.js renders them client-side. Unlike the HTML adapter, component calls (~plans/content-addressed-components/name ...) are NOT expanded — they are sent to the client as-is, allowing the browser to render them with its local component registry. HTML tags ARE serialized as s-expression source. This is the format used for SX-over-HTTP responses and the page boot payload.") (dict :slug "adapter-async" :filename "adapter-async.sx" :title "Async Adapter" :desc "Async versions of HTML and SX wire adapters for server-side rendering with I/O." :prose "The async adapter provides async-aware versions of the HTML and SX wire rendering functions. It intercepts I/O operations (database queries, service calls, fragment fetches) during evaluation, awaiting them before continuing. Entry points: async-render (HTML output with awaited I/O), async-aser (SX wire format with awaited I/O). The bootstrapper emits async def and automatic await insertion for all define-async functions. This adapter is what makes server-side SX pages work with real data.")))
(define browser-spec-items (list (dict :slug "engine" :filename "engine.sx" :title "SxEngine" :desc "Pure logic for fetch, swap, history, SSE, triggers, morph, and indicators." :prose "The engine specifies the pure logic of the browser-side fetch/swap/history system. Like HTMX but native to SX. It defines trigger parsing (click, submit, intersect, poll, load, revealed), swap algorithms (innerHTML, outerHTML, morph, beforebegin, etc.), the morph/diff algorithm for patching existing DOM, history management (push-url, replace-url, popstate), out-of-band swap identification, Server-Sent Events parsing, retry logic with exponential backoff, request header building, response header processing, and optimistic UI updates. This file contains no browser API calls — all platform interaction is in orchestration.sx.") (dict :slug "orchestration" :filename "orchestration.sx" :title "Orchestration" :desc "Browser wiring that binds engine logic to DOM events, fetch, and lifecycle." :prose "Orchestration is the browser wiring layer. It binds the pure engine logic to actual browser APIs: DOM event listeners, fetch(), AbortController, setTimeout/setInterval, IntersectionObserver, history.pushState, and EventSource (SSE). It implements the full request lifecycle — from trigger through fetch through swap — including CSS tracking, response type detection (SX vs HTML), OOB swap processing, script activation, element boosting, and preload. Dependency is strictly one-way: orchestration depends on engine, never the reverse.") (dict :slug "boot" :filename "boot.sx" :title "Boot" :desc "Browser startup lifecycle: mount, hydrate, script processing." :prose "Boot handles the browser startup sequence and provides the public API for mounting SX content. On page load it: (1) initializes CSS tracking, (2) processes <script type=\"text/sx\"> tags (component definitions and mount directives), (3) hydrates [data-sx] elements, and (4) activates the engine on all elements. It also provides the public mount/hydrate/update/render-component API, and the head element hoisting logic that moves <meta>, <title>, and <link> tags from rendered content into <head>.") (dict :slug "router" :filename "router.sx" :title "Router" :desc "Client-side route matching — Flask-style pattern parsing, segment matching, route table search." :prose "The router module provides pure functions for matching URL paths against Flask-style route patterns (e.g. /docs/<slug>). Used by client-side routing to determine if a page can be rendered locally without a server roundtrip. split-path-segments breaks a path into segments, parse-route-pattern converts patterns into typed segment descriptors, match-route-segments tests a path against a parsed pattern returning extracted params, and find-matching-route searches a route table for the first match.")))
(define reactive-spec-items (list (dict :slug "signals" :filename "signals.sx" :title "Signals" :desc "Fine-grained reactive primitives — signal, computed, effect, batch." :prose "The signals module defines a fine-grained reactive system for client-side islands. Signals are containers for values that notify subscribers on change. Computed signals derive values lazily from other signals. Effects run side-effects when their dependencies change, with automatic cleanup. Batch coalesces multiple signal writes into a single notification pass. Island scope management ensures all signals, computeds, and effects are cleaned up when an island is removed from the DOM. The spec defines the reactive graph topology and update algorithm — each platform implements the actual signal/tracking types natively.") (dict :slug "frames" :filename "frames.sx" :title "CEK Frames" :desc "Continuation frame types for the explicit CEK machine." :prose "Frames define what to do next when a sub-evaluation completes. Each frame type is a dict with a type key and frame-specific data. IfFrame, WhenFrame, BeginFrame handle control flow. LetFrame, DefineFrame, SetFrame handle bindings. ArgFrame tracks function call argument evaluation. MapFrame, FilterFrame, ReduceFrame, ForEachFrame drive higher-order forms element by element through the CEK machine. ReactiveResetFrame and DerefFrame enable deref-as-shift — the core reactive mechanism where deref inside a reactive boundary captures the continuation as a signal subscriber.") (dict :slug "cek" :filename "cek.sx" :title "CEK Machine" :desc "Explicit CEK machine evaluator — step function, run loop, reactive shift." :prose "The CEK machine makes evaluation explicit. Every step is a pure function from state to state: Control (expression), Environment (bindings), Kontinuation (stack of frames). step-eval dispatches on expression type — literals pass through, symbols are looked up, lists dispatch on head (special forms, higher-order forms, macros, function calls). step-continue dispatches on the top frame type. Higher-order forms (map, filter, reduce, for-each, some, every?) step element by element, so deref-as-shift works inside callbacks. cek-call replaces invoke as the universal function dispatch — lambdas go through cek-run, native callables through apply.")))
(define host-spec-items (list (dict :slug "boundary" :filename "boundary.sx" :title "Boundary" :desc "Language boundary contract — declares I/O primitives the host must provide." :prose "The boundary defines the contract between SX and its host environment. Tier 1 declares pure primitives (from primitives.sx). Tier 2 declares async I/O primitives the host must implement: fetch, async-eval, call-action, send-activity, and other operations that require network or database access. Tier 3 declares page helpers: format, highlight, scan-css-classes, parse-datetime. This is the interface every host must satisfy to run SX — framework-agnostic, universal to all targets. Boundary enforcement validates at registration time that all declared primitives are provided.") (dict :slug "forms" :filename "forms.sx" :title "Forms" :desc "Server-side definition forms — defhandler, defquery, defaction, defpage." :prose "Forms defines the server-side definition macros that compose the application layer. defhandler registers an HTTP route handler. defquery defines a read-only data source. defaction defines a mutation (write). defpage declares a client-routable page with path, auth, layout, data dependencies, and content. Each form parses &key parameter lists and creates typed definition objects. Platform-specific constructors are provided by the host — these have different bindings on server (Python/Quart) vs client (route matching only).") (dict :slug "page-helpers" :filename "page-helpers.sx" :title "Page Helpers" :desc "Pure data-transformation helpers for page rendering." :prose "Page helpers are pure functions that assist page rendering: categorizing special forms by type, formatting numbers and dates, highlighting code, scanning CSS classes, constructing page titles and descriptions. Unlike boundary I/O primitives, these are pure — they take data and return data with no side effects. They run identically on server and client. The host registers native implementations that match these declarations.")))
(define extension-spec-items (list (dict :slug "continuations" :filename "continuations.sx" :title "Continuations" :desc "Delimited continuations — shift/reset for suspendable rendering and cooperative scheduling." :prose "Delimited continuations capture the rest of a computation up to a delimiter. shift captures the continuation to the nearest reset as a first-class callable value. Unlike full call/cc, delimited continuations are composable — invoking one returns a value. This covers the practical use cases: suspendable server rendering, cooperative scheduling, linear async flows, wizard-style multi-step UIs, and undo. Each bootstrapper target implements the mechanism differently — generators in Python/JS, native shift/reset in Scheme, ContT in Haskell, CPS transform in Rust — but the semantics are identical. Optional extension: code that doesn't use continuations pays zero cost.") (dict :slug "callcc" :filename "callcc.sx" :title "call/cc" :desc "Full first-class continuations — call-with-current-continuation." :prose "Full call/cc captures the entire remaining computation as a first-class function — not just up to a delimiter, but all the way to the top level. Invoking the continuation abandons the current computation entirely and resumes from where it was captured. Strictly more powerful than delimited continuations, but harder to implement in targets that don't support it natively. Recommended for Scheme and Haskell targets where it's natural. Python, JavaScript, and Rust targets should prefer delimited continuations (continuations.sx) unless full escape semantics are genuinely needed. Optional extension: the continuation type is shared with continuations.sx if both are loaded.") (dict :slug "types" :filename "types.sx" :title "Types" :desc "Gradual type system — registration-time checking with zero runtime cost." :prose "The types module defines a gradual type system for SX. Type annotations on function parameters and return values are checked at registration time (when defcomp or define is evaluated), not at every call site. Base types include number, string, boolean, nil, symbol, keyword, element, any, and never. Union types (string|nil), function types, and type narrowing through control flow are supported. The system catches composition errors and boundary mismatches at definition time without any runtime overhead — unannotated code is unaffected.") (dict :slug "deps" :filename "deps.sx" :title "Deps" :desc "Component dependency analysis and IO detection — per-page bundling, transitive closure, CSS scoping." :prose "The deps module analyzes component dependency graphs and classifies components as pure or IO-dependent. Phase 1 (bundling): walks component AST bodies to find transitive ~component references, computes the minimal set needed per page, and collects per-page CSS classes from only the used components. Phase 2 (IO detection): scans component ASTs for references to IO primitive names (from boundary.sx declarations), computes transitive IO refs through the component graph, and caches the result. Components with no transitive IO refs are pure — they can render anywhere without server data. IO-dependent components must expand server-side.")))
(define all-spec-items (concat core-spec-items (concat adapter-spec-items (concat browser-spec-items (concat reactive-spec-items (concat host-spec-items extension-spec-items))))))
(define find-spec (fn (slug) (some (fn (item) (when (= (get item "slug") slug) item)) all-spec-items)))

View File

@@ -0,0 +1,6 @@
;; Navigation items for the Tools section
(define tools-nav-items
(list
(dict :label "SX Tools" :href "/sx/(tools.(sx-tools))")
(dict :label "Services" :href "/sx/(tools.(services))")))

View File

@@ -0,0 +1,213 @@
(define
find-current
(fn
(items slug)
(when
slug
(some
(fn
(item)
(when
(ends-with? (get item "href") (str "." slug "))"))
(get item "label")))
items))))
(defcomp
~nav-data/section-nav
(&key items current)
(<>
(map
(fn
(item)
(~shared:layout/nav-link
:href (get item "href")
:label (get item "label")
:is-selected (when (= (get item "label") current) "true")
:select-colours "aria-selected:bg-violet-200 aria-selected:text-violet-900"))
items)))
(define
sx-nav-tree
(dict
:href "/sx/"
:label "sx"
:children (list
(dict
:href "/sx/(geography)"
:label "Geography"
:children (list
(dict
:href "/sx/(geography.(reactive))"
:label "Reactive Islands"
:children reactive-islands-nav-items)
(dict
:href "/sx/(geography.(hypermedia))"
:label "Hypermedia Lakes"
:children (list
(dict
:href "/sx/(geography.(hypermedia.(reference)))"
:label "Reference"
:children reference-nav-items)
(dict
:href "/sx/(geography.(hypermedia.(example)))"
:label "Examples"
:children examples-nav-items)))
(dict
:href "/sx/(geography.(scopes))"
:label "Scopes"
:summary "The unified primitive beneath provide, collect!, spreads, and islands. Named scope with downward value, upward accumulation, and a dedup flag.")
(dict
:href "/sx/(geography.(provide))"
:label "Provide / Emit!"
:summary "Sugar for scope-with-value. Render-time dynamic scope — the substrate beneath spreads, CSSX, and script collection.")
(dict
:href "/sx/(geography.(spreads))"
:label "Spreads"
:summary "Child-to-parent communication across render boundaries — spread, collect!, reactive-spread, built on scopes.")
(dict
:href "/sx/(geography.(marshes))"
:label "Marshes"
:children marshes-examples-nav-items
:summary "Where reactivity and hypermedia interpenetrate — server writes to signals, reactive transforms reshape server content, client state modifies how hypermedia is interpreted.")
(dict
:href "/sx/(geography.(isomorphism))"
:label "Isomorphism"
:children isomorphism-nav-items)
(dict
:href "/sx/(geography.(cek))"
:label "CEK Machine"
:children cek-nav-items)
(dict :href "/sx/(geography.(capabilities))" :label "Capabilities")
(dict
:href "/sx/(geography.(reactive-runtime))"
:label "Reactive Runtime"
:children reactive-runtime-nav-items)))
(dict
:href "/sx/(language)"
:label "Language"
:children (list
(dict
:href "/sx/(language.(doc))"
:label "Docs"
:children docs-nav-items)
(dict
:href "/sx/(language.(spec))"
:label "Specs"
:children specs-nav-items)
(dict
:href "/sx/(language.(spec.(explore.evaluator)))"
:label "Spec Explorer")
(dict
:href "/sx/(language.(bootstrapper))"
:label "Bootstrappers"
:children bootstrappers-nav-items)
(dict
:href "/sx/(language.(test))"
:label "Testing"
:children testing-nav-items)))
(dict
:href "/sx/(applications)"
:label "Applications"
:children (list
(dict :href "/sx/(applications.(sx-urls))" :label "SX URLs")
(dict
:href "/sx/(applications.(cssx))"
:label "CSSX"
:children cssx-nav-items)
(dict
:href "/sx/(applications.(protocol))"
:label "Protocols"
:children protocols-nav-items)
(dict :href "/sx/(applications.(sx-pub))" :label "sx-pub")
(dict
:href "/sx/(applications.(native-browser))"
:label "Native Browser")
(dict :href "/sx/(applications.(sxtp))" :label "SXTP Protocol")))
(dict :href "/sx/(tools)" :label "Tools" :children tools-nav-items)
(dict
:href "/sx/(etc)"
:label "Etc"
:children (list
(dict
:href "/sx/(etc.(essay))"
:label "Essays"
:children essays-nav-items)
(dict
:href "/sx/(etc.(philosophy))"
:label "Philosophy"
:children philosophy-nav-items)
(dict
:href "/sx/(etc.(plan))"
:label "Plans"
:children plans-nav-items))))))
(define
has-descendant-href?
(fn
(node path)
(let
((children (get node "children")))
(when
children
(some
(fn
(child)
(or
(= (get child "href") path)
(has-descendant-href? child path)))
children)))))
(define
find-nav-match
(fn
(items path)
(or
(some (fn (item) (when (= (get item "href") path) item)) items)
(some (fn (item) (when (has-descendant-href? item path) item)) items))))
(define
resolve-nav-path
(fn
(tree path)
(let
((trail (list)))
(define
walk
(fn
(node)
(let
((children (get node "children")))
(when
children
(let
((match (find-nav-match children path)))
(when
match
(append! trail {:siblings children :node match})
(when (not (= (get match "href") path)) (walk match))))))))
(walk tree)
(let
((depth (len trail)))
(if
(= depth 0)
{:children (get tree "children") :depth 0 :trail trail}
(let ((deepest (nth trail (- depth 1)))) {:children (get (get deepest "node") "children") :depth depth :trail trail}))))))
(define
find-nav-index
(fn
(items node)
(let
((target-href (get node "href")) (count (len items)))
(define
find-loop
(fn
(i)
(if
(>= i count)
0
(if
(= (get (nth items i) "href") target-href)
i
(find-loop (+ i 1))))))
(find-loop 0))))

View File

@@ -0,0 +1,678 @@
(define
slug->component
(fn
(slug prefix infix suffix)
(if
infix
(make-symbol (str prefix slug infix slug suffix))
(make-symbol (str prefix slug suffix)))))
(define
make-page-fn
(fn
(default-name prefix infix suffix)
(fn
(slug)
(if
(nil? slug)
(list (make-symbol default-name))
(list (slug->component slug prefix infix suffix))))))
(define
home
(fn
(content)
(if (nil? content) (quote (~docs-content/home-content)) content)))
(define language (fn (content) (if (nil? content) nil content)))
(define
geography
(fn
(content)
(if (nil? content) (quote (~geography/index-content)) content)))
(define applications (fn (content) (if (nil? content) nil content)))
(define etc (fn (content) (if (nil? content) nil content)))
(define hypermedia (fn (content) (if (nil? content) nil content)))
(define
reactive
(fn
(content)
(if
(nil? content)
(quote (~reactive-islands/index/reactive-islands-index-content))
content)))
(define
examples
(make-page-fn
"~reactive-islands/demo/reactive-islands-demo-content"
"~reactive-islands/demo/example-"
nil
""))
(define
cek
(fn
(slug)
(if
(nil? slug)
(quote (~geography/cek/cek-content))
(case
slug
"demo"
(quote (~geography/cek/cek-demo-content))
"freeze"
(quote (~geography/cek/cek-freeze-content))
"content"
(quote (~geography/cek/cek-content-address-content))
:else (quote (~geography/cek/cek-content))))))
(define
provide
(fn
(content)
(if (nil? content) (quote (~geography/provide-content)) content)))
(define
scopes
(fn
(content)
(if (nil? content) (quote (~geography/scopes-content)) content)))
(define
spreads
(fn
(content)
(if (nil? content) (quote (~geography/spreads-content)) content)))
(define
marshes
(fn
(slug)
(if
(nil? slug)
(quote (~reactive-islands/marshes/reactive-islands-marshes-content))
(case
slug
"hypermedia-feeds"
(quote (~reactive-islands/marshes/example-hypermedia-feeds))
"server-signals"
(quote (~reactive-islands/marshes/example-server-signals))
"on-settle"
(quote (~reactive-islands/marshes/example-on-settle))
"signal-triggers"
(quote (~reactive-islands/marshes/example-signal-triggers))
"view-transform"
(quote (~reactive-islands/marshes/example-view-transform))
:else (quote (~reactive-islands/marshes/reactive-islands-marshes-content))))))
(define
isomorphism
(fn
(slug)
(if
(nil? slug)
(quote (~plans/isomorphic/plan-isomorphic-content))
(case
slug
"bundle-analyzer"
(let
((data (helper "bundle-analyzer-data")))
(quasiquote
(~analyzer/bundle-analyzer-content
:pages (unquote (get data "pages"))
:total-components (unquote (get data "total-components"))
:total-macros (unquote (get data "total-macros"))
:pure-count (unquote (get data "pure-count"))
:io-count (unquote (get data "io-count")))))
"routing-analyzer"
(let
((data (helper "routing-analyzer-data")))
(quasiquote
(~routing-analyzer/content
:pages (unquote (get data "pages"))
:total-pages (unquote (get data "total-pages"))
:client-count (unquote (get data "client-count"))
:server-count (unquote (get data "server-count"))
:registry-sample (unquote (get data "registry-sample")))))
"data-test"
(let
((data (helper "data-test-data")))
(quasiquote
(~data-test/content
:server-time (unquote (get data "server-time"))
:items (unquote (get data "items"))
:phase (unquote (get data "phase"))
:transport (unquote (get data "transport")))))
"async-io"
(quote (~async-io-demo/content))
"affinity"
(let
((data (helper "affinity-demo-data")))
(quasiquote
(~affinity-demo/content
:components (unquote (get data "components"))
:page-plans (unquote (get data "page-plans")))))
"optimistic"
(let
((data (helper "optimistic-demo-data")))
(quasiquote
(~optimistic-demo/content
:items (unquote (get data "items"))
:server-time (unquote (get data "server-time")))))
"offline"
(let
((data (helper "offline-demo-data")))
(quasiquote
(~offline-demo/content
:notes (unquote (get data "notes"))
:server-time (unquote (get data "server-time")))))
:else (quote (~plans/isomorphic/plan-isomorphic-content))))))
(define
doc
(fn
(slug)
(if
(nil? slug)
(quote (~docs-content/docs-introduction-content))
(case
slug
"introduction"
(quote (~docs-content/docs-introduction-content))
"getting-started"
(quote (~docs-content/docs-getting-started-content))
"components"
(quote (~docs-content/docs-components-content))
"evaluator"
(quote (~docs-content/docs-evaluator-content))
"primitives"
(let
((data (helper "primitives-data")))
(quasiquote
(~docs-content/docs-primitives-content
:prims (~docs/primitives-tables :primitives (unquote data)))))
"special-forms"
(let
((data (helper "special-forms-data")))
(quasiquote
(~docs-content/docs-special-forms-content
:forms (~docs/special-forms-tables :forms (unquote data)))))
"server-rendering"
(quote (~docs-content/docs-server-rendering-content))
:else (quote (~docs-content/docs-introduction-content))))))
(define
spec
(fn
(slug)
(if
(nil? slug)
(quote (~specs/architecture-content))
(case
slug
"core"
(let
((files (make-spec-files core-spec-items)))
(quasiquote
(~specs/overview-content
:spec-title "Core Language"
:spec-files (unquote files))))
"adapters"
(let
((files (make-spec-files adapter-spec-items)))
(quasiquote
(~specs/overview-content
:spec-title "Adapters"
:spec-files (unquote files))))
"browser"
(let
((files (make-spec-files browser-spec-items)))
(quasiquote
(~specs/overview-content
:spec-title "Browser Runtime"
:spec-files (unquote files))))
"reactive"
(let
((files (make-spec-files reactive-spec-items)))
(quasiquote
(~specs/overview-content
:spec-title "Reactive System"
:spec-files (unquote files))))
"host"
(let
((files (make-spec-files host-spec-items)))
(quasiquote
(~specs/overview-content
:spec-title "Host Interface"
:spec-files (unquote files))))
"extensions"
(let
((files (make-spec-files extension-spec-items)))
(quasiquote
(~specs/overview-content
:spec-title "Extensions"
:spec-files (unquote files))))
:else (let
((found-spec (find-spec slug)))
(if
found-spec
(let
((src (helper "read-spec-file" (get found-spec "filename"))))
(quasiquote
(~specs/detail-content
:spec-title (unquote (get found-spec "title"))
:spec-desc (unquote (get found-spec "desc"))
:spec-filename (unquote (get found-spec "filename"))
:spec-source (unquote src)
:spec-prose (unquote (get found-spec "prose")))))
(quasiquote (~specs/not-found :slug (unquote slug)))))))))
(define
explore
(fn
(slug)
(if
(nil? slug)
(quote (~specs/architecture-content))
(let
((found-spec (find-spec slug)))
(if
found-spec
(let
((data (spec-explore (get found-spec "filename") (get found-spec "title") (get found-spec "desc"))))
(if
data
(quasiquote
(~specs-explorer/spec-explorer-content :data (unquote data)))
(quasiquote (~specs/not-found :slug (unquote slug)))))
(quasiquote (~specs/not-found :slug (unquote slug))))))))
(define
make-spec-files
(fn
(items)
(map
(fn
(item)
(dict
:title (get item "title")
:desc (get item "desc")
:prose (get item "prose")
:filename (get item "filename")
:href (str "/sx/(language.(spec." (get item "slug") "))")
:source (helper "read-spec-file" (get item "filename"))))
items)))
(define
bootstrapper
(fn
(slug)
(if
(nil? slug)
(quote (~specs/bootstrappers-index-content))
(let
((data (helper "bootstrapper-data" slug)))
(if
(get data "bootstrapper-not-found")
(quasiquote (~specs/not-found :slug (unquote slug)))
(case
slug
"self-hosting"
(quasiquote
(~specs/bootstrapper-self-hosting-content
:py-sx-source (unquote (get data "py-sx-source"))
:g0-output (unquote (get data "g0-output"))
:g1-output (unquote (get data "g1-output"))
:defines-matched (unquote (get data "defines-matched"))
:defines-total (unquote (get data "defines-total"))
:g0-lines (unquote (get data "g0-lines"))
:g0-bytes (unquote (get data "g0-bytes"))
:verification-status (unquote (get data "verification-status"))))
"self-hosting-js"
(quasiquote
(~specs/bootstrapper-self-hosting-js-content
:js-sx-source (unquote (get data "js-sx-source"))
:defines-matched (unquote (get data "defines-matched"))
:defines-total (unquote (get data "defines-total"))
:js-sx-lines (unquote (get data "js-sx-lines"))
:verification-status (unquote (get data "verification-status"))))
"python"
(quasiquote
(~specs/bootstrapper-py-content
:bootstrapper-source (unquote (get data "bootstrapper-source"))
:bootstrapped-output (unquote (get data "bootstrapped-output"))))
"page-helpers"
(let
((ph-data (helper "page-helpers-demo-data")))
(quasiquote
(~page-helpers-demo/content
:sf-categories (unquote (get ph-data "sf-categories"))
:sf-total (unquote (get ph-data "sf-total"))
:sf-ms (unquote (get ph-data "sf-ms"))
:ref-sample (unquote (get ph-data "ref-sample"))
:ref-ms (unquote (get ph-data "ref-ms"))
:attr-result (unquote (get ph-data "attr-result"))
:attr-ms (unquote (get ph-data "attr-ms"))
:comp-source (unquote (get ph-data "comp-source"))
:comp-ms (unquote (get ph-data "comp-ms"))
:routing-result (unquote (get ph-data "routing-result"))
:routing-ms (unquote (get ph-data "routing-ms"))
:server-total-ms (unquote (get ph-data "server-total-ms"))
:sf-source (unquote (get ph-data "sf-source"))
:attr-detail (unquote (get ph-data "attr-detail"))
:req-attrs (unquote (get ph-data "req-attrs"))
:attr-keys (unquote (get ph-data "attr-keys")))))
:else (quasiquote
(~specs/bootstrapper-js-content
:bootstrapper-source (unquote (get data "bootstrapper-source"))
:bootstrapped-output (unquote (get data "bootstrapped-output"))))))))))
(define
test
(fn
(slug)
(if
(nil? slug)
(let
((data (helper "run-modular-tests" "all")))
(quasiquote
(~testing/overview-content
:server-results (unquote (get data "server-results"))
:framework-source (unquote (get data "framework-source"))
:eval-source (unquote (get data "eval-source"))
:parser-source (unquote (get data "parser-source"))
:router-source (unquote (get data "router-source"))
:render-source (unquote (get data "render-source"))
:deps-source (unquote (get data "deps-source"))
:engine-source (unquote (get data "engine-source")))))
(case
slug
"runners"
(quote (~testing/runners-content))
:else (let
((data (helper "run-modular-tests" slug)))
(case
slug
"eval"
(quasiquote
(~testing/spec-content
:spec-name "eval"
:spec-title "Evaluator Tests"
:spec-desc "81 tests covering the core evaluator and all primitives."
:spec-source (unquote (get data "spec-source"))
:framework-source (unquote (get data "framework-source"))
:server-results (unquote (get data "server-results"))))
"parser"
(quasiquote
(~testing/spec-content
:spec-name "parser"
:spec-title "Parser Tests"
:spec-desc "39 tests covering tokenization and parsing."
:spec-source (unquote (get data "spec-source"))
:framework-source (unquote (get data "framework-source"))
:server-results (unquote (get data "server-results"))))
"router"
(quasiquote
(~testing/spec-content
:spec-name "router"
:spec-title "Router Tests"
:spec-desc "18 tests covering client-side route matching."
:spec-source (unquote (get data "spec-source"))
:framework-source (unquote (get data "framework-source"))
:server-results (unquote (get data "server-results"))))
"render"
(quasiquote
(~testing/spec-content
:spec-name "render"
:spec-title "Renderer Tests"
:spec-desc "23 tests covering HTML rendering."
:spec-source (unquote (get data "spec-source"))
:framework-source (unquote (get data "framework-source"))
:server-results (unquote (get data "server-results"))))
"deps"
(quasiquote
(~testing/spec-content
:spec-name "deps"
:spec-title "Dependency Analysis Tests"
:spec-desc "33 tests covering component dependency analysis."
:spec-source (unquote (get data "spec-source"))
:framework-source (unquote (get data "framework-source"))
:server-results (unquote (get data "server-results"))))
"engine"
(quasiquote
(~testing/spec-content
:spec-name "engine"
:spec-title "Engine Tests"
:spec-desc "37 tests covering engine pure functions."
:spec-source (unquote (get data "spec-source"))
:framework-source (unquote (get data "framework-source"))
:server-results (unquote (get data "server-results"))))
"orchestration"
(quasiquote
(~testing/spec-content
:spec-name "orchestration"
:spec-title "Orchestration Tests"
:spec-desc "17 tests covering orchestration."
:spec-source (unquote (get data "spec-source"))
:framework-source (unquote (get data "framework-source"))
:server-results (unquote (get data "server-results"))))
:else (quasiquote
(~testing/overview-content
:server-results (unquote (get data "server-results"))))))))))
(define
reference
(fn
(slug)
(if
(nil? slug)
(quote (~examples/reference-index-content))
(let
((data (helper "reference-data" slug)))
(case
slug
"attributes"
(quasiquote
(~reference/attrs-content
:req-table (~docs/attr-table-from-data
:title "Request Attributes"
:attrs (unquote (get data "req-attrs")))
:beh-table (~docs/attr-table-from-data
:title "Behavior Attributes"
:attrs (unquote (get data "beh-attrs")))
:uniq-table (~docs/attr-table-from-data
:title "Unique to sx"
:attrs (unquote (get data "uniq-attrs")))))
"headers"
(quasiquote
(~reference/headers-content
:req-table (~docs/headers-table-from-data
:title "Request Headers"
:headers (unquote (get data "req-headers")))
:resp-table (~docs/headers-table-from-data
:title "Response Headers"
:headers (unquote (get data "resp-headers")))))
"events"
(quasiquote
(~reference/events-content
:table (~docs/two-col-table-from-data
:intro "sx fires custom DOM events at various points in the request lifecycle."
:col1 "Event"
:col2 "Description"
:items (unquote (get data "events-list")))))
"js-api"
(quasiquote
(~reference/js-api-content
:table (~docs/two-col-table-from-data
:intro "The client-side sx.js library exposes a public API for programmatic use."
:col1 "Method"
:col2 "Description"
:items (unquote (get data "js-api-list")))))
:else (quasiquote
(~reference/attrs-content
:req-table (~docs/attr-table-from-data
:title "Request Attributes"
:attrs (unquote (get data "req-attrs")))
:beh-table (~docs/attr-table-from-data
:title "Behavior Attributes"
:attrs (unquote (get data "beh-attrs")))
:uniq-table (~docs/attr-table-from-data
:title "Unique to sx"
:attrs (unquote (get data "uniq-attrs"))))))))))
(define
reference-detail
(fn
(kind slug)
(if
(nil? slug)
nil
(case
kind
"attributes"
(let
((data (helper "attr-detail-data" slug)))
(if
(get data "attr-not-found")
(quasiquote (~reference/attr-not-found :slug (unquote slug)))
(quasiquote
(~reference/attr-detail-content
:title (unquote (get data "attr-title"))
:description (unquote (get data "attr-description"))
:demo (unquote (get data "attr-demo"))
:example-code (unquote (get data "attr-example"))
:handler-code (unquote (get data "attr-handler"))
:wire-placeholder-id (unquote (get data "attr-wire-id"))))))
"headers"
(let
((data (helper "header-detail-data" slug)))
(if
(get data "header-not-found")
(quasiquote (~reference/attr-not-found :slug (unquote slug)))
(quasiquote
(~reference/header-detail-content
:title (unquote (get data "header-title"))
:direction (unquote (get data "header-direction"))
:description (unquote (get data "header-description"))
:example-code (unquote (get data "header-example"))
:demo (unquote (get data "header-demo"))))))
"events"
(let
((data (helper "event-detail-data" slug)))
(if
(get data "event-not-found")
(quasiquote (~reference/attr-not-found :slug (unquote slug)))
(quasiquote
(~reference/event-detail-content
:title (unquote (get data "event-title"))
:description (unquote (get data "event-description"))
:example-code (unquote (get data "event-example"))
:demo (unquote (get data "event-demo"))))))
:else nil))))
(define
example
(fn
(slug)
(if
(nil? slug)
nil
(list (slug->component slug "~examples-content/example-" nil "")))))
(define sx-urls (fn (slug) (quote (~sx-urls/urls-content))))
(define cssx (make-page-fn "~cssx/overview-content" "~cssx/" nil "-content"))
(define
protocol
(make-page-fn "~protocols/wire-format-content" "~protocols/" nil "-content"))
(define
sx-pub
(fn (slug) (if (nil? slug) (quote (~sx-pub/overview-content)) nil)))
(define
sx-tools
(fn
(&key title &rest args)
(quasiquote
(~sx-tools/overview-content
:title (unquote (or title "SX Tools"))
(splice-unquote args)))))
(define tools (fn (content) (if (nil? content) nil content)))
(define
services
(fn
(&key title &rest args)
(quasiquote
(~services-tools/overview-content
:title (unquote (or title "Services"))
(splice-unquote args)))))
(define
reactive-runtime
(make-page-fn
"~reactive-runtime/overview-content"
"~reactive-runtime/"
nil
"-content"))
(define
native-browser
(make-page-fn
"~applications/native-browser/content"
"~applications/native-browser/"
nil
"-content"))
(define
essay
(make-page-fn "~essays/index/essays-index-content" "~essays/" "/essay-" ""))
(define
philosophy
(fn
(slug)
(if
(nil? slug)
(quote (~essays/philosophy-index/content))
(case
slug
"sx-manifesto"
(quote (~essay-sx-manifesto))
"godel-escher-bach"
(quote (~essays/godel-escher-bach/essay-godel-escher-bach))
"wittgenstein"
(quote (~essays/sx-and-wittgenstein/essay-sx-and-wittgenstein))
"dennett"
(quote (~essays/sx-and-dennett/essay-sx-and-dennett))
"existentialism"
(quote (~essays/s-existentialism/essay-s-existentialism))
"platonic-sx"
(quote (~essays/platonic-sx/essay-platonic-sx))
:else (quote (~essays/philosophy-index/content))))))
(define
plan
(make-page-fn
"~plans/index/plans-index-content"
"~plans/"
"/plan-"
"-content"))
(define capabilities (fn (&key title &rest args) (quasiquote (~geography/capabilities-content))))
(define modules (fn (&key title &rest args) (quasiquote (~geography/modules-content))))
(define eval-rules (fn (&key title &rest args) (quasiquote (~geography/eval-rules-content))))
(define sxtp (make-page-fn "~applications/sxtp/content" "~applications/sxtp/" nil "-content"))

View File

@@ -1,407 +1,260 @@
;; @client — send all define forms to browser for client-side use.
;; CSSX — computed CSS from s-expressions.
;;
;; Tailwind-style utility component using spread + collect primitives.
;; Use as a child of any element — injects classes onto the parent:
;;
;; (div (~cssx/tw "bg-yellow-199 text-violet-700 p-4 font-bold")
;; "content")
;;
;; (button (~cssx/tw "hover:bg-rose-500 md:text-xl")
;; "click me")
;;
;; Each token becomes a deterministic class + JIT CSS rule.
;; Rules are collected into the "cssx" bucket, flushed once by ~cssx/flush.
;; No wrapper elements, no per-element <style> tags.
;;
;; Reusable style variables:
;; (define fancy (~cssx/tw "font-bold text-violet-700 text-4xl"))
;; (div fancy "styled content")
;;
;; This is one instance of the CSSX component pattern — other styling
;; components are possible with different vocabulary.
;; =========================================================================
;; Colour data — hue/saturation bases + shade-to-lightness curve
;; =========================================================================
(define colour-bases
{"violet" {"h" 263 "s" 70}
"purple" {"h" 271 "s" 81}
"indigo" {"h" 239 "s" 84}
"blue" {"h" 217 "s" 91}
"sky" {"h" 199 "s" 89}
"cyan" {"h" 188 "s" 94}
"teal" {"h" 173 "s" 80}
"emerald" {"h" 160 "s" 84}
"green" {"h" 142 "s" 71}
"lime" {"h" 84 "s" 78}
"yellow" {"h" 48 "s" 96}
"amber" {"h" 38 "s" 92}
"orange" {"h" 25 "s" 95}
"red" {"h" 0 "s" 72}
"rose" {"h" 350 "s" 89}
"pink" {"h" 330 "s" 81}
"stone" {"h" 25 "s" 6}
"slate" {"h" 215 "s" 16}
"gray" {"h" 220 "s" 9}
"zinc" {"h" 240 "s" 5}
"neutral" {"h" 0 "s" 0}
"white" {"h" 0 "s" 0}
"black" {"h" 0 "s" 0}})
(define colour-bases {:orange {:s 95 :h 25} :cyan {:s 94 :h 188} :sky {:s 89 :h 199} :pink {:s 81 :h 330} :zinc {:s 5 :h 240} :amber {:s 92 :h 38} :neutral {:s 0 :h 0} :lime {:s 78 :h 84} :violet {:s 70 :h 263} :stone {:s 6 :h 25} :black {:s 0 :h 0} :teal {:s 80 :h 173} :gray {:s 9 :h 220} :red {:s 72 :h 0} :rose {:s 89 :h 350} :blue {:s 91 :h 217} :emerald {:s 84 :h 160} :green {:s 71 :h 142} :yellow {:s 96 :h 48} :purple {:s 81 :h 271} :indigo {:s 84 :h 239} :white {:s 0 :h 0} :slate {:s 16 :h 215}})
(define lerp (fn (a b t) (+ a (* t (- b a)))))
(define shade-to-lightness
(fn (shade)
(define
shade-to-lightness
(fn
(shade)
(cond
(<= shade 50) (lerp 100 97 (/ shade 50))
(<= shade 100) (lerp 97 93 (/ (- shade 50) 50))
(<= shade 200) (lerp 93 87 (/ (- shade 100) 100))
(<= shade 300) (lerp 87 77 (/ (- shade 200) 100))
(<= shade 400) (lerp 77 64 (/ (- shade 300) 100))
(<= shade 500) (lerp 64 53 (/ (- shade 400) 100))
(<= shade 600) (lerp 53 45 (/ (- shade 500) 100))
(<= shade 700) (lerp 45 38 (/ (- shade 600) 100))
(<= shade 800) (lerp 38 30 (/ (- shade 700) 100))
(<= shade 900) (lerp 30 21 (/ (- shade 800) 100))
(<= shade 950) (lerp 21 13 (/ (- shade 900) 50))
true 13)))
(<= shade 50)
(lerp 100 97 (/ shade 50))
(<= shade 100)
(lerp 97 93 (/ (- shade 50) 50))
(<= shade 200)
(lerp 93 87 (/ (- shade 100) 100))
(<= shade 300)
(lerp 87 77 (/ (- shade 200) 100))
(<= shade 400)
(lerp 77 64 (/ (- shade 300) 100))
(<= shade 500)
(lerp 64 53 (/ (- shade 400) 100))
(<= shade 600)
(lerp 53 45 (/ (- shade 500) 100))
(<= shade 700)
(lerp 45 38 (/ (- shade 600) 100))
(<= shade 800)
(lerp 38 30 (/ (- shade 700) 100))
(<= shade 900)
(lerp 30 21 (/ (- shade 800) 100))
(<= shade 950)
(lerp 21 13 (/ (- shade 900) 50))
true
13)))
(define colour
(fn (name shade)
(let ((base (get colour-bases name)))
(if (nil? base)
(define
colour
(fn
(name shade)
(let
((base (get colour-bases name)))
(if
(nil? base)
name
(let ((h (get base "h"))
(s (get base "s"))
(l (shade-to-lightness shade)))
(let
((h (get base "h"))
(s (get base "s"))
(l (shade-to-lightness shade)))
(str "hsl(" h "," s "%," (round l) "%)"))))))
;; =========================================================================
;; Lookup tables — all the value vocabularies
;; =========================================================================
(define cssx-colour-props {:bg "background-color" :border "border-color" :text "color"})
;; Colour property shorthands → CSS property name
(define cssx-colour-props
{"bg" "background-color"
"text" "color"
"border" "border-color"})
(define cssx-spacing-props {:ml "margin-left:{v}" :mr "margin-right:{v}" :mt "margin-top:{v}" :mb "margin-bottom:{v}" :pl "padding-left:{v}" :m "margin:{v}" :my "margin-top:{v};margin-bottom:{v}" :px "padding-left:{v};padding-right:{v}" :pb "padding-bottom:{v}" :pr "padding-right:{v}" :p "padding:{v}" :py "padding-top:{v};padding-bottom:{v}" :pt "padding-top:{v}" :mx "margin-left:{v};margin-right:{v}"})
;; Spacing property shorthands → CSS declaration template ({v} = computed value)
(define cssx-spacing-props
{"p" "padding:{v}"
"px" "padding-left:{v};padding-right:{v}"
"py" "padding-top:{v};padding-bottom:{v}"
"pt" "padding-top:{v}"
"pb" "padding-bottom:{v}"
"pl" "padding-left:{v}"
"pr" "padding-right:{v}"
"m" "margin:{v}"
"mx" "margin-left:{v};margin-right:{v}"
"my" "margin-top:{v};margin-bottom:{v}"
"mt" "margin-top:{v}"
"mb" "margin-bottom:{v}"
"ml" "margin-left:{v}"
"mr" "margin-right:{v}"})
(define cssx-sizes {:xs "font-size:0.75rem;line-height:1rem" :3xl "font-size:1.875rem;line-height:2.25rem" :7xl "font-size:4.5rem;line-height:1" :sm "font-size:0.875rem;line-height:1.25rem" :8xl "font-size:6rem;line-height:1" :xl "font-size:1.25rem;line-height:1.75rem" :6xl "font-size:3.75rem;line-height:1" :9xl "font-size:8rem;line-height:1" :5xl "font-size:3rem;line-height:1" :lg "font-size:1.125rem;line-height:1.75rem" :2xl "font-size:1.5rem;line-height:2rem" :base "font-size:1rem;line-height:1.5rem" :4xl "font-size:2.25rem;line-height:2.5rem"})
;; Named font sizes (Tailwind v3 scale)
(define cssx-sizes
{"xs" "font-size:0.75rem;line-height:1rem"
"sm" "font-size:0.875rem;line-height:1.25rem"
"base" "font-size:1rem;line-height:1.5rem"
"lg" "font-size:1.125rem;line-height:1.75rem"
"xl" "font-size:1.25rem;line-height:1.75rem"
"2xl" "font-size:1.5rem;line-height:2rem"
"3xl" "font-size:1.875rem;line-height:2.25rem"
"4xl" "font-size:2.25rem;line-height:2.5rem"
"5xl" "font-size:3rem;line-height:1"
"6xl" "font-size:3.75rem;line-height:1"
"7xl" "font-size:4.5rem;line-height:1"
"8xl" "font-size:6rem;line-height:1"
"9xl" "font-size:8rem;line-height:1"})
(define cssx-weights {:light "300" :semibold "600" :bold "700" :extrabold "800" :black "900" :extralight "200" :thin "100" :medium "500" :normal "400"})
;; Named font weights
(define cssx-weights
{"thin" "100"
"extralight" "200"
"light" "300"
"normal" "400"
"medium" "500"
"semibold" "600"
"bold" "700"
"extrabold" "800"
"black" "900"})
(define cssx-families {:mono "ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace" :sans "ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,\"Noto Sans\",sans-serif" :serif "ui-serif,Georgia,Cambria,\"Times New Roman\",Times,serif"})
;; Named font families
(define cssx-families
{"sans" "ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,\"Noto Sans\",sans-serif"
"serif" "ui-serif,Georgia,Cambria,\"Times New Roman\",Times,serif"
"mono" "ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace"})
(define cssx-alignments {:center true :left true :right true :justify true})
;; Text alignment keywords
(define cssx-alignments
{"left" true "center" true "right" true "justify" true})
(define cssx-displays {:flex "flex" :grid "grid" :inline-block "inline-block" :inline "inline" :hidden "none" :block "block" :inline-flex "inline-flex"})
;; Display keywords → CSS value
(define cssx-displays
{"block" "block"
"inline" "inline"
"inline-block" "inline-block"
"flex" "flex"
"inline-flex" "inline-flex"
"grid" "grid"
"hidden" "none"})
(define cssx-max-widths {:xs "20rem" :3xl "48rem" :7xl "80rem" :sm "24rem" :xl "36rem" :full "100%" :md "28rem" :6xl "72rem" :prose "65ch" :5xl "64rem" :lg "32rem" :2xl "42rem" :4xl "56rem" :none "none" :screen "100vw"})
;; Named max-widths (Tailwind scale)
(define cssx-max-widths
{"xs" "20rem" "sm" "24rem" "md" "28rem"
"lg" "32rem" "xl" "36rem" "2xl" "42rem"
"3xl" "48rem" "4xl" "56rem" "5xl" "64rem"
"6xl" "72rem" "7xl" "80rem"
"full" "100%" "none" "none"
"prose" "65ch" "screen" "100vw"})
(define cssx-breakpoints {:sm "640px" :xl "1280px" :md "768px" :lg "1024px" :2xl "1536px"})
;; Responsive breakpoints (mobile-first min-width)
(define cssx-breakpoints
{"sm" "640px"
"md" "768px"
"lg" "1024px"
"xl" "1280px"
"2xl" "1536px"})
(define cssx-states {:focus ":focus" :first ":first-child" :hover ":hover" :focus-visible ":focus-visible" :last ":last-child" :active ":active" :focus-within ":focus-within"})
;; Pseudo-class states
(define cssx-states
{"hover" ":hover"
"focus" ":focus"
"active" ":active"
"focus-within" ":focus-within"
"focus-visible" ":focus-visible"
"first" ":first-child"
"last" ":last-child"})
;; =========================================================================
;; Utility resolver — token string → CSS declarations
;; =========================================================================
;; Spacing value: number → rem, "auto" → "auto", "px" → "1px"
(define cssx-spacing-value
(fn (v)
(define
cssx-spacing-value
(fn
(v)
(cond
(= v "auto") "auto"
(= v "px") "1px"
(= v "0") "0px"
true (let ((n (parse-int v nil)))
(if (nil? n) nil
(str (* n 0.25) "rem"))))))
(= v "auto")
"auto"
(= v "px")
"1px"
(= v "0")
"0px"
true
(let
((n (parse-int v nil)))
(if (nil? n) nil (str (* n 0.25) "rem"))))))
;; Replace {v} in a template string with a value
(define cssx-template
(fn (tmpl v)
(let ((i (index-of tmpl "{v}")))
(if (< i 0) tmpl
(let ((result (str (substring tmpl 0 i) v (substring tmpl (+ i 3) (len tmpl)))))
;; Handle templates with multiple {v} (e.g. padding-left:{v};padding-right:{v})
(let ((j (index-of result "{v}")))
(if (< j 0) result
(str (substring result 0 j) v (substring result (+ j 3) (len result))))))))))
(define
cssx-template
(fn
(tmpl v)
(let
((i (index-of tmpl "{v}")))
(if
(< i 0)
tmpl
(let
((result (str (substring tmpl 0 i) v (substring tmpl (+ i 3) (len tmpl)))))
(let
((j (index-of result "{v}")))
(if
(< j 0)
result
(str
(substring result 0 j)
v
(substring result (+ j 3) (len result))))))))))
;; Resolve a base utility token (no state/bp prefix) → CSS declaration string or nil.
;; Tries matchers in order: colour, text-size, text-align, font, spacing, display, max-w, rounded, opacity.
(define cssx-resolve
(fn (token)
(let ((parts (split token "-")))
(if (empty? parts) nil
(let ((head (first parts))
(rest (slice parts 1)))
(define
cssx-resolve
(fn
(token)
(let
((parts (split token "-")))
(if
(empty? parts)
nil
(let
((head (first parts)) (rest (slice parts 1)))
(cond
;; ---------------------------------------------------------
;; Colour utilities: bg-{colour}-{shade}, text-{colour}-{shade}, border-{colour}-{shade}
;; ---------------------------------------------------------
(and (get cssx-colour-props head)
(>= (len rest) 2)
(not (nil? (parse-int (last rest) nil)))
(not (nil? (get colour-bases (join "-" (slice rest 0 (- (len rest) 1)))))))
(let ((css-prop (get cssx-colour-props head))
(cname (join "-" (slice rest 0 (- (len rest) 1))))
(shade (parse-int (last rest) 0)))
(and
(get cssx-colour-props head)
(>= (len rest) 2)
(not (nil? (parse-int (last rest) nil)))
(not
(nil?
(get
colour-bases
(join "-" (slice rest 0 (- (len rest) 1)))))))
(let
((css-prop (get cssx-colour-props head))
(cname (join "-" (slice rest 0 (- (len rest) 1))))
(shade (parse-int (last rest) 0)))
(str css-prop ":" (colour cname shade)))
;; ---------------------------------------------------------
;; Text size: text-{size-name} (e.g. text-xl, text-2xl)
;; ---------------------------------------------------------
(and (= head "text")
(= (len rest) 1)
(not (nil? (get cssx-sizes (first rest)))))
(and
(= head "text")
(= (len rest) 1)
(not (nil? (get cssx-sizes (first rest)))))
(get cssx-sizes (first rest))
;; Also handle text-2xl etc where rest might be ("2xl") — covered above
;; But also "text-sm" etc — covered above since rest is ("sm")
;; ---------------------------------------------------------
;; Text alignment: text-left, text-center, text-right, text-justify
;; ---------------------------------------------------------
(and (= head "text")
(= (len rest) 1)
(get cssx-alignments (first rest)))
(and
(= head "text")
(= (len rest) 1)
(get cssx-alignments (first rest)))
(str "text-align:" (first rest))
;; ---------------------------------------------------------
;; Font weight: font-bold, font-semibold, etc.
;; ---------------------------------------------------------
(and (= head "font")
(= (len rest) 1)
(not (nil? (get cssx-weights (first rest)))))
(and
(= head "font")
(= (len rest) 1)
(not (nil? (get cssx-weights (first rest)))))
(str "font-weight:" (get cssx-weights (first rest)))
;; ---------------------------------------------------------
;; Font family: font-sans, font-serif, font-mono
;; ---------------------------------------------------------
(and (= head "font")
(= (len rest) 1)
(not (nil? (get cssx-families (first rest)))))
(and
(= head "font")
(= (len rest) 1)
(not (nil? (get cssx-families (first rest)))))
(str "font-family:" (get cssx-families (first rest)))
;; ---------------------------------------------------------
;; Spacing: p-4, px-2, mt-8, mx-auto, etc.
;; ---------------------------------------------------------
(and (get cssx-spacing-props head)
(= (len rest) 1))
(let ((tmpl (get cssx-spacing-props head))
(v (cssx-spacing-value (first rest))))
(and (get cssx-spacing-props head) (= (len rest) 1))
(let
((tmpl (get cssx-spacing-props head))
(v (cssx-spacing-value (first rest))))
(if (nil? v) nil (cssx-template tmpl v)))
;; ---------------------------------------------------------
;; Display: block, flex, grid, hidden, inline, inline-block
;; ---------------------------------------------------------
(and (= (len parts) 1)
(not (nil? (get cssx-displays head))))
(and (= (len parts) 1) (not (nil? (get cssx-displays head))))
(str "display:" (get cssx-displays head))
;; Inline-block, inline-flex (multi-word)
(and (= (len parts) 2)
(not (nil? (get cssx-displays token))))
(and (= (len parts) 2) (not (nil? (get cssx-displays token))))
(str "display:" (get cssx-displays token))
;; ---------------------------------------------------------
;; Max-width: max-w-xl, max-w-3xl, max-w-prose
;; ---------------------------------------------------------
(and (= head "max")
(>= (len rest) 2)
(= (first rest) "w"))
(let ((val-name (join "-" (slice rest 1)))
(val (get cssx-max-widths val-name)))
(and (= head "max") (>= (len rest) 2) (= (first rest) "w"))
(let
((val-name (join "-" (slice rest 1)))
(val (get cssx-max-widths val-name)))
(if (nil? val) nil (str "max-width:" val)))
;; ---------------------------------------------------------
;; Rounded: rounded, rounded-lg, rounded-full, etc.
;; ---------------------------------------------------------
(= head "rounded")
(cond
(empty? rest) "border-radius:0.25rem"
(= (first rest) "none") "border-radius:0"
(= (first rest) "sm") "border-radius:0.125rem"
(= (first rest) "md") "border-radius:0.375rem"
(= (first rest) "lg") "border-radius:0.5rem"
(= (first rest) "xl") "border-radius:0.75rem"
(= (first rest) "2xl") "border-radius:1rem"
(= (first rest) "3xl") "border-radius:1.5rem"
(= (first rest) "full") "border-radius:9999px"
true nil)
;; ---------------------------------------------------------
;; Opacity: opacity-{n} (0-100)
;; ---------------------------------------------------------
(and (= head "opacity")
(= (len rest) 1))
(let ((n (parse-int (first rest) nil)))
(empty? rest)
"border-radius:0.25rem"
(= (first rest) "none")
"border-radius:0"
(= (first rest) "sm")
"border-radius:0.125rem"
(= (first rest) "md")
"border-radius:0.375rem"
(= (first rest) "lg")
"border-radius:0.5rem"
(= (first rest) "xl")
"border-radius:0.75rem"
(= (first rest) "2xl")
"border-radius:1rem"
(= (first rest) "3xl")
"border-radius:1.5rem"
(= (first rest) "full")
"border-radius:9999px"
true
nil)
(and (= head "opacity") (= (len rest) 1))
(let
((n (parse-int (first rest) nil)))
(if (nil? n) nil (str "opacity:" (/ n 100))))
;; ---------------------------------------------------------
;; Width/height: w-{n}, h-{n}, w-full, h-full, h-screen
;; ---------------------------------------------------------
(and (or (= head "w") (= head "h"))
(= (len rest) 1))
(let ((prop (if (= head "w") "width" "height"))
(val (first rest)))
(and (or (= head "w") (= head "h")) (= (len rest) 1))
(let
((prop (if (= head "w") "width" "height"))
(val (first rest)))
(cond
(= val "full") (str prop ":100%")
(= val "screen") (str prop (if (= head "w") ":100vw" ":100vh"))
(= val "auto") (str prop ":auto")
(= val "min") (str prop ":min-content")
(= val "max") (str prop ":max-content")
(= val "fit") (str prop ":fit-content")
true (let ((n (parse-int val nil)))
(if (nil? n) nil
(str prop ":" (* n 0.25) "rem")))))
;; ---------------------------------------------------------
;; Gap: gap-{n}
;; ---------------------------------------------------------
(and (= head "gap")
(= (len rest) 1))
(let ((v (cssx-spacing-value (first rest))))
(= val "full")
(str prop ":100%")
(= val "screen")
(str prop (if (= head "w") ":100vw" ":100vh"))
(= val "auto")
(str prop ":auto")
(= val "min")
(str prop ":min-content")
(= val "max")
(str prop ":max-content")
(= val "fit")
(str prop ":fit-content")
true
(let
((n (parse-int val nil)))
(if (nil? n) nil (str prop ":" (* n 0.25) "rem")))))
(and (= head "gap") (= (len rest) 1))
(let
((v (cssx-spacing-value (first rest))))
(if (nil? v) nil (str "gap:" v)))
;; ---------------------------------------------------------
;; Text decoration: underline, no-underline, line-through
;; ---------------------------------------------------------
(and (= (len parts) 1)
(or (= head "underline") (= head "overline") (= head "line-through")))
(and
(= (len parts) 1)
(or
(= head "underline")
(= head "overline")
(= head "line-through")))
(str "text-decoration-line:" head)
(and (= (len parts) 2) (= head "no") (= (first rest) "underline"))
(and
(= (len parts) 2)
(= head "no")
(= (first rest) "underline"))
"text-decoration-line:none"
;; ---------------------------------------------------------
;; Cursor: cursor-pointer, cursor-default, etc.
;; ---------------------------------------------------------
(and (= head "cursor") (= (len rest) 1))
(str "cursor:" (first rest))
;; ---------------------------------------------------------
;; Overflow: overflow-hidden, overflow-auto, etc.
;; ---------------------------------------------------------
(and (= head "overflow") (= (len rest) 1))
(str "overflow:" (first rest))
;; ---------------------------------------------------------
;; Transition: transition, transition-colors, etc.
;; ---------------------------------------------------------
(and (= head "transition") (empty? rest))
"transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:150ms"
(and (= head "transition") (= (first rest) "colors"))
"transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:150ms"
true
nil))))))
;; ---------------------------------------------------------
;; Fallback: unrecognised → nil (skip)
;; ---------------------------------------------------------
true nil))))))
;; =========================================================================
;; Token processor — full token with optional state/bp prefixes
;; =========================================================================
;; Process one token string → {:cls "sx-..." :rule ".sx-...{...}"} or nil
;; Token format: [bp:]?[state:]?utility
;; Examples: "bg-yellow-199", "hover:bg-rose-500", "md:hover:text-xl"
(define cssx-process-token
(fn (token)
(let ((colon-parts (split token ":"))
(n (len colon-parts)))
;; Extract state, bp, and base utility from colon-separated parts
(let ((bp nil) (state nil) (base nil))
;; 1 part: just utility
;; 2 parts: modifier:utility (could be bp or state)
;; 3 parts: bp:state:utility
(define
cssx-process-token
(fn
(token)
(let
((colon-parts (split token ":")) (n (len colon-parts)))
(let
((bp nil) (state nil) (base nil))
(cond
(= n 1)
(do (set! base (first colon-parts)))
(= n 2)
(let ((prefix (first colon-parts)))
(let
((prefix (first colon-parts)))
(set! base (last colon-parts))
(if (not (nil? (get cssx-breakpoints prefix)))
(if
(not (nil? (get cssx-breakpoints prefix)))
(set! bp prefix)
(set! state prefix)))
(>= n 3)
@@ -409,98 +262,39 @@
(set! bp (first colon-parts))
(set! state (nth colon-parts 1))
(set! base (last colon-parts))))
(let
((css (cssx-resolve base)))
(if
(nil? css)
nil
(let
((cls (str "sx-" (join "-" (split token ":"))))
(pseudo
(if
(nil? state)
""
(or (get cssx-states state) (str ":" state))))
(decl (str "." cls pseudo "{" css "}")))
(if
(nil? bp)
{:rule decl :cls cls}
(let
((min-w (or (get cssx-breakpoints bp) bp)))
{:rule (str "@media(min-width:" min-w "){" decl "}") :cls cls})))))))))
;; Resolve the base utility to CSS declarations
(let ((css (cssx-resolve base)))
(if (nil? css) nil
(let ((cls (str "sx-" (join "-" (split token ":"))))
(pseudo (if (nil? state) ""
(or (get cssx-states state) (str ":" state))))
(decl (str "." cls pseudo "{" css "}")))
(if (nil? bp)
{"cls" cls "rule" decl}
(let ((min-w (or (get cssx-breakpoints bp) bp)))
{"cls" cls
"rule" (str "@media(min-width:" min-w "){" decl "}")})))))))))
;; =========================================================================
;; tw — Tailwind-style inline style string from utility tokens
;;
;; Same token format as ~cssx/tw, returns a CSS declaration string for :style.
;; Cannot do hover/responsive (use ~cssx/tw for those).
;;
;; Usage:
;; (div :style (tw "bg-yellow-199 text-violet-700 p-4 font-bold")
;; "content")
;; =========================================================================
(define tw
(fn (tokens-str)
(let ((tokens (split (or tokens-str "") " "))
(parts (list)))
(for-each (fn (tok)
(when (not (= tok ""))
(let ((css (cssx-resolve tok)))
(when (not (nil? css))
(append! parts (str css ";"))))))
(define
tw
(fn
(tokens-str)
(let
((tokens (split (or tokens-str "") " ")) (parts (list)))
(for-each
(fn
(tok)
(when
(not (= tok ""))
(let
((css (cssx-resolve tok)))
(when (not (nil? css)) (append! parts (str css ";"))))))
tokens)
(join "" parts))))
;; =========================================================================
;; ~cssx/tw — spread component that injects JIT classes onto parent element
;;
;; Usage — as a child of any element:
;; (div (~cssx/tw "bg-yellow-199 text-violet-700 p-4 font-bold")
;; (h1 "styled content"))
;;
;; (button (~cssx/tw "hover:bg-rose-500 focus:border-blue-400")
;; "interactive")
;;
;; Returns a spread value that merges :class and :data-tw onto the parent
;; element. Collects CSS rules into the "cssx" bucket for a single global
;; <style> flush. No wrapper element, no per-element <style> tags.
;;
;; Reusable as variables:
;; (define important (~cssx/tw "font-bold text-4xl"))
;; (div important "the queen is dead")
;;
;; Multiple spreads merge naturally:
;; (div (~cssx/tw "bg-red-500") (~cssx/tw "p-4") "content")
;; =========================================================================
(defcomp ~cssx/tw (&key tokens)
(let ((token-list (filter (fn (t) (not (= t "")))
(split (or tokens "") " ")))
(results (map cssx-process-token token-list))
(valid (filter (fn (r) (not (nil? r))) results))
(classes (map (fn (r) (get r "cls")) valid))
(rules (map (fn (r) (get r "rule")) valid))
(_ (for-each (fn (rule) (collect! "cssx" rule)) rules)))
;; Return spread: injects class + data-tw onto parent element
(if (empty? classes)
nil
(make-spread {"class" (join " " classes)
"data-tw" (or tokens "")}))))
;; =========================================================================
;; ~cssx/flush — emit collected CSS rules as a single <style> tag
;;
;; Place once in the page (typically in the layout, before </body>).
;; Emits all accumulated CSSX rules and clears the bucket.
;;
;; Usage:
;; (~cssx/flush)
;; =========================================================================
(defcomp ~cssx/flush () :affinity :client
(let ((rules (collected "cssx"))
(head-style (dom-query "#sx-css")))
;; On client: append rules to <style id="sx-css"> in <head>.
;; On server: head-style is nil (no DOM). Don't clear the bucket —
;; the shell's <head> template reads collected("cssx") and emits them.
(when head-style
(clear-collected! "cssx")
(when (not (empty? rules))
(dom-set-prop head-style "textContent"
(str (dom-get-prop head-style "textContent") (join "" rules)))))))

View File

@@ -36,11 +36,8 @@
(= (len rest) 2)
(or (= (first rest) "x") (= (first rest) "y")))
(let
((v (tw-spacing-value (nth rest 1))) (dir (first rest)))
(if
(nil? v)
nil
(if (= dir "x") (str "column-gap:" v) (str "row-gap:" v))))
((v (tw-spacing-value (last rest))) (dir (first rest)))
(if (nil? v) nil (if (= dir "x") {:suffix ">*+*" :css (str "margin-left:" v)} {:suffix ">*+*" :css (str "margin-top:" v)})))
(and (= head "flex") (empty? rest))
"display:flex"
(and (= head "flex") (= (len rest) 1))