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>
166 lines
6.7 KiB
Plaintext
166 lines
6.7 KiB
Plaintext
;; ==========================================================================
|
|
;; 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/<slug>" → ({"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/<slug>" "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
|
|
;; --------------------------------------------------------------------------
|