Spec URL evaluation in router.sx, bootstrap to Python/JS
Add url-to-expr, auto-quote-unknowns, prepare-url-expr to router.sx — the canonical URL-to-expression pipeline. Dots→spaces, parse, then auto-quote unknown symbols as strings (slugs). The same spec serves both server (Python) and client (JS) route handling. - router.sx: three new pure functions for URL evaluation - bootstrap_py.py: auto-include router module with html adapter - platform_js.py: export urlToExpr/autoQuoteUnknowns/prepareUrlExpr - sx_router.py: replace hand-written auto_quote_slugs with bootstrapped prepare_url_expr — delete ~50 lines of hardcoded function name sets - Rebootstrap sx_ref.py (4331 lines) and sx-browser.js Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
// =========================================================================
|
||||
|
||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||
var SX_VERSION = "2026-03-12T22:27:08Z";
|
||||
var SX_VERSION = "2026-03-12T22:55:39Z";
|
||||
|
||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||
@@ -3922,6 +3922,30 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
return (isSxTruthy((get(parsed, "type") == "special-form")) ? get(parsed, "inner") : NIL);
|
||||
})(); };
|
||||
|
||||
// url-to-expr
|
||||
var urlToExpr = function(urlPath) { return (isSxTruthy(sxOr((urlPath == "/"), isEmpty(urlPath))) ? [] : (function() {
|
||||
var trimmed = (isSxTruthy(startsWith(urlPath, "/")) ? slice(urlPath, 1) : urlPath);
|
||||
return (function() {
|
||||
var sxSource = replace_(trimmed, ".", " ");
|
||||
return (function() {
|
||||
var exprs = sxParse(sxSource);
|
||||
return (isSxTruthy(isEmpty(exprs)) ? [] : first(exprs));
|
||||
})();
|
||||
})();
|
||||
})()); };
|
||||
|
||||
// auto-quote-unknowns
|
||||
var autoQuoteUnknowns = function(expr, env) { return (isSxTruthy(!isSxTruthy(isList(expr))) ? expr : (isSxTruthy(isEmpty(expr)) ? expr : cons(first(expr), map(function(child) { return (isSxTruthy(isList(child)) ? autoQuoteUnknowns(child, env) : (isSxTruthy((typeOf(child) == "symbol")) ? (function() {
|
||||
var name = symbolName(child);
|
||||
return (isSxTruthy(sxOr(envHas(env, name), startsWith(name, ":"), startsWith(name, "~"), startsWith(name, "!"))) ? child : name);
|
||||
})() : child)); }, rest(expr))))); };
|
||||
|
||||
// prepare-url-expr
|
||||
var prepareUrlExpr = function(urlPath, env) { return (function() {
|
||||
var expr = urlToExpr(urlPath);
|
||||
return (isSxTruthy(isEmpty(expr)) ? expr : autoQuoteUnknowns(expr, env));
|
||||
})(); };
|
||||
|
||||
|
||||
// === Transpiled from signals (reactive signal runtime) ===
|
||||
|
||||
@@ -6300,6 +6324,9 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
||||
parseRoutePattern: parseRoutePattern,
|
||||
matchRoute: matchRoute,
|
||||
findMatchingRoute: findMatchingRoute,
|
||||
urlToExpr: urlToExpr,
|
||||
autoQuoteUnknowns: autoQuoteUnknowns,
|
||||
prepareUrlExpr: prepareUrlExpr,
|
||||
registerIo: typeof registerIoPrimitive === "function" ? registerIoPrimitive : null,
|
||||
registerIoDeps: typeof registerIoDeps === "function" ? registerIoDeps : null,
|
||||
asyncRender: typeof asyncSxRenderWithEnv === "function" ? asyncSxRenderWithEnv : null,
|
||||
|
||||
@@ -1254,7 +1254,8 @@ def compile_ref_to_py(
|
||||
if sm not in SPEC_MODULES:
|
||||
raise ValueError(f"Unknown spec module: {sm!r}. Valid: {', '.join(SPEC_MODULES)}")
|
||||
spec_mod_set.add(sm)
|
||||
# html adapter needs deps (component analysis) and signals (island rendering)
|
||||
# html adapter needs deps (component analysis), signals (island rendering),
|
||||
# router (URL-to-expression evaluation), and page-helpers
|
||||
if "html" in adapter_set:
|
||||
if "deps" in SPEC_MODULES:
|
||||
spec_mod_set.add("deps")
|
||||
@@ -1262,6 +1263,8 @@ def compile_ref_to_py(
|
||||
spec_mod_set.add("signals")
|
||||
if "page-helpers" in SPEC_MODULES:
|
||||
spec_mod_set.add("page-helpers")
|
||||
if "router" in SPEC_MODULES:
|
||||
spec_mod_set.add("router")
|
||||
has_deps = "deps" in spec_mod_set
|
||||
|
||||
# Core files always included, then selected adapters, then spec modules
|
||||
|
||||
@@ -3129,6 +3129,9 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has
|
||||
api_lines.append(' parseRoutePattern: parseRoutePattern,')
|
||||
api_lines.append(' matchRoute: matchRoute,')
|
||||
api_lines.append(' findMatchingRoute: findMatchingRoute,')
|
||||
api_lines.append(' urlToExpr: urlToExpr,')
|
||||
api_lines.append(' autoQuoteUnknowns: autoQuoteUnknowns,')
|
||||
api_lines.append(' prepareUrlExpr: prepareUrlExpr,')
|
||||
|
||||
if has_dom:
|
||||
api_lines.append(' registerIo: typeof registerIoPrimitive === "function" ? registerIoPrimitive : null,')
|
||||
|
||||
@@ -570,11 +570,111 @@
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform interface — none required
|
||||
;; 12. URL expression evaluation
|
||||
;; --------------------------------------------------------------------------
|
||||
;; All functions use only pure primitives:
|
||||
;; A URL is an expression. The system is the environment.
|
||||
;; eval(url, env) — that's it.
|
||||
;;
|
||||
;; The only URL-specific pre-processing:
|
||||
;; 1. Surface syntax → AST (dots to spaces, parse as SX)
|
||||
;; 2. Auto-quote unknowns (symbols not in env become strings)
|
||||
;;
|
||||
;; After that, it's standard eval. The host wires these into its route
|
||||
;; handlers (Python catch-all, JS client-side navigation). The same
|
||||
;; functions serve both.
|
||||
|
||||
(define url-to-expr :effects []
|
||||
(fn ((url-path :as string))
|
||||
;; Convert a URL path to an SX expression (AST).
|
||||
;;
|
||||
;; "/sx/(language.(doc.introduction))" → (language (doc introduction))
|
||||
;; "/(language.(doc.introduction))" → (language (doc introduction))
|
||||
;; "/" → (list) ; empty — home
|
||||
;;
|
||||
;; Steps:
|
||||
;; 1. Strip URL prefix ("/sx/" or "/") — host passes the path after prefix
|
||||
;; 2. Dots → spaces (URL-safe whitespace encoding)
|
||||
;; 3. Parse as SX expression
|
||||
;;
|
||||
;; The caller is responsible for stripping any app-level prefix.
|
||||
;; This function receives the raw expression portion: "(language.(doc.intro))"
|
||||
;; or "/" for home.
|
||||
(if (or (= url-path "/") (empty? url-path))
|
||||
(list)
|
||||
(let ((trimmed (if (starts-with? url-path "/")
|
||||
(slice url-path 1)
|
||||
url-path)))
|
||||
;; Dots → spaces
|
||||
(let ((sx-source (replace trimmed "." " ")))
|
||||
;; Parse — returns list of expressions, take the first
|
||||
(let ((exprs (sx-parse sx-source)))
|
||||
(if (empty? exprs)
|
||||
(list)
|
||||
(first exprs))))))))
|
||||
|
||||
|
||||
(define auto-quote-unknowns :effects []
|
||||
(fn ((expr :as list) (env :as dict))
|
||||
;; Walk an AST and replace symbols not in env with their name as a string.
|
||||
;; This makes URL slugs work without quoting:
|
||||
;; (language (doc introduction)) ; introduction is not a function
|
||||
;; → (language (doc "introduction"))
|
||||
;;
|
||||
;; Rules:
|
||||
;; - List head (call position) stays as-is — it's a function name
|
||||
;; - Tail symbols: if in env, keep as symbol; otherwise, string
|
||||
;; - Keywords, strings, numbers, nested lists: pass through
|
||||
;; - Non-list expressions: pass through unchanged
|
||||
(if (not (list? expr))
|
||||
expr
|
||||
(if (empty? expr)
|
||||
expr
|
||||
;; Head stays as symbol (function position), quote the rest
|
||||
(cons (first expr)
|
||||
(map (fn (child)
|
||||
(cond
|
||||
;; Nested list — recurse
|
||||
(list? child)
|
||||
(auto-quote-unknowns child env)
|
||||
;; Symbol — check env
|
||||
(= (type-of child) "symbol")
|
||||
(let ((name (symbol-name child)))
|
||||
(if (or (env-has? env name)
|
||||
;; Keep keywords, component refs, special forms
|
||||
(starts-with? name ":")
|
||||
(starts-with? name "~")
|
||||
(starts-with? name "!"))
|
||||
child
|
||||
name)) ;; unknown → string
|
||||
;; Everything else passes through
|
||||
:else child))
|
||||
(rest expr)))))))
|
||||
|
||||
|
||||
(define prepare-url-expr :effects []
|
||||
(fn ((url-path :as string) (env :as dict))
|
||||
;; Full pipeline: URL path → ready-to-eval AST.
|
||||
;;
|
||||
;; "(language.(doc.introduction))" + env
|
||||
;; → (language (doc "introduction"))
|
||||
;;
|
||||
;; The result can be fed directly to eval:
|
||||
;; (eval (prepare-url-expr path env) env)
|
||||
(let ((expr (url-to-expr url-path)))
|
||||
(if (empty? expr)
|
||||
expr
|
||||
(auto-quote-unknowns expr env)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform interface
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Pure primitives used:
|
||||
;; 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, index-of, and, or, cons,
|
||||
;; first, rest, append, parse-int, contains?, min, cond
|
||||
;; first, rest, append, parse-int, contains?, min, cond,
|
||||
;; symbol?, symbol-name, list?, env-has?, type-of
|
||||
;;
|
||||
;; From parser.sx: sx-parse, sx-serialize
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
@@ -2881,6 +2881,361 @@ def build_affinity_analysis(demo_components, page_plans):
|
||||
return {'components': demo_components, 'page-plans': page_plans}
|
||||
|
||||
|
||||
# === Transpiled from router (client-side route matching) ===
|
||||
|
||||
# split-path-segments
|
||||
def split_path_segments(path):
|
||||
trimmed = (slice(path, 1) if sx_truthy(starts_with_p(path, '/')) else path)
|
||||
trimmed2 = (slice(trimmed, 0, (len(trimmed) - 1)) if sx_truthy(((not sx_truthy(empty_p(trimmed))) if not sx_truthy((not sx_truthy(empty_p(trimmed)))) else ends_with_p(trimmed, '/'))) else trimmed)
|
||||
if sx_truthy(empty_p(trimmed2)):
|
||||
return []
|
||||
else:
|
||||
return split(trimmed2, '/')
|
||||
|
||||
# make-route-segment
|
||||
def make_route_segment(seg):
|
||||
if sx_truthy((starts_with_p(seg, '<') if not sx_truthy(starts_with_p(seg, '<')) else ends_with_p(seg, '>'))):
|
||||
param_name = slice(seg, 1, (len(seg) - 1))
|
||||
d = {}
|
||||
d['type'] = 'param'
|
||||
d['value'] = param_name
|
||||
return d
|
||||
else:
|
||||
d = {}
|
||||
d['type'] = 'literal'
|
||||
d['value'] = seg
|
||||
return d
|
||||
|
||||
# parse-route-pattern
|
||||
def parse_route_pattern(pattern):
|
||||
segments = split_path_segments(pattern)
|
||||
return map(make_route_segment, segments)
|
||||
|
||||
# match-route-segments
|
||||
def match_route_segments(path_segs, parsed_segs):
|
||||
_cells = {}
|
||||
if sx_truthy((not sx_truthy((len(path_segs) == len(parsed_segs))))):
|
||||
return NIL
|
||||
else:
|
||||
params = {}
|
||||
_cells['matched'] = True
|
||||
for_each_indexed(lambda i, parsed_seg: ((lambda path_seg: (lambda seg_type: ((_sx_cell_set(_cells, 'matched', False) if sx_truthy((not sx_truthy((path_seg == get(parsed_seg, 'value'))))) else NIL) if sx_truthy((seg_type == 'literal')) else (_sx_dict_set(params, get(parsed_seg, 'value'), path_seg) if sx_truthy((seg_type == 'param')) else _sx_cell_set(_cells, 'matched', False))))(get(parsed_seg, 'type')))(nth(path_segs, i)) if sx_truthy(_cells['matched']) else NIL), parsed_segs)
|
||||
if sx_truthy(_cells['matched']):
|
||||
return params
|
||||
else:
|
||||
return NIL
|
||||
|
||||
# match-route
|
||||
def match_route(path, pattern):
|
||||
path_segs = split_path_segments(path)
|
||||
parsed_segs = parse_route_pattern(pattern)
|
||||
return match_route_segments(path_segs, parsed_segs)
|
||||
|
||||
# find-matching-route
|
||||
def find_matching_route(path, routes):
|
||||
_cells = {}
|
||||
match_path = ((sx_url_to_path(path) if sx_truthy(sx_url_to_path(path)) else path) if sx_truthy(starts_with_p(path, '/(')) else path)
|
||||
path_segs = split_path_segments(match_path)
|
||||
_cells['result'] = NIL
|
||||
for route in routes:
|
||||
if sx_truthy(is_nil(_cells['result'])):
|
||||
params = match_route_segments(path_segs, get(route, 'parsed'))
|
||||
if sx_truthy((not sx_truthy(is_nil(params)))):
|
||||
matched = merge(route, {})
|
||||
matched['params'] = params
|
||||
_cells['result'] = matched
|
||||
return _cells['result']
|
||||
|
||||
# _fn-to-segment
|
||||
def _fn_to_segment(name):
|
||||
_match = name
|
||||
if _match == 'doc':
|
||||
return 'docs'
|
||||
elif _match == 'spec':
|
||||
return 'specs'
|
||||
elif _match == 'bootstrapper':
|
||||
return 'bootstrappers'
|
||||
elif _match == 'test':
|
||||
return 'testing'
|
||||
elif _match == 'example':
|
||||
return 'examples'
|
||||
elif _match == 'protocol':
|
||||
return 'protocols'
|
||||
elif _match == 'essay':
|
||||
return 'essays'
|
||||
elif _match == 'plan':
|
||||
return 'plans'
|
||||
elif _match == 'reference-detail':
|
||||
return 'reference'
|
||||
else:
|
||||
return name
|
||||
|
||||
# sx-url-to-path
|
||||
def sx_url_to_path(url):
|
||||
if sx_truthy((not sx_truthy((starts_with_p(url, '/(') if not sx_truthy(starts_with_p(url, '/(')) else ends_with_p(url, ')'))))):
|
||||
return NIL
|
||||
else:
|
||||
inner = slice(url, 2, (len(url) - 1))
|
||||
s = replace(replace(replace(inner, '.', '/'), '(', ''), ')', '')
|
||||
segs = filter(lambda s: (not sx_truthy(empty_p(s))), split(s, '/'))
|
||||
return sx_str('/', join('/', map(_fn_to_segment, segs)))
|
||||
|
||||
# _count-leading-dots
|
||||
def _count_leading_dots(s):
|
||||
if sx_truthy(empty_p(s)):
|
||||
return 0
|
||||
else:
|
||||
if sx_truthy(starts_with_p(s, '.')):
|
||||
return (1 + _count_leading_dots(slice(s, 1)))
|
||||
else:
|
||||
return 0
|
||||
|
||||
# _strip-trailing-close
|
||||
def _strip_trailing_close(s):
|
||||
if sx_truthy(ends_with_p(s, ')')):
|
||||
return _strip_trailing_close(slice(s, 0, (len(s) - 1)))
|
||||
else:
|
||||
return s
|
||||
|
||||
# _index-of-safe
|
||||
def _index_of_safe(s, needle):
|
||||
idx = index_of(s, needle)
|
||||
if sx_truthy((is_nil(idx) if sx_truthy(is_nil(idx)) else (idx < 0))):
|
||||
return NIL
|
||||
else:
|
||||
return idx
|
||||
|
||||
# _last-index-of
|
||||
def _last_index_of(s, needle):
|
||||
idx = _index_of_safe(s, needle)
|
||||
if sx_truthy(is_nil(idx)):
|
||||
return NIL
|
||||
else:
|
||||
rest_idx = _last_index_of(slice(s, (idx + 1)), needle)
|
||||
if sx_truthy(is_nil(rest_idx)):
|
||||
return idx
|
||||
else:
|
||||
return ((idx + 1) + rest_idx)
|
||||
|
||||
# _pop-sx-url-level
|
||||
def _pop_sx_url_level(url):
|
||||
stripped = _strip_trailing_close(url)
|
||||
close_count = (len(url) - len(_strip_trailing_close(url)))
|
||||
if sx_truthy((close_count <= 1)):
|
||||
return '/'
|
||||
else:
|
||||
last_dp = _last_index_of(stripped, '.(')
|
||||
if sx_truthy(is_nil(last_dp)):
|
||||
return '/'
|
||||
else:
|
||||
return sx_str(slice(stripped, 0, last_dp), slice(url, (len(url) - (close_count - 1))))
|
||||
|
||||
# _pop-sx-url-levels
|
||||
def _pop_sx_url_levels(url, n):
|
||||
if sx_truthy((n <= 0)):
|
||||
return url
|
||||
else:
|
||||
return _pop_sx_url_levels(_pop_sx_url_level(url), (n - 1))
|
||||
|
||||
# _split-pos-kw
|
||||
def _split_pos_kw(tokens, i, pos, kw):
|
||||
if sx_truthy((i >= len(tokens))):
|
||||
return {'positional': join('.', pos), 'keywords': kw}
|
||||
else:
|
||||
tok = nth(tokens, i)
|
||||
if sx_truthy(starts_with_p(tok, ':')):
|
||||
val = (nth(tokens, (i + 1)) if sx_truthy(((i + 1) < len(tokens))) else '')
|
||||
return _split_pos_kw(tokens, (i + 2), pos, append(kw, [[tok, val]]))
|
||||
else:
|
||||
return _split_pos_kw(tokens, (i + 1), append(pos, [tok]), kw)
|
||||
|
||||
# _parse-relative-body
|
||||
def _parse_relative_body(body):
|
||||
if sx_truthy(empty_p(body)):
|
||||
return {'positional': '', 'keywords': []}
|
||||
else:
|
||||
return _split_pos_kw(split(body, '.'), 0, [], [])
|
||||
|
||||
# _extract-innermost
|
||||
def _extract_innermost(url):
|
||||
stripped = _strip_trailing_close(url)
|
||||
suffix = slice(url, len(_strip_trailing_close(url)))
|
||||
last_dp = _last_index_of(stripped, '.(')
|
||||
if sx_truthy(is_nil(last_dp)):
|
||||
return {'before': '/(', 'content': slice(stripped, 2), 'suffix': suffix}
|
||||
else:
|
||||
return {'before': slice(stripped, 0, (last_dp + 2)), 'content': slice(stripped, (last_dp + 2)), 'suffix': suffix}
|
||||
|
||||
# _find-kw-in-tokens
|
||||
def _find_kw_in_tokens(tokens, i, kw):
|
||||
if sx_truthy((i >= len(tokens))):
|
||||
return NIL
|
||||
else:
|
||||
if sx_truthy(((nth(tokens, i) == kw) if not sx_truthy((nth(tokens, i) == kw)) else ((i + 1) < len(tokens)))):
|
||||
return nth(tokens, (i + 1))
|
||||
else:
|
||||
return _find_kw_in_tokens(tokens, (i + 1), kw)
|
||||
|
||||
# _find-keyword-value
|
||||
def _find_keyword_value(content, kw):
|
||||
return _find_kw_in_tokens(split(content, '.'), 0, kw)
|
||||
|
||||
# _replace-kw-in-tokens
|
||||
def _replace_kw_in_tokens(tokens, i, kw, value):
|
||||
if sx_truthy((i >= len(tokens))):
|
||||
return []
|
||||
else:
|
||||
if sx_truthy(((nth(tokens, i) == kw) if not sx_truthy((nth(tokens, i) == kw)) else ((i + 1) < len(tokens)))):
|
||||
return append([kw, value], _replace_kw_in_tokens(tokens, (i + 2), kw, value))
|
||||
else:
|
||||
return cons(nth(tokens, i), _replace_kw_in_tokens(tokens, (i + 1), kw, value))
|
||||
|
||||
# _set-keyword-in-content
|
||||
def _set_keyword_in_content(content, kw, value):
|
||||
current = _find_keyword_value(content, kw)
|
||||
if sx_truthy(is_nil(current)):
|
||||
return sx_str(content, '.', kw, '.', value)
|
||||
else:
|
||||
return join('.', _replace_kw_in_tokens(split(content, '.'), 0, kw, value))
|
||||
|
||||
# _is-delta-value?
|
||||
def _is_delta_value_p(s):
|
||||
return ((not sx_truthy(empty_p(s))) if not sx_truthy((not sx_truthy(empty_p(s)))) else ((len(s) > 1) if not sx_truthy((len(s) > 1)) else (starts_with_p(s, '+') if sx_truthy(starts_with_p(s, '+')) else starts_with_p(s, '-'))))
|
||||
|
||||
# _apply-delta
|
||||
def _apply_delta(current_str, delta_str):
|
||||
cur = parse_int(current_str, NIL)
|
||||
delta = parse_int(delta_str, NIL)
|
||||
if sx_truthy((is_nil(cur) if sx_truthy(is_nil(cur)) else is_nil(delta))):
|
||||
return delta_str
|
||||
else:
|
||||
return sx_str((cur + delta))
|
||||
|
||||
# _apply-kw-pairs
|
||||
def _apply_kw_pairs(content, kw_pairs):
|
||||
if sx_truthy(empty_p(kw_pairs)):
|
||||
return content
|
||||
else:
|
||||
pair = first(kw_pairs)
|
||||
kw = first(pair)
|
||||
raw_val = nth(pair, 1)
|
||||
actual_val = ((lambda current: (raw_val if sx_truthy(is_nil(current)) else _apply_delta(current, raw_val)))(_find_keyword_value(content, kw)) if sx_truthy(_is_delta_value_p(raw_val)) else raw_val)
|
||||
return _apply_kw_pairs(_set_keyword_in_content(content, kw, actual_val), rest(kw_pairs))
|
||||
|
||||
# _apply-keywords-to-url
|
||||
def _apply_keywords_to_url(url, kw_pairs):
|
||||
if sx_truthy(empty_p(kw_pairs)):
|
||||
return url
|
||||
else:
|
||||
parts = _extract_innermost(url)
|
||||
new_content = _apply_kw_pairs(get(parts, 'content'), kw_pairs)
|
||||
return sx_str(get(parts, 'before'), new_content, get(parts, 'suffix'))
|
||||
|
||||
# _normalize-relative
|
||||
def _normalize_relative(url):
|
||||
if sx_truthy(starts_with_p(url, '(')):
|
||||
return url
|
||||
else:
|
||||
return sx_str('(', url, ')')
|
||||
|
||||
# resolve-relative-url
|
||||
def resolve_relative_url(current, relative):
|
||||
canonical = _normalize_relative(relative)
|
||||
rel_inner = slice(canonical, 1, (len(canonical) - 1))
|
||||
dots = _count_leading_dots(rel_inner)
|
||||
body = slice(rel_inner, _count_leading_dots(rel_inner))
|
||||
if sx_truthy((dots == 0)):
|
||||
return current
|
||||
else:
|
||||
parsed = _parse_relative_body(body)
|
||||
pos_body = get(parsed, 'positional')
|
||||
kw_pairs = get(parsed, 'keywords')
|
||||
after_nav = ((current if sx_truthy(empty_p(pos_body)) else (lambda stripped: (lambda suffix: sx_str(stripped, '.', pos_body, suffix))(slice(current, len(_strip_trailing_close(current)))))(_strip_trailing_close(current))) if sx_truthy((dots == 1)) else (lambda base: (base if sx_truthy(empty_p(pos_body)) else (sx_str('/(', pos_body, ')') if sx_truthy((base == '/')) else (lambda stripped: (lambda suffix: sx_str(stripped, '.(', pos_body, ')', suffix))(slice(base, len(_strip_trailing_close(base)))))(_strip_trailing_close(base)))))(_pop_sx_url_levels(current, (dots - 1))))
|
||||
return _apply_keywords_to_url(after_nav, kw_pairs)
|
||||
|
||||
# relative-sx-url?
|
||||
def relative_sx_url_p(url):
|
||||
return ((starts_with_p(url, '(') if not sx_truthy(starts_with_p(url, '(')) else (not sx_truthy(starts_with_p(url, '/(')))) if sx_truthy((starts_with_p(url, '(') if not sx_truthy(starts_with_p(url, '(')) else (not sx_truthy(starts_with_p(url, '/('))))) else starts_with_p(url, '.'))
|
||||
|
||||
# _url-special-forms
|
||||
def _url_special_forms():
|
||||
return ['!source', '!inspect', '!diff', '!search', '!raw', '!json']
|
||||
|
||||
# url-special-form?
|
||||
def url_special_form_p(name):
|
||||
return (starts_with_p(name, '!') if not sx_truthy(starts_with_p(name, '!')) else contains_p(_url_special_forms(), name))
|
||||
|
||||
# parse-sx-url
|
||||
def parse_sx_url(url):
|
||||
if sx_truthy((url == '/')):
|
||||
return {'type': 'home', 'raw': url}
|
||||
elif sx_truthy(relative_sx_url_p(url)):
|
||||
return {'type': 'relative', 'raw': url}
|
||||
elif sx_truthy((starts_with_p(url, '/(!') if not sx_truthy(starts_with_p(url, '/(!')) else ends_with_p(url, ')'))):
|
||||
inner = slice(url, 2, (len(url) - 1))
|
||||
dot_pos = _index_of_safe(inner, '.')
|
||||
paren_pos = _index_of_safe(inner, '(')
|
||||
end_pos = (len(inner) if sx_truthy((is_nil(dot_pos) if not sx_truthy(is_nil(dot_pos)) else is_nil(paren_pos))) else (paren_pos if sx_truthy(is_nil(dot_pos)) else (dot_pos if sx_truthy(is_nil(paren_pos)) else min(dot_pos, paren_pos))))
|
||||
form_name = slice(inner, 0, end_pos)
|
||||
rest_part = slice(inner, end_pos)
|
||||
inner_expr = (slice(rest_part, 1) if sx_truthy(starts_with_p(rest_part, '.')) else rest_part)
|
||||
return {'type': 'special-form', 'form': form_name, 'inner': inner_expr, 'raw': url}
|
||||
elif sx_truthy((starts_with_p(url, '/(~') if not sx_truthy(starts_with_p(url, '/(~')) else ends_with_p(url, ')'))):
|
||||
name = slice(url, 2, (len(url) - 1))
|
||||
return {'type': 'direct-component', 'name': name, 'raw': url}
|
||||
elif sx_truthy((starts_with_p(url, '/(') if not sx_truthy(starts_with_p(url, '/(')) else ends_with_p(url, ')'))):
|
||||
return {'type': 'absolute', 'raw': url}
|
||||
else:
|
||||
return {'type': 'path', 'raw': url}
|
||||
|
||||
# url-special-form-name
|
||||
def url_special_form_name(url):
|
||||
parsed = parse_sx_url(url)
|
||||
if sx_truthy((get(parsed, 'type') == 'special-form')):
|
||||
return get(parsed, 'form')
|
||||
else:
|
||||
return NIL
|
||||
|
||||
# url-special-form-inner
|
||||
def url_special_form_inner(url):
|
||||
parsed = parse_sx_url(url)
|
||||
if sx_truthy((get(parsed, 'type') == 'special-form')):
|
||||
return get(parsed, 'inner')
|
||||
else:
|
||||
return NIL
|
||||
|
||||
# url-to-expr
|
||||
def url_to_expr(url_path):
|
||||
if sx_truthy(((url_path == '/') if sx_truthy((url_path == '/')) else empty_p(url_path))):
|
||||
return []
|
||||
else:
|
||||
trimmed = (slice(url_path, 1) if sx_truthy(starts_with_p(url_path, '/')) else url_path)
|
||||
sx_source = replace(trimmed, '.', ' ')
|
||||
exprs = sx_parse(sx_source)
|
||||
if sx_truthy(empty_p(exprs)):
|
||||
return []
|
||||
else:
|
||||
return first(exprs)
|
||||
|
||||
# auto-quote-unknowns
|
||||
def auto_quote_unknowns(expr, env):
|
||||
if sx_truthy((not sx_truthy(list_p(expr)))):
|
||||
return expr
|
||||
else:
|
||||
if sx_truthy(empty_p(expr)):
|
||||
return expr
|
||||
else:
|
||||
return cons(first(expr), map(lambda child: (auto_quote_unknowns(child, env) if sx_truthy(list_p(child)) else ((lambda name: (child if sx_truthy((env_has(env, name) if sx_truthy(env_has(env, name)) else (starts_with_p(name, ':') if sx_truthy(starts_with_p(name, ':')) else (starts_with_p(name, '~') if sx_truthy(starts_with_p(name, '~')) else starts_with_p(name, '!'))))) else name))(symbol_name(child)) if sx_truthy((type_of(child) == 'symbol')) else child)), rest(expr)))
|
||||
|
||||
# prepare-url-expr
|
||||
def prepare_url_expr(url_path, env):
|
||||
expr = url_to_expr(url_path)
|
||||
if sx_truthy(empty_p(expr)):
|
||||
return expr
|
||||
else:
|
||||
return auto_quote_unknowns(expr, env)
|
||||
|
||||
|
||||
# === Transpiled from signals (reactive signal runtime) ===
|
||||
|
||||
# signal
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
"""GraphSX URL router — evaluate s-expression URLs.
|
||||
|
||||
Handles URLs like /(language.(doc.introduction)) by:
|
||||
1. Converting dots to spaces (dot = whitespace sugar)
|
||||
2. Parsing the path as an SX expression
|
||||
3. Auto-quoting unknown symbols to strings (slugs)
|
||||
4. Evaluating the expression against page functions
|
||||
5. Wrapping the result in (~layouts/doc :path "..." content)
|
||||
6. Returning full page or OOB response
|
||||
1. Delegating to the bootstrapped spec (router.sx → prepare_url_expr):
|
||||
- Dots → spaces (URL-safe whitespace encoding)
|
||||
- Parse as SX expression
|
||||
- Auto-quote unknowns (symbols not in env → strings)
|
||||
2. Evaluating the prepared expression against page functions
|
||||
3. Wrapping the result in (~layouts/doc :path "..." content)
|
||||
4. Returning full page or OOB response
|
||||
|
||||
The URL evaluation logic lives in the SX spec (shared/sx/ref/router.sx)
|
||||
and is bootstrapped to Python (sx_ref.py) and JavaScript (sx-browser.js).
|
||||
This handler is generic infrastructure — all routing semantics are in SX.
|
||||
|
||||
Special cases:
|
||||
- "/" → home page
|
||||
@@ -22,110 +27,9 @@ from urllib.parse import unquote
|
||||
|
||||
logger = logging.getLogger("sx.router")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Page function names — known in the eval env, NOT auto-quoted
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Section functions (structural, pass through)
|
||||
_SECTION_FNS = {
|
||||
"home", "language", "geography", "applications", "etc",
|
||||
"hypermedia", "reactive", "marshes", "isomorphism",
|
||||
}
|
||||
|
||||
# Page functions (leaf dispatch)
|
||||
_PAGE_FNS = {
|
||||
"doc", "spec", "explore", "bootstrapper", "test",
|
||||
"reference", "reference-detail", "example",
|
||||
"cssx", "protocol", "essay", "philosophy", "plan",
|
||||
"sx-urls",
|
||||
}
|
||||
|
||||
# All known function names (don't auto-quote these)
|
||||
_KNOWN_FNS = _SECTION_FNS | _PAGE_FNS | {
|
||||
# Helpers defined in page-functions.sx
|
||||
"make-spec-files", "page-helpers-demo-content-fn",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Auto-quote slugs — convert unknown symbols to strings
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def auto_quote_slugs(expr: Any, known_fns: set[str]) -> Any:
|
||||
"""Walk AST and replace unknown symbols with their name as a string.
|
||||
|
||||
Known function names stay as symbols so they resolve to callables.
|
||||
Everything else (slugs like 'introduction', 'getting-started') becomes
|
||||
a string literal — no quoting needed in the URL.
|
||||
"""
|
||||
from shared.sx.types import Symbol, Keyword
|
||||
|
||||
if isinstance(expr, Symbol):
|
||||
if expr.name in known_fns or expr.name.startswith("~"):
|
||||
return expr
|
||||
return expr.name # auto-quote to string
|
||||
|
||||
if isinstance(expr, list) and expr:
|
||||
head = expr[0]
|
||||
# Head stays as-is (it's the function position)
|
||||
result = [head]
|
||||
for item in expr[1:]:
|
||||
result.append(auto_quote_slugs(item, known_fns))
|
||||
return result
|
||||
|
||||
return expr
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dot → space conversion
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _dots_to_spaces(s: str) -> str:
|
||||
"""Convert dots to spaces in URL expressions.
|
||||
|
||||
Dots are unreserved in RFC 3986 and serve as URL-safe whitespace.
|
||||
Applied before SX parsing: /(language.(doc.introduction))
|
||||
becomes /(language (doc introduction)).
|
||||
"""
|
||||
return s.replace(".", " ")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build expression from URL path
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _parse_url_path(raw_path: str) -> Any:
|
||||
"""Parse a URL path into an SX AST.
|
||||
|
||||
Returns the parsed expression, or None if the path isn't an SX URL.
|
||||
"""
|
||||
from shared.sx.parser import parse as sx_parse
|
||||
from shared.sx.types import Symbol
|
||||
|
||||
path = unquote(raw_path).strip()
|
||||
|
||||
if path == "/":
|
||||
return [Symbol("home")]
|
||||
|
||||
# SX URLs start with /( — e.g. /(language (doc intro))
|
||||
if path.startswith("/(") and path.endswith(")"):
|
||||
sx_source = _dots_to_spaces(path[1:]) # strip leading /
|
||||
return sx_parse(sx_source)
|
||||
|
||||
# Direct component URLs: /~component-name
|
||||
if path.startswith("/~"):
|
||||
name = path[1:] # keep the ~ prefix
|
||||
sx_source = _dots_to_spaces(name)
|
||||
if " " in sx_source:
|
||||
# /~comp.arg1.arg2 → (~comp arg1 arg2)
|
||||
return sx_parse(f"({sx_source})")
|
||||
return [Symbol(sx_source)]
|
||||
|
||||
return None # not an SX URL
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Streaming detection
|
||||
# Streaming detection (host-level concern, not in spec)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_STREAMING_PAGES = {
|
||||
@@ -176,25 +80,48 @@ async def eval_sx_url(raw_path: str) -> Any:
|
||||
|
||||
This is the main entry point for the catch-all route handler.
|
||||
Returns a Quart Response object, or None if the path isn't an SX URL.
|
||||
|
||||
URL parsing and auto-quoting are delegated to the bootstrapped spec
|
||||
functions (prepare_url_expr from router.sx). This handler provides
|
||||
only the host-level concerns: building the env, async eval, response
|
||||
formatting, streaming detection.
|
||||
"""
|
||||
from quart import make_response, Response
|
||||
from shared.sx.jinja_bridge import get_component_env, _get_request_context
|
||||
from shared.sx.pages import get_page, get_page_helpers, _eval_slot
|
||||
from shared.sx.types import Symbol, Keyword
|
||||
from shared.sx.parser import serialize
|
||||
from shared.sx.helpers import full_page_sx, oob_page_sx, sx_response
|
||||
from shared.sx.page import get_template_context
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
from shared.sx.ref.sx_ref import prepare_url_expr
|
||||
|
||||
# Parse URL
|
||||
expr = _parse_url_path(raw_path)
|
||||
if expr is None:
|
||||
return None # not an SX URL — let other handlers try
|
||||
path = unquote(raw_path).strip()
|
||||
|
||||
# Check for streaming page BEFORE auto-quoting
|
||||
# Home page
|
||||
if path == "/":
|
||||
expr = [Symbol("home")]
|
||||
else:
|
||||
# SX URLs: /(expr) or /~component
|
||||
if not (path.startswith("/(") or path.startswith("/~")):
|
||||
return None # not an SX URL — let other handlers try
|
||||
|
||||
# Build env for auto-quoting: components + page helpers
|
||||
env = dict(get_component_env())
|
||||
env.update(get_page_helpers("sx"))
|
||||
|
||||
# Use the bootstrapped spec: parse URL, auto-quote unknowns
|
||||
expr = prepare_url_expr(path[1:], env) # strip leading /
|
||||
|
||||
# Bare symbol (e.g. /~comp) → wrap in list for eval
|
||||
if isinstance(expr, Symbol):
|
||||
expr = [expr]
|
||||
|
||||
if not expr:
|
||||
return None
|
||||
|
||||
# Check for streaming page BEFORE eval
|
||||
streaming_page = _is_streaming_url(expr)
|
||||
if streaming_page:
|
||||
# Delegate to existing streaming PageDef infrastructure
|
||||
page_def = get_page("sx", streaming_page)
|
||||
if page_def:
|
||||
from shared.sx.pages import execute_page_streaming, execute_page_streaming_oob
|
||||
@@ -204,42 +131,24 @@ async def eval_sx_url(raw_path: str) -> Any:
|
||||
gen = await execute_page_streaming(page_def, "sx")
|
||||
return Response(gen, content_type="text/html; charset=utf-8")
|
||||
|
||||
# Build env: components + page helpers (includes page functions from define)
|
||||
env = dict(get_component_env())
|
||||
env.update(get_page_helpers("sx"))
|
||||
|
||||
# Auto-quote unknown symbols (slugs become strings)
|
||||
known = _KNOWN_FNS | set(env.keys())
|
||||
quoted_expr = auto_quote_slugs(expr, known)
|
||||
# Build env if not already built (home case)
|
||||
if path == "/":
|
||||
env = dict(get_component_env())
|
||||
env.update(get_page_helpers("sx"))
|
||||
|
||||
ctx = _get_request_context()
|
||||
|
||||
# Nav hrefs use /sx/ prefix — reconstruct the full path for nav matching
|
||||
path_str = f"/sx{raw_path}" if raw_path != "/" else "/sx/"
|
||||
|
||||
# Check if expression head is a component (~plans/content-addressed-components/name) — if so, skip
|
||||
# async_eval and pass directly to _eval_slot. Components contain HTML
|
||||
# tags that only the aser path can handle, not eval_expr.
|
||||
head = quoted_expr[0] if isinstance(quoted_expr, list) and quoted_expr else None
|
||||
is_component_call = (
|
||||
isinstance(head, Symbol)
|
||||
and head.name.startswith("~")
|
||||
)
|
||||
# Component calls go straight to _eval_slot (aser handles expansion).
|
||||
# Page function calls need async_eval first (routing + data fetching).
|
||||
head = expr[0] if isinstance(expr, list) and expr else None
|
||||
is_component_call = isinstance(head, Symbol) and head.name.startswith("~")
|
||||
|
||||
if is_component_call:
|
||||
# Direct component URL: /(~essays/sx-sucks/essay-sx-sucks) or /(~comp :key val)
|
||||
# Pass straight to _eval_slot — aser handles component expansion.
|
||||
page_ast = quoted_expr
|
||||
page_ast = expr
|
||||
else:
|
||||
# Two-phase evaluation for page function calls:
|
||||
# Phase 1: Evaluate the page function expression with async_eval.
|
||||
# Page functions return QUOTED expressions (unevaluated ASTs like
|
||||
# [Symbol("~docs-intro-content")] or quasiquoted trees with data).
|
||||
# This phase resolves routing + fetches data, but does NOT expand
|
||||
# components or handle HTML tags (eval_expr can't do that).
|
||||
# Phase 2: Wrap the returned AST in (~layouts/doc :path "..." <ast>) and
|
||||
# pass to _eval_slot (aser), which expands components and handles
|
||||
# HTML tags correctly.
|
||||
import os
|
||||
if os.environ.get("SX_USE_REF") == "1":
|
||||
from shared.sx.ref.async_eval_ref import async_eval
|
||||
@@ -247,14 +156,13 @@ async def eval_sx_url(raw_path: str) -> Any:
|
||||
from shared.sx.async_eval import async_eval
|
||||
|
||||
try:
|
||||
page_ast = await async_eval(quoted_expr, env, ctx)
|
||||
page_ast = await async_eval(expr, env, ctx)
|
||||
except Exception as e:
|
||||
logger.error("SX URL page-fn eval failed for %s: %s", raw_path, e, exc_info=True)
|
||||
return None
|
||||
|
||||
# page_ast is a quoted expression (list of Symbols/Keywords/data) or nil
|
||||
if page_ast is None:
|
||||
page_ast = [] # empty content for sections with no index
|
||||
page_ast = []
|
||||
|
||||
wrapped_ast = [
|
||||
Symbol("~layouts/doc"), Keyword("path"), path_str,
|
||||
|
||||
Reference in New Issue
Block a user