;; ========================================================================== ;; router.sx — Client-side route matching specification ;; ;; Pure functions for matching URL paths against Flask-style route patterns. ;; Used by client-side routing to determine if a page can be rendered ;; locally without a server roundtrip. ;; ;; All functions are pure — no IO, no platform-specific operations. ;; Uses only primitives from primitives.sx (string ops, list ops). ;; ========================================================================== ;; -------------------------------------------------------------------------- ;; 1. Split path into segments ;; -------------------------------------------------------------------------- ;; "/docs/hello" → ("docs" "hello") ;; "/" → () ;; "/docs/" → ("docs") (define split-path-segments :effects [] (fn ((path :as string)) (let ((trimmed (if (starts-with? path "/") (slice path 1) path))) (let ((trimmed2 (if (and (not (empty? trimmed)) (ends-with? trimmed "/")) (slice trimmed 0 (- (len trimmed) 1)) trimmed))) (if (empty? trimmed2) (list) (split trimmed2 "/")))))) ;; -------------------------------------------------------------------------- ;; 2. Parse Flask-style route pattern into segment descriptors ;; -------------------------------------------------------------------------- ;; "/docs/" → ({"type" "literal" "value" "docs"} ;; {"type" "param" "value" "slug"}) (define make-route-segment :effects [] (fn ((seg :as string)) (if (and (starts-with? seg "<") (ends-with? seg ">")) (let ((param-name (slice seg 1 (- (len seg) 1)))) (let ((d {})) (dict-set! d "type" "param") (dict-set! d "value" param-name) d)) (let ((d {})) (dict-set! d "type" "literal") (dict-set! d "value" seg) d)))) (define parse-route-pattern :effects [] (fn ((pattern :as string)) (let ((segments (split-path-segments pattern))) (map make-route-segment segments)))) ;; -------------------------------------------------------------------------- ;; 3. Match path segments against parsed pattern ;; -------------------------------------------------------------------------- ;; Returns params dict if match, nil if no match. (define match-route-segments :effects [] (fn ((path-segs :as list) (parsed-segs :as list)) (if (not (= (len path-segs) (len parsed-segs))) nil (let ((params {}) (matched true)) (for-each-indexed (fn ((i :as number) (parsed-seg :as dict)) (when matched (let ((path-seg (nth path-segs i)) (seg-type (get parsed-seg "type"))) (cond (= seg-type "literal") (when (not (= path-seg (get parsed-seg "value"))) (set! matched false)) (= seg-type "param") (dict-set! params (get parsed-seg "value") path-seg) :else (set! matched false))))) parsed-segs) (if matched params nil))))) ;; -------------------------------------------------------------------------- ;; 4. Public API: match a URL path against a pattern string ;; -------------------------------------------------------------------------- ;; Returns params dict (may be empty for exact matches) or nil. (define match-route :effects [] (fn ((path :as string) (pattern :as string)) (let ((path-segs (split-path-segments path)) (parsed-segs (parse-route-pattern pattern))) (match-route-segments path-segs parsed-segs)))) ;; -------------------------------------------------------------------------- ;; 5. Search a list of route entries for first match ;; -------------------------------------------------------------------------- ;; Each entry: {"pattern" "/docs/" "parsed" [...] "name" "docs-page" ...} ;; Returns matching entry with "params" added, or nil. (define find-matching-route :effects [] (fn ((path :as string) (routes :as list)) ;; If path is an SX expression URL, convert to old-style for matching. (let ((match-path (if (starts-with? path "/(") (or (sx-url-to-path path) path) path))) (let ((path-segs (split-path-segments match-path)) (result nil)) (for-each (fn ((route :as dict)) (when (nil? result) (let ((params (match-route-segments path-segs (get route "parsed")))) (when (not (nil? params)) (let ((matched (merge route {}))) (dict-set! matched "params" params) (set! result matched)))))) routes) result)))) ;; -------------------------------------------------------------------------- ;; 6. SX expression URL → old-style path conversion ;; -------------------------------------------------------------------------- ;; Converts /(language.(doc.introduction)) → /language/docs/introduction ;; so client-side routing can match SX URLs against Flask-style patterns. (define _fn-to-segment :effects [] (fn ((name :as string)) (case name "doc" "docs" "spec" "specs" "bootstrapper" "bootstrappers" "test" "testing" "example" "examples" "protocol" "protocols" "essay" "essays" "plan" "plans" "reference-detail" "reference" :else name))) (define sx-url-to-path :effects [] (fn ((url :as string)) ;; Convert an SX expression URL to an old-style slash path. ;; "/(language.(doc.introduction))" → "/language/docs/introduction" ;; Returns nil for non-SX URLs (those not starting with "/(" ). (if (not (and (starts-with? url "/(") (ends-with? url ")"))) nil (let ((inner (slice url 2 (- (len url) 1)))) ;; "language.(doc.introduction)" → dots to slashes, strip parens (let ((s (replace (replace (replace inner "." "/") "(" "") ")" ""))) ;; "language/doc/introduction" → split, map names, rejoin (let ((segs (filter (fn (s) (not (empty? s))) (split s "/")))) (str "/" (join "/" (map _fn-to-segment segs))))))))) ;; -------------------------------------------------------------------------- ;; Platform interface — none required ;; -------------------------------------------------------------------------- ;; All functions use only pure primitives: ;; split, slice, starts-with?, ends-with?, len, empty?, replace, ;; map, filter, for-each, for-each-indexed, nth, get, dict-set!, merge, ;; list, nil?, not, =, case, join, str ;; --------------------------------------------------------------------------