20 Commits

Author SHA1 Message Date
8f88e52b27 Add DOM primitives (dom-set-prop, dom-call-method, dom-post-message), bump SW cache v2, remove video demo
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6m57s
New platform_js primitives for direct DOM property/method access and
cross-origin iframe communication. Service worker static cache bumped
to v2 to flush stale assets. Removed experimental video embed from
header island, routes, and home page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 21:51:05 +00:00
b8018ba385 Add type annotations to federation-choose-username defcomp params
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 21:03:17 +00:00
95ffc0ecb7 Merge worktree-typed into macros: defcomp type annotations 2026-03-11 21:02:12 +00:00
477ce766ff Add (param :as type) annotations to defcomp params across all services and templates
Annotates ~500 defcomp params across 62 files: market (5), blog (7), cart (5),
events (3), federation (4), account (3), orders (2), shared templates (11),
sx docs (14), plus remaining spec fn params (z3, test-framework, adapter-dom,
adapter-async, engine, eval). Total annotations in codebase: 1043.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 21:01:02 +00:00
98c1023b81 Merge branch 'worktree-typed' into macros 2026-03-11 20:27:43 +00:00
b99e69d1bb Add (param :as type) annotations to all fn/lambda params across SX spec
Extend the type annotation system from defcomp-only to fn/lambda params:
- Infrastructure: sf-lambda, py/js-collect-params-loop, and bootstrap_py.py
  now recognize (name :as type) in param lists, extracting just the name
- bootstrap_py.py: add _extract_param_name() helper, fix _emit_for_each_stmt
- 521 type annotations across 22 .sx spec files (eval, types, adapters,
  transpilers, engine, orchestration, deps, signals, router, prove, etc.)
- Zero behavioral change: annotations are metadata for static analysis only
- All bootstrappers (Python, JS, G1) pass, 81/81 spec tests pass

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 20:27:36 +00:00
a425ea8ed4 Marsh demo: video embed with reactive+hypermedia interplay
- ~video-player defisland persists across SPA navigations (morph-safe)
- Clicking "reactive" cycles colour (signal) + fetches random YouTube video (sx-get)
- sx-trigger="fetch-video" + dom-first-child check: video keeps playing on repeat clicks
- Close button (x) clears video via /api/clear-video hypermedia endpoint
- Autoplay+mute removes YouTube's red play button overlay
- Header restructured: logo in anchor, tagline outside (no accidental navigation)
- Flex centering on video container

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 20:27:04 +00:00
c82941d93c Merge main into macros: resolve nav restructure conflicts
Take HEAD's updated typed-sx content (deftype, effect system details)
with main's /etc/plans/ path prefix. Take main's newer sx-browser.js
timestamp.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 19:26:56 +00:00
9b38ef2ce9 Add deftype and static effect system to typed-sx plan
Phase 6 (deftype): type aliases, unions, records (typed dict shapes),
parameterized types. Phase 7: pragmatic static effect checking — io,
dom, async, state annotations with render-mode enforcement, no
algebraic handlers, zero runtime cost. Phases 1-5 marked done.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 19:12:37 +00:00
4d54be6b6b Restructure SX docs nav into 4 top-level sections with nested routing
New hierarchy: Geography (Reactive Islands, Hypermedia Lakes, Marshes,
Isomorphism), Language (Docs, Specs, Bootstrappers, Testing),
Applications (CSSX, Protocols), Etc (Essays, Philosophy, Plans).

All routes updated to match: /reactive/* → /geography/reactive/*,
/docs/* → /language/docs/*, /essays/* → /etc/essays/*, etc.
Updates nav-data.sx, all defpage routes, API endpoints, internal links
across 43 files. Enhanced find-nav-match for nested group resolution.

Also includes: page-helpers-demo sf-total fix (reduce instead of set!),
rebootstrapped sx-browser.js and sx_ref.py, defensive slice/rest guards.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 18:50:31 +00:00
5d5512e74a Add typed params to 67 primitives, implement check-primitive-call
Annotate all primitives in primitives.sx with (:as type) param types
where meaningful (67/80 — 13 polymorphic ops stay untyped). Add
parse_primitive_param_types() to boundary_parser.py for extraction.
Implement check-primitive-call in types.sx with full positional + rest
param validation, thread prim-param-types through check-body-walk,
check-component, and check-all. 10 new tests (438 total, all pass).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 18:39:20 +00:00
8a530569a2 Add (name :as type) annotation syntax for defcomp params
parse-comp-params now recognizes (name :as type) — a 3-element list
with :as keyword separator. Type annotations are stored on the
Component via component-param-types and used by types.sx for call-site
checking. Unannotated params default to any. 428/428 tests pass (50
types tests including 6 annotation tests).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 17:12:54 +00:00
b82fd7822d Merge branch 'main' into worktree-typed-sx
# Conflicts:
#	shared/sx/ref/platform_py.py
#	shared/sx/ref/sx_ref.py
2026-03-11 17:06:30 +00:00
e5dbe9f3da Add types.sx gradual type system spec module with 44 tests
Implements subtype checking, type inference, type narrowing, and
component call-site checking. All type logic is in types.sx (spec),
bootstrapped to every host. Adds test-types.sx with full coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 17:06:09 +00:00
0174fbfea3 Regenerate sx-browser.js — file was accidentally emptied in previous commit
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:56:51 +00:00
cd7653d8c3 Fix cond ambiguity: check ALL clauses with cond-scheme?, not just first
The cond special form misclassified Clojure-style as scheme-style when
the first test was a 2-element list like (nil? x) — treating it as a
scheme clause ((test body)) instead of a function call. Define
cond-scheme? using every? to check ALL clauses, fix eval.sx sf-cond and
render.sx eval-cond, rewrite engine.sx parse-time/filter-params as
nested if to avoid the ambiguity, add regression tests across eval/
render/aser specs. 378/378 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:51:41 +00:00
ff6c1fab71 Fix process-bindings scope loss and async-invoke arity, bootstrap async adapter
Two bugs fixed:
1. process-bindings used merge(env) which returns {} for Env objects
   (Env is not a dict subclass). Changed to env-extend in render.sx
   and adapter-async.sx. This caused "Undefined symbol: theme" etc.
2. async-aser-eval-call passed evaled-args list to async-invoke(&rest),
   double-wrapping it. Changed to inline apply + coroutine check.

Also: bootstrap define-async into sx_ref.py (Phase 6), replace ~1000 LOC
hand-written async_eval_ref.py with 24-line thin re-export shim.

Test runner now uses Env (not flat dict) for render envs to catch scope bugs.
8 new regression tests (4 scope chain, 2 native callable arity, 2 render).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:38:47 +00:00
e843602ac9 Fix aser list flattening bug, add wire format test suite (41 tests)
The sync aser-call in adapter-sx.sx didn't flatten list results from
map/filter in positional children — serialize(list) wrapped in parens
creating ((div ...) ...) which re-parses as an invalid call. Rewrote
aser-call from reduce to for-each (bootstrapper can't nest for-each
inside reduce lambdas) and added list flattening in both aser-call
and aser-fragment.

Also adds test-aser.sx (41 tests), render-sx platform function,
expanded test-render.sx (+7 map/filter children tests), and specs
async-eval-slot-inner in adapter-async.sx.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:59:31 +00:00
c95e19dcf2 Page helpers demo: defisland, map-in-children fix, _eval_slot ref evaluator
- Add page-helpers-demo page with defisland ~demo-client-runner (pure SX,
  zero JS files) showing spec functions running on both server and client
- Fix _aser_component children serialization: flatten list results from map
  instead of serialize(list) which wraps in parens creating ((div ...) ...)
  that re-parses as invalid function call. Fixed in adapter-async.sx spec
  and async_eval_ref.py
- Switch _eval_slot to use async_eval_ref.py when SX_USE_REF=1 (was
  hardcoded to async_eval.py)
- Add Island type support to async_eval_ref.py: import, SSR rendering,
  aser dispatch, thread-first, defisland in _ASER_FORMS
- Add server affinity check: components with :affinity :server expand
  even when _expand_components is False
- Add diagnostic _aser_stack context to EvalError messages
- New spec files: adapter-async.sx, page-helpers.sx, platform_js.py
- Bootstrappers: page-helpers module support, performance.now() timing
- 0-arity lambda event handler fix in adapter-dom.sx

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:30:12 +00:00
29c90a625b Delete evaluator.py shim: all imports go directly to bootstrapped sx_ref.py
EvalError moved to types.py. All 27 files updated to import eval_expr,
trampoline, call_lambda, etc. directly from shared.sx.ref.sx_ref instead
of through the evaluator.py indirection layer. 320/320 spec tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 11:15:48 +00:00
150 changed files with 10960 additions and 3855 deletions

View File

@@ -1,12 +1,12 @@
;; Auth page components (device auth — account-specific) ;; Auth page components (device auth — account-specific)
;; Login and check-email components are shared: see shared/sx/templates/auth.sx ;; Login and check-email components are shared: see shared/sx/templates/auth.sx
(defcomp ~account-device-error (&key error) (defcomp ~account-device-error (&key (error :as string))
(when error (when error
(div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4" (div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4"
error))) error)))
(defcomp ~account-device-form (&key error action csrf-token code) (defcomp ~account-device-form (&key error (action :as string) (csrf-token :as string) (code :as string))
(div :class "py-8 max-w-md mx-auto" (div :class "py-8 max-w-md mx-auto"
(h1 :class "text-2xl font-bold mb-6" "Authorize device") (h1 :class "text-2xl font-bold mb-6" "Authorize device")
(p :class "text-stone-600 mb-4" "Enter the code shown in your terminal to sign in.") (p :class "text-stone-600 mb-4" "Enter the code shown in your terminal to sign in.")
@@ -29,21 +29,21 @@
;; Assembled auth page content — replaces Python _login_page_content etc. ;; Assembled auth page content — replaces Python _login_page_content etc.
(defcomp ~account-login-content (&key error email) (defcomp ~account-login-content (&key (error :as string?) (email :as string?))
(~auth-login-form (~auth-login-form
:error (when error (~auth-error-banner :error error)) :error (when error (~auth-error-banner :error error))
:action (url-for "auth.start_login") :action (url-for "auth.start_login")
:csrf-token (csrf-token) :csrf-token (csrf-token)
:email (or email ""))) :email (or email "")))
(defcomp ~account-device-content (&key error code) (defcomp ~account-device-content (&key (error :as string?) (code :as string?))
(~account-device-form (~account-device-form
:error (when error (~account-device-error :error error)) :error (when error (~account-device-error :error error))
:action (url-for "auth.device_submit") :action (url-for "auth.device_submit")
:csrf-token (csrf-token) :csrf-token (csrf-token)
:code (or code ""))) :code (or code "")))
(defcomp ~account-check-email-content (&key email email-error) (defcomp ~account-check-email-content (&key (email :as string?) (email-error :as string?))
(~auth-check-email (~auth-check-email
:email (escape (or email "")) :email (escape (or email ""))
:error (when email-error :error (when email-error

View File

@@ -1,26 +1,26 @@
;; Account dashboard components ;; Account dashboard components
(defcomp ~account-error-banner (&key error) (defcomp ~account-error-banner (&key (error :as string))
(when error (when error
(div :class "rounded-lg border border-red-200 bg-red-50 text-red-800 px-4 py-3 text-sm" (div :class "rounded-lg border border-red-200 bg-red-50 text-red-800 px-4 py-3 text-sm"
error))) error)))
(defcomp ~account-user-email (&key email) (defcomp ~account-user-email (&key (email :as string))
(when email (when email
(p :class "text-sm text-stone-500 mt-1" email))) (p :class "text-sm text-stone-500 mt-1" email)))
(defcomp ~account-user-name (&key name) (defcomp ~account-user-name (&key (name :as string))
(when name (when name
(p :class "text-sm text-stone-600" name))) (p :class "text-sm text-stone-600" name)))
(defcomp ~account-logout-form (&key csrf-token) (defcomp ~account-logout-form (&key (csrf-token :as string))
(form :action "/auth/logout/" :method "post" (form :action "/auth/logout/" :method "post"
(input :type "hidden" :name "csrf_token" :value csrf-token) (input :type "hidden" :name "csrf_token" :value csrf-token)
(button :type "submit" (button :type "submit"
:class "inline-flex items-center gap-2 rounded-full border border-stone-300 px-4 py-2 text-sm font-medium text-stone-700 hover:bg-stone-50 transition" :class "inline-flex items-center gap-2 rounded-full border border-stone-300 px-4 py-2 text-sm font-medium text-stone-700 hover:bg-stone-50 transition"
(i :class "fa-solid fa-right-from-bracket text-xs") " Sign out"))) (i :class "fa-solid fa-right-from-bracket text-xs") " Sign out")))
(defcomp ~account-label-item (&key name) (defcomp ~account-label-item (&key (name :as string))
(span :class "inline-flex items-center rounded-full border border-stone-200 px-3 py-1 text-xs font-medium bg-white/60" (span :class "inline-flex items-center rounded-full border border-stone-200 px-3 py-1 text-xs font-medium bg-white/60"
name)) name))
@@ -43,7 +43,7 @@
labels))) labels)))
;; Assembled dashboard content — replaces Python _account_main_panel_sx ;; Assembled dashboard content — replaces Python _account_main_panel_sx
(defcomp ~account-dashboard-content (&key error) (defcomp ~account-dashboard-content (&key (error :as string?))
(let* ((user (current-user)) (let* ((user (current-user))
(csrf (csrf-token))) (csrf (csrf-token)))
(~account-main-panel (~account-main-panel

View File

@@ -1,17 +1,17 @@
;; Newsletter management components ;; Newsletter management components
(defcomp ~account-newsletter-desc (&key description) (defcomp ~account-newsletter-desc (&key (description :as string))
(when description (when description
(p :class "text-xs text-stone-500 mt-0.5 truncate" description))) (p :class "text-xs text-stone-500 mt-0.5 truncate" description)))
(defcomp ~account-newsletter-toggle (&key id url hdrs target cls checked knob-cls) (defcomp ~account-newsletter-toggle (&key (id :as string) (url :as string) (hdrs :as dict) (target :as string) (cls :as string) (checked :as string) (knob-cls :as string))
(div :id id :class "flex items-center" (div :id id :class "flex items-center"
(button :sx-post url :sx-headers hdrs :sx-target target :sx-swap "outerHTML" (button :sx-post url :sx-headers hdrs :sx-target target :sx-swap "outerHTML"
:class cls :role "switch" :aria-checked checked :class cls :role "switch" :aria-checked checked
(span :class knob-cls)))) (span :class knob-cls))))
(defcomp ~account-newsletter-item (&key name desc toggle) (defcomp ~account-newsletter-item (&key (name :as string) desc toggle)
(div :class "flex items-center justify-between py-4 first:pt-0 last:pb-0" (div :class "flex items-center justify-between py-4 first:pt-0 last:pb-0"
(div :class "min-w-0 flex-1" (div :class "min-w-0 flex-1"
(p :class "text-sm font-medium text-stone-800" name) (p :class "text-sm font-medium text-stone-800" name)
@@ -32,7 +32,7 @@
;; Assembled newsletters content — replaces Python _newsletters_panel_sx ;; Assembled newsletters content — replaces Python _newsletters_panel_sx
;; Takes pre-fetched newsletter-list from page helper ;; Takes pre-fetched newsletter-list from page helper
(defcomp ~account-newsletters-content (&key newsletter-list account-url) (defcomp ~account-newsletters-content (&key (newsletter-list :as list) (account-url :as string?))
(let* ((csrf (csrf-token))) (let* ((csrf (csrf-token)))
(if (empty? newsletter-list) (if (empty? newsletter-list)
(~account-newsletter-empty) (~account-newsletter-empty)

View File

@@ -1,6 +1,6 @@
;; Blog admin panel components ;; Blog admin panel components
(defcomp ~blog-cache-panel (&key clear-url csrf) (defcomp ~blog-cache-panel (&key (clear-url :as string) (csrf :as string))
(div :class "max-w-2xl mx-auto px-4 py-6 space-y-6" (div :class "max-w-2xl mx-auto px-4 py-6 space-y-6"
(div :class "flex flex-col md:flex-row gap-3 items-start" (div :class "flex flex-col md:flex-row gap-3 items-start"
(form :sx-post clear-url :sx-trigger "submit" :sx-target "#cache-status" :sx-swap "innerHTML" (form :sx-post clear-url :sx-trigger "submit" :sx-target "#cache-status" :sx-swap "innerHTML"
@@ -19,10 +19,10 @@
:sx-headers hx-headers :class "text-sm border border-stone-300 rounded px-2 py-1" :sx-headers hx-headers :class "text-sm border border-stone-300 rounded px-2 py-1"
options)) options))
(defcomp ~blog-snippet-option (&key value selected label) (defcomp ~blog-snippet-option (&key (value :as string) (selected :as boolean) (label :as string))
(option :value value :selected selected label)) (option :value value :selected selected label))
(defcomp ~blog-snippet-row (&key name owner badge-cls visibility extra) (defcomp ~blog-snippet-row (&key (name :as string) (owner :as string) (badge-cls :as string) (visibility :as string) extra)
(div :class "flex items-center gap-4 p-4 hover:bg-stone-50 transition" (div :class "flex items-center gap-4 p-4 hover:bg-stone-50 transition"
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
(div :class "font-medium truncate" name) (div :class "font-medium truncate" name)
@@ -42,7 +42,7 @@
(div :id "menu-item-form" :class "mb-6") (div :id "menu-item-form" :class "mb-6")
(div :id "menu-items-list" list))) (div :id "menu-items-list" list)))
(defcomp ~blog-menu-item-row (&key img label slug sort-order edit-url delete-url confirm-text hx-headers) (defcomp ~blog-menu-item-row (&key img (label :as string) (slug :as string) (sort-order :as string) (edit-url :as string) (delete-url :as string) (confirm-text :as string) hx-headers)
(div :class "flex items-center gap-4 p-4 hover:bg-stone-50 transition" (div :class "flex items-center gap-4 p-4 hover:bg-stone-50 transition"
(div :class "text-stone-400 cursor-move" (i :class "fa fa-grip-vertical")) (div :class "text-stone-400 cursor-move" (i :class "fa fa-grip-vertical"))
img img
@@ -81,7 +81,7 @@
(div :class "h-8 w-8 rounded-full text-xs font-semibold flex items-center justify-center border border-stone-300 flex-shrink-0" (div :class "h-8 w-8 rounded-full text-xs font-semibold flex items-center justify-center border border-stone-300 flex-shrink-0"
:style style initial)) :style style initial))
(defcomp ~blog-tag-group-li (&key icon edit-href name slug sort-order) (defcomp ~blog-tag-group-li (&key icon (edit-href :as string) (name :as string) (slug :as string) (sort-order :as number))
(li :class "border rounded p-3 bg-white flex items-center gap-3" (li :class "border rounded p-3 bg-white flex items-center gap-3"
icon icon
(div :class "flex-1" (div :class "flex-1"
@@ -106,7 +106,7 @@
;; Tag group edit ;; Tag group edit
(defcomp ~blog-tag-checkbox (&key tag-id checked img name) (defcomp ~blog-tag-checkbox (&key (tag-id :as string) (checked :as boolean) img (name :as string))
(label :class "flex items-center gap-2 px-2 py-1 hover:bg-stone-50 rounded text-sm cursor-pointer" (label :class "flex items-center gap-2 px-2 py-1 hover:bg-stone-50 rounded text-sm cursor-pointer"
(input :type "checkbox" :name "tag_ids" :value tag-id :checked checked :class "rounded border-stone-300") (input :type "checkbox" :name "tag_ids" :value tag-id :checked checked :class "rounded border-stone-300")
img (span name))) img (span name)))
@@ -114,7 +114,7 @@
(defcomp ~blog-tag-checkbox-image (&key src) (defcomp ~blog-tag-checkbox-image (&key src)
(img :src src :alt "" :class "h-4 w-4 rounded-full object-cover")) (img :src src :alt "" :class "h-4 w-4 rounded-full object-cover"))
(defcomp ~blog-tag-group-edit-form (&key save-url csrf name colour sort-order feature-image tags) (defcomp ~blog-tag-group-edit-form (&key (save-url :as string) (csrf :as string) (name :as string) (colour :as string?) (sort-order :as number) (feature-image :as string?) tags)
(form :method "post" :action save-url :class "border rounded p-4 bg-white space-y-4" (form :method "post" :action save-url :class "border rounded p-4 bg-white space-y-4"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(div :class "space-y-3" (div :class "space-y-3"
@@ -133,7 +133,7 @@
(div :class "flex gap-3" (div :class "flex gap-3"
(button :type "submit" :class "border rounded px-4 py-2 bg-stone-800 text-white text-sm" "Save")))) (button :type "submit" :class "border rounded px-4 py-2 bg-stone-800 text-white text-sm" "Save"))))
(defcomp ~blog-tag-group-delete-form (&key delete-url csrf) (defcomp ~blog-tag-group-delete-form (&key (delete-url :as string) (csrf :as string))
(form :method "post" :action delete-url :class "border-t pt-4" (form :method "post" :action delete-url :class "border-t pt-4"
:onsubmit "return confirm('Delete this tag group? Tags will not be deleted.')" :onsubmit "return confirm('Delete this tag group? Tags will not be deleted.')"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)

View File

@@ -4,17 +4,17 @@
(div :class "absolute top-20 right-2 z-10 text-6xl md:text-4xl" (div :class "absolute top-20 right-2 z-10 text-6xl md:text-4xl"
(~blog-like-toggle :like-url like-url :hx-headers hx-headers :heart heart))) (~blog-like-toggle :like-url like-url :hx-headers hx-headers :heart heart)))
(defcomp ~blog-draft-status (&key publish-requested timestamp) (defcomp ~blog-draft-status (&key (publish-requested :as boolean) (timestamp :as string?))
(<> (div :class "flex justify-center gap-2 mt-1" (<> (div :class "flex justify-center gap-2 mt-1"
(span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-amber-100 text-amber-800" "Draft") (span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-amber-100 text-amber-800" "Draft")
(when publish-requested (span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-800" "Publish requested"))) (when publish-requested (span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-800" "Publish requested")))
(when timestamp (p :class "text-sm text-stone-500" (str "Updated: " timestamp))))) (when timestamp (p :class "text-sm text-stone-500" (str "Updated: " timestamp)))))
(defcomp ~blog-published-status (&key timestamp) (defcomp ~blog-published-status (&key (timestamp :as string))
(p :class "text-sm text-stone-500" (str "Published: " timestamp))) (p :class "text-sm text-stone-500" (str "Published: " timestamp)))
;; Tag components — accept data, not HTML ;; Tag components — accept data, not HTML
(defcomp ~blog-tag-icon (&key src name initial) (defcomp ~blog-tag-icon (&key (src :as string?) (name :as string) (initial :as string))
(if src (if src
(img :src src :alt name :class "h-4 w-4 rounded-full object-cover border border-stone-300 flex-shrink-0") (img :src src :alt name :class "h-4 w-4 rounded-full object-cover border border-stone-300 flex-shrink-0")
(div :class "h-4 w-4 rounded-full text-[8px] font-semibold flex items-center justify-center border border-stone-300 flex-shrink-0 bg-stone-200 text-stone-600" initial))) (div :class "h-4 w-4 rounded-full text-[8px] font-semibold flex items-center justify-center border border-stone-300 flex-shrink-0 bg-stone-200 text-stone-600" initial)))
@@ -45,12 +45,12 @@
(span :class "text-stone-700" name))) (span :class "text-stone-700" name)))
;; Card — accepts pure data ;; Card — accepts pure data
(defcomp ~blog-card (&key slug href hx-select title (defcomp ~blog-card (&key (slug :as string) (href :as string) (hx-select :as string?) (title :as string)
feature-image excerpt (feature-image :as string?) (excerpt :as string?)
status is-draft publish-requested status-timestamp status (is-draft :as boolean) (publish-requested :as boolean) (status-timestamp :as string?)
liked like-url csrf-token (liked :as boolean) (like-url :as string?) (csrf-token :as string?)
has-like (has-like :as boolean)
tags authors widget) (tags :as list?) (authors :as list?) widget)
(article :class "border-b pb-6 last:border-b-0 relative" (article :class "border-b pb-6 last:border-b-0 relative"
(when has-like (when has-like
(~blog-like-button (~blog-like-button
@@ -80,9 +80,9 @@
(ul :class "flex flex-wrap gap-2 text-sm" (ul :class "flex flex-wrap gap-2 text-sm"
(map (lambda (a) (~blog-author-item :image (get a "image") :name (get a "name"))) authors)))))))) (map (lambda (a) (~blog-author-item :image (get a "image") :name (get a "name"))) authors))))))))
(defcomp ~blog-card-tile (&key href hx-select feature-image title (defcomp ~blog-card-tile (&key (href :as string) (hx-select :as string?) (feature-image :as string?) (title :as string)
is-draft publish-requested status-timestamp (is-draft :as boolean) (publish-requested :as boolean) (status-timestamp :as string?)
excerpt tags authors) (excerpt :as string?) (tags :as list?) (authors :as list?))
(article :class "relative" (article :class "relative"
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select (or hx-select "#main-panel") :sx-swap "outerHTML" :sx-push-url "true" :sx-select (or hx-select "#main-panel") :sx-swap "outerHTML" :sx-push-url "true"
@@ -107,7 +107,7 @@
(map (lambda (a) (~blog-author-item :image (get a "image") :name (get a "name"))) authors)))))))) (map (lambda (a) (~blog-author-item :image (get a "image") :name (get a "name"))) authors))))))))
;; Data-driven blog cards list (replaces Python _blog_cards_sx loop) ;; Data-driven blog cards list (replaces Python _blog_cards_sx loop)
(defcomp ~blog-cards-from-data (&key posts view sentinel) (defcomp ~blog-cards-from-data (&key (posts :as list?) (view :as string?) sentinel)
(<> (<>
(map (lambda (p) (map (lambda (p)
(if (= view "tile") (if (= view "tile")
@@ -131,7 +131,7 @@
sentinel)) sentinel))
;; Data-driven page cards list (replaces Python _page_cards_sx loop) ;; Data-driven page cards list (replaces Python _page_cards_sx loop)
(defcomp ~page-cards-from-data (&key pages sentinel) (defcomp ~page-cards-from-data (&key (pages :as list?) sentinel)
(<> (<>
(map (lambda (pg) (map (lambda (pg)
(~blog-page-card (~blog-page-card
@@ -150,7 +150,7 @@
(when has-market (span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800" (when has-market (span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800"
(i :class "fa fa-shopping-bag mr-1") "Market")))) (i :class "fa fa-shopping-bag mr-1") "Market"))))
(defcomp ~blog-page-card (&key href hx-select title has-calendar has-market pub-timestamp feature-image excerpt) (defcomp ~blog-page-card (&key (href :as string) (hx-select :as string?) (title :as string) (has-calendar :as boolean) (has-market :as boolean) (pub-timestamp :as string?) (feature-image :as string?) (excerpt :as string?))
(article :class "border-b pb-6 last:border-b-0 relative" (article :class "border-b pb-6 last:border-b-0 relative"
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select (or hx-select "#main-panel") :sx-swap "outerHTML" :sx-push-url "true" :sx-select (or hx-select "#main-panel") :sx-swap "outerHTML" :sx-push-url "true"

View File

@@ -1,6 +1,6 @@
;; Blog post detail components ;; Blog post detail components
(defcomp ~blog-detail-edit-link (&key href hx-select) (defcomp ~blog-detail-edit-link (&key (href :as string) (hx-select :as string))
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class "inline-block px-3 py-1 rounded-full text-sm font-semibold bg-stone-700 text-white hover:bg-stone-800 transition-colors" :class "inline-block px-3 py-1 rounded-full text-sm font-semibold bg-stone-700 text-white hover:bg-stone-800 transition-colors"
@@ -20,7 +20,7 @@
(div :class "absolute top-2 right-2 z-10 text-8xl md:text-6xl" (div :class "absolute top-2 right-2 z-10 text-8xl md:text-6xl"
(~blog-like-toggle :like-url like-url :hx-headers hx-headers :heart heart))) (~blog-like-toggle :like-url like-url :hx-headers hx-headers :heart heart)))
(defcomp ~blog-detail-excerpt (&key excerpt) (defcomp ~blog-detail-excerpt (&key (excerpt :as string))
(div :class "w-full text-center italic text-3xl p-2" excerpt)) (div :class "w-full text-center italic text-3xl p-2" excerpt))
(defcomp ~blog-detail-chrome (&key like excerpt at-bar) (defcomp ~blog-detail-chrome (&key like excerpt at-bar)
@@ -43,10 +43,10 @@
;; Data-driven composition — replaces _post_main_panel_sx ;; Data-driven composition — replaces _post_main_panel_sx
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-post-detail-content (&key slug is-draft publish-requested can-edit edit-href (defcomp ~blog-post-detail-content (&key (slug :as string) (is-draft :as boolean) (publish-requested :as boolean) (can-edit :as boolean) (edit-href :as string?)
is-page has-user liked like-url csrf (is-page :as boolean) (has-user :as boolean) (liked :as boolean) (like-url :as string?) (csrf :as string?)
custom-excerpt tags authors (custom-excerpt :as string?) (tags :as list?) (authors :as list?)
feature-image html-content sx-content) (feature-image :as string?) (html-content :as string?) (sx-content :as string?))
(let* ((hx-select "#main-panel") (let* ((hx-select "#main-panel")
(draft-sx (when is-draft (draft-sx (when is-draft
(~blog-detail-draft (~blog-detail-draft
@@ -70,7 +70,7 @@
:html-content html-content :html-content html-content
:sx-content sx-content))) :sx-content sx-content)))
(defcomp ~blog-meta (&key robots page-title desc canonical og-type og-title image twitter-card twitter-title) (defcomp ~blog-meta (&key (robots :as string) (page-title :as string) (desc :as string) (canonical :as string?) (og-type :as string) (og-title :as string) (image :as string?) (twitter-card :as string) (twitter-title :as string))
(<> (<>
(meta :name "robots" :content robots) (meta :name "robots" :content robots)
(title page-title) (title page-title)

View File

@@ -4,7 +4,7 @@
(div :class "max-w-[768px] mx-auto mt-[16px] rounded-[8px] border border-red-300 bg-red-50 px-[16px] py-[12px] text-[14px] text-red-700" (div :class "max-w-[768px] mx-auto mt-[16px] rounded-[8px] border border-red-300 bg-red-50 px-[16px] py-[12px] text-[14px] text-red-700"
(strong "Save failed:") " " error)) (strong "Save failed:") " " error))
(defcomp ~blog-editor-form (&key csrf title-placeholder create-label) (defcomp ~blog-editor-form (&key (csrf :as string) (title-placeholder :as string) (create-label :as string))
(form :id "post-new-form" :method "post" :class "max-w-[768px] mx-auto pb-[48px]" (form :id "post-new-form" :method "post" :class "max-w-[768px] mx-auto pb-[48px]"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(input :type "hidden" :id "lexical-json-input" :name "lexical" :value "") (input :type "hidden" :id "lexical-json-input" :name "lexical" :value "")
@@ -56,11 +56,11 @@
:class "px-[20px] py-[6px] bg-stone-700 text-white text-[14px] rounded-[8px] hover:bg-stone-800 transition-colors cursor-pointer" create-label)))) :class "px-[20px] py-[6px] bg-stone-700 text-white text-[14px] rounded-[8px] hover:bg-stone-800 transition-colors cursor-pointer" create-label))))
;; Edit form — pre-populated version for /<slug>/admin/edit/ ;; Edit form — pre-populated version for /<slug>/admin/edit/
(defcomp ~blog-editor-edit-form (&key csrf updated-at title-val excerpt-val (defcomp ~blog-editor-edit-form (&key (csrf :as string) (updated-at :as string) (title-val :as string?) (excerpt-val :as string?)
feature-image feature-image-caption (feature-image :as string?) (feature-image-caption :as string?)
sx-content-val lexical-json (sx-content-val :as string?) (lexical-json :as string?)
has-sx title-placeholder (has-sx :as boolean) (title-placeholder :as string)
status already-emailed (status :as string) (already-emailed :as boolean)
newsletter-options footer-extra) newsletter-options footer-extra)
(let* ((sel-cls "text-[14px] rounded-[4px] border border-stone-200 px-[8px] py-[6px] bg-white text-stone-600") (let* ((sel-cls "text-[14px] rounded-[4px] border border-stone-200 px-[8px] py-[6px] bg-white text-stone-600")
(active "px-[12px] py-[6px] text-[13px] font-medium text-stone-700 border-b-2 border-stone-700 cursor-pointer bg-transparent") (active "px-[12px] py-[6px] text-[13px] font-medium text-stone-700 border-b-2 border-stone-700 cursor-pointer bg-transparent")
@@ -153,14 +153,14 @@
" sync();" " sync();"
"})();")) "})();"))
(defcomp ~blog-editor-styles (&key css-href) (defcomp ~blog-editor-styles (&key (css-href :as string))
(<> (link :rel "stylesheet" :href css-href) (<> (link :rel "stylesheet" :href css-href)
(style (style
"#lexical-editor { display: flow-root; }" "#lexical-editor { display: flow-root; }"
"#lexical-editor [data-kg-card=\"html\"] * { float: none !important; }" "#lexical-editor [data-kg-card=\"html\"] * { float: none !important; }"
"#lexical-editor [data-kg-card=\"html\"] table { width: 100% !important; }"))) "#lexical-editor [data-kg-card=\"html\"] table { width: 100% !important; }")))
(defcomp ~blog-editor-scripts (&key js-src sx-editor-js-src init-js) (defcomp ~blog-editor-scripts (&key (js-src :as string) (sx-editor-js-src :as string?) (init-js :as string))
(<> (script :src js-src) (<> (script :src js-src)
(when sx-editor-js-src (script :src sx-editor-js-src)) (when sx-editor-js-src (script :src sx-editor-js-src))
(script init-js))) (script init-js)))

View File

@@ -1,11 +1,11 @@
;; Blog filter components ;; Blog filter components
(defcomp ~blog-action-button (&key href hx-select btn-class title icon-class label) (defcomp ~blog-action-button (&key (href :as string) (hx-select :as string) (btn-class :as string) (title :as string) (icon-class :as string) (label :as string))
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class btn-class :title title (i :class icon-class) label)) :class btn-class :title title (i :class icon-class) label))
(defcomp ~blog-drafts-button (&key href hx-select btn-class title label draft-count) (defcomp ~blog-drafts-button (&key (href :as string) (hx-select :as string) (btn-class :as string) (title :as string) (label :as string) (draft-count :as number))
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class btn-class :title title (i :class "fa fa-file-text-o mr-1") " Drafts " :class btn-class :title title (i :class "fa fa-file-text-o mr-1") " Drafts "
@@ -61,7 +61,7 @@
(span :class "flex-1") (span :class "flex-1")
(span :class "inline-block bg-stone-100 text-stone-600 px-2 py-1 text-xs font-medium border border-stone-200" count)))) (span :class "inline-block bg-stone-100 text-stone-600 px-2 py-1 text-xs font-medium border border-stone-200" count))))
(defcomp ~blog-filter-summary (&key text) (defcomp ~blog-filter-summary (&key (text :as string))
(span :class "text-sm text-stone-600" text)) (span :class "text-sm text-stone-600" text))
;; Data-driven tag groups filter (replaces Python _tag_groups_filter_sx loop) ;; Data-driven tag groups filter (replaces Python _tag_groups_filter_sx loop)

View File

@@ -7,7 +7,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Image card ;; Image card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-image (&key src alt caption width href) (defcomp ~kg-image (&key (src :as string) (alt :as string?) (caption :as string?) (width :as string?) (href :as string?))
(figure :class (str "kg-card kg-image-card" (figure :class (str "kg-card kg-image-card"
(if (= width "wide") " kg-width-wide" (if (= width "wide") " kg-width-wide"
(if (= width "full") " kg-width-full" ""))) (if (= width "full") " kg-width-full" "")))
@@ -19,7 +19,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Gallery card ;; Gallery card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-gallery (&key images caption) (defcomp ~kg-gallery (&key (images :as list) (caption :as string?))
(figure :class "kg-card kg-gallery-card kg-width-wide" (figure :class "kg-card kg-gallery-card kg-width-wide"
(div :class "kg-gallery-container" (div :class "kg-gallery-container"
(map (lambda (row) (map (lambda (row)
@@ -48,7 +48,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Embed card ;; Embed card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-embed (&key html caption) (defcomp ~kg-embed (&key (html :as string) (caption :as string?))
(figure :class "kg-card kg-embed-card" (figure :class "kg-card kg-embed-card"
(~rich-text :html html) (~rich-text :html html)
(when caption (figcaption caption)))) (when caption (figcaption caption))))
@@ -56,7 +56,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Bookmark card ;; Bookmark card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-bookmark (&key url title description icon author publisher thumbnail caption) (defcomp ~kg-bookmark (&key (url :as string) (title :as string?) (description :as string?) (icon :as string?) (author :as string?) (publisher :as string?) (thumbnail :as string?) (caption :as string?))
(figure :class "kg-card kg-bookmark-card" (figure :class "kg-card kg-bookmark-card"
(a :class "kg-bookmark-container" :href url (a :class "kg-bookmark-container" :href url
(div :class "kg-bookmark-content" (div :class "kg-bookmark-content"
@@ -75,7 +75,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Callout card ;; Callout card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-callout (&key color emoji content) (defcomp ~kg-callout (&key (color :as string?) (emoji :as string?) (content :as string?))
(div :class (str "kg-card kg-callout-card kg-callout-card-" (or color "grey")) (div :class (str "kg-card kg-callout-card kg-callout-card-" (or color "grey"))
(when emoji (div :class "kg-callout-emoji" emoji)) (when emoji (div :class "kg-callout-emoji" emoji))
(div :class "kg-callout-text" (or content "")))) (div :class "kg-callout-text" (or content ""))))
@@ -83,14 +83,14 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Button card ;; Button card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-button (&key url text alignment) (defcomp ~kg-button (&key (url :as string) (text :as string?) (alignment :as string?))
(div :class (str "kg-card kg-button-card kg-align-" (or alignment "center")) (div :class (str "kg-card kg-button-card kg-align-" (or alignment "center"))
(a :href url :class "kg-btn kg-btn-accent" (or text "")))) (a :href url :class "kg-btn kg-btn-accent" (or text ""))))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Toggle card (accordion) ;; Toggle card (accordion)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-toggle (&key heading content) (defcomp ~kg-toggle (&key (heading :as string?) (content :as string?))
(div :class "kg-card kg-toggle-card" :data-kg-toggle-state "close" (div :class "kg-card kg-toggle-card" :data-kg-toggle-state "close"
(div :class "kg-toggle-heading" (div :class "kg-toggle-heading"
(h4 :class "kg-toggle-heading-text" (or heading "")) (h4 :class "kg-toggle-heading-text" (or heading ""))
@@ -101,7 +101,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Audio card ;; Audio card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-audio (&key src title duration thumbnail) (defcomp ~kg-audio (&key (src :as string) (title :as string?) (duration :as string?) (thumbnail :as string?))
(div :class "kg-card kg-audio-card" (div :class "kg-card kg-audio-card"
(if thumbnail (if thumbnail
(img :src thumbnail :alt "audio-thumbnail" :class "kg-audio-thumbnail") (img :src thumbnail :alt "audio-thumbnail" :class "kg-audio-thumbnail")
@@ -124,7 +124,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Video card ;; Video card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-video (&key src caption width thumbnail loop) (defcomp ~kg-video (&key (src :as string) (caption :as string?) (width :as string?) (thumbnail :as string?) (loop :as boolean?))
(figure :class (str "kg-card kg-video-card" (figure :class (str "kg-card kg-video-card"
(if (= width "wide") " kg-width-wide" (if (= width "wide") " kg-width-wide"
(if (= width "full") " kg-width-full" ""))) (if (= width "full") " kg-width-full" "")))
@@ -136,7 +136,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; File card ;; File card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-file (&key src filename title filesize caption) (defcomp ~kg-file (&key (src :as string) (filename :as string?) (title :as string?) (filesize :as string?) (caption :as string?))
(div :class "kg-card kg-file-card" (div :class "kg-card kg-file-card"
(a :class "kg-file-card-container" :href src :download (or filename "") (a :class "kg-file-card-container" :href src :download (or filename "")
(div :class "kg-file-card-contents" (div :class "kg-file-card-contents"

View File

@@ -1,6 +1,6 @@
;; Blog settings panel components (features, markets, associated entries) ;; Blog settings panel components (features, markets, associated entries)
(defcomp ~blog-features-form (&key features-url calendar-checked market-checked hs-trigger) (defcomp ~blog-features-form (&key (features-url :as string) (calendar-checked :as boolean) (market-checked :as boolean) (hs-trigger :as string))
(form :sx-put features-url :sx-target "#features-panel" :sx-swap "outerHTML" (form :sx-put features-url :sx-target "#features-panel" :sx-swap "outerHTML"
:sx-headers {:Content-Type "application/json"} :sx-encoding "json" :class "space-y-3" :sx-headers {:Content-Type "application/json"} :sx-encoding "json" :class "space-y-3"
(label :class "flex items-center gap-3 cursor-pointer" (label :class "flex items-center gap-3 cursor-pointer"
@@ -31,7 +31,7 @@
;; Markets panel ;; Markets panel
(defcomp ~blog-market-item (&key name slug delete-url confirm-text) (defcomp ~blog-market-item (&key (name :as string) (slug :as string) (delete-url :as string) (confirm-text :as string))
(li :class "flex items-center justify-between p-3 bg-stone-50 rounded" (li :class "flex items-center justify-between p-3 bg-stone-50 rounded"
(div (span :class "font-medium" name) (div (span :class "font-medium" name)
(span :class "text-stone-400 text-sm ml-2" (str "/" slug "/"))) (span :class "text-stone-400 text-sm ml-2" (str "/" slug "/")))
@@ -93,11 +93,11 @@
;; Associated entries ;; Associated entries
(defcomp ~blog-entry-image (&key src title) (defcomp ~blog-entry-image (&key (src :as string?) (title :as string))
(if src (img :src src :alt title :class "w-8 h-8 rounded-full object-cover flex-shrink-0") (if src (img :src src :alt title :class "w-8 h-8 rounded-full object-cover flex-shrink-0")
(div :class "w-8 h-8 rounded-full bg-stone-200 flex-shrink-0"))) (div :class "w-8 h-8 rounded-full bg-stone-200 flex-shrink-0")))
(defcomp ~blog-associated-entry (&key confirm-text toggle-url hx-headers img name date-str) (defcomp ~blog-associated-entry (&key (confirm-text :as string) (toggle-url :as string) hx-headers img (name :as string) (date-str :as string))
(button :type "button" (button :type "button"
:class "w-full text-left p-3 rounded border bg-green-50 border-green-300 transition hover:bg-green-100" :class "w-full text-left p-3 rounded border bg-green-50 border-green-300 transition hover:bg-green-100"
:data-confirm "" :data-confirm-title "Remove entry?" :data-confirm "" :data-confirm-title "Remove entry?"
@@ -150,7 +150,7 @@
;; Entries browser composition — replaces _h_post_entries_content ;; Entries browser composition — replaces _h_post_entries_content
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-calendar-browser-item (&key name title image view-url) (defcomp ~blog-calendar-browser-item (&key (name :as string) (title :as string) (image :as string?) (view-url :as string))
(details :class "border rounded-lg bg-white" :data-toggle-group "calendar-browser" (details :class "border rounded-lg bg-white" :data-toggle-group "calendar-browser"
(summary :class "p-4 cursor-pointer hover:bg-stone-50 flex items-center gap-3" (summary :class "p-4 cursor-pointer hover:bg-stone-50 flex items-center gap-3"
(if image (if image
@@ -182,11 +182,11 @@
;; Post settings form composition — replaces _h_post_settings_content ;; Post settings form composition — replaces _h_post_settings_content
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-settings-field-label (&key text field-for) (defcomp ~blog-settings-field-label (&key (text :as string) (field-for :as string))
(label :for field-for (label :for field-for
:class "block text-[13px] font-medium text-stone-500 mb-[4px]" text)) :class "block text-[13px] font-medium text-stone-500 mb-[4px]" text))
(defcomp ~blog-settings-section (&key title content is-open) (defcomp ~blog-settings-section (&key (title :as string) content (is-open :as boolean))
(details :class "border border-stone-200 rounded-[8px] overflow-hidden" :open is-open (details :class "border border-stone-200 rounded-[8px] overflow-hidden" :open is-open
(summary :class "px-[16px] py-[10px] bg-stone-50 text-[14px] font-medium text-stone-600 cursor-pointer select-none hover:bg-stone-100 transition-colors" (summary :class "px-[16px] py-[10px] bg-stone-50 text-[14px] font-medium text-stone-600 cursor-pointer select-none hover:bg-stone-100 transition-colors"
title) title)

View File

@@ -1,6 +1,6 @@
;; Cart calendar entry components ;; Cart calendar entry components
(defcomp ~cart-cal-entry (&key name date-str cost) (defcomp ~cart-cal-entry (&key (name :as string) (date-str :as string) (cost :as string))
(li :class "flex items-start justify-between text-sm" (li :class "flex items-start justify-between text-sm"
(div (div :class "font-medium" name) (div (div :class "font-medium" name)
(div :class "text-xs text-stone-500" date-str)) (div :class "text-xs text-stone-500" date-str))

View File

@@ -1,12 +1,12 @@
;; Cart item components ;; Cart item components
(defcomp ~cart-item-img (&key src alt) (defcomp ~cart-item-img (&key (src :as string) (alt :as string))
(img :src src :alt alt :class "w-24 h-24 sm:w-32 sm:h-28 object-cover rounded-xl border border-stone-100" :loading "lazy")) (img :src src :alt alt :class "w-24 h-24 sm:w-32 sm:h-28 object-cover rounded-xl border border-stone-100" :loading "lazy"))
(defcomp ~cart-item-price (&key text) (defcomp ~cart-item-price (&key (text :as string))
(p :class "text-sm sm:text-base font-semibold text-stone-900" text)) (p :class "text-sm sm:text-base font-semibold text-stone-900" text))
(defcomp ~cart-item-price-was (&key text) (defcomp ~cart-item-price-was (&key (text :as string))
(p :class "text-xs text-stone-400 line-through" text)) (p :class "text-xs text-stone-400 line-through" text))
(defcomp ~cart-item-no-price () (defcomp ~cart-item-no-price ()
@@ -17,13 +17,13 @@
(i :class "fa-solid fa-triangle-exclamation text-[0.6rem]" :aria-hidden "true") (i :class "fa-solid fa-triangle-exclamation text-[0.6rem]" :aria-hidden "true")
" This item is no longer available or price has changed")) " This item is no longer available or price has changed"))
(defcomp ~cart-item-brand (&key brand) (defcomp ~cart-item-brand (&key (brand :as string))
(p :class "mt-0.5 text-[0.7rem] sm:text-xs text-stone-500" brand)) (p :class "mt-0.5 text-[0.7rem] sm:text-xs text-stone-500" brand))
(defcomp ~cart-item-line-total (&key text) (defcomp ~cart-item-line-total (&key (text :as string))
(p :class "text-sm sm:text-base font-semibold text-stone-900" text)) (p :class "text-sm sm:text-base font-semibold text-stone-900" text))
(defcomp ~cart-item (&key id img prod-url title brand deleted price qty-url csrf minus qty plus line-total) (defcomp ~cart-item (&key (id :as string) img (prod-url :as string) (title :as string) brand deleted price (qty-url :as string) (csrf :as string) (minus :as string) (qty :as string) (plus :as string) line-total)
(article :id id :class "flex flex-col sm:flex-row gap-3 sm:gap-4 rounded-2xl bg-white shadow-sm border border-stone-200 p-3 sm:p-4 md:p-5" (article :id id :class "flex flex-col sm:flex-row gap-3 sm:gap-4 rounded-2xl bg-white shadow-sm border border-stone-200 p-3 sm:p-4 md:p-5"
(div :class "w-full sm:w-32 shrink-0 flex justify-center sm:block" (when img img)) (div :class "w-full sm:w-32 shrink-0 flex justify-center sm:block" (when img img))
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
@@ -54,7 +54,7 @@
summary)))) summary))))
;; Assembled cart item from serialized data — replaces Python _cart_item_sx ;; Assembled cart item from serialized data — replaces Python _cart_item_sx
(defcomp ~cart-item-from-data (&key item) (defcomp ~cart-item-from-data (&key (item :as dict))
(let* ((slug (or (get item "slug") "")) (let* ((slug (or (get item "slug") ""))
(title (or (get item "title") "")) (title (or (get item "title") ""))
(image (get item "image")) (image (get item "image"))
@@ -96,7 +96,7 @@
(~cart-item-line-total :text (str "Line total: " symbol (format-decimal line-total 2))))))) (~cart-item-line-total :text (str "Line total: " symbol (format-decimal line-total 2)))))))
;; Assembled calendar entries section — replaces Python _calendar_entries_sx ;; Assembled calendar entries section — replaces Python _calendar_entries_sx
(defcomp ~cart-cal-section-from-data (&key entries) (defcomp ~cart-cal-section-from-data (&key (entries :as list))
(when (not (empty? entries)) (when (not (empty? entries))
(~cart-cal-section (~cart-cal-section
:items (map (lambda (e) :items (map (lambda (e)
@@ -108,7 +108,7 @@
entries)))) entries))))
;; Assembled ticket groups section — replaces Python _ticket_groups_sx ;; Assembled ticket groups section — replaces Python _ticket_groups_sx
(defcomp ~cart-tickets-section-from-data (&key ticket-groups) (defcomp ~cart-tickets-section-from-data (&key (ticket-groups :as list))
(when (not (empty? ticket-groups)) (when (not (empty? ticket-groups))
(let* ((csrf (csrf-token)) (let* ((csrf (csrf-token))
(qty-url (url-for "cart_global.update_ticket_quantity"))) (qty-url (url-for "cart_global.update_ticket_quantity")))
@@ -137,7 +137,7 @@
ticket-groups))))) ticket-groups)))))
;; Assembled cart summary — replaces Python _cart_summary_sx ;; Assembled cart summary — replaces Python _cart_summary_sx
(defcomp ~cart-summary-from-data (&key item-count grand-total symbol is-logged-in checkout-action login-href user-email) (defcomp ~cart-summary-from-data (&key (item-count :as number) (grand-total :as number) (symbol :as string) (is-logged-in :as boolean) (checkout-action :as string) (login-href :as string) (user-email :as string?))
(~cart-summary-panel (~cart-summary-panel
:item-count (str item-count) :item-count (str item-count)
:subtotal (str symbol (format-decimal grand-total 2)) :subtotal (str symbol (format-decimal grand-total 2))
@@ -148,7 +148,7 @@
(~cart-checkout-signin :href login-href)))) (~cart-checkout-signin :href login-href))))
;; Assembled page cart content — replaces Python _page_cart_main_panel_sx ;; Assembled page cart content — replaces Python _page_cart_main_panel_sx
(defcomp ~cart-page-cart-content (&key cart-items cal-entries ticket-groups summary) (defcomp ~cart-page-cart-content (&key (cart-items :as list?) (cal-entries :as list?) (ticket-groups :as list?) summary)
(if (and (empty? (or cart-items (list))) (if (and (empty? (or cart-items (list)))
(empty? (or cal-entries (list))) (empty? (or cal-entries (list)))
(empty? (or ticket-groups (list)))) (empty? (or ticket-groups (list))))

View File

@@ -1,6 +1,6 @@
;; Cart overview components ;; Cart overview components
(defcomp ~cart-badge (&key icon text) (defcomp ~cart-badge (&key (icon :as string) (text :as string))
(span :class "inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-stone-100" (span :class "inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-stone-100"
(i :class icon :aria-hidden "true") text)) (i :class icon :aria-hidden "true") text))
@@ -8,13 +8,13 @@
(div :class "mt-1 flex flex-wrap gap-2 text-xs text-stone-600" (div :class "mt-1 flex flex-wrap gap-2 text-xs text-stone-600"
badges)) badges))
(defcomp ~cart-group-card-img (&key src alt) (defcomp ~cart-group-card-img (&key (src :as string) (alt :as string))
(img :src src :alt alt :class "h-16 w-16 rounded-xl object-cover border border-stone-200 flex-shrink-0")) (img :src src :alt alt :class "h-16 w-16 rounded-xl object-cover border border-stone-200 flex-shrink-0"))
(defcomp ~cart-mp-subtitle (&key title) (defcomp ~cart-mp-subtitle (&key (title :as string))
(p :class "text-xs text-stone-500 truncate" title)) (p :class "text-xs text-stone-500 truncate" title))
(defcomp ~cart-group-card (&key href img display-title subtitle badges total) (defcomp ~cart-group-card (&key (href :as string) img (display-title :as string) subtitle badges (total :as string))
(a :href href :class "block rounded-2xl border border-stone-200 bg-white shadow-sm hover:shadow-md hover:border-stone-300 transition p-4 sm:p-5" (a :href href :class "block rounded-2xl border border-stone-200 bg-white shadow-sm hover:shadow-md hover:border-stone-300 transition p-4 sm:p-5"
(div :class "flex items-start gap-4" (div :class "flex items-start gap-4"
img img
@@ -25,7 +25,7 @@
(div :class "text-lg font-bold text-stone-900" total) (div :class "text-lg font-bold text-stone-900" total)
(div :class "mt-1 text-xs text-emerald-700 font-medium" "View cart \u2192"))))) (div :class "mt-1 text-xs text-emerald-700 font-medium" "View cart \u2192")))))
(defcomp ~cart-orphan-card (&key badges total) (defcomp ~cart-orphan-card (&key badges (total :as string))
(div :class "rounded-2xl border border-dashed border-amber-300 bg-amber-50/60 p-4 sm:p-5" (div :class "rounded-2xl border border-dashed border-amber-300 bg-amber-50/60 p-4 sm:p-5"
(div :class "flex items-start gap-4" (div :class "flex items-start gap-4"
(div :class "h-16 w-16 rounded-xl bg-amber-100 flex items-center justify-center flex-shrink-0" (div :class "h-16 w-16 rounded-xl bg-amber-100 flex items-center justify-center flex-shrink-0"
@@ -46,7 +46,7 @@
(~empty-state :icon "fa fa-shopping-cart" :message "Your cart is empty" :cls "text-center")))) (~empty-state :icon "fa fa-shopping-cart" :message "Your cart is empty" :cls "text-center"))))
;; Assembled page group card — replaces Python _page_group_card_sx ;; Assembled page group card — replaces Python _page_group_card_sx
(defcomp ~cart-page-group-card-from-data (&key grp cart-url-base) (defcomp ~cart-page-group-card-from-data (&key (grp :as dict) (cart-url-base :as string))
(let* ((post (get grp "post")) (let* ((post (get grp "post"))
(product-count (or (get grp "product_count") 0)) (product-count (or (get grp "product_count") 0))
(calendar-count (or (get grp "calendar_count") 0)) (calendar-count (or (get grp "calendar_count") 0))
@@ -85,7 +85,7 @@
:total (str "\u00a3" (format-decimal total 2)))))) :total (str "\u00a3" (format-decimal total 2))))))
;; Assembled cart overview content — replaces Python _overview_main_panel_sx ;; Assembled cart overview content — replaces Python _overview_main_panel_sx
(defcomp ~cart-overview-content (&key page-groups cart-url-base) (defcomp ~cart-overview-content (&key (page-groups :as list) (cart-url-base :as string))
(if (empty? page-groups) (if (empty? page-groups)
(~cart-empty) (~cart-empty)
(~cart-overview-panel (~cart-overview-panel

View File

@@ -1,17 +1,17 @@
;; Cart summary / checkout components ;; Cart summary / checkout components
(defcomp ~cart-checkout-form (&key action csrf label) (defcomp ~cart-checkout-form (&key (action :as string) (csrf :as string) (label :as string))
(form :method "post" :action action :class "w-full" (form :method "post" :action action :class "w-full"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(button :type "submit" :class "w-full inline-flex items-center justify-center px-4 py-2 text-xs sm:text-sm rounded-full border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 transition" (button :type "submit" :class "w-full inline-flex items-center justify-center px-4 py-2 text-xs sm:text-sm rounded-full border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 transition"
(i :class "fa-solid fa-credit-card mr-2" :aria-hidden "true") label))) (i :class "fa-solid fa-credit-card mr-2" :aria-hidden "true") label)))
(defcomp ~cart-checkout-signin (&key href) (defcomp ~cart-checkout-signin (&key (href :as string))
(div :class "w-full flex" (div :class "w-full flex"
(a :href href :class "w-full cursor-pointer flex flex-row items-center justify-center p-3 gap-2 rounded bg-stone-200 text-black hover:bg-stone-300 transition" (a :href href :class "w-full cursor-pointer flex flex-row items-center justify-center p-3 gap-2 rounded bg-stone-200 text-black hover:bg-stone-300 transition"
(i :class "fa-solid fa-key") (span "sign in or register to checkout")))) (i :class "fa-solid fa-key") (span "sign in or register to checkout"))))
(defcomp ~cart-summary-panel (&key item-count subtotal checkout) (defcomp ~cart-summary-panel (&key (item-count :as string) (subtotal :as string) checkout)
(aside :id "cart-summary" :class "lg:pl-2" (aside :id "cart-summary" :class "lg:pl-2"
(div :class "rounded-2xl bg-white shadow-sm border border-stone-200 p-4 sm:p-5" (div :class "rounded-2xl bg-white shadow-sm border border-stone-200 p-4 sm:p-5"
(h2 :class "text-sm sm:text-base font-semibold text-stone-900 mb-3 sm:mb-4" "Order summary") (h2 :class "text-sm sm:text-base font-semibold text-stone-900 mb-3 sm:mb-4" "Order summary")

View File

@@ -1,12 +1,12 @@
;; Cart ticket components ;; Cart ticket components
(defcomp ~cart-ticket-type-name (&key name) (defcomp ~cart-ticket-type-name (&key (name :as string))
(p :class "mt-0.5 text-[0.7rem] sm:text-xs text-stone-500" name)) (p :class "mt-0.5 text-[0.7rem] sm:text-xs text-stone-500" name))
(defcomp ~cart-ticket-type-hidden (&key value) (defcomp ~cart-ticket-type-hidden (&key (value :as string))
(input :type "hidden" :name "ticket_type_id" :value value)) (input :type "hidden" :name "ticket_type_id" :value value))
(defcomp ~cart-ticket-article (&key name type-name date-str price qty-url csrf entry-id type-hidden minus qty plus line-total) (defcomp ~cart-ticket-article (&key (name :as string) type-name (date-str :as string) (price :as string) (qty-url :as string) (csrf :as string) (entry-id :as string) type-hidden (minus :as string) (qty :as string) (plus :as string) (line-total :as string))
(article :class "flex flex-col sm:flex-row gap-3 sm:gap-4 rounded-2xl bg-white shadow-sm border border-stone-200 p-3 sm:p-4" (article :class "flex flex-col sm:flex-row gap-3 sm:gap-4 rounded-2xl bg-white shadow-sm border border-stone-200 p-3 sm:p-4"
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
(div :class "flex flex-col sm:flex-row sm:items-start justify-between gap-2 sm:gap-3" (div :class "flex flex-col sm:flex-row sm:items-start justify-between gap-2 sm:gap-3"

View File

@@ -1,28 +1,28 @@
;; Events calendar components ;; Events calendar components
(defcomp ~events-calendar-nav-arrow (&key pill-cls href label) (defcomp ~events-calendar-nav-arrow (&key (pill-cls :as string) (href :as string) (label :as string))
(a :class (str pill-cls " text-xl") :href href (a :class (str pill-cls " text-xl") :href href
:sx-get href :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" label)) :sx-get href :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" label))
(defcomp ~events-calendar-month-label (&key month-name year) (defcomp ~events-calendar-month-label (&key (month-name :as string) (year :as string))
(div :class "px-3 font-medium" (str month-name " " year))) (div :class "px-3 font-medium" (str month-name " " year)))
(defcomp ~events-calendar-weekday (&key name) (defcomp ~events-calendar-weekday (&key (name :as string))
(div :class "py-1" name)) (div :class "py-1" name))
(defcomp ~events-calendar-day-short (&key day-str) (defcomp ~events-calendar-day-short (&key (day-str :as string))
(span :class "sm:hidden text-[16px] text-stone-500" day-str)) (span :class "sm:hidden text-[16px] text-stone-500" day-str))
(defcomp ~events-calendar-day-num (&key pill-cls href num) (defcomp ~events-calendar-day-num (&key (pill-cls :as string) (href :as string) (num :as string))
(a :class pill-cls :href href :sx-get href :sx-target "#main-panel" :sx-select "#main-panel" (a :class pill-cls :href href :sx-get href :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true" num)) :sx-swap "outerHTML" :sx-push-url "true" num))
(defcomp ~events-calendar-entry-badge (&key bg-cls name state-label) (defcomp ~events-calendar-entry-badge (&key (bg-cls :as string) (name :as string) (state-label :as string))
(div :class (str "flex items-center justify-between gap-1 text-[11px] rounded px-1 py-0.5 " bg-cls) (div :class (str "flex items-center justify-between gap-1 text-[11px] rounded px-1 py-0.5 " bg-cls)
(span :class "truncate" name) (span :class "truncate" name)
(span :class "shrink-0 text-[10px] font-semibold uppercase tracking-tight" state-label))) (span :class "shrink-0 text-[10px] font-semibold uppercase tracking-tight" state-label)))
(defcomp ~events-calendar-cell (&key cell-cls day-short day-num badges) (defcomp ~events-calendar-cell (&key (cell-cls :as string) day-short day-num badges)
(div :class cell-cls (div :class cell-cls
(div :class "flex justify-between items-center" (div :class "flex justify-between items-center"
(div :class "flex flex-col" day-short day-num)) (div :class "flex flex-col" day-short day-num))
@@ -37,10 +37,10 @@
(div :class "grid grid-cols-1 sm:grid-cols-7 gap-px bg-stone-200 rounded-xl overflow-hidden" cells)))) (div :class "grid grid-cols-1 sm:grid-cols-7 gap-px bg-stone-200 rounded-xl overflow-hidden" cells))))
;; Calendar grid from data — all iteration in sx ;; Calendar grid from data — all iteration in sx
(defcomp ~events-calendar-grid-from-data (&key pill-cls month-name year (defcomp ~events-calendar-grid-from-data (&key (pill-cls :as string) (month-name :as string) (year :as string)
prev-year-href prev-month-href (prev-year-href :as string) (prev-month-href :as string)
next-month-href next-year-href (next-month-href :as string) (next-year-href :as string)
weekday-names cells) (weekday-names :as list) (cells :as list))
(~events-calendar-grid (~events-calendar-grid
:arrows (<> :arrows (<>
(~events-calendar-nav-arrow :pill-cls pill-cls :href prev-year-href :label "\u00ab") (~events-calendar-nav-arrow :pill-cls pill-cls :href prev-year-href :label "\u00ab")
@@ -66,7 +66,7 @@
(get cell "badges")))))) (get cell "badges"))))))
(or cells (list)))))) (or cells (list))))))
(defcomp ~events-calendar-description-display (&key description edit-url) (defcomp ~events-calendar-description-display (&key (description :as string?) (edit-url :as string))
(div :id "calendar-description" (div :id "calendar-description"
(if description (if description
(p :class "text-stone-700 whitespace-pre-line break-all" description) (p :class "text-stone-700 whitespace-pre-line break-all" description)
@@ -75,12 +75,12 @@
:sx-get edit-url :sx-target "#calendar-description" :sx-swap "outerHTML" :sx-get edit-url :sx-target "#calendar-description" :sx-swap "outerHTML"
(i :class "fas fa-edit")))) (i :class "fas fa-edit"))))
(defcomp ~events-calendar-description-title-oob (&key description) (defcomp ~events-calendar-description-title-oob (&key (description :as string))
(div :id "calendar-description-title" :sx-swap-oob "outerHTML" (div :id "calendar-description-title" :sx-swap-oob "outerHTML"
:class "text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block" :class "text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block"
description)) description))
(defcomp ~events-calendar-description-edit-form (&key save-url cancel-url csrf description) (defcomp ~events-calendar-description-edit-form (&key (save-url :as string) (cancel-url :as string) (csrf :as string) (description :as string?))
(div :id "calendar-description" (div :id "calendar-description"
(form :sx-post save-url :sx-target "#calendar-description" :sx-swap "outerHTML" (form :sx-post save-url :sx-target "#calendar-description" :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)

View File

@@ -1,6 +1,6 @@
;; Events day components ;; Events day components
(defcomp ~events-day-entry-link (&key href name time-str) (defcomp ~events-day-entry-link (&key (href :as string) (name :as string) (time-str :as string))
(a :href href :class "flex items-center gap-2 px-3 py-2 hover:bg-stone-100 rounded transition text-sm border sm:whitespace-nowrap sm:flex-shrink-0" (a :href href :class "flex items-center gap-2 px-3 py-2 hover:bg-stone-100 rounded transition text-sm border sm:whitespace-nowrap sm:flex-shrink-0"
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
(div :class "font-medium truncate" name) (div :class "font-medium truncate" name)
@@ -12,7 +12,7 @@
(div :class "flex overflow-x-auto gap-1 scrollbar-thin" (div :class "flex overflow-x-auto gap-1 scrollbar-thin"
inner))) inner)))
(defcomp ~events-day-table (&key list-container rows pre-action add-url) (defcomp ~events-day-table (&key (list-container :as string) rows (pre-action :as string) (add-url :as string))
(section :id "day-entries" :class list-container (section :id "day-entries" :class list-container
(table :class "w-full text-sm border table-fixed" (table :class "w-full text-sm border table-fixed"
(thead :class "bg-stone-100" (thead :class "bg-stone-100"
@@ -32,27 +32,27 @@
(defcomp ~events-day-empty-row () (defcomp ~events-day-empty-row ()
(tr (td :colspan "6" :class "p-3 text-stone-500" "No entries yet."))) (tr (td :colspan "6" :class "p-3 text-stone-500" "No entries yet.")))
(defcomp ~events-day-row-name (&key href pill-cls name) (defcomp ~events-day-row-name (&key (href :as string) (pill-cls :as string) (name :as string))
(td :class "p-2 align-top w-2/6" (div :class "font-medium" (td :class "p-2 align-top w-2/6" (div :class "font-medium"
(a :href href :class pill-cls :sx-get href :sx-target "#main-panel" :sx-select "#main-panel" (a :href href :class pill-cls :sx-get href :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true" name)))) :sx-swap "outerHTML" :sx-push-url "true" name))))
(defcomp ~events-day-row-slot (&key href pill-cls slot-name time-str) (defcomp ~events-day-row-slot (&key (href :as string) (pill-cls :as string) (slot-name :as string) (time-str :as string))
(td :class "p-2 align-top w-1/6" (div :class "text-xs font-medium" (td :class "p-2 align-top w-1/6" (div :class "text-xs font-medium"
(a :href href :class pill-cls :sx-get href :sx-target "#main-panel" :sx-select "#main-panel" (a :href href :class pill-cls :sx-get href :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true" slot-name) :sx-swap "outerHTML" :sx-push-url "true" slot-name)
(span :class "text-stone-600 font-normal" time-str)))) (span :class "text-stone-600 font-normal" time-str))))
(defcomp ~events-day-row-time (&key start end) (defcomp ~events-day-row-time (&key (start :as string) (end :as string))
(td :class "p-2 align-top w-1/6" (div :class "text-xs text-stone-600" (str start end)))) (td :class "p-2 align-top w-1/6" (div :class "text-xs text-stone-600" (str start end))))
(defcomp ~events-day-row-state (&key state-id badge) (defcomp ~events-day-row-state (&key (state-id :as string) badge)
(td :class "p-2 align-top w-1/6" (div :id state-id badge))) (td :class "p-2 align-top w-1/6" (div :id state-id badge)))
(defcomp ~events-day-row-cost (&key cost-str) (defcomp ~events-day-row-cost (&key (cost-str :as string))
(td :class "p-2 align-top w-1/6" (span :class "font-medium text-green-600" cost-str))) (td :class "p-2 align-top w-1/6" (span :class "font-medium text-green-600" cost-str)))
(defcomp ~events-day-row-tickets (&key price-str count-str) (defcomp ~events-day-row-tickets (&key (price-str :as string) (count-str :as string))
(td :class "p-2 align-top w-1/6" (div :class "text-xs space-y-1" (td :class "p-2 align-top w-1/6" (div :class "text-xs space-y-1"
(div :class "font-medium text-green-600" price-str) (div :class "font-medium text-green-600" price-str)
(div :class "text-stone-600" count-str)))) (div :class "text-stone-600" count-str))))
@@ -63,7 +63,7 @@
(defcomp ~events-day-row-actions () (defcomp ~events-day-row-actions ()
(td :class "p-2 align-top w-1/6")) (td :class "p-2 align-top w-1/6"))
(defcomp ~events-day-row (&key tr-cls name slot state cost tickets actions) (defcomp ~events-day-row (&key (tr-cls :as string) name slot state cost tickets actions)
(tr :class tr-cls name slot state cost tickets actions)) (tr :class tr-cls name slot state cost tickets actions))
(defcomp ~events-day-admin-panel () (defcomp ~events-day-admin-panel ()
@@ -77,14 +77,14 @@
:id "day-entries-nav-wrapper" :sx-swap-oob "true" :id "day-entries-nav-wrapper" :sx-swap-oob "true"
(div :class "flex overflow-x-auto gap-1 scrollbar-thin" items))) (div :class "flex overflow-x-auto gap-1 scrollbar-thin" items)))
(defcomp ~events-day-nav-entry (&key href nav-btn name time-str) (defcomp ~events-day-nav-entry (&key (href :as string) (nav-btn :as string) (name :as string) (time-str :as string))
(a :href href :class nav-btn (a :href href :class nav-btn
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
(div :class "font-medium truncate" name) (div :class "font-medium truncate" name)
(div :class "text-xs text-stone-600 truncate" time-str)))) (div :class "text-xs text-stone-600 truncate" time-str))))
;; Day table from data — all row iteration in sx ;; Day table from data — all row iteration in sx
(defcomp ~events-day-table-from-data (&key list-container pre-action add-url tr-cls pill-cls rows) (defcomp ~events-day-table-from-data (&key (list-container :as string) (pre-action :as string) (add-url :as string) (tr-cls :as string) (pill-cls :as string) (rows :as list?))
(~events-day-table (~events-day-table
:list-container list-container :list-container list-container
:rows (if (empty? (or rows (list))) :rows (if (empty? (or rows (list)))
@@ -112,7 +112,7 @@
:pre-action pre-action :add-url add-url)) :pre-action pre-action :add-url add-url))
;; Day entries nav OOB from data ;; Day entries nav OOB from data
(defcomp ~events-day-entries-nav-oob-from-data (&key nav-btn entries) (defcomp ~events-day-entries-nav-oob-from-data (&key (nav-btn :as string) (entries :as list?))
(if (empty? (or entries (list))) (if (empty? (or entries (list)))
(~events-day-entries-nav-oob-empty) (~events-day-entries-nav-oob-empty)
(~events-day-entries-nav-oob (~events-day-entries-nav-oob

View File

@@ -1,6 +1,6 @@
;; Events ticket components ;; Events ticket components
(defcomp ~events-ticket-card (&key href entry-name type-name time-str cal-name badge code-prefix) (defcomp ~events-ticket-card (&key (href :as string) (entry-name :as string) (type-name :as string?) (time-str :as string?) (cal-name :as string?) badge (code-prefix :as string))
(a :href href :class "block rounded-xl border border-stone-200 bg-white p-4 hover:shadow-md transition" (a :href href :class "block rounded-xl border border-stone-200 bg-white p-4 hover:shadow-md transition"
(div :class "flex items-start justify-between gap-4" (div :class "flex items-start justify-between gap-4"
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
@@ -12,7 +12,7 @@
badge badge
(span :class "text-xs text-stone-400 font-mono" (str code-prefix "...")))))) (span :class "text-xs text-stone-400 font-mono" (str code-prefix "..."))))))
(defcomp ~events-tickets-panel (&key list-container has-tickets cards) (defcomp ~events-tickets-panel (&key (list-container :as string) (has-tickets :as boolean) cards)
(section :id "tickets-list" :class list-container (section :id "tickets-list" :class list-container
(h1 :class "text-2xl font-bold mb-6" "My Tickets") (h1 :class "text-2xl font-bold mb-6" "My Tickets")
(if has-tickets (if has-tickets
@@ -22,9 +22,9 @@
(p :class "text-lg" "No tickets yet") (p :class "text-lg" "No tickets yet")
(p :class "text-sm mt-1" "Tickets will appear here after you purchase them."))))) (p :class "text-sm mt-1" "Tickets will appear here after you purchase them.")))))
(defcomp ~events-ticket-detail (&key list-container back-href header-bg entry-name badge (defcomp ~events-ticket-detail (&key (list-container :as string) (back-href :as string) (header-bg :as string) (entry-name :as string) badge
type-name code time-date time-range cal-name (type-name :as string?) (code :as string) (time-date :as string?) (time-range :as string?) (cal-name :as string?)
type-desc checkin-str qr-script) (type-desc :as string?) (checkin-str :as string?) (qr-script :as string))
(section :id "ticket-detail" :class (str list-container " max-w-lg mx-auto") (section :id "ticket-detail" :class (str list-container " max-w-lg mx-auto")
(a :href back-href :class "inline-flex items-center gap-1 text-sm text-stone-500 hover:text-stone-700 mb-4" (a :href back-href :class "inline-flex items-center gap-1 text-sm text-stone-500 hover:text-stone-700 mb-4"
(i :class "fa fa-arrow-left" :aria-hidden "true") " Back to my tickets") (i :class "fa fa-arrow-left" :aria-hidden "true") " Back to my tickets")
@@ -54,25 +54,25 @@
(script :src "https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js") (script :src "https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js")
(script qr-script))) (script qr-script)))
(defcomp ~events-ticket-admin-stat (&key border bg text-cls label-cls value label) (defcomp ~events-ticket-admin-stat (&key (border :as string) (bg :as string) (text-cls :as string) (label-cls :as string) (value :as string) (label :as string))
(div :class (str "rounded-xl border " border " " bg " p-4 text-center") (div :class (str "rounded-xl border " border " " bg " p-4 text-center")
(div :class (str "text-2xl font-bold " text-cls) value) (div :class (str "text-2xl font-bold " text-cls) value)
(div :class (str "text-xs " label-cls " uppercase tracking-wide") label))) (div :class (str "text-xs " label-cls " uppercase tracking-wide") label)))
(defcomp ~events-ticket-admin-date (&key date-str) (defcomp ~events-ticket-admin-date (&key (date-str :as string))
(div :class "text-xs text-stone-500" date-str)) (div :class "text-xs text-stone-500" date-str))
(defcomp ~events-ticket-admin-checkin-form (&key checkin-url code csrf) (defcomp ~events-ticket-admin-checkin-form (&key (checkin-url :as string) (code :as string) (csrf :as string))
(form :sx-post checkin-url :sx-target (str "#ticket-row-" code) :sx-swap "outerHTML" (form :sx-post checkin-url :sx-target (str "#ticket-row-" code) :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(button :type "submit" :class "px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700 transition" (button :type "submit" :class "px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700 transition"
(i :class "fa fa-check mr-1" :aria-hidden "true") "Check in"))) (i :class "fa fa-check mr-1" :aria-hidden "true") "Check in")))
(defcomp ~events-ticket-admin-checked-in (&key time-str) (defcomp ~events-ticket-admin-checked-in (&key (time-str :as string))
(span :class "text-xs text-blue-600" (span :class "text-xs text-blue-600"
(i :class "fa fa-check-circle" :aria-hidden "true") (str " " time-str))) (i :class "fa fa-check-circle" :aria-hidden "true") (str " " time-str)))
(defcomp ~events-ticket-admin-row (&key code code-short entry-name date type-name badge action) (defcomp ~events-ticket-admin-row (&key (code :as string) (code-short :as string) (entry-name :as string) date (type-name :as string) badge action)
(tr :class "hover:bg-stone-50 transition" :id (str "ticket-row-" code) (tr :class "hover:bg-stone-50 transition" :id (str "ticket-row-" code)
(td :class "px-4 py-3" (span :class "font-mono text-xs" code-short)) (td :class "px-4 py-3" (span :class "font-mono text-xs" code-short))
(td :class "px-4 py-3" (div :class "font-medium" entry-name) date) (td :class "px-4 py-3" (div :class "font-medium" entry-name) date)
@@ -80,7 +80,7 @@
(td :class "px-4 py-3" badge) (td :class "px-4 py-3" badge)
(td :class "px-4 py-3" action))) (td :class "px-4 py-3" action)))
(defcomp ~events-ticket-admin-panel (&key list-container stats lookup-url has-tickets rows) (defcomp ~events-ticket-admin-panel (&key (list-container :as string) stats (lookup-url :as string) (has-tickets :as boolean) rows)
(section :id "ticket-admin" :class list-container (section :id "ticket-admin" :class list-container
(h1 :class "text-2xl font-bold mb-6" "Ticket Admin") (h1 :class "text-2xl font-bold mb-6" "Ticket Admin")
(div :class "grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8" stats) (div :class "grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8" stats)
@@ -113,11 +113,11 @@
(tbody :class "divide-y divide-stone-100" rows)) (tbody :class "divide-y divide-stone-100" rows))
(div :class "px-6 py-8 text-center text-stone-500" "No tickets yet")))))) (div :class "px-6 py-8 text-center text-stone-500" "No tickets yet"))))))
(defcomp ~events-checkin-error (&key message) (defcomp ~events-checkin-error (&key (message :as string))
(div :class "rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-800" (div :class "rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-800"
(i :class "fa fa-exclamation-circle mr-2" :aria-hidden "true") message)) (i :class "fa fa-exclamation-circle mr-2" :aria-hidden "true") message))
(defcomp ~events-checkin-success-row (&key code code-short entry-name date type-name badge time-str) (defcomp ~events-checkin-success-row (&key (code :as string) (code-short :as string) (entry-name :as string) date (type-name :as string) badge (time-str :as string))
(tr :class "bg-blue-50" :id (str "ticket-row-" code) (tr :class "bg-blue-50" :id (str "ticket-row-" code)
(td :class "px-4 py-3" (span :class "font-mono text-xs" code-short)) (td :class "px-4 py-3" (span :class "font-mono text-xs" code-short))
(td :class "px-4 py-3" (div :class "font-medium" entry-name) date) (td :class "px-4 py-3" (div :class "font-medium" entry-name) date)
@@ -127,29 +127,29 @@
(span :class "text-xs text-blue-600" (span :class "text-xs text-blue-600"
(i :class "fa fa-check-circle" :aria-hidden "true") (str " " time-str))))) (i :class "fa fa-check-circle" :aria-hidden "true") (str " " time-str)))))
(defcomp ~events-lookup-error (&key message) (defcomp ~events-lookup-error (&key (message :as string))
(div :class "rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-800" (div :class "rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-800"
(i :class "fa fa-exclamation-circle mr-2" :aria-hidden "true") message)) (i :class "fa fa-exclamation-circle mr-2" :aria-hidden "true") message))
(defcomp ~events-lookup-info (&key entry-name) (defcomp ~events-lookup-info (&key (entry-name :as string))
(div :class "font-semibold text-lg" entry-name)) (div :class "font-semibold text-lg" entry-name))
(defcomp ~events-lookup-type (&key type-name) (defcomp ~events-lookup-type (&key (type-name :as string))
(div :class "text-sm text-stone-600" type-name)) (div :class "text-sm text-stone-600" type-name))
(defcomp ~events-lookup-date (&key date-str) (defcomp ~events-lookup-date (&key (date-str :as string))
(div :class "text-sm text-stone-500 mt-1" date-str)) (div :class "text-sm text-stone-500 mt-1" date-str))
(defcomp ~events-lookup-cal (&key cal-name) (defcomp ~events-lookup-cal (&key (cal-name :as string))
(div :class "text-xs text-stone-400 mt-0.5" cal-name)) (div :class "text-xs text-stone-400 mt-0.5" cal-name))
(defcomp ~events-lookup-status (&key badge code) (defcomp ~events-lookup-status (&key badge (code :as string))
(div :class "mt-2" badge (span :class "text-xs text-stone-400 ml-2 font-mono" code))) (div :class "mt-2" badge (span :class "text-xs text-stone-400 ml-2 font-mono" code)))
(defcomp ~events-lookup-checkin-time (&key date-str) (defcomp ~events-lookup-checkin-time (&key (date-str :as string))
(div :class "text-xs text-blue-600 mt-1" (str "Checked in: " date-str))) (div :class "text-xs text-blue-600 mt-1" (str "Checked in: " date-str)))
(defcomp ~events-lookup-checkin-btn (&key checkin-url code csrf) (defcomp ~events-lookup-checkin-btn (&key (checkin-url :as string) (code :as string) (csrf :as string))
(form :sx-post checkin-url :sx-target (str "#checkin-action-" code) :sx-swap "innerHTML" (form :sx-post checkin-url :sx-target (str "#checkin-action-" code) :sx-swap "innerHTML"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(button :type "submit" (button :type "submit"
@@ -166,20 +166,20 @@
(i :class "fa fa-times-circle text-3xl" :aria-hidden "true") (i :class "fa fa-times-circle text-3xl" :aria-hidden "true")
(div :class "text-sm font-medium mt-1" "Cancelled"))) (div :class "text-sm font-medium mt-1" "Cancelled")))
(defcomp ~events-lookup-card (&key info code action) (defcomp ~events-lookup-card (&key info (code :as string) action)
(div :class "rounded-lg border border-stone-200 bg-stone-50 p-4" (div :class "rounded-lg border border-stone-200 bg-stone-50 p-4"
(div :class "flex items-start justify-between gap-4" (div :class "flex items-start justify-between gap-4"
(div :class "flex-1" info) (div :class "flex-1" info)
(div :id (str "checkin-action-" code) action)))) (div :id (str "checkin-action-" code) action))))
(defcomp ~events-entry-tickets-admin-row (&key code code-short type-name badge action) (defcomp ~events-entry-tickets-admin-row (&key (code :as string) (code-short :as string) (type-name :as string) badge action)
(tr :class "hover:bg-stone-50" :id (str "entry-ticket-row-" code) (tr :class "hover:bg-stone-50" :id (str "entry-ticket-row-" code)
(td :class "px-4 py-2 font-mono text-xs" code-short) (td :class "px-4 py-2 font-mono text-xs" code-short)
(td :class "px-4 py-2" type-name) (td :class "px-4 py-2" type-name)
(td :class "px-4 py-2" badge) (td :class "px-4 py-2" badge)
(td :class "px-4 py-2" action))) (td :class "px-4 py-2" action)))
(defcomp ~events-entry-tickets-admin-checkin (&key checkin-url code csrf) (defcomp ~events-entry-tickets-admin-checkin (&key (checkin-url :as string) (code :as string) (csrf :as string))
(form :sx-post checkin-url :sx-target (str "#entry-ticket-row-" code) :sx-swap "outerHTML" (form :sx-post checkin-url :sx-target (str "#entry-ticket-row-" code) :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(button :type "submit" :class "px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700" (button :type "submit" :class "px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700"
@@ -198,7 +198,7 @@
(defcomp ~events-entry-tickets-admin-empty () (defcomp ~events-entry-tickets-admin-empty ()
(div :class "text-center py-6 text-stone-500 text-sm" "No tickets for this entry")) (div :class "text-center py-6 text-stone-500 text-sm" "No tickets for this entry"))
(defcomp ~events-entry-tickets-admin-panel (&key entry-name count-label body) (defcomp ~events-entry-tickets-admin-panel (&key (entry-name :as string) (count-label :as string) body)
(div :class "space-y-4" (div :class "space-y-4"
(div :class "flex items-center justify-between" (div :class "flex items-center justify-between"
(h3 :class "text-lg font-semibold" (str "Tickets for: " entry-name)) (h3 :class "text-lg font-semibold" (str "Tickets for: " entry-name))
@@ -211,7 +211,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; My tickets panel from data ;; My tickets panel from data
(defcomp ~events-tickets-panel-from-data (&key list-container tickets) (defcomp ~events-tickets-panel-from-data (&key (list-container :as string) (tickets :as list?))
(~events-tickets-panel (~events-tickets-panel
:list-container list-container :list-container list-container
:has-tickets (not (empty? (or tickets (list)))) :has-tickets (not (empty? (or tickets (list))))
@@ -225,9 +225,9 @@
(or tickets (list)))))) (or tickets (list))))))
;; Ticket detail from data — uses lg badge variant ;; Ticket detail from data — uses lg badge variant
(defcomp ~events-ticket-detail-from-data (&key list-container back-href header-bg entry-name (defcomp ~events-ticket-detail-from-data (&key (list-container :as string) (back-href :as string) (header-bg :as string) (entry-name :as string)
state type-name code time-date time-range (state :as string) (type-name :as string?) (code :as string) (time-date :as string?) (time-range :as string?)
cal-name type-desc checkin-str qr-script) (cal-name :as string?) (type-desc :as string?) (checkin-str :as string?) (qr-script :as string))
(~events-ticket-detail (~events-ticket-detail
:list-container list-container :back-href back-href :list-container list-container :back-href back-href
:header-bg header-bg :entry-name entry-name :header-bg header-bg :entry-name entry-name
@@ -238,9 +238,9 @@
:checkin-str checkin-str :qr-script qr-script)) :checkin-str checkin-str :qr-script qr-script))
;; Ticket admin row from data — conditional action column ;; Ticket admin row from data — conditional action column
(defcomp ~events-ticket-admin-row-from-data (&key code code-short entry-name date-str (defcomp ~events-ticket-admin-row-from-data (&key (code :as string) (code-short :as string) (entry-name :as string) (date-str :as string?)
type-name state checkin-url csrf (type-name :as string) (state :as string) (checkin-url :as string) (csrf :as string)
checked-in-time) (checked-in-time :as string?))
(~events-ticket-admin-row (~events-ticket-admin-row
:code code :code-short code-short :code code :code-short code-short
:entry-name entry-name :entry-name entry-name
@@ -256,8 +256,8 @@
(true nil)))) (true nil))))
;; Ticket admin panel from data ;; Ticket admin panel from data
(defcomp ~events-ticket-admin-panel-from-data (&key list-container lookup-url tickets (defcomp ~events-ticket-admin-panel-from-data (&key (list-container :as string) (lookup-url :as string) (tickets :as list?)
total confirmed checked-in reserved) (total :as number?) (confirmed :as number?) (checked-in :as number?) (reserved :as number?))
(~events-ticket-admin-panel (~events-ticket-admin-panel
:list-container list-container :list-container list-container
:stats (<> :stats (<>
@@ -285,7 +285,7 @@
(or tickets (list)))))) (or tickets (list))))))
;; Entry tickets admin from data ;; Entry tickets admin from data
(defcomp ~events-entry-tickets-admin-from-data (&key entry-name count-label tickets csrf) (defcomp ~events-entry-tickets-admin-from-data (&key (entry-name :as string) (count-label :as string) (tickets :as list?) (csrf :as string))
(~events-entry-tickets-admin-panel (~events-entry-tickets-admin-panel
:entry-name entry-name :count-label count-label :entry-name entry-name :count-label count-label
:body (if (empty? (or tickets (list))) :body (if (empty? (or tickets (list)))
@@ -306,7 +306,7 @@
(or tickets (list)))))))) (or tickets (list))))))))
;; Checkin success row from data ;; Checkin success row from data
(defcomp ~events-checkin-success-row-from-data (&key code code-short entry-name date-str type-name time-str) (defcomp ~events-checkin-success-row-from-data (&key (code :as string) (code-short :as string) (entry-name :as string) (date-str :as string?) (type-name :as string) (time-str :as string))
(~events-checkin-success-row (~events-checkin-success-row
:code code :code-short code-short :code code :code-short code-short
:entry-name entry-name :entry-name entry-name
@@ -316,8 +316,8 @@
:time-str time-str)) :time-str time-str))
;; Ticket types table from data ;; Ticket types table from data
(defcomp ~events-ticket-types-table-from-data (&key list-container ticket-types action-btn add-url (defcomp ~events-ticket-types-table-from-data (&key (list-container :as string) (ticket-types :as list?) (action-btn :as string) (add-url :as string)
tr-cls pill-cls hx-select csrf-hdr) (tr-cls :as string) (pill-cls :as string) (hx-select :as string) (csrf-hdr :as string))
(~events-ticket-types-table (~events-ticket-types-table
:list-container list-container :list-container list-container
:rows (if (empty? (or ticket-types (list))) :rows (if (empty? (or ticket-types (list)))
@@ -333,9 +333,9 @@
:action-btn action-btn :add-url add-url)) :action-btn action-btn :add-url add-url))
;; Lookup result from data ;; Lookup result from data
(defcomp ~events-lookup-result-from-data (&key entry-name type-name date-str cal-name (defcomp ~events-lookup-result-from-data (&key (entry-name :as string) (type-name :as string?) (date-str :as string?) (cal-name :as string?)
state code checked-in-str (state :as string) (code :as string) (checked-in-str :as string?)
checkin-url csrf) (checkin-url :as string) (csrf :as string))
(~events-lookup-card (~events-lookup-card
:info (<> :info (<>
(~events-lookup-info :entry-name entry-name) (~events-lookup-info :entry-name entry-name)

View File

@@ -1,7 +1,7 @@
;; Auth components (choose username — federation-specific) ;; Auth components (choose username — federation-specific)
;; Login and check-email components are shared: see shared/sx/templates/auth.sx ;; Login and check-email components are shared: see shared/sx/templates/auth.sx
(defcomp ~federation-choose-username (&key domain error csrf username check-url) (defcomp ~federation-choose-username (&key (domain :as string) error (csrf :as string) (username :as string) (check-url :as string))
(div :class "py-8 max-w-md mx-auto" (div :class "py-8 max-w-md mx-auto"
(h1 :class "text-2xl font-bold mb-2" "Choose your username") (h1 :class "text-2xl font-bold mb-2" "Choose your username")
(p :class "text-stone-600 mb-6" "This will be your identity on the fediverse: " (p :class "text-stone-600 mb-6" "This will be your identity on the fediverse: "

View File

@@ -1,9 +1,9 @@
;; Notification components ;; Notification components
(defcomp ~federation-notification-preview (&key preview) (defcomp ~federation-notification-preview (&key (preview :as string))
(div :class "text-sm text-stone-500 mt-1 truncate" preview)) (div :class "text-sm text-stone-500 mt-1 truncate" preview))
(defcomp ~federation-notification-card (&key cls avatar from-name from-username from-domain action-text preview time) (defcomp ~federation-notification-card (&key (cls :as string) avatar (from-name :as string) (from-username :as string) (from-domain :as string) (action-text :as string) preview (time :as string))
(div :class cls (div :class cls
(div :class "flex items-start gap-3" (div :class "flex items-start gap-3"
avatar avatar
@@ -15,14 +15,14 @@
preview preview
(div :class "text-xs text-stone-400 mt-1" time))))) (div :class "text-xs text-stone-400 mt-1" time)))))
(defcomp ~federation-notifications-list (&key items) (defcomp ~federation-notifications-list (&key (items :as list))
(div :class "space-y-2" items)) (div :class "space-y-2" items))
(defcomp ~federation-notifications-page (&key notifs) (defcomp ~federation-notifications-page (&key notifs)
(h1 :class "text-2xl font-bold mb-6" "Notifications") notifs) (h1 :class "text-2xl font-bold mb-6" "Notifications") notifs)
;; Assembled notification card — replaces Python _notification_sx ;; Assembled notification card — replaces Python _notification_sx
(defcomp ~federation-notification-from-data (&key notif) (defcomp ~federation-notification-from-data (&key (notif :as dict))
(let* ((from-name (or (get notif "from_actor_name") "?")) (let* ((from-name (or (get notif "from_actor_name") "?"))
(from-username (or (get notif "from_actor_username") "")) (from-username (or (get notif "from_actor_username") ""))
(from-domain (or (get notif "from_actor_domain") "")) (from-domain (or (get notif "from_actor_domain") ""))
@@ -59,7 +59,7 @@
:time created))) :time created)))
;; Assembled notifications content — replaces Python _notifications_content_sx ;; Assembled notifications content — replaces Python _notifications_content_sx
(defcomp ~federation-notifications-content (&key notifications) (defcomp ~federation-notifications-content (&key (notifications :as list))
(~federation-notifications-page (~federation-notifications-page
:notifs (if (empty? notifications) :notifs (if (empty? notifications)
(~empty-state :message "No notifications yet." :cls "text-stone-500") (~empty-state :message "No notifications yet." :cls "text-stone-500")

View File

@@ -1,6 +1,6 @@
;; Profile and actor timeline components ;; Profile and actor timeline components
(defcomp ~federation-actor-profile-header (&key avatar display-name username domain summary follow) (defcomp ~federation-actor-profile-header (&key avatar (display-name :as string) (username :as string) (domain :as string) summary follow)
(div :class "bg-white rounded-lg shadow-sm border border-stone-200 p-6 mb-6" (div :class "bg-white rounded-lg shadow-sm border border-stone-200 p-6 mb-6"
(div :class "flex items-center gap-4" (div :class "flex items-center gap-4"
avatar avatar
@@ -14,35 +14,35 @@
header header
(div :id "timeline" timeline)) (div :id "timeline" timeline))
(defcomp ~federation-follow-form (&key action csrf actor-url label cls) (defcomp ~federation-follow-form (&key (action :as string) (csrf :as string) (actor-url :as string) (label :as string) (cls :as string))
(div :class "flex-shrink-0" (div :class "flex-shrink-0"
(form :method "post" :action action (form :method "post" :action action
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(input :type "hidden" :name "actor_url" :value actor-url) (input :type "hidden" :name "actor_url" :value actor-url)
(button :type "submit" :class cls label)))) (button :type "submit" :class cls label))))
(defcomp ~federation-profile-summary (&key summary) (defcomp ~federation-profile-summary (&key (summary :as string))
(div :class "text-sm text-stone-600 mt-2" (~rich-text :html summary))) (div :class "text-sm text-stone-600 mt-2" (~rich-text :html summary)))
;; Public profile page ;; Public profile page
(defcomp ~federation-activity-obj-type (&key obj-type) (defcomp ~federation-activity-obj-type (&key (obj-type :as string))
(span :class "text-sm text-stone-500" obj-type)) (span :class "text-sm text-stone-500" obj-type))
(defcomp ~federation-activity-card (&key activity-type published obj-type) (defcomp ~federation-activity-card (&key (activity-type :as string) (published :as string) obj-type)
(div :class "bg-white rounded-lg shadow p-4" (div :class "bg-white rounded-lg shadow p-4"
(div :class "flex justify-between items-start" (div :class "flex justify-between items-start"
(span :class "font-medium" activity-type) (span :class "font-medium" activity-type)
(span :class "text-sm text-stone-400" published)) (span :class "text-sm text-stone-400" published))
obj-type)) obj-type))
(defcomp ~federation-activities-list (&key items) (defcomp ~federation-activities-list (&key (items :as list))
(div :class "space-y-4" items)) (div :class "space-y-4" items))
(defcomp ~federation-activities-empty () (defcomp ~federation-activities-empty ()
(p :class "text-stone-500" "No activities yet.")) (p :class "text-stone-500" "No activities yet."))
(defcomp ~federation-profile-page (&key display-name username domain summary activities-heading activities) (defcomp ~federation-profile-page (&key (display-name :as string) (username :as string) (domain :as string) summary (activities-heading :as string) activities)
(div :class "py-8" (div :class "py-8"
(div :class "bg-white rounded-lg shadow p-6 mb-6" (div :class "bg-white rounded-lg shadow p-6 mb-6"
(h1 :class "text-2xl font-bold" display-name) (h1 :class "text-2xl font-bold" display-name)
@@ -51,11 +51,11 @@
(h2 :class "text-xl font-bold mb-4" activities-heading) (h2 :class "text-xl font-bold mb-4" activities-heading)
activities)) activities))
(defcomp ~federation-profile-summary-text (&key text) (defcomp ~federation-profile-summary-text (&key (text :as string))
(p :class "mt-2" text)) (p :class "mt-2" text))
;; Assembled actor timeline content — replaces Python _actor_timeline_content_sx ;; Assembled actor timeline content — replaces Python _actor_timeline_content_sx
(defcomp ~federation-actor-timeline-content (&key remote-actor items is-following actor) (defcomp ~federation-actor-timeline-content (&key (remote-actor :as dict) (items :as list) (is-following :as boolean) actor)
(let* ((display-name (or (get remote-actor "display_name") (get remote-actor "preferred_username") "")) (let* ((display-name (or (get remote-actor "display_name") (get remote-actor "preferred_username") ""))
(icon-url (get remote-actor "icon_url")) (icon-url (get remote-actor "icon_url"))
(summary (get remote-actor "summary")) (summary (get remote-actor "summary"))
@@ -92,7 +92,7 @@
:before (get (last items) "before_cursor"))))))) :before (get (last items) "before_cursor")))))))
;; Data-driven activities list (replaces Python loop in render_profile_page) ;; Data-driven activities list (replaces Python loop in render_profile_page)
(defcomp ~federation-activities-from-data (&key activities) (defcomp ~federation-activities-from-data (&key (activities :as list))
(if (empty? (or activities (list))) (if (empty? (or activities (list)))
(~federation-activities-empty) (~federation-activities-empty)
(~federation-activities-list (~federation-activities-list

View File

@@ -1,37 +1,37 @@
;; Search and actor card components ;; Search and actor card components
;; Aliases — delegate to shared ~avatar ;; Aliases — delegate to shared ~avatar
(defcomp ~federation-actor-avatar-img (&key src cls) (defcomp ~federation-actor-avatar-img (&key (src :as string) (cls :as string))
(~avatar :src src :cls cls)) (~avatar :src src :cls cls))
(defcomp ~federation-actor-avatar-placeholder (&key cls initial) (defcomp ~federation-actor-avatar-placeholder (&key (cls :as string) (initial :as string))
(~avatar :cls cls :initial initial)) (~avatar :cls cls :initial initial))
(defcomp ~federation-actor-name-link (&key href name) (defcomp ~federation-actor-name-link (&key (href :as string) (name :as string))
(a :href href :class "font-semibold text-stone-900 hover:underline" name)) (a :href href :class "font-semibold text-stone-900 hover:underline" name))
(defcomp ~federation-actor-name-link-external (&key href name) (defcomp ~federation-actor-name-link-external (&key (href :as string) (name :as string))
(a :href href :target "_blank" :rel "noopener" (a :href href :target "_blank" :rel "noopener"
:class "font-semibold text-stone-900 hover:underline" name)) :class "font-semibold text-stone-900 hover:underline" name))
(defcomp ~federation-actor-summary (&key summary) (defcomp ~federation-actor-summary (&key (summary :as string))
(div :class "text-sm text-stone-600 mt-1 truncate" (~rich-text :html summary))) (div :class "text-sm text-stone-600 mt-1 truncate" (~rich-text :html summary)))
(defcomp ~federation-unfollow-button (&key action csrf actor-url) (defcomp ~federation-unfollow-button (&key (action :as string) (csrf :as string) (actor-url :as string))
(div :class "flex-shrink-0" (div :class "flex-shrink-0"
(form :method "post" :action action :sx-post action :sx-target "closest article" :sx-swap "outerHTML" (form :method "post" :action action :sx-post action :sx-target "closest article" :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(input :type "hidden" :name "actor_url" :value actor-url) (input :type "hidden" :name "actor_url" :value actor-url)
(button :type "submit" :class "text-sm border border-stone-300 rounded px-3 py-1 hover:bg-stone-100" "Unfollow")))) (button :type "submit" :class "text-sm border border-stone-300 rounded px-3 py-1 hover:bg-stone-100" "Unfollow"))))
(defcomp ~federation-follow-button (&key action csrf actor-url label) (defcomp ~federation-follow-button (&key (action :as string) (csrf :as string) (actor-url :as string) (label :as string))
(div :class "flex-shrink-0" (div :class "flex-shrink-0"
(form :method "post" :action action :sx-post action :sx-target "closest article" :sx-swap "outerHTML" (form :method "post" :action action :sx-post action :sx-target "closest article" :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(input :type "hidden" :name "actor_url" :value actor-url) (input :type "hidden" :name "actor_url" :value actor-url)
(button :type "submit" :class "text-sm bg-stone-800 text-white rounded px-3 py-1 hover:bg-stone-700" label)))) (button :type "submit" :class "text-sm bg-stone-800 text-white rounded px-3 py-1 hover:bg-stone-700" label))))
(defcomp ~federation-actor-card (&key cls id avatar name username domain summary button) (defcomp ~federation-actor-card (&key (cls :as string) (id :as string) avatar name (username :as string) (domain :as string) summary button)
(article :class cls :id id (article :class cls :id id
avatar avatar
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
@@ -41,7 +41,7 @@
button)) button))
;; Data-driven actor card (replaces Python _actor_card_sx loop) ;; Data-driven actor card (replaces Python _actor_card_sx loop)
(defcomp ~federation-actor-card-from-data (&key d has-actor csrf follow-url unfollow-url list-type) (defcomp ~federation-actor-card-from-data (&key (d :as dict) (has-actor :as boolean) (csrf :as string) (follow-url :as string) (unfollow-url :as string) (list-type :as string))
(let* ((icon-url (get d "icon_url")) (let* ((icon-url (get d "icon_url"))
(display-name (get d "display_name")) (display-name (get d "display_name"))
(username (get d "username")) (username (get d "username"))
@@ -72,8 +72,8 @@
:summary summary-sx :button button))) :summary summary-sx :button button)))
;; Data-driven actor list (replaces Python _search_results_sx / _actor_list_items_sx loops) ;; Data-driven actor list (replaces Python _search_results_sx / _actor_list_items_sx loops)
(defcomp ~federation-actor-list-from-data (&key actors next-url has-actor csrf (defcomp ~federation-actor-list-from-data (&key (actors :as list) (next-url :as string?) (has-actor :as boolean) (csrf :as string)
follow-url unfollow-url list-type) (follow-url :as string) (unfollow-url :as string) (list-type :as string))
(<> (<>
(map (lambda (d) (map (lambda (d)
(~federation-actor-card-from-data :d d :has-actor has-actor :csrf csrf (~federation-actor-card-from-data :d d :has-actor has-actor :csrf csrf
@@ -81,10 +81,10 @@
(or actors (list))) (or actors (list)))
(when next-url (~federation-scroll-sentinel :url next-url)))) (when next-url (~federation-scroll-sentinel :url next-url))))
(defcomp ~federation-search-info (&key cls text) (defcomp ~federation-search-info (&key (cls :as string) (text :as string))
(p :class cls text)) (p :class cls text))
(defcomp ~federation-search-page (&key search-url search-page-url query info results) (defcomp ~federation-search-page (&key (search-url :as string) (search-page-url :as string) (query :as string) info results)
(h1 :class "text-2xl font-bold mb-6" "Search") (h1 :class "text-2xl font-bold mb-6" "Search")
(form :method "get" :action search-url :class "mb-6" (form :method "get" :action search-url :class "mb-6"
:sx-get search-page-url :sx-target "#search-results" :sx-push-url search-url :sx-get search-page-url :sx-target "#search-results" :sx-push-url search-url
@@ -97,7 +97,7 @@
(div :id "search-results" results)) (div :id "search-results" results))
;; Following / Followers list page ;; Following / Followers list page
(defcomp ~federation-actor-list-page (&key title count-str items) (defcomp ~federation-actor-list-page (&key (title :as string) (count-str :as string) items)
(h1 :class "text-2xl font-bold mb-6" title " " (h1 :class "text-2xl font-bold mb-6" title " "
(span :class "text-stone-400 font-normal" count-str)) (span :class "text-stone-400 font-normal" count-str))
(div :id "actor-list" items)) (div :id "actor-list" items))
@@ -106,7 +106,7 @@
;; Assembled actor card — replaces Python _actor_card_sx ;; Assembled actor card — replaces Python _actor_card_sx
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~federation-actor-card-from-data (&key a actor followed-urls list-type) (defcomp ~federation-actor-card-from-data (&key (a :as dict) actor (followed-urls :as list) (list-type :as string))
(let* ((display-name (or (get a "display_name") (get a "preferred_username") "")) (let* ((display-name (or (get a "display_name") (get a "preferred_username") ""))
(username (or (get a "preferred_username") "")) (username (or (get a "preferred_username") ""))
(domain (or (get a "domain") "")) (domain (or (get a "domain") ""))
@@ -146,7 +146,7 @@
:label (if (= list-type "followers") "Follow Back" "Follow"))))))) :label (if (= list-type "followers") "Follow Back" "Follow")))))))
;; Assembled search content — replaces Python _search_content_sx ;; Assembled search content — replaces Python _search_content_sx
(defcomp ~federation-search-content (&key query actors total followed-urls actor) (defcomp ~federation-search-content (&key (query :as string?) (actors :as list) (total :as number) (followed-urls :as list) actor)
(~federation-search-page (~federation-search-page
:search-url (url-for "social.defpage_search") :search-url (url-for "social.defpage_search")
:search-page-url (url-for "social.search_page") :search-page-url (url-for "social.search_page")
@@ -172,7 +172,7 @@
:url (url-for "social.search_page" :q query :page 2))))))) :url (url-for "social.search_page" :q query :page 2)))))))
;; Assembled following/followers content — replaces Python _following_content_sx etc. ;; Assembled following/followers content — replaces Python _following_content_sx etc.
(defcomp ~federation-following-content (&key actors total actor) (defcomp ~federation-following-content (&key (actors :as list) (total :as number) actor)
(~federation-actor-list-page (~federation-actor-list-page
:title "Following" :count-str (str "(" total ")") :title "Following" :count-str (str "(" total ")")
:items (when (not (empty? actors)) :items (when (not (empty? actors))
@@ -185,7 +185,7 @@
(~federation-scroll-sentinel (~federation-scroll-sentinel
:url (url-for "social.following_list_page" :page 2))))))) :url (url-for "social.following_list_page" :page 2)))))))
(defcomp ~federation-followers-content (&key actors total followed-urls actor) (defcomp ~federation-followers-content (&key (actors :as list) (total :as number) (followed-urls :as list) actor)
(~federation-actor-list-page (~federation-actor-list-page
:title "Followers" :count-str (str "(" total ")") :title "Followers" :count-str (str "(" total ")")
:items (when (not (empty? actors)) :items (when (not (empty? actors))

View File

@@ -2,11 +2,11 @@
;; --- Navigation --- ;; --- Navigation ---
(defcomp ~federation-nav-choose-username (&key url) (defcomp ~federation-nav-choose-username (&key (url :as string))
(nav :class "flex gap-3 text-sm items-center" (nav :class "flex gap-3 text-sm items-center"
(a :href url :class "px-2 py-1 rounded hover:bg-stone-200 font-bold" "Choose username"))) (a :href url :class "px-2 py-1 rounded hover:bg-stone-200 font-bold" "Choose username")))
(defcomp ~federation-nav-notification-link (&key href cls count-url) (defcomp ~federation-nav-notification-link (&key (href :as string) (cls :as string) (count-url :as string))
(a :href href :class cls "Notifications" (a :href href :class cls "Notifications"
(span :sx-get count-url :sx-trigger "load, every 30s" :sx-swap "innerHTML" (span :sx-get count-url :sx-trigger "load, every 30s" :sx-swap "innerHTML"
:class "absolute -top-2 -right-3 text-xs bg-red-500 text-white rounded-full px-1 empty:hidden"))) :class "absolute -top-2 -right-3 text-xs bg-red-500 text-white rounded-full px-1 empty:hidden")))
@@ -20,28 +20,28 @@
;; --- Post card --- ;; --- Post card ---
(defcomp ~federation-boost-label (&key name) (defcomp ~federation-boost-label (&key (name :as string))
(div :class "text-sm text-stone-500 mb-2" "Boosted by " name)) (div :class "text-sm text-stone-500 mb-2" "Boosted by " name))
;; Aliases — delegate to shared ~avatar ;; Aliases — delegate to shared ~avatar
(defcomp ~federation-avatar-img (&key src cls) (defcomp ~federation-avatar-img (&key (src :as string) (cls :as string))
(~avatar :src src :cls cls)) (~avatar :src src :cls cls))
(defcomp ~federation-avatar-placeholder (&key cls initial) (defcomp ~federation-avatar-placeholder (&key (cls :as string) (initial :as string))
(~avatar :cls cls :initial initial)) (~avatar :cls cls :initial initial))
(defcomp ~federation-content (&key content summary) (defcomp ~federation-content (&key (content :as string) (summary :as string?))
(if summary (if summary
(details :class "mt-2" (details :class "mt-2"
(summary :class "text-stone-500 cursor-pointer" "CW: " (~rich-text :html summary)) (summary :class "text-stone-500 cursor-pointer" "CW: " (~rich-text :html summary))
(div :class "mt-2 prose prose-sm prose-stone max-w-none" (~rich-text :html content))) (div :class "mt-2 prose prose-sm prose-stone max-w-none" (~rich-text :html content)))
(div :class "mt-2 prose prose-sm prose-stone max-w-none" (~rich-text :html content)))) (div :class "mt-2 prose prose-sm prose-stone max-w-none" (~rich-text :html content))))
(defcomp ~federation-original-link (&key url) (defcomp ~federation-original-link (&key (url :as string))
(a :href url :target "_blank" :rel "noopener" (a :href url :target "_blank" :rel "noopener"
:class "text-sm text-stone-400 hover:underline mt-1 inline-block" "original")) :class "text-sm text-stone-400 hover:underline mt-1 inline-block" "original"))
(defcomp ~federation-post-card (&key boost avatar actor-name actor-username domain time content original interactions) (defcomp ~federation-post-card (&key boost avatar (actor-name :as string) (actor-username :as string) (domain :as string) (time :as string) content original interactions)
(article :class "bg-white rounded-lg shadow-sm border border-stone-200 p-4 mb-4" (article :class "bg-white rounded-lg shadow-sm border border-stone-200 p-4 mb-4"
boost boost
(div :class "flex items-start gap-3" (div :class "flex items-start gap-3"
@@ -55,17 +55,17 @@
;; --- Interaction buttons --- ;; --- Interaction buttons ---
(defcomp ~federation-reply-link (&key url) (defcomp ~federation-reply-link (&key (url :as string))
(a :href url :class "hover:text-stone-700" "Reply")) (a :href url :class "hover:text-stone-700" "Reply"))
(defcomp ~federation-like-form (&key action target oid ainbox csrf cls icon count) (defcomp ~federation-like-form (&key (action :as string) (target :as string) (oid :as string) (ainbox :as string) (csrf :as string) (cls :as string) (icon :as string) count)
(form :sx-post action :sx-target target :sx-swap "innerHTML" (form :sx-post action :sx-target target :sx-swap "innerHTML"
(input :type "hidden" :name "object_id" :value oid) (input :type "hidden" :name "object_id" :value oid)
(input :type "hidden" :name "author_inbox" :value ainbox) (input :type "hidden" :name "author_inbox" :value ainbox)
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(button :type "submit" :class cls (span icon) " " count))) (button :type "submit" :class cls (span icon) " " count)))
(defcomp ~federation-boost-form (&key action target oid ainbox csrf cls count) (defcomp ~federation-boost-form (&key (action :as string) (target :as string) (oid :as string) (ainbox :as string) (csrf :as string) (cls :as string) count)
(form :sx-post action :sx-target target :sx-swap "innerHTML" (form :sx-post action :sx-target target :sx-swap "innerHTML"
(input :type "hidden" :name "object_id" :value oid) (input :type "hidden" :name "object_id" :value oid)
(input :type "hidden" :name "author_inbox" :value ainbox) (input :type "hidden" :name "author_inbox" :value ainbox)
@@ -78,13 +78,13 @@
;; --- Timeline --- ;; --- Timeline ---
(defcomp ~federation-scroll-sentinel (&key url) (defcomp ~federation-scroll-sentinel (&key (url :as string))
(div :sx-get url :sx-trigger "revealed" :sx-swap "outerHTML")) (div :sx-get url :sx-trigger "revealed" :sx-swap "outerHTML"))
(defcomp ~federation-compose-button (&key url) (defcomp ~federation-compose-button (&key (url :as string))
(a :href url :class "bg-stone-800 text-white px-4 py-2 rounded hover:bg-stone-700" "Compose")) (a :href url :class "bg-stone-800 text-white px-4 py-2 rounded hover:bg-stone-700" "Compose"))
(defcomp ~federation-timeline-page (&key label compose timeline) (defcomp ~federation-timeline-page (&key (label :as string) compose timeline)
(div :class "flex items-center justify-between mb-6" (div :class "flex items-center justify-between mb-6"
(h1 :class "text-2xl font-bold" label " Timeline") (h1 :class "text-2xl font-bold" label " Timeline")
compose) compose)
@@ -92,9 +92,9 @@
;; --- Data-driven post card (replaces Python _post_card_sx loop) --- ;; --- Data-driven post card (replaces Python _post_card_sx loop) ---
(defcomp ~federation-post-card-from-data (&key d has-actor csrf (defcomp ~federation-post-card-from-data (&key (d :as dict) (has-actor :as boolean) (csrf :as string)
like-url unlike-url (like-url :as string) (unlike-url :as string)
boost-url unboost-url) (boost-url :as string) (unboost-url :as string))
(let* ((boosted-by (get d "boosted_by")) (let* ((boosted-by (get d "boosted_by"))
(actor-icon (get d "actor_icon")) (actor-icon (get d "actor_icon"))
(actor-name (get d "actor_name")) (actor-name (get d "actor_name"))
@@ -140,8 +140,8 @@
:interactions interactions))) :interactions interactions)))
;; Data-driven timeline items (replaces Python _timeline_items_sx loop) ;; Data-driven timeline items (replaces Python _timeline_items_sx loop)
(defcomp ~federation-timeline-items-from-data (&key items next-url has-actor csrf (defcomp ~federation-timeline-items-from-data (&key (items :as list) (next-url :as string?) (has-actor :as boolean) (csrf :as string)
like-url unlike-url boost-url unboost-url) (like-url :as string) (unlike-url :as string) (boost-url :as string) (unboost-url :as string))
(<> (<>
(map (lambda (d) (map (lambda (d)
(~federation-post-card-from-data :d d :has-actor has-actor :csrf csrf (~federation-post-card-from-data :d d :has-actor has-actor :csrf csrf
@@ -151,11 +151,11 @@
;; --- Compose --- ;; --- Compose ---
(defcomp ~federation-compose-reply (&key reply-to) (defcomp ~federation-compose-reply (&key (reply-to :as string))
(input :type "hidden" :name "in_reply_to" :value reply-to) (input :type "hidden" :name "in_reply_to" :value reply-to)
(div :class "text-sm text-stone-500" "Replying to " (span :class "font-mono" reply-to))) (div :class "text-sm text-stone-500" "Replying to " (span :class "font-mono" reply-to)))
(defcomp ~federation-compose-form (&key action csrf reply) (defcomp ~federation-compose-form (&key (action :as string) (csrf :as string) reply)
(h1 :class "text-2xl font-bold mb-6" "Compose") (h1 :class "text-2xl font-bold mb-6" "Compose")
(form :method "post" :action action :class "space-y-4" (form :method "post" :action action :class "space-y-4"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
@@ -208,7 +208,7 @@
;; Assembled post card — replaces Python _post_card_sx ;; Assembled post card — replaces Python _post_card_sx
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~federation-post-card-from-data (&key item actor) (defcomp ~federation-post-card-from-data (&key (item :as dict) actor)
(let* ((boosted-by (get item "boosted_by")) (let* ((boosted-by (get item "boosted_by"))
(actor-icon (get item "actor_icon")) (actor-icon (get item "actor_icon"))
(actor-name (or (get item "actor_name") "?")) (actor-name (or (get item "actor_name") "?"))
@@ -267,7 +267,7 @@
;; Assembled timeline items — replaces Python _timeline_items_sx ;; Assembled timeline items — replaces Python _timeline_items_sx
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~federation-timeline-items (&key items timeline-type actor next-url) (defcomp ~federation-timeline-items (&key (items :as list) (timeline-type :as string) actor (next-url :as string?))
(<> (<>
(map (lambda (item) (map (lambda (item)
(~federation-post-card-from-data :item item :actor actor)) (~federation-post-card-from-data :item item :actor actor))
@@ -276,7 +276,7 @@
(~federation-scroll-sentinel :url next-url)))) (~federation-scroll-sentinel :url next-url))))
;; Assembled timeline content — replaces Python _timeline_content_sx ;; Assembled timeline content — replaces Python _timeline_content_sx
(defcomp ~federation-timeline-content (&key items timeline-type actor) (defcomp ~federation-timeline-content (&key (items :as list) (timeline-type :as string) actor)
(let* ((label (if (= timeline-type "home") "Home" "Public"))) (let* ((label (if (= timeline-type "home") "Home" "Public")))
(~federation-timeline-page (~federation-timeline-page
:label label :label label
@@ -289,7 +289,7 @@
:before (get (last items) "before_cursor"))))))) :before (get (last items) "before_cursor")))))))
;; Assembled compose content — replaces Python _compose_content_sx ;; Assembled compose content — replaces Python _compose_content_sx
(defcomp ~federation-compose-content (&key reply-to) (defcomp ~federation-compose-content (&key (reply-to :as string?))
(~federation-compose-form (~federation-compose-form
:action (url-for "social.compose_submit") :action (url-for "social.compose_submit")
:csrf (csrf-token) :csrf (csrf-token)

View File

@@ -1,10 +1,10 @@
;; Market card components — pure data, no raw! HTML injection ;; Market card components — pure data, no raw! HTML injection
(defcomp ~market-label-overlay (&key src) (defcomp ~market-label-overlay (&key (src :as string))
(img :src src :alt "" (img :src src :alt ""
:class "pointer-events-none absolute inset-0 w-full h-full object-contain object-top")) :class "pointer-events-none absolute inset-0 w-full h-full object-contain object-top"))
(defcomp ~market-card-image (&key image labels brand brand-highlight) (defcomp ~market-card-image (&key (image :as string) (labels :as list?) (brand :as string) (brand-highlight :as string?))
(div :class "w-full aspect-square bg-stone-100 relative" (div :class "w-full aspect-square bg-stone-100 relative"
(figure :class "inline-block w-full h-full" (figure :class "inline-block w-full h-full"
(div :class "relative w-full h-full" (div :class "relative w-full h-full"
@@ -12,35 +12,35 @@
(when labels (map (lambda (src) (~market-label-overlay :src src)) labels))) (when labels (map (lambda (src) (~market-label-overlay :src src)) labels)))
(figcaption :class (str "mt-2 text-sm text-center" brand-highlight " text-stone-600") brand)))) (figcaption :class (str "mt-2 text-sm text-center" brand-highlight " text-stone-600") brand))))
(defcomp ~market-card-no-image (&key labels brand) (defcomp ~market-card-no-image (&key (labels :as list?) (brand :as string))
(div :class "w-full aspect-square bg-stone-100 relative" (div :class "w-full aspect-square bg-stone-100 relative"
(div :class "p-2 flex flex-col items-center justify-center gap-2 text-red-500 h-full relative" (div :class "p-2 flex flex-col items-center justify-center gap-2 text-red-500 h-full relative"
(div :class "text-stone-400 text-xs" "No image") (div :class "text-stone-400 text-xs" "No image")
(when labels (ul :class "flex flex-row gap-1" (map (lambda (l) (li l)) labels))) (when labels (ul :class "flex flex-row gap-1" (map (lambda (l) (li l)) labels)))
(div :class "text-stone-900 text-center line-clamp-3 break-words [overflow-wrap:anywhere]" brand)))) (div :class "text-stone-900 text-center line-clamp-3 break-words [overflow-wrap:anywhere]" brand))))
(defcomp ~market-card-sticker (&key src name ring-cls) (defcomp ~market-card-sticker (&key (src :as string) (name :as string) (ring-cls :as string?))
(img :src src :alt name :class (str "w-6 h-6" ring-cls))) (img :src src :alt name :class (str "w-6 h-6" ring-cls)))
(defcomp ~market-card-stickers (&key stickers) (defcomp ~market-card-stickers (&key (stickers :as list))
(div :class "flex flex-row justify-center gap-2 p-2" (div :class "flex flex-row justify-center gap-2 p-2"
(map (lambda (s) (~market-card-sticker :src (get s "src") :name (get s "name") :ring-cls (get s "ring-cls"))) stickers))) (map (lambda (s) (~market-card-sticker :src (get s "src") :name (get s "name") :ring-cls (get s "ring-cls"))) stickers)))
(defcomp ~market-card-highlight (&key pre mid post) (defcomp ~market-card-highlight (&key (pre :as string) (mid :as string) (post :as string))
(<> pre (mark mid) post)) (<> pre (mark mid) post))
;; Price — delegates to shared ~price ;; Price — delegates to shared ~price
(defcomp ~market-card-price (&key special-price regular-price) (defcomp ~market-card-price (&key (special-price :as string?) (regular-price :as string?))
(~price :special-price special-price :regular-price regular-price)) (~price :special-price special-price :regular-price regular-price))
;; Main product card — accepts pure data, composes sub-components ;; Main product card — accepts pure data, composes sub-components
(defcomp ~market-product-card (&key href hx-select (defcomp ~market-product-card (&key (href :as string) (hx-select :as string)
has-like liked slug csrf like-action (has-like :as boolean) (liked :as boolean?) (slug :as string) (csrf :as string) (like-action :as string?)
image labels brand brand-highlight (image :as string?) (labels :as list?) (brand :as string) (brand-highlight :as string?)
special-price regular-price (special-price :as string?) (regular-price :as string?)
cart-action quantity cart-href (cart-action :as string) (quantity :as number?) (cart-href :as string)
stickers (stickers :as list?)
title has-highlight search-pre search-mid search-post) (title :as string) (has-highlight :as boolean) (search-pre :as string?) (search-mid :as string?) (search-post :as string?))
(div :class "flex flex-col rounded-xl bg-white shadow hover:shadow-md transition overflow-hidden relative" (div :class "flex flex-col rounded-xl bg-white shadow hover:shadow-md transition overflow-hidden relative"
(when has-like (when has-like
(~market-like-button :form-id (str "like-" slug) :action like-action :slug slug :csrf csrf (~market-like-button :form-id (str "like-" slug) :action like-action :slug slug :csrf csrf
@@ -65,7 +65,7 @@
(~market-card-highlight :pre search-pre :mid search-mid :post search-post) (~market-card-highlight :pre search-pre :mid search-mid :post search-post)
title))))) title)))))
(defcomp ~market-like-button (&key form-id action slug csrf icon-cls) (defcomp ~market-like-button (&key (form-id :as string) (action :as string) (slug :as string) (csrf :as string) (icon-cls :as string))
(div :class "absolute top-2 right-2 z-10 text-6xl md:text-xl" (div :class "absolute top-2 right-2 z-10 text-6xl md:text-xl"
(form :id form-id :action action :method "post" (form :id form-id :action action :method "post"
:sx-post action :sx-target (str "#like-" slug) :sx-swap "outerHTML" :sx-post action :sx-target (str "#like-" slug) :sx-swap "outerHTML"
@@ -73,22 +73,22 @@
(button :type "submit" :class "cursor-pointer" (button :type "submit" :class "cursor-pointer"
(i :class icon-cls :aria-hidden "true"))))) (i :class icon-cls :aria-hidden "true")))))
(defcomp ~market-market-card-title-link (&key href name) (defcomp ~market-market-card-title-link (&key (href :as string) (name :as string))
(a :href href :class "hover:text-emerald-700" (a :href href :class "hover:text-emerald-700"
(h2 :class "text-lg font-semibold text-stone-900" name))) (h2 :class "text-lg font-semibold text-stone-900" name)))
(defcomp ~market-market-card-title (&key name) (defcomp ~market-market-card-title (&key (name :as string))
(h2 :class "text-lg font-semibold text-stone-900" name)) (h2 :class "text-lg font-semibold text-stone-900" name))
(defcomp ~market-market-card-desc (&key description) (defcomp ~market-market-card-desc (&key (description :as string))
(p :class "text-sm text-stone-600 mt-1 line-clamp-2" description)) (p :class "text-sm text-stone-600 mt-1 line-clamp-2" description))
(defcomp ~market-market-card-badge (&key href title) (defcomp ~market-market-card-badge (&key (href :as string) (title :as string))
(div :class "flex flex-wrap items-center gap-1.5 mt-3" (div :class "flex flex-wrap items-center gap-1.5 mt-3"
(a :href href :class "inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 hover:bg-amber-200" (a :href href :class "inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 hover:bg-amber-200"
title))) title)))
(defcomp ~market-market-card (&key title-content desc-content badge-content title desc badge) (defcomp ~market-market-card (&key (title-content :as list?) (desc-content :as list?) (badge-content :as list?) (title :as string?) (desc :as string?) (badge :as string?))
(article :class "rounded-xl bg-white shadow-sm border border-stone-200 p-5 flex flex-col justify-between hover:border-stone-400 transition-colors" (article :class "rounded-xl bg-white shadow-sm border border-stone-200 p-5 flex flex-col justify-between hover:border-stone-400 transition-colors"
(div (div
(if title-content title-content (when title title)) (if title-content title-content (when title title))
@@ -101,8 +101,8 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Product cards grid with infinite scroll sentinels ;; Product cards grid with infinite scroll sentinels
(defcomp ~market-product-cards-content (&key products page total-pages next-url (defcomp ~market-product-cards-content (&key (products :as list) (page :as number) (total-pages :as number) (next-url :as string)
mobile-sentinel-hs desktop-sentinel-hs) (mobile-sentinel-hs :as string?) (desktop-sentinel-hs :as string?))
(<> (<>
(map (lambda (p) (map (lambda (p)
(~market-product-card (~market-product-card
@@ -126,7 +126,7 @@
(~end-of-results)))) (~end-of-results))))
;; Single market card from data (handles conditional title/desc/badge) ;; Single market card from data (handles conditional title/desc/badge)
(defcomp ~market-card-from-data (&key name description href show-badge badge-href badge-title) (defcomp ~market-card-from-data (&key (name :as string) (description :as string?) (href :as string?) (show-badge :as boolean) (badge-href :as string?) (badge-title :as string?))
(~market-market-card (~market-market-card
:title-content (if href :title-content (if href
(~market-market-card-title-link :href href :name name) (~market-market-card-title-link :href href :name name)
@@ -137,7 +137,7 @@
(~market-market-card-badge :href badge-href :title badge-title)))) (~market-market-card-badge :href badge-href :title badge-title))))
;; Market cards list with infinite scroll sentinel ;; Market cards list with infinite scroll sentinel
(defcomp ~market-cards-content (&key markets page has-more next-url) (defcomp ~market-cards-content (&key (markets :as list) (page :as number) (has-more :as boolean) (next-url :as string))
(<> (<>
(map (lambda (m) (map (lambda (m)
(~market-card-from-data (~market-card-from-data
@@ -149,7 +149,7 @@
(~sentinel-simple :id (str "sentinel-" page) :next-url next-url)))) (~sentinel-simple :id (str "sentinel-" page) :next-url next-url))))
;; Market landing page content from data ;; Market landing page content from data
(defcomp ~market-landing-from-data (&key excerpt feature-image html) (defcomp ~market-landing-from-data (&key (excerpt :as string?) (feature-image :as string?) (html :as string?))
(~market-landing-content :inner (~market-landing-content :inner
(<> (when excerpt (~market-landing-excerpt :text excerpt)) (<> (when excerpt (~market-landing-excerpt :text excerpt))
(when feature-image (~market-landing-image :src feature-image)) (when feature-image (~market-landing-image :src feature-image))

View File

@@ -1,6 +1,6 @@
;; Market product detail components ;; Market product detail components
(defcomp ~market-detail-gallery-inner (&key like image alt labels brand) (defcomp ~market-detail-gallery-inner (&key (like :as list?) (image :as string) (alt :as string) (labels :as list?) (brand :as string))
(<> like (<> like
(figure :class "inline-block" (figure :class "inline-block"
(div :class "relative w-full aspect-square" (div :class "relative w-full aspect-square"
@@ -18,79 +18,79 @@
:class "absolute right-2 top-1/2 -translate-y-1/2 z-10 grid place-items-center w-12 h-12 md:w-14 md:h-14 rounded-full bg-white/90 hover:bg-white shadow-lg text-3xl md:text-4xl" :class "absolute right-2 top-1/2 -translate-y-1/2 z-10 grid place-items-center w-12 h-12 md:w-14 md:h-14 rounded-full bg-white/90 hover:bg-white shadow-lg text-3xl md:text-4xl"
:title "Next" "\u203a"))) :title "Next" "\u203a")))
(defcomp ~market-detail-gallery (&key inner nav) (defcomp ~market-detail-gallery (&key (inner :as list) (nav :as list?))
(div :class "relative rounded-xl overflow-hidden bg-stone-100" (div :class "relative rounded-xl overflow-hidden bg-stone-100"
inner nav)) inner nav))
(defcomp ~market-detail-thumb (&key title src alt) (defcomp ~market-detail-thumb (&key (title :as string) (src :as string) (alt :as string))
(<> (button :type "button" :data-thumb "" (<> (button :type "button" :data-thumb ""
:class "shrink-0 rounded-lg overflow-hidden bg-stone-100 hover:opacity-90 ring-offset-2" :class "shrink-0 rounded-lg overflow-hidden bg-stone-100 hover:opacity-90 ring-offset-2"
:title title :title title
(img :src src :class "h-16 w-16 object-contain" :alt alt :loading "lazy" :decoding "async")) (img :src src :class "h-16 w-16 object-contain" :alt alt :loading "lazy" :decoding "async"))
(span :data-image-src src :class "hidden"))) (span :data-image-src src :class "hidden")))
(defcomp ~market-detail-thumbs (&key thumbs) (defcomp ~market-detail-thumbs (&key (thumbs :as list))
(div :class "flex flex-row justify-center" (div :class "flex flex-row justify-center"
(div :class "mt-3 flex gap-2 overflow-x-auto no-scrollbar" thumbs))) (div :class "mt-3 flex gap-2 overflow-x-auto no-scrollbar" thumbs)))
(defcomp ~market-detail-no-image (&key like) (defcomp ~market-detail-no-image (&key (like :as list?))
(div :class "relative aspect-square bg-stone-100 rounded-xl flex items-center justify-center text-stone-400" (div :class "relative aspect-square bg-stone-100 rounded-xl flex items-center justify-center text-stone-400"
like "No image")) like "No image"))
(defcomp ~market-detail-sticker (&key src name) (defcomp ~market-detail-sticker (&key (src :as string) (name :as string))
(img :src src :alt name :class "w-10 h-10")) (img :src src :alt name :class "w-10 h-10"))
(defcomp ~market-detail-stickers (&key items) (defcomp ~market-detail-stickers (&key (items :as list))
(div :class "p-2 flex flex-row justify-center gap-2" items)) (div :class "p-2 flex flex-row justify-center gap-2" items))
(defcomp ~market-detail-unit-price (&key price) (defcomp ~market-detail-unit-price (&key (price :as string))
(div (str "Unit price: " price))) (div (str "Unit price: " price)))
(defcomp ~market-detail-case-size (&key size) (defcomp ~market-detail-case-size (&key (size :as string))
(div (str "Case size: " size))) (div (str "Case size: " size)))
(defcomp ~market-detail-extras (&key inner) (defcomp ~market-detail-extras (&key (inner :as list))
(div :class "mt-2 space-y-1 text-sm text-stone-600" inner)) (div :class "mt-2 space-y-1 text-sm text-stone-600" inner))
(defcomp ~market-detail-desc-short (&key text) (defcomp ~market-detail-desc-short (&key (text :as string))
(p :class "leading-relaxed text-lg" text)) (p :class "leading-relaxed text-lg" text))
(defcomp ~market-detail-desc-html (&key html) (defcomp ~market-detail-desc-html (&key (html :as string))
(div :class "max-w-none text-sm leading-relaxed" (~rich-text :html html))) (div :class "max-w-none text-sm leading-relaxed" (~rich-text :html html)))
(defcomp ~market-detail-desc-wrapper (&key inner) (defcomp ~market-detail-desc-wrapper (&key (inner :as list))
(div :class "mt-4 text-stone-800 space-y-3" inner)) (div :class "mt-4 text-stone-800 space-y-3" inner))
(defcomp ~market-detail-section (&key title html) (defcomp ~market-detail-section (&key (title :as string) (html :as string))
(details :class "group rounded-xl border bg-white shadow-sm open:shadow p-0" (details :class "group rounded-xl border bg-white shadow-sm open:shadow p-0"
(summary :class "cursor-pointer select-none px-4 py-3 flex items-center justify-between" (summary :class "cursor-pointer select-none px-4 py-3 flex items-center justify-between"
(span :class "font-medium" title) (span :class "font-medium" title)
(span :class "ml-2 text-xl transition-transform group-open:rotate-180" "\u2304")) (span :class "ml-2 text-xl transition-transform group-open:rotate-180" "\u2304"))
(div :class "px-4 pb-4 max-w-none text-sm leading-relaxed" (~rich-text :html html)))) (div :class "px-4 pb-4 max-w-none text-sm leading-relaxed" (~rich-text :html html))))
(defcomp ~market-detail-sections (&key items) (defcomp ~market-detail-sections (&key (items :as list))
(div :class "mt-8 space-y-3" items)) (div :class "mt-8 space-y-3" items))
(defcomp ~market-detail-right-col (&key inner) (defcomp ~market-detail-right-col (&key (inner :as list))
(div :class "md:col-span-3" inner)) (div :class "md:col-span-3" inner))
(defcomp ~market-detail-layout (&key gallery stickers details) (defcomp ~market-detail-layout (&key (gallery :as list) (stickers :as list?) (details :as list))
(<> (div :class "mt-3 grid grid-cols-1 md:grid-cols-5 gap-6" :data-gallery-root "" (<> (div :class "mt-3 grid grid-cols-1 md:grid-cols-5 gap-6" :data-gallery-root ""
(div :class "md:col-span-2" gallery stickers) (div :class "md:col-span-2" gallery stickers)
details) details)
(div :class "pb-8"))) (div :class "pb-8")))
(defcomp ~market-landing-excerpt (&key text) (defcomp ~market-landing-excerpt (&key (text :as string))
(div :class "w-full text-center italic text-3xl p-2" text)) (div :class "w-full text-center italic text-3xl p-2" text))
(defcomp ~market-landing-image (&key src) (defcomp ~market-landing-image (&key (src :as string))
(div :class "mb-3 flex justify-center" (div :class "mb-3 flex justify-center"
(img :src src :alt "" :class "rounded-lg w-full md:w-3/4 object-cover"))) (img :src src :alt "" :class "rounded-lg w-full md:w-3/4 object-cover")))
(defcomp ~market-landing-html (&key html) (defcomp ~market-landing-html (&key (html :as string))
(div :class "blog-content p-2" (~rich-text :html html))) (div :class "blog-content p-2" (~rich-text :html html)))
(defcomp ~market-landing-content (&key inner) (defcomp ~market-landing-content (&key (inner :as list))
(<> (article :class "relative w-full" inner) (div :class "pb-8"))) (<> (article :class "relative w-full" inner) (div :class "pb-8")))
@@ -99,7 +99,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Gallery section from pre-computed data ;; Gallery section from pre-computed data
(defcomp ~market-detail-gallery-from-data (&key images labels brand like-data has-nav-buttons thumbs) (defcomp ~market-detail-gallery-from-data (&key (images :as list?) (labels :as list?) (brand :as string) (like-data :as dict?) (has-nav-buttons :as boolean) (thumbs :as list?))
(let ((like-sx (when like-data (let ((like-sx (when like-data
(~market-like-button (~market-like-button
:form-id (get like-data "form-id") :action (get like-data "action") :form-id (get like-data "form-id") :action (get like-data "action")
@@ -124,7 +124,7 @@
(~market-detail-no-image :like like-sx)))) (~market-detail-no-image :like like-sx))))
;; Right column details from data ;; Right column details from data
(defcomp ~market-detail-info-from-data (&key extras desc-short desc-html sections) (defcomp ~market-detail-info-from-data (&key (extras :as list?) (desc-short :as string?) (desc-html :as string?) (sections :as list?))
(~market-detail-right-col :inner (~market-detail-right-col :inner
(<> (<>
(when extras (when extras
@@ -145,9 +145,9 @@
sections))))))) sections)))))))
;; Full product detail layout from data ;; Full product detail layout from data
(defcomp ~market-product-detail-from-data (&key images labels brand like-data (defcomp ~market-product-detail-from-data (&key (images :as list?) (labels :as list?) (brand :as string) (like-data :as dict?)
has-nav-buttons thumbs sticker-items (has-nav-buttons :as boolean) (thumbs :as list?) (sticker-items :as list?)
extras desc-short desc-html sections) (extras :as list?) (desc-short :as string?) (desc-html :as string?) (sections :as list?))
(~market-detail-layout (~market-detail-layout
:gallery (~market-detail-gallery-from-data :gallery (~market-detail-gallery-from-data
:images images :labels labels :brand brand :like-data like-data :images images :labels labels :brand brand :like-data like-data

View File

@@ -1,21 +1,21 @@
;; Market meta/SEO components ;; Market meta/SEO components
(defcomp ~market-meta-title (&key title) (defcomp ~market-meta-title (&key (title :as string))
(title title)) (title title))
(defcomp ~market-meta-description (&key description) (defcomp ~market-meta-description (&key (description :as string))
(meta :name "description" :content description)) (meta :name "description" :content description))
(defcomp ~market-meta-canonical (&key href) (defcomp ~market-meta-canonical (&key (href :as string))
(link :rel "canonical" :href href)) (link :rel "canonical" :href href))
(defcomp ~market-meta-og (&key property content) (defcomp ~market-meta-og (&key (property :as string) (content :as string))
(meta :property property :content content)) (meta :property property :content content))
(defcomp ~market-meta-twitter (&key name content) (defcomp ~market-meta-twitter (&key (name :as string) (content :as string))
(meta :name name :content content)) (meta :name name :content content))
(defcomp ~market-meta-jsonld (&key json) (defcomp ~market-meta-jsonld (&key (json :as string))
(script :type "application/ld+json" (~rich-text :html json))) (script :type "application/ld+json" (~rich-text :html json)))
@@ -23,9 +23,10 @@
;; Composition: all product meta tags from data ;; Composition: all product meta tags from data
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~market-product-meta-from-data (&key title description canonical image-url (defcomp ~market-product-meta-from-data (&key (title :as string) (description :as string) (canonical :as string?)
site-title brand price price-currency (image-url :as string?)
jsonld-json) (site-title :as string) (brand :as string?) (price :as string?) (price-currency :as string?)
(jsonld-json :as string))
(<> (<>
(~market-meta-title :title title) (~market-meta-title :title title)
(~market-meta-description :description description) (~market-meta-description :description description)

View File

@@ -1,6 +1,6 @@
;; Market navigation components ;; Market navigation components
(defcomp ~market-category-link (&key href hx-select active select-colours label) (defcomp ~market-category-link (&key (href :as string) (hx-select :as string) (active :as boolean) (select-colours :as string) (label :as string))
(div :class "relative nav-group" (div :class "relative nav-group"
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
@@ -8,14 +8,14 @@
:class (str "block px-2 py-1 rounded text-center whitespace-normal break-words leading-snug bg-stone-200 text-black " select-colours) :class (str "block px-2 py-1 rounded text-center whitespace-normal break-words leading-snug bg-stone-200 text-black " select-colours)
label))) label)))
(defcomp ~market-desktop-category-nav (&key links admin) (defcomp ~market-desktop-category-nav (&key (links :as list) (admin :as list?))
(nav :class "hidden md:flex gap-4 text-sm ml-2 w-full justify-end items-center" (nav :class "hidden md:flex gap-4 text-sm ml-2 w-full justify-end items-center"
links admin)) links admin))
(defcomp ~market-mobile-nav-wrapper (&key items) (defcomp ~market-mobile-nav-wrapper (&key (items :as list))
(div :class "px-4 py-2" (div :class "divide-y" items))) (div :class "px-4 py-2" (div :class "divide-y" items)))
(defcomp ~market-mobile-all-link (&key href hx-select active select-colours) (defcomp ~market-mobile-all-link (&key (href :as string) (hx-select :as string) (active :as boolean) (select-colours :as string))
(a :role "option" :href href :sx-get href :sx-target "#main-panel" (a :role "option" :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:aria-selected (if active "true" "false") :aria-selected (if active "true" "false")
@@ -28,7 +28,7 @@
(path :fill-rule "evenodd" :clip-rule "evenodd" (path :fill-rule "evenodd" :clip-rule "evenodd"
:d "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"))) :d "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z")))
(defcomp ~market-mobile-cat-summary (&key bg-cls href hx-select select-colours cat-name count-label count-str chevron) (defcomp ~market-mobile-cat-summary (&key (bg-cls :as string) (href :as string) (hx-select :as string) (select-colours :as string) (cat-name :as string) (count-label :as string) (count-str :as string) (chevron :as list))
(summary :class (str "flex items-center justify-between cursor-pointer select-none block rounded-lg px-3 py-3 text-base hover:bg-stone-50" bg-cls) (summary :class (str "flex items-center justify-between cursor-pointer select-none block rounded-lg px-3 py-3 text-base hover:bg-stone-50" bg-cls)
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
@@ -37,7 +37,7 @@
(div :aria-label count-label count-str)) (div :aria-label count-label count-str))
chevron)) chevron))
(defcomp ~market-mobile-sub-link (&key select-colours active href hx-select label count-label count-str) (defcomp ~market-mobile-sub-link (&key (select-colours :as string) (active :as boolean) (href :as string) (hx-select :as string) (label :as string) (count-label :as string) (count-str :as string))
(a :class (str "snap-start px-2 py-3 rounded " select-colours " flex flex-row gap-2") (a :class (str "snap-start px-2 py-3 rounded " select-colours " flex flex-row gap-2")
:aria-selected (if active "true" "false") :aria-selected (if active "true" "false")
:href href :sx-get href :sx-target "#main-panel" :href href :sx-get href :sx-target "#main-panel"
@@ -45,20 +45,20 @@
(div label) (div label)
(div :aria-label count-label count-str))) (div :aria-label count-label count-str)))
(defcomp ~market-mobile-subs-panel (&key links) (defcomp ~market-mobile-subs-panel (&key (links :as list))
(div :class "pb-3 pl-2" (div :class "pb-3 pl-2"
(div :data-peek-viewport "" :data-peek-size-px "18" :data-peek-edge "bottom" :data-peek-mask "true" :class "m-2 bg-stone-100" (div :data-peek-viewport "" :data-peek-size-px "18" :data-peek-edge "bottom" :data-peek-mask "true" :class "m-2 bg-stone-100"
(div :data-peek-inner "" :class "grid grid-cols-1 gap-1 snap-y snap-mandatory pr-1" :aria-label "Subcategories" (div :data-peek-inner "" :class "grid grid-cols-1 gap-1 snap-y snap-mandatory pr-1" :aria-label "Subcategories"
links)))) links))))
(defcomp ~market-mobile-view-all (&key href hx-select) (defcomp ~market-mobile-view-all (&key (href :as string) (hx-select :as string))
(div :class "pb-3 pl-2" (div :class "pb-3 pl-2"
(a :class "px-2 py-1 rounded hover:bg-stone-100 block" (a :class "px-2 py-1 rounded hover:bg-stone-100 block"
:href href :sx-get href :sx-target "#main-panel" :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
"View all"))) "View all")))
(defcomp ~market-mobile-cat-details (&key open summary subs) (defcomp ~market-mobile-cat-details (&key (open :as boolean) (summary :as list) (subs :as list))
(details :class "group/cat py-1" :open open (details :class "group/cat py-1" :open open
summary subs)) summary subs))
@@ -67,7 +67,7 @@
;; Composition: mobile nav panel from pre-computed category data ;; Composition: mobile nav panel from pre-computed category data
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~market-mobile-nav-from-data (&key categories all-href all-active hx-select select-colours) (defcomp ~market-mobile-nav-from-data (&key (categories :as list) (all-href :as string) (all-active :as boolean) (hx-select :as string) (select-colours :as string))
(~market-mobile-nav-wrapper :items (~market-mobile-nav-wrapper :items
(<> (<>
(~market-mobile-all-link :href all-href :hx-select hx-select (~market-mobile-all-link :href all-href :hx-select hx-select

View File

@@ -1,36 +1,36 @@
;; Market price display components ;; Market price display components
(defcomp ~market-price-special (&key price) (defcomp ~market-price-special (&key (price :as string))
(div :class "text-lg font-semibold text-emerald-700" price)) (div :class "text-lg font-semibold text-emerald-700" price))
(defcomp ~market-price-regular-strike (&key price) (defcomp ~market-price-regular-strike (&key (price :as string))
(div :class "text-sm line-through text-stone-500" price)) (div :class "text-sm line-through text-stone-500" price))
(defcomp ~market-price-regular (&key price) (defcomp ~market-price-regular (&key (price :as string))
(div :class "mt-1 text-lg font-semibold" price)) (div :class "mt-1 text-lg font-semibold" price))
(defcomp ~market-price-line (&key inner) (defcomp ~market-price-line (&key (inner :as list))
(div :class "mt-1 flex items-baseline gap-2 justify-center" inner)) (div :class "mt-1 flex items-baseline gap-2 justify-center" inner))
(defcomp ~market-header-price-special-label () (defcomp ~market-header-price-special-label ()
(div :class "text-md font-bold text-emerald-700" "Special price")) (div :class "text-md font-bold text-emerald-700" "Special price"))
(defcomp ~market-header-price-special (&key price) (defcomp ~market-header-price-special (&key (price :as string))
(div :class "text-xl font-semibold text-emerald-700" price)) (div :class "text-xl font-semibold text-emerald-700" price))
(defcomp ~market-header-price-strike (&key price) (defcomp ~market-header-price-strike (&key (price :as string))
(div :class "text-base text-md line-through text-stone-500" price)) (div :class "text-base text-md line-through text-stone-500" price))
(defcomp ~market-header-price-regular-label () (defcomp ~market-header-price-regular-label ()
(div :class "hidden md:block text-xl font-bold" "Our price")) (div :class "hidden md:block text-xl font-bold" "Our price"))
(defcomp ~market-header-price-regular (&key price) (defcomp ~market-header-price-regular (&key (price :as string))
(div :class "text-xl font-semibold" price)) (div :class "text-xl font-semibold" price))
(defcomp ~market-header-rrp (&key rrp) (defcomp ~market-header-rrp (&key (rrp :as string))
(div :class "text-base text-stone-400" (span "rrp:") " " (span rrp))) (div :class "text-base text-stone-400" (span "rrp:") " " (span rrp)))
(defcomp ~market-prices-row (&key inner) (defcomp ~market-prices-row (&key (inner :as list))
(div :class "flex flex-row items-center justify-between md:gap-2 md:px-2" inner)) (div :class "flex flex-row items-center justify-between md:gap-2 md:px-2" inner))
@@ -38,8 +38,9 @@
;; Composition: prices header + cart button from data ;; Composition: prices header + cart button from data
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~market-prices-header-from-data (&key cart-id cart-action csrf quantity cart-href (defcomp ~market-prices-header-from-data (&key (cart-id :as string) (cart-action :as string) (csrf :as string) (quantity :as number?)
sp-val sp-str rp-val rp-str rrp-str) (cart-href :as string)
(sp-val :as number?) (sp-str :as string?) (rp-val :as number?) (rp-str :as string?) (rrp-str :as string?))
(~market-prices-row :inner (~market-prices-row :inner
(<> (<>
(if quantity (if quantity
@@ -57,7 +58,7 @@
(when rrp-str (~market-header-rrp :rrp rrp-str))))) (when rrp-str (~market-header-rrp :rrp rrp-str)))))
;; Card price line from data (used in product cards) ;; Card price line from data (used in product cards)
(defcomp ~market-card-price-from-data (&key sp-val sp-str rp-val rp-str) (defcomp ~market-card-price-from-data (&key (sp-val :as number?) (sp-str :as string?) (rp-val :as number?) (rp-str :as string?))
(~market-price-line :inner (~market-price-line :inner
(<> (<>
(when sp-val (when sp-val

View File

@@ -1,6 +1,6 @@
;; Checkout return page components ;; Checkout return page components
(defcomp ~checkout-return-header (&key status) (defcomp ~checkout-return-header (&key (status :as string))
(header :class "mb-1 sm:mb-2 flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4" (header :class "mb-1 sm:mb-2 flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4"
(div :class "space-y-1" (div :class "space-y-1"
(h1 :class "text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight" (h1 :class "text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight"
@@ -21,7 +21,7 @@
(div :class "rounded-2xl border border-dashed border-rose-300 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-800" (div :class "rounded-2xl border border-dashed border-rose-300 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-800"
"We couldn\u2019t find that order. If you reached this page from an old link, please start a new order."))) "We couldn\u2019t find that order. If you reached this page from an old link, please start a new order.")))
(defcomp ~checkout-return-failed (&key order-id) (defcomp ~checkout-return-failed (&key (order-id :as string))
(div :class "rounded-2xl border border-rose-200 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-900 space-y-2" (div :class "rounded-2xl border border-rose-200 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-900 space-y-2"
(p :class "font-medium" "Your payment was not completed.") (p :class "font-medium" "Your payment was not completed.")
(p "You can go back to your cart and try checkout again. If the problem persists, please contact us and mention order " (p "You can go back to your cart and try checkout again. If the problem persists, please contact us and mention order "
@@ -32,7 +32,7 @@
(p :class "font-medium" "All done!") (p :class "font-medium" "All done!")
(p "We\u2019ll start processing your order shortly."))) (p "We\u2019ll start processing your order shortly.")))
(defcomp ~checkout-return-ticket (&key name pill state type-name date-str code price) (defcomp ~checkout-return-ticket (&key (name :as string) (pill :as string) (state :as string) (type-name :as string?) (date-str :as string) (code :as string) (price :as string))
(li :class "px-4 py-3 flex items-start justify-between text-sm" (li :class "px-4 py-3 flex items-start justify-between text-sm"
(div (div
(div :class "font-medium flex items-center gap-2" (div :class "font-medium flex items-center gap-2"
@@ -48,7 +48,7 @@
(ul :class "divide-y divide-stone-200 rounded-2xl border border-stone-200 bg-white/80" items))) (ul :class "divide-y divide-stone-200 rounded-2xl border border-stone-200 bg-white/80" items)))
;; Data-driven ticket items (replaces Python loop) ;; Data-driven ticket items (replaces Python loop)
(defcomp ~checkout-return-tickets-from-data (&key tickets) (defcomp ~checkout-return-tickets-from-data (&key (tickets :as list))
(~checkout-return-tickets (~checkout-return-tickets
:items (<> (map (lambda (tk) :items (<> (map (lambda (tk)
(~checkout-return-ticket (~checkout-return-ticket

View File

@@ -3,13 +3,13 @@
;; --- orders layout: root + auth + orders rows --- ;; --- orders layout: root + auth + orders rows ---
(defcomp ~orders-layout-full (&key list-url) (defcomp ~orders-layout-full (&key (list-url :as string))
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~header-child-sx
:inner (<> (~auth-header-row-auto) :inner (<> (~auth-header-row-auto)
(~orders-header-row :list-url (or list-url "/")))))) (~orders-header-row :list-url (or list-url "/"))))))
(defcomp ~orders-layout-oob (&key list-url) (defcomp ~orders-layout-oob (&key (list-url :as string))
(<> (~auth-header-row-auto true) (<> (~auth-header-row-auto true)
(~oob-header-sx (~oob-header-sx
:parent-id "auth-header-child" :parent-id "auth-header-child"
@@ -21,7 +21,7 @@
;; --- order-detail layout: root + auth + orders + order rows --- ;; --- order-detail layout: root + auth + orders + order rows ---
(defcomp ~order-detail-layout-full (&key list-url detail-url) (defcomp ~order-detail-layout-full (&key (list-url :as string) (detail-url :as string))
(<> (~root-header-auto) (<> (~root-header-auto)
(~order-detail-header-stack (~order-detail-header-stack
:auth (~auth-header-row-auto) :auth (~auth-header-row-auto)
@@ -30,7 +30,7 @@
:link-href (or detail-url "/") :link-label "Order" :link-href (or detail-url "/") :link-label "Order"
:icon "fa fa-gbp")))) :icon "fa fa-gbp"))))
(defcomp ~order-detail-layout-oob (&key detail-url) (defcomp ~order-detail-layout-oob (&key (detail-url :as string))
(<> (~oob-header-sx (<> (~oob-header-sx
:parent-id "orders-header-child" :parent-id "orders-header-child"
:row (~menu-row-sx :id "order-row" :level 3 :colour "sky" :row (~menu-row-sx :id "order-row" :level 3 :colour "sky"

View File

@@ -14,7 +14,7 @@
// ========================================================================= // =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-11T04:41:27Z"; var SX_VERSION = "2026-03-11T21:11:04Z";
function isNil(x) { return x === NIL || x === null || x === undefined; } function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); } function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -204,7 +204,7 @@
// JSON / dict helpers for island state serialization // JSON / dict helpers for island state serialization
function jsonSerialize(obj) { function jsonSerialize(obj) {
try { return JSON.stringify(obj); } catch(e) { return "{}"; } return JSON.stringify(obj);
} }
function isEmptyDict(d) { function isEmptyDict(d) {
if (!d || typeof d !== "object") return true; if (!d || typeof d !== "object") return true;
@@ -214,11 +214,34 @@
function envHas(env, name) { return name in env; } function envHas(env, name) { return name in env; }
function envGet(env, name) { return env[name]; } function envGet(env, name) { return env[name]; }
function envSet(env, name, val) { env[name] = val; } function envSet(env, name, val) {
// Walk prototype chain to find where the variable is defined (for set!)
var obj = env;
while (obj !== null && obj !== Object.prototype) {
if (obj.hasOwnProperty(name)) { obj[name] = val; return; }
obj = Object.getPrototypeOf(obj);
}
// Not found in any parent scope — set on the immediate env
env[name] = val;
}
function envExtend(env) { return Object.create(env); } function envExtend(env) { return Object.create(env); }
function envMerge(base, overlay) { function envMerge(base, overlay) {
// Same env or overlay is descendant of base — just extend, no copy.
// This prevents set! inside lambdas from modifying shadow copies.
if (base === overlay) return Object.create(base);
var p = overlay;
for (var d = 0; p && p !== Object.prototype && d < 100; d++) {
if (p === base) return Object.create(base);
p = Object.getPrototypeOf(p);
}
// General case: extend base, copy ONLY overlay properties that don't
// exist in the base chain (avoids shadowing closure bindings).
var child = Object.create(base); var child = Object.create(base);
if (overlay) for (var k in overlay) if (overlay.hasOwnProperty(k)) child[k] = overlay[k]; if (overlay) {
for (var k in overlay) {
if (overlay.hasOwnProperty(k) && !(k in base)) child[k] = overlay[k];
}
}
return child; return child;
} }
@@ -252,6 +275,7 @@
function error(msg) { throw new Error(msg); } function error(msg) { throw new Error(msg); }
function inspect(x) { return JSON.stringify(x); } function inspect(x) { return JSON.stringify(x); }
function debugLog() { console.error.apply(console, ["[sx-debug]"].concat(Array.prototype.slice.call(arguments))); }
@@ -332,7 +356,7 @@
PRIMITIVES["index-of"] = function(s, needle, from) { return String(s).indexOf(needle, from || 0); }; PRIMITIVES["index-of"] = function(s, needle, from) { return String(s).indexOf(needle, from || 0); };
PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; }; PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; };
PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; }; PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; };
PRIMITIVES["slice"] = function(c, a, b) { return b !== undefined ? c.slice(a, b) : c.slice(a); }; PRIMITIVES["slice"] = function(c, a, b) { if (!c || typeof c.slice !== "function") { console.error("[sx-debug] slice called on non-sliceable:", typeof c, c, "a=", a, "b=", b, new Error().stack); return []; } return b !== undefined ? c.slice(a, b) : c.slice(a); };
PRIMITIVES["substring"] = function(s, a, b) { return String(s).substring(a, b); }; PRIMITIVES["substring"] = function(s, a, b) { return String(s).substring(a, b); };
PRIMITIVES["string-length"] = function(s) { return String(s).length; }; PRIMITIVES["string-length"] = function(s) { return String(s).length; };
PRIMITIVES["string-contains?"] = function(s, sub) { return String(s).indexOf(String(sub)) !== -1; }; PRIMITIVES["string-contains?"] = function(s, sub) { return String(s).indexOf(String(sub)) !== -1; };
@@ -359,7 +383,7 @@
PRIMITIVES["len"] = function(c) { return Array.isArray(c) ? c.length : typeof c === "string" ? c.length : Object.keys(c).length; }; PRIMITIVES["len"] = function(c) { return Array.isArray(c) ? c.length : typeof c === "string" ? c.length : Object.keys(c).length; };
PRIMITIVES["first"] = function(c) { return c && c.length > 0 ? c[0] : NIL; }; PRIMITIVES["first"] = function(c) { return c && c.length > 0 ? c[0] : NIL; };
PRIMITIVES["last"] = function(c) { return c && c.length > 0 ? c[c.length - 1] : NIL; }; PRIMITIVES["last"] = function(c) { return c && c.length > 0 ? c[c.length - 1] : NIL; };
PRIMITIVES["rest"] = function(c) { return c ? c.slice(1) : []; }; PRIMITIVES["rest"] = function(c) { if (c && typeof c.slice !== "function") { console.error("[sx-debug] rest called on non-sliceable:", typeof c, c, new Error().stack); return []; } return c ? c.slice(1) : []; };
PRIMITIVES["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; }; PRIMITIVES["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; };
PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); }; PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); };
PRIMITIVES["append"] = function(c, x) { return (c || []).concat([x]); }; PRIMITIVES["append"] = function(c, x) { return (c || []).concat([x]); };
@@ -696,7 +720,7 @@
// eval-expr // eval-expr
var evalExpr = function(expr, env) { return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if (_m == "string") return expr; if (_m == "boolean") return expr; if (_m == "nil") return NIL; if (_m == "symbol") return (function() { var evalExpr = function(expr, env) { return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if (_m == "string") return expr; if (_m == "boolean") return expr; if (_m == "nil") return NIL; if (_m == "symbol") return (function() {
var name = symbolName(expr); var name = symbolName(expr);
return (isSxTruthy(envHas(env, name)) ? envGet(env, name) : (isSxTruthy(isPrimitive(name)) ? getPrimitive(name) : (isSxTruthy((name == "true")) ? true : (isSxTruthy((name == "false")) ? false : (isSxTruthy((name == "nil")) ? NIL : error((String("Undefined symbol: ") + String(name)))))))); return (isSxTruthy(envHas(env, name)) ? envGet(env, name) : (isSxTruthy(isPrimitive(name)) ? getPrimitive(name) : (isSxTruthy((name == "true")) ? true : (isSxTruthy((name == "false")) ? false : (isSxTruthy((name == "nil")) ? NIL : (debugLog("Undefined symbol:", name, "primitive?:", isPrimitive(name)), error((String("Undefined symbol: ") + String(name)))))))));
})(); if (_m == "keyword") return keywordName(expr); if (_m == "dict") return mapDict(function(k, v) { return trampoline(evalExpr(v, env)); }, expr); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? [] : evalList(expr, env)); return expr; })(); }; })(); if (_m == "keyword") return keywordName(expr); if (_m == "dict") return mapDict(function(k, v) { return trampoline(evalExpr(v, env)); }, expr); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? [] : evalList(expr, env)); return expr; })(); };
// eval-list // eval-list
@@ -732,9 +756,9 @@
var kwargs = first(parsed); var kwargs = first(parsed);
var children = nth(parsed, 1); var children = nth(parsed, 1);
var local = envMerge(componentClosure(comp), env); var local = envMerge(componentClosure(comp), env);
{ var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = sxOr(dictGet(kwargs, p), NIL); } } { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, sxOr(dictGet(kwargs, p), NIL)); } }
if (isSxTruthy(componentHasChildren(comp))) { if (isSxTruthy(componentHasChildren(comp))) {
local["children"] = children; envSet(local, "children", children);
} }
return makeThunk(componentBody(comp), local); return makeThunk(componentBody(comp), local);
})(); }; })(); };
@@ -764,8 +788,11 @@
return (isSxTruthy((isSxTruthy(condition) && !isSxTruthy(isNil(condition)))) ? (forEach(function(e) { return trampoline(evalExpr(e, env)); }, slice(args, 1, (len(args) - 1))), makeThunk(last(args), env)) : NIL); return (isSxTruthy((isSxTruthy(condition) && !isSxTruthy(isNil(condition)))) ? (forEach(function(e) { return trampoline(evalExpr(e, env)); }, slice(args, 1, (len(args) - 1))), makeThunk(last(args), env)) : NIL);
})(); }; })(); };
// cond-scheme?
var condScheme_p = function(clauses) { return isEvery(function(c) { return (isSxTruthy((typeOf(c) == "list")) && (len(c) == 2)); }, clauses); };
// sf-cond // sf-cond
var sfCond = function(args, env) { return (isSxTruthy((isSxTruthy((typeOf(first(args)) == "list")) && (len(first(args)) == 2))) ? sfCondScheme(args, env) : sfCondClojure(args, env)); }; var sfCond = function(args, env) { return (isSxTruthy(condScheme_p(args)) ? sfCondScheme(args, env) : sfCondClojure(args, env)); };
// sf-cond-scheme // sf-cond-scheme
var sfCondScheme = function(clauses, env) { return (isSxTruthy(isEmpty(clauses)) ? NIL : (function() { var sfCondScheme = function(clauses, env) { return (isSxTruthy(isEmpty(clauses)) ? NIL : (function() {
@@ -841,7 +868,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var loopBody = (isSxTruthy((len(body) == 1)) ? first(body) : cons(makeSymbol("begin"), body)); var loopBody = (isSxTruthy((len(body) == 1)) ? first(body) : cons(makeSymbol("begin"), body));
var loopFn = makeLambda(params, loopBody, env); var loopFn = makeLambda(params, loopBody, env);
loopFn.name = loopName; loopFn.name = loopName;
lambdaClosure(loopFn)[loopName] = loopFn; envSet(lambdaClosure(loopFn), loopName, loopFn);
return (function() { return (function() {
var initVals = map(function(e) { return trampoline(evalExpr(e, env)); }, inits); var initVals = map(function(e) { return trampoline(evalExpr(e, env)); }, inits);
return callLambda(loopFn, initVals, env); return callLambda(loopFn, initVals, env);
@@ -854,7 +881,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var paramsExpr = first(args); var paramsExpr = first(args);
var bodyExprs = rest(args); var bodyExprs = rest(args);
var body = (isSxTruthy((len(bodyExprs) == 1)) ? first(bodyExprs) : cons(makeSymbol("begin"), bodyExprs)); var body = (isSxTruthy((len(bodyExprs) == 1)) ? first(bodyExprs) : cons(makeSymbol("begin"), bodyExprs));
var paramNames = map(function(p) { return (isSxTruthy((typeOf(p) == "symbol")) ? symbolName(p) : p); }, paramsExpr); var paramNames = map(function(p) { return (isSxTruthy((typeOf(p) == "symbol")) ? symbolName(p) : (isSxTruthy((isSxTruthy((typeOf(p) == "list")) && isSxTruthy((len(p) == 3)) && isSxTruthy((typeOf(nth(p, 1)) == "keyword")) && (keywordName(nth(p, 1)) == "as"))) ? symbolName(first(p)) : p)); }, paramsExpr);
return makeLambda(paramNames, body, env); return makeLambda(paramNames, body, env);
})(); }; })(); };
@@ -865,7 +892,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
if (isSxTruthy((isSxTruthy(isLambda(value)) && isNil(lambdaName(value))))) { if (isSxTruthy((isSxTruthy(isLambda(value)) && isNil(lambdaName(value))))) {
value.name = symbolName(nameSym); value.name = symbolName(nameSym);
} }
env[symbolName(nameSym)] = value; envSet(env, symbolName(nameSym), value);
return value; return value;
})(); }; })(); };
@@ -878,10 +905,14 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var parsed = parseCompParams(paramsRaw); var parsed = parseCompParams(paramsRaw);
var params = first(parsed); var params = first(parsed);
var hasChildren = nth(parsed, 1); var hasChildren = nth(parsed, 1);
var paramTypes = nth(parsed, 2);
var affinity = defcompKwarg(args, "affinity", "auto"); var affinity = defcompKwarg(args, "affinity", "auto");
return (function() { return (function() {
var comp = makeComponent(compName, params, hasChildren, body, env, affinity); var comp = makeComponent(compName, params, hasChildren, body, env, affinity);
env[symbolName(nameSym)] = comp; if (isSxTruthy((isSxTruthy(!isSxTruthy(isNil(paramTypes))) && !isSxTruthy(isEmpty(keys(paramTypes)))))) {
componentSetParamTypes_b(comp, paramTypes);
}
envSet(env, symbolName(nameSym), comp);
return comp; return comp;
})(); })();
})(); }; })(); };
@@ -902,15 +933,21 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
// parse-comp-params // parse-comp-params
var parseCompParams = function(paramsExpr) { return (function() { var parseCompParams = function(paramsExpr) { return (function() {
var params = []; var params = [];
var paramTypes = {};
var hasChildren = false; var hasChildren = false;
var inKey = false; var inKey = false;
{ var _c = paramsExpr; for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; if (isSxTruthy((typeOf(p) == "symbol"))) { { var _c = paramsExpr; for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; (isSxTruthy((isSxTruthy((typeOf(p) == "list")) && isSxTruthy((len(p) == 3)) && isSxTruthy((typeOf(first(p)) == "symbol")) && isSxTruthy((typeOf(nth(p, 1)) == "keyword")) && (keywordName(nth(p, 1)) == "as"))) ? (function() {
(function() { var name = symbolName(first(p));
var ptype = nth(p, 2);
return (function() {
var typeVal = (isSxTruthy((typeOf(ptype) == "symbol")) ? symbolName(ptype) : ptype);
return (isSxTruthy(!isSxTruthy(hasChildren)) ? (append_b(params, name), dictSet(paramTypes, name, typeVal)) : NIL);
})();
})() : (isSxTruthy((typeOf(p) == "symbol")) ? (function() {
var name = symbolName(p); var name = symbolName(p);
return (isSxTruthy((name == "&key")) ? (inKey = true) : (isSxTruthy((name == "&rest")) ? (hasChildren = true) : (isSxTruthy((name == "&children")) ? (hasChildren = true) : (isSxTruthy(hasChildren) ? NIL : (isSxTruthy(inKey) ? append_b(params, name) : append_b(params, name)))))); return (isSxTruthy((name == "&key")) ? (inKey = true) : (isSxTruthy((name == "&rest")) ? (hasChildren = true) : (isSxTruthy((name == "&children")) ? (hasChildren = true) : (isSxTruthy(hasChildren) ? NIL : (isSxTruthy(inKey) ? append_b(params, name) : append_b(params, name))))));
})(); })() : NIL)); } }
} } } return [params, hasChildren, paramTypes];
return [params, hasChildren];
})(); }; })(); };
// sf-defisland // sf-defisland
@@ -924,7 +961,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var hasChildren = nth(parsed, 1); var hasChildren = nth(parsed, 1);
return (function() { return (function() {
var island = makeIsland(compName, params, hasChildren, body, env); var island = makeIsland(compName, params, hasChildren, body, env);
env[symbolName(nameSym)] = island; envSet(env, symbolName(nameSym), island);
return island; return island;
})(); })();
})(); }; })(); };
@@ -939,7 +976,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var restParam = nth(parsed, 1); var restParam = nth(parsed, 1);
return (function() { return (function() {
var mac = makeMacro(params, restParam, body, env, symbolName(nameSym)); var mac = makeMacro(params, restParam, body, env, symbolName(nameSym));
env[symbolName(nameSym)] = mac; envSet(env, symbolName(nameSym), mac);
return mac; return mac;
})(); })();
})(); }; })(); };
@@ -956,7 +993,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var sfDefstyle = function(args, env) { return (function() { var sfDefstyle = function(args, env) { return (function() {
var nameSym = first(args); var nameSym = first(args);
var value = trampoline(evalExpr(nth(args, 1), env)); var value = trampoline(evalExpr(nth(args, 1), env));
env[symbolName(nameSym)] = value; envSet(env, symbolName(nameSym), value);
return value; return value;
})(); }; })(); };
@@ -974,8 +1011,8 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var head = first(template); var head = first(template);
return (isSxTruthy((isSxTruthy((typeOf(head) == "symbol")) && (symbolName(head) == "unquote"))) ? trampoline(evalExpr(nth(template, 1), env)) : reduce(function(result, item) { return (isSxTruthy((isSxTruthy((typeOf(item) == "list")) && isSxTruthy((len(item) == 2)) && isSxTruthy((typeOf(first(item)) == "symbol")) && (symbolName(first(item)) == "splice-unquote"))) ? (function() { return (isSxTruthy((isSxTruthy((typeOf(head) == "symbol")) && (symbolName(head) == "unquote"))) ? trampoline(evalExpr(nth(template, 1), env)) : reduce(function(result, item) { return (isSxTruthy((isSxTruthy((typeOf(item) == "list")) && isSxTruthy((len(item) == 2)) && isSxTruthy((typeOf(first(item)) == "symbol")) && (symbolName(first(item)) == "splice-unquote"))) ? (function() {
var spliced = trampoline(evalExpr(nth(item, 1), env)); var spliced = trampoline(evalExpr(nth(item, 1), env));
return (isSxTruthy((typeOf(spliced) == "list")) ? concat(result, spliced) : (isSxTruthy(isNil(spliced)) ? result : append(result, spliced))); return (isSxTruthy((typeOf(spliced) == "list")) ? concat(result, spliced) : (isSxTruthy(isNil(spliced)) ? result : concat(result, [spliced])));
})() : append(result, qqExpand(item, env))); }, [], template)); })() : concat(result, [qqExpand(item, env)])); }, [], template));
})())); }; })())); };
// sf-thread-first // sf-thread-first
@@ -996,7 +1033,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var sfSetBang = function(args, env) { return (function() { var sfSetBang = function(args, env) { return (function() {
var name = symbolName(first(args)); var name = symbolName(first(args));
var value = trampoline(evalExpr(nth(args, 1), env)); var value = trampoline(evalExpr(nth(args, 1), env));
env[name] = value; envSet(env, name, value);
return value; return value;
})(); }; })(); };
@@ -1021,7 +1058,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
})(); }, NIL, range(0, (len(bindings) / 2)))); })(); }, NIL, range(0, (len(bindings) / 2))));
(function() { (function() {
var values = map(function(e) { return trampoline(evalExpr(e, local)); }, valExprs); var values = map(function(e) { return trampoline(evalExpr(e, local)); }, valExprs);
{ var _c = zip(names, values); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; local[first(pair)] = nth(pair, 1); } } { var _c = zip(names, values); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; envSet(local, first(pair), nth(pair, 1)); } }
return forEach(function(val) { return (isSxTruthy(isLambda(val)) ? forEach(function(n) { return envSet(lambdaClosure(val), n, envGet(local, n)); }, names) : NIL); }, values); return forEach(function(val) { return (isSxTruthy(isLambda(val)) ? forEach(function(n) { return envSet(lambdaClosure(val), n, envGet(local, n)); }, names) : NIL); }, values);
})(); })();
{ var _c = slice(body, 0, (len(body) - 1)); for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; trampoline(evalExpr(e, local)); } } { var _c = slice(body, 0, (len(body) - 1)); for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; trampoline(evalExpr(e, local)); } }
@@ -1046,9 +1083,9 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
// expand-macro // expand-macro
var expandMacro = function(mac, rawArgs, env) { return (function() { var expandMacro = function(mac, rawArgs, env) { return (function() {
var local = envMerge(macroClosure(mac), env); var local = envMerge(macroClosure(mac), env);
{ var _c = mapIndexed(function(i, p) { return [p, i]; }, macroParams(mac)); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; local[first(pair)] = (isSxTruthy((nth(pair, 1) < len(rawArgs))) ? nth(rawArgs, nth(pair, 1)) : NIL); } } { var _c = mapIndexed(function(i, p) { return [p, i]; }, macroParams(mac)); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; envSet(local, first(pair), (isSxTruthy((nth(pair, 1) < len(rawArgs))) ? nth(rawArgs, nth(pair, 1)) : NIL)); } }
if (isSxTruthy(macroRestParam(mac))) { if (isSxTruthy(macroRestParam(mac))) {
local[macroRestParam(mac)] = slice(rawArgs, len(macroParams(mac))); envSet(local, macroRestParam(mac), slice(rawArgs, len(macroParams(mac))));
} }
return trampoline(evalExpr(macroBody(mac), local)); return trampoline(evalExpr(macroBody(mac), local));
})(); }; })(); };
@@ -1143,7 +1180,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
})(); }, keys(attrs))); }; })(); }, keys(attrs))); };
// eval-cond // eval-cond
var evalCond = function(clauses, env) { return (isSxTruthy((isSxTruthy(!isSxTruthy(isEmpty(clauses))) && isSxTruthy((typeOf(first(clauses)) == "list")) && (len(first(clauses)) == 2))) ? evalCondScheme(clauses, env) : evalCondClojure(clauses, env)); }; var evalCond = function(clauses, env) { return (isSxTruthy(condScheme_p(clauses)) ? evalCondScheme(clauses, env) : evalCondClojure(clauses, env)); };
// eval-cond-scheme // eval-cond-scheme
var evalCondScheme = function(clauses, env) { return (isSxTruthy(isEmpty(clauses)) ? NIL : (function() { var evalCondScheme = function(clauses, env) { return (isSxTruthy(isEmpty(clauses)) ? NIL : (function() {
@@ -1162,7 +1199,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
// process-bindings // process-bindings
var processBindings = function(bindings, env) { return (function() { var processBindings = function(bindings, env) { return (function() {
var local = merge(env); var local = envExtend(env);
{ var _c = bindings; for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; if (isSxTruthy((isSxTruthy((typeOf(pair) == "list")) && (len(pair) >= 2)))) { { var _c = bindings; for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; if (isSxTruthy((isSxTruthy((typeOf(pair) == "list")) && (len(pair) >= 2)))) {
(function() { (function() {
var name = (isSxTruthy((typeOf(first(pair)) == "symbol")) ? symbolName(first(pair)) : (String(first(pair)))); var name = (isSxTruthy((typeOf(first(pair)) == "symbol")) ? symbolName(first(pair)) : (String(first(pair))));
@@ -1392,9 +1429,9 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
})(); }, {["i"]: 0, ["skip"]: false}, args); })(); }, {["i"]: 0, ["skip"]: false}, args);
return (function() { return (function() {
var local = envMerge(componentClosure(comp), env); var local = envMerge(componentClosure(comp), env);
{ var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } } { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
if (isSxTruthy(componentHasChildren(comp))) { if (isSxTruthy(componentHasChildren(comp))) {
local["children"] = makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children))); envSet(local, "children", makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children))));
} }
return renderToHtml(componentBody(comp), local); return renderToHtml(componentBody(comp), local);
})(); })();
@@ -1458,20 +1495,20 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
return (function() { return (function() {
var local = envMerge(componentClosure(island), env); var local = envMerge(componentClosure(island), env);
var islandName = componentName(island); var islandName = componentName(island);
{ var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } } { var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
if (isSxTruthy(componentHasChildren(island))) { if (isSxTruthy(componentHasChildren(island))) {
local["children"] = makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children))); envSet(local, "children", makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children))));
} }
return (function() { return (function() {
var bodyHtml = renderToHtml(componentBody(island), local); var bodyHtml = renderToHtml(componentBody(island), local);
var stateJson = serializeIslandState(kwargs); var stateSx = serializeIslandState(kwargs);
return (String("<span data-sx-island=\"") + String(escapeAttr(islandName)) + String("\"") + String((isSxTruthy(stateJson) ? (String(" data-sx-state=\"") + String(escapeAttr(stateJson)) + String("\"")) : "")) + String(">") + String(bodyHtml) + String("</span>")); return (String("<span data-sx-island=\"") + String(escapeAttr(islandName)) + String("\"") + String((isSxTruthy(stateSx) ? (String(" data-sx-state=\"") + String(escapeAttr(stateSx)) + String("\"")) : "")) + String(">") + String(bodyHtml) + String("</span>"));
})(); })();
})(); })();
})(); }; })(); };
// serialize-island-state // serialize-island-state
var serializeIslandState = function(kwargs) { return (isSxTruthy(isEmptyDict(kwargs)) ? NIL : jsonSerialize(kwargs)); }; var serializeIslandState = function(kwargs) { return (isSxTruthy(isEmptyDict(kwargs)) ? NIL : sxSerialize(kwargs)); };
// === Transpiled from adapter-sx === // === Transpiled from adapter-sx ===
@@ -1505,30 +1542,34 @@ return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if
// aser-fragment // aser-fragment
var aserFragment = function(children, env) { return (function() { var aserFragment = function(children, env) { return (function() {
var parts = filter(function(x) { return !isSxTruthy(isNil(x)); }, map(function(c) { return aser(c, env); }, children)); var parts = [];
return (isSxTruthy(isEmpty(parts)) ? "" : (String("(<> ") + String(join(" ", map(serialize, parts))) + String(")"))); { var _c = children; for (var _i = 0; _i < _c.length; _i++) { var c = _c[_i]; (function() {
var result = aser(c, env);
return (isSxTruthy((typeOf(result) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? append_b(parts, serialize(item)) : NIL); }, result) : (isSxTruthy(!isSxTruthy(isNil(result))) ? append_b(parts, serialize(result)) : NIL));
})(); } }
return (isSxTruthy(isEmpty(parts)) ? "" : (String("(<> ") + String(join(" ", parts)) + String(")")));
})(); }; })(); };
// aser-call // aser-call
var aserCall = function(name, args, env) { return (function() { var aserCall = function(name, args, env) { return (function() {
var parts = [name]; var parts = [name];
reduce(function(state, arg) { return (function() { var skip = false;
var skip = get(state, "skip"); var i = 0;
return (isSxTruthy(skip) ? assoc(state, "skip", false, "i", (get(state, "i") + 1)) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() { { var _c = args; for (var _i = 0; _i < _c.length; _i++) { var arg = _c[_i]; (isSxTruthy(skip) ? ((skip = false), (i = (i + 1))) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((i + 1) < len(args)))) ? (function() {
var val = aser(nth(args, (get(state, "i") + 1)), env); var val = aser(nth(args, (i + 1)), env);
if (isSxTruthy(!isSxTruthy(isNil(val)))) { if (isSxTruthy(!isSxTruthy(isNil(val)))) {
parts.push((String(":") + String(keywordName(arg)))); parts.push((String(":") + String(keywordName(arg))));
parts.push(serialize(val)); parts.push(serialize(val));
} }
return assoc(state, "skip", true, "i", (get(state, "i") + 1)); skip = true;
return (i = (i + 1));
})() : (function() { })() : (function() {
var val = aser(arg, env); var val = aser(arg, env);
if (isSxTruthy(!isSxTruthy(isNil(val)))) { if (isSxTruthy(!isSxTruthy(isNil(val)))) {
parts.push(serialize(val)); (isSxTruthy((typeOf(val) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? append_b(parts, serialize(item)) : NIL); }, val) : append_b(parts, serialize(val)));
} }
return assoc(state, "i", (get(state, "i") + 1)); return (i = (i + 1));
})())); })())); } }
})(); }, {["i"]: 0, ["skip"]: false}, args);
return (String("(") + String(join(" ", parts)) + String(")")); return (String("(") + String(join(" ", parts)) + String(")"));
})(); }; })(); };
@@ -1582,7 +1623,7 @@ return result; }, args);
var coll = trampoline(evalExpr(nth(args, 1), env)); var coll = trampoline(evalExpr(nth(args, 1), env));
return map(function(item) { return (isSxTruthy(isLambda(f)) ? (function() { return map(function(item) { return (isSxTruthy(isLambda(f)) ? (function() {
var local = envMerge(lambdaClosure(f), env); var local = envMerge(lambdaClosure(f), env);
local[first(lambdaParams(f))] = item; envSet(local, first(lambdaParams(f)), item);
return aser(lambdaBody(f), local); return aser(lambdaBody(f), local);
})() : invoke(f, item)); }, coll); })() : invoke(f, item)); }, coll);
})() : (isSxTruthy((name == "map-indexed")) ? (function() { })() : (isSxTruthy((name == "map-indexed")) ? (function() {
@@ -1590,8 +1631,8 @@ return result; }, args);
var coll = trampoline(evalExpr(nth(args, 1), env)); var coll = trampoline(evalExpr(nth(args, 1), env));
return mapIndexed(function(i, item) { return (isSxTruthy(isLambda(f)) ? (function() { return mapIndexed(function(i, item) { return (isSxTruthy(isLambda(f)) ? (function() {
var local = envMerge(lambdaClosure(f), env); var local = envMerge(lambdaClosure(f), env);
local[first(lambdaParams(f))] = i; envSet(local, first(lambdaParams(f)), i);
local[nth(lambdaParams(f), 1)] = item; envSet(local, nth(lambdaParams(f), 1), item);
return aser(lambdaBody(f), local); return aser(lambdaBody(f), local);
})() : invoke(f, i, item)); }, coll); })() : invoke(f, i, item)); }, coll);
})() : (isSxTruthy((name == "for-each")) ? (function() { })() : (isSxTruthy((name == "for-each")) ? (function() {
@@ -1600,7 +1641,7 @@ return result; }, args);
var results = []; var results = [];
{ var _c = coll; for (var _i = 0; _i < _c.length; _i++) { var item = _c[_i]; (isSxTruthy(isLambda(f)) ? (function() { { var _c = coll; for (var _i = 0; _i < _c.length; _i++) { var item = _c[_i]; (isSxTruthy(isLambda(f)) ? (function() {
var local = envMerge(lambdaClosure(f), env); var local = envMerge(lambdaClosure(f), env);
local[first(lambdaParams(f))] = item; envSet(local, first(lambdaParams(f)), item);
return append_b(results, aser(lambdaBody(f), local)); return append_b(results, aser(lambdaBody(f), local));
})() : invoke(f, item)); } } })() : invoke(f, item)); } }
return (isSxTruthy(isEmpty(results)) ? NIL : results); return (isSxTruthy(isEmpty(results)) ? NIL : results);
@@ -1692,7 +1733,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
})(); }, {["i"]: 0, ["skip"]: false}, args); })(); }, {["i"]: 0, ["skip"]: false}, args);
return (function() { return (function() {
var local = envMerge(componentClosure(comp), env); var local = envMerge(componentClosure(comp), env);
{ var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } } { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
if (isSxTruthy(componentHasChildren(comp))) { if (isSxTruthy(componentHasChildren(comp))) {
(function() { (function() {
var childFrag = createFragment(); var childFrag = createFragment();
@@ -1883,7 +1924,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
return (function() { return (function() {
var local = envMerge(componentClosure(island), env); var local = envMerge(componentClosure(island), env);
var islandName = componentName(island); var islandName = componentName(island);
{ var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } } { var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
if (isSxTruthy(componentHasChildren(island))) { if (isSxTruthy(componentHasChildren(island))) {
(function() { (function() {
var childFrag = createFragment(); var childFrag = createFragment();
@@ -2998,9 +3039,9 @@ return postSwap(target); }))) : NIL);
var exprs = sxParse(body); var exprs = sxParse(body);
return domListen(el, eventName, function(e) { return (function() { return domListen(el, eventName, function(e) { return (function() {
var handlerEnv = envExtend({}); var handlerEnv = envExtend({});
handlerEnv["event"] = e; envSet(handlerEnv, "event", e);
handlerEnv["this"] = el; envSet(handlerEnv, "this", el);
handlerEnv["detail"] = eventDetail(e); envSet(handlerEnv, "detail", eventDetail(e));
return forEach(function(expr) { return evalExpr(expr, handlerEnv); }, exprs); return forEach(function(expr) { return evalExpr(expr, handlerEnv); }, exprs);
})(); }); })(); });
})()) : NIL); })()) : NIL);
@@ -3229,17 +3270,17 @@ callExpr.push(dictGet(kwargs, k)); } }
// hydrate-island // hydrate-island
var hydrateIsland = function(el) { return (function() { var hydrateIsland = function(el) { return (function() {
var name = domGetAttr(el, "data-sx-island"); var name = domGetAttr(el, "data-sx-island");
var stateJson = sxOr(domGetAttr(el, "data-sx-state"), "{}"); var stateSx = sxOr(domGetAttr(el, "data-sx-state"), "{}");
return (function() { return (function() {
var compName = (String("~") + String(name)); var compName = (String("~") + String(name));
var env = getRenderEnv(NIL); var env = getRenderEnv(NIL);
return (function() { return (function() {
var comp = envGet(env, compName); var comp = envGet(env, compName);
return (isSxTruthy(!isSxTruthy(sxOr(isComponent(comp), isIsland(comp)))) ? logWarn((String("hydrate-island: unknown island ") + String(compName))) : (function() { return (isSxTruthy(!isSxTruthy(sxOr(isComponent(comp), isIsland(comp)))) ? logWarn((String("hydrate-island: unknown island ") + String(compName))) : (function() {
var kwargs = jsonParse(stateJson); var kwargs = sxOr(first(sxParse(stateSx)), {});
var disposers = []; var disposers = [];
var local = envMerge(componentClosure(comp), env); var local = envMerge(componentClosure(comp), env);
{ var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } } { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
return (function() { return (function() {
var bodyDom = withIslandScope(function(disposable) { return append_b(disposers, disposable); }, function() { return renderToDom(componentBody(comp), local, NIL); }); var bodyDom = withIslandScope(function(disposable) { return append_b(disposers, disposable); }, function() { return renderToDom(componentBody(comp), local, NIL); });
domSetTextContent(el, ""); domSetTextContent(el, "");
@@ -3433,6 +3474,125 @@ callExpr.push(dictGet(kwargs, k)); } }
})(); }, keys(env)); }; })(); }, keys(env)); };
// === Transpiled from page-helpers (pure data transformation helpers) ===
// special-form-category-map
var specialFormCategoryMap = {"if": "Control Flow", "when": "Control Flow", "cond": "Control Flow", "case": "Control Flow", "and": "Control Flow", "or": "Control Flow", "let": "Binding", "let*": "Binding", "letrec": "Binding", "define": "Binding", "set!": "Binding", "lambda": "Functions & Components", "fn": "Functions & Components", "defcomp": "Functions & Components", "defmacro": "Functions & Components", "begin": "Sequencing & Threading", "do": "Sequencing & Threading", "->": "Sequencing & Threading", "quote": "Quoting", "quasiquote": "Quoting", "reset": "Continuations", "shift": "Continuations", "dynamic-wind": "Guards", "map": "Higher-Order Forms", "map-indexed": "Higher-Order Forms", "filter": "Higher-Order Forms", "reduce": "Higher-Order Forms", "some": "Higher-Order Forms", "every?": "Higher-Order Forms", "for-each": "Higher-Order Forms", "defstyle": "Domain Definitions", "defhandler": "Domain Definitions", "defpage": "Domain Definitions", "defquery": "Domain Definitions", "defaction": "Domain Definitions"};
// extract-define-kwargs
var extractDefineKwargs = function(expr) { return (function() {
var result = {};
var items = slice(expr, 2);
var n = len(items);
{ var _c = range(0, n); for (var _i = 0; _i < _c.length; _i++) { var idx = _c[_i]; if (isSxTruthy((isSxTruthy(((idx + 1) < n)) && (typeOf(nth(items, idx)) == "keyword")))) {
(function() {
var key = keywordName(nth(items, idx));
var val = nth(items, (idx + 1));
return dictSet(result, key, (isSxTruthy((typeOf(val) == "list")) ? (String("(") + String(join(" ", map(serialize, val))) + String(")")) : (String(val))));
})();
} } }
return result;
})(); };
// categorize-special-forms
var categorizeSpecialForms = function(parsedExprs) { return (function() {
var categories = {};
{ var _c = parsedExprs; for (var _i = 0; _i < _c.length; _i++) { var expr = _c[_i]; if (isSxTruthy((isSxTruthy((typeOf(expr) == "list")) && isSxTruthy((len(expr) >= 2)) && isSxTruthy((typeOf(first(expr)) == "symbol")) && (symbolName(first(expr)) == "define-special-form")))) {
(function() {
var name = nth(expr, 1);
var kwargs = extractDefineKwargs(expr);
var category = sxOr(get(specialFormCategoryMap, name), "Other");
if (isSxTruthy(!isSxTruthy(dictHas(categories, category)))) {
categories[category] = [];
}
return append_b(get(categories, category), {"name": name, "syntax": sxOr(get(kwargs, "syntax"), ""), "doc": sxOr(get(kwargs, "doc"), ""), "tail-position": sxOr(get(kwargs, "tail-position"), ""), "example": sxOr(get(kwargs, "example"), "")});
})();
} } }
return categories;
})(); };
// build-ref-items-with-href
var buildRefItemsWithHref = function(items, basePath, detailKeys, nFields) { return map(function(item) { return (isSxTruthy((nFields == 3)) ? (function() {
var name = nth(item, 0);
var field2 = nth(item, 1);
var field3 = nth(item, 2);
return {"name": name, "desc": field2, "exists": field3, "href": (isSxTruthy((isSxTruthy(field3) && some(function(k) { return (k == name); }, detailKeys))) ? (String(basePath) + String(name)) : NIL)};
})() : (function() {
var name = nth(item, 0);
var desc = nth(item, 1);
return {"name": name, "desc": desc, "href": (isSxTruthy(some(function(k) { return (k == name); }, detailKeys)) ? (String(basePath) + String(name)) : NIL)};
})()); }, items); };
// build-reference-data
var buildReferenceData = function(slug, rawData, detailKeys) { return (function() { var _m = slug; if (_m == "attributes") return {"req-attrs": buildRefItemsWithHref(get(rawData, "req-attrs"), "/geography/hypermedia/reference/attributes/", detailKeys, 3), "beh-attrs": buildRefItemsWithHref(get(rawData, "beh-attrs"), "/geography/hypermedia/reference/attributes/", detailKeys, 3), "uniq-attrs": buildRefItemsWithHref(get(rawData, "uniq-attrs"), "/geography/hypermedia/reference/attributes/", detailKeys, 3)}; if (_m == "headers") return {"req-headers": buildRefItemsWithHref(get(rawData, "req-headers"), "/geography/hypermedia/reference/headers/", detailKeys, 3), "resp-headers": buildRefItemsWithHref(get(rawData, "resp-headers"), "/geography/hypermedia/reference/headers/", detailKeys, 3)}; if (_m == "events") return {"events-list": buildRefItemsWithHref(get(rawData, "events-list"), "/geography/hypermedia/reference/events/", detailKeys, 2)}; if (_m == "js-api") return {"js-api-list": map(function(item) { return {"name": nth(item, 0), "desc": nth(item, 1)}; }, get(rawData, "js-api-list"))}; return {"req-attrs": buildRefItemsWithHref(get(rawData, "req-attrs"), "/geography/hypermedia/reference/attributes/", detailKeys, 3), "beh-attrs": buildRefItemsWithHref(get(rawData, "beh-attrs"), "/geography/hypermedia/reference/attributes/", detailKeys, 3), "uniq-attrs": buildRefItemsWithHref(get(rawData, "uniq-attrs"), "/geography/hypermedia/reference/attributes/", detailKeys, 3)}; })(); };
// build-attr-detail
var buildAttrDetail = function(slug, detail) { return (isSxTruthy(isNil(detail)) ? {"attr-not-found": true} : {"attr-not-found": NIL, "attr-title": slug, "attr-description": get(detail, "description"), "attr-example": get(detail, "example"), "attr-handler": get(detail, "handler"), "attr-demo": get(detail, "demo"), "attr-wire-id": (isSxTruthy(dictHas(detail, "handler")) ? (String("ref-wire-") + String(replace_(replace_(slug, ":", "-"), "*", "star"))) : NIL)}); };
// build-header-detail
var buildHeaderDetail = function(slug, detail) { return (isSxTruthy(isNil(detail)) ? {"header-not-found": true} : {"header-not-found": NIL, "header-title": slug, "header-direction": get(detail, "direction"), "header-description": get(detail, "description"), "header-example": get(detail, "example"), "header-demo": get(detail, "demo")}); };
// build-event-detail
var buildEventDetail = function(slug, detail) { return (isSxTruthy(isNil(detail)) ? {"event-not-found": true} : {"event-not-found": NIL, "event-title": slug, "event-description": get(detail, "description"), "event-example": get(detail, "example"), "event-demo": get(detail, "demo")}); };
// build-component-source
var buildComponentSource = function(compData) { return (function() {
var compType = get(compData, "type");
var name = get(compData, "name");
var params = get(compData, "params");
var hasChildren = get(compData, "has-children");
var bodySx = get(compData, "body-sx");
var affinity = get(compData, "affinity");
return (isSxTruthy((compType == "not-found")) ? (String(";; component ") + String(name) + String(" not found")) : (function() {
var paramStrs = (isSxTruthy(isEmpty(params)) ? (isSxTruthy(hasChildren) ? ["&rest", "children"] : []) : (isSxTruthy(hasChildren) ? append(cons("&key", params), ["&rest", "children"]) : cons("&key", params)));
var paramsSx = (String("(") + String(join(" ", paramStrs)) + String(")"));
var formName = (isSxTruthy((compType == "island")) ? "defisland" : "defcomp");
var affinityStr = (isSxTruthy((isSxTruthy((compType == "component")) && isSxTruthy(!isSxTruthy(isNil(affinity))) && !isSxTruthy((affinity == "auto")))) ? (String(" :affinity ") + String(affinity)) : "");
return (String("(") + String(formName) + String(" ") + String(name) + String(" ") + String(paramsSx) + String(affinityStr) + String("\n ") + String(bodySx) + String(")"));
})());
})(); };
// build-bundle-analysis
var buildBundleAnalysis = function(pagesRaw, componentsRaw, totalComponents, totalMacros, pureCount, ioCount) { return (function() {
var pagesData = [];
{ var _c = pagesRaw; for (var _i = 0; _i < _c.length; _i++) { var page = _c[_i]; (function() {
var neededNames = get(page, "needed-names");
var n = len(neededNames);
var pct = (isSxTruthy((totalComponents > 0)) ? round(((n / totalComponents) * 100)) : 0);
var savings = (100 - pct);
var pureInPage = 0;
var ioInPage = 0;
var pageIoRefs = [];
var compDetails = [];
{ var _c = neededNames; for (var _i = 0; _i < _c.length; _i++) { var compName = _c[_i]; (function() {
var info = get(componentsRaw, compName);
return (isSxTruthy(!isSxTruthy(isNil(info))) ? ((isSxTruthy(get(info, "is-pure")) ? (pureInPage = (pureInPage + 1)) : ((ioInPage = (ioInPage + 1)), forEach(function(ref) { return (isSxTruthy(!isSxTruthy(some(function(r) { return (r == ref); }, pageIoRefs))) ? append_b(pageIoRefs, ref) : NIL); }, sxOr(get(info, "io-refs"), [])))), append_b(compDetails, {"name": compName, "is-pure": get(info, "is-pure"), "affinity": get(info, "affinity"), "render-target": get(info, "render-target"), "io-refs": sxOr(get(info, "io-refs"), []), "deps": sxOr(get(info, "deps"), []), "source": get(info, "source")})) : NIL);
})(); } }
return append_b(pagesData, {"name": get(page, "name"), "path": get(page, "path"), "direct": get(page, "direct"), "needed": n, "pct": pct, "savings": savings, "io-refs": len(pageIoRefs), "pure-in-page": pureInPage, "io-in-page": ioInPage, "components": compDetails});
})(); } }
return {"pages": pagesData, "total-components": totalComponents, "total-macros": totalMacros, "pure-count": pureCount, "io-count": ioCount};
})(); };
// build-routing-analysis
var buildRoutingAnalysis = function(pagesRaw) { return (function() {
var pagesData = [];
var clientCount = 0;
var serverCount = 0;
{ var _c = pagesRaw; for (var _i = 0; _i < _c.length; _i++) { var page = _c[_i]; (function() {
var hasData = get(page, "has-data");
var contentSrc = sxOr(get(page, "content-src"), "");
var mode = NIL;
var reason = "";
(isSxTruthy(hasData) ? ((mode = "server"), (reason = "Has :data expression — needs server IO"), (serverCount = (serverCount + 1))) : (isSxTruthy(isEmpty(contentSrc)) ? ((mode = "server"), (reason = "No content expression"), (serverCount = (serverCount + 1))) : ((mode = "client"), (clientCount = (clientCount + 1)))));
return append_b(pagesData, {"name": get(page, "name"), "path": get(page, "path"), "mode": mode, "has-data": hasData, "content-expr": (isSxTruthy((len(contentSrc) > 80)) ? (String(slice(contentSrc, 0, 80)) + String("...")) : contentSrc), "reason": reason});
})(); } }
return {"pages": pagesData, "total-pages": (clientCount + serverCount), "client-count": clientCount, "server-count": serverCount};
})(); };
// build-affinity-analysis
var buildAffinityAnalysis = function(demoComponents, pagePlans) { return {"components": demoComponents, "page-plans": pagePlans}; };
// === Transpiled from router (client-side route matching) === // === Transpiled from router (client-side route matching) ===
// split-path-segments // split-path-segments
@@ -3835,6 +3995,26 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
function domGetProp(el, name) { return el ? el[name] : NIL; } function domGetProp(el, name) { return el ? el[name] : NIL; }
function domSetProp(el, name, val) { if (el) el[name] = val; } function domSetProp(el, name, val) { if (el) el[name] = val; }
// Call a method on an object with correct this binding: (dom-call-method obj "methodName" arg1 arg2 ...)
function domCallMethod() {
var obj = arguments[0], method = arguments[1];
var args = Array.prototype.slice.call(arguments, 2);
if (obj && typeof obj[method] === 'function') {
try { return obj[method].apply(obj, args); }
catch(e) { console.error("[sx] dom-call-method error:", e); return NIL; }
}
return NIL;
}
// Post a message to an iframe's contentWindow without exposing the cross-origin
// Window object to the SX evaluator (which would trigger _thunk access errors).
function domPostMessage(iframe, msg, origin) {
try {
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage(msg, origin || '*');
}
} catch(e) { console.error("[sx] domPostMessage error:", e); }
return NIL;
}
function domAddClass(el, cls) { function domAddClass(el, cls) {
if (el && el.classList) el.classList.add(cls); if (el && el.classList) el.classList.add(cls);
@@ -3853,8 +4033,11 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
function domListen(el, name, handler) { function domListen(el, name, handler) {
if (!_hasDom || !el) return function() {}; if (!_hasDom || !el) return function() {};
// Wrap SX lambdas from runtime-evaluated island code into native fns // Wrap SX lambdas from runtime-evaluated island code into native fns
// If lambda takes 0 params, call without event arg (convenience for on-click handlers)
var wrapped = isLambda(handler) var wrapped = isLambda(handler)
? function(e) { try { invoke(handler, e); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } } ? (lambdaParams(handler).length === 0
? function(e) { try { invoke(handler); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
: function(e) { try { invoke(handler, e); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } })
: handler; : handler;
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler)); if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
el.addEventListener(name, wrapped); el.addEventListener(name, wrapped);
@@ -3947,7 +4130,7 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
} }
} }
function nowMs() { return Date.now(); } function nowMs() { return (typeof performance !== "undefined") ? performance.now() : Date.now(); }
function parseHeaderValue(s) { function parseHeaderValue(s) {
if (!s) return null; if (!s) return null;
@@ -5036,6 +5219,9 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
PRIMITIVES["dom-focus"] = domFocus; PRIMITIVES["dom-focus"] = domFocus;
PRIMITIVES["dom-tag-name"] = domTagName; PRIMITIVES["dom-tag-name"] = domTagName;
PRIMITIVES["dom-get-prop"] = domGetProp; PRIMITIVES["dom-get-prop"] = domGetProp;
PRIMITIVES["dom-set-prop"] = domSetProp;
PRIMITIVES["dom-call-method"] = domCallMethod;
PRIMITIVES["dom-post-message"] = domPostMessage;
PRIMITIVES["stop-propagation"] = stopPropagation_; PRIMITIVES["stop-propagation"] = stopPropagation_;
PRIMITIVES["error-message"] = errorMessage; PRIMITIVES["error-message"] = errorMessage;
PRIMITIVES["schedule-idle"] = scheduleIdle; PRIMITIVES["schedule-idle"] = scheduleIdle;
@@ -5060,6 +5246,11 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
if (typeof elementValue === "function") PRIMITIVES["element-value"] = elementValue; if (typeof elementValue === "function") PRIMITIVES["element-value"] = elementValue;
if (typeof domOuterHtml === "function") PRIMITIVES["dom-outer-html"] = domOuterHtml; if (typeof domOuterHtml === "function") PRIMITIVES["dom-outer-html"] = domOuterHtml;
if (typeof domInnerHtml === "function") PRIMITIVES["dom-inner-html"] = domInnerHtml; if (typeof domInnerHtml === "function") PRIMITIVES["dom-inner-html"] = domInnerHtml;
if (typeof domTextContent === "function") PRIMITIVES["dom-text-content"] = domTextContent;
if (typeof jsonParse === "function") PRIMITIVES["json-parse"] = jsonParse;
if (typeof nowMs === "function") PRIMITIVES["now-ms"] = nowMs;
PRIMITIVES["sx-parse"] = sxParse;
PRIMITIVES["console-log"] = function() { console.log.apply(console, ["[sx]"].concat(Array.prototype.slice.call(arguments))); return arguments.length > 0 ? arguments[0] : NIL; };
// Expose deps module functions as primitives so runtime-evaluated SX code // Expose deps module functions as primitives so runtime-evaluated SX code
// (e.g. test-deps.sx in browser) can call them // (e.g. test-deps.sx in browser) can call them
@@ -5090,6 +5281,19 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
PRIMITIVES["render-target"] = renderTarget; PRIMITIVES["render-target"] = renderTarget;
PRIMITIVES["page-render-plan"] = pageRenderPlan; PRIMITIVES["page-render-plan"] = pageRenderPlan;
// Expose page-helper functions as primitives
PRIMITIVES["categorize-special-forms"] = categorizeSpecialForms;
PRIMITIVES["extract-define-kwargs"] = extractDefineKwargs;
PRIMITIVES["build-reference-data"] = buildReferenceData;
PRIMITIVES["build-ref-items-with-href"] = buildRefItemsWithHref;
PRIMITIVES["build-attr-detail"] = buildAttrDetail;
PRIMITIVES["build-header-detail"] = buildHeaderDetail;
PRIMITIVES["build-event-detail"] = buildEventDetail;
PRIMITIVES["build-component-source"] = buildComponentSource;
PRIMITIVES["build-bundle-analysis"] = buildBundleAnalysis;
PRIMITIVES["build-routing-analysis"] = buildRoutingAnalysis;
PRIMITIVES["build-affinity-analysis"] = buildAffinityAnalysis;
// ========================================================================= // =========================================================================
// Async IO: Promise-aware rendering for client-side IO primitives // Async IO: Promise-aware rendering for client-side IO primitives
// ========================================================================= // =========================================================================
@@ -5823,6 +6027,15 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
transitiveIoRefs: transitiveIoRefs, transitiveIoRefs: transitiveIoRefs,
computeAllIoRefs: computeAllIoRefs, computeAllIoRefs: computeAllIoRefs,
componentPure_p: componentPure_p, componentPure_p: componentPure_p,
categorizeSpecialForms: categorizeSpecialForms,
buildReferenceData: buildReferenceData,
buildAttrDetail: buildAttrDetail,
buildHeaderDetail: buildHeaderDetail,
buildEventDetail: buildEventDetail,
buildComponentSource: buildComponentSource,
buildBundleAnalysis: buildBundleAnalysis,
buildRoutingAnalysis: buildRoutingAnalysis,
buildAffinityAnalysis: buildAffinityAnalysis,
splitPathSegments: splitPathSegments, splitPathSegments: splitPathSegments,
parseRoutePattern: parseRoutePattern, parseRoutePattern: parseRoutePattern,
matchRoute: matchRoute, matchRoute: matchRoute,

View File

@@ -14,7 +14,7 @@
var IDB_NAME = "sx-offline"; var IDB_NAME = "sx-offline";
var IDB_VERSION = 1; var IDB_VERSION = 1;
var IDB_STORE = "responses"; var IDB_STORE = "responses";
var STATIC_CACHE = "sx-static-v1"; var STATIC_CACHE = "sx-static-v2";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// IndexedDB helpers // IndexedDB helpers

View File

@@ -31,11 +31,8 @@ from .parser import (
parse_all, parse_all,
serialize, serialize,
) )
from .evaluator import ( from .types import EvalError
EvalError, from .ref.sx_ref import evaluate, make_env
evaluate,
make_env,
)
from .primitives import ( from .primitives import (
all_primitives, all_primitives,

View File

@@ -53,7 +53,8 @@ from .types import Component, Island, Keyword, Lambda, Macro, NIL, Symbol
_expand_components: contextvars.ContextVar[bool] = contextvars.ContextVar( _expand_components: contextvars.ContextVar[bool] = contextvars.ContextVar(
"_expand_components", default=False "_expand_components", default=False
) )
from .evaluator import _expand_macro, EvalError from .ref.sx_ref import expand_macro as _expand_macro
from .types import EvalError
from .primitives import _PRIMITIVES from .primitives import _PRIMITIVES
from .primitives_io import IO_PRIMITIVES, RequestContext, execute_io from .primitives_io import IO_PRIMITIVES, RequestContext, execute_io
from .parser import SxExpr, serialize from .parser import SxExpr, serialize
@@ -420,23 +421,23 @@ async def _asf_define(expr, env, ctx):
async def _asf_defcomp(expr, env, ctx): async def _asf_defcomp(expr, env, ctx):
from .evaluator import _sf_defcomp from .ref.sx_ref import sf_defcomp
return _sf_defcomp(expr, env) return sf_defcomp(expr[1:], env)
async def _asf_defstyle(expr, env, ctx): async def _asf_defstyle(expr, env, ctx):
from .evaluator import _sf_defstyle from .ref.sx_ref import sf_defstyle
return _sf_defstyle(expr, env) return sf_defstyle(expr[1:], env)
async def _asf_defmacro(expr, env, ctx): async def _asf_defmacro(expr, env, ctx):
from .evaluator import _sf_defmacro from .ref.sx_ref import sf_defmacro
return _sf_defmacro(expr, env) return sf_defmacro(expr[1:], env)
async def _asf_defhandler(expr, env, ctx): async def _asf_defhandler(expr, env, ctx):
from .evaluator import _sf_defhandler from .ref.sx_ref import sf_defhandler
return _sf_defhandler(expr, env) return sf_defhandler(expr[1:], env)
async def _asf_begin(expr, env, ctx): async def _asf_begin(expr, env, ctx):
@@ -599,7 +600,7 @@ async def _asf_reset(expr, env, ctx):
_ASYNC_RESET_RESUME.append(value if value is not None else NIL) _ASYNC_RESET_RESUME.append(value if value is not None else NIL)
try: try:
# Sync re-evaluation; the async caller will trampoline # Sync re-evaluation; the async caller will trampoline
from .evaluator import _eval as sync_eval, _trampoline from .ref.sx_ref import eval_expr as sync_eval, trampoline as _trampoline
return _trampoline(sync_eval(body, env)) return _trampoline(sync_eval(body, env))
finally: finally:
_ASYNC_RESET_RESUME.pop() _ASYNC_RESET_RESUME.pop()

View File

@@ -1,94 +0,0 @@
"""
S-expression evaluator — thin shim over bootstrapped sx_ref.py.
All evaluation logic lives in the spec (shared/sx/ref/eval.sx) and is
bootstrapped to Python (shared/sx/ref/sx_ref.py). This module re-exports
the public API and internal helpers under their historical names so that
existing callers don't need updating.
Imports are lazy (inside functions/properties) to avoid circular imports
during bootstrapping: bootstrap_py.py → parser → __init__ → evaluator → sx_ref.
"""
from __future__ import annotations
def _ref():
"""Lazy import of the bootstrapped evaluator."""
from .ref import sx_ref
return sx_ref
# ---------------------------------------------------------------------------
# Public API — these are the most used, so we make them importable directly
# ---------------------------------------------------------------------------
class EvalError(Exception):
"""Error during expression evaluation.
Delegates to the bootstrapped EvalError at runtime but is defined here
so imports don't fail during bootstrapping.
"""
pass
def evaluate(expr, env=None):
return _ref().evaluate(expr, env)
def make_env(**kwargs):
return _ref().make_env(**kwargs)
# ---------------------------------------------------------------------------
# Internal helpers — used by html.py, async_eval.py, handlers.py, etc.
# ---------------------------------------------------------------------------
def _eval(expr, env):
return _ref().eval_expr(expr, env)
def _trampoline(val):
return _ref().trampoline(val)
def _call_lambda(fn, args, caller_env):
return _ref().call_lambda(fn, args, caller_env)
def _call_component(comp, raw_args, env):
return _ref().call_component(comp, raw_args, env)
def _expand_macro(macro, raw_args, env):
return _ref().expand_macro(macro, raw_args, env)
# ---------------------------------------------------------------------------
# Special-form wrappers: callers pass (expr, env) with expr[0] = head symbol.
# sx_ref.py special forms take (args, env) where args = expr[1:].
# ---------------------------------------------------------------------------
def _sf_defcomp(expr, env):
return _ref().sf_defcomp(expr[1:], env)
def _sf_defisland(expr, env):
return _ref().sf_defisland(expr[1:], env)
def _sf_defstyle(expr, env):
return _ref().sf_defstyle(expr[1:], env)
def _sf_defmacro(expr, env):
return _ref().sf_defmacro(expr[1:], env)
def _sf_defhandler(expr, env):
return _ref().sf_defhandler(expr[1:], env)
def _sf_defpage(expr, env):
return _ref().sf_defpage(expr[1:], env)
def _sf_defquery(expr, env):
return _ref().sf_defquery(expr[1:], env)
def _sf_defaction(expr, env):
return _ref().sf_defaction(expr[1:], env)

View File

@@ -70,10 +70,7 @@ def load_handler_file(filepath: str, service_name: str) -> list[HandlerDef]:
"""Parse an .sx file, evaluate it, and register any HandlerDef values.""" """Parse an .sx file, evaluate it, and register any HandlerDef values."""
from .parser import parse_all from .parser import parse_all
import os import os
if os.environ.get("SX_USE_REF") == "1": from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
else:
from .evaluator import _eval as _raw_eval, _trampoline
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env)) _eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
from .jinja_bridge import get_component_env from .jinja_bridge import get_component_env

View File

@@ -28,7 +28,7 @@ import contextvars
from typing import Any from typing import Any
from .types import Component, Island, Keyword, Lambda, Macro, NIL, Symbol from .types import Component, Island, Keyword, Lambda, Macro, NIL, Symbol
from .evaluator import _eval as _raw_eval, _call_component as _raw_call_component, _expand_macro, _trampoline from .ref.sx_ref import eval_expr as _raw_eval, call_component as _raw_call_component, expand_macro as _expand_macro, trampoline as _trampoline
def _eval(expr, env): def _eval(expr, env):
"""Evaluate and unwrap thunks — all html.py _eval calls are non-tail.""" """Evaluate and unwrap thunks — all html.py _eval calls are non-tail."""
@@ -414,10 +414,10 @@ def _render_component(comp: Component, args: list, env: dict[str, Any]) -> str:
def _render_island(island: Island, args: list, env: dict[str, Any]) -> str: def _render_island(island: Island, args: list, env: dict[str, Any]) -> str:
"""Render an island as static HTML with hydration attributes. """Render an island as static HTML with hydration attributes.
Produces: <span data-sx-island="name" data-sx-state='{"k":"v",...}'>body HTML</span> Produces: <span data-sx-island="name" data-sx-state="{:k &quot;v&quot;}">body HTML</span>
The client hydrates this into a reactive island. The client hydrates this into a reactive island via sx-parse (not JSON).
""" """
import json as _json from .parser import serialize as _sx_serialize
kwargs: dict[str, Any] = {} kwargs: dict[str, Any] = {}
children: list[Any] = [] children: list[Any] = []
@@ -443,26 +443,13 @@ def _render_island(island: Island, args: list, env: dict[str, Any]) -> str:
body_html = _render(island.body, local) body_html = _render(island.body, local)
# Serialize state for hydration — only keyword args # Serialize state for hydration — SX format (not JSON)
state = {} state_sx = _escape_attr(_sx_serialize(kwargs)) if kwargs else ""
for k, v in kwargs.items():
if isinstance(v, (str, int, float, bool)):
state[k] = v
elif v is NIL or v is None:
state[k] = None
elif isinstance(v, list):
state[k] = v
elif isinstance(v, dict):
state[k] = v
else:
state[k] = str(v)
state_json = _escape_attr(_json.dumps(state, separators=(",", ":"))) if state else ""
island_name = _escape_attr(island.name) island_name = _escape_attr(island.name)
parts = [f'<span data-sx-island="{island_name}"'] parts = [f'<span data-sx-island="{island_name}"']
if state_json: if state_sx:
parts.append(f' data-sx-state="{state_json}"') parts.append(f' data-sx-state="{state_sx}"')
parts.append(">") parts.append(">")
parts.append(body_html) parts.append(body_html)
parts.append("</span>") parts.append("</span>")

View File

@@ -229,10 +229,7 @@ def register_components(sx_source: str) -> None:
(div :class "..." (div :class "..." title))))) (div :class "..." (div :class "..." title)))))
''') ''')
""" """
if _os.environ.get("SX_USE_REF") == "1": from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
else:
from .evaluator import _eval as _raw_eval, _trampoline
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env)) _eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
from .parser import parse_all from .parser import parse_all
from .css_registry import scan_classes_from_sx from .css_registry import scan_classes_from_sx

View File

@@ -76,7 +76,7 @@ def register_page_helpers(service: str, helpers: dict[str, Any]) -> None:
Then in .sx:: Then in .sx::
(defpage docs-page (defpage docs-page
:path "/docs/<slug>" :path "/language/docs/<slug>"
:auth :public :auth :public
:content (docs-content slug)) :content (docs-content slug))
""" """
@@ -127,7 +127,7 @@ def get_page_helpers(service: str) -> dict[str, Any]:
def load_page_file(filepath: str, service_name: str) -> list[PageDef]: def load_page_file(filepath: str, service_name: str) -> list[PageDef]:
"""Parse an .sx file, evaluate it, and register any PageDef values.""" """Parse an .sx file, evaluate it, and register any PageDef values."""
from .parser import parse_all from .parser import parse_all
from .evaluator import _eval as _raw_eval, _trampoline from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env)) _eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
from .jinja_bridge import get_component_env from .jinja_bridge import get_component_env
@@ -170,7 +170,11 @@ async def _eval_slot(expr: Any, env: dict, ctx: Any) -> str:
Expands component calls (so IO in the body executes) but serializes Expands component calls (so IO in the body executes) but serializes
the result as SX wire format, not HTML. the result as SX wire format, not HTML.
""" """
from .async_eval import async_eval_slot_to_sx import os
if os.environ.get("SX_USE_REF") == "1":
from .ref.async_eval_ref import async_eval_slot_to_sx
else:
from .async_eval import async_eval_slot_to_sx
return await async_eval_slot_to_sx(expr, env, ctx) return await async_eval_slot_to_sx(expr, env, ctx)

View File

@@ -41,7 +41,7 @@ def _resolve_sx_reader_macro(name: str):
""" """
try: try:
from .jinja_bridge import get_component_env from .jinja_bridge import get_component_env
from .evaluator import _trampoline, _call_lambda from .ref.sx_ref import trampoline as _trampoline, call_lambda as _call_lambda
from .types import Lambda from .types import Lambda
except ImportError: except ImportError:
return None return None

View File

@@ -78,7 +78,7 @@ def clear(service: str | None = None) -> None:
def load_query_file(filepath: str, service_name: str) -> list[QueryDef]: def load_query_file(filepath: str, service_name: str) -> list[QueryDef]:
"""Parse an .sx file and register any defquery definitions.""" """Parse an .sx file and register any defquery definitions."""
from .parser import parse_all from .parser import parse_all
from .evaluator import _eval as _raw_eval, _trampoline from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env)) _eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
from .jinja_bridge import get_component_env from .jinja_bridge import get_component_env
@@ -103,7 +103,7 @@ def load_query_file(filepath: str, service_name: str) -> list[QueryDef]:
def load_action_file(filepath: str, service_name: str) -> list[ActionDef]: def load_action_file(filepath: str, service_name: str) -> list[ActionDef]:
"""Parse an .sx file and register any defaction definitions.""" """Parse an .sx file and register any defaction definitions."""
from .parser import parse_all from .parser import parse_all
from .evaluator import _eval as _raw_eval, _trampoline from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env)) _eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
from .jinja_bridge import get_component_env from .jinja_bridge import get_component_env

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-to-dom (define render-to-dom
(fn (expr env ns) (fn (expr (env :as dict) (ns :as string))
(set-render-active! true) (set-render-active! true)
(case (type-of expr) (case (type-of expr)
;; nil / boolean false / boolean true → empty fragment ;; nil / boolean false / boolean true → empty fragment
@@ -67,7 +67,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-dom-list (define render-dom-list
(fn (expr env ns) (fn (expr (env :as dict) (ns :as string))
(let ((head (first expr))) (let ((head (first expr)))
(cond (cond
;; Symbol head — dispatch on name ;; Symbol head — dispatch on name
@@ -166,7 +166,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-dom-element (define render-dom-element
(fn (tag args env ns) (fn ((tag :as string) (args :as list) (env :as dict) (ns :as string))
;; Detect namespace from tag ;; Detect namespace from tag
(let ((new-ns (cond (= tag "svg") SVG_NS (let ((new-ns (cond (= tag "svg") SVG_NS
(= tag "math") MATH_NS (= tag "math") MATH_NS
@@ -237,7 +237,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-dom-component (define render-dom-component
(fn (comp args env ns) (fn ((comp :as component) (args :as list) (env :as dict) (ns :as string))
;; Parse kwargs and children, bind into component env, render body. ;; Parse kwargs and children, bind into component env, render body.
(let ((kwargs (dict)) (let ((kwargs (dict))
(children (list))) (children (list)))
@@ -284,7 +284,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-dom-fragment (define render-dom-fragment
(fn (args env ns) (fn ((args :as list) (env :as dict) (ns :as string))
(let ((frag (create-fragment))) (let ((frag (create-fragment)))
(for-each (for-each
(fn (x) (dom-append frag (render-to-dom x env ns))) (fn (x) (dom-append frag (render-to-dom x env ns)))
@@ -297,7 +297,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-dom-raw (define render-dom-raw
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((frag (create-fragment))) (let ((frag (create-fragment)))
(for-each (for-each
(fn (arg) (fn (arg)
@@ -318,7 +318,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-dom-unknown-component (define render-dom-unknown-component
(fn (name) (fn ((name :as string))
(error (str "Unknown component: " name)))) (error (str "Unknown component: " name))))
@@ -335,11 +335,11 @@
"error-boundary")) "error-boundary"))
(define render-dom-form? (define render-dom-form?
(fn (name) (fn ((name :as string))
(contains? RENDER_DOM_FORMS name))) (contains? RENDER_DOM_FORMS name)))
(define dispatch-render-form (define dispatch-render-form
(fn (name expr env ns) (fn ((name :as string) expr (env :as dict) (ns :as string))
(cond (cond
;; if — reactive inside islands (re-renders when signal deps change) ;; if — reactive inside islands (re-renders when signal deps change)
(= name "if") (= name "if")
@@ -581,7 +581,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-lambda-dom (define render-lambda-dom
(fn (f args env ns) (fn ((f :as lambda) (args :as list) (env :as dict) (ns :as string))
;; Bind lambda params and render body as DOM ;; Bind lambda params and render body as DOM
(let ((local (env-merge (lambda-closure f) env))) (let ((local (env-merge (lambda-closure f) env)))
(for-each-indexed (for-each-indexed
@@ -605,7 +605,7 @@
;; - Conditional fragments: (when (deref sig) ...) → reactive show/hide ;; - Conditional fragments: (when (deref sig) ...) → reactive show/hide
(define render-dom-island (define render-dom-island
(fn (island args env ns) (fn ((island :as island) (args :as list) (env :as dict) (ns :as string))
;; Parse kwargs and children (same as component) ;; Parse kwargs and children (same as component)
(let ((kwargs (dict)) (let ((kwargs (dict))
(children (list))) (children (list)))
@@ -679,7 +679,7 @@
;; Supports :tag keyword to change wrapper element (default "div"). ;; Supports :tag keyword to change wrapper element (default "div").
(define render-dom-lake (define render-dom-lake
(fn (args env ns) (fn ((args :as list) (env :as dict) (ns :as string))
(let ((lake-id nil) (let ((lake-id nil)
(lake-tag "div") (lake-tag "div")
(children (list))) (children (list)))
@@ -723,7 +723,7 @@
;; Stores the island env and transform on the element for morph retrieval. ;; Stores the island env and transform on the element for morph retrieval.
(define render-dom-marsh (define render-dom-marsh
(fn (args env ns) (fn ((args :as list) (env :as dict) (ns :as string))
(let ((marsh-id nil) (let ((marsh-id nil)
(marsh-tag "div") (marsh-tag "div")
(marsh-transform nil) (marsh-transform nil)
@@ -781,7 +781,7 @@
;; Marks the attribute name on the element via data-sx-reactive-attrs so ;; Marks the attribute name on the element via data-sx-reactive-attrs so
;; the morph algorithm knows not to overwrite it with server content. ;; the morph algorithm knows not to overwrite it with server content.
(define reactive-attr (define reactive-attr
(fn (el attr-name compute-fn) (fn (el (attr-name :as string) (compute-fn :as lambda))
;; Mark this attribute as reactively managed ;; Mark this attribute as reactively managed
(let ((existing (or (dom-get-attr el "data-sx-reactive-attrs") "")) (let ((existing (or (dom-get-attr el "data-sx-reactive-attrs") ""))
(updated (if (empty? existing) attr-name (str existing "," attr-name)))) (updated (if (empty? existing) attr-name (str existing "," attr-name))))
@@ -802,7 +802,7 @@
;; reactive-fragment — conditionally render a fragment based on a signal ;; reactive-fragment — conditionally render a fragment based on a signal
;; Used for (when (deref sig) ...) or (if (deref sig) ...) inside an island. ;; Used for (when (deref sig) ...) or (if (deref sig) ...) inside an island.
(define reactive-fragment (define reactive-fragment
(fn (test-fn render-fn env ns) (fn ((test-fn :as lambda) (render-fn :as lambda) (env :as dict) (ns :as string))
(let ((marker (create-comment "island-fragment")) (let ((marker (create-comment "island-fragment"))
(current-nodes (list))) (current-nodes (list)))
(effect (fn () (effect (fn ()
@@ -824,13 +824,13 @@
;; and reorderings touch the DOM. Without keys, falls back to clear+rerender. ;; and reorderings touch the DOM. Without keys, falls back to clear+rerender.
(define render-list-item (define render-list-item
(fn (map-fn item env ns) (fn ((map-fn :as lambda) item (env :as dict) (ns :as string))
(if (lambda? map-fn) (if (lambda? map-fn)
(render-lambda-dom map-fn (list item) env ns) (render-lambda-dom map-fn (list item) env ns)
(render-to-dom (apply map-fn (list item)) env ns)))) (render-to-dom (apply map-fn (list item)) env ns))))
(define extract-key (define extract-key
(fn (node index) (fn (node (index :as number))
;; Extract key from rendered node: :key attr, data-key, or index fallback ;; Extract key from rendered node: :key attr, data-key, or index fallback
(let ((k (dom-get-attr node "key"))) (let ((k (dom-get-attr node "key")))
(if k (if k
@@ -839,7 +839,7 @@
(if dk (str dk) (str "__idx_" index))))))) (if dk (str dk) (str "__idx_" index)))))))
(define reactive-list (define reactive-list
(fn (map-fn items-sig env ns) (fn ((map-fn :as lambda) (items-sig :as signal) (env :as dict) (ns :as string))
(let ((container (create-fragment)) (let ((container (create-fragment))
(marker (create-comment "island-list")) (marker (create-comment "island-list"))
(key-map (dict)) (key-map (dict))
@@ -925,7 +925,7 @@
;; Handles: input[text/number/email/...], textarea, select, checkbox, radio ;; Handles: input[text/number/email/...], textarea, select, checkbox, radio
(define bind-input (define bind-input
(fn (el sig) (fn (el (sig :as signal))
(let ((input-type (lower (or (dom-get-attr el "type") ""))) (let ((input-type (lower (or (dom-get-attr el "type") "")))
(is-checkbox (or (= input-type "checkbox") (is-checkbox (or (= input-type "checkbox")
(= input-type "radio")))) (= input-type "radio"))))
@@ -960,7 +960,7 @@
;; teardown. ;; teardown.
(define render-dom-portal (define render-dom-portal
(fn (args env ns) (fn ((args :as list) (env :as dict) (ns :as string))
(let ((selector (trampoline (eval-expr (first args) env))) (let ((selector (trampoline (eval-expr (first args) env)))
(target (or (dom-query selector) (target (or (dom-query selector)
(dom-ensure-element selector)))) (dom-ensure-element selector))))
@@ -1000,7 +1000,7 @@
;; Calling (retry) re-renders the body, replacing the fallback. ;; Calling (retry) re-renders the body, replacing the fallback.
(define render-dom-error-boundary (define render-dom-error-boundary
(fn (args env ns) (fn ((args :as list) (env :as dict) (ns :as string))
(let ((fallback-expr (first args)) (let ((fallback-expr (first args))
(body-exprs (rest args)) (body-exprs (rest args))
(container (dom-create-element "div" nil)) (container (dom-create-element "div" nil))

View File

@@ -14,7 +14,7 @@
(define render-to-html (define render-to-html
(fn (expr env) (fn (expr (env :as dict))
(set-render-active! true) (set-render-active! true)
(case (type-of expr) (case (type-of expr)
;; Literals — render directly ;; Literals — render directly
@@ -34,7 +34,7 @@
:else (render-value-to-html (trampoline (eval-expr expr env)) env)))) :else (render-value-to-html (trampoline (eval-expr expr env)) env))))
(define render-value-to-html (define render-value-to-html
(fn (val env) (fn (val (env :as dict))
(case (type-of val) (case (type-of val)
"nil" "" "nil" ""
"string" (escape-html val) "string" (escape-html val)
@@ -55,7 +55,7 @@
"map" "map-indexed" "filter" "for-each")) "map" "map-indexed" "filter" "for-each"))
(define render-html-form? (define render-html-form?
(fn (name) (fn ((name :as string))
(contains? RENDER_HTML_FORMS name))) (contains? RENDER_HTML_FORMS name)))
@@ -64,7 +64,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-list-to-html (define render-list-to-html
(fn (expr env) (fn ((expr :as list) (env :as dict))
(if (empty? expr) (if (empty? expr)
"" ""
(let ((head (first expr))) (let ((head (first expr)))
@@ -135,7 +135,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define dispatch-html-form (define dispatch-html-form
(fn (name expr env) (fn ((name :as string) (expr :as list) (env :as dict))
(cond (cond
;; if ;; if
(= name "if") (= name "if")
@@ -235,7 +235,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-lambda-html (define render-lambda-html
(fn (f args env) (fn ((f :as lambda) (args :as list) (env :as dict))
(let ((local (env-merge (lambda-closure f) env))) (let ((local (env-merge (lambda-closure f) env)))
(for-each-indexed (for-each-indexed
(fn (i p) (fn (i p)
@@ -249,7 +249,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-html-component (define render-html-component
(fn (comp args env) (fn ((comp :as component) (args :as list) (env :as dict))
;; Expand component and render body through HTML adapter. ;; Expand component and render body through HTML adapter.
;; Component body contains rendering forms (HTML tags) that only the ;; Component body contains rendering forms (HTML tags) that only the
;; adapter understands, so expansion must happen here, not in eval-expr. ;; adapter understands, so expansion must happen here, not in eval-expr.
@@ -288,7 +288,7 @@
(define render-html-element (define render-html-element
(fn (tag args env) (fn ((tag :as string) (args :as list) (env :as dict))
(let ((parsed (parse-element-args args env)) (let ((parsed (parse-element-args args env))
(attrs (first parsed)) (attrs (first parsed))
(children (nth parsed 1)) (children (nth parsed 1))
@@ -312,7 +312,7 @@
;; content while preserving surrounding reactive DOM. ;; content while preserving surrounding reactive DOM.
(define render-html-lake (define render-html-lake
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((lake-id nil) (let ((lake-id nil)
(lake-tag "div") (lake-tag "div")
(children (list))) (children (list)))
@@ -351,7 +351,7 @@
;; the :transform is a client-only concern. ;; the :transform is a client-only concern.
(define render-html-marsh (define render-html-marsh
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((marsh-id nil) (let ((marsh-id nil)
(marsh-tag "div") (marsh-tag "div")
(children (list))) (children (list)))
@@ -394,7 +394,7 @@
;; (swap! s f) → no-op ;; (swap! s f) → no-op
(define render-html-island (define render-html-island
(fn (island args env) (fn ((island :as island) (args :as list) (env :as dict))
;; Parse kwargs and children (same pattern as render-html-component) ;; Parse kwargs and children (same pattern as render-html-component)
(let ((kwargs (dict)) (let ((kwargs (dict))
(children (list))) (children (list)))
@@ -433,11 +433,11 @@
;; Render the island body as HTML ;; Render the island body as HTML
(let ((body-html (render-to-html (component-body island) local)) (let ((body-html (render-to-html (component-body island) local))
(state-json (serialize-island-state kwargs))) (state-sx (serialize-island-state kwargs)))
;; Wrap in container with hydration attributes ;; Wrap in container with hydration attributes
(str "<span data-sx-island=\"" (escape-attr island-name) "\"" (str "<span data-sx-island=\"" (escape-attr island-name) "\""
(if state-json (if state-sx
(str " data-sx-state=\"" (escape-attr state-json) "\"") (str " data-sx-state=\"" (escape-attr state-sx) "\"")
"") "")
">" ">"
body-html body-html
@@ -445,17 +445,17 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
;; serialize-island-state — serialize kwargs to JSON for hydration ;; serialize-island-state — serialize kwargs to SX for hydration
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
;; ;;
;; Only serializes simple values (numbers, strings, booleans, nil, lists, dicts). ;; Uses the SX serializer (not JSON) so the client can parse with sx-parse.
;; Functions, components, and other non-serializable values are skipped. ;; Handles all SX types natively: numbers, strings, booleans, nil, lists, dicts.
(define serialize-island-state (define serialize-island-state
(fn (kwargs) (fn ((kwargs :as dict))
(if (empty-dict? kwargs) (if (empty-dict? kwargs)
nil nil
(json-serialize kwargs)))) (sx-serialize kwargs))))
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
@@ -476,8 +476,8 @@
;; Raw HTML construction: ;; Raw HTML construction:
;; (make-raw-html s) → wrap string as raw HTML (not double-escaped) ;; (make-raw-html s) → wrap string as raw HTML (not double-escaped)
;; ;;
;; JSON serialization (for island state): ;; Island state serialization:
;; (json-serialize dict) → JSON string ;; (sx-serialize val) → SX source string (from parser.sx)
;; (empty-dict? d) → boolean ;; (empty-dict? d) → boolean
;; (escape-attr s) → HTML attribute escape ;; (escape-attr s) → HTML attribute escape
;; ;;

View File

@@ -12,7 +12,7 @@
(define render-to-sx (define render-to-sx
(fn (expr env) (fn (expr (env :as dict))
(let ((result (aser expr env))) (let ((result (aser expr env)))
;; aser-call already returns serialized SX strings; ;; aser-call already returns serialized SX strings;
;; only serialize non-string values ;; only serialize non-string values
@@ -21,7 +21,7 @@
(serialize result))))) (serialize result)))))
(define aser (define aser
(fn (expr env) (fn (expr (env :as dict))
;; Evaluate for SX wire format — serialize rendering forms, ;; Evaluate for SX wire format — serialize rendering forms,
;; evaluate control flow and function calls. ;; evaluate control flow and function calls.
(set-render-active! true) (set-render-active! true)
@@ -52,7 +52,7 @@
(define aser-list (define aser-list
(fn (expr env) (fn ((expr :as list) (env :as dict))
(let ((head (first expr)) (let ((head (first expr))
(args (rest expr))) (args (rest expr)))
(if (not (= (type-of head) "symbol")) (if (not (= (type-of head) "symbol"))
@@ -104,37 +104,58 @@
(define aser-fragment (define aser-fragment
(fn (children env) (fn ((children :as list) (env :as dict))
;; Serialize (<> child1 child2 ...) to sx source string ;; Serialize (<> child1 child2 ...) to sx source string
(let ((parts (filter ;; Must flatten list results (e.g. from map/filter) to avoid nested parens
(fn (x) (not (nil? x))) (let ((parts (list)))
(map (fn (c) (aser c env)) children)))) (for-each
(fn (c)
(let ((result (aser c env)))
(if (= (type-of result) "list")
(for-each
(fn (item)
(when (not (nil? item))
(append! parts (serialize item))))
result)
(when (not (nil? result))
(append! parts (serialize result))))))
children)
(if (empty? parts) (if (empty? parts)
"" ""
(str "(<> " (join " " (map serialize parts)) ")"))))) (str "(<> " (join " " parts) ")")))))
(define aser-call (define aser-call
(fn (name args env) (fn ((name :as string) (args :as list) (env :as dict))
;; Serialize (name :key val child ...) — evaluate args but keep as sx ;; Serialize (name :key val child ...) — evaluate args but keep as sx
(let ((parts (list name))) ;; Uses for-each + mutable state (not reduce) so bootstrapper emits for-loops
(reduce ;; that can contain nested for-each for list flattening.
(fn (state arg) (let ((parts (list name))
(let ((skip (get state "skip"))) (skip false)
(if skip (i 0))
(assoc state "skip" false "i" (inc (get state "i"))) (for-each
(if (and (= (type-of arg) "keyword") (fn (arg)
(< (inc (get state "i")) (len args))) (if skip
(let ((val (aser (nth args (inc (get state "i"))) env))) (do (set! skip false)
(when (not (nil? val)) (set! i (inc i)))
(append! parts (str ":" (keyword-name arg))) (if (and (= (type-of arg) "keyword")
(append! parts (serialize val))) (< (inc i) (len args)))
(assoc state "skip" true "i" (inc (get state "i")))) (let ((val (aser (nth args (inc i)) env)))
(let ((val (aser arg env))) (when (not (nil? val))
(when (not (nil? val)) (append! parts (str ":" (keyword-name arg)))
(append! parts (serialize val))) (append! parts (serialize val)))
(assoc state "i" (inc (get state "i")))))))) (set! skip true)
(dict "i" 0 "skip" false) (set! i (inc i)))
(let ((val (aser arg env)))
(when (not (nil? val))
(if (= (type-of val) "list")
(for-each
(fn (item)
(when (not (nil? item))
(append! parts (serialize item))))
val)
(append! parts (serialize val))))
(set! i (inc i))))))
args) args)
(str "(" (join " " parts) ")")))) (str "(" (join " " parts) ")"))))
@@ -156,11 +177,11 @@
"some" "every?" "for-each")) "some" "every?" "for-each"))
(define special-form? (define special-form?
(fn (name) (fn ((name :as string))
(contains? SPECIAL_FORM_NAMES name))) (contains? SPECIAL_FORM_NAMES name)))
(define ho-form? (define ho-form?
(fn (name) (fn ((name :as string))
(contains? HO_FORM_NAMES name))) (contains? HO_FORM_NAMES name)))
@@ -173,7 +194,7 @@
;; Definition forms evaluate for side effects and return nil. ;; Definition forms evaluate for side effects and return nil.
(define aser-special (define aser-special
(fn (name expr env) (fn ((name :as string) (expr :as list) (env :as dict))
(let ((args (rest expr))) (let ((args (rest expr)))
(cond (cond
;; if — evaluate condition, aser chosen branch ;; if — evaluate condition, aser chosen branch
@@ -293,7 +314,7 @@
;; Helper: case dispatch for aser mode ;; Helper: case dispatch for aser mode
(define eval-case-aser (define eval-case-aser
(fn (match-val clauses env) (fn (match-val (clauses :as list) (env :as dict))
(if (< (len clauses) 2) (if (< (len clauses) 2)
nil nil
(let ((test (first clauses)) (let ((test (first clauses))

File diff suppressed because it is too large Load Diff

View File

@@ -72,7 +72,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define sx-mount (define sx-mount
(fn (target source extra-env) (fn (target (source :as string) (extra-env :as dict))
;; Render SX source string into target element. ;; Render SX source string into target element.
;; target: Element or CSS selector string ;; target: Element or CSS selector string
;; source: SX source string ;; source: SX source string
@@ -101,7 +101,7 @@
;; new SX content, and replaces the wrapper's children. ;; new SX content, and replaces the wrapper's children.
(define resolve-suspense (define resolve-suspense
(fn (id sx) (fn ((id :as string) (sx :as string))
;; Process any new <script type="text/sx"> tags that arrived via ;; Process any new <script type="text/sx"> tags that arrived via
;; streaming (e.g. extra component defs) before resolving. ;; streaming (e.g. extra component defs) before resolving.
(process-sx-scripts nil) (process-sx-scripts nil)
@@ -166,7 +166,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define sx-render-component (define sx-render-component
(fn (name kwargs extra-env) (fn ((name :as string) (kwargs :as dict) (extra-env :as dict))
;; Render a named component with keyword args. ;; Render a named component with keyword args.
;; name: component name (with or without ~ prefix) ;; name: component name (with or without ~ prefix)
;; kwargs: dict of param-name → value ;; kwargs: dict of param-name → value
@@ -179,7 +179,7 @@
;; Build synthetic call expression ;; Build synthetic call expression
(let ((call-expr (list (make-symbol full-name)))) (let ((call-expr (list (make-symbol full-name))))
(for-each (for-each
(fn (k) (fn ((k :as string))
(append! call-expr (make-keyword (to-kebab k))) (append! call-expr (make-keyword (to-kebab k)))
(append! call-expr (dict-get kwargs k))) (append! call-expr (dict-get kwargs k)))
(keys kwargs)) (keys kwargs))
@@ -236,7 +236,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define process-component-script (define process-component-script
(fn (script text) (fn (script (text :as string))
;; Handle <script type="text/sx" data-components data-hash="..."> ;; Handle <script type="text/sx" data-components data-hash="...">
(let ((hash (dom-get-attr script "data-hash"))) (let ((hash (dom-get-attr script "data-hash")))
(if (nil? hash) (if (nil? hash)
@@ -304,7 +304,7 @@
(let ((pages (parse text))) (let ((pages (parse text)))
(log-info (str "pages: parsed " (len pages) " entries")) (log-info (str "pages: parsed " (len pages) " entries"))
(for-each (for-each
(fn (page) (fn ((page :as dict))
(append! _page-routes (append! _page-routes
(merge page (merge page
{"parsed" (parse-route-pattern (get page "path"))}))) {"parsed" (parse-route-pattern (get page "path"))})))
@@ -344,21 +344,21 @@
(define hydrate-island (define hydrate-island
(fn (el) (fn (el)
(let ((name (dom-get-attr el "data-sx-island")) (let ((name (dom-get-attr el "data-sx-island"))
(state-json (or (dom-get-attr el "data-sx-state") "{}"))) (state-sx (or (dom-get-attr el "data-sx-state") "{}")))
(let ((comp-name (str "~" name)) (let ((comp-name (str "~" name))
(env (get-render-env nil))) (env (get-render-env nil)))
(let ((comp (env-get env comp-name))) (let ((comp (env-get env comp-name)))
(if (not (or (component? comp) (island? comp))) (if (not (or (component? comp) (island? comp)))
(log-warn (str "hydrate-island: unknown island " comp-name)) (log-warn (str "hydrate-island: unknown island " comp-name))
;; Parse state and build keyword args ;; Parse state and build keyword args — SX format, not JSON
(let ((kwargs (json-parse state-json)) (let ((kwargs (or (first (sx-parse state-sx)) {}))
(disposers (list)) (disposers (list))
(local (env-merge (component-closure comp) env))) (local (env-merge (component-closure comp) env)))
;; Bind params from kwargs ;; Bind params from kwargs
(for-each (for-each
(fn (p) (fn ((p :as string))
(env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil))) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
(component-params comp)) (component-params comp))
@@ -393,7 +393,7 @@
(let ((disposers (dom-get-data el "sx-disposers"))) (let ((disposers (dom-get-data el "sx-disposers")))
(when disposers (when disposers
(for-each (for-each
(fn (d) (fn ((d :as lambda))
(when (callable? d) (d))) (when (callable? d) (d)))
disposers) disposers)
(dom-set-data el "sx-disposers" nil))))) (dom-set-data el "sx-disposers" nil)))))
@@ -494,8 +494,8 @@
;; (log-info msg) → void (console.log with prefix) ;; (log-info msg) → void (console.log with prefix)
;; (log-parse-error label text err) → void (diagnostic parse error) ;; (log-parse-error label text err) → void (diagnostic parse error)
;; ;;
;; === JSON === ;; === Parsing (island state) ===
;; (json-parse str) → dict/list/value (JSON.parse) ;; (sx-parse str) → list of AST expressions (from parser.sx)
;; ;;
;; === Processing markers === ;; === Processing markers ===
;; (mark-processed! el key) → void ;; (mark-processed! el key) → void

View File

@@ -49,6 +49,8 @@ class PyEmitter:
def __init__(self): def __init__(self):
self.indent = 0 self.indent = 0
self._async_names: set[str] = set() # SX names of define-async functions
self._in_async: bool = False # Currently emitting async def body?
def emit(self, expr) -> str: def emit(self, expr) -> str:
"""Emit a Python expression from an SX AST node.""" """Emit a Python expression from an SX AST node."""
@@ -80,6 +82,8 @@ class PyEmitter:
name = head.name name = head.name
if name == "define": if name == "define":
return self._emit_define(expr, indent) return self._emit_define(expr, indent)
if name == "define-async":
return self._emit_define_async(expr, indent)
if name == "set!": if name == "set!":
return f"{pad}{self._mangle(expr[1].name)} = {self.emit(expr[2])}" return f"{pad}{self._mangle(expr[1].name)} = {self.emit(expr[2])}"
if name == "when": if name == "when":
@@ -138,6 +142,8 @@ class PyEmitter:
"component-has-children?": "component_has_children", "component-has-children?": "component_has_children",
"component-name": "component_name", "component-name": "component_name",
"component-affinity": "component_affinity", "component-affinity": "component_affinity",
"component-param-types": "component_param_types",
"component-set-param-types!": "component_set_param_types",
"macro-params": "macro_params", "macro-params": "macro_params",
"macro-rest-param": "macro_rest_param", "macro-rest-param": "macro_rest_param",
"macro-body": "macro_body", "macro-body": "macro_body",
@@ -275,6 +281,19 @@ class PyEmitter:
"sf-defisland": "sf_defisland", "sf-defisland": "sf_defisland",
# adapter-sx.sx # adapter-sx.sx
"render-to-sx": "render_to_sx", "render-to-sx": "render_to_sx",
# adapter-async.sx platform primitives
"svg-context-set!": "svg_context_set",
"svg-context-reset!": "svg_context_reset",
"css-class-collect!": "css_class_collect",
"is-raw-html?": "is_raw_html",
"async-coroutine?": "is_async_coroutine",
"async-await!": "async_await",
"is-sx-expr?": "is_sx_expr",
"sx-expr?": "is_sx_expr",
"io-primitive?": "io_primitive_p",
"expand-components?": "expand_components_p",
"svg-context?": "svg_context_p",
"make-sx-expr": "make_sx_expr",
"aser": "aser", "aser": "aser",
"eval-case-aser": "eval_case_aser", "eval-case-aser": "eval_case_aser",
"sx-serialize": "sx_serialize", "sx-serialize": "sx_serialize",
@@ -417,10 +436,21 @@ class PyEmitter:
# Regular function call # Regular function call
fn_name = self._mangle(name) fn_name = self._mangle(name)
args = ", ".join(self.emit(x) for x in expr[1:]) args = ", ".join(self.emit(x) for x in expr[1:])
if self._in_async and name in self._async_names:
return f"(await {fn_name}({args}))"
return f"{fn_name}({args})" return f"{fn_name}({args})"
# --- Special form emitters --- # --- Special form emitters ---
@staticmethod
def _extract_param_name(p):
"""Extract the name from a param, handling (name :as type) annotations."""
if isinstance(p, list) and len(p) == 3 and isinstance(p[1], Keyword) and p[1].name == "as":
return p[0].name if isinstance(p[0], Symbol) else str(p[0])
if isinstance(p, Symbol):
return p.name
return str(p)
def _emit_fn(self, expr) -> str: def _emit_fn(self, expr) -> str:
params = expr[1] params = expr[1]
body = expr[2:] body = expr[2:]
@@ -432,16 +462,13 @@ class PyEmitter:
if isinstance(p, Symbol) and p.name == "&rest": if isinstance(p, Symbol) and p.name == "&rest":
# Next param is the rest parameter # Next param is the rest parameter
if i + 1 < len(params): if i + 1 < len(params):
rest_name = self._mangle(params[i + 1].name if isinstance(params[i + 1], Symbol) else str(params[i + 1])) rest_name = self._mangle(self._extract_param_name(params[i + 1]))
i += 2 i += 2
continue continue
else: else:
i += 1 i += 1
continue continue
if isinstance(p, Symbol): param_names.append(self._mangle(self._extract_param_name(p)))
param_names.append(self._mangle(p.name))
else:
param_names.append(str(p))
i += 1 i += 1
if rest_name: if rest_name:
param_names.append(f"*{rest_name}") param_names.append(f"*{rest_name}")
@@ -513,13 +540,16 @@ class PyEmitter:
body_parts = expr[2:] body_parts = expr[2:]
lines = [f"{pad}if sx_truthy({cond}):"] lines = [f"{pad}if sx_truthy({cond}):"]
for b in body_parts: for b in body_parts:
lines.append(self.emit_statement(b, indent + 1)) self._emit_stmt_recursive(b, lines, indent + 1)
return "\n".join(lines) return "\n".join(lines)
def _emit_cond(self, expr) -> str: def _emit_cond(self, expr) -> str:
clauses = expr[1:] clauses = expr[1:]
if not clauses: if not clauses:
return "NIL" return "NIL"
# Check ALL clauses are 2-element lists (scheme-style).
# Checking only the first is ambiguous — (nil? x) is a 2-element
# function call, not a scheme clause ((test body)).
is_scheme = ( is_scheme = (
all(isinstance(c, list) and len(c) == 2 for c in clauses) all(isinstance(c, list) and len(c) == 2 for c in clauses)
and not any(isinstance(c, Keyword) for c in clauses) and not any(isinstance(c, Keyword) for c in clauses)
@@ -642,6 +672,16 @@ class PyEmitter:
val = self.emit(val_expr) val = self.emit(val_expr)
return f"{pad}{self._mangle(name)} = {val}" return f"{pad}{self._mangle(name)} = {val}"
def _emit_define_async(self, expr, indent: int = 0) -> str:
"""Emit a define-async form as an async def statement."""
name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
val_expr = expr[2]
if (isinstance(val_expr, list) and val_expr and
isinstance(val_expr[0], Symbol) and val_expr[0].name in ("fn", "lambda")):
return self._emit_define_as_def(name, val_expr, indent, is_async=True)
# Shouldn't happen — define-async should always wrap fn/lambda
return self._emit_define(expr, indent)
def _body_uses_set(self, fn_expr) -> bool: def _body_uses_set(self, fn_expr) -> bool:
"""Check if a fn expression's body (recursively) uses set!.""" """Check if a fn expression's body (recursively) uses set!."""
def _has_set(node): def _has_set(node):
@@ -654,12 +694,16 @@ class PyEmitter:
body = fn_expr[2:] body = fn_expr[2:]
return any(_has_set(b) for b in body) return any(_has_set(b) for b in body)
def _emit_define_as_def(self, name: str, fn_expr, indent: int = 0) -> str: def _emit_define_as_def(self, name: str, fn_expr, indent: int = 0,
is_async: bool = False) -> str:
"""Emit a define with fn value as a proper def statement. """Emit a define with fn value as a proper def statement.
This is used for functions that contain set! — Python closures can't This is used for functions that contain set! — Python closures can't
rebind outer lambda params, so we need proper def + local variables. rebind outer lambda params, so we need proper def + local variables.
Variables mutated by set! from nested lambdas use a _cells dict. Variables mutated by set! from nested lambdas use a _cells dict.
When is_async=True, emits 'async def' and sets _in_async so that
calls to other async functions receive 'await'.
""" """
pad = " " * indent pad = " " * indent
params = fn_expr[1] params = fn_expr[1]
@@ -670,30 +714,32 @@ class PyEmitter:
p = params[i] p = params[i]
if isinstance(p, Symbol) and p.name == "&rest": if isinstance(p, Symbol) and p.name == "&rest":
if i + 1 < len(params): if i + 1 < len(params):
rest_name = self._mangle(params[i + 1].name if isinstance(params[i + 1], Symbol) else str(params[i + 1])) rest_name = self._mangle(self._extract_param_name(params[i + 1]))
param_names.append(f"*{rest_name}") param_names.append(f"*{rest_name}")
i += 2 i += 2
continue continue
else: else:
i += 1 i += 1
continue continue
if isinstance(p, Symbol): param_names.append(self._mangle(self._extract_param_name(p)))
param_names.append(self._mangle(p.name))
else:
param_names.append(str(p))
i += 1 i += 1
params_str = ", ".join(param_names) params_str = ", ".join(param_names)
py_name = self._mangle(name) py_name = self._mangle(name)
# Find set! target variables that are used from nested lambda scopes # Find set! target variables that are used from nested lambda scopes
nested_set_vars = self._find_nested_set_vars(body) nested_set_vars = self._find_nested_set_vars(body)
lines = [f"{pad}def {py_name}({params_str}):"] def_kw = "async def" if is_async else "def"
lines = [f"{pad}{def_kw} {py_name}({params_str}):"]
if nested_set_vars: if nested_set_vars:
lines.append(f"{pad} _cells = {{}}") lines.append(f"{pad} _cells = {{}}")
# Emit body with cell var tracking # Emit body with cell var tracking (and async context if needed)
old_cells = getattr(self, '_current_cell_vars', set()) old_cells = getattr(self, '_current_cell_vars', set())
old_async = self._in_async
self._current_cell_vars = nested_set_vars self._current_cell_vars = nested_set_vars
if is_async:
self._in_async = True
self._emit_body_stmts(body, lines, indent + 1) self._emit_body_stmts(body, lines, indent + 1)
self._current_cell_vars = old_cells self._current_cell_vars = old_cells
self._in_async = old_async
return "\n".join(lines) return "\n".join(lines)
def _find_nested_set_vars(self, body) -> set[str]: def _find_nested_set_vars(self, body) -> set[str]:
@@ -750,7 +796,7 @@ class PyEmitter:
if is_last: if is_last:
self._emit_return_expr(expr, lines, indent) self._emit_return_expr(expr, lines, indent)
else: else:
lines.append(self.emit_statement(expr, indent)) self._emit_stmt_recursive(expr, lines, indent)
def _emit_return_expr(self, expr, lines: list, indent: int) -> None: def _emit_return_expr(self, expr, lines: list, indent: int) -> None:
"""Emit an expression in return position, flattening control flow.""" """Emit an expression in return position, flattening control flow."""
@@ -775,6 +821,11 @@ class PyEmitter:
if name in ("do", "begin"): if name in ("do", "begin"):
self._emit_body_stmts(expr[1:], lines, indent) self._emit_body_stmts(expr[1:], lines, indent)
return return
if name == "for-each":
# for-each in return position: emit as statement, return NIL
lines.append(self._emit_for_each_stmt(expr, indent))
lines.append(f"{pad}return NIL")
return
lines.append(f"{pad}return {self.emit(expr)}") lines.append(f"{pad}return {self.emit(expr)}")
def _emit_if_return(self, expr, lines: list, indent: int) -> None: def _emit_if_return(self, expr, lines: list, indent: int) -> None:
@@ -908,7 +959,7 @@ class PyEmitter:
if isinstance(fn_expr, list) and isinstance(fn_expr[0], Symbol) and fn_expr[0].name == "fn": if isinstance(fn_expr, list) and isinstance(fn_expr[0], Symbol) and fn_expr[0].name == "fn":
params = fn_expr[1] params = fn_expr[1]
body = fn_expr[2:] body = fn_expr[2:]
p = params[0].name if isinstance(params[0], Symbol) else str(params[0]) p = self._extract_param_name(params[0])
p_py = self._mangle(p) p_py = self._mangle(p)
lines = [f"{pad}for {p_py} in {coll}:"] lines = [f"{pad}for {p_py} in {coll}:"]
# Emit body as statements with proper let/set! handling # Emit body as statements with proper let/set! handling
@@ -1034,12 +1085,15 @@ class PyEmitter:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def extract_defines(source: str) -> list[tuple[str, list]]: def extract_defines(source: str) -> list[tuple[str, list]]:
"""Parse .sx source, return list of (name, define-expr) for top-level defines.""" """Parse .sx source, return list of (name, define-expr) for top-level defines.
Extracts both (define ...) and (define-async ...) forms.
"""
exprs = parse_all(source) exprs = parse_all(source)
defines = [] defines = []
for expr in exprs: for expr in exprs:
if isinstance(expr, list) and expr and isinstance(expr[0], Symbol): if isinstance(expr, list) and expr and isinstance(expr[0], Symbol):
if expr[0].name == "define": if expr[0].name in ("define", "define-async"):
name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1]) name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
defines.append((name, expr)) defines.append((name, expr))
return defines return defines
@@ -1196,6 +1250,8 @@ def compile_ref_to_py(
spec_mod_set.add("deps") spec_mod_set.add("deps")
if "signals" in SPEC_MODULES: if "signals" in SPEC_MODULES:
spec_mod_set.add("signals") spec_mod_set.add("signals")
if "page-helpers" in SPEC_MODULES:
spec_mod_set.add("page-helpers")
has_deps = "deps" in spec_mod_set has_deps = "deps" in spec_mod_set
# Core files always included, then selected adapters, then spec modules # Core files always included, then selected adapters, then spec modules
@@ -1210,6 +1266,28 @@ def compile_ref_to_py(
for name in sorted(spec_mod_set): for name in sorted(spec_mod_set):
sx_files.append(SPEC_MODULES[name]) sx_files.append(SPEC_MODULES[name])
# Pre-scan define-async names (needed before transpilation so emitter
# knows which calls require 'await')
has_async = "async" in adapter_set
if has_async:
async_filename = ADAPTER_FILES["async"][0]
async_filepath = os.path.join(ref_dir, async_filename)
if os.path.exists(async_filepath):
with open(async_filepath) as f:
async_src = f.read()
for aexpr in parse_all(async_src):
if (isinstance(aexpr, list) and aexpr
and isinstance(aexpr[0], Symbol)
and aexpr[0].name == "define-async"):
aname = aexpr[1].name if isinstance(aexpr[1], Symbol) else str(aexpr[1])
emitter._async_names.add(aname)
# Platform async primitives (provided by host, also need await)
emitter._async_names.update({
"async-eval", "execute-io", "async-await!",
})
# Async adapter is transpiled last (after sync adapters)
sx_files.append(ADAPTER_FILES["async"])
all_sections = [] all_sections = []
for filename, label in sx_files: for filename, label in sx_files:
filepath = os.path.join(ref_dir, filename) filepath = os.path.join(ref_dir, filename)
@@ -1246,6 +1324,9 @@ def compile_ref_to_py(
if has_deps: if has_deps:
parts.append(PLATFORM_DEPS_PY) parts.append(PLATFORM_DEPS_PY)
if has_async:
parts.append(PLATFORM_ASYNC_PY)
for label, defines in all_sections: for label, defines in all_sections:
parts.append(f"\n# === Transpiled from {label} ===\n") parts.append(f"\n# === Transpiled from {label} ===\n")
for name, expr in defines: for name, expr in defines:
@@ -1256,7 +1337,7 @@ def compile_ref_to_py(
parts.append(FIXUPS_PY) parts.append(FIXUPS_PY)
if has_continuations: if has_continuations:
parts.append(CONTINUATIONS_PY) parts.append(CONTINUATIONS_PY)
parts.append(public_api_py(has_html, has_sx, has_deps)) parts.append(public_api_py(has_html, has_sx, has_deps, has_async))
return "\n".join(parts) return "\n".join(parts)

View File

@@ -143,7 +143,7 @@ def _emit_py(suites: list[dict], preamble: list) -> str:
lines.append('') lines.append('')
lines.append('import pytest') lines.append('import pytest')
lines.append('from shared.sx.parser import parse_all') lines.append('from shared.sx.parser import parse_all')
lines.append('from shared.sx.evaluator import _eval, _trampoline') lines.append('from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline')
lines.append('') lines.append('')
lines.append('') lines.append('')
lines.append(f"_PREAMBLE = '''{preamble_escaped}'''") lines.append(f"_PREAMBLE = '''{preamble_escaped}'''")

View File

@@ -169,6 +169,83 @@ def parse_primitives_by_module() -> dict[str, frozenset[str]]:
return {mod: frozenset(names) for mod, names in modules.items()} return {mod: frozenset(names) for mod, names in modules.items()}
def _parse_param_type(param) -> tuple[str, str | None, bool]:
"""Parse a single param entry from a :params list.
Returns (name, type_or_none, is_rest).
A bare symbol like ``x`` → ("x", None, False).
A typed form ``(x :as number)`` → ("x", "number", False).
The ``&rest`` marker is tracked externally.
"""
if isinstance(param, Symbol):
return (param.name, None, False)
if isinstance(param, list) and len(param) == 3:
# (name :as type)
name_sym, kw, type_val = param
if (isinstance(name_sym, Symbol)
and isinstance(kw, Keyword) and kw.name == "as"):
type_str = type_val.name if isinstance(type_val, Symbol) else str(type_val)
return (name_sym.name, type_str, False)
return (str(param), None, False)
def parse_primitive_param_types() -> dict[str, dict]:
"""Parse primitives.sx and extract param type info for each primitive.
Returns a dict mapping primitive name to param type descriptor::
{
"+": {"positional": [], "rest_type": "number"},
"/": {"positional": [("a", "number"), ("b", "number")], "rest_type": None},
"get": {"positional": [("coll", None), ("key", None)], "rest_type": None},
}
Each positional entry is (name, type_or_none). rest_type is the
type of the &rest parameter (or None if no &rest, or None if untyped &rest).
"""
source = _read_file("primitives.sx")
exprs = parse_all(source)
result: dict[str, dict] = {}
for expr in exprs:
if not isinstance(expr, list) or len(expr) < 2:
continue
if not isinstance(expr[0], Symbol) or expr[0].name != "define-primitive":
continue
name = expr[1]
if not isinstance(name, str):
continue
params_list = _extract_keyword_arg(expr, "params")
if not isinstance(params_list, list):
continue
positional: list[tuple[str, str | None]] = []
rest_type: str | None = None
i = 0
while i < len(params_list):
item = params_list[i]
if isinstance(item, Symbol) and item.name == "&rest":
# Next item is the rest param
if i + 1 < len(params_list):
rname, rtype, _ = _parse_param_type(params_list[i + 1])
rest_type = rtype
i += 2
else:
pname, ptype, _ = _parse_param_type(item)
if pname != "&rest":
positional.append((pname, ptype))
i += 1
# Only store if at least one param has a type
has_types = rest_type is not None or any(t is not None for _, t in positional)
if has_types:
result[name] = {"positional": positional, "rest_type": rest_type}
return result
def parse_boundary_sx() -> tuple[frozenset[str], dict[str, frozenset[str]]]: def parse_boundary_sx() -> tuple[frozenset[str], dict[str, frozenset[str]]]:
"""Parse all boundary sources and return (io_names, {service: helper_names}). """Parse all boundary sources and return (io_names, {service: helper_names}).

View File

@@ -82,7 +82,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define sf-reset (define sf-reset
(fn (args env) (fn ((args :as list) (env :as dict))
;; Single argument: the body expression. ;; Single argument: the body expression.
;; Install a continuation delimiter, then evaluate body. ;; Install a continuation delimiter, then evaluate body.
;; The implementation is target-specific: ;; The implementation is target-specific:
@@ -136,7 +136,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define sf-shift (define sf-shift
(fn (args env) (fn ((args :as list) (env :as dict))
;; Two arguments: the continuation variable name, and the body. ;; Two arguments: the continuation variable name, and the body.
(let ((k-name (symbol-name (first args))) (let ((k-name (symbol-name (first args)))
(body (second args))) (body (second args)))

View File

@@ -39,7 +39,7 @@
(define scan-refs-walk (define scan-refs-walk
(fn (node refs) (fn (node (refs :as list))
(cond (cond
;; Symbol starting with ~ → component reference ;; Symbol starting with ~ → component reference
(= (type-of node) "symbol") (= (type-of node) "symbol")
@@ -68,26 +68,26 @@
;; that it can transitively render. Handles cycles via seen-set. ;; that it can transitively render. Handles cycles via seen-set.
(define transitive-deps-walk (define transitive-deps-walk
(fn (n seen env) (fn ((n :as string) (seen :as list) (env :as dict))
(when (not (contains? seen n)) (when (not (contains? seen n))
(append! seen n) (append! seen n)
(let ((val (env-get env n))) (let ((val (env-get env n)))
(cond (cond
(= (type-of val) "component") (= (type-of val) "component")
(for-each (fn (ref) (transitive-deps-walk ref seen env)) (for-each (fn ((ref :as string)) (transitive-deps-walk ref seen env))
(scan-refs (component-body val))) (scan-refs (component-body val)))
(= (type-of val) "macro") (= (type-of val) "macro")
(for-each (fn (ref) (transitive-deps-walk ref seen env)) (for-each (fn ((ref :as string)) (transitive-deps-walk ref seen env))
(scan-refs (macro-body val))) (scan-refs (macro-body val)))
:else nil))))) :else nil)))))
(define transitive-deps (define transitive-deps
(fn (name env) (fn ((name :as string) (env :as dict))
(let ((seen (list)) (let ((seen (list))
(key (if (starts-with? name "~") name (str "~" name)))) (key (if (starts-with? name "~") name (str "~" name))))
(transitive-deps-walk key seen env) (transitive-deps-walk key seen env)
(filter (fn (x) (not (= x key))) seen)))) (filter (fn ((x :as string)) (not (= x key))) seen))))
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
@@ -101,9 +101,9 @@
;; (component-set-deps! comp deps) → store deps on component ;; (component-set-deps! comp deps) → store deps on component
(define compute-all-deps (define compute-all-deps
(fn (env) (fn ((env :as dict))
(for-each (for-each
(fn (name) (fn ((name :as string))
(let ((val (env-get env name))) (let ((val (env-get env name)))
(when (= (type-of val) "component") (when (= (type-of val) "component")
(component-set-deps! val (transitive-deps name env))))) (component-set-deps! val (transitive-deps name env)))))
@@ -120,9 +120,9 @@
;; (regex-find-all pattern source) → list of matched group strings ;; (regex-find-all pattern source) → list of matched group strings
(define scan-components-from-source (define scan-components-from-source
(fn (source) (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) (str "~" m)) matches)))) (map (fn ((m :as string)) (str "~" m)) matches))))
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
@@ -132,13 +132,13 @@
;; the transitive closure. Returns list of ~names. ;; the transitive closure. Returns list of ~names.
(define components-needed (define components-needed
(fn (page-source env) (fn ((page-source :as string) (env :as dict))
(let ((direct (scan-components-from-source page-source)) (let ((direct (scan-components-from-source page-source))
(all-needed (list))) (all-needed (list)))
;; Add each direct ref + its transitive deps ;; Add each direct ref + its transitive deps
(for-each (for-each
(fn (name) (fn ((name :as string))
(when (not (contains? all-needed name)) (when (not (contains? all-needed name))
(append! all-needed name)) (append! all-needed name))
(let ((val (env-get env name))) (let ((val (env-get env name)))
@@ -147,7 +147,7 @@
(component-deps val) (component-deps val)
(transitive-deps name env)))) (transitive-deps name env))))
(for-each (for-each
(fn (dep) (fn ((dep :as string))
(when (not (contains? all-needed dep)) (when (not (contains? all-needed dep))
(append! all-needed dep))) (append! all-needed dep)))
deps)))) deps))))
@@ -166,7 +166,7 @@
;; This replaces the "send everything" approach with per-page bundles. ;; This replaces the "send everything" approach with per-page bundles.
(define page-component-bundle (define page-component-bundle
(fn (page-source env) (fn ((page-source :as string) (env :as dict))
(components-needed page-source env))) (components-needed page-source env)))
@@ -181,17 +181,17 @@
;; (scan-css-classes source) → set/list of class strings from source ;; (scan-css-classes source) → set/list of class strings from source
(define page-css-classes (define page-css-classes
(fn (page-source env) (fn ((page-source :as string) (env :as dict))
(let ((needed (components-needed page-source env)) (let ((needed (components-needed page-source env))
(classes (list))) (classes (list)))
;; Collect classes from needed components ;; Collect classes from needed components
(for-each (for-each
(fn (name) (fn ((name :as string))
(let ((val (env-get env name))) (let ((val (env-get env name)))
(when (= (type-of val) "component") (when (= (type-of val) "component")
(for-each (for-each
(fn (cls) (fn ((cls :as string))
(when (not (contains? classes cls)) (when (not (contains? classes cls))
(append! classes cls))) (append! classes cls)))
(component-css-classes val))))) (component-css-classes val)))))
@@ -199,7 +199,7 @@
;; Add classes from page source ;; Add classes from page source
(for-each (for-each
(fn (cls) (fn ((cls :as string))
(when (not (contains? classes cls)) (when (not (contains? classes cls))
(append! classes cls))) (append! classes cls)))
(scan-css-classes page-source)) (scan-css-classes page-source))
@@ -219,7 +219,7 @@
;; (component-set-io-refs! c r) → cache IO refs on component ;; (component-set-io-refs! c r) → cache IO refs on component
(define scan-io-refs-walk (define scan-io-refs-walk
(fn (node io-names refs) (fn (node (io-names :as list) (refs :as list))
(cond (cond
;; Symbol → check if name is in the IO set ;; Symbol → check if name is in the IO set
(= (type-of node) "symbol") (= (type-of node) "symbol")
@@ -242,7 +242,7 @@
(define scan-io-refs (define scan-io-refs
(fn (node io-names) (fn (node (io-names :as list))
(let ((refs (list))) (let ((refs (list)))
(scan-io-refs-walk node io-names refs) (scan-io-refs-walk node io-names refs)
refs))) refs)))
@@ -253,7 +253,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define transitive-io-refs-walk (define transitive-io-refs-walk
(fn (n seen all-refs env io-names) (fn ((n :as string) (seen :as list) (all-refs :as list) (env :as dict) (io-names :as list))
(when (not (contains? seen n)) (when (not (contains? seen n))
(append! seen n) (append! seen n)
(let ((val (env-get env n))) (let ((val (env-get env n)))
@@ -262,31 +262,31 @@
(do (do
;; Scan this component's body for IO refs ;; Scan this component's body for IO refs
(for-each (for-each
(fn (ref) (fn ((ref :as string))
(when (not (contains? all-refs ref)) (when (not (contains? all-refs ref))
(append! all-refs ref))) (append! all-refs ref)))
(scan-io-refs (component-body val) io-names)) (scan-io-refs (component-body val) io-names))
;; Recurse into component deps ;; Recurse into component deps
(for-each (for-each
(fn (dep) (transitive-io-refs-walk dep seen all-refs env io-names)) (fn ((dep :as string)) (transitive-io-refs-walk dep seen all-refs env io-names))
(scan-refs (component-body val)))) (scan-refs (component-body val))))
(= (type-of val) "macro") (= (type-of val) "macro")
(do (do
(for-each (for-each
(fn (ref) (fn ((ref :as string))
(when (not (contains? all-refs ref)) (when (not (contains? all-refs ref))
(append! all-refs ref))) (append! all-refs ref)))
(scan-io-refs (macro-body val) io-names)) (scan-io-refs (macro-body val) io-names))
(for-each (for-each
(fn (dep) (transitive-io-refs-walk dep seen all-refs env io-names)) (fn ((dep :as string)) (transitive-io-refs-walk dep seen all-refs env io-names))
(scan-refs (macro-body val)))) (scan-refs (macro-body val))))
:else nil))))) :else nil)))))
(define transitive-io-refs (define transitive-io-refs
(fn (name env io-names) (fn ((name :as string) (env :as dict) (io-names :as list))
(let ((all-refs (list)) (let ((all-refs (list))
(seen (list)) (seen (list))
(key (if (starts-with? name "~") name (str "~" name)))) (key (if (starts-with? name "~") name (str "~" name))))
@@ -299,9 +299,9 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define compute-all-io-refs (define compute-all-io-refs
(fn (env io-names) (fn ((env :as dict) (io-names :as list))
(for-each (for-each
(fn (name) (fn ((name :as string))
(let ((val (env-get env name))) (let ((val (env-get env name)))
(when (= (type-of val) "component") (when (= (type-of val) "component")
(component-set-io-refs! val (transitive-io-refs name env io-names))))) (component-set-io-refs! val (transitive-io-refs name env io-names)))))
@@ -309,7 +309,7 @@
(define component-io-refs-cached (define component-io-refs-cached
(fn (name env io-names) (fn ((name :as string) (env :as dict) (io-names :as list))
(let ((key (if (starts-with? name "~") name (str "~" name)))) (let ((key (if (starts-with? name "~") name (str "~" name))))
(let ((val (env-get env key))) (let ((val (env-get env key)))
(if (and (= (type-of val) "component") (if (and (= (type-of val) "component")
@@ -320,7 +320,7 @@
(transitive-io-refs name env io-names)))))) (transitive-io-refs name env io-names))))))
(define component-pure? (define component-pure?
(fn (name env io-names) (fn ((name :as string) (env :as dict) (io-names :as list))
(let ((key (if (starts-with? name "~") name (str "~" name)))) (let ((key (if (starts-with? name "~") name (str "~" name))))
(let ((val (env-get env key))) (let ((val (env-get env key)))
(if (and (= (type-of val) "component") (if (and (= (type-of val) "component")
@@ -344,7 +344,7 @@
;; Returns: "server" | "client" ;; Returns: "server" | "client"
(define render-target (define render-target
(fn (name env io-names) (fn ((name :as string) (env :as dict) (io-names :as list))
(let ((key (if (starts-with? name "~") name (str "~" name)))) (let ((key (if (starts-with? name "~") name (str "~" name))))
(let ((val (env-get env key))) (let ((val (env-get env key)))
(if (not (= (type-of val) "component")) (if (not (= (type-of val) "component"))
@@ -373,7 +373,7 @@
;; without recomputing at every request. ;; without recomputing at every request.
(define page-render-plan (define page-render-plan
(fn (page-source env io-names) (fn ((page-source :as string) (env :as dict) (io-names :as list))
(let ((needed (components-needed page-source env)) (let ((needed (components-needed page-source env))
(comp-targets (dict)) (comp-targets (dict))
(server-list (list)) (server-list (list))
@@ -381,7 +381,7 @@
(io-deps (list))) (io-deps (list)))
(for-each (for-each
(fn (name) (fn ((name :as string))
(let ((target (render-target name env io-names))) (let ((target (render-target name env io-names)))
(dict-set! comp-targets name target) (dict-set! comp-targets name target)
(if (= target "server") (if (= target "server")
@@ -389,7 +389,7 @@
(append! server-list name) (append! server-list name)
;; Collect IO deps from server components (use cache) ;; Collect IO deps from server components (use cache)
(for-each (for-each
(fn (io-ref) (fn ((io-ref :as string))
(when (not (contains? io-deps io-ref)) (when (not (contains? io-deps io-ref))
(append! io-deps io-ref))) (append! io-deps io-ref)))
(component-io-refs-cached name env io-names))) (component-io-refs-cached name env io-names)))
@@ -451,9 +451,9 @@
;; Moved from platform to spec: pure logic using type predicates. ;; Moved from platform to spec: pure logic using type predicates.
(define env-components (define env-components
(fn (env) (fn ((env :as dict))
(filter (filter
(fn (k) (fn ((k :as string))
(let ((v (env-get env k))) (let ((v (env-get env k)))
(or (component? v) (macro? v)))) (or (component? v) (macro? v))))
(keys env)))) (keys env))))

View File

@@ -32,17 +32,18 @@
;; Each descriptor is a dict with "event" and "modifiers" keys. ;; Each descriptor is a dict with "event" and "modifiers" keys.
(define parse-time (define parse-time
(fn (s) (fn ((s :as string))
;; Parse time string: "2s" → 2000, "500ms" → 500 ;; Parse time string: "2s" → 2000, "500ms" → 500
(cond ;; Uses nested if (not cond) because cond misclassifies 2-element
(nil? s) 0 ;; function calls like (nil? s) as scheme-style ((test body)) clauses.
(ends-with? s "ms") (parse-int s 0) (if (nil? s) 0
(ends-with? s "s") (* (parse-int (replace s "s" "") 0) 1000) (if (ends-with? s "ms") (parse-int s 0)
:else (parse-int s 0)))) (if (ends-with? s "s") (* (parse-int (replace s "s" "") 0) 1000)
(parse-int s 0))))))
(define parse-trigger-spec (define parse-trigger-spec
(fn (spec) (fn ((spec :as string))
;; Parse "click delay:500ms once,change" → list of trigger descriptors ;; Parse "click delay:500ms once,change" → list of trigger descriptors
(if (nil? spec) (if (nil? spec)
nil nil
@@ -50,7 +51,7 @@
(filter (filter
(fn (x) (not (nil? x))) (fn (x) (not (nil? x)))
(map (map
(fn (part) (fn ((part :as string))
(let ((tokens (split (trim part) " "))) (let ((tokens (split (trim part) " ")))
(if (empty? tokens) (if (empty? tokens)
nil nil
@@ -62,7 +63,7 @@
;; Normal trigger with optional modifiers ;; Normal trigger with optional modifiers
(let ((mods (dict))) (let ((mods (dict)))
(for-each (for-each
(fn (tok) (fn ((tok :as string))
(cond (cond
(= tok "once") (= tok "once")
(dict-set! mods "once" true) (dict-set! mods "once" true)
@@ -80,7 +81,7 @@
(define default-trigger (define default-trigger
(fn (tag-name) (fn ((tag-name :as string))
;; Default trigger for element type ;; Default trigger for element type
(cond (cond
(= tag-name "FORM") (= tag-name "FORM")
@@ -101,7 +102,7 @@
(fn (el) (fn (el)
;; Check element for sx-get, sx-post, etc. Returns (dict "method" "url") or nil. ;; Check element for sx-get, sx-post, etc. Returns (dict "method" "url") or nil.
(some (some
(fn (verb) (fn ((verb :as string))
(let ((url (dom-get-attr el (str "sx-" verb)))) (let ((url (dom-get-attr el (str "sx-" verb))))
(if url (if url
(dict "method" (upper verb) "url" url) (dict "method" (upper verb) "url" url)
@@ -114,7 +115,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define build-request-headers (define build-request-headers
(fn (el loaded-components css-hash) (fn (el (loaded-components :as list) (css-hash :as string))
;; Build the SX request headers dict ;; Build the SX request headers dict
(let ((headers (dict (let ((headers (dict
"SX-Request" "true" "SX-Request" "true"
@@ -139,7 +140,7 @@
(let ((parsed (parse-header-value extra-h))) (let ((parsed (parse-header-value extra-h)))
(when parsed (when parsed
(for-each (for-each
(fn (key) (dict-set! headers key (str (get parsed key)))) (fn ((key :as string)) (dict-set! headers key (str (get parsed key))))
(keys parsed)))))) (keys parsed))))))
headers))) headers)))
@@ -150,7 +151,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define process-response-headers (define process-response-headers
(fn (get-header) (fn ((get-header :as lambda))
;; Extract all SX response header directives into a dict. ;; Extract all SX response header directives into a dict.
;; get-header is (fn (name) → string or nil). ;; get-header is (fn (name) → string or nil).
(dict (dict
@@ -174,13 +175,13 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define parse-swap-spec (define parse-swap-spec
(fn (raw-swap global-transitions?) (fn ((raw-swap :as string) (global-transitions? :as boolean))
;; Parse "innerHTML transition:true" → dict with style + transition flag ;; Parse "innerHTML transition:true" → dict with style + transition flag
(let ((parts (split (or raw-swap DEFAULT_SWAP) " ")) (let ((parts (split (or raw-swap DEFAULT_SWAP) " "))
(style (first parts)) (style (first parts))
(use-transition global-transitions?)) (use-transition global-transitions?))
(for-each (for-each
(fn (p) (fn ((p :as string))
(cond (cond
(= p "transition:true") (set! use-transition true) (= p "transition:true") (set! use-transition true)
(= p "transition:false") (set! use-transition false))) (= p "transition:false") (set! use-transition false)))
@@ -193,7 +194,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define parse-retry-spec (define parse-retry-spec
(fn (retry-attr) (fn ((retry-attr :as string))
;; Parse "exponential:1000:30000" → spec dict or nil ;; Parse "exponential:1000:30000" → spec dict or nil
(if (nil? retry-attr) (if (nil? retry-attr)
nil nil
@@ -205,7 +206,7 @@
(define next-retry-ms (define next-retry-ms
(fn (current-ms cap-ms) (fn ((current-ms :as number) (cap-ms :as number))
;; Exponential backoff: double current, cap at max ;; Exponential backoff: double current, cap at max
(min (* current-ms 2) cap-ms))) (min (* current-ms 2) cap-ms)))
@@ -215,24 +216,23 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define filter-params (define filter-params
(fn (params-spec all-params) (fn ((params-spec :as string) (all-params :as list))
;; Filter form parameters by sx-params spec. ;; Filter form parameters by sx-params spec.
;; all-params is a list of (key value) pairs. ;; all-params is a list of (key value) pairs.
;; Returns filtered list of (key value) pairs. ;; Returns filtered list of (key value) pairs.
(cond ;; Uses nested if (not cond) — see parse-time comment.
(nil? params-spec) all-params (if (nil? params-spec) all-params
(= params-spec "none") (list) (if (= params-spec "none") (list)
(= params-spec "*") all-params (if (= params-spec "*") all-params
(starts-with? params-spec "not ") (if (starts-with? params-spec "not ")
(let ((excluded (map trim (split (slice params-spec 4) ",")))) (let ((excluded (map trim (split (slice params-spec 4) ","))))
(filter (filter
(fn (p) (not (contains? excluded (first p)))) (fn ((p :as list)) (not (contains? excluded (first p))))
all-params)) all-params))
:else (let ((allowed (map trim (split params-spec ","))))
(let ((allowed (map trim (split params-spec ",")))) (filter
(filter (fn ((p :as list)) (contains? allowed (first p)))
(fn (p) (contains? allowed (first p))) all-params))))))))
all-params)))))
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
@@ -279,7 +279,7 @@
(define revert-optimistic (define revert-optimistic
(fn (state) (fn ((state :as dict))
;; Revert an optimistic update ;; Revert an optimistic update
(when state (when state
(let ((target (get state "target")) (let ((target (get state "target"))
@@ -305,7 +305,7 @@
;; Returns list of (dict "element" el "swap-type" type "target-id" id). ;; Returns list of (dict "element" el "swap-type" type "target-id" id).
(let ((results (list))) (let ((results (list)))
(for-each (for-each
(fn (attr) (fn ((attr :as string))
(let ((oob-els (dom-query-all container (str "[" attr "]")))) (let ((oob-els (dom-query-all container (str "[" attr "]"))))
(for-each (for-each
(fn (oob) (fn (oob)
@@ -380,7 +380,7 @@
(reactive-attrs (if (empty? ra-str) (list) (split ra-str ",")))) (reactive-attrs (if (empty? ra-str) (list) (split ra-str ","))))
;; Add/update attributes from new, skip reactive ones ;; Add/update attributes from new, skip reactive ones
(for-each (for-each
(fn (attr) (fn ((attr :as list))
(let ((name (first attr)) (let ((name (first attr))
(val (nth attr 1))) (val (nth attr 1)))
(when (and (not (= (dom-get-attr old-el name) val)) (when (and (not (= (dom-get-attr old-el name) val))
@@ -389,7 +389,7 @@
(dom-attr-list new-el)) (dom-attr-list new-el))
;; Remove attributes not in new, skip reactive + marker attrs ;; Remove attributes not in new, skip reactive + marker attrs
(for-each (for-each
(fn (attr) (fn ((attr :as list))
(let ((aname (first attr))) (let ((aname (first attr)))
(when (and (not (dom-has-attr? new-el aname)) (when (and (not (dom-has-attr? new-el aname))
(not (contains? reactive-attrs aname)) (not (contains? reactive-attrs aname))
@@ -406,7 +406,7 @@
(new-kids (dom-child-list new-parent)) (new-kids (dom-child-list new-parent))
;; Build ID map of old children for keyed matching ;; Build ID map of old children for keyed matching
(old-by-id (reduce (old-by-id (reduce
(fn (acc kid) (fn ((acc :as dict) kid)
(let ((id (dom-id kid))) (let ((id (dom-id kid)))
(if id (do (dict-set! acc id kid) acc) acc))) (if id (do (dict-set! acc id kid) acc) acc)))
(dict) old-kids)) (dict) old-kids))
@@ -447,7 +447,7 @@
;; Remove leftover old children ;; Remove leftover old children
(for-each (for-each
(fn (i) (fn ((i :as number))
(when (>= i oi) (when (>= i oi)
(let ((leftover (nth old-kids i))) (let ((leftover (nth old-kids i)))
(when (and (dom-is-child-of? leftover old-parent) (when (and (dom-is-child-of? leftover old-parent)
@@ -577,7 +577,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define swap-dom-nodes (define swap-dom-nodes
(fn (target new-nodes strategy) (fn (target new-nodes (strategy :as string))
;; Execute a swap strategy on live DOM nodes. ;; Execute a swap strategy on live DOM nodes.
;; new-nodes is typically a DocumentFragment or Element. ;; new-nodes is typically a DocumentFragment or Element.
(case strategy (case strategy
@@ -644,7 +644,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define swap-html-string (define swap-html-string
(fn (target html strategy) (fn (target (html :as string) (strategy :as string))
;; Execute a swap strategy using an HTML string (DOMParser pipeline). ;; Execute a swap strategy using an HTML string (DOMParser pipeline).
(case strategy (case strategy
"innerHTML" "innerHTML"
@@ -675,7 +675,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define handle-history (define handle-history
(fn (el url resp-headers) (fn (el (url :as string) (resp-headers :as dict))
;; Process history push/replace based on element attrs and response headers ;; Process history push/replace based on element attrs and response headers
(let ((push-url (dom-get-attr el "sx-push-url")) (let ((push-url (dom-get-attr el "sx-push-url"))
(replace-url (dom-get-attr el "sx-replace-url")) (replace-url (dom-get-attr el "sx-replace-url"))
@@ -701,7 +701,7 @@
(define PRELOAD_TTL 30000) ;; 30 seconds (define PRELOAD_TTL 30000) ;; 30 seconds
(define preload-cache-get (define preload-cache-get
(fn (cache url) (fn ((cache :as dict) (url :as string))
;; Get and consume a cached preload response. ;; Get and consume a cached preload response.
;; Returns (dict "text" ... "content-type" ...) or nil. ;; Returns (dict "text" ... "content-type" ...) or nil.
(let ((entry (dict-get cache url))) (let ((entry (dict-get cache url)))
@@ -713,7 +713,7 @@
(define preload-cache-set (define preload-cache-set
(fn (cache url text content-type) (fn ((cache :as dict) (url :as string) (text :as string) (content-type :as string))
;; Store a preloaded response ;; Store a preloaded response
(dict-set! cache url (dict-set! cache url
(dict "text" text "content-type" content-type "timestamp" (now-ms))))) (dict "text" text "content-type" content-type "timestamp" (now-ms)))))
@@ -726,7 +726,7 @@
;; This is the logic; actual browser event binding is platform interface. ;; This is the logic; actual browser event binding is platform interface.
(define classify-trigger (define classify-trigger
(fn (trigger) (fn ((trigger :as dict))
;; Classify a parsed trigger descriptor for binding. ;; Classify a parsed trigger descriptor for binding.
;; Returns one of: "poll", "intersect", "load", "revealed", "event" ;; Returns one of: "poll", "intersect", "load", "revealed", "event"
(let ((event (get trigger "event"))) (let ((event (get trigger "event")))

View File

@@ -73,7 +73,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define eval-expr (define eval-expr
(fn (expr env) (fn (expr (env :as dict))
(case (type-of expr) (case (type-of expr)
;; --- literals pass through --- ;; --- literals pass through ---
@@ -91,7 +91,8 @@
(= name "true") true (= name "true") true
(= name "false") false (= name "false") false
(= name "nil") nil (= name "nil") nil
:else (error (str "Undefined symbol: " name)))) :else (do (debug-log "Undefined symbol:" name "primitive?:" (primitive? name))
(error (str "Undefined symbol: " name)))))
;; --- keyword → its string name --- ;; --- keyword → its string name ---
"keyword" (keyword-name expr) "keyword" (keyword-name expr)
@@ -115,7 +116,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define eval-list (define eval-list
(fn (expr env) (fn (expr (env :as dict))
(let ((head (first expr)) (let ((head (first expr))
(args (rest expr))) (args (rest expr)))
@@ -190,7 +191,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define eval-call (define eval-call
(fn (head args env) (fn (head (args :as list) (env :as dict))
(let ((f (trampoline (eval-expr head env))) (let ((f (trampoline (eval-expr head env)))
(evaluated-args (map (fn (a) (trampoline (eval-expr a env))) args))) (evaluated-args (map (fn (a) (trampoline (eval-expr a env))) args)))
(cond (cond
@@ -214,7 +215,7 @@
(define call-lambda (define call-lambda
(fn (f args caller-env) (fn ((f :as lambda) (args :as list) (caller-env :as dict))
(let ((params (lambda-params f)) (let ((params (lambda-params f))
(local (env-merge (lambda-closure f) caller-env))) (local (env-merge (lambda-closure f) caller-env)))
;; Too many args is an error; too few pads with nil ;; Too many args is an error; too few pads with nil
@@ -234,7 +235,7 @@
(define call-component (define call-component
(fn (comp raw-args env) (fn ((comp :as component) (raw-args :as list) (env :as dict))
;; Parse keyword args and children from unevaluated arg list ;; Parse keyword args and children from unevaluated arg list
(let ((parsed (parse-keyword-args raw-args env)) (let ((parsed (parse-keyword-args raw-args env))
(kwargs (first parsed)) (kwargs (first parsed))
@@ -252,7 +253,7 @@
(define parse-keyword-args (define parse-keyword-args
(fn (raw-args env) (fn ((raw-args :as list) (env :as dict))
;; Walk args: keyword + next-val → kwargs dict, else → children list ;; Walk args: keyword + next-val → kwargs dict, else → children list
(let ((kwargs (dict)) (let ((kwargs (dict))
(children (list)) (children (list))
@@ -286,7 +287,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define sf-if (define sf-if
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((condition (trampoline (eval-expr (first args) env)))) (let ((condition (trampoline (eval-expr (first args) env))))
(if (and condition (not (nil? condition))) (if (and condition (not (nil? condition)))
(make-thunk (nth args 1) env) (make-thunk (nth args 1) env)
@@ -296,7 +297,7 @@
(define sf-when (define sf-when
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((condition (trampoline (eval-expr (first args) env)))) (let ((condition (trampoline (eval-expr (first args) env))))
(if (and condition (not (nil? condition))) (if (and condition (not (nil? condition)))
(do (do
@@ -309,18 +310,22 @@
nil)))) nil))))
;; cond-scheme? — check if ALL clauses are 2-element lists (scheme-style).
;; Checking only the first arg is ambiguous — (nil? x) is a 2-element
;; function call, not a scheme clause ((test body)).
(define cond-scheme?
(fn ((clauses :as list))
(every? (fn (c) (and (= (type-of c) "list") (= (len c) 2)))
clauses)))
(define sf-cond (define sf-cond
(fn (args env) (fn ((args :as list) (env :as dict))
;; Detect scheme-style: first arg is a 2-element list (if (cond-scheme? args)
(if (and (= (type-of (first args)) "list")
(= (len (first args)) 2))
;; Scheme-style: ((test body) ...)
(sf-cond-scheme args env) (sf-cond-scheme args env)
;; Clojure-style: test body test body ...
(sf-cond-clojure args env)))) (sf-cond-clojure args env))))
(define sf-cond-scheme (define sf-cond-scheme
(fn (clauses env) (fn ((clauses :as list) (env :as dict))
(if (empty? clauses) (if (empty? clauses)
nil nil
(let ((clause (first clauses)) (let ((clause (first clauses))
@@ -337,7 +342,7 @@
(sf-cond-scheme (rest clauses) env))))))) (sf-cond-scheme (rest clauses) env)))))))
(define sf-cond-clojure (define sf-cond-clojure
(fn (clauses env) (fn ((clauses :as list) (env :as dict))
(if (< (len clauses) 2) (if (< (len clauses) 2)
nil nil
(let ((test (first clauses)) (let ((test (first clauses))
@@ -353,13 +358,13 @@
(define sf-case (define sf-case
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((match-val (trampoline (eval-expr (first args) env))) (let ((match-val (trampoline (eval-expr (first args) env)))
(clauses (rest args))) (clauses (rest args)))
(sf-case-loop match-val clauses env)))) (sf-case-loop match-val clauses env))))
(define sf-case-loop (define sf-case-loop
(fn (match-val clauses env) (fn (match-val (clauses :as list) (env :as dict))
(if (< (len clauses) 2) (if (< (len clauses) 2)
nil nil
(let ((test (first clauses)) (let ((test (first clauses))
@@ -375,7 +380,7 @@
(define sf-and (define sf-and
(fn (args env) (fn ((args :as list) (env :as dict))
(if (empty? args) (if (empty? args)
true true
(let ((val (trampoline (eval-expr (first args) env)))) (let ((val (trampoline (eval-expr (first args) env))))
@@ -387,7 +392,7 @@
(define sf-or (define sf-or
(fn (args env) (fn ((args :as list) (env :as dict))
(if (empty? args) (if (empty? args)
false false
(let ((val (trampoline (eval-expr (first args) env)))) (let ((val (trampoline (eval-expr (first args) env))))
@@ -397,7 +402,7 @@
(define sf-let (define sf-let
(fn (args env) (fn ((args :as list) (env :as dict))
;; Detect named let: (let name ((x 0) ...) body) ;; Detect named let: (let name ((x 0) ...) body)
;; If first arg is a symbol, delegate to sf-named-let. ;; If first arg is a symbol, delegate to sf-named-let.
(if (= (type-of (first args)) "symbol") (if (= (type-of (first args)) "symbol")
@@ -438,7 +443,7 @@
;; Desugars to a self-recursive lambda called with initial values. ;; Desugars to a self-recursive lambda called with initial values.
;; The loop name is bound in the body so recursive calls produce TCO thunks. ;; The loop name is bound in the body so recursive calls produce TCO thunks.
(define sf-named-let (define sf-named-let
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((loop-name (symbol-name (first args))) (let ((loop-name (symbol-name (first args)))
(bindings (nth args 1)) (bindings (nth args 1))
(body (slice args 2)) (body (slice args 2))
@@ -478,22 +483,29 @@
(define sf-lambda (define sf-lambda
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((params-expr (first args)) (let ((params-expr (first args))
(body-exprs (rest args)) (body-exprs (rest args))
(body (if (= (len body-exprs) 1) (body (if (= (len body-exprs) 1)
(first body-exprs) (first body-exprs)
(cons (make-symbol "begin") body-exprs))) (cons (make-symbol "begin") body-exprs)))
(param-names (map (fn (p) (param-names (map (fn (p)
(if (= (type-of p) "symbol") (cond
(symbol-name p) (= (type-of p) "symbol")
p)) (symbol-name p)
;; Annotated param: (name :as type) → extract name
(and (= (type-of p) "list")
(= (len p) 3)
(= (type-of (nth p 1)) "keyword")
(= (keyword-name (nth p 1)) "as"))
(symbol-name (first p))
:else p))
params-expr))) params-expr)))
(make-lambda param-names body env)))) (make-lambda param-names body env))))
(define sf-define (define sf-define
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((name-sym (first args)) (let ((name-sym (first args))
(value (trampoline (eval-expr (nth args 1) env)))) (value (trampoline (eval-expr (nth args 1) env))))
(when (and (lambda? value) (nil? (lambda-name value))) (when (and (lambda? value) (nil? (lambda-name value)))
@@ -503,7 +515,7 @@
(define sf-defcomp (define sf-defcomp
(fn (args env) (fn ((args :as list) (env :as dict))
;; (defcomp ~name (params) [:affinity :client|:server] body) ;; (defcomp ~name (params) [:affinity :client|:server] body)
;; Body is always the last element. Optional keyword annotations ;; Body is always the last element. Optional keyword annotations
;; may appear between the params list and the body. ;; may appear between the params list and the body.
@@ -514,13 +526,18 @@
(parsed (parse-comp-params params-raw)) (parsed (parse-comp-params params-raw))
(params (first parsed)) (params (first parsed))
(has-children (nth parsed 1)) (has-children (nth parsed 1))
(param-types (nth parsed 2))
(affinity (defcomp-kwarg args "affinity" "auto"))) (affinity (defcomp-kwarg args "affinity" "auto")))
(let ((comp (make-component comp-name params has-children body env affinity))) (let ((comp (make-component comp-name params has-children body env affinity)))
;; Store type annotations if any were declared
(when (and (not (nil? param-types))
(not (empty? (keys param-types))))
(component-set-param-types! comp param-types))
(env-set! env (symbol-name name-sym) comp) (env-set! env (symbol-name name-sym) comp)
comp)))) comp))))
(define defcomp-kwarg (define defcomp-kwarg
(fn (args key default) (fn ((args :as list) (key :as string) default)
;; Search for :key value between params (index 2) and body (last). ;; Search for :key value between params (index 2) and body (last).
(let ((end (- (len args) 1)) (let ((end (- (len args) 1))
(result default)) (result default))
@@ -536,29 +553,49 @@
result))) result)))
(define parse-comp-params (define parse-comp-params
(fn (params-expr) (fn ((params-expr :as list))
;; Parse (&key param1 param2 &children) → (params has-children) ;; Parse (&key param1 param2 &children) → (params has-children param-types)
;; Also accepts &rest as synonym for &children. ;; Also accepts &rest as synonym for &children.
;; Supports typed params: (name :as type) — a 3-element list where
;; the second element is the keyword :as. Unannotated params get no
;; type entry. param-types is a dict {name → type-expr} or empty dict.
(let ((params (list)) (let ((params (list))
(param-types (dict))
(has-children false) (has-children false)
(in-key false)) (in-key false))
(for-each (for-each
(fn (p) (fn (p)
(when (= (type-of p) "symbol") (if (and (= (type-of p) "list")
(let ((name (symbol-name p))) (= (len p) 3)
(cond (= (type-of (first p)) "symbol")
(= name "&key") (set! in-key true) (= (type-of (nth p 1)) "keyword")
(= name "&rest") (set! has-children true) (= (keyword-name (nth p 1)) "as"))
(= name "&children") (set! has-children true) ;; Typed param: (name :as type)
has-children nil ;; skip params after &children/&rest (let ((name (symbol-name (first p)))
in-key (append! params name) (ptype (nth p 2)))
:else (append! params name))))) ;; Convert type to string if it's a symbol
(let ((type-val (if (= (type-of ptype) "symbol")
(symbol-name ptype)
ptype)))
(when (not has-children)
(append! params name)
(dict-set! param-types name type-val))))
;; Untyped param or marker
(when (= (type-of p) "symbol")
(let ((name (symbol-name p)))
(cond
(= name "&key") (set! in-key true)
(= name "&rest") (set! has-children true)
(= name "&children") (set! has-children true)
has-children nil ;; skip params after &children/&rest
in-key (append! params name)
:else (append! params name))))))
params-expr) params-expr)
(list params has-children)))) (list params has-children param-types))))
(define sf-defisland (define sf-defisland
(fn (args env) (fn ((args :as list) (env :as dict))
;; (defisland ~name (params) body) ;; (defisland ~name (params) body)
;; Like defcomp but creates an island (reactive component). ;; Like defcomp but creates an island (reactive component).
;; Islands have the same calling convention as components but ;; Islands have the same calling convention as components but
@@ -576,7 +613,7 @@
(define sf-defmacro (define sf-defmacro
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((name-sym (first args)) (let ((name-sym (first args))
(params-raw (nth args 1)) (params-raw (nth args 1))
(body (nth args 2)) (body (nth args 2))
@@ -588,7 +625,7 @@
mac)))) mac))))
(define parse-macro-params (define parse-macro-params
(fn (params-expr) (fn ((params-expr :as list))
;; Parse (a b &rest rest) → ((a b) rest) ;; Parse (a b &rest rest) → ((a b) rest)
(let ((params (list)) (let ((params (list))
(rest-param nil)) (rest-param nil))
@@ -609,7 +646,7 @@
(define sf-defstyle (define sf-defstyle
(fn (args env) (fn ((args :as list) (env :as dict))
;; (defstyle name expr) — bind name to evaluated expr (string, function, etc.) ;; (defstyle name expr) — bind name to evaluated expr (string, function, etc.)
(let ((name-sym (first args)) (let ((name-sym (first args))
(value (trampoline (eval-expr (nth args 1) env)))) (value (trampoline (eval-expr (nth args 1) env))))
@@ -618,7 +655,7 @@
(define sf-begin (define sf-begin
(fn (args env) (fn ((args :as list) (env :as dict))
(if (empty? args) (if (empty? args)
nil nil
(do (do
@@ -629,16 +666,16 @@
(define sf-quote (define sf-quote
(fn (args env) (fn ((args :as list) (env :as dict))
(if (empty? args) nil (first args)))) (if (empty? args) nil (first args))))
(define sf-quasiquote (define sf-quasiquote
(fn (args env) (fn ((args :as list) (env :as dict))
(qq-expand (first args) env))) (qq-expand (first args) env)))
(define qq-expand (define qq-expand
(fn (template env) (fn (template (env :as dict))
(if (not (= (type-of template) "list")) (if (not (= (type-of template) "list"))
template template
(if (empty? template) (if (empty? template)
@@ -663,7 +700,7 @@
(define sf-thread-first (define sf-thread-first
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((val (trampoline (eval-expr (first args) env)))) (let ((val (trampoline (eval-expr (first args) env))))
(reduce (reduce
(fn (result form) (fn (result form)
@@ -690,7 +727,7 @@
(define sf-set! (define sf-set!
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((name (symbol-name (first args))) (let ((name (symbol-name (first args)))
(value (trampoline (eval-expr (nth args 1) env)))) (value (trampoline (eval-expr (nth args 1) env))))
(env-set! env name value) (env-set! env name value)
@@ -711,7 +748,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define sf-letrec (define sf-letrec
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((bindings (first args)) (let ((bindings (first args))
(body (rest args)) (body (rest args))
(local (env-extend env)) (local (env-extend env))
@@ -786,7 +823,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define sf-dynamic-wind (define sf-dynamic-wind
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((before (trampoline (eval-expr (first args) env))) (let ((before (trampoline (eval-expr (first args) env)))
(body (trampoline (eval-expr (nth args 1) env))) (body (trampoline (eval-expr (nth args 1) env)))
(after (trampoline (eval-expr (nth args 2) env)))) (after (trampoline (eval-expr (nth args 2) env))))
@@ -805,7 +842,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define expand-macro (define expand-macro
(fn (mac raw-args env) (fn ((mac :as macro) (raw-args :as list) (env :as dict))
(let ((local (env-merge (macro-closure mac) env))) (let ((local (env-merge (macro-closure mac) env)))
;; Bind positional params (unevaluated) ;; Bind positional params (unevaluated)
(for-each (for-each
@@ -829,20 +866,20 @@
;; call-fn: unified caller for HO forms — handles both Lambda and native callable ;; call-fn: unified caller for HO forms — handles both Lambda and native callable
(define call-fn (define call-fn
(fn (f args env) (fn (f (args :as list) (env :as dict))
(cond (cond
(lambda? f) (trampoline (call-lambda f args env)) (lambda? f) (trampoline (call-lambda f args env))
(callable? f) (apply f args) (callable? f) (apply f args)
:else (error (str "Not callable in HO form: " (inspect f)))))) :else (error (str "Not callable in HO form: " (inspect f))))))
(define ho-map (define ho-map
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((f (trampoline (eval-expr (first args) env))) (let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env)))) (coll (trampoline (eval-expr (nth args 1) env))))
(map (fn (item) (call-fn f (list item) env)) coll)))) (map (fn (item) (call-fn f (list item) env)) coll))))
(define ho-map-indexed (define ho-map-indexed
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((f (trampoline (eval-expr (first args) env))) (let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env)))) (coll (trampoline (eval-expr (nth args 1) env))))
(map-indexed (map-indexed
@@ -850,7 +887,7 @@
coll)))) coll))))
(define ho-filter (define ho-filter
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((f (trampoline (eval-expr (first args) env))) (let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env)))) (coll (trampoline (eval-expr (nth args 1) env))))
(filter (filter
@@ -858,7 +895,7 @@
coll)))) coll))))
(define ho-reduce (define ho-reduce
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((f (trampoline (eval-expr (first args) env))) (let ((f (trampoline (eval-expr (first args) env)))
(init (trampoline (eval-expr (nth args 1) env))) (init (trampoline (eval-expr (nth args 1) env)))
(coll (trampoline (eval-expr (nth args 2) env)))) (coll (trampoline (eval-expr (nth args 2) env))))
@@ -868,7 +905,7 @@
coll)))) coll))))
(define ho-some (define ho-some
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((f (trampoline (eval-expr (first args) env))) (let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env)))) (coll (trampoline (eval-expr (nth args 1) env))))
(some (some
@@ -876,7 +913,7 @@
coll)))) coll))))
(define ho-every (define ho-every
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((f (trampoline (eval-expr (first args) env))) (let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env)))) (coll (trampoline (eval-expr (nth args 1) env))))
(every? (every?
@@ -885,7 +922,7 @@
(define ho-for-each (define ho-for-each
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((f (trampoline (eval-expr (first args) env))) (let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env)))) (coll (trampoline (eval-expr (nth args 1) env))))
(for-each (for-each

View File

@@ -22,7 +22,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define parse-key-params (define parse-key-params
(fn (params-expr) (fn ((params-expr :as list))
(let ((params (list)) (let ((params (list))
(in-key false)) (in-key false))
(for-each (for-each
@@ -42,7 +42,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define sf-defhandler (define sf-defhandler
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((name-sym (first args)) (let ((name-sym (first args))
(params-raw (nth args 1)) (params-raw (nth args 1))
(body (nth args 2)) (body (nth args 2))
@@ -58,7 +58,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define sf-defquery (define sf-defquery
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((name-sym (first args)) (let ((name-sym (first args))
(params-raw (nth args 1)) (params-raw (nth args 1))
(name (symbol-name name-sym)) (name (symbol-name name-sym))
@@ -77,7 +77,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define sf-defaction (define sf-defaction
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((name-sym (first args)) (let ((name-sym (first args))
(params-raw (nth args 1)) (params-raw (nth args 1))
(name (symbol-name name-sym)) (name (symbol-name name-sym))
@@ -98,7 +98,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define sf-defpage (define sf-defpage
(fn (args env) (fn ((args :as list) (env :as dict))
(let ((name-sym (first args)) (let ((name-sym (first args))
(name (symbol-name name-sym)) (name (symbol-name name-sym))
(slots {})) (slots {}))
@@ -106,7 +106,7 @@
(let ((i 1) (let ((i 1)
(max-i (len args))) (max-i (len args)))
(for-each (for-each
(fn (idx) (fn ((idx :as number))
(when (and (< idx max-i) (when (and (< idx max-i)
(= (type-of (nth args idx)) "keyword")) (= (type-of (nth args idx)) "keyword"))
(when (< (+ idx 1) max-i) (when (< (+ idx 1) max-i)
@@ -195,28 +195,28 @@
;; Extract stream-id from a data chunk dict, defaulting to "stream-content" ;; Extract stream-id from a data chunk dict, defaulting to "stream-content"
(define stream-chunk-id (define stream-chunk-id
(fn (chunk) (fn ((chunk :as dict))
(if (has-key? chunk "stream-id") (if (has-key? chunk "stream-id")
(get chunk "stream-id") (get chunk "stream-id")
"stream-content"))) "stream-content")))
;; Remove stream-id from chunk, returning only the bindings ;; Remove stream-id from chunk, returning only the bindings
(define stream-chunk-bindings (define stream-chunk-bindings
(fn (chunk) (fn ((chunk :as dict))
(dissoc chunk "stream-id"))) (dissoc chunk "stream-id")))
;; Normalize binding keys: underscore → hyphen ;; Normalize binding keys: underscore → hyphen
(define normalize-binding-key (define normalize-binding-key
(fn (key) (fn ((key :as string))
(replace key "_" "-"))) (replace key "_" "-")))
;; Bind a data chunk's keys into a fresh env (isolated per chunk) ;; Bind a data chunk's keys into a fresh env (isolated per chunk)
(define bind-stream-chunk (define bind-stream-chunk
(fn (chunk base-env) (fn ((chunk :as dict) (base-env :as dict))
(let ((env (merge {} base-env)) (let ((env (merge {} base-env))
(bindings (stream-chunk-bindings chunk))) (bindings (stream-chunk-bindings chunk)))
(for-each (for-each
(fn (key) (fn ((key :as string))
(env-set! env (normalize-binding-key key) (env-set! env (normalize-binding-key key)
(get bindings key))) (get bindings key)))
(keys bindings)) (keys bindings))

View File

@@ -528,7 +528,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define js-mangle (define js-mangle
(fn (name) (fn ((name :as string))
(let ((renamed (get js-renames name))) (let ((renamed (get js-renames name)))
(if (not (nil? renamed)) (if (not (nil? renamed))
renamed renamed
@@ -549,7 +549,7 @@
result)))))))) result))))))))
(define js-kebab-to-camel (define js-kebab-to-camel
(fn (s) (fn ((s :as string))
(let ((parts (split s "-"))) (let ((parts (split s "-")))
(if (<= (len parts) 1) (if (<= (len parts) 1)
s s
@@ -557,7 +557,7 @@
(join "" (map (fn (p) (js-capitalize p)) (rest parts)))))))) (join "" (map (fn (p) (js-capitalize p)) (rest parts))))))))
(define js-capitalize (define js-capitalize
(fn (s) (fn ((s :as string))
(if (empty? s) s (if (empty? s) s
(str (upper (slice s 0 1)) (slice s 1))))) (str (upper (slice s 0 1)) (slice s 1)))))
@@ -567,7 +567,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define js-quote-string (define js-quote-string
(fn (s) (fn ((s :as string))
(str "\"" (str "\""
(replace (replace (replace (replace (replace (replace (replace (replace (replace (replace (replace (replace
s "\\" "\\\\") "\"" "\\\"") "\n" "\\n") "\r" "\\r") "\t" "\\t") "\0" "\\0") s "\\" "\\\\") "\"" "\\\"") "\n" "\\n") "\r" "\\r") "\t" "\\t") "\0" "\\0")
@@ -582,11 +582,11 @@
(list "+" "-" "*" "/" "=" "!=" "<" ">" "<=" ">=" "mod")) (list "+" "-" "*" "/" "=" "!=" "<" ">" "<=" ">=" "mod"))
(define js-infix? (define js-infix?
(fn (op) (fn ((op :as string))
(some (fn (x) (= x op)) js-infix-ops))) (some (fn (x) (= x op)) js-infix-ops)))
(define js-op-symbol (define js-op-symbol
(fn (op) (fn ((op :as string))
(case op (case op
"=" "==" "=" "=="
"!=" "!=" "!=" "!="
@@ -599,13 +599,13 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define js-is-self-tail-recursive? (define js-is-self-tail-recursive?
(fn (name body) (fn ((name :as string) (body :as list))
(if (empty? body) (if (empty? body)
false false
(js-has-tail-call? name (last body))))) (js-has-tail-call? name (last body)))))
(define js-has-tail-call? (define js-has-tail-call?
(fn (name expr) (fn ((name :as string) expr)
(if (not (and (list? expr) (not (empty? expr)))) (if (not (and (list? expr) (not (empty? expr))))
false false
(let ((head (first expr))) (let ((head (first expr)))
@@ -642,7 +642,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define js-emit-tail-as-stmt (define js-emit-tail-as-stmt
(fn (name expr) (fn ((name :as string) expr)
(if (not (and (list? expr) (not (empty? expr)))) (if (not (and (list? expr) (not (empty? expr))))
(str "return " (js-expr expr) ";") (str "return " (js-expr expr) ";")
(let ((head (first expr))) (let ((head (first expr)))
@@ -702,7 +702,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define js-emit-cond-as-loop-stmt (define js-emit-cond-as-loop-stmt
(fn (name clauses) (fn ((name :as string) (clauses :as list))
(if (empty? clauses) (if (empty? clauses)
"return NIL;" "return NIL;"
;; Detect scheme vs clojure ;; Detect scheme vs clojure
@@ -714,7 +714,7 @@
(js-cond-clojure-loop name clauses 0 0 false)))))) (js-cond-clojure-loop name clauses 0 0 false))))))
(define js-cond-scheme-loop (define js-cond-scheme-loop
(fn (name clauses i) (fn ((name :as string) (clauses :as list) (i :as number))
(if (>= i (len clauses)) (if (>= i (len clauses))
"else { return NIL; }" "else { return NIL; }"
(let ((clause (nth clauses i)) (let ((clause (nth clauses i))
@@ -728,7 +728,7 @@
(js-cond-scheme-loop name clauses (+ i 1)))))))) (js-cond-scheme-loop name clauses (+ i 1))))))))
(define js-cond-clojure-loop (define js-cond-clojure-loop
(fn (name clauses i clause-idx has-else) (fn ((name :as string) (clauses :as list) (i :as number) (clause-idx :as number) (has-else :as boolean))
(if (>= i (len clauses)) (if (>= i (len clauses))
(if has-else "" " else { return NIL; }") (if has-else "" " else { return NIL; }")
(let ((c (nth clauses i))) (let ((c (nth clauses i)))
@@ -749,7 +749,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define js-emit-loop-body (define js-emit-loop-body
(fn (name body) (fn ((name :as string) (body :as list))
(if (empty? body) (if (empty? body)
"return NIL;" "return NIL;"
(str (join "\n" (map (fn (e) (js-statement e)) (str (join "\n" (map (fn (e) (js-statement e))
@@ -805,7 +805,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define js-emit-native-dict (define js-emit-native-dict
(fn (d) (fn ((d :as dict))
(let ((items (keys d))) (let ((items (keys d)))
(str "{" (join ", " (map (fn (k) (str "{" (join ", " (map (fn (k)
(str (js-quote-string k) ": " (js-expr (get d k)))) (str (js-quote-string k) ": " (js-expr (get d k))))
@@ -963,11 +963,11 @@
(str "function(" params-str ") { " (join "\n" parts) " }"))))))))) (str "function(" params-str ") { " (join "\n" parts) " }")))))))))
(define js-collect-params (define js-collect-params
(fn (params) (fn ((params :as list))
(js-collect-params-loop params 0 (list) nil))) (js-collect-params-loop params 0 (list) nil)))
(define js-collect-params-loop (define js-collect-params-loop
(fn (params i result rest-name) (fn ((params :as list) (i :as number) (result :as list) rest-name)
(if (>= i (len params)) (if (>= i (len params))
(list result rest-name) (list result rest-name)
(let ((p (nth params i))) (let ((p (nth params i)))
@@ -975,13 +975,25 @@
;; &rest marker ;; &rest marker
(and (= (type-of p) "symbol") (= (symbol-name p) "&rest")) (and (= (type-of p) "symbol") (= (symbol-name p) "&rest"))
(if (< (+ i 1) (len params)) (if (< (+ i 1) (len params))
(js-collect-params-loop params (+ i 2) result (let ((rp (nth params (+ i 1))))
(js-mangle (symbol-name (nth params (+ i 1))))) (js-collect-params-loop params (+ i 2) result
(js-mangle
(if (and (= (type-of rp) "list") (= (len rp) 3)
(= (type-of (nth rp 1)) "keyword")
(= (keyword-name (nth rp 1)) "as"))
(symbol-name (first rp))
(if (= (type-of rp) "symbol") (symbol-name rp) (str rp))))))
(js-collect-params-loop params (+ i 1) result rest-name)) (js-collect-params-loop params (+ i 1) result rest-name))
;; Normal param ;; Normal param
(= (type-of p) "symbol") (= (type-of p) "symbol")
(js-collect-params-loop params (+ i 1) (js-collect-params-loop params (+ i 1)
(append result (js-mangle (symbol-name p))) rest-name) (append result (js-mangle (symbol-name p))) rest-name)
;; Annotated param: (name :as type) → extract name
(and (= (type-of p) "list") (= (len p) 3)
(= (type-of (nth p 1)) "keyword")
(= (keyword-name (nth p 1)) "as"))
(js-collect-params-loop params (+ i 1)
(append result (js-mangle (symbol-name (first p)))) rest-name)
;; Something else ;; Something else
:else :else
(js-collect-params-loop params (+ i 1) (js-collect-params-loop params (+ i 1)
@@ -1024,7 +1036,7 @@
(js-parse-clojure-let-bindings bindings 0 (list)))))) (js-parse-clojure-let-bindings bindings 0 (list))))))
(define js-parse-clojure-let-bindings (define js-parse-clojure-let-bindings
(fn (bindings i result) (fn (bindings (i :as number) (result :as list))
(if (>= i (- (len bindings) 1)) (if (>= i (- (len bindings) 1))
result result
(let ((vname (if (= (type-of (nth bindings i)) "symbol") (let ((vname (if (= (type-of (nth bindings i)) "symbol")
@@ -1050,7 +1062,7 @@
(str (js-emit-clojure-let-vars bindings 0 (list)) " "))))) (str (js-emit-clojure-let-vars bindings 0 (list)) " ")))))
(define js-emit-clojure-let-vars (define js-emit-clojure-let-vars
(fn (bindings i result) (fn (bindings (i :as number) (result :as list))
(if (>= i (- (len bindings) 1)) (if (>= i (- (len bindings) 1))
(join " " result) (join " " result)
(let ((vname (if (= (type-of (nth bindings i)) "symbol") (let ((vname (if (= (type-of (nth bindings i)) "symbol")
@@ -1062,7 +1074,7 @@
;; Helper to append let binding var declarations to a parts list ;; Helper to append let binding var declarations to a parts list
(define js-append-let-binding-parts (define js-append-let-binding-parts
(fn (bindings parts) (fn (bindings (parts :as list))
(when (and (list? bindings) (not (empty? bindings))) (when (and (list? bindings) (not (empty? bindings)))
(if (list? (first bindings)) (if (list? (first bindings))
;; Scheme-style ;; Scheme-style
@@ -1076,7 +1088,7 @@
(js-append-clojure-bindings bindings parts 0))))) (js-append-clojure-bindings bindings parts 0)))))
(define js-append-clojure-bindings (define js-append-clojure-bindings
(fn (bindings parts i) (fn (bindings (parts :as list) (i :as number))
(when (< i (- (len bindings) 1)) (when (< i (- (len bindings) 1))
(let ((vname (if (= (type-of (nth bindings i)) "symbol") (let ((vname (if (= (type-of (nth bindings i)) "symbol")
(symbol-name (nth bindings i)) (symbol-name (nth bindings i))
@@ -1105,7 +1117,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define js-emit-cond (define js-emit-cond
(fn (clauses) (fn ((clauses :as list))
(if (empty? clauses) (if (empty? clauses)
"NIL" "NIL"
;; Detect scheme vs clojure style ;; Detect scheme vs clojure style
@@ -1123,7 +1135,7 @@
(and (= (type-of test) "keyword") (= (keyword-name test) "else"))))) (and (= (type-of test) "keyword") (= (keyword-name test) "else")))))
(define js-cond-scheme (define js-cond-scheme
(fn (clauses) (fn ((clauses :as list))
(if (empty? clauses) (if (empty? clauses)
"NIL" "NIL"
(let ((clause (first clauses)) (let ((clause (first clauses))
@@ -1135,7 +1147,7 @@
" : " (js-cond-scheme (rest clauses)) ")")))))) " : " (js-cond-scheme (rest clauses)) ")"))))))
(define js-cond-clojure (define js-cond-clojure
(fn (clauses) (fn ((clauses :as list))
(if (< (len clauses) 2) (if (< (len clauses) 2)
"NIL" "NIL"
(let ((test (first clauses)) (let ((test (first clauses))
@@ -1151,14 +1163,14 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define js-emit-case (define js-emit-case
(fn (args) (fn ((args :as list))
(let ((match-expr (js-expr (first args))) (let ((match-expr (js-expr (first args)))
(clauses (rest args))) (clauses (rest args)))
(str "(function() { var _m = " match-expr "; " (str "(function() { var _m = " match-expr "; "
(js-case-chain clauses) " })()")))) (js-case-chain clauses) " })()"))))
(define js-case-chain (define js-case-chain
(fn (clauses) (fn ((clauses :as list))
(if (< (len clauses) 2) (if (< (len clauses) 2)
"return NIL;" "return NIL;"
(let ((test (nth clauses 0)) (let ((test (nth clauses 0))
@@ -1175,7 +1187,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define js-emit-and (define js-emit-and
(fn (args) (fn ((args :as list))
(let ((parts (map js-expr args))) (let ((parts (map js-expr args)))
(if (= (len parts) 1) (if (= (len parts) 1)
(first parts) (first parts)
@@ -1190,7 +1202,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define js-emit-or (define js-emit-or
(fn (args) (fn ((args :as list))
(if (= (len args) 1) (if (= (len args) 1)
(js-expr (first args)) (js-expr (first args))
(str "sxOr(" (join ", " (map js-expr args)) ")")))) (str "sxOr(" (join ", " (map js-expr args)) ")"))))
@@ -1201,7 +1213,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define js-emit-do (define js-emit-do
(fn (args) (fn ((args :as list))
(if (= (len args) 1) (if (= (len args) 1)
(js-expr (first args)) (js-expr (first args))
(str "(" (join ", " (map js-expr args)) ")")))) (str "(" (join ", " (map js-expr args)) ")"))))
@@ -1212,11 +1224,11 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define js-emit-dict-literal (define js-emit-dict-literal
(fn (pairs) (fn ((pairs :as list))
(str "{" (js-dict-pairs-str pairs 0 (list)) "}"))) (str "{" (js-dict-pairs-str pairs 0 (list)) "}")))
(define js-dict-pairs-str (define js-dict-pairs-str
(fn (pairs i result) (fn ((pairs :as list) (i :as number) (result :as list))
(if (>= i (- (len pairs) 1)) (if (>= i (- (len pairs) 1))
(join ", " result) (join ", " result)
(let ((key (nth pairs i)) (let ((key (nth pairs i))
@@ -1234,7 +1246,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define js-emit-infix (define js-emit-infix
(fn (op args) (fn ((op :as string) (args :as list))
(let ((js-op (js-op-symbol op))) (let ((js-op (js-op-symbol op)))
(if (and (= (len args) 1) (= op "-")) (if (and (= (len args) 1) (= op "-"))
(str "(-" (js-expr (first args)) ")") (str "(-" (js-expr (first args)) ")")
@@ -1290,8 +1302,9 @@
(= name "append!") (= name "append!")
(str (js-expr (nth expr 1)) ".push(" (js-expr (nth expr 2)) ");") (str (js-expr (nth expr 1)) ".push(" (js-expr (nth expr 2)) ");")
(= name "env-set!") (= name "env-set!")
(str (js-expr (nth expr 1)) "[" (js-expr (nth expr 2)) (str "envSet(" (js-expr (nth expr 1))
"] = " (js-expr (nth expr 3)) ";") ", " (js-expr (nth expr 2))
", " (js-expr (nth expr 3)) ");")
(= name "set-lambda-name!") (= name "set-lambda-name!")
(str (js-expr (nth expr 1)) ".name = " (js-expr (nth expr 2)) ";") (str (js-expr (nth expr 1)) ".name = " (js-expr (nth expr 2)) ";")
:else :else
@@ -1356,9 +1369,16 @@
;; Inline lambda → for loop ;; Inline lambda → for loop
(let ((params (nth fn-expr 1)) (let ((params (nth fn-expr 1))
(body (rest (rest fn-expr))) (body (rest (rest fn-expr)))
(p (if (= (type-of (first params)) "symbol") (raw-p (first params))
(symbol-name (first params)) (p (cond
(str (first params)))) (= (type-of raw-p) "symbol")
(symbol-name raw-p)
;; (name :as type) annotation → extract name
(and (= (type-of raw-p) "list") (= (len raw-p) 3)
(= (type-of (nth raw-p 1)) "keyword")
(= (keyword-name (nth raw-p 1)) "as"))
(symbol-name (first raw-p))
:else (str raw-p)))
(p-js (js-mangle p))) (p-js (js-mangle p)))
(str "{ var _c = " coll "; for (var _i = 0; _i < _c.length; _i++) { var " (str "{ var _c = " coll "; for (var _i = 0; _i < _c.length; _i++) { var "
p-js " = _c[_i]; " p-js " = _c[_i]; "
@@ -1373,7 +1393,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define js-translate-file (define js-translate-file
(fn (defines) (fn ((defines :as list))
(join "\n" (map (fn (pair) (join "\n" (map (fn (pair)
(let ((name (first pair)) (let ((name (first pair))
(expr (nth pair 1))) (expr (nth pair 1)))

View File

@@ -34,7 +34,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define dispatch-trigger-events (define dispatch-trigger-events
(fn (el header-val) (fn (el (header-val :as string))
;; Dispatch events from SX-Trigger / SX-Trigger-After-Swap headers. ;; Dispatch events from SX-Trigger / SX-Trigger-After-Swap headers.
;; Value can be JSON object (name → detail) or comma-separated names. ;; Value can be JSON object (name → detail) or comma-separated names.
(when header-val (when header-val
@@ -42,12 +42,12 @@
(if parsed (if parsed
;; JSON object: keys are event names, values are detail ;; JSON object: keys are event names, values are detail
(for-each (for-each
(fn (key) (fn ((key :as string))
(dom-dispatch el key (get parsed key))) (dom-dispatch el key (get parsed key)))
(keys parsed)) (keys parsed))
;; Comma-separated event names ;; Comma-separated event names
(for-each (for-each
(fn (name) (fn ((name :as string))
(let ((trimmed (trim name))) (let ((trimmed (trim name)))
(when (not (empty? trimmed)) (when (not (empty? trimmed))
(dom-dispatch el trimmed (dict))))) (dom-dispatch el trimmed (dict)))))
@@ -73,7 +73,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define execute-request (define execute-request
(fn (el verbInfo extraParams) (fn (el (verbInfo :as dict) (extraParams :as dict))
;; Gate checks then delegate to do-fetch. ;; Gate checks then delegate to do-fetch.
;; verbInfo: dict with "method" and "url" (or nil to read from element). ;; verbInfo: dict with "method" and "url" (or nil to read from element).
;; Re-read from element in case attributes were morphed since binding. ;; Re-read from element in case attributes were morphed since binding.
@@ -106,7 +106,7 @@
(define do-fetch (define do-fetch
(fn (el verb method url extraParams) (fn (el (verb :as string) (method :as string) (url :as string) (extraParams :as dict))
;; Execute the actual fetch. Manages abort, headers, body, loading state. ;; Execute the actual fetch. Manages abort, headers, body, loading state.
(let ((sync (dom-get-attr el "sx-sync"))) (let ((sync (dom-get-attr el "sx-sync")))
;; Abort previous if sync mode (per-element) ;; Abort previous if sync mode (per-element)
@@ -140,7 +140,7 @@
;; Merge extra params as headers ;; Merge extra params as headers
(when extraParams (when extraParams
(for-each (for-each
(fn (k) (dict-set! headers k (get extraParams k))) (fn ((k :as string)) (dict-set! headers k (get extraParams k)))
(keys extraParams))) (keys extraParams)))
;; Content-Type ;; Content-Type
@@ -172,7 +172,7 @@
"cross-origin" (cross-origin? final-url) "cross-origin" (cross-origin? final-url)
"preloaded" cached) "preloaded" cached)
;; Success callback ;; Success callback
(fn (resp-ok status get-header text) (fn ((resp-ok :as boolean) (status :as number) get-header (text :as string))
(do (do
(clear-loading-state el indicator disabled-elts) (clear-loading-state el indicator disabled-elts)
(revert-optimistic optimistic-state) (revert-optimistic optimistic-state)
@@ -202,7 +202,7 @@
(define handle-fetch-success (define handle-fetch-success
(fn (el url verb extraParams get-header text) (fn (el (url :as string) (verb :as string) (extraParams :as dict) get-header (text :as string))
;; Route a successful response through the appropriate handler. ;; Route a successful response through the appropriate handler.
(let ((resp-headers (process-response-headers get-header))) (let ((resp-headers (process-response-headers get-header)))
;; CSS hash update ;; CSS hash update
@@ -270,7 +270,7 @@
(define handle-sx-response (define handle-sx-response
(fn (el target text swap-style use-transition) (fn (el target (text :as string) (swap-style :as string) (use-transition :as boolean))
;; Handle SX-format response: strip components, extract CSS, render, swap. ;; Handle SX-format response: strip components, extract CSS, render, swap.
(let ((cleaned (strip-component-scripts text))) (let ((cleaned (strip-component-scripts text)))
(let ((final (extract-response-css cleaned))) (let ((final (extract-response-css cleaned)))
@@ -281,7 +281,7 @@
(dom-append container rendered) (dom-append container rendered)
;; Process OOB swaps ;; Process OOB swaps
(process-oob-swaps container (process-oob-swaps container
(fn (t oob s) (fn (t oob (s :as string))
(dispose-islands-in t) (dispose-islands-in t)
(swap-dom-nodes t oob s) (swap-dom-nodes t oob s)
(sx-hydrate t) (sx-hydrate t)
@@ -301,7 +301,7 @@
(define handle-html-response (define handle-html-response
(fn (el target text swap-style use-transition) (fn (el target (text :as string) (swap-style :as string) (use-transition :as boolean))
;; Handle HTML-format response: parse, OOB, select, swap. ;; Handle HTML-format response: parse, OOB, select, swap.
(let ((doc (dom-parse-html-document text))) (let ((doc (dom-parse-html-document text)))
(when doc (when doc
@@ -320,7 +320,7 @@
(dom-set-inner-html container (dom-body-inner-html doc)) (dom-set-inner-html container (dom-body-inner-html doc))
;; Process OOB swaps ;; Process OOB swaps
(process-oob-swaps container (process-oob-swaps container
(fn (t oob s) (fn (t oob (s :as string))
(dispose-islands-in t) (dispose-islands-in t)
(swap-dom-nodes t oob s) (swap-dom-nodes t oob s)
(post-swap t))) (post-swap t)))
@@ -338,7 +338,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define handle-retry (define handle-retry
(fn (el verb method url extraParams) (fn (el (verb :as string) (method :as string) (url :as string) (extraParams :as dict))
;; Handle retry on failure if sx-retry is configured ;; Handle retry on failure if sx-retry is configured
(let ((retry-attr (dom-get-attr el "sx-retry")) (let ((retry-attr (dom-get-attr el "sx-retry"))
(spec (parse-retry-spec retry-attr))) (spec (parse-retry-spec retry-attr)))
@@ -358,12 +358,12 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define bind-triggers (define bind-triggers
(fn (el verbInfo) (fn (el (verbInfo :as dict))
;; Bind triggers from sx-trigger attribute (or defaults) ;; Bind triggers from sx-trigger attribute (or defaults)
(let ((triggers (or (parse-trigger-spec (dom-get-attr el "sx-trigger")) (let ((triggers (or (parse-trigger-spec (dom-get-attr el "sx-trigger"))
(default-trigger (dom-tag-name el))))) (default-trigger (dom-tag-name el)))))
(for-each (for-each
(fn (trigger) (fn ((trigger :as dict))
(let ((kind (classify-trigger trigger)) (let ((kind (classify-trigger trigger))
(mods (get trigger "modifiers"))) (mods (get trigger "modifiers")))
(cond (cond
@@ -393,7 +393,7 @@
(define bind-event (define bind-event
(fn (el event-name mods verbInfo) (fn (el (event-name :as string) (mods :as dict) (verbInfo :as dict))
;; Bind a standard DOM event trigger. ;; Bind a standard DOM event trigger.
;; Handles delay, once, changed, optimistic, preventDefault. ;; Handles delay, once, changed, optimistic, preventDefault.
(let ((timer nil) (let ((timer nil)
@@ -506,12 +506,12 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define process-oob-swaps (define process-oob-swaps
(fn (container swap-fn) (fn (container (swap-fn :as lambda))
;; Find and process out-of-band swaps in container. ;; Find and process out-of-band swaps in container.
;; swap-fn is (fn (target oob-element swap-type) ...). ;; swap-fn is (fn (target oob-element swap-type) ...).
(let ((oobs (find-oob-swaps container))) (let ((oobs (find-oob-swaps container)))
(for-each (for-each
(fn (oob) (fn ((oob :as dict))
(let ((target-id (get oob "target-id")) (let ((target-id (get oob "target-id"))
(target (dom-query-by-id target-id)) (target (dom-query-by-id target-id))
(oob-el (get oob "element")) (oob-el (get oob "element"))
@@ -610,7 +610,7 @@
(define _page-data-cache-ttl 30000) ;; 30 seconds in ms (define _page-data-cache-ttl 30000) ;; 30 seconds in ms
(define page-data-cache-key (define page-data-cache-key
(fn (page-name params) (fn ((page-name :as string) (params :as dict))
;; Build a cache key from page name + params. ;; Build a cache key from page name + params.
;; Params are from route matching so order is deterministic. ;; Params are from route matching so order is deterministic.
(let ((base page-name)) (let ((base page-name))
@@ -618,13 +618,13 @@
base base
(let ((parts (list))) (let ((parts (list)))
(for-each (for-each
(fn (k) (fn ((k :as string))
(append! parts (str k "=" (get params k)))) (append! parts (str k "=" (get params k))))
(keys params)) (keys params))
(str base ":" (join "&" parts))))))) (str base ":" (join "&" parts)))))))
(define page-data-cache-get (define page-data-cache-get
(fn (cache-key) (fn ((cache-key :as string))
;; Return cached data if fresh, else nil. ;; Return cached data if fresh, else nil.
(let ((entry (get _page-data-cache cache-key))) (let ((entry (get _page-data-cache cache-key)))
(if (nil? entry) (if (nil? entry)
@@ -636,7 +636,7 @@
(get entry "data")))))) (get entry "data"))))))
(define page-data-cache-set (define page-data-cache-set
(fn (cache-key data) (fn ((cache-key :as string) data)
;; Store data with current timestamp. ;; Store data with current timestamp.
(dict-set! _page-data-cache cache-key (dict-set! _page-data-cache cache-key
{"data" data "ts" (now-ms)}))) {"data" data "ts" (now-ms)})))
@@ -647,12 +647,12 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define invalidate-page-cache (define invalidate-page-cache
(fn (page-name) (fn ((page-name :as string))
;; Clear cached data for a page. Removes all cache entries whose key ;; Clear cached data for a page. Removes all cache entries whose key
;; matches page-name (exact) or starts with "page-name:" (with params). ;; matches page-name (exact) or starts with "page-name:" (with params).
;; Also notifies the service worker to clear its IndexedDB entries. ;; Also notifies the service worker to clear its IndexedDB entries.
(for-each (for-each
(fn (k) (fn ((k :as string))
(when (or (= k page-name) (starts-with? k (str page-name ":"))) (when (or (= k page-name) (starts-with? k (str page-name ":")))
(dict-set! _page-data-cache k nil))) (dict-set! _page-data-cache k nil)))
(keys _page-data-cache)) (keys _page-data-cache))
@@ -667,7 +667,7 @@
(log-info "sx:cache invalidate *"))) (log-info "sx:cache invalidate *")))
(define update-page-cache (define update-page-cache
(fn (page-name data) (fn ((page-name :as string) data)
;; Replace cached data for a page with server-provided data. ;; Replace cached data for a page with server-provided data.
;; Uses a bare page-name key (no params) — the server knows the ;; Uses a bare page-name key (no params) — the server knows the
;; canonical data shape for the page. ;; canonical data shape for the page.
@@ -676,7 +676,7 @@
(log-info (str "sx:cache update " page-name))))) (log-info (str "sx:cache update " page-name)))))
(define process-cache-directives (define process-cache-directives
(fn (el resp-headers response-text) (fn (el (resp-headers :as dict) (response-text :as string))
;; Process cache invalidation and update directives from both ;; Process cache invalidation and update directives from both
;; element attributes and response headers. ;; element attributes and response headers.
;; ;;
@@ -722,7 +722,7 @@
(define _optimistic-snapshots (dict)) (define _optimistic-snapshots (dict))
(define optimistic-cache-update (define optimistic-cache-update
(fn (cache-key mutator) (fn ((cache-key :as string) (mutator :as lambda))
;; Apply predicted mutation to cached data. Saves snapshot for rollback. ;; Apply predicted mutation to cached data. Saves snapshot for rollback.
;; Returns predicted data or nil if no cached data exists. ;; Returns predicted data or nil if no cached data exists.
(let ((cached (page-data-cache-get cache-key))) (let ((cached (page-data-cache-get cache-key)))
@@ -735,7 +735,7 @@
predicted))))) predicted)))))
(define optimistic-cache-revert (define optimistic-cache-revert
(fn (cache-key) (fn ((cache-key :as string))
;; Revert to pre-mutation snapshot. Returns restored data or nil. ;; Revert to pre-mutation snapshot. Returns restored data or nil.
(let ((snapshot (get _optimistic-snapshots cache-key))) (let ((snapshot (get _optimistic-snapshots cache-key)))
(when snapshot (when snapshot
@@ -744,12 +744,12 @@
snapshot)))) snapshot))))
(define optimistic-cache-confirm (define optimistic-cache-confirm
(fn (cache-key) (fn ((cache-key :as string))
;; Server accepted — discard the rollback snapshot. ;; Server accepted — discard the rollback snapshot.
(dict-delete! _optimistic-snapshots cache-key))) (dict-delete! _optimistic-snapshots cache-key)))
(define submit-mutation (define submit-mutation
(fn (page-name params action-name payload mutator-fn on-complete) (fn ((page-name :as string) (params :as dict) (action-name :as string) payload (mutator-fn :as lambda) (on-complete :as lambda))
;; Optimistic mutation: predict locally, send to server, confirm or revert. ;; Optimistic mutation: predict locally, send to server, confirm or revert.
;; on-complete is called with "confirmed" or "reverted" status. ;; on-complete is called with "confirmed" or "reverted" status.
(let ((cache-key (page-data-cache-key page-name params)) (let ((cache-key (page-data-cache-key page-name params))
@@ -768,7 +768,7 @@
(try-rerender-page page-name params result)) (try-rerender-page page-name params result))
(log-info (str "sx:optimistic confirmed " page-name)) (log-info (str "sx:optimistic confirmed " page-name))
(when on-complete (on-complete "confirmed"))) (when on-complete (on-complete "confirmed")))
(fn (error) (fn ((error :as string))
;; Failure: revert to snapshot ;; Failure: revert to snapshot
(let ((reverted (optimistic-cache-revert cache-key))) (let ((reverted (optimistic-cache-revert cache-key)))
(when reverted (when reverted
@@ -791,11 +791,11 @@
(fn () _is-online)) (fn () _is-online))
(define offline-set-online! (define offline-set-online!
(fn (val) (fn ((val :as boolean))
(set! _is-online val))) (set! _is-online val)))
(define offline-queue-mutation (define offline-queue-mutation
(fn (action-name payload page-name params mutator-fn) (fn ((action-name :as string) payload (page-name :as string) (params :as dict) (mutator-fn :as lambda))
;; Queue a mutation for later sync. Apply optimistic update locally. ;; Queue a mutation for later sync. Apply optimistic update locally.
(let ((cache-key (page-data-cache-key page-name params)) (let ((cache-key (page-data-cache-key page-name params))
(entry (dict (entry (dict
@@ -816,26 +816,26 @@
(define offline-sync (define offline-sync
(fn () (fn ()
;; Replay all pending mutations. Called on reconnect. ;; Replay all pending mutations. Called on reconnect.
(let ((pending (filter (fn (e) (= (get e "status") "pending")) _offline-queue))) (let ((pending (filter (fn ((e :as dict)) (= (get e "status") "pending")) _offline-queue)))
(when (not (empty? pending)) (when (not (empty? pending))
(log-info (str "sx:offline syncing " (len pending) " mutations")) (log-info (str "sx:offline syncing " (len pending) " mutations"))
(for-each (for-each
(fn (entry) (fn ((entry :as dict))
(execute-action (get entry "action") (get entry "payload") (execute-action (get entry "action") (get entry "payload")
(fn (result) (fn (result)
(dict-set! entry "status" "synced") (dict-set! entry "status" "synced")
(log-info (str "sx:offline synced " (get entry "action")))) (log-info (str "sx:offline synced " (get entry "action"))))
(fn (error) (fn ((error :as string))
(dict-set! entry "status" "failed") (dict-set! entry "status" "failed")
(log-warn (str "sx:offline sync failed " (get entry "action") ": " error))))) (log-warn (str "sx:offline sync failed " (get entry "action") ": " error)))))
pending))))) pending)))))
(define offline-pending-count (define offline-pending-count
(fn () (fn ()
(len (filter (fn (e) (= (get e "status") "pending")) _offline-queue)))) (len (filter (fn ((e :as dict)) (= (get e "status") "pending")) _offline-queue))))
(define offline-aware-mutation (define offline-aware-mutation
(fn (page-name params action-name payload mutator-fn on-complete) (fn ((page-name :as string) (params :as dict) (action-name :as string) payload (mutator-fn :as lambda) (on-complete :as lambda))
;; Top-level mutation function. Routes to submit-mutation when online, ;; Top-level mutation function. Routes to submit-mutation when online,
;; offline-queue-mutation when offline. ;; offline-queue-mutation when offline.
(if _is-online (if _is-online
@@ -860,7 +860,7 @@
(define swap-rendered-content (define swap-rendered-content
(fn (target rendered pathname) (fn (target rendered (pathname :as string))
;; Swap rendered DOM content into target and run post-processing. ;; Swap rendered DOM content into target and run post-processing.
;; Shared by pure and data page client routes. ;; Shared by pure and data page client routes.
(do (do
@@ -876,7 +876,7 @@
(define resolve-route-target (define resolve-route-target
(fn (target-sel) (fn ((target-sel :as string))
;; Resolve a target selector to a DOM element, or nil. ;; Resolve a target selector to a DOM element, or nil.
(if (and target-sel (not (= target-sel "true"))) (if (and target-sel (not (= target-sel "true")))
(dom-query target-sel) (dom-query target-sel)
@@ -884,17 +884,17 @@
(define deps-satisfied? (define deps-satisfied?
(fn (match) (fn ((match :as dict))
;; Check if all component deps for a page are loaded client-side. ;; Check if all component deps for a page are loaded client-side.
(let ((deps (get match "deps")) (let ((deps (get match "deps"))
(loaded (loaded-component-names))) (loaded (loaded-component-names)))
(if (or (nil? deps) (empty? deps)) (if (or (nil? deps) (empty? deps))
true true
(every? (fn (dep) (contains? loaded dep)) deps))))) (every? (fn ((dep :as string)) (contains? loaded dep)) deps)))))
(define try-client-route (define try-client-route
(fn (pathname target-sel) (fn ((pathname :as string) (target-sel :as string))
;; Try to render a page client-side. Returns true if successful, false otherwise. ;; Try to render a page client-side. Returns true if successful, false otherwise.
;; target-sel is the CSS selector for the swap target (from sx-boost value). ;; target-sel is the CSS selector for the swap target (from sx-boost value).
;; For pure pages: renders immediately. For :data pages: fetches data then renders. ;; For pure pages: renders immediately. For :data pages: fetches data then renders.
@@ -968,7 +968,7 @@
(do (do
(log-info (str "sx:route client+data " pathname)) (log-info (str "sx:route client+data " pathname))
(resolve-page-data page-name params (resolve-page-data page-name params
(fn (data) (fn ((data :as dict))
(page-data-cache-set cache-key data) (page-data-cache-set cache-key data)
(let ((env (merge closure params data))) (let ((env (merge closure params data)))
(if has-io (if has-io
@@ -1012,7 +1012,7 @@
(define bind-client-route-link (define bind-client-route-link
(fn (link href) (fn (link (href :as string))
;; Bind a boost link with client-side routing. If the route can be ;; Bind a boost link with client-side routing. If the route can be
;; rendered client-side (pure page, no :data), do so. Otherwise ;; rendered client-side (pure page, no :data), do so. Otherwise
;; fall back to standard server fetch via bind-boost-link. ;; fall back to standard server fetch via bind-boost-link.
@@ -1045,12 +1045,12 @@
(let ((source (event-source-connect url el)) (let ((source (event-source-connect url el))
(event-name (parse-sse-swap el))) (event-name (parse-sse-swap el)))
(event-source-listen source event-name (event-source-listen source event-name
(fn (data) (fn ((data :as string))
(bind-sse-swap el data)))))))) (bind-sse-swap el data))))))))
(define bind-sse-swap (define bind-sse-swap
(fn (el data) (fn (el (data :as string))
;; Handle an SSE event: swap data into element ;; Handle an SSE event: swap data into element
(let ((target (resolve-target el)) (let ((target (resolve-target el))
(swap-spec (parse-swap-spec (swap-spec (parse-swap-spec
@@ -1089,7 +1089,7 @@
(for-each (for-each
(fn (el) (fn (el)
(for-each (for-each
(fn (attr) (fn ((attr :as list))
(let ((name (first attr)) (let ((name (first attr))
(body (nth attr 1))) (body (nth attr 1)))
(when (starts-with? name "sx-on:") (when (starts-with? name "sx-on:")
@@ -1135,7 +1135,7 @@
(define do-preload (define do-preload
(fn (url headers) (fn ((url :as string) (headers :as dict))
;; Execute a preload fetch into the cache ;; Execute a preload fetch into the cache
(when (nil? (preload-cache-get _preload-cache url)) (when (nil? (preload-cache-get _preload-cache url))
(fetch-preload url headers _preload-cache)))) (fetch-preload url headers _preload-cache))))
@@ -1215,7 +1215,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define handle-popstate (define handle-popstate
(fn (scrollY) (fn ((scrollY :as number))
;; Handle browser back/forward navigation. ;; Handle browser back/forward navigation.
;; Derive target from [sx-boost] container or fall back to #main-panel. ;; Derive target from [sx-boost] container or fall back to #main-panel.
;; Try client-side route first, fall back to server fetch. ;; Try client-side route first, fall back to server fetch.

View File

@@ -0,0 +1,368 @@
;; ==========================================================================
;; page-helpers.sx — Pure data-transformation page helpers
;;
;; These functions take raw data (from Python I/O edge) and return
;; structured dicts for page rendering. No I/O — pure transformations
;; only. Bootstrapped to every host.
;; ==========================================================================
;; --------------------------------------------------------------------------
;; categorize-special-forms
;;
;; Parses define-special-form declarations from special-forms.sx AST,
;; categorizes each form by name lookup, returns dict of category → forms.
;; --------------------------------------------------------------------------
(define special-form-category-map
{"if" "Control Flow" "when" "Control Flow" "cond" "Control Flow"
"case" "Control Flow" "and" "Control Flow" "or" "Control Flow"
"let" "Binding" "let*" "Binding" "letrec" "Binding"
"define" "Binding" "set!" "Binding"
"lambda" "Functions & Components" "fn" "Functions & Components"
"defcomp" "Functions & Components" "defmacro" "Functions & Components"
"begin" "Sequencing & Threading" "do" "Sequencing & Threading"
"->" "Sequencing & Threading"
"quote" "Quoting" "quasiquote" "Quoting"
"reset" "Continuations" "shift" "Continuations"
"dynamic-wind" "Guards"
"map" "Higher-Order Forms" "map-indexed" "Higher-Order Forms"
"filter" "Higher-Order Forms" "reduce" "Higher-Order Forms"
"some" "Higher-Order Forms" "every?" "Higher-Order Forms"
"for-each" "Higher-Order Forms"
"defstyle" "Domain Definitions"
"defhandler" "Domain Definitions" "defpage" "Domain Definitions"
"defquery" "Domain Definitions" "defaction" "Domain Definitions"})
(define extract-define-kwargs
(fn ((expr :as list))
;; Extract keyword args from a define-special-form expression.
;; Returns dict of keyword-name → string value.
;; Walks items pairwise: when item[i] is a keyword, item[i+1] is its value.
(let ((result {})
(items (slice expr 2))
(n (len items)))
(for-each
(fn ((idx :as number))
(when (and (< (+ idx 1) n)
(= (type-of (nth items idx)) "keyword"))
(let ((key (keyword-name (nth items idx)))
(val (nth items (+ idx 1))))
(dict-set! result key
(if (= (type-of val) "list")
(str "(" (join " " (map serialize val)) ")")
(str val))))))
(range 0 n))
result)))
(define categorize-special-forms
(fn ((parsed-exprs :as list))
;; parsed-exprs: result of parse-all on special-forms.sx
;; Returns dict of category-name → list of form dicts.
(let ((categories {}))
(for-each
(fn (expr)
(when (and (= (type-of expr) "list")
(>= (len expr) 2)
(= (type-of (first expr)) "symbol")
(= (symbol-name (first expr)) "define-special-form"))
(let ((name (nth expr 1))
(kwargs (extract-define-kwargs expr))
(category (or (get special-form-category-map name) "Other")))
(when (not (has-key? categories category))
(dict-set! categories category (list)))
(append! (get categories category)
{"name" name
"syntax" (or (get kwargs "syntax") "")
"doc" (or (get kwargs "doc") "")
"tail-position" (or (get kwargs "tail-position") "")
"example" (or (get kwargs "example") "")}))))
parsed-exprs)
categories)))
;; --------------------------------------------------------------------------
;; build-reference-data
;;
;; Takes a slug and raw reference data, returns structured dict for rendering.
;; --------------------------------------------------------------------------
(define build-ref-items-with-href
(fn ((items :as list) (base-path :as string) (detail-keys :as list) (n-fields :as number))
;; items: list of lists (tuples), each with n-fields elements
;; base-path: e.g. "/geography/hypermedia/reference/attributes/"
;; detail-keys: list of strings (keys that have detail pages)
;; n-fields: 2 or 3 (number of fields per tuple)
(map
(fn ((item :as list))
(if (= n-fields 3)
;; [name, desc/value, exists/desc]
(let ((name (nth item 0))
(field2 (nth item 1))
(field3 (nth item 2)))
{"name" name
"desc" field2
"exists" field3
"href" (if (and field3 (some (fn ((k :as string)) (= k name)) detail-keys))
(str base-path name)
nil)})
;; [name, desc]
(let ((name (nth item 0))
(desc (nth item 1)))
{"name" name
"desc" desc
"href" (if (some (fn ((k :as string)) (= k name)) detail-keys)
(str base-path name)
nil)})))
items)))
(define build-reference-data
(fn ((slug :as string) (raw-data :as dict) (detail-keys :as list))
;; slug: "attributes", "headers", "events", "js-api"
;; raw-data: dict with the raw data lists for this slug
;; detail-keys: list of names that have detail pages
(case slug
"attributes"
{"req-attrs" (build-ref-items-with-href
(get raw-data "req-attrs")
"/geography/hypermedia/reference/attributes/" detail-keys 3)
"beh-attrs" (build-ref-items-with-href
(get raw-data "beh-attrs")
"/geography/hypermedia/reference/attributes/" detail-keys 3)
"uniq-attrs" (build-ref-items-with-href
(get raw-data "uniq-attrs")
"/geography/hypermedia/reference/attributes/" detail-keys 3)}
"headers"
{"req-headers" (build-ref-items-with-href
(get raw-data "req-headers")
"/geography/hypermedia/reference/headers/" detail-keys 3)
"resp-headers" (build-ref-items-with-href
(get raw-data "resp-headers")
"/geography/hypermedia/reference/headers/" detail-keys 3)}
"events"
{"events-list" (build-ref-items-with-href
(get raw-data "events-list")
"/geography/hypermedia/reference/events/" detail-keys 2)}
"js-api"
{"js-api-list" (map (fn ((item :as list)) {"name" (nth item 0) "desc" (nth item 1)})
(get raw-data "js-api-list"))}
;; default: attributes
:else
{"req-attrs" (build-ref-items-with-href
(get raw-data "req-attrs")
"/geography/hypermedia/reference/attributes/" detail-keys 3)
"beh-attrs" (build-ref-items-with-href
(get raw-data "beh-attrs")
"/geography/hypermedia/reference/attributes/" detail-keys 3)
"uniq-attrs" (build-ref-items-with-href
(get raw-data "uniq-attrs")
"/geography/hypermedia/reference/attributes/" detail-keys 3)})))
;; --------------------------------------------------------------------------
;; build-attr-detail / build-header-detail / build-event-detail
;;
;; Lookup a slug in a detail dict, reshape for page rendering.
;; --------------------------------------------------------------------------
(define build-attr-detail
(fn ((slug :as string) detail)
;; detail: dict with "description", "example", "handler", "demo" keys or nil
(if (nil? detail)
{"attr-not-found" true}
{"attr-not-found" nil
"attr-title" slug
"attr-description" (get detail "description")
"attr-example" (get detail "example")
"attr-handler" (get detail "handler")
"attr-demo" (get detail "demo")
"attr-wire-id" (if (has-key? detail "handler")
(str "ref-wire-"
(replace (replace slug ":" "-") "*" "star"))
nil)})))
(define build-header-detail
(fn ((slug :as string) detail)
(if (nil? detail)
{"header-not-found" true}
{"header-not-found" nil
"header-title" slug
"header-direction" (get detail "direction")
"header-description" (get detail "description")
"header-example" (get detail "example")
"header-demo" (get detail "demo")})))
(define build-event-detail
(fn ((slug :as string) detail)
(if (nil? detail)
{"event-not-found" true}
{"event-not-found" nil
"event-title" slug
"event-description" (get detail "description")
"event-example" (get detail "example")
"event-demo" (get detail "demo")})))
;; --------------------------------------------------------------------------
;; build-component-source
;;
;; Reconstruct defcomp/defisland source from component metadata.
;; --------------------------------------------------------------------------
(define build-component-source
(fn ((comp-data :as dict))
;; comp-data: dict with "type", "name", "params", "has-children", "body-sx", "affinity"
(let ((comp-type (get comp-data "type"))
(name (get comp-data "name"))
(params (get comp-data "params"))
(has-children (get comp-data "has-children"))
(body-sx (get comp-data "body-sx"))
(affinity (get comp-data "affinity")))
(if (= comp-type "not-found")
(str ";; component " name " not found")
(let ((param-strs (if (empty? params)
(if has-children
(list "&rest" "children")
(list))
(if has-children
(append (cons "&key" params) (list "&rest" "children"))
(cons "&key" params))))
(params-sx (str "(" (join " " param-strs) ")"))
(form-name (if (= comp-type "island") "defisland" "defcomp"))
(affinity-str (if (and (= comp-type "component")
(not (nil? affinity))
(not (= affinity "auto")))
(str " :affinity " affinity)
"")))
(str "(" form-name " " name " " params-sx affinity-str "\n " body-sx ")"))))))
;; --------------------------------------------------------------------------
;; build-bundle-analysis
;;
;; Compute per-page bundle stats from pre-extracted component data.
;; --------------------------------------------------------------------------
(define build-bundle-analysis
(fn ((pages-raw :as list) (components-raw :as dict) (total-components :as number) (total-macros :as number) (pure-count :as number) (io-count :as number))
;; pages-raw: list of {:name :path :direct :needed-names}
;; components-raw: dict of name → {:is-pure :affinity :render-target :io-refs :deps :source}
(let ((pages-data (list)))
(for-each
(fn ((page :as dict))
(let ((needed-names (get page "needed-names"))
(n (len needed-names))
(pct (if (> total-components 0)
(round (* (/ n total-components) 100))
0))
(savings (- 100 pct))
(pure-in-page 0)
(io-in-page 0)
(page-io-refs (list))
(comp-details (list)))
;; Walk needed components
(for-each
(fn ((comp-name :as string))
(let ((info (get components-raw comp-name)))
(when (not (nil? info))
(if (get info "is-pure")
(set! pure-in-page (+ pure-in-page 1))
(do
(set! io-in-page (+ io-in-page 1))
(for-each
(fn ((ref :as string)) (when (not (some (fn ((r :as string)) (= r ref)) page-io-refs))
(append! page-io-refs ref)))
(or (get info "io-refs") (list)))))
(append! comp-details
{"name" comp-name
"is-pure" (get info "is-pure")
"affinity" (get info "affinity")
"render-target" (get info "render-target")
"io-refs" (or (get info "io-refs") (list))
"deps" (or (get info "deps") (list))
"source" (get info "source")}))))
needed-names)
(append! pages-data
{"name" (get page "name")
"path" (get page "path")
"direct" (get page "direct")
"needed" n
"pct" pct
"savings" savings
"io-refs" (len page-io-refs)
"pure-in-page" pure-in-page
"io-in-page" io-in-page
"components" comp-details})))
pages-raw)
{"pages" pages-data
"total-components" total-components
"total-macros" total-macros
"pure-count" pure-count
"io-count" io-count})))
;; --------------------------------------------------------------------------
;; build-routing-analysis
;;
;; Classify pages by routing mode (client vs server).
;; --------------------------------------------------------------------------
(define build-routing-analysis
(fn ((pages-raw :as list))
;; pages-raw: list of {:name :path :has-data :content-src}
(let ((pages-data (list))
(client-count 0)
(server-count 0))
(for-each
(fn ((page :as dict))
(let ((has-data (get page "has-data"))
(content-src (or (get page "content-src") ""))
(mode nil)
(reason ""))
(cond
has-data
(do (set! mode "server")
(set! reason "Has :data expression — needs server IO")
(set! server-count (+ server-count 1)))
(empty? content-src)
(do (set! mode "server")
(set! reason "No content expression")
(set! server-count (+ server-count 1)))
:else
(do (set! mode "client")
(set! client-count (+ client-count 1))))
(append! pages-data
{"name" (get page "name")
"path" (get page "path")
"mode" mode
"has-data" has-data
"content-expr" (if (> (len content-src) 80)
(str (slice content-src 0 80) "...")
content-src)
"reason" reason})))
pages-raw)
{"pages" pages-data
"total-pages" (+ client-count server-count)
"client-count" client-count
"server-count" server-count})))
;; --------------------------------------------------------------------------
;; build-affinity-analysis
;;
;; Package component affinity info + page render plans for display.
;; --------------------------------------------------------------------------
(define build-affinity-analysis
(fn ((demo-components :as list) (page-plans :as list))
{"components" demo-components
"page-plans" page-plans}))

View File

@@ -50,7 +50,7 @@
;; Returns a list of top-level AST expressions. ;; Returns a list of top-level AST expressions.
(define sx-parse (define sx-parse
(fn (source) (fn ((source :as string))
(let ((pos 0) (let ((pos 0)
(len-src (len source))) (len-src (len source)))
@@ -170,7 +170,7 @@
;; -- Composite readers -- ;; -- Composite readers --
(define read-list (define read-list
(fn (close-ch) (fn ((close-ch :as string))
(let ((items (list))) (let ((items (list)))
(define read-list-loop (define read-list-loop
(fn () (fn ()
@@ -352,11 +352,11 @@
(define sx-serialize-dict (define sx-serialize-dict
(fn (d) (fn ((d :as dict))
(str "{" (str "{"
(join " " (join " "
(reduce (reduce
(fn (acc key) (fn ((acc :as list) (key :as string))
(concat acc (list (str ":" key) (sx-serialize (dict-get d key))))) (concat acc (list (str ":" key) (sx-serialize (dict-get d key)))))
(list) (list)
(keys d))) (keys d)))

3214
shared/sx/ref/platform_js.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -307,6 +307,14 @@ def component_affinity(c):
return getattr(c, 'affinity', 'auto') return getattr(c, 'affinity', 'auto')
def component_param_types(c):
return getattr(c, 'param_types', None)
def component_set_param_types(c, d):
c.param_types = d
def macro_params(m): def macro_params(m):
return m.params return m.params
@@ -462,10 +470,7 @@ def invoke(f, *args):
def json_serialize(obj): def json_serialize(obj):
import json import json
try: return json.dumps(obj)
return json.dumps(obj)
except (TypeError, ValueError):
return "{}"
def is_empty_dict(d): def is_empty_dict(d):
@@ -600,7 +605,7 @@ def sx_expr_source(x):
try: try:
from shared.sx.evaluator import EvalError from shared.sx.types import EvalError
except ImportError: except ImportError:
class EvalError(Exception): class EvalError(Exception):
pass pass
@@ -1067,10 +1072,19 @@ import inspect
from shared.sx.primitives_io import ( from shared.sx.primitives_io import (
IO_PRIMITIVES, RequestContext, execute_io, IO_PRIMITIVES, RequestContext, execute_io,
css_class_collector as _css_class_collector_cv,
_svg_context as _svg_context_cv,
) )
# Lazy imports to avoid circular dependency (html.py imports sx_ref.py)
_css_class_collector_cv = None
_svg_context_cv = None
def _ensure_html_imports():
global _css_class_collector_cv, _svg_context_cv
if _css_class_collector_cv is None:
from shared.sx.html import css_class_collector, _svg_context
_css_class_collector_cv = css_class_collector
_svg_context_cv = _svg_context
# When True, async_aser expands known components server-side # When True, async_aser expands known components server-side
_expand_components_cv: contextvars.ContextVar[bool] = contextvars.ContextVar( _expand_components_cv: contextvars.ContextVar[bool] = contextvars.ContextVar(
"_expand_components_ref", default=False "_expand_components_ref", default=False
@@ -1094,18 +1108,22 @@ def expand_components_p():
def svg_context_p(): def svg_context_p():
_ensure_html_imports()
return _svg_context_cv.get(False) return _svg_context_cv.get(False)
def svg_context_set(val): def svg_context_set(val):
_ensure_html_imports()
return _svg_context_cv.set(val) return _svg_context_cv.set(val)
def svg_context_reset(token): def svg_context_reset(token):
_ensure_html_imports()
_svg_context_cv.reset(token) _svg_context_cv.reset(token)
def css_class_collect(val): def css_class_collect(val):
_ensure_html_imports()
collector = _css_class_collector_cv.get(None) collector = _css_class_collector_cv.get(None)
if collector is not None: if collector is not None:
collector.update(str(val).split()) collector.update(str(val).split())
@@ -1123,6 +1141,25 @@ def is_sx_expr(x):
return isinstance(x, SxExpr) return isinstance(x, SxExpr)
# Predicate helpers used by adapter-async (these are in PRIMITIVES but
# the bootstrapped code calls them as plain functions)
def string_p(x):
return isinstance(x, str)
def list_p(x):
return isinstance(x, _b_list)
def number_p(x):
return isinstance(x, (int, float)) and not isinstance(x, bool)
def sx_parse(src):
from shared.sx.parser import parse_all
return parse_all(src)
def is_async_coroutine(x): def is_async_coroutine(x):
return inspect.iscoroutine(x) return inspect.iscoroutine(x)
@@ -1199,48 +1236,16 @@ async def async_eval_slot_to_sx(expr, env, ctx=None):
ctx = RequestContext() ctx = RequestContext()
token = _expand_components_cv.set(True) token = _expand_components_cv.set(True)
try: try:
return await _eval_slot_inner(expr, env, ctx) result = await async_eval_slot_inner(expr, env, ctx)
if isinstance(result, SxExpr):
return result
if result is None or result is NIL:
return SxExpr("")
if isinstance(result, str):
return SxExpr(result)
return SxExpr(sx_serialize(result))
finally: finally:
_expand_components_cv.reset(token) _expand_components_cv.reset(token)
async def _eval_slot_inner(expr, env, ctx):
if isinstance(expr, list) and expr:
head = expr[0]
if isinstance(head, Symbol) and head.name.startswith("~"):
comp = env.get(head.name)
if isinstance(comp, Component):
result = await async_aser_component(comp, expr[1:], env, ctx)
if isinstance(result, SxExpr):
return result
if result is None or result is NIL:
return SxExpr("")
if isinstance(result, str):
return SxExpr(result)
return SxExpr(sx_serialize(result))
result = await async_aser(expr, env, ctx)
result = await _maybe_expand_component_result(result, env, ctx)
if isinstance(result, SxExpr):
return result
if result is None or result is NIL:
return SxExpr("")
if isinstance(result, str):
return SxExpr(result)
return SxExpr(sx_serialize(result))
async def _maybe_expand_component_result(result, env, ctx):
raw = None
if isinstance(result, SxExpr):
raw = str(result).strip()
elif isinstance(result, str):
raw = result.strip()
if raw and raw.startswith("(~"):
from shared.sx.parser import parse_all as _pa
parsed = _pa(raw)
if parsed:
return await async_eval_slot_to_sx(parsed[0], env, ctx)
return result
''' '''
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -1366,7 +1371,8 @@ aser_special = _aser_special_with_continuations
# Public API generator # Public API generator
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def public_api_py(has_html: bool, has_sx: bool, has_deps: bool = False) -> str: def public_api_py(has_html: bool, has_sx: bool, has_deps: bool = False,
has_async: bool = False) -> str:
lines = [ lines = [
'', '',
'# =========================================================================', '# =========================================================================',
@@ -1419,8 +1425,9 @@ def public_api_py(has_html: bool, has_sx: bool, has_deps: bool = False) -> str:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
ADAPTER_FILES = { ADAPTER_FILES = {
"html": ("adapter-html.sx", "adapter-html"), "html": ("adapter-html.sx", "adapter-html"),
"sx": ("adapter-sx.sx", "adapter-sx"), "sx": ("adapter-sx.sx", "adapter-sx"),
"async": ("adapter-async.sx", "adapter-async"),
} }
SPEC_MODULES = { SPEC_MODULES = {
@@ -1428,6 +1435,8 @@ SPEC_MODULES = {
"router": ("router.sx", "router (client-side route matching)"), "router": ("router.sx", "router (client-side route matching)"),
"engine": ("engine.sx", "engine (fetch/swap/trigger pure logic)"), "engine": ("engine.sx", "engine (fetch/swap/trigger pure logic)"),
"signals": ("signals.sx", "signals (reactive signal runtime)"), "signals": ("signals.sx", "signals (reactive signal runtime)"),
"page-helpers": ("page-helpers.sx", "page-helpers (pure data transformation helpers)"),
"types": ("types.sx", "types (gradual type system)"),
} }
EXTENSION_NAMES = {"continuations"} EXTENSION_NAMES = {"continuations"}

View File

@@ -15,6 +15,15 @@
;; :doc "description" ;; :doc "description"
;; :body (reference-implementation ...)) ;; :body (reference-implementation ...))
;; ;;
;; Typed params use (name :as type) syntax:
;; (define-primitive "+"
;; :params (&rest (args :as number))
;; :returns "number"
;; :doc "Sum all arguments.")
;;
;; Untyped params default to `any`. Typed params enable the gradual
;; type checker (types.sx) to catch mistyped primitive calls.
;;
;; The :body is optional — when provided, it gives a reference ;; The :body is optional — when provided, it gives a reference
;; implementation in SX that bootstrap compilers MAY use for testing ;; implementation in SX that bootstrap compilers MAY use for testing
;; or as a fallback. Most targets will implement natively for performance. ;; or as a fallback. Most targets will implement natively for performance.
@@ -32,89 +41,89 @@
(define-module :core.arithmetic) (define-module :core.arithmetic)
(define-primitive "+" (define-primitive "+"
:params (&rest args) :params (&rest (args :as number))
:returns "number" :returns "number"
:doc "Sum all arguments." :doc "Sum all arguments."
:body (reduce (fn (a b) (native-add a b)) 0 args)) :body (reduce (fn (a b) (native-add a b)) 0 args))
(define-primitive "-" (define-primitive "-"
:params (a &rest b) :params ((a :as number) &rest (b :as number))
:returns "number" :returns "number"
:doc "Subtract. Unary: negate. Binary: a - b." :doc "Subtract. Unary: negate. Binary: a - b."
:body (if (empty? b) (native-neg a) (native-sub a (first b)))) :body (if (empty? b) (native-neg a) (native-sub a (first b))))
(define-primitive "*" (define-primitive "*"
:params (&rest args) :params (&rest (args :as number))
:returns "number" :returns "number"
:doc "Multiply all arguments." :doc "Multiply all arguments."
:body (reduce (fn (a b) (native-mul a b)) 1 args)) :body (reduce (fn (a b) (native-mul a b)) 1 args))
(define-primitive "/" (define-primitive "/"
:params (a b) :params ((a :as number) (b :as number))
:returns "number" :returns "number"
:doc "Divide a by b." :doc "Divide a by b."
:body (native-div a b)) :body (native-div a b))
(define-primitive "mod" (define-primitive "mod"
:params (a b) :params ((a :as number) (b :as number))
:returns "number" :returns "number"
:doc "Modulo a % b." :doc "Modulo a % b."
:body (native-mod a b)) :body (native-mod a b))
(define-primitive "sqrt" (define-primitive "sqrt"
:params (x) :params ((x :as number))
:returns "number" :returns "number"
:doc "Square root.") :doc "Square root.")
(define-primitive "pow" (define-primitive "pow"
:params (x n) :params ((x :as number) (n :as number))
:returns "number" :returns "number"
:doc "x raised to power n.") :doc "x raised to power n.")
(define-primitive "abs" (define-primitive "abs"
:params (x) :params ((x :as number))
:returns "number" :returns "number"
:doc "Absolute value.") :doc "Absolute value.")
(define-primitive "floor" (define-primitive "floor"
:params (x) :params ((x :as number))
:returns "number" :returns "number"
:doc "Floor to integer.") :doc "Floor to integer.")
(define-primitive "ceil" (define-primitive "ceil"
:params (x) :params ((x :as number))
:returns "number" :returns "number"
:doc "Ceiling to integer.") :doc "Ceiling to integer.")
(define-primitive "round" (define-primitive "round"
:params (x &rest ndigits) :params ((x :as number) &rest (ndigits :as number))
:returns "number" :returns "number"
:doc "Round to ndigits decimal places (default 0).") :doc "Round to ndigits decimal places (default 0).")
(define-primitive "min" (define-primitive "min"
:params (&rest args) :params (&rest (args :as number))
:returns "number" :returns "number"
:doc "Minimum. Single list arg or variadic.") :doc "Minimum. Single list arg or variadic.")
(define-primitive "max" (define-primitive "max"
:params (&rest args) :params (&rest (args :as number))
:returns "number" :returns "number"
:doc "Maximum. Single list arg or variadic.") :doc "Maximum. Single list arg or variadic.")
(define-primitive "clamp" (define-primitive "clamp"
:params (x lo hi) :params ((x :as number) (lo :as number) (hi :as number))
:returns "number" :returns "number"
:doc "Clamp x to range [lo, hi]." :doc "Clamp x to range [lo, hi]."
:body (max lo (min hi x))) :body (max lo (min hi x)))
(define-primitive "inc" (define-primitive "inc"
:params (n) :params ((n :as number))
:returns "number" :returns "number"
:doc "Increment by 1." :doc "Increment by 1."
:body (+ n 1)) :body (+ n 1))
(define-primitive "dec" (define-primitive "dec"
:params (n) :params ((n :as number))
:returns "number" :returns "number"
:doc "Decrement by 1." :doc "Decrement by 1."
:body (- n 1)) :body (- n 1))
@@ -159,22 +168,22 @@
Same semantics as = but explicit Scheme name.") Same semantics as = but explicit Scheme name.")
(define-primitive "<" (define-primitive "<"
:params (a b) :params ((a :as number) (b :as number))
:returns "boolean" :returns "boolean"
:doc "Less than.") :doc "Less than.")
(define-primitive ">" (define-primitive ">"
:params (a b) :params ((a :as number) (b :as number))
:returns "boolean" :returns "boolean"
:doc "Greater than.") :doc "Greater than.")
(define-primitive "<=" (define-primitive "<="
:params (a b) :params ((a :as number) (b :as number))
:returns "boolean" :returns "boolean"
:doc "Less than or equal.") :doc "Less than or equal.")
(define-primitive ">=" (define-primitive ">="
:params (a b) :params ((a :as number) (b :as number))
:returns "boolean" :returns "boolean"
:doc "Greater than or equal.") :doc "Greater than or equal.")
@@ -186,19 +195,19 @@
(define-module :core.predicates) (define-module :core.predicates)
(define-primitive "odd?" (define-primitive "odd?"
:params (n) :params ((n :as number))
:returns "boolean" :returns "boolean"
:doc "True if n is odd." :doc "True if n is odd."
:body (= (mod n 2) 1)) :body (= (mod n 2) 1))
(define-primitive "even?" (define-primitive "even?"
:params (n) :params ((n :as number))
:returns "boolean" :returns "boolean"
:doc "True if n is even." :doc "True if n is even."
:body (= (mod n 2) 0)) :body (= (mod n 2) 0))
(define-primitive "zero?" (define-primitive "zero?"
:params (n) :params ((n :as number))
:returns "boolean" :returns "boolean"
:doc "True if n is zero." :doc "True if n is zero."
:body (= n 0)) :body (= n 0))
@@ -274,82 +283,82 @@
:doc "Concatenate all args as strings. nil → empty string, bool → true/false.") :doc "Concatenate all args as strings. nil → empty string, bool → true/false.")
(define-primitive "concat" (define-primitive "concat"
:params (&rest colls) :params (&rest (colls :as list))
:returns "list" :returns "list"
:doc "Concatenate multiple lists into one. Skips nil values.") :doc "Concatenate multiple lists into one. Skips nil values.")
(define-primitive "upper" (define-primitive "upper"
:params (s) :params ((s :as string))
:returns "string" :returns "string"
:doc "Uppercase string.") :doc "Uppercase string.")
(define-primitive "upcase" (define-primitive "upcase"
:params (s) :params ((s :as string))
:returns "string" :returns "string"
:doc "Alias for upper. Uppercase string.") :doc "Alias for upper. Uppercase string.")
(define-primitive "lower" (define-primitive "lower"
:params (s) :params ((s :as string))
:returns "string" :returns "string"
:doc "Lowercase string.") :doc "Lowercase string.")
(define-primitive "downcase" (define-primitive "downcase"
:params (s) :params ((s :as string))
:returns "string" :returns "string"
:doc "Alias for lower. Lowercase string.") :doc "Alias for lower. Lowercase string.")
(define-primitive "string-length" (define-primitive "string-length"
:params (s) :params ((s :as string))
:returns "number" :returns "number"
:doc "Length of string in characters.") :doc "Length of string in characters.")
(define-primitive "substring" (define-primitive "substring"
:params (s start end) :params ((s :as string) (start :as number) (end :as number))
:returns "string" :returns "string"
:doc "Extract substring from start (inclusive) to end (exclusive).") :doc "Extract substring from start (inclusive) to end (exclusive).")
(define-primitive "string-contains?" (define-primitive "string-contains?"
:params (s needle) :params ((s :as string) (needle :as string))
:returns "boolean" :returns "boolean"
:doc "True if string s contains substring needle.") :doc "True if string s contains substring needle.")
(define-primitive "trim" (define-primitive "trim"
:params (s) :params ((s :as string))
:returns "string" :returns "string"
:doc "Strip leading/trailing whitespace.") :doc "Strip leading/trailing whitespace.")
(define-primitive "split" (define-primitive "split"
:params (s &rest sep) :params ((s :as string) &rest (sep :as string))
:returns "list" :returns "list"
:doc "Split string by separator (default space).") :doc "Split string by separator (default space).")
(define-primitive "join" (define-primitive "join"
:params (sep coll) :params ((sep :as string) (coll :as list))
:returns "string" :returns "string"
:doc "Join collection items with separator string.") :doc "Join collection items with separator string.")
(define-primitive "replace" (define-primitive "replace"
:params (s old new) :params ((s :as string) (old :as string) (new :as string))
:returns "string" :returns "string"
:doc "Replace all occurrences of old with new in s.") :doc "Replace all occurrences of old with new in s.")
(define-primitive "slice" (define-primitive "slice"
:params (coll start &rest end) :params (coll (start :as number) &rest (end :as number))
:returns "any" :returns "any"
:doc "Slice a string or list from start to end (exclusive). End is optional.") :doc "Slice a string or list from start to end (exclusive). End is optional.")
(define-primitive "index-of" (define-primitive "index-of"
:params (s needle &rest from) :params ((s :as string) (needle :as string) &rest (from :as number))
:returns "number" :returns "number"
:doc "Index of first occurrence of needle in s, or -1 if not found. Optional start index.") :doc "Index of first occurrence of needle in s, or -1 if not found. Optional start index.")
(define-primitive "starts-with?" (define-primitive "starts-with?"
:params (s prefix) :params ((s :as string) (prefix :as string))
:returns "boolean" :returns "boolean"
:doc "True if string s starts with prefix.") :doc "True if string s starts with prefix.")
(define-primitive "ends-with?" (define-primitive "ends-with?"
:params (s suffix) :params ((s :as string) (suffix :as string))
:returns "boolean" :returns "boolean"
:doc "True if string s ends with suffix.") :doc "True if string s ends with suffix.")
@@ -371,7 +380,7 @@
:doc "Create a dict from key/value pairs: (dict :a 1 :b 2).") :doc "Create a dict from key/value pairs: (dict :a 1 :b 2).")
(define-primitive "range" (define-primitive "range"
:params (start end &rest step) :params ((start :as number) (end :as number) &rest (step :as number))
:returns "list" :returns "list"
:doc "Integer range [start, end) with optional step.") :doc "Integer range [start, end) with optional step.")
@@ -386,57 +395,57 @@
:doc "Length of string, list, or dict.") :doc "Length of string, list, or dict.")
(define-primitive "first" (define-primitive "first"
:params (coll) :params ((coll :as list))
:returns "any" :returns "any"
:doc "First element, or nil if empty.") :doc "First element, or nil if empty.")
(define-primitive "last" (define-primitive "last"
:params (coll) :params ((coll :as list))
:returns "any" :returns "any"
:doc "Last element, or nil if empty.") :doc "Last element, or nil if empty.")
(define-primitive "rest" (define-primitive "rest"
:params (coll) :params ((coll :as list))
:returns "list" :returns "list"
:doc "All elements except the first.") :doc "All elements except the first.")
(define-primitive "nth" (define-primitive "nth"
:params (coll n) :params ((coll :as list) (n :as number))
:returns "any" :returns "any"
:doc "Element at index n, or nil if out of bounds.") :doc "Element at index n, or nil if out of bounds.")
(define-primitive "cons" (define-primitive "cons"
:params (x coll) :params (x (coll :as list))
:returns "list" :returns "list"
:doc "Prepend x to coll.") :doc "Prepend x to coll.")
(define-primitive "append" (define-primitive "append"
:params (coll x) :params ((coll :as list) x)
:returns "list" :returns "list"
:doc "If x is a list, concatenate. Otherwise append x as single element.") :doc "If x is a list, concatenate. Otherwise append x as single element.")
(define-primitive "append!" (define-primitive "append!"
:params (coll x) :params ((coll :as list) x)
:returns "list" :returns "list"
:doc "Mutate coll by appending x in-place. Returns coll.") :doc "Mutate coll by appending x in-place. Returns coll.")
(define-primitive "reverse" (define-primitive "reverse"
:params (coll) :params ((coll :as list))
:returns "list" :returns "list"
:doc "Return coll in reverse order.") :doc "Return coll in reverse order.")
(define-primitive "flatten" (define-primitive "flatten"
:params (coll) :params ((coll :as list))
:returns "list" :returns "list"
:doc "Flatten one level of nesting. Nested lists become top-level elements.") :doc "Flatten one level of nesting. Nested lists become top-level elements.")
(define-primitive "chunk-every" (define-primitive "chunk-every"
:params (coll n) :params ((coll :as list) (n :as number))
:returns "list" :returns "list"
:doc "Split coll into sub-lists of size n.") :doc "Split coll into sub-lists of size n.")
(define-primitive "zip-pairs" (define-primitive "zip-pairs"
:params (coll) :params ((coll :as list))
:returns "list" :returns "list"
:doc "Consecutive pairs: (1 2 3 4) → ((1 2) (2 3) (3 4)).") :doc "Consecutive pairs: (1 2 3 4) → ((1 2) (2 3) (3 4)).")
@@ -448,37 +457,37 @@
(define-module :core.dict) (define-module :core.dict)
(define-primitive "keys" (define-primitive "keys"
:params (d) :params ((d :as dict))
:returns "list" :returns "list"
:doc "List of dict keys.") :doc "List of dict keys.")
(define-primitive "vals" (define-primitive "vals"
:params (d) :params ((d :as dict))
:returns "list" :returns "list"
:doc "List of dict values.") :doc "List of dict values.")
(define-primitive "merge" (define-primitive "merge"
:params (&rest dicts) :params (&rest (dicts :as dict))
:returns "dict" :returns "dict"
:doc "Merge dicts left to right. Later keys win. Skips nil.") :doc "Merge dicts left to right. Later keys win. Skips nil.")
(define-primitive "has-key?" (define-primitive "has-key?"
:params (d key) :params ((d :as dict) key)
:returns "boolean" :returns "boolean"
:doc "True if dict d contains key.") :doc "True if dict d contains key.")
(define-primitive "assoc" (define-primitive "assoc"
:params (d &rest pairs) :params ((d :as dict) &rest pairs)
:returns "dict" :returns "dict"
:doc "Return new dict with key/value pairs added/overwritten.") :doc "Return new dict with key/value pairs added/overwritten.")
(define-primitive "dissoc" (define-primitive "dissoc"
:params (d &rest keys) :params ((d :as dict) &rest keys)
:returns "dict" :returns "dict"
:doc "Return new dict with keys removed.") :doc "Return new dict with keys removed.")
(define-primitive "dict-set!" (define-primitive "dict-set!"
:params (d key val) :params ((d :as dict) key val)
:returns "any" :returns "any"
:doc "Mutate dict d by setting key to val in-place. Returns val.") :doc "Mutate dict d by setting key to val in-place. Returns val.")
@@ -495,12 +504,12 @@
(define-module :stdlib.format) (define-module :stdlib.format)
(define-primitive "format-date" (define-primitive "format-date"
:params (date-str fmt) :params ((date-str :as string) (fmt :as string))
:returns "string" :returns "string"
:doc "Parse ISO date string and format with strftime-style format.") :doc "Parse ISO date string and format with strftime-style format.")
(define-primitive "format-decimal" (define-primitive "format-decimal"
:params (val &rest places) :params ((val :as number) &rest (places :as number))
:returns "string" :returns "string"
:doc "Format number with fixed decimal places (default 2).") :doc "Format number with fixed decimal places (default 2).")
@@ -510,7 +519,7 @@
:doc "Parse string to integer with optional default on failure.") :doc "Parse string to integer with optional default on failure.")
(define-primitive "parse-datetime" (define-primitive "parse-datetime"
:params (s) :params ((s :as string))
:returns "string" :returns "string"
:doc "Parse datetime string — identity passthrough (returns string or nil).") :doc "Parse datetime string — identity passthrough (returns string or nil).")
@@ -522,17 +531,17 @@
(define-module :stdlib.text) (define-module :stdlib.text)
(define-primitive "pluralize" (define-primitive "pluralize"
:params (count &rest forms) :params ((count :as number) &rest (forms :as string))
:returns "string" :returns "string"
:doc "Pluralize: (pluralize 1) → \"\", (pluralize 2) → \"s\". Or (pluralize n \"item\" \"items\").") :doc "Pluralize: (pluralize 1) → \"\", (pluralize 2) → \"s\". Or (pluralize n \"item\" \"items\").")
(define-primitive "escape" (define-primitive "escape"
:params (s) :params ((s :as string))
:returns "string" :returns "string"
:doc "HTML-escape a string (&, <, >, \", ').") :doc "HTML-escape a string (&, <, >, \", ').")
(define-primitive "strip-tags" (define-primitive "strip-tags"
:params (s) :params ((s :as string))
:returns "string" :returns "string"
:doc "Remove HTML tags from string.") :doc "Remove HTML tags from string.")
@@ -567,16 +576,16 @@
:doc "Return type name: number, string, boolean, nil, symbol, keyword, list, dict, lambda, component, island, macro.") :doc "Return type name: number, string, boolean, nil, symbol, keyword, list, dict, lambda, component, island, macro.")
(define-primitive "symbol-name" (define-primitive "symbol-name"
:params (sym) :params ((sym :as symbol))
:returns "string" :returns "string"
:doc "Return the name string of a symbol.") :doc "Return the name string of a symbol.")
(define-primitive "keyword-name" (define-primitive "keyword-name"
:params (kw) :params ((kw :as keyword))
:returns "string" :returns "string"
:doc "Return the name string of a keyword.") :doc "Return the name string of a keyword.")
(define-primitive "sx-parse" (define-primitive "sx-parse"
:params (source) :params ((source :as string))
:returns "list" :returns "list"
:doc "Parse SX source string into a list of AST expressions.") :doc "Parse SX source string into a list of AST expressions.")

View File

@@ -25,7 +25,7 @@
;; Evaluate an SMT-LIB expression in a variable environment ;; Evaluate an SMT-LIB expression in a variable environment
(define smt-eval (define smt-eval
(fn (expr env) (fn (expr (env :as dict))
(cond (cond
;; Numbers ;; Numbers
(number? expr) expr (number? expr) expr
@@ -136,11 +136,11 @@
;; Bind parameter names to values ;; Bind parameter names to values
(define smt-bind-params (define smt-bind-params
(fn (params vals) (fn ((params :as list) (vals :as list))
(smt-bind-loop params vals {}))) (smt-bind-loop params vals {})))
(define smt-bind-loop (define smt-bind-loop
(fn (params vals acc) (fn ((params :as list) (vals :as list) (acc :as dict))
(if (or (empty? params) (empty? vals)) (if (or (empty? params) (empty? vals))
acc acc
(smt-bind-loop (rest params) (rest vals) (smt-bind-loop (rest params) (rest vals)
@@ -153,11 +153,11 @@
;; Extract declarations and assertions from parsed SMT-LIB ;; Extract declarations and assertions from parsed SMT-LIB
(define smt-extract-statements (define smt-extract-statements
(fn (exprs) (fn ((exprs :as list))
(smt-extract-loop exprs {} (list)))) (smt-extract-loop exprs {} (list))))
(define smt-extract-loop (define smt-extract-loop
(fn (exprs decls assertions) (fn ((exprs :as list) (decls :as dict) (assertions :as list))
(if (empty? exprs) (if (empty? exprs)
{:decls decls :assertions assertions} {:decls decls :assertions assertions}
(let ((expr (first exprs)) (let ((expr (first exprs))
@@ -286,7 +286,7 @@
;; Verify a single definitional assertion by construction + evaluation ;; Verify a single definitional assertion by construction + evaluation
(define smt-verify-definition (define smt-verify-definition
(fn (def-info decls) (fn ((def-info :as dict) (decls :as dict))
(let ((name (get def-info "name")) (let ((name (get def-info "name"))
(params (get def-info "params")) (params (get def-info "params"))
(body (get def-info "body")) (body (get def-info "body"))
@@ -295,10 +295,10 @@
;; Build the model: define f = λparams.body ;; Build the model: define f = λparams.body
(let ((model (assoc decls name {:params params :body body})) (let ((model (assoc decls name {:params params :body body}))
;; Select test values matching arity ;; Select test values matching arity
(tests (filter (fn (tv) (= (len tv) n-params)) smt-test-values)) (tests (filter (fn ((tv :as list)) (= (len tv) n-params)) smt-test-values))
;; Run tests ;; Run tests
(results (map (results (map
(fn (test-vals) (fn ((test-vals :as list))
(let ((env (merge model (smt-bind-params params test-vals))) (let ((env (merge model (smt-bind-params params test-vals)))
;; Evaluate body directly ;; Evaluate body directly
(body-result (smt-eval body env)) (body-result (smt-eval body env))
@@ -311,9 +311,9 @@
:equal (= body-result call-result)})) :equal (= body-result call-result)}))
tests))) tests)))
{:name name {:name name
:status (if (every? (fn (r) (get r "equal")) results) "sat" "FAIL") :status (if (every? (fn ((r :as dict)) (get r "equal")) results) "sat" "FAIL")
:proof "by construction (definition is the model)" :proof "by construction (definition is the model)"
:tests-passed (len (filter (fn (r) (get r "equal")) results)) :tests-passed (len (filter (fn ((r :as dict)) (get r "equal")) results))
:tests-total (len results) :tests-total (len results)
:sample (if (empty? results) nil (first results))})))) :sample (if (empty? results) nil (first results))}))))
@@ -325,16 +325,16 @@
;; Strip SMT-LIB comment lines (starting with ;) and return only actual forms. ;; Strip SMT-LIB comment lines (starting with ;) and return only actual forms.
;; Handles comments that contain ( characters. ;; Handles comments that contain ( characters.
(define smt-strip-comments (define smt-strip-comments
(fn (s) (fn ((s :as string))
(let ((lines (split s "\n")) (let ((lines (split s "\n"))
(non-comment (filter (non-comment (filter
(fn (line) (not (starts-with? (trim line) ";"))) (fn ((line :as string)) (not (starts-with? (trim line) ";")))
lines))) lines)))
(join "\n" non-comment)))) (join "\n" non-comment))))
;; Verify SMT-LIB output (string) — parse, classify, prove ;; Verify SMT-LIB output (string) — parse, classify, prove
(define prove-check (define prove-check
(fn (smtlib-str) (fn ((smtlib-str :as string))
(let ((parsed (sx-parse (smt-strip-comments smtlib-str))) (let ((parsed (sx-parse (smt-strip-comments smtlib-str)))
(stmts (smt-extract-statements parsed)) (stmts (smt-extract-statements parsed))
(decls (get stmts "decls")) (decls (get stmts "decls"))
@@ -351,7 +351,7 @@
{:status "unknown" {:status "unknown"
:reason "non-definitional assertion (needs full SMT solver)"})) :reason "non-definitional assertion (needs full SMT solver)"}))
assertions))) assertions)))
{:status (if (every? (fn (r) (= (get r "status") "sat")) results) {:status (if (every? (fn ((r :as dict)) (= (get r "status") "sat")) results)
"sat" "unknown") "sat" "unknown")
:assertions (len assertions) :assertions (len assertions)
:results results}))))) :results results})))))
@@ -377,7 +377,7 @@
;; Batch verify: translate and prove all define-* forms ;; Batch verify: translate and prove all define-* forms
(define prove-file (define prove-file
(fn (exprs) (fn ((exprs :as list))
(let ((translatable (let ((translatable
(filter (filter
(fn (expr) (fn (expr)
@@ -396,7 +396,7 @@
(name (nth expr 1))) (name (nth expr 1)))
(assoc proof "name" name))) (assoc proof "name" name)))
translatable)) translatable))
(sat-count (len (filter (fn (r) (= (get r "status") "sat")) results))) (sat-count (len (filter (fn ((r :as dict)) (= (get r "status") "sat")) results)))
(total (len results))) (total (len results)))
{:total total {:total total
:sat sat-count :sat sat-count
@@ -424,7 +424,7 @@
;; Default domain bounds by arity — balance coverage vs. combinatorics ;; Default domain bounds by arity — balance coverage vs. combinatorics
(define prove-domain-for (define prove-domain-for
(fn (arity) (fn ((arity :as number))
(cond (cond
(<= arity 1) (range -50 51) ;; 101 values (<= arity 1) (range -50 51) ;; 101 values
(= arity 2) (range -20 21) ;; 41^2 = 1,681 pairs (= arity 2) (range -20 21) ;; 41^2 = 1,681 pairs
@@ -433,7 +433,7 @@
;; Cartesian product: all n-tuples from a domain ;; Cartesian product: all n-tuples from a domain
(define prove-tuples (define prove-tuples
(fn (domain arity) (fn ((domain :as list) (arity :as number))
(if (<= arity 0) (list (list)) (if (<= arity 0) (list (list))
(if (= arity 1) (if (= arity 1)
(map (fn (x) (list x)) domain) (map (fn (x) (list x)) domain)
@@ -441,12 +441,12 @@
(prove-tuples-expand domain sub (list))))))) (prove-tuples-expand domain sub (list)))))))
(define prove-tuples-expand (define prove-tuples-expand
(fn (domain sub acc) (fn ((domain :as list) (sub :as list) (acc :as list))
(if (empty? domain) acc (if (empty? domain) acc
(prove-tuples-expand (prove-tuples-expand
(rest domain) sub (rest domain) sub
(append acc (append acc
(map (fn (t) (cons (first domain) t)) sub)))))) (map (fn ((t :as list)) (cons (first domain) t)) sub))))))
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
@@ -454,7 +454,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define prove-call (define prove-call
(fn (f vals) (fn ((f :as lambda) (vals :as list))
(let ((n (len vals))) (let ((n (len vals)))
(cond (cond
(= n 0) (f) (= n 0) (f)
@@ -472,13 +472,13 @@
;; Search for a counterexample. Returns nil if property holds for all tested ;; Search for a counterexample. Returns nil if property holds for all tested
;; values, or the first counterexample found. ;; values, or the first counterexample found.
(define prove-search (define prove-search
(fn (test-fn given-fn domain vars) (fn ((test-fn :as lambda) given-fn (domain :as list) (vars :as list))
(let ((arity (len vars)) (let ((arity (len vars))
(tuples (prove-tuples domain arity))) (tuples (prove-tuples domain arity)))
(prove-search-loop test-fn given-fn tuples 0 0)))) (prove-search-loop test-fn given-fn tuples 0 0))))
(define prove-search-loop (define prove-search-loop
(fn (test-fn given-fn tuples tested skipped) (fn ((test-fn :as lambda) given-fn (tuples :as list) (tested :as number) (skipped :as number))
(if (empty? tuples) (if (empty? tuples)
{:status "verified" :tested tested :skipped skipped} {:status "verified" :tested tested :skipped skipped}
(let ((vals (first tuples)) (let ((vals (first tuples))
@@ -505,7 +505,7 @@
;; Verify a single property via bounded model checking ;; Verify a single property via bounded model checking
(define prove-property (define prove-property
(fn (prop) (fn ((prop :as dict))
(let ((name (get prop "name")) (let ((name (get prop "name"))
(vars (get prop "vars")) (vars (get prop "vars"))
(test-fn (get prop "test")) (test-fn (get prop "test"))
@@ -519,10 +519,10 @@
;; Batch verify a list of properties ;; Batch verify a list of properties
(define prove-properties (define prove-properties
(fn (props) (fn ((props :as list))
(let ((results (map prove-property props)) (let ((results (map prove-property props))
(verified (filter (fn (r) (= (get r "status") "verified")) results)) (verified (filter (fn ((r :as dict)) (= (get r "status") "verified")) results))
(falsified (filter (fn (r) (= (get r "status") "falsified")) results))) (falsified (filter (fn ((r :as dict)) (= (get r "status") "falsified")) results)))
{:total (len results) {:total (len results)
:verified (len verified) :verified (len verified)
:falsified (len falsified) :falsified (len falsified)
@@ -537,13 +537,13 @@
;; Generate SMT-LIB for a property — asserts (not (forall ...)) so that ;; Generate SMT-LIB for a property — asserts (not (forall ...)) so that
;; Z3 returning "unsat" proves the property holds universally. ;; Z3 returning "unsat" proves the property holds universally.
(define prove-property-smtlib (define prove-property-smtlib
(fn (prop) (fn ((prop :as dict))
(let ((name (get prop "name")) (let ((name (get prop "name"))
(vars (get prop "vars")) (vars (get prop "vars"))
(holds (get prop "holds")) (holds (get prop "holds"))
(given-e (get prop "given-expr" nil)) (given-e (get prop "given-expr" nil))
(bindings (join " " (bindings (join " "
(map (fn (v) (str "(" v " Int)")) vars))) (map (fn ((v :as string)) (str "(" v " Int)")) vars)))
(holds-smt (z3-expr holds)) (holds-smt (z3-expr holds))
(body (if (nil? given-e) (body (if (nil? given-e)
holds-smt holds-smt
@@ -556,7 +556,7 @@
;; Generate SMT-LIB for all properties, including necessary definitions ;; Generate SMT-LIB for all properties, including necessary definitions
(define prove-properties-smtlib (define prove-properties-smtlib
(fn (props primitives-exprs) (fn ((props :as list) (primitives-exprs :as list))
(let ((defs (z3-translate-file primitives-exprs)) (let ((defs (z3-translate-file primitives-exprs))
(prop-smts (map prove-property-smtlib props))) (prop-smts (map prove-property-smtlib props)))
(str ";; ================================================================\n" (str ";; ================================================================\n"

View File

@@ -253,7 +253,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-mangle (define py-mangle
(fn (name) (fn ((name :as string))
(let ((renamed (get py-renames name))) (let ((renamed (get py-renames name)))
(if (not (nil? renamed)) (if (not (nil? renamed))
renamed renamed
@@ -279,7 +279,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-quote-string (define py-quote-string
(fn (s) (fn ((s :as string))
;; Produce a Python repr-style string literal ;; Produce a Python repr-style string literal
(str "'" (replace (replace (replace (replace s "\\" "\\\\") "'" "\\'") "\n" "\\n") "\t" "\\t") "'"))) (str "'" (replace (replace (replace (replace s "\\" "\\\\") "'" "\\'") "\n" "\\n") "\t" "\\t") "'")))
@@ -292,11 +292,11 @@
(list "+" "-" "*" "/" "=" "!=" "<" ">" "<=" ">=" "mod")) (list "+" "-" "*" "/" "=" "!=" "<" ">" "<=" ">=" "mod"))
(define py-infix? (define py-infix?
(fn (op) (fn ((op :as string))
(some (fn (x) (= x op)) py-infix-ops))) (some (fn (x) (= x op)) py-infix-ops)))
(define py-op-symbol (define py-op-symbol
(fn (op) (fn ((op :as string))
(case op (case op
"=" "==" "=" "=="
"!=" "!=" "!=" "!="
@@ -309,7 +309,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-find-nested-set-vars (define py-find-nested-set-vars
(fn (body) (fn ((body :as list))
;; Returns a list of mangled variable names that are set! from within ;; Returns a list of mangled variable names that are set! from within
;; nested fn/lambda bodies ;; nested fn/lambda bodies
(let ((result (list))) (let ((result (list)))
@@ -318,7 +318,7 @@
result)))) result))))
(define py-scan-set-vars (define py-scan-set-vars
(fn (node in-nested result) (fn (node (in-nested :as boolean) (result :as list))
(when (and (list? node) (not (empty? node))) (when (and (list? node) (not (empty? node)))
(let ((head (first node))) (let ((head (first node)))
(cond (cond
@@ -353,7 +353,7 @@
(py-has-set? body)))) (py-has-set? body))))
(define py-has-set? (define py-has-set?
(fn (nodes) (fn ((nodes :as list))
(some (fn (node) (some (fn (node)
(and (list? node) (and (list? node)
(not (empty? node)) (not (empty? node))
@@ -372,7 +372,7 @@
(py-expr-with-cells expr (list)))) (py-expr-with-cells expr (list))))
(define py-expr-with-cells (define py-expr-with-cells
(fn (expr cell-vars) (fn (expr (cell-vars :as list))
(cond (cond
;; Bool MUST come before number check (Python: bool is subclass of int) ;; Bool MUST come before number check (Python: bool is subclass of int)
(= (type-of expr) "boolean") (= (type-of expr) "boolean")
@@ -417,7 +417,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-emit-native-dict (define py-emit-native-dict
(fn (d cell-vars) (fn ((d :as dict) (cell-vars :as list))
(let ((items (keys d))) (let ((items (keys d)))
(str "{" (join ", " (map (fn (k) (str "{" (join ", " (map (fn (k)
(str (py-quote-string k) ": " (py-expr-with-cells (get d k) cell-vars))) (str (py-quote-string k) ": " (py-expr-with-cells (get d k) cell-vars)))
@@ -429,7 +429,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-emit-list (define py-emit-list
(fn (expr cell-vars) (fn (expr (cell-vars :as list))
(let ((head (first expr)) (let ((head (first expr))
(args (rest expr))) (args (rest expr)))
(if (not (= (type-of head) "symbol")) (if (not (= (type-of head) "symbol"))
@@ -548,7 +548,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-emit-fn (define py-emit-fn
(fn (expr cell-vars) (fn (expr (cell-vars :as list))
(let ((params (nth expr 1)) (let ((params (nth expr 1))
(body (rest (rest expr))) (body (rest (rest expr)))
(param-strs (py-collect-params params))) (param-strs (py-collect-params params)))
@@ -562,11 +562,11 @@
"\n)[-1])")))))) "\n)[-1])"))))))
(define py-collect-params (define py-collect-params
(fn (params) (fn ((params :as list))
(py-collect-params-loop params 0 (list)))) (py-collect-params-loop params 0 (list))))
(define py-collect-params-loop (define py-collect-params-loop
(fn (params i result) (fn ((params :as list) (i :as number) (result :as list))
(if (>= i (len params)) (if (>= i (len params))
result result
(let ((p (nth params i))) (let ((p (nth params i)))
@@ -574,13 +574,25 @@
;; &rest marker ;; &rest marker
(and (= (type-of p) "symbol") (= (symbol-name p) "&rest")) (and (= (type-of p) "symbol") (= (symbol-name p) "&rest"))
(if (< (+ i 1) (len params)) (if (< (+ i 1) (len params))
(py-collect-params-loop params (+ i 2) (let ((rp (nth params (+ i 1))))
(append result (str "*" (py-mangle (symbol-name (nth params (+ i 1))))))) (py-collect-params-loop params (+ i 2)
(append result (str "*" (py-mangle
(if (and (= (type-of rp) "list") (= (len rp) 3)
(= (type-of (nth rp 1)) "keyword")
(= (keyword-name (nth rp 1)) "as"))
(symbol-name (first rp))
(if (= (type-of rp) "symbol") (symbol-name rp) (str rp))))))))
(py-collect-params-loop params (+ i 1) result)) (py-collect-params-loop params (+ i 1) result))
;; Normal param ;; Normal param
(= (type-of p) "symbol") (= (type-of p) "symbol")
(py-collect-params-loop params (+ i 1) (py-collect-params-loop params (+ i 1)
(append result (py-mangle (symbol-name p)))) (append result (py-mangle (symbol-name p))))
;; Annotated param: (name :as type) → extract name
(and (= (type-of p) "list") (= (len p) 3)
(= (type-of (nth p 1)) "keyword")
(= (keyword-name (nth p 1)) "as"))
(py-collect-params-loop params (+ i 1)
(append result (py-mangle (symbol-name (first p)))))
;; Something else ;; Something else
:else :else
(py-collect-params-loop params (+ i 1) (py-collect-params-loop params (+ i 1)
@@ -592,7 +604,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-emit-let (define py-emit-let
(fn (expr cell-vars) (fn (expr (cell-vars :as list))
(let ((bindings (nth expr 1)) (let ((bindings (nth expr 1))
(body (rest (rest expr)))) (body (rest (rest expr))))
(let ((assignments (py-parse-bindings bindings cell-vars))) (let ((assignments (py-parse-bindings bindings cell-vars)))
@@ -603,7 +615,7 @@
(py-wrap-let-bindings assignments body-str cell-vars)))))) (py-wrap-let-bindings assignments body-str cell-vars))))))
(define py-parse-bindings (define py-parse-bindings
(fn (bindings cell-vars) (fn (bindings (cell-vars :as list))
(if (and (list? bindings) (not (empty? bindings))) (if (and (list? bindings) (not (empty? bindings)))
(if (list? (first bindings)) (if (list? (first bindings))
;; Scheme-style: ((name val) ...) ;; Scheme-style: ((name val) ...)
@@ -618,7 +630,7 @@
(list)))) (list))))
(define py-parse-clojure-bindings (define py-parse-clojure-bindings
(fn (bindings i result cell-vars) (fn (bindings (i :as number) (result :as list) (cell-vars :as list))
(if (>= i (- (len bindings) 1)) (if (>= i (- (len bindings) 1))
result result
(let ((vname (if (= (type-of (nth bindings i)) "symbol") (let ((vname (if (= (type-of (nth bindings i)) "symbol")
@@ -629,7 +641,7 @@
cell-vars))))) cell-vars)))))
(define py-wrap-let-bindings (define py-wrap-let-bindings
(fn (assignments body-str cell-vars) (fn ((assignments :as list) (body-str :as string) (cell-vars :as list))
(if (empty? assignments) (if (empty? assignments)
body-str body-str
(let ((binding (last assignments)) (let ((binding (last assignments))
@@ -649,7 +661,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-emit-when (define py-emit-when
(fn (expr cell-vars) (fn (expr (cell-vars :as list))
(let ((cond-e (py-expr-with-cells (nth expr 1) cell-vars)) (let ((cond-e (py-expr-with-cells (nth expr 1) cell-vars))
(body-parts (rest (rest expr)))) (body-parts (rest (rest expr))))
(if (= (len body-parts) 1) (if (= (len body-parts) 1)
@@ -663,7 +675,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-emit-cond (define py-emit-cond
(fn (clauses cell-vars) (fn ((clauses :as list) (cell-vars :as list))
(if (empty? clauses) (if (empty? clauses)
"NIL" "NIL"
;; Detect scheme vs clojure style ;; Detect scheme vs clojure style
@@ -681,7 +693,7 @@
(and (= (type-of test) "keyword") (= (keyword-name test) "else"))))) (and (= (type-of test) "keyword") (= (keyword-name test) "else")))))
(define py-cond-scheme (define py-cond-scheme
(fn (clauses cell-vars) (fn ((clauses :as list) (cell-vars :as list))
(if (empty? clauses) (if (empty? clauses)
"NIL" "NIL"
(let ((clause (first clauses)) (let ((clause (first clauses))
@@ -694,7 +706,7 @@
") else " (py-cond-scheme (rest clauses) cell-vars) ")")))))) ") else " (py-cond-scheme (rest clauses) cell-vars) ")"))))))
(define py-cond-clojure (define py-cond-clojure
(fn (clauses cell-vars) (fn ((clauses :as list) (cell-vars :as list))
(if (< (len clauses) 2) (if (< (len clauses) 2)
"NIL" "NIL"
(let ((test (first clauses)) (let ((test (first clauses))
@@ -711,17 +723,17 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-emit-case (define py-emit-case
(fn (args cell-vars) (fn ((args :as list) (cell-vars :as list))
(let ((match-expr (py-expr-with-cells (first args) cell-vars)) (let ((match-expr (py-expr-with-cells (first args) cell-vars))
(clauses (rest args))) (clauses (rest args)))
(str "_sx_case(" match-expr ", [" (py-case-pairs clauses cell-vars) "])")))) (str "_sx_case(" match-expr ", [" (py-case-pairs clauses cell-vars) "])"))))
(define py-case-pairs (define py-case-pairs
(fn (clauses cell-vars) (fn ((clauses :as list) (cell-vars :as list))
(py-case-pairs-loop clauses 0 (list) cell-vars))) (py-case-pairs-loop clauses 0 (list) cell-vars)))
(define py-case-pairs-loop (define py-case-pairs-loop
(fn (clauses i result cell-vars) (fn ((clauses :as list) (i :as number) (result :as list) (cell-vars :as list))
(if (>= i (- (len clauses) 1)) (if (>= i (- (len clauses) 1))
(join ", " result) (join ", " result)
(let ((test (nth clauses i)) (let ((test (nth clauses i))
@@ -738,28 +750,28 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-emit-and (define py-emit-and
(fn (args cell-vars) (fn ((args :as list) (cell-vars :as list))
(let ((parts (map (fn (x) (py-expr-with-cells x cell-vars)) args))) (let ((parts (map (fn (x) (py-expr-with-cells x cell-vars)) args)))
(if (= (len parts) 1) (if (= (len parts) 1)
(first parts) (first parts)
(py-and-chain parts))))) (py-and-chain parts)))))
(define py-and-chain (define py-and-chain
(fn (parts) (fn ((parts :as list))
(if (= (len parts) 1) (if (= (len parts) 1)
(first parts) (first parts)
(let ((p (first parts))) (let ((p (first parts)))
(str "(" p " if not sx_truthy(" p ") else " (py-and-chain (rest parts)) ")"))))) (str "(" p " if not sx_truthy(" p ") else " (py-and-chain (rest parts)) ")")))))
(define py-emit-or (define py-emit-or
(fn (args cell-vars) (fn ((args :as list) (cell-vars :as list))
(if (= (len args) 1) (if (= (len args) 1)
(py-expr-with-cells (first args) cell-vars) (py-expr-with-cells (first args) cell-vars)
(let ((parts (map (fn (x) (py-expr-with-cells x cell-vars)) args))) (let ((parts (map (fn (x) (py-expr-with-cells x cell-vars)) args)))
(py-or-chain parts))))) (py-or-chain parts)))))
(define py-or-chain (define py-or-chain
(fn (parts) (fn ((parts :as list))
(if (= (len parts) 1) (if (= (len parts) 1)
(first parts) (first parts)
(let ((p (first parts))) (let ((p (first parts)))
@@ -771,7 +783,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-emit-do (define py-emit-do
(fn (args cell-vars) (fn ((args :as list) (cell-vars :as list))
(if (= (len args) 1) (if (= (len args) 1)
(py-expr-with-cells (first args) cell-vars) (py-expr-with-cells (first args) cell-vars)
(str "_sx_begin(" (join ", " (map (fn (e) (py-expr-with-cells e cell-vars)) args)) ")")))) (str "_sx_begin(" (join ", " (map (fn (e) (py-expr-with-cells e cell-vars)) args)) ")"))))
@@ -782,11 +794,11 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-emit-dict-literal (define py-emit-dict-literal
(fn (pairs cell-vars) (fn ((pairs :as list) (cell-vars :as list))
(str "{" (py-dict-pairs-str pairs 0 (list) cell-vars) "}"))) (str "{" (py-dict-pairs-str pairs 0 (list) cell-vars) "}")))
(define py-dict-pairs-str (define py-dict-pairs-str
(fn (pairs i result cell-vars) (fn ((pairs :as list) (i :as number) (result :as list) (cell-vars :as list))
(if (>= i (- (len pairs) 1)) (if (>= i (- (len pairs) 1))
(join ", " result) (join ", " result)
(let ((key (nth pairs i)) (let ((key (nth pairs i))
@@ -805,7 +817,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-emit-infix (define py-emit-infix
(fn (op args cell-vars) (fn ((op :as string) (args :as list) (cell-vars :as list))
(let ((py-op (py-op-symbol op))) (let ((py-op (py-op-symbol op)))
(if (and (= (len args) 1) (= op "-")) (if (and (= (len args) 1) (= op "-"))
(str "(-" (py-expr-with-cells (first args) cell-vars) ")") (str "(-" (py-expr-with-cells (first args) cell-vars) ")")
@@ -839,15 +851,15 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-pad (define py-pad
(fn (indent) (fn ((indent :as number))
(join "" (map (fn (i) " ") (range 0 indent))))) (join "" (map (fn (i) " ") (range 0 indent)))))
(define py-statement (define py-statement
(fn (expr indent) (fn (expr (indent :as number))
(py-statement-with-cells expr indent (list)))) (py-statement-with-cells expr indent (list))))
(define py-statement-with-cells (define py-statement-with-cells
(fn (expr indent cell-vars) (fn (expr (indent :as number) (cell-vars :as list))
(let ((pad (py-pad indent))) (let ((pad (py-pad indent)))
(if (and (list? expr) (not (empty? expr)) (if (and (list? expr) (not (empty? expr))
(= (type-of (first expr)) "symbol")) (= (type-of (first expr)) "symbol"))
@@ -889,7 +901,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-emit-define (define py-emit-define
(fn (expr indent cell-vars) (fn (expr (indent :as number) (cell-vars :as list))
(let ((pad (py-pad indent)) (let ((pad (py-pad indent))
(name (if (= (type-of (nth expr 1)) "symbol") (name (if (= (type-of (nth expr 1)) "symbol")
(symbol-name (nth expr 1)) (symbol-name (nth expr 1))
@@ -911,7 +923,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-emit-define-as-def (define py-emit-define-as-def
(fn (name fn-expr indent) (fn ((name :as string) fn-expr (indent :as number))
(let ((pad (py-pad indent)) (let ((pad (py-pad indent))
(params (nth fn-expr 1)) (params (nth fn-expr 1))
(body (rest (rest fn-expr))) (body (rest (rest fn-expr)))
@@ -932,13 +944,13 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-emit-body-stmts (define py-emit-body-stmts
(fn (body lines indent cell-vars) (fn ((body :as list) (lines :as list) (indent :as number) (cell-vars :as list))
(let ((pad (py-pad indent)) (let ((pad (py-pad indent))
(total (len body))) (total (len body)))
(py-emit-body-stmts-loop body lines indent cell-vars 0 total pad)))) (py-emit-body-stmts-loop body lines indent cell-vars 0 total pad))))
(define py-emit-body-stmts-loop (define py-emit-body-stmts-loop
(fn (body lines indent cell-vars i total pad) (fn ((body :as list) (lines :as list) (indent :as number) (cell-vars :as list) (i :as number) (total :as number) (pad :as string))
(when (< i total) (when (< i total)
(let ((expr (nth body i)) (let ((expr (nth body i))
(is-last (= i (- total 1)))) (is-last (= i (- total 1))))
@@ -968,7 +980,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-emit-let-as-stmts (define py-emit-let-as-stmts
(fn (expr lines indent is-last cell-vars) (fn (expr (lines :as list) (indent :as number) (is-last :as boolean) (cell-vars :as list))
(let ((pad (py-pad indent)) (let ((pad (py-pad indent))
(bindings (nth expr 1)) (bindings (nth expr 1))
(body (rest (rest expr)))) (body (rest (rest expr))))
@@ -981,7 +993,7 @@
(for-each (fn (b) (py-emit-stmt-recursive b lines indent cell-vars)) body)))))) (for-each (fn (b) (py-emit-stmt-recursive b lines indent cell-vars)) body))))))
(define py-emit-binding-assignments (define py-emit-binding-assignments
(fn (bindings lines indent cell-vars) (fn (bindings (lines :as list) (indent :as number) (cell-vars :as list))
(let ((pad (py-pad indent))) (let ((pad (py-pad indent)))
(when (and (list? bindings) (not (empty? bindings))) (when (and (list? bindings) (not (empty? bindings)))
(if (list? (first bindings)) (if (list? (first bindings))
@@ -1002,7 +1014,7 @@
(py-emit-clojure-binding-assignments bindings lines indent 0 cell-vars)))))) (py-emit-clojure-binding-assignments bindings lines indent 0 cell-vars))))))
(define py-emit-clojure-binding-assignments (define py-emit-clojure-binding-assignments
(fn (bindings lines indent i cell-vars) (fn (bindings (lines :as list) (indent :as number) (i :as number) (cell-vars :as list))
(let ((pad (py-pad indent))) (let ((pad (py-pad indent)))
(when (< i (- (len bindings) 1)) (when (< i (- (len bindings) 1))
(let ((vname (if (= (type-of (nth bindings i)) "symbol") (let ((vname (if (= (type-of (nth bindings i)) "symbol")
@@ -1024,7 +1036,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-emit-stmt-recursive (define py-emit-stmt-recursive
(fn (expr lines indent cell-vars) (fn (expr (lines :as list) (indent :as number) (cell-vars :as list))
(let ((pad (py-pad indent))) (let ((pad (py-pad indent)))
(if (not (and (list? expr) (not (empty? expr)))) (if (not (and (list? expr) (not (empty? expr))))
(append! lines (py-statement-with-cells expr indent cell-vars)) (append! lines (py-statement-with-cells expr indent cell-vars))
@@ -1082,7 +1094,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-emit-cond-stmt (define py-emit-cond-stmt
(fn (expr lines indent cell-vars) (fn (expr (lines :as list) (indent :as number) (cell-vars :as list))
(let ((pad (py-pad indent)) (let ((pad (py-pad indent))
(clauses (rest expr))) (clauses (rest expr)))
;; Detect scheme vs clojure ;; Detect scheme vs clojure
@@ -1094,7 +1106,7 @@
(py-cond-stmt-clojure clauses lines indent 0 true cell-vars)))))) (py-cond-stmt-clojure clauses lines indent 0 true cell-vars))))))
(define py-cond-stmt-scheme (define py-cond-stmt-scheme
(fn (clauses lines indent first-clause cell-vars) (fn ((clauses :as list) (lines :as list) (indent :as number) (first-clause :as boolean) (cell-vars :as list))
(let ((pad (py-pad indent))) (let ((pad (py-pad indent)))
(when (not (empty? clauses)) (when (not (empty? clauses))
(let ((clause (first clauses)) (let ((clause (first clauses))
@@ -1111,7 +1123,7 @@
(py-cond-stmt-scheme (rest clauses) lines indent false cell-vars))))))) (py-cond-stmt-scheme (rest clauses) lines indent false cell-vars)))))))
(define py-cond-stmt-clojure (define py-cond-stmt-clojure
(fn (clauses lines indent i first-clause cell-vars) (fn ((clauses :as list) (lines :as list) (indent :as number) (i :as number) (first-clause :as boolean) (cell-vars :as list))
(let ((pad (py-pad indent))) (let ((pad (py-pad indent)))
(when (< i (- (len clauses) 1)) (when (< i (- (len clauses) 1))
(let ((test (nth clauses i)) (let ((test (nth clauses i))
@@ -1132,7 +1144,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-emit-when-stmt (define py-emit-when-stmt
(fn (expr indent cell-vars) (fn (expr (indent :as number) (cell-vars :as list))
(let ((pad (py-pad indent)) (let ((pad (py-pad indent))
(cond-e (py-expr-with-cells (nth expr 1) cell-vars)) (cond-e (py-expr-with-cells (nth expr 1) cell-vars))
(body-parts (rest (rest expr)))) (body-parts (rest (rest expr))))
@@ -1146,7 +1158,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-emit-for-each-stmt (define py-emit-for-each-stmt
(fn (expr indent cell-vars) (fn (expr (indent :as number) (cell-vars :as list))
(let ((pad (py-pad indent)) (let ((pad (py-pad indent))
(fn-expr (nth expr 1)) (fn-expr (nth expr 1))
(coll-expr (nth expr 2)) (coll-expr (nth expr 2))
@@ -1175,7 +1187,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define py-translate-file (define py-translate-file
(fn (defines) (fn ((defines :as list))
(join "\n" (map (fn (pair) (join "\n" (map (fn (pair)
(let ((name (first pair)) (let ((name (first pair))
(expr (nth pair 1))) (expr (nth pair 1)))

View File

@@ -39,7 +39,7 @@ def _get_z3_env() -> dict[str, Any]:
return _z3_env return _z3_env
from shared.sx.parser import parse_all from shared.sx.parser import parse_all
from shared.sx.evaluator import make_env, _eval, _trampoline from shared.sx.ref.sx_ref import make_env, eval_expr as _eval, trampoline as _trampoline
env = make_env() env = make_env()
z3_path = os.path.join(os.path.dirname(__file__), "z3.sx") z3_path = os.path.join(os.path.dirname(__file__), "z3.sx")
@@ -60,7 +60,7 @@ def z3_translate(expr: Any) -> str:
Delegates to z3-translate defined in z3.sx. Delegates to z3-translate defined in z3.sx.
""" """
from shared.sx.evaluator import _trampoline, _call_lambda from shared.sx.ref.sx_ref import trampoline as _trampoline, call_lambda as _call_lambda
env = _get_z3_env() env = _get_z3_env()
return _trampoline(_call_lambda(env["z3-translate"], [expr], env)) return _trampoline(_call_lambda(env["z3-translate"], [expr], env))
@@ -72,7 +72,7 @@ def z3_translate_file(source: str) -> str:
Delegates to z3-translate-file defined in z3.sx. Delegates to z3-translate-file defined in z3.sx.
""" """
from shared.sx.parser import parse_all from shared.sx.parser import parse_all
from shared.sx.evaluator import _trampoline, _call_lambda from shared.sx.ref.sx_ref import trampoline as _trampoline, call_lambda as _call_lambda
env = _get_z3_env() env = _get_z3_env()
exprs = parse_all(source) exprs = parse_all(source)

View File

@@ -72,18 +72,18 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define definition-form? (define definition-form?
(fn (name) (fn ((name :as string))
(or (= name "define") (= name "defcomp") (= name "defisland") (or (= name "define") (= name "defcomp") (= name "defisland")
(= name "defmacro") (= name "defstyle") (= name "defhandler")))) (= name "defmacro") (= name "defstyle") (= name "defhandler"))))
(define parse-element-args (define parse-element-args
(fn (args env) (fn ((args :as list) (env :as dict))
;; Parse (:key val :key2 val2 child1 child2) into (attrs-dict children-list) ;; Parse (:key val :key2 val2 child1 child2) into (attrs-dict children-list)
(let ((attrs (dict)) (let ((attrs (dict))
(children (list))) (children (list)))
(reduce (reduce
(fn (state arg) (fn ((state :as dict) arg)
(let ((skip (get state "skip"))) (let ((skip (get state "skip")))
(if skip (if skip
(assoc state "skip" false "i" (inc (get state "i"))) (assoc state "skip" false "i" (inc (get state "i")))
@@ -101,12 +101,12 @@
(define render-attrs (define render-attrs
(fn (attrs) (fn ((attrs :as dict))
;; Render an attrs dict to an HTML attribute string. ;; Render an attrs dict to an HTML attribute string.
;; Used by adapter-html.sx and adapter-sx.sx. ;; Used by adapter-html.sx and adapter-sx.sx.
(join "" (join ""
(map (map
(fn (key) (fn ((key :as string))
(let ((val (dict-get attrs key))) (let ((val (dict-get attrs key)))
(cond (cond
;; Boolean attrs ;; Boolean attrs
@@ -133,17 +133,13 @@
;; Handles both scheme-style ((test body) ...) and clojure-style ;; Handles both scheme-style ((test body) ...) and clojure-style
;; (test body test body ...). ;; (test body test body ...).
(define eval-cond (define eval-cond
(fn (clauses env) (fn ((clauses :as list) (env :as dict))
(if (and (not (empty? clauses)) (if (cond-scheme? clauses)
(= (type-of (first clauses)) "list")
(= (len (first clauses)) 2))
;; Scheme-style
(eval-cond-scheme clauses env) (eval-cond-scheme clauses env)
;; Clojure-style
(eval-cond-clojure clauses env)))) (eval-cond-clojure clauses env))))
(define eval-cond-scheme (define eval-cond-scheme
(fn (clauses env) (fn ((clauses :as list) (env :as dict))
(if (empty? clauses) (if (empty? clauses)
nil nil
(let ((clause (first clauses)) (let ((clause (first clauses))
@@ -160,7 +156,7 @@
(eval-cond-scheme (rest clauses) env))))))) (eval-cond-scheme (rest clauses) env)))))))
(define eval-cond-clojure (define eval-cond-clojure
(fn (clauses env) (fn ((clauses :as list) (env :as dict))
(if (< (len clauses) 2) (if (< (len clauses) 2)
nil nil
(let ((test (first clauses)) (let ((test (first clauses))
@@ -177,10 +173,12 @@
;; process-bindings: evaluate let-binding pairs, return extended env. ;; process-bindings: evaluate let-binding pairs, return extended env.
;; bindings = ((name1 expr1) (name2 expr2) ...) ;; bindings = ((name1 expr1) (name2 expr2) ...)
(define process-bindings (define process-bindings
(fn (bindings env) (fn ((bindings :as list) (env :as dict))
(let ((local (merge env))) ;; env-extend (not merge) — Env is not a dict subclass, so merge()
;; returns an empty dict, losing all parent scope bindings.
(let ((local (env-extend env)))
(for-each (for-each
(fn (pair) (fn ((pair :as list))
(when (and (= (type-of pair) "list") (>= (len pair) 2)) (when (and (= (type-of pair) "list") (>= (len pair) 2))
(let ((name (if (= (type-of (first pair)) "symbol") (let ((name (if (= (type-of (first pair)) "symbol")
(symbol-name (first pair)) (symbol-name (first pair))

View File

@@ -18,7 +18,7 @@
;; "/docs/" → ("docs") ;; "/docs/" → ("docs")
(define split-path-segments (define split-path-segments
(fn (path) (fn ((path :as string))
(let ((trimmed (if (starts-with? path "/") (slice path 1) path))) (let ((trimmed (if (starts-with? path "/") (slice path 1) path)))
(let ((trimmed2 (if (and (not (empty? trimmed)) (let ((trimmed2 (if (and (not (empty? trimmed))
(ends-with? trimmed "/")) (ends-with? trimmed "/"))
@@ -36,7 +36,7 @@
;; {"type" "param" "value" "slug"}) ;; {"type" "param" "value" "slug"})
(define make-route-segment (define make-route-segment
(fn (seg) (fn ((seg :as string))
(if (and (starts-with? seg "<") (ends-with? seg ">")) (if (and (starts-with? seg "<") (ends-with? seg ">"))
(let ((param-name (slice seg 1 (- (len seg) 1)))) (let ((param-name (slice seg 1 (- (len seg) 1))))
(let ((d {})) (let ((d {}))
@@ -49,7 +49,7 @@
d)))) d))))
(define parse-route-pattern (define parse-route-pattern
(fn (pattern) (fn ((pattern :as string))
(let ((segments (split-path-segments pattern))) (let ((segments (split-path-segments pattern)))
(map make-route-segment segments)))) (map make-route-segment segments))))
@@ -60,13 +60,13 @@
;; Returns params dict if match, nil if no match. ;; Returns params dict if match, nil if no match.
(define match-route-segments (define match-route-segments
(fn (path-segs parsed-segs) (fn ((path-segs :as list) (parsed-segs :as list))
(if (not (= (len path-segs) (len parsed-segs))) (if (not (= (len path-segs) (len parsed-segs)))
nil nil
(let ((params {}) (let ((params {})
(matched true)) (matched true))
(for-each-indexed (for-each-indexed
(fn (i parsed-seg) (fn ((i :as number) (parsed-seg :as dict))
(when matched (when matched
(let ((path-seg (nth path-segs i)) (let ((path-seg (nth path-segs i))
(seg-type (get parsed-seg "type"))) (seg-type (get parsed-seg "type")))
@@ -88,7 +88,7 @@
;; Returns params dict (may be empty for exact matches) or nil. ;; Returns params dict (may be empty for exact matches) or nil.
(define match-route (define match-route
(fn (path pattern) (fn ((path :as string) (pattern :as string))
(let ((path-segs (split-path-segments path)) (let ((path-segs (split-path-segments path))
(parsed-segs (parse-route-pattern pattern))) (parsed-segs (parse-route-pattern pattern)))
(match-route-segments path-segs parsed-segs)))) (match-route-segments path-segs parsed-segs))))
@@ -101,11 +101,11 @@
;; Returns matching entry with "params" added, or nil. ;; Returns matching entry with "params" added, or nil.
(define find-matching-route (define find-matching-route
(fn (path routes) (fn ((path :as string) (routes :as list))
(let ((path-segs (split-path-segments path)) (let ((path-segs (split-path-segments path))
(result nil)) (result nil))
(for-each (for-each
(fn (route) (fn ((route :as dict))
(when (nil? result) (when (nil? result)
(let ((params (match-route-segments path-segs (get route "parsed")))) (let ((params (match-route-segments path-segs (get route "parsed"))))
(when (not (nil? params)) (when (not (nil? params))

View File

@@ -49,7 +49,7 @@ def load_js_sx() -> dict:
exprs = parse_all(source) exprs = parse_all(source)
from shared.sx.evaluator import evaluate, make_env from shared.sx.ref.sx_ref import evaluate, make_env
env = make_env() env = make_env()
for expr in exprs: for expr in exprs:
@@ -74,7 +74,7 @@ def compile_ref_to_js(
spec_modules: List of spec modules (deps, router, signals). None = auto. spec_modules: List of spec modules (deps, router, signals). None = auto.
""" """
from datetime import datetime, timezone from datetime import datetime, timezone
from shared.sx.evaluator import evaluate from shared.sx.ref.sx_ref import evaluate
ref_dir = _HERE ref_dir = _HERE
env = load_js_sx() env = load_js_sx()
@@ -103,8 +103,11 @@ def compile_ref_to_js(
if "boot" in adapter_set: if "boot" in adapter_set:
spec_mod_set.add("router") spec_mod_set.add("router")
spec_mod_set.add("deps") spec_mod_set.add("deps")
if "page-helpers" in SPEC_MODULES:
spec_mod_set.add("page-helpers")
has_deps = "deps" in spec_mod_set has_deps = "deps" in spec_mod_set
has_router = "router" in spec_mod_set has_router = "router" in spec_mod_set
has_page_helpers = "page-helpers" in spec_mod_set
# Resolve extensions # Resolve extensions
ext_set = set() ext_set = set()
@@ -198,12 +201,12 @@ def compile_ref_to_js(
if name in adapter_set and name in adapter_platform: if name in adapter_set and name in adapter_platform:
parts.append(adapter_platform[name]) parts.append(adapter_platform[name])
parts.append(fixups_js(has_html, has_sx, has_dom, has_signals, has_deps)) parts.append(fixups_js(has_html, has_sx, has_dom, has_signals, has_deps, has_page_helpers))
if has_continuations: if has_continuations:
parts.append(CONTINUATIONS_JS) parts.append(CONTINUATIONS_JS)
if has_dom: if has_dom:
parts.append(ASYNC_IO_JS) parts.append(ASYNC_IO_JS)
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router, has_signals)) parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router, has_signals, has_page_helpers))
parts.append(EPILOGUE) parts.append(EPILOGUE)
build_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") build_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

View File

@@ -38,7 +38,7 @@ def load_py_sx(evaluator_env: dict) -> dict:
exprs = parse_all(source) exprs = parse_all(source)
# Import the evaluator # Import the evaluator
from shared.sx.evaluator import evaluate, make_env from shared.sx.ref.sx_ref import evaluate, make_env
env = make_env() env = make_env()
for expr in exprs: for expr in exprs:
@@ -60,7 +60,7 @@ def extract_defines(source: str) -> list[tuple[str, list]]:
def main(): def main():
from shared.sx.evaluator import evaluate from shared.sx.ref.sx_ref import evaluate
# Load py.sx into evaluator # Load py.sx into evaluator
env = load_py_sx({}) env = load_py_sx({})

View File

@@ -72,7 +72,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define reset! (define reset!
(fn (s value) (fn ((s :as signal) value)
(when (signal? s) (when (signal? s)
(let ((old (signal-value s))) (let ((old (signal-value s)))
(when (not (identical? old value)) (when (not (identical? old value))
@@ -85,7 +85,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define swap! (define swap!
(fn (s f &rest args) (fn ((s :as signal) (f :as lambda) &rest args)
(when (signal? s) (when (signal? s)
(let ((old (signal-value s)) (let ((old (signal-value s))
(new-val (apply f (cons old args)))) (new-val (apply f (cons old args))))
@@ -103,7 +103,7 @@
;; by tracking deref calls during evaluation. ;; by tracking deref calls during evaluation.
(define computed (define computed
(fn (compute-fn) (fn ((compute-fn :as lambda))
(let ((s (make-signal nil)) (let ((s (make-signal nil))
(deps (list)) (deps (list))
(compute-ctx nil)) (compute-ctx nil))
@@ -113,7 +113,7 @@
(fn () (fn ()
;; Unsubscribe from old deps ;; Unsubscribe from old deps
(for-each (for-each
(fn (dep) (signal-remove-sub! dep recompute)) (fn ((dep :as signal)) (signal-remove-sub! dep recompute))
(signal-deps s)) (signal-deps s))
(signal-set-deps! s (list)) (signal-set-deps! s (list))
@@ -146,7 +146,7 @@
;; function that tears down the effect. ;; function that tears down the effect.
(define effect (define effect
(fn (effect-fn) (fn ((effect-fn :as lambda))
(let ((deps (list)) (let ((deps (list))
(disposed false) (disposed false)
(cleanup-fn nil)) (cleanup-fn nil))
@@ -159,7 +159,7 @@
;; Unsubscribe from old deps ;; Unsubscribe from old deps
(for-each (for-each
(fn (dep) (signal-remove-sub! dep run-effect)) (fn ((dep :as signal)) (signal-remove-sub! dep run-effect))
deps) deps)
(set! deps (list)) (set! deps (list))
@@ -183,7 +183,7 @@
(set! disposed true) (set! disposed true)
(when cleanup-fn (invoke cleanup-fn)) (when cleanup-fn (invoke cleanup-fn))
(for-each (for-each
(fn (dep) (signal-remove-sub! dep run-effect)) (fn ((dep :as signal)) (signal-remove-sub! dep run-effect))
deps) deps)
(set! deps (list))))) (set! deps (list)))))
;; Auto-register with island scope so disposal happens on swap ;; Auto-register with island scope so disposal happens on swap
@@ -202,7 +202,7 @@
(define *batch-queue* (list)) (define *batch-queue* (list))
(define batch (define batch
(fn (thunk) (fn ((thunk :as lambda))
(set! *batch-depth* (+ *batch-depth* 1)) (set! *batch-depth* (+ *batch-depth* 1))
(invoke thunk) (invoke thunk)
(set! *batch-depth* (- *batch-depth* 1)) (set! *batch-depth* (- *batch-depth* 1))
@@ -214,15 +214,15 @@
(let ((seen (list)) (let ((seen (list))
(pending (list))) (pending (list)))
(for-each (for-each
(fn (s) (fn ((s :as signal))
(for-each (for-each
(fn (sub) (fn ((sub :as lambda))
(when (not (contains? seen sub)) (when (not (contains? seen sub))
(append! seen sub) (append! seen sub)
(append! pending sub))) (append! pending sub)))
(signal-subscribers s))) (signal-subscribers s)))
queue) queue)
(for-each (fn (sub) (sub)) pending)))))) (for-each (fn ((sub :as lambda)) (sub)) pending))))))
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
@@ -232,16 +232,16 @@
;; If inside a batch, queues the signal. Otherwise, notifies immediately. ;; If inside a batch, queues the signal. Otherwise, notifies immediately.
(define notify-subscribers (define notify-subscribers
(fn (s) (fn ((s :as signal))
(if (> *batch-depth* 0) (if (> *batch-depth* 0)
(when (not (contains? *batch-queue* s)) (when (not (contains? *batch-queue* s))
(append! *batch-queue* s)) (append! *batch-queue* s))
(flush-subscribers s)))) (flush-subscribers s))))
(define flush-subscribers (define flush-subscribers
(fn (s) (fn ((s :as signal))
(for-each (for-each
(fn (sub) (sub)) (fn ((sub :as lambda)) (sub))
(signal-subscribers s)))) (signal-subscribers s))))
@@ -269,10 +269,10 @@
;; For effects, the dispose function is returned by effect itself. ;; For effects, the dispose function is returned by effect itself.
(define dispose-computed (define dispose-computed
(fn (s) (fn ((s :as signal))
(when (signal? s) (when (signal? s)
(for-each (for-each
(fn (dep) (signal-remove-sub! dep nil)) (fn ((dep :as signal)) (signal-remove-sub! dep nil))
(signal-deps s)) (signal-deps s))
(signal-set-deps! s (list))))) (signal-set-deps! s (list)))))
@@ -288,7 +288,7 @@
(define *island-scope* nil) (define *island-scope* nil)
(define with-island-scope (define with-island-scope
(fn (scope-fn body-fn) (fn ((scope-fn :as lambda) (body-fn :as lambda))
(let ((prev *island-scope*)) (let ((prev *island-scope*))
(set! *island-scope* scope-fn) (set! *island-scope* scope-fn)
(let ((result (body-fn))) (let ((result (body-fn)))
@@ -300,7 +300,7 @@
;; *island-scope* is non-nil. ;; *island-scope* is non-nil.
(define register-in-scope (define register-in-scope
(fn (disposable) (fn ((disposable :as lambda))
(when *island-scope* (when *island-scope*
(*island-scope* disposable)))) (*island-scope* disposable))))
@@ -323,7 +323,7 @@
;; (dom-get-data el key) → any — retrieve stored value ;; (dom-get-data el key) → any — retrieve stored value
(define with-marsh-scope (define with-marsh-scope
(fn (marsh-el body-fn) (fn (marsh-el (body-fn :as lambda))
;; Execute body-fn collecting all disposables into a marsh-local list. ;; Execute body-fn collecting all disposables into a marsh-local list.
;; Nested under the current island scope — if the island is disposed, ;; Nested under the current island scope — if the island is disposed,
;; the marsh is disposed too (because island scope collected the marsh's ;; the marsh is disposed too (because island scope collected the marsh's
@@ -341,7 +341,7 @@
;; Parent island scope and sibling marshes are unaffected. ;; Parent island scope and sibling marshes are unaffected.
(let ((disposers (dom-get-data marsh-el "sx-marsh-disposers"))) (let ((disposers (dom-get-data marsh-el "sx-marsh-disposers")))
(when disposers (when disposers
(for-each (fn (d) (invoke d)) disposers) (for-each (fn ((d :as lambda)) (invoke d)) disposers)
(dom-set-data marsh-el "sx-marsh-disposers" nil))))) (dom-set-data marsh-el "sx-marsh-disposers" nil)))))
@@ -359,7 +359,7 @@
(define *store-registry* (dict)) (define *store-registry* (dict))
(define def-store (define def-store
(fn (name init-fn) (fn ((name :as string) (init-fn :as lambda))
(let ((registry *store-registry*)) (let ((registry *store-registry*))
;; Only create the store once — subsequent calls return existing ;; Only create the store once — subsequent calls return existing
(when (not (has-key? registry name)) (when (not (has-key? registry name))
@@ -367,7 +367,7 @@
(get *store-registry* name)))) (get *store-registry* name))))
(define use-store (define use-store
(fn (name) (fn ((name :as string))
(if (has-key? *store-registry* name) (if (has-key? *store-registry* name)
(get *store-registry* name) (get *store-registry* name)
(error (str "Store not found: " name (error (str "Store not found: " name
@@ -402,11 +402,11 @@
;; These are platform primitives because they require browser DOM APIs. ;; These are platform primitives because they require browser DOM APIs.
(define emit-event (define emit-event
(fn (el event-name detail) (fn (el (event-name :as string) detail)
(dom-dispatch el event-name detail))) (dom-dispatch el event-name detail)))
(define on-event (define on-event
(fn (el event-name handler) (fn (el (event-name :as string) (handler :as lambda))
(dom-listen el event-name handler))) (dom-listen el event-name handler)))
;; Convenience: create an effect that listens for a DOM event on an ;; Convenience: create an effect that listens for a DOM event on an
@@ -416,7 +416,7 @@
;; removed automatically via the cleanup return. ;; removed automatically via the cleanup return.
(define bridge-event (define bridge-event
(fn (el event-name target-signal transform-fn) (fn (el (event-name :as string) (target-signal :as signal) transform-fn)
(effect (fn () (effect (fn ()
(let ((remove (dom-listen el event-name (let ((remove (dom-listen el event-name
(fn (e) (fn (e)
@@ -450,7 +450,7 @@
;; (promise-then promise on-resolve on-reject) → void ;; (promise-then promise on-resolve on-reject) → void
(define resource (define resource
(fn (fetch-fn) (fn ((fetch-fn :as lambda))
(let ((state (signal (dict "loading" true "data" nil "error" nil)))) (let ((state (signal (dict "loading" true "data" nil "error" nil))))
;; Kick off the async operation ;; Kick off the async operation
(promise-then (invoke fetch-fn) (promise-then (invoke fetch-fn)

File diff suppressed because it is too large Load Diff

272
shared/sx/ref/test-aser.sx Normal file
View File

@@ -0,0 +1,272 @@
;; ==========================================================================
;; test-aser.sx — Tests for the SX wire format (aser) adapter
;;
;; Requires: test-framework.sx loaded first.
;; Modules tested: adapter-sx.sx (aser, aser-call, aser-fragment, aser-special)
;;
;; Platform functions required (beyond test framework):
;; render-sx (sx-source) -> SX wire format string
;; Parses the sx-source string, evaluates via aser in a
;; fresh env, and returns the resulting SX wire format string.
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Basic serialization
;; --------------------------------------------------------------------------
(defsuite "aser-basics"
(deftest "number literal passes through"
(assert-equal "42"
(render-sx "42")))
(deftest "string literal passes through"
;; aser returns the raw string value; render-sx concatenates it directly
(assert-equal "hello"
(render-sx "\"hello\"")))
(deftest "boolean true passes through"
(assert-equal "true"
(render-sx "true")))
(deftest "boolean false passes through"
(assert-equal "false"
(render-sx "false")))
(deftest "nil produces empty"
(assert-equal ""
(render-sx "nil"))))
;; --------------------------------------------------------------------------
;; HTML tag serialization
;; --------------------------------------------------------------------------
(defsuite "aser-tags"
(deftest "simple div"
(assert-equal "(div \"hello\")"
(render-sx "(div \"hello\")")))
(deftest "nested tags"
(assert-equal "(div (span \"hi\"))"
(render-sx "(div (span \"hi\"))")))
(deftest "multiple children"
(assert-equal "(div (p \"a\") (p \"b\"))"
(render-sx "(div (p \"a\") (p \"b\"))")))
(deftest "attributes serialize"
(assert-equal "(div :class \"foo\" \"bar\")"
(render-sx "(div :class \"foo\" \"bar\")")))
(deftest "multiple attributes"
(assert-equal "(a :href \"/home\" :class \"link\" \"Home\")"
(render-sx "(a :href \"/home\" :class \"link\" \"Home\")")))
(deftest "void elements"
(assert-equal "(br)"
(render-sx "(br)")))
(deftest "void element with attrs"
(assert-equal "(img :src \"pic.jpg\")"
(render-sx "(img :src \"pic.jpg\")"))))
;; --------------------------------------------------------------------------
;; Fragment serialization
;; --------------------------------------------------------------------------
(defsuite "aser-fragments"
(deftest "simple fragment"
(assert-equal "(<> (p \"a\") (p \"b\"))"
(render-sx "(<> (p \"a\") (p \"b\"))")))
(deftest "empty fragment"
(assert-equal ""
(render-sx "(<>)")))
(deftest "single-child fragment"
(assert-equal "(<> (div \"x\"))"
(render-sx "(<> (div \"x\"))"))))
;; --------------------------------------------------------------------------
;; Control flow in aser mode
;; --------------------------------------------------------------------------
(defsuite "aser-control-flow"
(deftest "if true branch"
(assert-equal "(p \"yes\")"
(render-sx "(if true (p \"yes\") (p \"no\"))")))
(deftest "if false branch"
(assert-equal "(p \"no\")"
(render-sx "(if false (p \"yes\") (p \"no\"))")))
(deftest "when true"
(assert-equal "(p \"ok\")"
(render-sx "(when true (p \"ok\"))")))
(deftest "when false"
(assert-equal ""
(render-sx "(when false (p \"ok\"))")))
(deftest "cond serializes matching branch"
(assert-equal "(p \"two\")"
(render-sx "(cond false (p \"one\") true (p \"two\") :else (p \"three\"))")))
(deftest "cond with 2-element predicate test"
;; Regression: cond misclassifies (nil? x) as scheme-style clause.
(assert-equal "(p \"yes\")"
(render-sx "(cond (nil? nil) (p \"yes\") :else (p \"no\"))"))
(assert-equal "(p \"no\")"
(render-sx "(cond (nil? \"x\") (p \"yes\") :else (p \"no\"))")))
(deftest "let binds then serializes"
(assert-equal "(p \"hello\")"
(render-sx "(let ((x \"hello\")) (p x))")))
(deftest "let preserves outer scope bindings"
;; Regression: process-bindings must preserve parent env scope chain.
;; Using merge() instead of env-extend loses parent scope items.
(assert-equal "(p \"outer\")"
(render-sx "(do (define theme \"outer\") (let ((x 1)) (p theme)))")))
(deftest "nested let preserves outer scope"
(assert-equal "(div (span \"hello\") (span \"world\"))"
(render-sx "(do (define a \"hello\")
(define b \"world\")
(div (let ((x 1)) (span a))
(let ((y 2)) (span b))))")))
(deftest "begin serializes last"
(assert-equal "(p \"last\")"
(render-sx "(begin (p \"first\") (p \"last\"))"))))
;; --------------------------------------------------------------------------
;; THE BUG — map/filter list flattening in children (critical regression)
;; --------------------------------------------------------------------------
(defsuite "aser-list-flattening"
(deftest "map inside tag flattens children"
(assert-equal "(div (span \"a\") (span \"b\") (span \"c\"))"
(render-sx "(do (define items (list \"a\" \"b\" \"c\"))
(div (map (fn (x) (span x)) items)))")))
(deftest "map inside tag with other children"
(assert-equal "(ul (li \"first\") (li \"a\") (li \"b\"))"
(render-sx "(do (define items (list \"a\" \"b\"))
(ul (li \"first\") (map (fn (x) (li x)) items)))")))
(deftest "filter result via let binding as children"
;; Note: (filter ...) is treated as an SVG tag in aser dispatch (SVG has <filter>),
;; so we evaluate filter via let binding + map to serialize children
(assert-equal "(ul (li \"a\") (li \"b\"))"
(render-sx "(do (define items (list \"a\" nil \"b\"))
(define kept (filter (fn (x) (not (nil? x))) items))
(ul (map (fn (x) (li x)) kept)))")))
(deftest "map inside fragment flattens"
(assert-equal "(<> (p \"a\") (p \"b\"))"
(render-sx "(do (define items (list \"a\" \"b\"))
(<> (map (fn (x) (p x)) items)))")))
(deftest "nested map does not double-wrap"
(assert-equal "(div (span \"1\") (span \"2\"))"
(render-sx "(do (define nums (list 1 2))
(div (map (fn (n) (span (str n))) nums)))")))
(deftest "map with component-like output flattens"
(assert-equal "(div (li \"x\") (li \"y\"))"
(render-sx "(do (define items (list \"x\" \"y\"))
(div (map (fn (x) (li x)) items)))"))))
;; --------------------------------------------------------------------------
;; Component serialization (NOT expanded in basic aser mode)
;; --------------------------------------------------------------------------
(defsuite "aser-components"
(deftest "unknown component serializes as-is"
(assert-equal "(~foo :title \"bar\")"
(render-sx "(~foo :title \"bar\")")))
(deftest "defcomp then unexpanded component call"
(assert-equal "(~card :title \"Hi\")"
(render-sx "(do (defcomp ~card (&key title) (h1 title)) (~card :title \"Hi\"))")))
(deftest "component with children serializes unexpanded"
(assert-equal "(~box (p \"inside\"))"
(render-sx "(do (defcomp ~box (&key &rest children) (div children))
(~box (p \"inside\")))"))))
;; --------------------------------------------------------------------------
;; Definition forms in aser mode
;; --------------------------------------------------------------------------
(defsuite "aser-definitions"
(deftest "define evaluates for side effects, returns nil"
(assert-equal "(p 42)"
(render-sx "(do (define x 42) (p x))")))
(deftest "defcomp evaluates and returns nil"
(assert-equal "(~tag :x 1)"
(render-sx "(do (defcomp ~tag (&key x) (span x)) (~tag :x 1))")))
(deftest "defisland evaluates AND serializes"
(let ((result (render-sx "(defisland ~counter (&key count) (span count))")))
(assert-true (string-contains? result "defisland")))))
;; --------------------------------------------------------------------------
;; Function calls in aser mode
;; --------------------------------------------------------------------------
(defsuite "aser-function-calls"
(deftest "named function call evaluates fully"
(assert-equal "3"
(render-sx "(do (define inc1 (fn (x) (+ x 1))) (inc1 2))")))
(deftest "define + call"
(assert-equal "10"
(render-sx "(do (define double (fn (x) (* x 2))) (double 5))")))
(deftest "native callable with multiple args"
;; Regression: async-aser-eval-call passed evaled-args list to
;; async-invoke (&rest), wrapping it in another list. apply(f, [list])
;; calls f(list) instead of f(*list).
(assert-equal "3"
(render-sx "(do (define my-add +) (my-add 1 2))")))
(deftest "native callable with two args via alias"
(assert-equal "hello world"
(render-sx "(do (define my-join str) (my-join \"hello\" \" world\"))")))
(deftest "higher-order: map returns list"
(let ((result (render-sx "(map (fn (x) (+ x 1)) (list 1 2 3))")))
;; map at top level returns a list, not serialized tags
(assert-true (not (nil? result))))))
;; --------------------------------------------------------------------------
;; and/or short-circuit in aser mode
;; --------------------------------------------------------------------------
(defsuite "aser-logic"
(deftest "and short-circuits on false"
(assert-equal "false"
(render-sx "(and true false true)")))
(deftest "and returns last truthy"
(assert-equal "3"
(render-sx "(and 1 2 3)")))
(deftest "or short-circuits on true"
(assert-equal "1"
(render-sx "(or 1 2 3)")))
(deftest "or returns last falsy"
(assert-equal "false"
(render-sx "(or false false)"))))

View File

@@ -277,6 +277,29 @@
false "b" false "b"
:else "c"))) :else "c")))
(deftest "cond with 2-element predicate as first test"
;; Regression: cond misclassifies Clojure-style as scheme-style when
;; the first test is a 2-element list like (nil? x) or (empty? x).
;; The evaluator checks: is first arg a 2-element list? If yes, treats
;; as scheme-style ((test body) ...) — returning the arg instead of
;; evaluating the predicate call.
(assert-equal 0 (cond (nil? nil) 0 :else 1))
(assert-equal 1 (cond (nil? "x") 0 :else 1))
(assert-equal "empty" (cond (empty? (list)) "empty" :else "not-empty"))
(assert-equal "not-empty" (cond (empty? (list 1)) "empty" :else "not-empty"))
(assert-equal "yes" (cond (not false) "yes" :else "no"))
(assert-equal "no" (cond (not true) "yes" :else "no")))
(deftest "cond with 2-element predicate and no :else"
;; Same bug, but without :else — this is the worst case because the
;; bootstrapper heuristic also breaks (all clauses are 2-element lists).
(assert-equal "found"
(cond (nil? nil) "found"
(nil? "x") "other"))
(assert-equal "b"
(cond (nil? "x") "a"
(not false) "b")))
(deftest "and" (deftest "and"
(assert-true (and true true)) (assert-true (and true true))
(assert-false (and true false)) (assert-false (and true false))

View File

@@ -57,7 +57,7 @@
(assert (nil? val) (str "Expected nil but got " (str val))))) (assert (nil? val) (str "Expected nil but got " (str val)))))
(define assert-type (define assert-type
(fn (expected-type val) (fn ((expected-type :as string) val)
(let ((actual-type (let ((actual-type
(if (nil? val) "nil" (if (nil? val) "nil"
(if (boolean? val) "boolean" (if (boolean? val) "boolean"
@@ -70,17 +70,17 @@
(str "Expected type " expected-type " but got " actual-type))))) (str "Expected type " expected-type " but got " actual-type)))))
(define assert-length (define assert-length
(fn (expected-len col) (fn ((expected-len :as number) (col :as list))
(assert (= (len col) expected-len) (assert (= (len col) expected-len)
(str "Expected length " expected-len " but got " (len col))))) (str "Expected length " expected-len " but got " (len col)))))
(define assert-contains (define assert-contains
(fn (item col) (fn (item (col :as list))
(assert (some (fn (x) (equal? x item)) col) (assert (some (fn (x) (equal? x item)) col)
(str "Expected collection to contain " (str item))))) (str "Expected collection to contain " (str item)))))
(define assert-throws (define assert-throws
(fn (thunk) (fn ((thunk :as lambda))
(let ((result (try-call thunk))) (let ((result (try-call thunk)))
(assert (not (get result "ok")) (assert (not (get result "ok"))
"Expected an error to be thrown but none was")))) "Expected an error to be thrown but none was"))))

View File

@@ -149,7 +149,27 @@
(deftest "let in render context" (deftest "let in render context"
(assert-equal "<p>hello</p>" (assert-equal "<p>hello</p>"
(render-html "(let ((x \"hello\")) (p x))")))) (render-html "(let ((x \"hello\")) (p x))")))
(deftest "cond with 2-element predicate test"
;; Regression: cond misclassifies (nil? x) as scheme-style clause.
(assert-equal "<p>yes</p>"
(render-html "(cond (nil? nil) (p \"yes\") :else (p \"no\"))"))
(assert-equal "<p>no</p>"
(render-html "(cond (nil? \"x\") (p \"yes\") :else (p \"no\"))")))
(deftest "let preserves outer scope bindings"
;; Regression: process-bindings must preserve parent env scope chain.
;; Using merge() on Env objects returns empty dict (Env is not dict subclass).
(assert-equal "<p>outer</p>"
(render-html "(do (define theme \"outer\") (let ((x 1)) (p theme)))")))
(deftest "nested let preserves outer scope"
(assert-equal "<div><span>hello</span><span>world</span></div>"
(render-html "(do (define a \"hello\")
(define b \"world\")
(div (let ((x 1)) (span a))
(let ((y 2)) (span b))))"))))
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
@@ -165,3 +185,46 @@
(let ((html (render-html "(do (defcomp ~box (&key &rest children) (div :class \"box\" children)) (~box (p \"inside\")))"))) (let ((html (render-html "(do (defcomp ~box (&key &rest children) (div :class \"box\" children)) (~box (p \"inside\")))")))
(assert-true (string-contains? html "class=\"box\"")) (assert-true (string-contains? html "class=\"box\""))
(assert-true (string-contains? html "<p>inside</p>"))))) (assert-true (string-contains? html "<p>inside</p>")))))
;; --------------------------------------------------------------------------
;; Map/filter producing multiple children (aser-adjacent regression tests)
;; --------------------------------------------------------------------------
(defsuite "render-map-children"
(deftest "map producing multiple children inside tag"
(assert-equal "<ul><li>a</li><li>b</li><li>c</li></ul>"
(render-html "(do (define items (list \"a\" \"b\" \"c\"))
(ul (map (fn (x) (li x)) items)))")))
(deftest "map with other siblings"
(assert-equal "<ul><li>first</li><li>a</li><li>b</li></ul>"
(render-html "(do (define items (list \"a\" \"b\"))
(ul (li \"first\") (map (fn (x) (li x)) items)))")))
(deftest "filter with nil results inside tag"
(assert-equal "<ul><li>a</li><li>c</li></ul>"
(render-html "(do (define items (list \"a\" nil \"c\"))
(ul (map (fn (x) (li x))
(filter (fn (x) (not (nil? x))) items))))")))
(deftest "nested map inside let"
(assert-equal "<div><span>1</span><span>2</span></div>"
(render-html "(let ((nums (list 1 2)))
(div (map (fn (n) (span n)) nums)))")))
(deftest "component with &rest receiving mapped results"
(let ((html (render-html "(do (defcomp ~list-box (&key &rest children) (div :class \"lb\" children))
(define items (list \"x\" \"y\"))
(~list-box (map (fn (x) (p x)) items)))")))
(assert-true (string-contains? html "class=\"lb\""))
(assert-true (string-contains? html "<p>x</p>"))
(assert-true (string-contains? html "<p>y</p>"))))
(deftest "map-indexed renders with index"
(assert-equal "<li>0: a</li><li>1: b</li>"
(render-html "(map-indexed (fn (i x) (li (str i \": \" x))) (list \"a\" \"b\"))")))
(deftest "for-each renders each item"
(assert-equal "<p>1</p><p>2</p>"
(render-html "(for-each (fn (x) (p x)) (list 1 2))"))))

432
shared/sx/ref/test-types.sx Normal file
View File

@@ -0,0 +1,432 @@
;; ==========================================================================
;; test-types.sx — Tests for the SX gradual type system
;;
;; Requires: test-framework.sx loaded first.
;; Modules tested: types.sx (subtype?, infer-type, check-component, etc.)
;;
;; Platform functions required (beyond test framework):
;; All type system functions from types.sx must be loaded.
;; test-prim-types — a dict of primitive return types for testing.
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Subtype checking
;; --------------------------------------------------------------------------
(defsuite "subtype-basics"
(deftest "any accepts everything"
(assert-true (subtype? "number" "any"))
(assert-true (subtype? "string" "any"))
(assert-true (subtype? "nil" "any"))
(assert-true (subtype? "boolean" "any"))
(assert-true (subtype? "any" "any")))
(deftest "never is subtype of everything"
(assert-true (subtype? "never" "number"))
(assert-true (subtype? "never" "string"))
(assert-true (subtype? "never" "any"))
(assert-true (subtype? "never" "nil")))
(deftest "identical types"
(assert-true (subtype? "number" "number"))
(assert-true (subtype? "string" "string"))
(assert-true (subtype? "boolean" "boolean"))
(assert-true (subtype? "nil" "nil")))
(deftest "different base types are not subtypes"
(assert-false (subtype? "number" "string"))
(assert-false (subtype? "string" "number"))
(assert-false (subtype? "boolean" "number"))
(assert-false (subtype? "string" "boolean")))
(deftest "any is not subtype of specific type"
(assert-false (subtype? "any" "number"))
(assert-false (subtype? "any" "string"))))
(defsuite "subtype-nullable"
(deftest "nil is subtype of nullable types"
(assert-true (subtype? "nil" "string?"))
(assert-true (subtype? "nil" "number?"))
(assert-true (subtype? "nil" "dict?"))
(assert-true (subtype? "nil" "boolean?")))
(deftest "base is subtype of its nullable"
(assert-true (subtype? "string" "string?"))
(assert-true (subtype? "number" "number?"))
(assert-true (subtype? "dict" "dict?")))
(deftest "nullable is not subtype of base"
(assert-false (subtype? "string?" "string"))
(assert-false (subtype? "number?" "number")))
(deftest "different nullable types are not subtypes"
(assert-false (subtype? "number" "string?"))
(assert-false (subtype? "string" "number?"))))
(defsuite "subtype-unions"
(deftest "member is subtype of union"
(assert-true (subtype? "number" (list "or" "number" "string")))
(assert-true (subtype? "string" (list "or" "number" "string"))))
(deftest "non-member is not subtype of union"
(assert-false (subtype? "boolean" (list "or" "number" "string"))))
(deftest "union is subtype if all members are"
(assert-true (subtype? (list "or" "number" "string")
(list "or" "number" "string" "boolean")))
(assert-true (subtype? (list "or" "number" "string") "any")))
(deftest "union is not subtype if any member is not"
(assert-false (subtype? (list "or" "number" "string") "number"))))
(defsuite "subtype-list-of"
(deftest "list-of covariance"
(assert-true (subtype? (list "list-of" "number") (list "list-of" "number")))
(assert-true (subtype? (list "list-of" "number") (list "list-of" "any"))))
(deftest "list-of is subtype of list"
(assert-true (subtype? (list "list-of" "number") "list")))
(deftest "list is subtype of list-of any"
(assert-true (subtype? "list" (list "list-of" "any")))))
;; --------------------------------------------------------------------------
;; Type union
;; --------------------------------------------------------------------------
(defsuite "type-union"
(deftest "same types"
(assert-equal "number" (type-union "number" "number"))
(assert-equal "string" (type-union "string" "string")))
(deftest "any absorbs"
(assert-equal "any" (type-union "any" "number"))
(assert-equal "any" (type-union "number" "any")))
(deftest "never is identity"
(assert-equal "number" (type-union "never" "number"))
(assert-equal "string" (type-union "string" "never")))
(deftest "nil + base creates nullable"
(assert-equal "string?" (type-union "nil" "string"))
(assert-equal "number?" (type-union "number" "nil")))
(deftest "subtype collapses"
(assert-equal "string?" (type-union "string" "string?"))
(assert-equal "string?" (type-union "string?" "string")))
(deftest "incompatible creates union"
(let ((result (type-union "number" "string")))
(assert-true (= (type-of result) "list"))
(assert-equal "or" (first result))
(assert-true (contains? result "number"))
(assert-true (contains? result "string")))))
;; --------------------------------------------------------------------------
;; Type narrowing
;; --------------------------------------------------------------------------
(defsuite "type-narrowing"
(deftest "nil? narrows to nil in then branch"
(let ((result (narrow-type "string?" "nil?")))
(assert-equal "nil" (first result))
(assert-equal "string" (nth result 1))))
(deftest "nil? narrows any stays any"
(let ((result (narrow-type "any" "nil?")))
(assert-equal "nil" (first result))
(assert-equal "any" (nth result 1))))
(deftest "string? narrows to string in then branch"
(let ((result (narrow-type "any" "string?")))
(assert-equal "string" (first result))
;; else branch — can't narrow any
(assert-equal "any" (nth result 1))))
(deftest "nil? on nil type narrows to never in else"
(let ((result (narrow-type "nil" "nil?")))
(assert-equal "nil" (first result))
(assert-equal "never" (nth result 1)))))
;; --------------------------------------------------------------------------
;; Type inference
;; --------------------------------------------------------------------------
(defsuite "infer-literals"
(deftest "number literal"
(assert-equal "number" (infer-type 42 (dict) (test-prim-types))))
(deftest "string literal"
(assert-equal "string" (infer-type "hello" (dict) (test-prim-types))))
(deftest "boolean literal"
(assert-equal "boolean" (infer-type true (dict) (test-prim-types))))
(deftest "nil"
(assert-equal "nil" (infer-type nil (dict) (test-prim-types)))))
(defsuite "infer-calls"
(deftest "known primitive return type"
;; (+ 1 2) → number
(let ((expr (sx-parse "(+ 1 2)")))
(assert-equal "number"
(infer-type (first expr) (dict) (test-prim-types)))))
(deftest "str returns string"
(let ((expr (sx-parse "(str 1 2)")))
(assert-equal "string"
(infer-type (first expr) (dict) (test-prim-types)))))
(deftest "comparison returns boolean"
(let ((expr (sx-parse "(= 1 2)")))
(assert-equal "boolean"
(infer-type (first expr) (dict) (test-prim-types)))))
(deftest "component call returns element"
(let ((expr (sx-parse "(~card :title \"hi\")")))
(assert-equal "element"
(infer-type (first expr) (dict) (test-prim-types)))))
(deftest "unknown function returns any"
(let ((expr (sx-parse "(unknown-fn 1 2)")))
(assert-equal "any"
(infer-type (first expr) (dict) (test-prim-types))))))
(defsuite "infer-special-forms"
(deftest "if produces union of branches"
(let ((expr (sx-parse "(if true 42 \"hello\")")))
(let ((t (infer-type (first expr) (dict) (test-prim-types))))
;; number | string — should be a union
(assert-true (or (= t (list "or" "number" "string"))
(= t "any"))))))
(deftest "if with no else includes nil"
(let ((expr (sx-parse "(if true 42)")))
(let ((t (infer-type (first expr) (dict) (test-prim-types))))
(assert-equal "number?" t))))
(deftest "when includes nil"
(let ((expr (sx-parse "(when true 42)")))
(let ((t (infer-type (first expr) (dict) (test-prim-types))))
(assert-equal "number?" t))))
(deftest "do returns last type"
(let ((expr (sx-parse "(do 1 2 \"hello\")")))
(assert-equal "string"
(infer-type (first expr) (dict) (test-prim-types)))))
(deftest "let infers binding types"
(let ((expr (sx-parse "(let ((x 42)) x)")))
(assert-equal "number"
(infer-type (first expr) (dict) (test-prim-types)))))
(deftest "lambda returns lambda"
(let ((expr (sx-parse "(fn (x) (+ x 1))")))
(assert-equal "lambda"
(infer-type (first expr) (dict) (test-prim-types))))))
;; --------------------------------------------------------------------------
;; Component call checking
;; --------------------------------------------------------------------------
(defsuite "check-component-calls"
(deftest "type mismatch produces error"
;; Create a component with typed params, then check a bad call
(let ((env (test-env)))
;; Define a typed component
(do
(define dummy-env env)
(defcomp ~typed-card (&key title price) (div title price))
(component-set-param-types! ~typed-card
{:title "string" :price "number"}))
;; Check a call with wrong type
(let ((diagnostics
(check-component-call "~typed-card" ~typed-card
(rest (first (sx-parse "(~typed-card :title 42 :price \"bad\")")))
(dict) (test-prim-types))))
(assert-true (> (len diagnostics) 0))
(assert-equal "error" (dict-get (first diagnostics) "level")))))
(deftest "correct call produces no errors"
(let ((env (test-env)))
(do
(define dummy-env env)
(defcomp ~ok-card (&key title price) (div title price))
(component-set-param-types! ~ok-card
{:title "string" :price "number"}))
(let ((diagnostics
(check-component-call "~ok-card" ~ok-card
(rest (first (sx-parse "(~ok-card :title \"hi\" :price 42)")))
(dict) (test-prim-types))))
(assert-equal 0 (len diagnostics)))))
(deftest "unknown kwarg produces warning"
(let ((env (test-env)))
(do
(define dummy-env env)
(defcomp ~warn-card (&key title) (div title))
(component-set-param-types! ~warn-card
{:title "string"}))
(let ((diagnostics
(check-component-call "~warn-card" ~warn-card
(rest (first (sx-parse "(~warn-card :title \"hi\" :colour \"red\")")))
(dict) (test-prim-types))))
(assert-true (> (len diagnostics) 0))
(assert-equal "warning" (dict-get (first diagnostics) "level"))))))
;; --------------------------------------------------------------------------
;; Annotation syntax: (name :as type) in defcomp params
;; --------------------------------------------------------------------------
(defsuite "typed-defcomp"
(deftest "typed params are parsed and stored"
(let ((env (test-env)))
(defcomp ~typed-widget (&key (title :as string) (count :as number)) (div title count))
(let ((pt (component-param-types ~typed-widget)))
(assert-true (not (nil? pt)))
(assert-equal "string" (dict-get pt "title"))
(assert-equal "number" (dict-get pt "count")))))
(deftest "mixed typed and untyped params"
(let ((env (test-env)))
(defcomp ~mixed-widget (&key (title :as string) subtitle) (div title subtitle))
(let ((pt (component-param-types ~mixed-widget)))
(assert-true (not (nil? pt)))
(assert-equal "string" (dict-get pt "title"))
;; subtitle has no annotation — should not be in param-types
(assert-false (has-key? pt "subtitle")))))
(deftest "untyped defcomp has nil param-types"
(let ((env (test-env)))
(defcomp ~plain-widget (&key title subtitle) (div title subtitle))
(assert-true (nil? (component-param-types ~plain-widget)))))
(deftest "typed component catches type error on call"
(let ((env (test-env)))
(defcomp ~strict-card (&key (title :as string) (price :as number)) (div title price))
;; Call with wrong types
(let ((diagnostics
(check-component-call "~strict-card" ~strict-card
(rest (first (sx-parse "(~strict-card :title 42 :price \"bad\")")))
(dict) (test-prim-types))))
;; Should have errors for both wrong-type args
(assert-true (>= (len diagnostics) 1))
(assert-equal "error" (dict-get (first diagnostics) "level")))))
(deftest "typed component passes correct call"
(let ((env (test-env)))
(defcomp ~ok-widget (&key (name :as string) (age :as number)) (div name age))
(let ((diagnostics
(check-component-call "~ok-widget" ~ok-widget
(rest (first (sx-parse "(~ok-widget :name \"Alice\" :age 30)")))
(dict) (test-prim-types))))
(assert-equal 0 (len diagnostics)))))
(deftest "nullable type accepts nil"
(let ((env (test-env)))
(defcomp ~nullable-widget (&key (title :as string) (subtitle :as string?)) (div title subtitle))
;; Passing nil for nullable param should be fine
(let ((diagnostics
(check-component-call "~nullable-widget" ~nullable-widget
(rest (first (sx-parse "(~nullable-widget :title \"hi\" :subtitle nil)")))
(dict) (test-prim-types))))
(assert-equal 0 (len diagnostics))))))
;; --------------------------------------------------------------------------
;; Primitive call checking (Phase 5)
;; --------------------------------------------------------------------------
(defsuite "check-primitive-calls"
(deftest "correct types produce no errors"
(let ((ppt (test-prim-param-types)))
(let ((diagnostics
(check-primitive-call "+" (rest (first (sx-parse "(+ 1 2 3)")))
(dict) (test-prim-types) ppt nil)))
(assert-equal 0 (len diagnostics)))))
(deftest "string arg to numeric primitive produces error"
(let ((ppt (test-prim-param-types)))
(let ((diagnostics
(check-primitive-call "+" (rest (first (sx-parse "(+ 1 \"hello\")")))
(dict) (test-prim-types) ppt nil)))
(assert-true (> (len diagnostics) 0))
(assert-equal "error" (get (first diagnostics) "level")))))
(deftest "number arg to string primitive produces error"
(let ((ppt (test-prim-param-types)))
(let ((diagnostics
(check-primitive-call "upper" (rest (first (sx-parse "(upper 42)")))
(dict) (test-prim-types) ppt nil)))
(assert-true (> (len diagnostics) 0))
(assert-equal "error" (get (first diagnostics) "level")))))
(deftest "positional and rest params both checked"
;; (- "bad" 1) — first positional arg is string, expects number
(let ((ppt (test-prim-param-types)))
(let ((diagnostics
(check-primitive-call "-" (rest (first (sx-parse "(- \"bad\" 1)")))
(dict) (test-prim-types) ppt nil)))
(assert-true (> (len diagnostics) 0)))))
(deftest "dict arg to keys is valid"
(let ((ppt (test-prim-param-types)))
(let ((diagnostics
(check-primitive-call "keys" (rest (first (sx-parse "(keys {:a 1})")))
(dict) (test-prim-types) ppt nil)))
(assert-equal 0 (len diagnostics)))))
(deftest "number arg to keys produces error"
(let ((ppt (test-prim-param-types)))
(let ((diagnostics
(check-primitive-call "keys" (rest (first (sx-parse "(keys 42)")))
(dict) (test-prim-types) ppt nil)))
(assert-true (> (len diagnostics) 0)))))
(deftest "variable with known type passes check"
;; Variable n is known to be number in type-env
(let ((ppt (test-prim-param-types))
(tenv {"n" "number"}))
(let ((diagnostics
(check-primitive-call "inc" (rest (first (sx-parse "(inc n)")))
tenv (test-prim-types) ppt nil)))
(assert-equal 0 (len diagnostics)))))
(deftest "variable with wrong type fails check"
;; Variable s is known to be string in type-env
(let ((ppt (test-prim-param-types))
(tenv {"s" "string"}))
(let ((diagnostics
(check-primitive-call "inc" (rest (first (sx-parse "(inc s)")))
tenv (test-prim-types) ppt nil)))
(assert-true (> (len diagnostics) 0)))))
(deftest "any-typed variable skips check"
;; Variable x has type any — should not produce errors
(let ((ppt (test-prim-param-types))
(tenv {"x" "any"}))
(let ((diagnostics
(check-primitive-call "upper" (rest (first (sx-parse "(upper x)")))
tenv (test-prim-types) ppt nil)))
(assert-equal 0 (len diagnostics)))))
(deftest "body-walk catches primitive errors in component"
;; Manually build a component and check it via check-body-walk directly
(let ((ppt (test-prim-param-types))
(body (first (sx-parse "(div (+ name 1))")))
(type-env {"name" "string"})
(diagnostics (list)))
(check-body-walk body "~bad-math" type-env (test-prim-types) ppt (test-env) diagnostics)
(assert-true (> (len diagnostics) 0))
(assert-equal "error" (get (first diagnostics) "level")))))

657
shared/sx/ref/types.sx Normal file
View File

@@ -0,0 +1,657 @@
;; ==========================================================================
;; types.sx — Gradual type system for SX
;;
;; Registration-time type checking: zero runtime cost.
;; Annotations are optional — unannotated code defaults to `any`.
;;
;; Depends on: eval.sx (type-of, component accessors, env ops)
;; primitives.sx, boundary.sx (return type declarations)
;;
;; Platform interface (from eval.sx, already provided):
;; (type-of x) → type string
;; (symbol-name s) → string
;; (keyword-name k) → string
;; (component-body c) → AST
;; (component-params c) → list of param name strings
;; (component-has-children c) → boolean
;; (env-get env k) → value or nil
;;
;; New platform functions for types.sx:
;; (component-param-types c) → dict {param-name → type} or nil
;; (component-set-param-types! c d) → store param types on component
;; ==========================================================================
;; --------------------------------------------------------------------------
;; 1. Type representation
;; --------------------------------------------------------------------------
;; Types are plain SX values:
;; - Strings for base types: "number", "string", "boolean", "nil",
;; "symbol", "keyword", "element", "any", "never"
;; - Nullable shorthand: "string?", "number?", "dict?", "boolean?"
;; → equivalent to (or string nil) etc.
;; - Lists for compound types:
;; (or t1 t2 ...) — union
;; (list-of t) — homogeneous list
;; (dict-of tk tv) — typed dict
;; (-> t1 t2 ... treturn) — function type (last is return)
;; Base type names
(define base-types
(list "number" "string" "boolean" "nil" "symbol" "keyword"
"element" "any" "never" "list" "dict"
"lambda" "component" "island" "macro" "signal"))
;; --------------------------------------------------------------------------
;; 2. Type predicates
;; --------------------------------------------------------------------------
(define type-any?
(fn (t) (= t "any")))
(define type-never?
(fn (t) (= t "never")))
(define type-nullable?
(fn (t)
;; A type is nullable if it's "any", "nil", a "?" shorthand, or
;; a union containing "nil".
(if (= t "any") true
(if (= t "nil") true
(if (and (= (type-of t) "string") (ends-with? t "?")) true
(if (and (= (type-of t) "list")
(not (empty? t))
(= (first t) "or"))
(contains? (rest t) "nil")
false))))))
(define nullable-base
(fn (t)
;; Strip "?" from nullable shorthand: "string?" → "string"
(if (and (= (type-of t) "string")
(ends-with? t "?")
(not (= t "?")))
(slice t 0 (- (string-length t) 1))
t)))
;; --------------------------------------------------------------------------
;; 3. Subtype checking
;; --------------------------------------------------------------------------
;; subtype?(a, b) — is type `a` assignable to type `b`?
(define subtype?
(fn (a b)
;; any accepts everything
(if (type-any? b) true
;; never is subtype of everything
(if (type-never? a) true
;; any is not a subtype of a specific type
(if (type-any? a) false
;; identical types
(if (= a b) true
;; nil is subtype of nullable types
(if (= a "nil")
(type-nullable? b)
;; nullable shorthand: "string?" = (or string nil)
(if (and (= (type-of b) "string") (ends-with? b "?"))
(let ((base (nullable-base b)))
(or (= a base) (= a "nil")))
;; a is a union: (or t1 t2 ...) <: b if ALL members <: b
;; Must check before b-union — (or A B) <: (or A B C) needs
;; each member of a checked against the full union b.
(if (and (= (type-of a) "list")
(not (empty? a))
(= (first a) "or"))
(every? (fn (member) (subtype? member b)) (rest a))
;; union: a <: (or t1 t2 ...) if a <: any member
(if (and (= (type-of b) "list")
(not (empty? b))
(= (first b) "or"))
(some (fn (member) (subtype? a member)) (rest b))
;; list-of covariance
(if (and (= (type-of a) "list") (= (type-of b) "list")
(= (len a) 2) (= (len b) 2)
(= (first a) "list-of") (= (first b) "list-of"))
(subtype? (nth a 1) (nth b 1))
;; "list" <: (list-of any)
(if (and (= a "list")
(= (type-of b) "list")
(= (len b) 2)
(= (first b) "list-of"))
(type-any? (nth b 1))
;; (list-of t) <: "list"
(if (and (= (type-of a) "list")
(= (len a) 2)
(= (first a) "list-of")
(= b "list"))
true
;; "element" is subtype of "string?" (rendered HTML)
false)))))))))))))
;; --------------------------------------------------------------------------
;; 4. Type union
;; --------------------------------------------------------------------------
(define type-union
(fn (a b)
;; Compute the smallest type that encompasses both a and b.
(if (= a b) a
(if (type-any? a) "any"
(if (type-any? b) "any"
(if (type-never? a) b
(if (type-never? b) a
(if (subtype? a b) b
(if (subtype? b a) a
;; neither is subtype — create a union
(if (= a "nil")
;; nil + string → string?
(if (and (= (type-of b) "string")
(not (ends-with? b "?")))
(str b "?")
(list "or" a b))
(if (= b "nil")
(if (and (= (type-of a) "string")
(not (ends-with? a "?")))
(str a "?")
(list "or" a b))
(list "or" a b))))))))))))
;; --------------------------------------------------------------------------
;; 5. Type narrowing
;; --------------------------------------------------------------------------
(define narrow-type
(fn (t (predicate-name :as string))
;; Narrow type based on a predicate test in a truthy branch.
;; (if (nil? x) ..then.. ..else..) → in else, x excludes nil.
;; Returns (narrowed-then narrowed-else).
(if (= predicate-name "nil?")
(list "nil" (narrow-exclude-nil t))
(if (= predicate-name "string?")
(list "string" (narrow-exclude t "string"))
(if (= predicate-name "number?")
(list "number" (narrow-exclude t "number"))
(if (= predicate-name "list?")
(list "list" (narrow-exclude t "list"))
(if (= predicate-name "dict?")
(list "dict" (narrow-exclude t "dict"))
(if (= predicate-name "boolean?")
(list "boolean" (narrow-exclude t "boolean"))
;; Unknown predicate — no narrowing
(list t t)))))))))
(define narrow-exclude-nil
(fn (t)
;; Remove nil from a type.
(if (= t "nil") "never"
(if (= t "any") "any" ;; can't narrow any
(if (and (= (type-of t) "string") (ends-with? t "?"))
(nullable-base t)
(if (and (= (type-of t) "list")
(not (empty? t))
(= (first t) "or"))
(let ((members (filter (fn (m) (not (= m "nil"))) (rest t))))
(if (= (len members) 1) (first members)
(if (empty? members) "never"
(cons "or" members))))
t))))))
(define narrow-exclude
(fn (t excluded)
;; Remove a specific type from a union.
(if (= t excluded) "never"
(if (= t "any") "any"
(if (and (= (type-of t) "list")
(not (empty? t))
(= (first t) "or"))
(let ((members (filter (fn (m) (not (= m excluded))) (rest t))))
(if (= (len members) 1) (first members)
(if (empty? members) "never"
(cons "or" members))))
t)))))
;; --------------------------------------------------------------------------
;; 6. Type inference
;; --------------------------------------------------------------------------
;; infer-type walks an AST node and returns its inferred type.
;; type-env is a dict mapping variable names → types.
(define infer-type
(fn (node (type-env :as dict) (prim-types :as dict))
(let ((kind (type-of node)))
(if (= kind "number") "number"
(if (= kind "string") "string"
(if (= kind "boolean") "boolean"
(if (nil? node) "nil"
(if (= kind "keyword") "keyword"
(if (= kind "symbol")
(let ((name (symbol-name node)))
;; Look up in type env
(if (dict-has? type-env name)
(dict-get type-env name)
;; Builtins
(if (= name "true") "boolean"
(if (= name "false") "boolean"
(if (= name "nil") "nil"
;; Check primitive return types
(if (dict-has? prim-types name)
(dict-get prim-types name)
"any"))))))
(if (= kind "dict") "dict"
(if (= kind "list")
(infer-list-type node type-env prim-types)
"any")))))))))))
(define infer-list-type
(fn (node (type-env :as dict) (prim-types :as dict))
;; Infer type of a list expression (function call, special form, etc.)
(if (empty? node) "list"
(let ((head (first node))
(args (rest node)))
(if (not (= (type-of head) "symbol"))
"any" ;; complex head — can't infer
(let ((name (symbol-name head)))
;; Special forms
(if (= name "if")
(infer-if-type args type-env prim-types)
(if (= name "when")
(if (>= (len args) 2)
(type-union (infer-type (last args) type-env prim-types) "nil")
"nil")
(if (or (= name "cond") (= name "case"))
"any" ;; complex — could be refined later
(if (= name "let")
(infer-let-type args type-env prim-types)
(if (or (= name "do") (= name "begin"))
(if (empty? args) "nil"
(infer-type (last args) type-env prim-types))
(if (or (= name "lambda") (= name "fn"))
"lambda"
(if (= name "and")
(if (empty? args) "boolean"
(infer-type (last args) type-env prim-types))
(if (= name "or")
(if (empty? args) "boolean"
;; or returns first truthy — union of all args
(reduce type-union "never"
(map (fn (a) (infer-type a type-env prim-types)) args)))
(if (= name "map")
;; map returns a list
(if (>= (len args) 2)
(let ((fn-type (infer-type (first args) type-env prim-types)))
;; If the fn's return type is known, produce (list-of return-type)
(if (and (= (type-of fn-type) "list")
(= (first fn-type) "->"))
(list "list-of" (last fn-type))
"list"))
"list")
(if (= name "filter")
;; filter preserves element type
(if (>= (len args) 2)
(infer-type (nth args 1) type-env prim-types)
"list")
(if (= name "reduce")
;; reduce returns the accumulator type — too complex to infer
"any"
(if (= name "list")
"list"
(if (= name "dict")
"dict"
(if (= name "quote")
"any"
(if (= name "str")
"string"
(if (= name "not")
"boolean"
(if (starts-with? name "~")
"element" ;; component call
;; Regular function call: look up return type
(if (dict-has? prim-types name)
(dict-get prim-types name)
"any"))))))))))))))))))))))))
(define infer-if-type
(fn ((args :as list) (type-env :as dict) (prim-types :as dict))
;; (if test then else?) → union of then and else types
(if (< (len args) 2) "nil"
(let ((then-type (infer-type (nth args 1) type-env prim-types)))
(if (>= (len args) 3)
(type-union then-type (infer-type (nth args 2) type-env prim-types))
(type-union then-type "nil"))))))
(define infer-let-type
(fn ((args :as list) (type-env :as dict) (prim-types :as dict))
;; (let ((x expr) ...) body) → type of body in extended type-env
(if (< (len args) 2) "nil"
(let ((bindings (first args))
(body (last args))
(extended (merge type-env (dict))))
;; Add binding types
(for-each
(fn (binding)
(when (and (= (type-of binding) "list") (>= (len binding) 2))
(let ((name (if (= (type-of (first binding)) "symbol")
(symbol-name (first binding))
(str (first binding))))
(val-type (infer-type (nth binding 1) extended prim-types)))
(dict-set! extended name val-type))))
bindings)
(infer-type body extended prim-types)))))
;; --------------------------------------------------------------------------
;; 7. Diagnostic types
;; --------------------------------------------------------------------------
;; Diagnostics are dicts:
;; {:level "error"|"warning"|"info"
;; :message "human-readable explanation"
;; :component "~name" (or nil for top-level)
;; :expr <the offending AST node>}
(define make-diagnostic
(fn ((level :as string) (message :as string) component expr)
{:level level
:message message
:component component
:expr expr}))
;; --------------------------------------------------------------------------
;; 8. Call-site checking
;; --------------------------------------------------------------------------
(define check-primitive-call
(fn ((name :as string) (args :as list) (type-env :as dict) (prim-types :as dict) prim-param-types (comp-name :as string))
;; Check a primitive call site against declared param types.
;; prim-param-types is a dict: {prim-name → {:positional [...] :rest-type type-or-nil}}
;; Each positional entry is a list (name type-or-nil).
;; Returns list of diagnostics.
(let ((diagnostics (list)))
(when (and (not (nil? prim-param-types))
(dict-has? prim-param-types name))
(let ((sig (get prim-param-types name))
(positional (get sig "positional"))
(rest-type (get sig "rest-type")))
;; Check each positional arg
(for-each
(fn (idx)
(when (< idx (len args))
(if (< idx (len positional))
;; Positional param — check against declared type
(let ((param-info (nth positional idx))
(arg-expr (nth args idx)))
(let ((expected-type (nth param-info 1)))
(when (not (nil? expected-type))
(let ((actual (infer-type arg-expr type-env prim-types)))
(when (and (not (type-any? expected-type))
(not (type-any? actual))
(not (subtype? actual expected-type)))
(append! diagnostics
(make-diagnostic "error"
(str "Argument " (+ idx 1) " of `" name
"` expects " expected-type ", got " actual)
comp-name arg-expr)))))))
;; Rest param — check against rest-type
(when (not (nil? rest-type))
(let ((arg-expr (nth args idx))
(actual (infer-type arg-expr type-env prim-types)))
(when (and (not (type-any? rest-type))
(not (type-any? actual))
(not (subtype? actual rest-type)))
(append! diagnostics
(make-diagnostic "error"
(str "Argument " (+ idx 1) " of `" name
"` expects " rest-type ", got " actual)
comp-name arg-expr))))))))
(range 0 (len args) 1))))
diagnostics)))
(define check-component-call
(fn ((comp-name :as string) (comp :as component) (call-args :as list) (type-env :as dict) (prim-types :as dict))
;; Check a component call site against its declared param types.
;; comp is the component value, call-args is the list of args
;; from the call site (after the component name).
(let ((diagnostics (list))
(param-types (component-param-types comp))
(params (component-params comp)))
(when (and (not (nil? param-types))
(not (empty? (keys param-types))))
;; Parse keyword args from call site
(let ((i 0)
(provided-keys (list)))
(for-each
(fn (idx)
(when (< idx (len call-args))
(let ((arg (nth call-args idx)))
(when (= (type-of arg) "keyword")
(let ((key-name (keyword-name arg)))
(append! provided-keys key-name)
(when (< (+ idx 1) (len call-args))
(let ((val-expr (nth call-args (+ idx 1))))
;; Check type of value against declared param type
(when (dict-has? param-types key-name)
(let ((expected (dict-get param-types key-name))
(actual (infer-type val-expr type-env prim-types)))
(when (and (not (type-any? expected))
(not (type-any? actual))
(not (subtype? actual expected)))
(append! diagnostics
(make-diagnostic "error"
(str "Keyword :" key-name " of " comp-name
" expects " expected ", got " actual)
comp-name val-expr))))))))))))
(range 0 (len call-args) 1))
;; Check for missing required params (those with declared types)
(for-each
(fn (param-name)
(when (and (dict-has? param-types param-name)
(not (contains? provided-keys param-name))
(not (type-nullable? (dict-get param-types param-name))))
(append! diagnostics
(make-diagnostic "warning"
(str "Required param :" param-name " of " comp-name " not provided")
comp-name nil))))
params)
;; Check for unknown kwargs
(for-each
(fn (key)
(when (not (contains? params key))
(append! diagnostics
(make-diagnostic "warning"
(str "Unknown keyword :" key " passed to " comp-name)
comp-name nil))))
provided-keys)))
diagnostics)))
;; --------------------------------------------------------------------------
;; 9. AST walker — check a component body
;; --------------------------------------------------------------------------
(define check-body-walk
(fn (node (comp-name :as string) (type-env :as dict) (prim-types :as dict) prim-param-types env (diagnostics :as list))
;; Recursively walk an AST and collect diagnostics.
;; prim-param-types: dict of {name → {:positional [...] :rest-type t}} or nil
(let ((kind (type-of node)))
(when (= kind "list")
(when (not (empty? node))
(let ((head (first node))
(args (rest node)))
;; Check calls when head is a symbol
(when (= (type-of head) "symbol")
(let ((name (symbol-name head)))
;; Component call
(when (starts-with? name "~")
(let ((comp-val (env-get env name)))
(when (= (type-of comp-val) "component")
(for-each
(fn (d) (append! diagnostics d))
(check-component-call name comp-val args
type-env prim-types)))))
;; Primitive call — check param types
(when (and (not (starts-with? name "~"))
(not (nil? prim-param-types))
(dict-has? prim-param-types name))
(for-each
(fn (d) (append! diagnostics d))
(check-primitive-call name args type-env prim-types
prim-param-types comp-name)))
;; Recurse into let with extended type env
(when (or (= name "let") (= name "let*"))
(when (>= (len args) 2)
(let ((bindings (first args))
(body-exprs (rest args))
(extended (merge type-env (dict))))
(for-each
(fn (binding)
(when (and (= (type-of binding) "list")
(>= (len binding) 2))
(let ((bname (if (= (type-of (first binding)) "symbol")
(symbol-name (first binding))
(str (first binding))))
(val-type (infer-type (nth binding 1) extended prim-types)))
(dict-set! extended bname val-type))))
bindings)
(for-each
(fn (body)
(check-body-walk body comp-name extended prim-types prim-param-types env diagnostics))
body-exprs))))
;; Recurse into define with type binding
(when (= name "define")
(when (>= (len args) 2)
(let ((def-name (if (= (type-of (first args)) "symbol")
(symbol-name (first args))
nil))
(def-val (nth args 1)))
(when def-name
(dict-set! type-env def-name
(infer-type def-val type-env prim-types)))
(check-body-walk def-val comp-name type-env prim-types prim-param-types env diagnostics))))))
;; Recurse into all child expressions
(for-each
(fn (child)
(check-body-walk child comp-name type-env prim-types prim-param-types env diagnostics))
args)))))))
;; --------------------------------------------------------------------------
;; 10. Check a single component
;; --------------------------------------------------------------------------
(define check-component
(fn ((comp-name :as string) env (prim-types :as dict) prim-param-types)
;; Type-check a component's body. Returns list of diagnostics.
;; prim-param-types: dict of param type info, or nil to skip primitive checking.
(let ((comp (env-get env comp-name))
(diagnostics (list)))
(when (= (type-of comp) "component")
(let ((body (component-body comp))
(params (component-params comp))
(param-types (component-param-types comp))
;; Build initial type env from component params
(type-env (dict)))
;; Add param types (annotated or default to any)
(for-each
(fn (p)
(dict-set! type-env p
(if (and (not (nil? param-types))
(dict-has? param-types p))
(dict-get param-types p)
"any")))
params)
;; Add children as (list-of element) if component has children
(when (component-has-children comp)
(dict-set! type-env "children" (list "list-of" "element")))
(check-body-walk body comp-name type-env prim-types prim-param-types env diagnostics)))
diagnostics)))
;; --------------------------------------------------------------------------
;; 11. Check all components in an environment
;; --------------------------------------------------------------------------
(define check-all
(fn (env (prim-types :as dict) prim-param-types)
;; Type-check every component in the environment.
;; prim-param-types: dict of param type info, or nil to skip primitive checking.
;; Returns list of all diagnostics.
(let ((all-diagnostics (list)))
(for-each
(fn (name)
(let ((val (env-get env name)))
(when (= (type-of val) "component")
(for-each
(fn (d) (append! all-diagnostics d))
(check-component name env prim-types prim-param-types)))))
(keys env))
all-diagnostics)))
;; --------------------------------------------------------------------------
;; 12. Build primitive type registry
;; --------------------------------------------------------------------------
;; Builds a dict mapping primitive-name → return-type from
;; the declarations parsed by boundary_parser.py.
;; This is called by the host at startup with the parsed declarations.
(define build-type-registry
(fn ((prim-declarations :as list) (io-declarations :as list))
;; Both are lists of dicts: {:name "+" :returns "number" :params (...)}
;; Returns a flat dict: {"+" "number", "str" "string", ...}
(let ((registry (dict)))
(for-each
(fn (decl)
(let ((name (dict-get decl "name"))
(returns (dict-get decl "returns")))
(when (and (not (nil? name)) (not (nil? returns)))
(dict-set! registry name returns))))
prim-declarations)
(for-each
(fn (decl)
(let ((name (dict-get decl "name"))
(returns (dict-get decl "returns")))
(when (and (not (nil? name)) (not (nil? returns)))
(dict-set! registry name returns))))
io-declarations)
registry)))
;; --------------------------------------------------------------------------
;; Platform interface summary
;; --------------------------------------------------------------------------
;;
;; From eval.sx (already provided):
;; (type-of x), (symbol-name s), (keyword-name k), (env-get env k)
;; (component-body c), (component-params c), (component-has-children c)
;;
;; New for types.sx (each host implements):
;; (component-param-types c) → dict {param-name → type} or nil
;; (component-set-param-types! c d) → store param types on component
;; (merge d1 d2) → new dict merging d1 and d2
;;
;; Primitive param types:
;; The host provides prim-param-types as a dict mapping primitive names
;; to param type descriptors. Each descriptor is a dict:
;; {"positional" [["name" "type-or-nil"] ...] "rest-type" "type-or-nil"}
;; Built by boundary_parser.parse_primitive_param_types() in Python.
;; Passed to check-component/check-all as an optional extra argument.
;;
;; --------------------------------------------------------------------------

View File

@@ -25,7 +25,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define z3-sort (define z3-sort
(fn (sx-type) (fn ((sx-type :as string))
(case sx-type (case sx-type
"number" "Int" "number" "Int"
"boolean" "Bool" "boolean" "Bool"
@@ -40,7 +40,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define z3-name (define z3-name
(fn (name) (fn ((name :as string))
(cond (cond
(= name "!=") "neq" (= name "!=") "neq"
(= name "+") "+" (= name "+") "+"
@@ -74,7 +74,7 @@
;; Operators that get renamed ;; Operators that get renamed
(define z3-rename-op (define z3-rename-op
(fn (op) (fn ((op :as string))
(case op (case op
"if" "ite" "if" "ite"
"str" "str.++" "str" "str.++"
@@ -176,7 +176,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define z3-extract-kwargs (define z3-extract-kwargs
(fn (expr) (fn ((expr :as list))
;; Returns a dict of keyword args from a define-* form ;; Returns a dict of keyword args from a define-* form
;; (define-primitive "name" :params (...) :returns "type" ...) → {:params ... :returns ...} ;; (define-primitive "name" :params (...) :returns "type" ...) → {:params ... :returns ...}
(let ((result {}) (let ((result {})
@@ -184,7 +184,7 @@
(z3-extract-kwargs-loop items result)))) (z3-extract-kwargs-loop items result))))
(define z3-extract-kwargs-loop (define z3-extract-kwargs-loop
(fn (items result) (fn ((items :as list) (result :as dict))
(if (or (empty? items) (< (len items) 2)) (if (or (empty? items) (< (len items) 2))
result result
(if (= (type-of (first items)) "keyword") (if (= (type-of (first items)) "keyword")
@@ -199,12 +199,12 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define z3-params-to-sorts (define z3-params-to-sorts
(fn (params) (fn ((params :as list))
;; Convert SX param list to list of (name sort) pairs, skipping &rest/&key ;; Convert SX param list to list of (name sort) pairs, skipping &rest/&key
(z3-params-loop params false (list)))) (z3-params-loop params false (list))))
(define z3-params-loop (define z3-params-loop
(fn (params skip-next acc) (fn ((params :as list) (skip-next :as boolean) (acc :as list))
(if (empty? params) (if (empty? params)
acc acc
(let ((p (first params)) (let ((p (first params))
@@ -227,7 +227,7 @@
(z3-params-loop rest-p false acc)))))) (z3-params-loop rest-p false acc))))))
(define z3-has-rest? (define z3-has-rest?
(fn (params) (fn ((params :as list))
(some (fn (p) (and (= (type-of p) "symbol") (= (symbol-name p) "&rest"))) (some (fn (p) (and (= (type-of p) "symbol") (= (symbol-name p) "&rest")))
params))) params)))
@@ -237,7 +237,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define z3-translate-primitive (define z3-translate-primitive
(fn (expr) (fn ((expr :as list))
(let ((name (nth expr 1)) (let ((name (nth expr 1))
(kwargs (z3-extract-kwargs expr)) (kwargs (z3-extract-kwargs expr))
(params (or (get kwargs "params") (list))) (params (or (get kwargs "params") (list)))
@@ -282,7 +282,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define z3-translate-io (define z3-translate-io
(fn (expr) (fn ((expr :as list))
(let ((name (nth expr 1)) (let ((name (nth expr 1))
(kwargs (z3-extract-kwargs expr)) (kwargs (z3-extract-kwargs expr))
(doc (or (get kwargs "doc") "")) (doc (or (get kwargs "doc") ""))
@@ -297,7 +297,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define z3-translate-special-form (define z3-translate-special-form
(fn (expr) (fn ((expr :as list))
(let ((name (nth expr 1)) (let ((name (nth expr 1))
(kwargs (z3-extract-kwargs expr)) (kwargs (z3-extract-kwargs expr))
(doc (or (get kwargs "doc") ""))) (doc (or (get kwargs "doc") "")))
@@ -342,7 +342,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define z3-translate-file (define z3-translate-file
(fn (exprs) (fn ((exprs :as list))
;; Filter to translatable forms and translate each ;; Filter to translatable forms and translate each
(let ((translatable (let ((translatable
(filter (filter

View File

@@ -31,7 +31,7 @@ import asyncio
from typing import Any from typing import Any
from .types import Component, Keyword, Lambda, NIL, Symbol from .types import Component, Keyword, Lambda, NIL, Symbol
from .evaluator import _eval as _raw_eval, _trampoline from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
def _eval(expr, env): def _eval(expr, env):
"""Evaluate and unwrap thunks — all resolver.py _eval calls are non-tail.""" """Evaluate and unwrap thunks — all resolver.py _eval calls are non-tail."""

View File

@@ -6,7 +6,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Auth section nav items (newsletters link + account_nav slot) ;; Auth section nav items (newsletters link + account_nav slot)
(defcomp ~auth-nav-items (&key account-url select-colours account-nav) (defcomp ~auth-nav-items (&key (account-url :as string?) (select-colours :as string?) account-nav)
(<> (<>
(~nav-link :href (str (or account-url "") "/newsletters/") (~nav-link :href (str (or account-url "") "/newsletters/")
:label "newsletters" :label "newsletters"
@@ -14,7 +14,7 @@
(when account-nav account-nav))) (when account-nav account-nav)))
;; Auth header row — wraps ~menu-row-sx for account section ;; Auth header row — wraps ~menu-row-sx for account section
(defcomp ~auth-header-row (&key account-url select-colours account-nav oob) (defcomp ~auth-header-row (&key (account-url :as string?) (select-colours :as string?) account-nav (oob :as boolean?))
(~menu-row-sx :id "auth-row" :level 1 :colour "sky" (~menu-row-sx :id "auth-row" :level 1 :colour "sky"
:link-href (str (or account-url "") "/") :link-href (str (or account-url "") "/")
:link-label "account" :icon "fa-solid fa-user" :link-label "account" :icon "fa-solid fa-user"
@@ -24,7 +24,7 @@
:child-id "auth-header-child" :oob oob)) :child-id "auth-header-child" :oob oob))
;; Auth header row without nav (for cart service) ;; Auth header row without nav (for cart service)
(defcomp ~auth-header-row-simple (&key account-url oob) (defcomp ~auth-header-row-simple (&key (account-url :as string?) (oob :as boolean?))
(~menu-row-sx :id "auth-row" :level 1 :colour "sky" (~menu-row-sx :id "auth-row" :level 1 :colour "sky"
:link-href (str (or account-url "") "/") :link-href (str (or account-url "") "/")
:link-label "account" :icon "fa-solid fa-user" :link-label "account" :icon "fa-solid fa-user"
@@ -52,7 +52,7 @@
:account-nav (account-nav-ctx)))) :account-nav (account-nav-ctx))))
;; Orders header row ;; Orders header row
(defcomp ~orders-header-row (&key list-url) (defcomp ~orders-header-row (&key (list-url :as string))
(~menu-row-sx :id "orders-row" :level 2 :colour "sky" (~menu-row-sx :id "orders-row" :level 2 :colour "sky"
:link-href list-url :link-label "Orders" :icon "fa fa-gbp" :link-href list-url :link-label "Orders" :icon "fa fa-gbp"
:child-id "orders-header-child")) :child-id "orders-header-child"))
@@ -61,12 +61,12 @@
;; Auth forms — login flow, check email ;; Auth forms — login flow, check email
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~auth-error-banner (&key error) (defcomp ~auth-error-banner (&key (error :as string?))
(when error (when error
(div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4" (div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4"
error))) error)))
(defcomp ~auth-login-form (&key error action csrf-token email) (defcomp ~auth-login-form (&key error (action :as string) (csrf-token :as string) (email :as string?))
(div :class "py-8 max-w-md mx-auto" (div :class "py-8 max-w-md mx-auto"
(h1 :class "text-2xl font-bold mb-6" "Sign in") (h1 :class "text-2xl font-bold mb-6" "Sign in")
error error
@@ -80,12 +80,12 @@
:class "w-full bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition" :class "w-full bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition"
"Send magic link")))) "Send magic link"))))
(defcomp ~auth-check-email-error (&key error) (defcomp ~auth-check-email-error (&key (error :as string?))
(when error (when error
(div :class "bg-yellow-50 border border-yellow-200 text-yellow-700 p-3 rounded mt-4" (div :class "bg-yellow-50 border border-yellow-200 text-yellow-700 p-3 rounded mt-4"
error))) error)))
(defcomp ~auth-check-email (&key email error) (defcomp ~auth-check-email (&key (email :as string) error)
(div :class "py-8 max-w-md mx-auto text-center" (div :class "py-8 max-w-md mx-auto text-center"
(h1 :class "text-2xl font-bold mb-4" "Check your email") (h1 :class "text-2xl font-bold mb-4" "Check your email")
(p :class "text-stone-600 mb-2" "We sent a sign-in link to " (strong email) ".") (p :class "text-stone-600 mb-2" "We sent a sign-in link to " (strong email) ".")

View File

@@ -1,6 +1,6 @@
(defcomp ~post-card (&key title slug href feature-image excerpt (defcomp ~post-card (&key (title :as string) (slug :as string) (href :as string) (feature-image :as string?)
status published-at updated-at publish-requested (excerpt :as string?) (status :as string?) (published-at :as string?) (updated-at :as string?)
hx-select like widgets at-bar) (publish-requested :as boolean?) (hx-select :as string?) like widgets at-bar)
(article :class "border-b pb-6 last:border-b-0 relative" (article :class "border-b pb-6 last:border-b-0 relative"
(when like like) (when like like)
(a :href href (a :href href
@@ -31,7 +31,8 @@
(when widgets widgets) (when widgets widgets)
(when at-bar at-bar))) (when at-bar at-bar)))
(defcomp ~order-summary-card (&key order-id created-at description status currency total-amount) (defcomp ~order-summary-card (&key (order-id :as string) (created-at :as string?) (description :as string?)
(status :as string?) (currency :as string?) (total-amount :as string?))
(div :class "rounded-2xl border border-stone-200 bg-white/80 p-4 sm:p-6 space-y-2 text-xs sm:text-sm text-stone-800" (div :class "rounded-2xl border border-stone-200 bg-white/80 p-4 sm:p-6 space-y-2 text-xs sm:text-sm text-stone-800"
(p (span :class "font-medium" "Order ID:") " " (span :class "font-mono" (str "#" order-id))) (p (span :class "font-medium" "Order ID:") " " (span :class "font-mono" (str "#" order-id)))
(p (span :class "font-medium" "Created:") " " (or created-at "\u2014")) (p (span :class "font-medium" "Created:") " " (or created-at "\u2014"))

View File

@@ -1,4 +1,5 @@
(defcomp ~search-mobile (&key current-local-href search search-count hx-select search-headers-mobile) (defcomp ~search-mobile (&key (current-local-href :as string) (search :as string?) (search-count :as number?)
(hx-select :as string?) (search-headers-mobile :as string?))
(div :id "search-mobile-wrapper" (div :id "search-mobile-wrapper"
:class "flex flex-row gap-2 items-center flex-1 min-w-0 pr-2" :class "flex flex-row gap-2 items-center flex-1 min-w-0 pr-2"
(input :id "search-mobile" (input :id "search-mobile"
@@ -20,7 +21,8 @@
:class (if (not search-count) "text-xl text-red-500" "") :class (if (not search-count) "text-xl text-red-500" "")
(when search (str search-count))))) (when search (str search-count)))))
(defcomp ~search-desktop (&key current-local-href search search-count hx-select search-headers-desktop) (defcomp ~search-desktop (&key (current-local-href :as string) (search :as string?) (search-count :as number?)
(hx-select :as string?) (search-headers-desktop :as string?))
(div :id "search-desktop-wrapper" (div :id "search-desktop-wrapper"
:class "flex flex-row gap-2 items-center" :class "flex flex-row gap-2 items-center"
(input :id "search-desktop" (input :id "search-desktop"
@@ -62,7 +64,8 @@
(div :id "filter-details-mobile" :style "display:contents" (div :id "filter-details-mobile" :style "display:contents"
(when filter-details filter-details)))) (when filter-details filter-details))))
(defcomp ~infinite-scroll (&key url page total-pages id-prefix colspan) (defcomp ~infinite-scroll (&key (url :as string) (page :as number) (total-pages :as number)
(id-prefix :as string) (colspan :as number))
(if (< page total-pages) (if (< page total-pages)
(tr :id (str id-prefix "-sentinel-" page) (tr :id (str id-prefix "-sentinel-" page)
:sx-get url :sx-get url
@@ -82,7 +85,7 @@
(tr (td :colspan colspan :class "px-3 py-4 text-center text-xs text-stone-400" (tr (td :colspan colspan :class "px-3 py-4 text-center text-xs text-stone-400"
"End of results")))) "End of results"))))
(defcomp ~status-pill (&key status size) (defcomp ~status-pill (&key (status :as string?) (size :as string?))
(let* ((s (or status "pending")) (let* ((s (or status "pending"))
(lower (lower s)) (lower (lower s))
(sz (or size "xs")) (sz (or size "xs"))

View File

@@ -1,4 +1,5 @@
(defcomp ~link-card (&key link title image icon subtitle detail data-app) (defcomp ~link-card (&key (link :as string) (title :as string) (image :as string?) (icon :as string?)
(subtitle :as string?) (detail :as string?) (data-app :as string?))
(a :href link (a :href link
:class "block rounded border border-stone-200 bg-white hover:bg-stone-50 transition-colors no-underline" :class "block rounded border border-stone-200 bg-white hover:bg-stone-50 transition-colors no-underline"
:data-fragment "link-card" :data-fragment "link-card"
@@ -16,7 +17,7 @@
(when detail (when detail
(div :class "text-xs text-stone-400 mt-1" detail)))))) (div :class "text-xs text-stone-400 mt-1" detail))))))
(defcomp ~cart-mini (&key cart-count blog-url cart-url oob) (defcomp ~cart-mini (&key (cart-count :as number) (blog-url :as string) (cart-url :as string) (oob :as string?))
(div :id "cart-mini" (div :id "cart-mini"
:sx-swap-oob oob :sx-swap-oob oob
(if (= cart-count 0) (if (= cart-count 0)
@@ -33,7 +34,7 @@
(span :class "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded-full bg-emerald-600 text-white text-sm w-5 h-5" (span :class "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded-full bg-emerald-600 text-white text-sm w-5 h-5"
cart-count))))) cart-count)))))
(defcomp ~auth-menu (&key user-email account-url) (defcomp ~auth-menu (&key (user-email :as string?) (account-url :as string))
(<> (<>
(span :id "auth-menu-desktop" :class "hidden md:inline-flex" (span :id "auth-menu-desktop" :class "hidden md:inline-flex"
(if user-email (if user-email
@@ -65,7 +66,7 @@
(i :class "fa-solid fa-key") (i :class "fa-solid fa-key")
(span "sign in or register")))))) (span "sign in or register"))))))
(defcomp ~account-nav-item (&key href label) (defcomp ~account-nav-item (&key (href :as string) (label :as string))
(div :class "relative nav-group" (div :class "relative nav-group"
(a :href href (a :href href
:class "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3" :class "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3"

View File

@@ -48,19 +48,19 @@
:class "w-12 h-12 rotate-180 transition-transform group-open/root:block hidden self-start" :class "w-12 h-12 rotate-180 transition-transform group-open/root:block hidden self-start"
(path :d "M6 9l6 6 6-6" :fill "currentColor")))) (path :d "M6 9l6 6 6-6" :fill "currentColor"))))
(defcomp ~post-label (&key feature-image title) (defcomp ~post-label (&key (feature-image :as string?) (title :as string))
(<> (when feature-image (<> (when feature-image
(img :src feature-image :class "h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0")) (img :src feature-image :class "h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0"))
(span title))) (span title)))
(defcomp ~page-cart-badge (&key href count) (defcomp ~page-cart-badge (&key (href :as string) (count :as string))
(a :href href :class "relative inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-full border border-emerald-300 bg-emerald-50 text-emerald-800 hover:bg-emerald-100 transition" (a :href href :class "relative inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-full border border-emerald-300 bg-emerald-50 text-emerald-800 hover:bg-emerald-100 transition"
(i :class "fa fa-shopping-cart" :aria-hidden "true") (i :class "fa fa-shopping-cart" :aria-hidden "true")
(span count))) (span count)))
(defcomp ~header-row-sx (&key cart-mini blog-url site-title app-label (defcomp ~header-row-sx (&key cart-mini (blog-url :as string?) (site-title :as string?)
nav-tree auth-menu nav-panel (app-label :as string?) nav-tree auth-menu nav-panel
settings-url is-admin oob) (settings-url :as string?) (is-admin :as boolean?) (oob :as boolean?))
(<> (<>
(div :id "root-row" (div :id "root-row"
:sx-swap-oob (if oob "outerHTML" nil) :sx-swap-oob (if oob "outerHTML" nil)
@@ -85,8 +85,10 @@
; @css bg-sky-400 bg-sky-300 bg-sky-200 bg-sky-100 bg-violet-400 bg-violet-300 bg-violet-200 bg-violet-100 ; @css bg-sky-400 bg-sky-300 bg-sky-200 bg-sky-100 bg-violet-400 bg-violet-300 bg-violet-200 bg-violet-100
; @css aria-selected:bg-violet-200 aria-selected:text-violet-900 aria-selected:bg-stone-500 aria-selected:text-white ; @css aria-selected:bg-violet-200 aria-selected:text-violet-900 aria-selected:bg-stone-500 aria-selected:text-white
(defcomp ~menu-row-sx (&key id level colour link-href link-label link-label-content icon (defcomp ~menu-row-sx (&key (id :as string) (level :as number?) (colour :as string?)
selected hx-select nav child-id child oob external) (link-href :as string) (link-label :as string?) link-label-content
(icon :as string?) (selected :as string?) (hx-select :as string?)
nav (child-id :as string?) child (oob :as boolean?) (external :as boolean?))
(let* ((c (or colour "sky")) (let* ((c (or colour "sky"))
(lv (or level 1)) (lv (or level 1))
(shade (str (- 500 (* lv 100))))) (shade (str (- 500 (* lv 100)))))
@@ -115,11 +117,11 @@
(div :id child-id :class "flex flex-col w-full items-center" (div :id child-id :class "flex flex-col w-full items-center"
(when child child)))))) (when child child))))))
(defcomp ~oob-header-sx (&key parent-id row) (defcomp ~oob-header-sx (&key (parent-id :as string) row)
(div :id parent-id :sx-swap-oob "outerHTML" :class "flex flex-col w-full items-center" (div :id parent-id :sx-swap-oob "outerHTML" :class "flex flex-col w-full items-center"
row)) row))
(defcomp ~header-child-sx (&key id inner) (defcomp ~header-child-sx (&key (id :as string?) inner)
(div :id (or id "root-header-child") :class "flex flex-col w-full items-center" inner)) (div :id (or id "root-header-child") :class "flex flex-col w-full items-center" inner))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
@@ -127,7 +129,8 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Labelled section: colour bar header + vertical nav items ;; Labelled section: colour bar header + vertical nav items
(defcomp ~mobile-menu-section (&key label href colour level items) (defcomp ~mobile-menu-section (&key (label :as string) (href :as string?) (colour :as string?)
(level :as number?) items)
(let* ((c (or colour "sky")) (let* ((c (or colour "sky"))
(lv (or level 1)) (lv (or level 1))
(shade (str (- 500 (* lv 100))))) (shade (str (- 500 (* lv 100)))))
@@ -153,8 +156,9 @@
;; nested component calls in _aser are serialized without expansion. ;; nested component calls in _aser are serialized without expansion.
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~root-header (&key cart-mini blog-url site-title app-label (defcomp ~root-header (&key cart-mini (blog-url :as string?) (site-title :as string?)
nav-tree auth-menu nav-panel settings-url is-admin oob) (app-label :as string?) nav-tree auth-menu nav-panel
(settings-url :as string?) (is-admin :as boolean?) (oob :as boolean?))
(~header-row-sx :cart-mini cart-mini :blog-url blog-url :site-title site-title (~header-row-sx :cart-mini cart-mini :blog-url blog-url :site-title site-title
:app-label app-label :nav-tree nav-tree :auth-menu auth-menu :app-label app-label :nav-tree nav-tree :auth-menu auth-menu
:nav-panel nav-panel :settings-url settings-url :is-admin is-admin :nav-panel nav-panel :settings-url settings-url :is-admin is-admin
@@ -226,18 +230,18 @@
(~root-mobile-auto)))) (~root-mobile-auto))))
;; Post-admin layout — root + post header with nested admin row ;; Post-admin layout — root + post header with nested admin row
(defcomp ~layout-post-admin-full (&key selected) (defcomp ~layout-post-admin-full (&key (selected :as string?))
(let ((__admin-hdr (~post-admin-header-auto nil selected))) (let ((__admin-hdr (~post-admin-header-auto nil selected)))
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~header-child-sx
:inner (~post-header-auto nil))))) :inner (~post-header-auto nil)))))
(defcomp ~layout-post-admin-oob (&key selected) (defcomp ~layout-post-admin-oob (&key (selected :as string?))
(<> (~post-header-auto true) (<> (~post-header-auto true)
(~oob-header-sx :parent-id "post-header-child" (~oob-header-sx :parent-id "post-header-child"
:row (~post-admin-header-auto nil selected)))) :row (~post-admin-header-auto nil selected))))
(defcomp ~layout-post-admin-mobile (&key selected) (defcomp ~layout-post-admin-mobile (&key (selected :as string?))
(let ((__phctx (post-header-ctx))) (let ((__phctx (post-header-ctx)))
(<> (<>
(when (get __phctx "slug") (when (get __phctx "slug")
@@ -254,7 +258,7 @@
:items (~post-nav-auto))) :items (~post-nav-auto)))
(~root-mobile-auto)))) (~root-mobile-auto))))
(defcomp ~error-content (&key errnum message image) (defcomp ~error-content (&key (errnum :as string) (message :as string) (image :as string?))
(div :class "text-center p-8 max-w-lg mx-auto" (div :class "text-center p-8 max-w-lg mx-auto"
(div :class "font-bold text-2xl md:text-4xl text-red-500 mb-4" errnum) (div :class "font-bold text-2xl md:text-4xl text-red-500 mb-4" errnum)
(div :class "text-stone-600 mb-4" message) (div :class "text-stone-600 mb-4" message)
@@ -262,7 +266,7 @@
(div :class "flex justify-center" (div :class "flex justify-center"
(img :src image :width "300" :height "300"))))) (img :src image :width "300" :height "300")))))
(defcomp ~clear-oob-div (&key id) (defcomp ~clear-oob-div (&key (id :as string))
(div :id id :sx-swap-oob "outerHTML")) (div :id id :sx-swap-oob "outerHTML"))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
@@ -354,21 +358,22 @@
content)) content))
; @css justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3 !bg-stone-500 !text-white ; @css justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3 !bg-stone-500 !text-white
(defcomp ~admin-cog-button (&key href is-admin-page) (defcomp ~admin-cog-button (&key (href :as string) (is-admin-page :as boolean?))
(div :class "relative nav-group" (div :class "relative nav-group"
(a :href href (a :href href
:class (str "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3 " :class (str "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3 "
(if is-admin-page "!bg-stone-500 !text-white" "")) (if is-admin-page "!bg-stone-500 !text-white" ""))
(i :class "fa fa-cog" :aria-hidden "true")))) (i :class "fa fa-cog" :aria-hidden "true"))))
(defcomp ~post-admin-label (&key selected) (defcomp ~post-admin-label (&key (selected :as string?))
(<> (<>
(i :class "fa fa-shield-halved" :aria-hidden "true") (i :class "fa fa-shield-halved" :aria-hidden "true")
" admin" " admin"
(when selected (when selected
(span :class "text-white" selected)))) (span :class "text-white" selected))))
(defcomp ~nav-link (&key href hx-select label icon aclass select-colours is-selected) (defcomp ~nav-link (&key (href :as string) (hx-select :as string?) (label :as string?) (icon :as string?)
(aclass :as string?) (select-colours :as string?) (is-selected :as string?))
(div :class "relative nav-group" (div :class "relative nav-group"
(a :href href (a :href href
:sx-get href :sx-get href

View File

@@ -2,32 +2,33 @@
;; The single place where raw! lives — for CMS content (Ghost post body, ;; The single place where raw! lives — for CMS content (Ghost post body,
;; product descriptions, etc.) that arrives as pre-rendered HTML. ;; product descriptions, etc.) that arrives as pre-rendered HTML.
(defcomp ~rich-text (&key html) (defcomp ~rich-text (&key (html :as string))
(raw! html)) (raw! html))
(defcomp ~error-inline (&key message) (defcomp ~error-inline (&key (message :as string))
(div :class "text-red-600 text-sm" message)) (div :class "text-red-600 text-sm" message))
(defcomp ~notification-badge (&key count) (defcomp ~notification-badge (&key (count :as number))
(span :class "bg-red-500 text-white text-xs rounded-full px-1.5 py-0.5" count)) (span :class "bg-red-500 text-white text-xs rounded-full px-1.5 py-0.5" count))
(defcomp ~cache-cleared (&key time-str) (defcomp ~cache-cleared (&key (time-str :as string))
(span :class "text-green-600 font-bold" "Cache cleared at " time-str)) (span :class "text-green-600 font-bold" "Cache cleared at " time-str))
(defcomp ~error-list (&key items) (defcomp ~error-list (&key (items :as list?))
(ul :class "list-disc pl-5 space-y-1 text-sm text-red-600" (ul :class "list-disc pl-5 space-y-1 text-sm text-red-600"
(when items items))) (when items items)))
(defcomp ~error-list-item (&key message) (defcomp ~error-list-item (&key (message :as string))
(li message)) (li message))
(defcomp ~fragment-error (&key service) (defcomp ~fragment-error (&key (service :as string))
(p :class "text-sm text-red-600" "Service " (b service) " is unavailable.")) (p :class "text-sm text-red-600" "Service " (b service) " is unavailable."))
(defcomp ~htmx-sentinel (&key id hx-get hx-trigger hx-swap class extra-attrs) (defcomp ~htmx-sentinel (&key (id :as string) (hx-get :as string) (hx-trigger :as string)
(hx-swap :as string) (class :as string?) extra-attrs)
(div :id id :sx-get hx-get :sx-trigger hx-trigger :sx-swap hx-swap :class class)) (div :id id :sx-get hx-get :sx-trigger hx-trigger :sx-swap hx-swap :class class))
(defcomp ~nav-group-link (&key href hx-select nav-class label) (defcomp ~nav-group-link (&key (href :as string) (hx-select :as string?) (nav-class :as string?) (label :as string))
(div :class "relative nav-group" (div :class "relative nav-group"
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-select hx-select :sx-swap "outerHTML"
@@ -38,7 +39,7 @@
;; Shared sentinel components — infinite scroll triggers ;; Shared sentinel components — infinite scroll triggers
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~sentinel-mobile (&key id next-url hyperscript) (defcomp ~sentinel-mobile (&key (id :as string) (next-url :as string) (hyperscript :as string?))
(div :id id :class "block md:hidden h-[60vh] opacity-0 pointer-events-none js-mobile-sentinel" (div :id id :class "block md:hidden h-[60vh] opacity-0 pointer-events-none js-mobile-sentinel"
:sx-get next-url :sx-trigger "intersect once delay:250ms, sentinelmobile:retry" :sx-get next-url :sx-trigger "intersect once delay:250ms, sentinelmobile:retry"
:sx-swap "outerHTML" :_ hyperscript :sx-swap "outerHTML" :_ hyperscript
@@ -49,7 +50,7 @@
(i :class "fa fa-exclamation-triangle text-2xl") (i :class "fa fa-exclamation-triangle text-2xl")
(p :class "mt-2" "Loading failed \u2014 retrying\u2026")))) (p :class "mt-2" "Loading failed \u2014 retrying\u2026"))))
(defcomp ~sentinel-desktop (&key id next-url hyperscript) (defcomp ~sentinel-desktop (&key (id :as string) (next-url :as string) (hyperscript :as string?))
(div :id id :class "hidden md:block h-4 opacity-0 pointer-events-none" (div :id id :class "hidden md:block h-4 opacity-0 pointer-events-none"
:sx-get next-url :sx-trigger "intersect once delay:250ms, sentinel:retry" :sx-get next-url :sx-trigger "intersect once delay:250ms, sentinel:retry"
:sx-swap "outerHTML" :_ hyperscript :sx-swap "outerHTML" :_ hyperscript
@@ -58,20 +59,20 @@
(div :class "animate-spin h-6 w-6 border-2 border-stone-300 border-t-stone-600 rounded-full")) (div :class "animate-spin h-6 w-6 border-2 border-stone-300 border-t-stone-600 rounded-full"))
(div :class "js-neterr hidden text-center py-2 text-stone-400 text-sm" "Retry\u2026"))) (div :class "js-neterr hidden text-center py-2 text-stone-400 text-sm" "Retry\u2026")))
(defcomp ~sentinel-simple (&key id next-url) (defcomp ~sentinel-simple (&key (id :as string) (next-url :as string))
(div :id id :class "h-4 opacity-0 pointer-events-none" (div :id id :class "h-4 opacity-0 pointer-events-none"
:sx-get next-url :sx-trigger "intersect once delay:250ms" :sx-swap "outerHTML" :sx-get next-url :sx-trigger "intersect once delay:250ms" :sx-swap "outerHTML"
:role "status" :aria-hidden "true" :role "status" :aria-hidden "true"
(div :class "text-center text-xs text-stone-400" "loading..."))) (div :class "text-center text-xs text-stone-400" "loading...")))
(defcomp ~end-of-results (&key cls) (defcomp ~end-of-results (&key (cls :as string?))
(div :class (or cls "col-span-full mt-4 text-center text-xs text-stone-400") "End of results")) (div :class (or cls "col-span-full mt-4 text-center text-xs text-stone-400") "End of results"))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Shared empty state — icon + message + optional action ;; Shared empty state — icon + message + optional action
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~empty-state (&key icon message cls &rest children) (defcomp ~empty-state (&key (icon :as string?) (message :as string) (cls :as string?) &rest children)
(div :class (or cls "p-8 text-center text-stone-400") (div :class (or cls "p-8 text-center text-stone-400")
(when icon (div (i :class (str icon " text-4xl mb-2") :aria-hidden "true"))) (when icon (div (i :class (str icon " text-4xl mb-2") :aria-hidden "true")))
(p message) (p message)
@@ -81,7 +82,7 @@
;; Shared badge — inline pill with configurable colours ;; Shared badge — inline pill with configurable colours
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~badge (&key label cls) (defcomp ~badge (&key (label :as string) (cls :as string?))
(span :class (str "inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium " (or cls "bg-stone-100 text-stone-700")) (span :class (str "inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium " (or cls "bg-stone-100 text-stone-700"))
label)) label))
@@ -89,8 +90,9 @@
;; Shared delete button with confirm dialog ;; Shared delete button with confirm dialog
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~delete-btn (&key url trigger-target title text confirm-text cancel-text (defcomp ~delete-btn (&key (url :as string) (trigger-target :as string) (title :as string?)
sx-headers cls) (text :as string?) (confirm-text :as string?) (cancel-text :as string?)
(sx-headers :as string?) (cls :as string?))
(button :type "button" (button :type "button"
:data-confirm "" :data-confirm-title (or title "Delete?") :data-confirm "" :data-confirm-title (or title "Delete?")
:data-confirm-text (or text "Are you sure?") :data-confirm-text (or text "Are you sure?")
@@ -108,7 +110,7 @@
;; Shared price display — special + regular with strikethrough ;; Shared price display — special + regular with strikethrough
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~price (&key special-price regular-price) (defcomp ~price (&key (special-price :as string?) (regular-price :as string?))
(div :class "mt-1 flex items-baseline gap-2 justify-center" (div :class "mt-1 flex items-baseline gap-2 justify-center"
(when special-price (div :class "text-lg font-semibold text-emerald-700" special-price)) (when special-price (div :class "text-lg font-semibold text-emerald-700" special-price))
(when (and special-price regular-price) (div :class "text-sm line-through text-stone-500" regular-price)) (when (and special-price regular-price) (div :class "text-sm line-through text-stone-500" regular-price))
@@ -118,7 +120,8 @@
;; Shared image-or-placeholder ;; Shared image-or-placeholder
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~img-or-placeholder (&key src alt size-cls placeholder-icon placeholder-text) (defcomp ~img-or-placeholder (&key (src :as string?) (alt :as string?) (size-cls :as string?)
(placeholder-icon :as string?) (placeholder-text :as string?))
(if src (if src
(img :src src :alt (or alt "") :class (or size-cls "w-12 h-12 rounded-full object-cover flex-shrink-0")) (img :src src :alt (or alt "") :class (or size-cls "w-12 h-12 rounded-full object-cover flex-shrink-0"))
(div :class (str (or size-cls "w-12 h-12 rounded-full") " bg-stone-200 flex items-center justify-center flex-shrink-0") (div :class (str (or size-cls "w-12 h-12 rounded-full") " bg-stone-200 flex items-center justify-center flex-shrink-0")
@@ -141,8 +144,9 @@
(path :stroke-linecap "round" :stroke-linejoin "round" (path :stroke-linecap "round" :stroke-linejoin "round"
:d "M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"))) :d "M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z")))
(defcomp ~view-toggle (&key list-href tile-href hx-select list-cls tile-cls (defcomp ~view-toggle (&key (list-href :as string) (tile-href :as string) (hx-select :as string?)
storage-key list-svg tile-svg) (list-cls :as string?) (tile-cls :as string?) (storage-key :as string?)
list-svg tile-svg)
(div :class "hidden md:flex justify-end px-3 pt-3 gap-1" (div :class "hidden md:flex justify-end px-3 pt-3 gap-1"
(a :href list-href :sx-get list-href :sx-target "#main-panel" :sx-select hx-select (a :href list-href :sx-get list-href :sx-target "#main-panel" :sx-select hx-select
:sx-swap "outerHTML" :sx-push-url "true" :class (str "p-1.5 rounded " list-cls) :title "List view" :sx-swap "outerHTML" :sx-push-url "true" :class (str "p-1.5 rounded " list-cls) :title "List view"
@@ -157,7 +161,9 @@
;; Shared CRUD admin panel — for calendars, markets, etc. ;; Shared CRUD admin panel — for calendars, markets, etc.
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~crud-create-form (&key create-url csrf errors-id list-id placeholder label btn-label) (defcomp ~crud-create-form (&key (create-url :as string) (csrf :as string) (errors-id :as string?)
(list-id :as string?) (placeholder :as string?) (label :as string?)
(btn-label :as string?))
(<> (<>
(div :id (or errors-id "crud-create-errors") :class "mt-2 text-sm text-red-600") (div :id (or errors-id "crud-create-errors") :class "mt-2 text-sm text-red-600")
(form :class "mt-4 flex gap-2 items-end" :sx-post create-url (form :class "mt-4 flex gap-2 items-end" :sx-post create-url
@@ -171,13 +177,14 @@
:placeholder (or placeholder "Name"))) :placeholder (or placeholder "Name")))
(button :type "submit" :class "border rounded px-3 py-2" (or btn-label "Add"))))) (button :type "submit" :class "border rounded px-3 py-2" (or btn-label "Add")))))
(defcomp ~crud-panel (&key form list list-id) (defcomp ~crud-panel (&key form list (list-id :as string?))
(section :class "p-4" (section :class "p-4"
form form
(div :id (or list-id "crud-list") :class "mt-6" list))) (div :id (or list-id "crud-list") :class "mt-6" list)))
(defcomp ~crud-item (&key href name slug del-url csrf-hdr list-id (defcomp ~crud-item (&key (href :as string) (name :as string) (slug :as string) (del-url :as string)
confirm-title confirm-text) (csrf-hdr :as string) (list-id :as string?) (confirm-title :as string?)
(confirm-text :as string?))
(div :class "mt-6 border rounded-lg p-4" (div :class "mt-6 border rounded-lg p-4"
(div :class "flex items-center justify-between gap-3" (div :class "flex items-center justify-between gap-3"
(a :class "flex items-baseline gap-3" :href href (a :class "flex items-baseline gap-3" :href href
@@ -199,9 +206,10 @@
;; checkout prefix) used by blog, events, and cart admin panels. ;; checkout prefix) used by blog, events, and cart admin panels.
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~sumup-settings-form (&key update-url csrf merchant-code placeholder (defcomp ~sumup-settings-form (&key (update-url :as string) (csrf :as string?) (merchant-code :as string?)
input-cls sumup-configured checkout-prefix (placeholder :as string?) (input-cls :as string?)
panel-id sx-select) (sumup-configured :as boolean?) (checkout-prefix :as string?)
(panel-id :as string?) (sx-select :as string?))
(div :id (or panel-id "payments-panel") :class "space-y-4 p-4 bg-white rounded-lg border border-stone-200" (div :id (or panel-id "payments-panel") :class "space-y-4 p-4 bg-white rounded-lg border border-stone-200"
(h3 :class "text-lg font-semibold text-stone-800" (h3 :class "text-lg font-semibold text-stone-800"
(i :class "fa fa-credit-card text-purple-600 mr-1") " SumUp Payment") (i :class "fa fa-credit-card text-purple-600 mr-1") " SumUp Payment")
@@ -233,7 +241,7 @@
;; Shared avatar — image or initial-letter placeholder ;; Shared avatar — image or initial-letter placeholder
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~avatar (&key src cls initial) (defcomp ~avatar (&key (src :as string?) (cls :as string?) (initial :as string?))
(if src (if src
(img :src src :alt "" :class cls) (img :src src :alt "" :class cls)
(div :class cls initial))) (div :class cls initial)))
@@ -242,7 +250,9 @@
;; Shared scroll-nav wrapper — horizontal scrollable nav with arrows ;; Shared scroll-nav wrapper — horizontal scrollable nav with arrows
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~scroll-nav-wrapper (&key wrapper-id container-id arrow-cls left-hs scroll-hs right-hs items oob) (defcomp ~scroll-nav-wrapper (&key (wrapper-id :as string) (container-id :as string) (arrow-cls :as string?)
(left-hs :as string?) (scroll-hs :as string?) (right-hs :as string?)
items (oob :as boolean?))
(div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl" (div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"
:id wrapper-id :sx-swap-oob (if oob "outerHTML" nil) :id wrapper-id :sx-swap-oob (if oob "outerHTML" nil)
(button :class (str (or arrow-cls "nav-arrow") " hidden flex-shrink-0 p-2 hover:bg-stone-200 rounded") (button :class (str (or arrow-cls "nav-arrow") " hidden flex-shrink-0 p-2 hover:bg-stone-200 rounded")

View File

@@ -1,11 +1,12 @@
(defcomp ~calendar-entry-nav (&key href name date-str nav-class) (defcomp ~calendar-entry-nav (&key (href :as string) (name :as string) (date-str :as string) (nav-class :as string?))
(a :href href :class nav-class (a :href href :class nav-class
(div :class "w-8 h-8 rounded bg-stone-200 flex-shrink-0") (div :class "w-8 h-8 rounded bg-stone-200 flex-shrink-0")
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
(div :class "font-medium truncate" name) (div :class "font-medium truncate" name)
(div :class "text-xs text-stone-600 truncate" date-str)))) (div :class "text-xs text-stone-600 truncate" date-str))))
(defcomp ~calendar-link-nav (&key href name nav-class is-selected select-colours) (defcomp ~calendar-link-nav (&key (href :as string) (name :as string) (nav-class :as string?)
(is-selected :as string?) (select-colours :as string?))
(a :href href (a :href href
:sx-get href :sx-get href
:sx-target "#main-panel" :sx-target "#main-panel"
@@ -17,12 +18,14 @@
(i :class "fa fa-calendar" :aria-hidden "true") (i :class "fa fa-calendar" :aria-hidden "true")
(span name))) (span name)))
(defcomp ~market-link-nav (&key href name nav-class select-colours) (defcomp ~market-link-nav (&key (href :as string) (name :as string) (nav-class :as string?)
(select-colours :as string?))
(a :href href :class (str (or nav-class "") " " (or select-colours "")) (a :href href :class (str (or nav-class "") " " (or select-colours ""))
(i :class "fa fa-shopping-bag" :aria-hidden "true") (i :class "fa fa-shopping-bag" :aria-hidden "true")
(span name))) (span name)))
(defcomp ~relation-nav (&key href name icon nav-class relation-type) (defcomp ~relation-nav (&key (href :as string) (name :as string) (icon :as string?)
(nav-class :as string?) (relation-type :as string?))
(a :href href :class (or nav-class "flex items-center gap-3 rounded-lg py-2 px-3 text-sm text-stone-700 hover:bg-stone-100 transition-colors") (a :href href :class (or nav-class "flex items-center gap-3 rounded-lg py-2 px-3 text-sm text-stone-700 hover:bg-stone-100 transition-colors")
(when icon (when icon
(div :class "w-8 h-8 rounded bg-stone-200 flex items-center justify-center flex-shrink-0" (div :class "w-8 h-8 rounded bg-stone-200 flex items-center justify-center flex-shrink-0"

View File

@@ -6,7 +6,8 @@
;; Order table rows ;; Order table rows
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~order-row-desktop (&key oid created desc total pill status url) (defcomp ~order-row-desktop (&key (oid :as string) (created :as string) (desc :as string) (total :as string)
(pill :as string) (status :as string) (url :as string))
(tr :class "hidden sm:table-row border-t border-stone-100 hover:bg-stone-50/60" (tr :class "hidden sm:table-row border-t border-stone-100 hover:bg-stone-50/60"
(td :class "px-3 py-2 align-top" (span :class "font-mono text-[11px] sm:text-xs" oid)) (td :class "px-3 py-2 align-top" (span :class "font-mono text-[11px] sm:text-xs" oid))
(td :class "px-3 py-2 align-top text-stone-700 text-xs sm:text-sm" created) (td :class "px-3 py-2 align-top text-stone-700 text-xs sm:text-sm" created)
@@ -16,7 +17,8 @@
(td :class "px-3 py-0.5 align-top text-right" (td :class "px-3 py-0.5 align-top text-right"
(a :href url :class "inline-flex items-center px-3 py-1.5 text-xs sm:text-sm rounded-full border border-stone-300 bg-white hover:bg-stone-50 transition" "View")))) (a :href url :class "inline-flex items-center px-3 py-1.5 text-xs sm:text-sm rounded-full border border-stone-300 bg-white hover:bg-stone-50 transition" "View"))))
(defcomp ~order-row-mobile (&key oid created total pill status url) (defcomp ~order-row-mobile (&key (oid :as string) (created :as string) (total :as string)
(pill :as string) (status :as string) (url :as string))
(tr :class "sm:hidden border-t border-stone-100" (tr :class "sm:hidden border-t border-stone-100"
(td :colspan "5" :class "px-3 py-3" (td :colspan "5" :class "px-3 py-3"
(div :class "flex flex-col gap-2 text-xs" (div :class "flex flex-col gap-2 text-xs"
@@ -61,13 +63,14 @@
;; Order detail panels ;; Order detail panels
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~order-item-image (&key src alt) (defcomp ~order-item-image (&key (src :as string) (alt :as string))
(img :src src :alt alt :class "w-full h-full object-contain object-center" :loading "lazy" :decoding "async")) (img :src src :alt alt :class "w-full h-full object-contain object-center" :loading "lazy" :decoding "async"))
(defcomp ~order-item-no-image () (defcomp ~order-item-no-image ()
(div :class "w-full h-full flex items-center justify-center text-[9px] text-stone-400" "No image")) (div :class "w-full h-full flex items-center justify-center text-[9px] text-stone-400" "No image"))
(defcomp ~order-item-row (&key href img title pid qty price) (defcomp ~order-item-row (&key (href :as string) img (title :as string) (pid :as string)
(qty :as string) (price :as string))
(li (a :class "w-full py-2 flex gap-3" :href href (li (a :class "w-full py-2 flex gap-3" :href href
(div :class "w-12 h-12 sm:w-14 sm:h-14 rounded-md bg-stone-100 flex-shrink-0 overflow-hidden" img) (div :class "w-12 h-12 sm:w-14 sm:h-14 rounded-md bg-stone-100 flex-shrink-0 overflow-hidden" img)
(div :class "flex-1 flex justify-between gap-3" (div :class "flex-1 flex justify-between gap-3"
@@ -83,7 +86,8 @@
(h2 :class "text-sm sm:text-base font-semibold mb-3" "Items") (h2 :class "text-sm sm:text-base font-semibold mb-3" "Items")
(ul :class "divide-y divide-stone-100 text-xs sm:text-sm" items))) (ul :class "divide-y divide-stone-100 text-xs sm:text-sm" items)))
(defcomp ~order-calendar-entry (&key name pill status date-str cost) (defcomp ~order-calendar-entry (&key (name :as string) (pill :as string) (status :as string)
(date-str :as string) (cost :as string))
(li :class "px-4 py-3 flex items-start justify-between text-sm" (li :class "px-4 py-3 flex items-start justify-between text-sm"
(div (div :class "font-medium flex items-center gap-2" (div (div :class "font-medium flex items-center gap-2"
name (span :class pill status)) name (span :class pill status))
@@ -98,11 +102,12 @@
(defcomp ~order-detail-panel (&key summary items calendar) (defcomp ~order-detail-panel (&key summary items calendar)
(div :class "max-w-full px-3 py-3 space-y-4" summary items calendar)) (div :class "max-w-full px-3 py-3 space-y-4" summary items calendar))
(defcomp ~order-pay-btn (&key url) (defcomp ~order-pay-btn (&key (url :as string))
(a :href url :class "inline-flex items-center px-3 py-2 text-xs sm:text-sm rounded-full border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 transition" (a :href url :class "inline-flex items-center px-3 py-2 text-xs sm:text-sm rounded-full border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 transition"
(i :class "fa fa-credit-card mr-2" :aria-hidden "true") "Open payment page")) (i :class "fa fa-credit-card mr-2" :aria-hidden "true") "Open payment page"))
(defcomp ~order-detail-filter (&key info list-url recheck-url csrf pay) (defcomp ~order-detail-filter (&key (info :as string) (list-url :as string) (recheck-url :as string)
(csrf :as string) pay)
(header :class "mb-6 sm:mb-8 flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4" (header :class "mb-6 sm:mb-8 flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4"
(div :class "space-y-1" (div :class "space-y-1"
(p :class "text-xs sm:text-sm text-stone-600" info)) (p :class "text-xs sm:text-sm text-stone-600" info))
@@ -124,7 +129,8 @@
;; Data-driven order rows (replaces Python loop) ;; Data-driven order rows (replaces Python loop)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~order-rows-from-data (&key orders page total-pages next-url) (defcomp ~order-rows-from-data (&key (orders :as list?) (page :as number) (total-pages :as number)
(next-url :as string?))
(<> (<>
(map (lambda (o) (map (lambda (o)
(<> (<>
@@ -144,7 +150,7 @@
;; Data-driven order items (replaces Python loop) ;; Data-driven order items (replaces Python loop)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~order-items-from-data (&key items) (defcomp ~order-items-from-data (&key (items :as list?))
(~order-items-panel (~order-items-panel
:items (<> (map (lambda (item) :items (<> (map (lambda (item)
(let* ((img (if (get item "product_image") (let* ((img (if (get item "product_image")
@@ -162,7 +168,7 @@
;; Data-driven calendar entries (replaces Python loop) ;; Data-driven calendar entries (replaces Python loop)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~order-calendar-from-data (&key entries) (defcomp ~order-calendar-from-data (&key (entries :as list?))
(~order-calendar-section (~order-calendar-section
:items (<> (map (lambda (e) :items (<> (map (lambda (e)
(~order-calendar-entry (~order-calendar-entry
@@ -180,7 +186,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Status pill class mapping ;; Status pill class mapping
(defcomp ~order-status-pill-cls (&key status) (defcomp ~order-status-pill-cls (&key (status :as string?))
(let* ((sl (lower (or status "")))) (let* ((sl (lower (or status ""))))
(cond (cond
((= sl "paid") "border-emerald-300 bg-emerald-50 text-emerald-700") ((= sl "paid") "border-emerald-300 bg-emerald-50 text-emerald-700")
@@ -188,7 +194,7 @@
(true "border-stone-300 bg-stone-50 text-stone-700")))) (true "border-stone-300 bg-stone-50 text-stone-700"))))
;; Single order row pair (desktop + mobile) — takes serialized order data dict ;; Single order row pair (desktop + mobile) — takes serialized order data dict
(defcomp ~order-row-pair (&key order detail-url-prefix) (defcomp ~order-row-pair (&key (order :as dict) (detail-url-prefix :as string))
(let* ((status (or (get order "status") "pending")) (let* ((status (or (get order "status") "pending"))
(pill-base (~order-status-pill-cls :status status)) (pill-base (~order-status-pill-cls :status status))
(oid (str "#" (get order "id"))) (oid (str "#" (get order "id")))
@@ -207,7 +213,8 @@
:status status :url url)))) :status status :url url))))
;; Assembled orders list content ;; Assembled orders list content
(defcomp ~orders-list-content (&key orders page total-pages rows-url detail-url-prefix) (defcomp ~orders-list-content (&key (orders :as list) (page :as number) (total-pages :as number)
(rows-url :as string) (detail-url-prefix :as string))
(if (empty? orders) (if (empty? orders)
(~order-empty-state) (~order-empty-state)
(~order-table (~order-table
@@ -223,7 +230,7 @@
(~order-end-row)))))) (~order-end-row))))))
;; Assembled order detail content — replaces Python _order_main_sx ;; Assembled order detail content — replaces Python _order_main_sx
(defcomp ~order-detail-content (&key order calendar-entries) (defcomp ~order-detail-content (&key (order :as dict) (calendar-entries :as list?))
(let* ((items (get order "items"))) (let* ((items (get order "items")))
(~order-detail-panel (~order-detail-panel
:summary (~order-summary-card :summary (~order-summary-card
@@ -265,7 +272,8 @@
calendar-entries)))))) calendar-entries))))))
;; Assembled order detail filter — replaces Python _order_filter_sx ;; Assembled order detail filter — replaces Python _order_filter_sx
(defcomp ~order-detail-filter-content (&key order list-url recheck-url pay-url csrf) (defcomp ~order-detail-filter-content (&key (order :as dict) (list-url :as string) (recheck-url :as string)
(pay-url :as string) (csrf :as string))
(let* ((status (or (get order "status") "pending")) (let* ((status (or (get order "status") "pending"))
(created (or (get order "created_at_formatted") "\u2014"))) (created (or (get order "created_at_formatted") "\u2014")))
(~order-detail-filter (~order-detail-filter
@@ -280,7 +288,7 @@
;; Checkout return components ;; Checkout return components
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~checkout-return-header (&key status) (defcomp ~checkout-return-header (&key (status :as string))
(header :class "mb-6 sm:mb-8" (header :class "mb-6 sm:mb-8"
(h1 :class "text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight" "Payment complete") (h1 :class "text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight" "Payment complete")
(p :class "text-xs sm:text-sm text-stone-600" (p :class "text-xs sm:text-sm text-stone-600"
@@ -290,7 +298,9 @@
(div :class "max-w-full px-3 py-3 space-y-4" (div :class "max-w-full px-3 py-3 space-y-4"
(p :class "text-sm text-stone-600" "Order not found."))) (p :class "text-sm text-stone-600" "Order not found.")))
(defcomp ~checkout-return-ticket (&key name pill state type-name date-str code price) (defcomp ~checkout-return-ticket (&key (name :as string) (pill :as string) (state :as string)
(type-name :as string?) (date-str :as string) (code :as string?)
(price :as string))
(li :class "px-4 py-3 flex items-start justify-between text-sm" (li :class "px-4 py-3 flex items-start justify-between text-sm"
(div (div
(div :class "font-medium flex items-center gap-2" (div :class "font-medium flex items-center gap-2"
@@ -305,7 +315,7 @@
(h2 :class "text-base sm:text-lg font-semibold" "Tickets") (h2 :class "text-base sm:text-lg font-semibold" "Tickets")
(ul :class "divide-y divide-stone-200 rounded-2xl border border-stone-200 bg-white/80" items))) (ul :class "divide-y divide-stone-200 rounded-2xl border border-stone-200 bg-white/80" items)))
(defcomp ~checkout-return-failed (&key order-id) (defcomp ~checkout-return-failed (&key (order-id :as string?))
(div :class "rounded-lg border border-rose-200 bg-rose-50 p-4 text-sm text-rose-900" (div :class "rounded-lg border border-rose-200 bg-rose-50 p-4 text-sm text-rose-900"
(p :class "font-medium" "Payment failed") (p :class "font-medium" "Payment failed")
(p "Please try again or contact support." (p "Please try again or contact support."
@@ -329,10 +339,10 @@
(h1 :class "text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight" "Checkout error") (h1 :class "text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight" "Checkout error")
(p :class "text-xs sm:text-sm text-stone-600" "We tried to start your payment with SumUp but hit a problem."))) (p :class "text-xs sm:text-sm text-stone-600" "We tried to start your payment with SumUp but hit a problem.")))
(defcomp ~checkout-error-order-id (&key oid) (defcomp ~checkout-error-order-id (&key (oid :as string))
(p :class "text-xs text-rose-800/80" "Order ID: " (span :class "font-mono" oid))) (p :class "text-xs text-rose-800/80" "Order ID: " (span :class "font-mono" oid)))
(defcomp ~checkout-error-content (&key msg order back-url) (defcomp ~checkout-error-content (&key (msg :as string) order (back-url :as string))
(div :class "max-w-full px-3 py-3 space-y-4" (div :class "max-w-full px-3 py-3 space-y-4"
(div :class "rounded-2xl border border-rose-200 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-900 space-y-2" (div :class "rounded-2xl border border-rose-200 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-900 space-y-2"
(p :class "font-medium" "Something went wrong.") (p :class "font-medium" "Something went wrong.")

View File

@@ -1,4 +1,4 @@
(defcomp ~base-shell (&key title asset-url &rest children) (defcomp ~base-shell (&key (title :as string) (asset-url :as string) &rest children)
(<> (<>
(raw! "<!doctype html>") (raw! "<!doctype html>")
(html :lang "en" (html :lang "en"
@@ -23,13 +23,13 @@
;; <script>__sxResolve("id", "(resolved sx ...)")</script> ;; <script>__sxResolve("id", "(resolved sx ...)")</script>
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~suspense (&key id fallback &rest children) (defcomp ~suspense (&key (id :as string) fallback &rest children)
(div :id (str "sx-suspense-" id) (div :id (str "sx-suspense-" id)
:data-suspense id :data-suspense id
:style "display:contents" :style "display:contents"
(if (not (empty? children)) children fallback))) (if (not (empty? children)) children fallback)))
(defcomp ~error-page (&key title message image asset-url) (defcomp ~error-page (&key (title :as string) (message :as string) (image :as string?) (asset-url :as string))
(~base-shell :title title :asset-url asset-url (~base-shell :title title :asset-url asset-url
(div :class "text-center p-8 max-w-lg mx-auto" (div :class "text-center p-8 max-w-lg mx-auto"
(div :class "font-bold text-2xl md:text-4xl text-red-500 mb-4" (div :class "font-bold text-2xl md:text-4xl text-red-500 mb-4"

View File

@@ -1,4 +1,4 @@
(defcomp ~relation-attach (&key create-url label icon) (defcomp ~relation-attach (&key (create-url :as string) (label :as string?) (icon :as string?))
(a :href create-url (a :href create-url
:sx-get create-url :sx-get create-url
:sx-target "#main-panel" :sx-target "#main-panel"
@@ -8,7 +8,7 @@
(when icon (i :class icon)) (when icon (i :class icon))
(span (or label "Add")))) (span (or label "Add"))))
(defcomp ~relation-detach (&key detach-url name) (defcomp ~relation-detach (&key (detach-url :as string) (name :as string?))
(button :sx-delete detach-url (button :sx-delete detach-url
:sx-confirm (str "Remove " (or name "this item") "?") :sx-confirm (str "Remove " (or name "this item") "?")
:class "text-red-500 hover:text-red-700 text-sm p-1 rounded hover:bg-red-50 transition-colors" :class "text-red-500 hover:text-red-700 text-sm p-1 rounded hover:bg-red-50 transition-colors"

View File

@@ -11,13 +11,13 @@
;; - Pre-rendered meta HTML from callers ;; - Pre-rendered meta HTML from callers
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~sx-page-shell (&key title meta-html csrf (defcomp ~sx-page-shell (&key (title :as string) (meta-html :as string?) (csrf :as string)
sx-css sx-css-classes (sx-css :as string?) (sx-css-classes :as string?)
component-hash component-defs (component-hash :as string?) (component-defs :as string?)
pages-sx page-sx (pages-sx :as string?) (page-sx :as string?)
asset-url sx-js-hash body-js-hash (asset-url :as string) (sx-js-hash :as string) (body-js-hash :as string?)
head-scripts inline-css inline-head-js (head-scripts :as list?) (inline-css :as string?) (inline-head-js :as string?)
init-sx body-scripts) (init-sx :as string?) (body-scripts :as list?))
(<> (<>
(raw! "<!doctype html>") (raw! "<!doctype html>")
(html :lang "en" (html :lang "en"

View File

@@ -20,7 +20,7 @@ _PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
sys.path.insert(0, _PROJECT) sys.path.insert(0, _PROJECT)
from shared.sx.parser import parse_all from shared.sx.parser import parse_all
from shared.sx.evaluator import _eval, _trampoline, _call_lambda from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline, call_lambda as _call_lambda
from shared.sx.types import Symbol, Keyword, Lambda, NIL, Component, Island from shared.sx.types import Symbol, Keyword, Lambda, NIL, Component, Island
# --- Test state --- # --- Test state ---
@@ -127,13 +127,38 @@ def render_html(sx_source):
except ImportError: except ImportError:
raise RuntimeError("render-to-html not available — sx_ref.py not built") raise RuntimeError("render-to-html not available — sx_ref.py not built")
exprs = parse_all(sx_source) exprs = parse_all(sx_source)
render_env = dict(env) # Use Env (not flat dict) so tests exercise the real scope chain path.
render_env = _Env(dict(env))
result = "" result = ""
for expr in exprs: for expr in exprs:
result += _render_to_html(expr, render_env) result += _render_to_html(expr, render_env)
return result return result
# --- Render SX (aser) platform function ---
def render_sx(sx_source):
"""Parse SX source and serialize to SX wire format via the bootstrapped evaluator."""
try:
from shared.sx.ref.sx_ref import aser as _aser, serialize as _serialize
except ImportError:
raise RuntimeError("aser not available — sx_ref.py not built")
exprs = parse_all(sx_source)
# Use Env (not flat dict) so tests exercise the real scope chain path.
# Using dict(env) hides bugs where merge() drops Env parent scopes.
render_env = _Env(dict(env))
result = ""
for expr in exprs:
val = _aser(expr, render_env)
if isinstance(val, str):
result += val
elif val is None or val is NIL:
pass
else:
result += _serialize(val)
return result
# --- Signal platform primitives --- # --- Signal platform primitives ---
# Implements the signal runtime platform interface for testing signals.sx # Implements the signal runtime platform interface for testing signals.sx
@@ -258,10 +283,12 @@ SPECS = {
"parser": {"file": "test-parser.sx", "needs": ["sx-parse"]}, "parser": {"file": "test-parser.sx", "needs": ["sx-parse"]},
"router": {"file": "test-router.sx", "needs": []}, "router": {"file": "test-router.sx", "needs": []},
"render": {"file": "test-render.sx", "needs": ["render-html"]}, "render": {"file": "test-render.sx", "needs": ["render-html"]},
"aser": {"file": "test-aser.sx", "needs": ["render-sx"]},
"deps": {"file": "test-deps.sx", "needs": []}, "deps": {"file": "test-deps.sx", "needs": []},
"engine": {"file": "test-engine.sx", "needs": []}, "engine": {"file": "test-engine.sx", "needs": []},
"orchestration": {"file": "test-orchestration.sx", "needs": []}, "orchestration": {"file": "test-orchestration.sx", "needs": []},
"signals": {"file": "test-signals.sx", "needs": ["make-signal"]}, "signals": {"file": "test-signals.sx", "needs": ["make-signal"]},
"types": {"file": "test-types.sx", "needs": []},
} }
REF_DIR = os.path.join(_HERE, "..", "ref") REF_DIR = os.path.join(_HERE, "..", "ref")
@@ -296,8 +323,9 @@ env = _Env({
"make-keyword": make_keyword, "make-keyword": make_keyword,
"symbol-name": symbol_name, "symbol-name": symbol_name,
"keyword-name": keyword_name, "keyword-name": keyword_name,
# Render platform function # Render platform functions
"render-html": render_html, "render-html": render_html,
"render-sx": render_sx,
# Extra primitives needed by spec modules (router.sx, deps.sx) # Extra primitives needed by spec modules (router.sx, deps.sx)
"for-each-indexed": "_deferred", # replaced below "for-each-indexed": "_deferred", # replaced below
"dict-set!": "_deferred", "dict-set!": "_deferred",
@@ -722,6 +750,165 @@ def _load_signals(env):
env["batch"] = _batch env["batch"] = _batch
def _load_types(env):
"""Load types.sx spec — gradual type system."""
from shared.sx.types import Component
def _component_param_types(c):
return getattr(c, 'param_types', None)
def _component_set_param_types(c, d):
c.param_types = d
env["component-param-types"] = _component_param_types
env["component-set-param-types!"] = _component_set_param_types
# test-prim-types: a minimal type registry for testing
def _test_prim_types():
return {
"+": "number", "-": "number", "*": "number", "/": "number",
"mod": "number", "abs": "number", "floor": "number",
"ceil": "number", "round": "number", "min": "number",
"max": "number", "parse-int": "number", "parse-float": "number",
"=": "boolean", "!=": "boolean", "<": "boolean", ">": "boolean",
"<=": "boolean", ">=": "boolean",
"str": "string", "string-length": "number",
"substring": "string", "upcase": "string", "downcase": "string",
"trim": "string", "split": "list", "join": "string",
"string-contains?": "boolean", "starts-with?": "boolean",
"ends-with?": "boolean", "replace": "string",
"not": "boolean", "nil?": "boolean", "number?": "boolean",
"string?": "boolean", "list?": "boolean", "dict?": "boolean",
"boolean?": "boolean", "symbol?": "boolean", "empty?": "boolean",
"list": "list", "first": "any", "rest": "list", "nth": "any",
"last": "any", "cons": "list", "append": "list",
"reverse": "list", "len": "number", "contains?": "boolean",
"flatten": "list", "concat": "list", "slice": "list",
"range": "list", "sort": "list", "sort-by": "list",
"map": "list", "filter": "list", "reduce": "any",
"some": "boolean", "every?": "boolean",
"dict": "dict", "assoc": "dict", "dissoc": "dict",
"get": "any", "keys": "list", "vals": "list",
"has-key?": "boolean", "merge": "dict",
}
env["test-prim-types"] = _test_prim_types
# test-prim-param-types: param type signatures for primitive call checking
def _test_prim_param_types():
# Each entry: {"positional": [["name", "type"|None], ...], "rest-type": "type"|None}
return {
"+": {"positional": [], "rest-type": "number"},
"-": {"positional": [["a", "number"]], "rest-type": "number"},
"*": {"positional": [], "rest-type": "number"},
"/": {"positional": [["a", "number"], ["b", "number"]], "rest-type": None},
"mod": {"positional": [["a", "number"], ["b", "number"]], "rest-type": None},
"sqrt": {"positional": [["x", "number"]], "rest-type": None},
"pow": {"positional": [["x", "number"], ["n", "number"]], "rest-type": None},
"abs": {"positional": [["x", "number"]], "rest-type": None},
"floor": {"positional": [["x", "number"]], "rest-type": None},
"ceil": {"positional": [["x", "number"]], "rest-type": None},
"round": {"positional": [["x", "number"]], "rest-type": "number"},
"min": {"positional": [], "rest-type": "number"},
"max": {"positional": [], "rest-type": "number"},
"clamp": {"positional": [["x", "number"], ["lo", "number"], ["hi", "number"]], "rest-type": None},
"inc": {"positional": [["n", "number"]], "rest-type": None},
"dec": {"positional": [["n", "number"]], "rest-type": None},
"<": {"positional": [["a", "number"], ["b", "number"]], "rest-type": None},
">": {"positional": [["a", "number"], ["b", "number"]], "rest-type": None},
"<=": {"positional": [["a", "number"], ["b", "number"]], "rest-type": None},
">=": {"positional": [["a", "number"], ["b", "number"]], "rest-type": None},
"odd?": {"positional": [["n", "number"]], "rest-type": None},
"even?": {"positional": [["n", "number"]], "rest-type": None},
"zero?": {"positional": [["n", "number"]], "rest-type": None},
"upper": {"positional": [["s", "string"]], "rest-type": None},
"upcase": {"positional": [["s", "string"]], "rest-type": None},
"lower": {"positional": [["s", "string"]], "rest-type": None},
"downcase": {"positional": [["s", "string"]], "rest-type": None},
"string-length": {"positional": [["s", "string"]], "rest-type": None},
"substring": {"positional": [["s", "string"], ["start", "number"], ["end", "number"]], "rest-type": None},
"string-contains?": {"positional": [["s", "string"], ["needle", "string"]], "rest-type": None},
"trim": {"positional": [["s", "string"]], "rest-type": None},
"split": {"positional": [["s", "string"]], "rest-type": "string"},
"join": {"positional": [["sep", "string"], ["coll", "list"]], "rest-type": None},
"replace": {"positional": [["s", "string"], ["old", "string"], ["new", "string"]], "rest-type": None},
"index-of": {"positional": [["s", "string"], ["needle", "string"]], "rest-type": "number"},
"starts-with?": {"positional": [["s", "string"], ["prefix", "string"]], "rest-type": None},
"ends-with?": {"positional": [["s", "string"], ["suffix", "string"]], "rest-type": None},
"concat": {"positional": [], "rest-type": "list"},
"range": {"positional": [["start", "number"], ["end", "number"]], "rest-type": "number"},
"first": {"positional": [["coll", "list"]], "rest-type": None},
"last": {"positional": [["coll", "list"]], "rest-type": None},
"rest": {"positional": [["coll", "list"]], "rest-type": None},
"nth": {"positional": [["coll", "list"], ["n", "number"]], "rest-type": None},
"cons": {"positional": [["x", None], ["coll", "list"]], "rest-type": None},
"append": {"positional": [["coll", "list"]], "rest-type": None},
"append!": {"positional": [["coll", "list"]], "rest-type": None},
"reverse": {"positional": [["coll", "list"]], "rest-type": None},
"flatten": {"positional": [["coll", "list"]], "rest-type": None},
"chunk-every": {"positional": [["coll", "list"], ["n", "number"]], "rest-type": None},
"zip-pairs": {"positional": [["coll", "list"]], "rest-type": None},
"keys": {"positional": [["d", "dict"]], "rest-type": None},
"vals": {"positional": [["d", "dict"]], "rest-type": None},
"merge": {"positional": [], "rest-type": "dict"},
"has-key?": {"positional": [["d", "dict"]], "rest-type": None},
"assoc": {"positional": [["d", "dict"]], "rest-type": None},
"dissoc": {"positional": [["d", "dict"]], "rest-type": None},
"dict-set!": {"positional": [["d", "dict"]], "rest-type": None},
"format-date": {"positional": [["date-str", "string"], ["fmt", "string"]], "rest-type": None},
"format-decimal": {"positional": [["val", "number"]], "rest-type": "number"},
"parse-datetime": {"positional": [["s", "string"]], "rest-type": None},
"pluralize": {"positional": [["count", "number"]], "rest-type": "string"},
"escape": {"positional": [["s", "string"]], "rest-type": None},
"strip-tags": {"positional": [["s", "string"]], "rest-type": None},
"symbol-name": {"positional": [["sym", "symbol"]], "rest-type": None},
"keyword-name": {"positional": [["kw", "keyword"]], "rest-type": None},
"sx-parse": {"positional": [["source", "string"]], "rest-type": None},
}
env["test-prim-param-types"] = _test_prim_param_types
env["test-env"] = lambda: env
# Platform functions needed by types.sx check-body-walk
if "env-get" not in env:
env["env-get"] = lambda e, k: e.get(k) if hasattr(e, 'get') else None
if "env-has?" not in env:
env["env-has?"] = lambda e, k: k in e
if "dict-has?" not in env:
env["dict-has?"] = lambda d, k: k in d if isinstance(d, dict) else False
if "dict-get" not in env:
env["dict-get"] = lambda d, k, *default: d.get(k, default[0] if default else None) if isinstance(d, dict) else (default[0] if default else None)
# types.sx uses component-has-children (no ?), test runner has component-has-children?
if "component-has-children" not in env:
env["component-has-children"] = lambda c: getattr(c, 'has_children', False)
# Try bootstrapped types first, fall back to eval
try:
from shared.sx.ref.sx_ref import (
subtype_p, type_union, narrow_type,
infer_type, check_component_call, check_component,
check_all, build_type_registry, type_any_p,
type_never_p, type_nullable_p, nullable_base,
narrow_exclude_nil, narrow_exclude,
)
env["subtype?"] = subtype_p
env["type-union"] = type_union
env["narrow-type"] = narrow_type
env["infer-type"] = infer_type
env["check-component-call"] = check_component_call
env["check-component"] = check_component
env["check-all"] = check_all
env["build-type-registry"] = build_type_registry
env["type-any?"] = type_any_p
env["type-never?"] = type_never_p
env["type-nullable?"] = type_nullable_p
env["nullable-base"] = nullable_base
env["narrow-exclude-nil"] = narrow_exclude_nil
env["narrow-exclude"] = narrow_exclude
except ImportError:
eval_file("types.sx", env)
def main(): def main():
global passed, failed, test_num global passed, failed, test_num
@@ -769,13 +956,15 @@ def main():
_load_orchestration(env) _load_orchestration(env)
if spec_name == "signals": if spec_name == "signals":
_load_signals(env) _load_signals(env)
if spec_name == "types":
_load_types(env)
print(f"# --- {spec_name} ---") print(f"# --- {spec_name} ---")
eval_file(spec["file"], env) eval_file(spec["file"], env)
# Reset render state after render tests to avoid leaking # Reset render state after render/aser tests to avoid leaking
# into subsequent specs (bootstrapped evaluator checks render_active) # into subsequent specs (bootstrapped evaluator checks render_active)
if spec_name == "render": if spec_name in ("render", "aser"):
try: try:
from shared.sx.ref.sx_ref import set_render_active_b from shared.sx.ref.sx_ref import set_render_active_b
set_render_active_b(False) set_render_active_b(False)

View File

@@ -21,7 +21,7 @@ class TestJsSxTranslation:
def _translate(self, sx_source: str) -> str: def _translate(self, sx_source: str) -> str:
"""Translate a single SX expression to JS using js.sx.""" """Translate a single SX expression to JS using js.sx."""
from shared.sx.evaluator import evaluate from shared.sx.ref.sx_ref import evaluate
env = load_js_sx() env = load_js_sx()
expr = parse(sx_source) expr = parse(sx_source)
env["_def_expr"] = expr env["_def_expr"] = expr

View File

@@ -18,7 +18,7 @@ from shared.sx.deps import (
def make_env(*sx_sources: str) -> dict: def make_env(*sx_sources: str) -> dict:
"""Parse and evaluate component definitions into an env dict.""" """Parse and evaluate component definitions into an env dict."""
from shared.sx.evaluator import _eval, _trampoline from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
env: dict = {} env: dict = {}
for source in sx_sources: for source in sx_sources:
exprs = parse_all(source) exprs = parse_all(source)

View File

@@ -23,7 +23,7 @@ from shared.sx.deps import (
def make_env(*sx_sources: str) -> dict: def make_env(*sx_sources: str) -> dict:
"""Parse and evaluate component definitions into an env dict.""" """Parse and evaluate component definitions into an env dict."""
from shared.sx.evaluator import _eval, _trampoline from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
env: dict = {} env: dict = {}
for source in sx_sources: for source in sx_sources:
exprs = parse_all(source) exprs = parse_all(source)

View File

@@ -20,7 +20,7 @@ from shared.sx.deps import (
def make_env(*sx_sources: str) -> dict: def make_env(*sx_sources: str) -> dict:
"""Parse and evaluate component definitions into an env dict.""" """Parse and evaluate component definitions into an env dict."""
from shared.sx.evaluator import _eval, _trampoline from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
env: dict = {} env: dict = {}
for source in sx_sources: for source in sx_sources:
exprs = parse_all(source) exprs = parse_all(source)
@@ -282,7 +282,7 @@ class TestIoRoutingLogic:
""" """
def _eval(self, src, env): def _eval(self, src, env):
from shared.sx.evaluator import _eval, _trampoline from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
result = None result = None
for expr in parse_all(src): for expr in parse_all(src):
result = _trampoline(_eval(expr, env)) result = _trampoline(_eval(expr, env))

View File

@@ -156,7 +156,7 @@ class TestDataPageDeps:
def test_deps_computed_for_data_page(self): def test_deps_computed_for_data_page(self):
from shared.sx.deps import components_needed from shared.sx.deps import components_needed
from shared.sx.parser import parse_all as pa from shared.sx.parser import parse_all as pa
from shared.sx.evaluator import _eval, _trampoline from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
# Define a component # Define a component
env = {} env = {}
@@ -172,7 +172,7 @@ class TestDataPageDeps:
def test_deps_transitive_for_data_page(self): def test_deps_transitive_for_data_page(self):
from shared.sx.deps import components_needed from shared.sx.deps import components_needed
from shared.sx.parser import parse_all as pa from shared.sx.parser import parse_all as pa
from shared.sx.evaluator import _eval, _trampoline from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
env = {} env = {}
source = """ source = """
@@ -205,7 +205,7 @@ class TestDataPipelineSimulation:
def test_full_pipeline(self): def test_full_pipeline(self):
from shared.sx.parser import parse_all as pa from shared.sx.parser import parse_all as pa
from shared.sx.evaluator import _eval, _trampoline from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
# 1. Define a component that uses only pure primitives # 1. Define a component that uses only pure primitives
env = {} env = {}
@@ -236,7 +236,7 @@ class TestDataPipelineSimulation:
def test_pipeline_with_list_data(self): def test_pipeline_with_list_data(self):
from shared.sx.parser import parse_all as pa from shared.sx.parser import parse_all as pa
from shared.sx.evaluator import _eval, _trampoline from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
env = {} env = {}
for expr in pa(''' for expr in pa('''
@@ -262,7 +262,7 @@ class TestDataPipelineSimulation:
def test_pipeline_data_isolation(self): def test_pipeline_data_isolation(self):
"""Different data for the same content produces different results.""" """Different data for the same content produces different results."""
from shared.sx.parser import parse_all as pa from shared.sx.parser import parse_all as pa
from shared.sx.evaluator import _eval, _trampoline from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
env = {} env = {}
for expr in pa('(defcomp ~page (&key title count) (str title ": " count))'): for expr in pa('(defcomp ~page (&key title count) (str title ": " count))'):
@@ -298,7 +298,7 @@ class TestDataCache:
def _make_env(self, current_time_ms=1000): def _make_env(self, current_time_ms=1000):
"""Create an env with cache functions and a controllable now-ms.""" """Create an env with cache functions and a controllable now-ms."""
from shared.sx.parser import parse_all as pa from shared.sx.parser import parse_all as pa
from shared.sx.evaluator import _eval, _trampoline from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
env = {} env = {}
# Mock now-ms as a callable that returns current_time_ms # Mock now-ms as a callable that returns current_time_ms
@@ -344,7 +344,7 @@ class TestDataCache:
def _eval(self, src, env): def _eval(self, src, env):
from shared.sx.parser import parse_all as pa from shared.sx.parser import parse_all as pa
from shared.sx.evaluator import _eval, _trampoline from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
result = None result = None
for expr in pa(src): for expr in pa(src):
result = _trampoline(_eval(expr, env)) result = _trampoline(_eval(expr, env))

View File

@@ -18,7 +18,7 @@ from shared.sx.types import Symbol, Keyword, Lambda, Component, Macro, NIL
def hw_eval(text, env=None): def hw_eval(text, env=None):
"""Evaluate via hand-written evaluator.py.""" """Evaluate via hand-written evaluator.py."""
from shared.sx.evaluator import evaluate as _evaluate, EvalError from shared.sx.ref.sx_ref import evaluate as _evaluate
if env is None: if env is None:
env = {} env = {}
return _evaluate(parse(text), env) return _evaluate(parse(text), env)
@@ -50,7 +50,7 @@ def ref_render(text, env=None):
def hw_eval_multi(text, env=None): def hw_eval_multi(text, env=None):
"""Evaluate multiple expressions (e.g. defines then call).""" """Evaluate multiple expressions (e.g. defines then call)."""
from shared.sx.evaluator import evaluate as _evaluate from shared.sx.ref.sx_ref import evaluate as _evaluate
if env is None: if env is None:
env = {} env = {}
result = None result = None
@@ -736,7 +736,7 @@ class TestParityDeps:
class TestParityErrors: class TestParityErrors:
def test_undefined_symbol(self): def test_undefined_symbol(self):
from shared.sx.evaluator import EvalError as HwError from shared.sx.types import EvalError as HwError
from shared.sx.ref.sx_ref import EvalError as RefError from shared.sx.ref.sx_ref import EvalError as RefError
with pytest.raises(HwError): with pytest.raises(HwError):
hw_eval("undefined_var") hw_eval("undefined_var")

View File

@@ -12,7 +12,7 @@ import pytest
from shared.sx.parser import parse, parse_all from shared.sx.parser import parse, parse_all
from shared.sx.html import render as py_render from shared.sx.html import render as py_render
from shared.sx.evaluator import evaluate from shared.sx.ref.sx_ref import evaluate
SX_JS = Path(__file__).resolve().parents[2] / "static" / "scripts" / "sx.js" SX_JS = Path(__file__).resolve().parents[2] / "static" / "scripts" / "sx.js"
SX_TEST_JS = Path(__file__).resolve().parents[2] / "static" / "scripts" / "sx-test.js" SX_TEST_JS = Path(__file__).resolve().parents[2] / "static" / "scripts" / "sx-test.js"

View File

@@ -7,7 +7,7 @@ from __future__ import annotations
import pytest import pytest
from shared.sx.parser import parse_all from shared.sx.parser import parse_all
from shared.sx.evaluator import _eval, _trampoline from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
_PREAMBLE = '''(define assert-equal (fn (expected actual) (assert (equal? expected actual) (str "Expected " (str expected) " but got " (str actual))))) _PREAMBLE = '''(define assert-equal (fn (expected actual) (assert (equal? expected actual) (str "Expected " (str expected) " but got " (str actual)))))

View File

@@ -170,6 +170,7 @@ class Component:
deps: set[str] = field(default_factory=set) # transitive component deps (~names) deps: set[str] = field(default_factory=set) # transitive component deps (~names)
io_refs: set[str] | None = None # transitive IO primitive refs (None = not computed) io_refs: set[str] | None = None # transitive IO primitive refs (None = not computed)
affinity: str = "auto" # "auto" | "client" | "server" affinity: str = "auto" # "auto" | "client" | "server"
param_types: dict[str, Any] | None = None # {param_name: type_expr} for gradual typing
@property @property
def is_pure(self) -> bool: def is_pure(self) -> bool:
@@ -375,6 +376,15 @@ class _ShiftSignal(BaseException):
self.env = env self.env = env
# ---------------------------------------------------------------------------
# EvalError
# ---------------------------------------------------------------------------
class EvalError(Exception):
"""Error during expression evaluation."""
pass
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Type alias # Type alias
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

Some files were not shown because too many files have changed in this diff Show More