From 355f57a60bb1067ef37eb3902f6bb4592ab1ca7d Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 12 Mar 2026 22:27:52 +0000 Subject: [PATCH] Fix component name regex to support : and / in paths The dep scanner regex only matched [a-zA-Z0-9_-] in component names, missing the new path separators (/) and namespace delimiters (:). Fixed in deps.sx spec + rebootstrapped sx_ref.py and sx-browser.js. Also fixed the Python fallback in deps.py. Co-Authored-By: Claude Opus 4.6 --- shared/static/scripts/sx-browser.js | 4 +- shared/sx/deps.py | 2 +- shared/sx/ref/deps.sx | 2 +- shared/sx/ref/sx_ref.py | 326 +--------------------------- 4 files changed, 5 insertions(+), 329 deletions(-) diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 46a85e5..8d7c26b 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-12T18:28:35Z"; + var SX_VERSION = "2026-03-12T22:27:08Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -3409,7 +3409,7 @@ callExpr.push(dictGet(kwargs, k)); } } // scan-components-from-source var scanComponentsFromSource = function(source) { return (function() { - var matches = regexFindAll("\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)", source); + var matches = regexFindAll("\\(~([a-zA-Z_][a-zA-Z0-9_\\-:/]*)", source); return map(function(m) { return (String("~") + String(m)); }, matches); })(); }; diff --git a/shared/sx/deps.py b/shared/sx/deps.py index d2eec5c..f8d681a 100644 --- a/shared/sx/deps.py +++ b/shared/sx/deps.py @@ -126,7 +126,7 @@ def _compute_all_io_refs_fallback( def _scan_components_from_sx_fallback(source: str) -> set[str]: import re - return {f"~{m}" for m in re.findall(r'\(~([a-zA-Z_][a-zA-Z0-9_\-]*)', source)} + return {f"~{m}" for m in re.findall(r'\(~([a-zA-Z_][a-zA-Z0-9_\-:/]*)', source)} def _components_needed_fallback(page_sx: str, env: dict[str, Any]) -> set[str]: diff --git a/shared/sx/ref/deps.sx b/shared/sx/ref/deps.sx index b52a62e..b28becb 100644 --- a/shared/sx/ref/deps.sx +++ b/shared/sx/ref/deps.sx @@ -121,7 +121,7 @@ (define scan-components-from-source :effects [] (fn ((source :as string)) - (let ((matches (regex-find-all "\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)" source))) + (let ((matches (regex-find-all "\\(~([a-zA-Z_][a-zA-Z0-9_\\-:/]*)" source))) (map (fn ((m :as string)) (str "~" m)) matches)))) diff --git a/shared/sx/ref/sx_ref.py b/shared/sx/ref/sx_ref.py index ad43393..ef96090 100644 --- a/shared/sx/ref/sx_ref.py +++ b/shared/sx/ref/sx_ref.py @@ -2578,7 +2578,7 @@ def compute_all_deps(env): # scan-components-from-source def scan_components_from_source(source): - matches = regex_find_all('\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)', source) + matches = regex_find_all('\\(~([a-zA-Z_][a-zA-Z0-9_\\-:/]*)', source) return map(lambda m: sx_str('~', m), matches) # components-needed @@ -2881,330 +2881,6 @@ 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 - - # === Transpiled from signals (reactive signal runtime) === # signal