From f70861c175e31669381a88b601e0dce6e94c9801 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 6 Mar 2026 20:31:23 +0000 Subject: [PATCH] Try client-side routing for all sx-get link clicks, not just boost links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bind-event now checks tryClientRoute before executeRequest for GET clicks on links. Previously only boost links (inside [sx-boost] containers) attempted client routing — explicit sx-get links like ~nav-link always hit the network. Now essay/doc nav links render client-side when possible. Co-Authored-By: Claude Opus 4.6 --- shared/static/scripts/sx-browser.js | 285 +--------------------------- shared/sx/ref/orchestration.sx | 26 ++- 2 files changed, 19 insertions(+), 292 deletions(-) diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 46eb395c..eb3254ed 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -512,92 +512,6 @@ return NIL; } - // ========================================================================= - // Platform: deps module — component dependency analysis - // ========================================================================= - - function componentDeps(c) { - return c.deps ? c.deps.slice() : []; - } - - function componentSetDeps(c, deps) { - c.deps = deps; - } - - function componentCssClasses(c) { - return c.cssClasses ? c.cssClasses.slice() : []; - } - - function envComponents(env) { - var names = []; - for (var k in env) { - var v = env[k]; - if (v && (v._component || v._macro)) names.push(k); - } - return names; - } - - function regexFindAll(pattern, source) { - var re = new RegExp(pattern, "g"); - var results = []; - var m; - while ((m = re.exec(source)) !== null) { - if (m[1] !== undefined) results.push(m[1]); - else results.push(m[0]); - } - return results; - } - - function scanCssClasses(source) { - var classes = {}; - var result = []; - var m; - var re1 = /:class\s+"([^"]*)"/g; - while ((m = re1.exec(source)) !== null) { - var parts = m[1].split(/\s+/); - for (var i = 0; i < parts.length; i++) { - if (parts[i] && !classes[parts[i]]) { - classes[parts[i]] = true; - result.push(parts[i]); - } - } - } - var re2 = /:class\s+\(str\s+((?:"[^"]*"\s*)+)\)/g; - while ((m = re2.exec(source)) !== null) { - var re3 = /"([^"]*)"/g; - var m2; - while ((m2 = re3.exec(m[1])) !== null) { - var parts2 = m2[1].split(/\s+/); - for (var j = 0; j < parts2.length; j++) { - if (parts2[j] && !classes[parts2[j]]) { - classes[parts2[j]] = true; - result.push(parts2[j]); - } - } - } - } - var re4 = /;;\s*@css\s+(.+)/g; - while ((m = re4.exec(source)) !== null) { - var parts3 = m[1].split(/\s+/); - for (var k = 0; k < parts3.length; k++) { - if (parts3[k] && !classes[parts3[k]]) { - classes[parts3[k]] = true; - result.push(parts3[k]); - } - } - } - return result; - } - - function componentIoRefs(c) { - return c.ioRefs ? c.ioRefs.slice() : []; - } - - function componentSetIoRefs(c, refs) { - c.ioRefs = refs; - } - - // ========================================================================= // Platform interface — Parser // ========================================================================= @@ -1935,7 +1849,7 @@ return postSwap(target); }); return (isSxTruthy((val == lastVal)) ? (shouldFire = false) : (lastVal = val)); })(); } - return (isSxTruthy(shouldFire) ? ((isSxTruthy(sxOr((eventName == "submit"), (isSxTruthy((eventName == "click")) && domHasAttr(el, "href")))) ? preventDefault_(e) : NIL), (isSxTruthy(get(mods, "delay")) ? (clearTimeout_(timer), (timer = setTimeout_(function() { return executeRequest(el, verbInfo, NIL); }, get(mods, "delay")))) : executeRequest(el, verbInfo, NIL))) : NIL); + return (isSxTruthy(shouldFire) ? ((isSxTruthy(sxOr((eventName == "submit"), (isSxTruthy((eventName == "click")) && domHasAttr(el, "href")))) ? preventDefault_(e) : NIL), (isSxTruthy((isSxTruthy((eventName == "click")) && isSxTruthy((get(verbInfo, "method") == "GET")) && isSxTruthy(domHasAttr(el, "href")) && isSxTruthy(!get(mods, "delay")) && tryClientRoute(urlPathname(get(verbInfo, "url"))))) ? (browserPushState(get(verbInfo, "url")), browserScrollTo(0, 0)) : (isSxTruthy(get(mods, "delay")) ? (clearTimeout_(timer), (timer = setTimeout_(function() { return executeRequest(el, verbInfo, NIL); }, get(mods, "delay")))) : executeRequest(el, verbInfo, NIL)))) : NIL); })(); }, (isSxTruthy(get(mods, "once")) ? {["once"]: true} : NIL)) : NIL); })(); }; @@ -2438,189 +2352,6 @@ callExpr.push(dictGet(kwargs, k)); } } var bootInit = function() { return (initCssTracking(), initStyleDict(), processSxScripts(NIL), processPageScripts(), sxHydrateElements(NIL), processElements(NIL)); }; - // === Transpiled from deps (component dependency analysis) === - - // scan-refs - var scanRefs = function(node) { return (function() { - var refs = []; - scanRefsWalk(node, refs); - return refs; -})(); }; - - // scan-refs-walk - var scanRefsWalk = function(node, refs) { return (isSxTruthy((typeOf(node) == "symbol")) ? (function() { - var name = symbolName(node); - return (isSxTruthy(startsWith(name, "~")) ? (isSxTruthy(!contains(refs, name)) ? append_b(refs, name) : NIL) : NIL); -})() : (isSxTruthy((typeOf(node) == "list")) ? forEach(function(item) { return scanRefsWalk(item, refs); }, node) : (isSxTruthy((typeOf(node) == "dict")) ? forEach(function(key) { return scanRefsWalk(dictGet(node, key), refs); }, keys(node)) : NIL))); }; - - // transitive-deps-walk - var transitiveDepsWalk = function(n, seen, env) { return (isSxTruthy(!contains(seen, n)) ? (append_b(seen, n), (function() { - var val = envGet(env, n); - return (isSxTruthy((typeOf(val) == "component")) ? forEach(function(ref) { return transitiveDepsWalk(ref, seen, env); }, scanRefs(componentBody(val))) : (isSxTruthy((typeOf(val) == "macro")) ? forEach(function(ref) { return transitiveDepsWalk(ref, seen, env); }, scanRefs(macroBody(val))) : NIL)); -})()) : NIL); }; - - // transitive-deps - var transitiveDeps = function(name, env) { return (function() { - var seen = []; - var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name))); - transitiveDepsWalk(key, seen, env); - return filter(function(x) { return !(x == key); }, seen); -})(); }; - - // compute-all-deps - var computeAllDeps = function(env) { return forEach(function(name) { return (function() { - var val = envGet(env, name); - return (isSxTruthy((typeOf(val) == "component")) ? componentSetDeps(val, transitiveDeps(name, env)) : NIL); -})(); }, envComponents(env)); }; - - // scan-components-from-source - var scanComponentsFromSource = function(source) { return (function() { - var matches = regexFindAll("\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)", source); - return map(function(m) { return (String("~") + String(m)); }, matches); -})(); }; - - // components-needed - var componentsNeeded = function(pageSource, env) { return (function() { - var direct = scanComponentsFromSource(pageSource); - var allNeeded = []; - { var _c = direct; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; if (isSxTruthy(!contains(allNeeded, name))) { - allNeeded.push(name); -} -(function() { - var val = envGet(env, name); - return (function() { - var deps = (isSxTruthy((isSxTruthy((typeOf(val) == "component")) && !isEmpty(componentDeps(val)))) ? componentDeps(val) : transitiveDeps(name, env)); - return forEach(function(dep) { return (isSxTruthy(!contains(allNeeded, dep)) ? append_b(allNeeded, dep) : NIL); }, deps); -})(); -})(); } } - return allNeeded; -})(); }; - - // page-component-bundle - var pageComponentBundle = function(pageSource, env) { return componentsNeeded(pageSource, env); }; - - // page-css-classes - var pageCssClasses = function(pageSource, env) { return (function() { - var needed = componentsNeeded(pageSource, env); - var classes = []; - { var _c = needed; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; (function() { - var val = envGet(env, name); - return (isSxTruthy((typeOf(val) == "component")) ? forEach(function(cls) { return (isSxTruthy(!contains(classes, cls)) ? append_b(classes, cls) : NIL); }, componentCssClasses(val)) : NIL); -})(); } } - { var _c = scanCssClasses(pageSource); for (var _i = 0; _i < _c.length; _i++) { var cls = _c[_i]; if (isSxTruthy(!contains(classes, cls))) { - classes.push(cls); -} } } - return classes; -})(); }; - - // scan-io-refs-walk - var scanIoRefsWalk = function(node, ioNames, refs) { return (isSxTruthy((typeOf(node) == "symbol")) ? (function() { - var name = symbolName(node); - return (isSxTruthy(contains(ioNames, name)) ? (isSxTruthy(!contains(refs, name)) ? append_b(refs, name) : NIL) : NIL); -})() : (isSxTruthy((typeOf(node) == "list")) ? forEach(function(item) { return scanIoRefsWalk(item, ioNames, refs); }, node) : (isSxTruthy((typeOf(node) == "dict")) ? forEach(function(key) { return scanIoRefsWalk(dictGet(node, key), ioNames, refs); }, keys(node)) : NIL))); }; - - // scan-io-refs - var scanIoRefs = function(node, ioNames) { return (function() { - var refs = []; - scanIoRefsWalk(node, ioNames, refs); - return refs; -})(); }; - - // transitive-io-refs-walk - var transitiveIoRefsWalk = function(n, seen, allRefs, env, ioNames) { return (isSxTruthy(!contains(seen, n)) ? (append_b(seen, n), (function() { - var val = envGet(env, n); - return (isSxTruthy((typeOf(val) == "component")) ? (forEach(function(ref) { return (isSxTruthy(!contains(allRefs, ref)) ? append_b(allRefs, ref) : NIL); }, scanIoRefs(componentBody(val), ioNames)), forEach(function(dep) { return transitiveIoRefsWalk(dep, seen, allRefs, env, ioNames); }, scanRefs(componentBody(val)))) : (isSxTruthy((typeOf(val) == "macro")) ? (forEach(function(ref) { return (isSxTruthy(!contains(allRefs, ref)) ? append_b(allRefs, ref) : NIL); }, scanIoRefs(macroBody(val), ioNames)), forEach(function(dep) { return transitiveIoRefsWalk(dep, seen, allRefs, env, ioNames); }, scanRefs(macroBody(val)))) : NIL)); -})()) : NIL); }; - - // transitive-io-refs - var transitiveIoRefs = function(name, env, ioNames) { return (function() { - var allRefs = []; - var seen = []; - var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name))); - transitiveIoRefsWalk(key, seen, allRefs, env, ioNames); - return allRefs; -})(); }; - - // compute-all-io-refs - var computeAllIoRefs = function(env, ioNames) { return forEach(function(name) { return (function() { - var val = envGet(env, name); - return (isSxTruthy((typeOf(val) == "component")) ? componentSetIoRefs(val, transitiveIoRefs(name, env, ioNames)) : NIL); -})(); }, envComponents(env)); }; - - // component-pure? - var componentPure_p = function(name, env, ioNames) { return isEmpty(transitiveIoRefs(name, env, ioNames)); }; - - - // === Transpiled from router (client-side route matching) === - - // split-path-segments - var splitPathSegments = function(path) { return (function() { - var trimmed = (isSxTruthy(startsWith(path, "/")) ? slice(path, 1) : path); - return (function() { - var trimmed2 = (isSxTruthy((isSxTruthy(!isEmpty(trimmed)) && endsWith(trimmed, "/"))) ? slice(trimmed, 0, (length(trimmed) - 1)) : trimmed); - return (isSxTruthy(isEmpty(trimmed2)) ? [] : split(trimmed2, "/")); -})(); -})(); }; - - // make-route-segment - var makeRouteSegment = function(seg) { return (isSxTruthy((isSxTruthy(startsWith(seg, "<")) && endsWith(seg, ">"))) ? (function() { - var paramName = slice(seg, 1, (length(seg) - 1)); - return (function() { - var d = {}; - d["type"] = "param"; - d["value"] = paramName; - return d; -})(); -})() : (function() { - var d = {}; - d["type"] = "literal"; - d["value"] = seg; - return d; -})()); }; - - // parse-route-pattern - var parseRoutePattern = function(pattern) { return (function() { - var segments = splitPathSegments(pattern); - return map(makeRouteSegment, segments); -})(); }; - - // match-route-segments - var matchRouteSegments = function(pathSegs, parsedSegs) { return (isSxTruthy(!(length(pathSegs) == length(parsedSegs))) ? NIL : (function() { - var params = {}; - var matched = true; - forEachIndexed(function(i, parsedSeg) { return (isSxTruthy(matched) ? (function() { - var pathSeg = nth(pathSegs, i); - var segType = get(parsedSeg, "type"); - return (isSxTruthy((segType == "literal")) ? (isSxTruthy(!(pathSeg == get(parsedSeg, "value"))) ? (matched = false) : NIL) : (isSxTruthy((segType == "param")) ? dictSet(params, get(parsedSeg, "value"), pathSeg) : (matched = false))); -})() : NIL); }, parsedSegs); - return (isSxTruthy(matched) ? params : NIL); -})()); }; - - // match-route - var matchRoute = function(path, pattern) { return (function() { - var pathSegs = splitPathSegments(path); - var parsedSegs = parseRoutePattern(pattern); - return matchRouteSegments(pathSegs, parsedSegs); -})(); }; - - // find-matching-route - var findMatchingRoute = function(path, routes) { return (function() { - var pathSegs = splitPathSegments(path); - var result = NIL; - { var _c = routes; for (var _i = 0; _i < _c.length; _i++) { var route = _c[_i]; if (isSxTruthy(isNil(result))) { - (function() { - var params = matchRouteSegments(pathSegs, get(route, "parsed")); - return (isSxTruthy(!isNil(params)) ? (function() { - var matched = merge(route, {}); - matched["params"] = params; - return (result = matched); -})() : NIL); -})(); -} } } - return result; -})(); }; - - // ========================================================================= // Platform interface — DOM adapter (browser-only) // ========================================================================= @@ -3753,20 +3484,6 @@ callExpr.push(dictGet(kwargs, k)); } } renderComponent: typeof sxRenderComponent === "function" ? sxRenderComponent : null, getEnv: function() { return componentEnv; }, init: typeof bootInit === "function" ? bootInit : null, - scanRefs: scanRefs, - transitiveDeps: transitiveDeps, - computeAllDeps: computeAllDeps, - componentsNeeded: componentsNeeded, - pageComponentBundle: pageComponentBundle, - pageCssClasses: pageCssClasses, - scanIoRefs: scanIoRefs, - transitiveIoRefs: transitiveIoRefs, - computeAllIoRefs: computeAllIoRefs, - componentPure_p: componentPure_p, - splitPathSegments: splitPathSegments, - parseRoutePattern: parseRoutePattern, - matchRoute: matchRoute, - findMatchingRoute: findMatchingRoute, _version: "ref-2.0 (boot+cssx+dom+engine+html+orchestration+parser+sx, bootstrap-compiled)" }; diff --git a/shared/sx/ref/orchestration.sx b/shared/sx/ref/orchestration.sx index 43fcee13..27b9c3f9 100644 --- a/shared/sx/ref/orchestration.sx +++ b/shared/sx/ref/orchestration.sx @@ -388,15 +388,25 @@ (dom-has-attr? el "href"))) (prevent-default e)) - ;; Delay modifier - (if (get mods "delay") + ;; For GET clicks on links, try client-side routing first + (if (and (= event-name "click") + (= (get verbInfo "method") "GET") + (dom-has-attr? el "href") + (not (get mods "delay")) + (try-client-route (url-pathname (get verbInfo "url")))) + ;; Client route succeeded — push state, scroll to top (do - (clear-timeout timer) - (set! timer - (set-timeout - (fn () (execute-request el verbInfo nil)) - (get mods "delay")))) - (execute-request el verbInfo nil))))) + (browser-push-state (get verbInfo "url")) + (browser-scroll-to 0 0)) + ;; Fall through to server fetch + (if (get mods "delay") + (do + (clear-timeout timer) + (set! timer + (set-timeout + (fn () (execute-request el verbInfo nil)) + (get mods "delay")))) + (execute-request el verbInfo nil)))))) (if (get mods "once") (dict "once" true) nil))))))