Phase 4: Client-side routing for SX expression URLs

Add sx-url-to-path to router.sx that converts SX expression URLs to
old-style slash paths for route matching. find-matching-route now
transparently handles both formats — the browser URL stays as the SX
expression while matching uses the equivalent old-style path.

/(language.(doc.introduction)) → /language/docs/introduction for matching
but pushState keeps the SX URL in the browser bar.

- router.sx: add _fn-to-segment (doc→docs, etc.), sx-url-to-path
- router.sx: modify find-matching-route to convert SX URLs before matching
- Rebootstrap sx-browser.js and sx_ref.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 10:27:28 +00:00
parent feecbb66ba
commit 2076e1805f
3 changed files with 86 additions and 20 deletions

View File

@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-11T23:22:03Z";
var SX_VERSION = "2026-03-12T10:26:23Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -3706,7 +3706,9 @@ callExpr.push(dictGet(kwargs, k)); } }
// find-matching-route
var findMatchingRoute = function(path, routes) { return (function() {
var pathSegs = splitPathSegments(path);
var matchPath = (isSxTruthy(startsWith(path, "/(")) ? sxOr(sxUrlToPath(path), path) : path);
return (function() {
var pathSegs = splitPathSegments(matchPath);
var result = NIL;
{ var _c = routes; for (var _i = 0; _i < _c.length; _i++) { var route = _c[_i]; if (isSxTruthy(isNil(result))) {
(function() {
@@ -3719,8 +3721,24 @@ callExpr.push(dictGet(kwargs, k)); } }
})();
} } }
return result;
})();
})(); };
// _fn-to-segment
var _fnToSegment = function(name) { return (function() { var _m = name; if (_m == "doc") return "docs"; if (_m == "spec") return "specs"; if (_m == "bootstrapper") return "bootstrappers"; if (_m == "test") return "testing"; if (_m == "example") return "examples"; if (_m == "protocol") return "protocols"; if (_m == "essay") return "essays"; if (_m == "plan") return "plans"; if (_m == "reference-detail") return "reference"; return name; })(); };
// sx-url-to-path
var sxUrlToPath = function(url) { return (isSxTruthy(!isSxTruthy((isSxTruthy(startsWith(url, "/(")) && endsWith(url, ")")))) ? NIL : (function() {
var inner = slice(url, 2, (len(url) - 1));
return (function() {
var s = replace_(replace_(replace_(inner, ".", "/"), "(", ""), ")", "");
return (function() {
var segs = filter(function(s) { return !isSxTruthy(isEmpty(s)); }, split(s, "/"));
return (String("/") + String(join("/", map(_fnToSegment, segs))));
})();
})();
})()); };
// === Transpiled from signals (reactive signal runtime) ===
@@ -4058,12 +4076,20 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
function domCallMethod() {
var obj = arguments[0], method = arguments[1];
var args = Array.prototype.slice.call(arguments, 2);
console.log("[sx] dom-call-method:", obj, method, args);
if (obj && typeof obj[method] === 'function') {
try { return obj[method].apply(obj, args); }
catch(e) { console.error("[sx] dom-call-method error:", e); return NIL; }
}
console.warn("[sx] dom-call-method: method not found or obj null", obj, method);
return NIL;
}
// Post a message to an iframe's contentWindow without exposing the cross-origin
// Window object to the SX evaluator (which would trigger _thunk access errors).
function domPostMessage(iframe, msg, origin) {
try {
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage(msg, origin || '*');
}
} catch(e) { console.error("[sx] domPostMessage error:", e); }
return NIL;
}
@@ -5272,6 +5298,7 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
PRIMITIVES["dom-get-prop"] = domGetProp;
PRIMITIVES["dom-set-prop"] = domSetProp;
PRIMITIVES["dom-call-method"] = domCallMethod;
PRIMITIVES["dom-post-message"] = domPostMessage;
PRIMITIVES["stop-propagation"] = stopPropagation_;
PRIMITIVES["error-message"] = errorMessage;
PRIMITIVES["schedule-idle"] = scheduleIdle;