Root cause: process-elements during WASM boot-init marks elements as processed but process-one silently fails (effect functions don't execute in WASM boot context). Deferred process-elements then skips them. Fixes: - boot-init: defer process-elements via set-timeout 0 - hydrate-island: defer process-elements via set-timeout 0 - process-elements: move mark-processed! after process-one so failed boot-context calls don't poison the flag - observe-intersection: native JS platform function (K.registerNative) to avoid bytecode callback issue with IntersectionObserver - Remove SX observe-intersection from boot-helpers.sx (was overriding the working native version) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
720 lines
19 KiB
Plaintext
720 lines
19 KiB
Plaintext
(define _sx-bound-prefix "_sxBound")
|
|
|
|
(define
|
|
mark-processed!
|
|
(fn (el key) (host-set! el (str _sx-bound-prefix key) true)))
|
|
|
|
(define
|
|
is-processed?
|
|
(fn
|
|
(el key)
|
|
(let ((v (host-get el (str _sx-bound-prefix key)))) (if v true false))))
|
|
|
|
(define
|
|
clear-processed!
|
|
(fn (el key) (host-set! el (str _sx-bound-prefix key) nil)))
|
|
|
|
(define
|
|
callable?
|
|
(fn
|
|
(v)
|
|
(let
|
|
((t (type-of v)))
|
|
(or (= t "lambda") (= t "native-fn") (= t "continuation")))))
|
|
|
|
(define
|
|
to-kebab
|
|
(fn
|
|
(s)
|
|
"Convert camelCase to kebab-case."
|
|
(let
|
|
((result (list)) (i 0))
|
|
(let
|
|
loop
|
|
((i 0))
|
|
(when
|
|
(< i (len s))
|
|
(let
|
|
((ch (nth s i)))
|
|
(if
|
|
(and (>= ch "A") (<= ch "Z"))
|
|
(do
|
|
(when (> i 0) (append! result "-"))
|
|
(append! result (lower ch)))
|
|
(append! result ch))
|
|
(loop (+ i 1)))))
|
|
(join "" result))))
|
|
|
|
(define
|
|
sx-load-components
|
|
(fn
|
|
(text)
|
|
"Parse and evaluate component definitions from text."
|
|
(when
|
|
(and text (> (len text) 0))
|
|
(let
|
|
((exprs (sx-parse text)))
|
|
(for-each (fn (expr) (cek-eval expr)) exprs)))))
|
|
|
|
(define
|
|
call-expr
|
|
(fn
|
|
(expr-text env-bindings)
|
|
"Parse and evaluate an SX expression string."
|
|
(let
|
|
((exprs (sx-parse expr-text)))
|
|
(when (not (empty? exprs)) (cek-eval (first exprs))))))
|
|
|
|
(define
|
|
base-env
|
|
(fn () "Return the current global environment." (global-env)))
|
|
|
|
(define
|
|
get-render-env
|
|
(fn
|
|
(extra)
|
|
"Get the rendering environment (global env, optionally merged with extra)."
|
|
(let
|
|
((env (base-env)))
|
|
(if (and extra (not (nil? extra))) (env-merge env extra) env))))
|
|
|
|
(define
|
|
merge-envs
|
|
(fn
|
|
(a b)
|
|
"Merge two environments."
|
|
(if (and a b) (env-merge a b) (or a b (global-env)))))
|
|
|
|
(define
|
|
sx-render-with-env
|
|
(fn
|
|
(source extra-env)
|
|
"Parse SX source and render to DOM fragment."
|
|
(let
|
|
((doc (host-global "document"))
|
|
(frag (host-call doc "createDocumentFragment"))
|
|
(exprs (sx-parse source)))
|
|
(for-each
|
|
(fn
|
|
(expr)
|
|
(let
|
|
((html (render-to-html expr)))
|
|
(when
|
|
(and html (> (len html) 0))
|
|
(let
|
|
((temp (host-call doc "createElement" "template")))
|
|
(host-set! temp "innerHTML" html)
|
|
(host-call frag "appendChild" (host-get temp "content"))))))
|
|
exprs)
|
|
frag)))
|
|
|
|
(define
|
|
parse-env-attr
|
|
(fn (el) "Parse data-sx-env attribute (JSON key-value pairs)." nil))
|
|
|
|
(define store-env-attr (fn (el base new-env) nil))
|
|
|
|
(define
|
|
resolve-mount-target
|
|
(fn
|
|
(target)
|
|
"Resolve a CSS selector string to a DOM element."
|
|
(if (string? target) (dom-query target) target)))
|
|
|
|
(define
|
|
remove-head-element
|
|
(fn
|
|
(sel)
|
|
"Remove a <head> element matching selector."
|
|
(let ((el (dom-query sel))) (when el (dom-remove el)))))
|
|
|
|
(define set-sx-comp-cookie (fn (hash) (set-cookie "sx-components" hash)))
|
|
|
|
(define clear-sx-comp-cookie (fn () (set-cookie "sx-components" "")))
|
|
|
|
(define
|
|
log-parse-error
|
|
(fn (label text err) (log-error (str "Parse error in " label ": " err))))
|
|
|
|
(define
|
|
loaded-component-names
|
|
(fn
|
|
()
|
|
(let
|
|
((scripts (dom-query-all (dom-body) "script[data-components]"))
|
|
(names (list)))
|
|
(for-each
|
|
(fn
|
|
(script)
|
|
(let
|
|
((text (or (dom-get-attr script "data-components") "")))
|
|
(when
|
|
(> (len text) 0)
|
|
(for-each
|
|
(fn
|
|
(name)
|
|
(when
|
|
(> (len (trim name)) 0)
|
|
(append! names (trim name))))
|
|
(split text ",")))))
|
|
scripts)
|
|
names)))
|
|
|
|
(define
|
|
csrf-token
|
|
(fn
|
|
()
|
|
(let
|
|
((meta (dom-query "meta[name=\"csrf-token\"]")))
|
|
(if meta (dom-get-attr meta "content") nil))))
|
|
|
|
(define validate-for-request (fn (el) true))
|
|
|
|
(define
|
|
build-request-body
|
|
(fn
|
|
(el method url)
|
|
(let
|
|
((m (upper method)))
|
|
(if
|
|
(or (= m "GET") (= m "HEAD"))
|
|
(if
|
|
(and el (= (upper (or (dom-tag-name el) "")) "FORM"))
|
|
(let
|
|
((fd (host-new "FormData" el))
|
|
(params (host-new "URLSearchParams" fd))
|
|
(qs (host-call params "toString")))
|
|
(dict
|
|
"url"
|
|
(if
|
|
(and qs (> (len qs) 0))
|
|
(str url (if (contains? url "?") "&" "?") qs)
|
|
url)
|
|
"body"
|
|
nil
|
|
"content-type"
|
|
nil))
|
|
(dict "url" url "body" nil "content-type" nil))
|
|
(if
|
|
(and el (= (upper (or (dom-tag-name el) "")) "FORM"))
|
|
(let
|
|
((enctype (or (dom-get-attr el "enctype") "application/x-www-form-urlencoded")))
|
|
(if
|
|
(= enctype "multipart/form-data")
|
|
(let
|
|
((fd (host-new "FormData" el)))
|
|
(dict "url" url "body" fd "content-type" nil))
|
|
(let
|
|
((fd (host-new "FormData" el))
|
|
(params (host-new "URLSearchParams" fd)))
|
|
(dict
|
|
"url"
|
|
url
|
|
"body"
|
|
(host-call params "toString")
|
|
"content-type"
|
|
"application/x-www-form-urlencoded"))))
|
|
(dict "url" url "body" nil "content-type" nil))))))
|
|
|
|
(define abort-previous-target (fn (el) nil))
|
|
|
|
(define abort-previous (fn (el) nil))
|
|
|
|
(define track-controller (fn (el ctrl) nil))
|
|
|
|
(define track-controller-target (fn (el ctrl) nil))
|
|
|
|
(define new-abort-controller (fn () (host-new "AbortController")))
|
|
|
|
(define abort-signal (fn (ctrl) (host-get ctrl "signal")))
|
|
|
|
(define apply-optimistic (fn (el) nil))
|
|
|
|
(define revert-optimistic (fn (el) nil))
|
|
|
|
(define dom-has-attr? (fn (el name) (host-call el "hasAttribute" name)))
|
|
|
|
(define
|
|
show-indicator
|
|
(fn
|
|
(el)
|
|
(let
|
|
((indicator-sel (dom-get-attr el "sx-indicator")))
|
|
(when
|
|
indicator-sel
|
|
(let
|
|
((indicator (dom-query indicator-sel)))
|
|
(when
|
|
indicator
|
|
(dom-remove-class indicator "hidden")
|
|
(dom-add-class indicator "sx-indicator-visible"))))
|
|
indicator-sel)))
|
|
|
|
(define
|
|
disable-elements
|
|
(fn
|
|
(el)
|
|
(let
|
|
((disable-sel (dom-get-attr el "sx-disabled-elt")))
|
|
(if
|
|
disable-sel
|
|
(let
|
|
((elts (dom-query-all (dom-body) disable-sel)))
|
|
(for-each (fn (e) (dom-set-attr e "disabled" "")) elts)
|
|
elts)
|
|
(list)))))
|
|
|
|
(define
|
|
clear-loading-state
|
|
(fn
|
|
(el indicator disabled-elts)
|
|
(dom-remove-class el "sx-request")
|
|
(dom-remove-attr el "aria-busy")
|
|
(when
|
|
indicator
|
|
(let
|
|
((ind (dom-query indicator)))
|
|
(when
|
|
ind
|
|
(dom-add-class ind "hidden")
|
|
(dom-remove-class ind "sx-indicator-visible"))))
|
|
(when
|
|
disabled-elts
|
|
(for-each (fn (e) (dom-remove-attr e "disabled")) disabled-elts))))
|
|
|
|
(define abort-error? (fn (err) (= (host-get err "name") "AbortError")))
|
|
|
|
(define
|
|
promise-catch
|
|
(fn (p f) (let ((cb (host-callback f))) (host-call p "catch" cb))))
|
|
|
|
(define
|
|
fetch-request
|
|
(fn
|
|
(config success-fn error-fn)
|
|
(let
|
|
((url (get config "url"))
|
|
(method (or (get config "method") "GET"))
|
|
(headers (or (get config "headers") (dict)))
|
|
(body (get config "body"))
|
|
(signal (get config "signal"))
|
|
(preloaded (get config "preloaded")))
|
|
(if
|
|
preloaded
|
|
(success-fn true 200 (fn (name) nil) preloaded)
|
|
(let
|
|
((h (host-new "Headers")) (js-opts (host-new "Object")))
|
|
(for-each
|
|
(fn (k) (host-call h "set" k (get headers k)))
|
|
(keys headers))
|
|
(host-set! js-opts "method" method)
|
|
(host-set! js-opts "headers" h)
|
|
(when body (host-set! js-opts "body" body))
|
|
(when signal (host-set! js-opts "signal" signal))
|
|
(promise-then
|
|
(host-call (dom-window) "fetch" url js-opts)
|
|
(fn
|
|
(response)
|
|
(let
|
|
((ok (host-get response "ok"))
|
|
(status (host-get response "status"))
|
|
(get-header
|
|
(fn
|
|
(name)
|
|
(host-call (host-get response "headers") "get" name))))
|
|
(promise-then
|
|
(host-call response "text")
|
|
(fn (text) (success-fn ok status get-header text))
|
|
error-fn)))
|
|
error-fn))))))
|
|
|
|
(define
|
|
fetch-location
|
|
(fn
|
|
(url)
|
|
(let
|
|
((target (or (dom-query "[sx-boost]") (dom-query "#main-panel"))))
|
|
(when target (browser-navigate url)))))
|
|
|
|
(define
|
|
fetch-and-restore
|
|
(fn
|
|
(main url headers scroll-y)
|
|
(fetch-request
|
|
(dict "url" url "method" "GET" "headers" headers "body" nil "signal" nil)
|
|
(fn
|
|
(resp-ok status get-header text)
|
|
(when
|
|
resp-ok
|
|
(dom-set-inner-html main text)
|
|
(post-swap main)
|
|
(host-call (dom-window) "scrollTo" 0 scroll-y)))
|
|
(fn (err) (log-warn (str "fetch-and-restore error: " err))))))
|
|
|
|
(define
|
|
fetch-preload
|
|
(fn
|
|
(url headers cache)
|
|
(fetch-request
|
|
(dict "url" url "method" "GET" "headers" headers "body" nil "signal" nil)
|
|
(fn
|
|
(resp-ok status get-header text)
|
|
(when resp-ok (preload-cache-set cache url text)))
|
|
(fn (err) nil))))
|
|
|
|
(define
|
|
fetch-streaming
|
|
(fn
|
|
(target pathname headers swap-fn)
|
|
(fetch-and-restore target pathname headers 0)))
|
|
|
|
(define
|
|
dom-parse-html-document
|
|
(fn
|
|
(text)
|
|
(let
|
|
((parser (host-new "DOMParser")))
|
|
(host-call parser "parseFromString" text "text/html"))))
|
|
|
|
(define
|
|
dom-body-inner-html
|
|
(fn (doc) (host-get (host-get doc "body") "innerHTML")))
|
|
|
|
(define
|
|
create-script-clone
|
|
(fn
|
|
(dead)
|
|
(let
|
|
((doc (host-global "document"))
|
|
(live (host-call doc "createElement" "script")))
|
|
(let
|
|
((attrs (host-get dead "attributes")))
|
|
(let
|
|
loop
|
|
((i 0))
|
|
(when
|
|
(< i (host-get attrs "length"))
|
|
(let
|
|
((attr (host-call attrs "item" i)))
|
|
(host-call
|
|
live
|
|
"setAttribute"
|
|
(host-get attr "name")
|
|
(host-get attr "value"))
|
|
(loop (+ i 1))))))
|
|
(host-set! live "textContent" (host-get dead "textContent"))
|
|
live)))
|
|
|
|
(define
|
|
cross-origin?
|
|
(fn
|
|
(url)
|
|
(if
|
|
(or (starts-with? url "http://") (starts-with? url "https://"))
|
|
(not (starts-with? url (browser-location-origin)))
|
|
false)))
|
|
|
|
(define
|
|
browser-scroll-to
|
|
(fn (x y) (host-call (dom-window) "scrollTo" x y)))
|
|
|
|
(define
|
|
with-transition
|
|
(fn
|
|
(enabled thunk)
|
|
(if
|
|
(and enabled (host-get (host-global "document") "startViewTransition"))
|
|
(host-call
|
|
(host-global "document")
|
|
"startViewTransition"
|
|
(host-callback thunk))
|
|
(thunk))))
|
|
|
|
(define
|
|
event-source-connect
|
|
(fn
|
|
(url el)
|
|
(let
|
|
((source (host-new "EventSource" url)))
|
|
(host-set! source "_sxElement" el)
|
|
source)))
|
|
|
|
(define
|
|
event-source-listen
|
|
(fn
|
|
(source event-name handler)
|
|
(host-call
|
|
source
|
|
"addEventListener"
|
|
event-name
|
|
(host-callback (fn (e) (handler e))))))
|
|
|
|
(define
|
|
bind-boost-link
|
|
(fn
|
|
(el href)
|
|
(dom-listen
|
|
el
|
|
"click"
|
|
(fn
|
|
(e)
|
|
(when
|
|
(not (event-modifier-key? e))
|
|
(prevent-default e)
|
|
(when
|
|
(not (dom-has-attr? el "sx-get"))
|
|
(dom-set-attr el "sx-get" href))
|
|
(when
|
|
(not (dom-has-attr? el "sx-push-url"))
|
|
(dom-set-attr el "sx-push-url" "true"))
|
|
(execute-request el nil nil))))))
|
|
|
|
(define
|
|
bind-boost-form
|
|
(fn
|
|
(form method action)
|
|
(dom-listen
|
|
form
|
|
"submit"
|
|
(fn (e) (prevent-default e) (execute-request form nil nil)))))
|
|
|
|
(define
|
|
bind-client-route-click
|
|
(fn
|
|
(link href fallback-fn)
|
|
(dom-listen
|
|
link
|
|
"click"
|
|
(fn
|
|
(e)
|
|
(when
|
|
(not (event-modifier-key? e))
|
|
(prevent-default e)
|
|
(let
|
|
((boost-el (dom-query "[sx-boost]"))
|
|
(target-sel
|
|
(if
|
|
boost-el
|
|
(let
|
|
((attr (dom-get-attr boost-el "sx-boost")))
|
|
(if (and attr (not (= attr "true"))) attr "#sx-content"))
|
|
"#sx-content")))
|
|
(if
|
|
(try-client-route (url-pathname href) target-sel)
|
|
(do (browser-push-state nil "" href) (browser-scroll-to 0 0))
|
|
(do
|
|
(log-info (str "sx:route server fetch " href))
|
|
(dom-set-attr link "sx-get" href)
|
|
(dom-set-attr link "sx-target" target-sel)
|
|
(dom-set-attr link "sx-select" target-sel)
|
|
(dom-set-attr link "sx-push-url" "true")
|
|
(execute-request link nil nil)))))))))
|
|
|
|
(define sw-post-message (fn (msg) nil))
|
|
|
|
(define try-parse-json (fn (text) (json-parse text)))
|
|
|
|
(define
|
|
strip-component-scripts
|
|
(fn
|
|
(text)
|
|
(let
|
|
((result text)
|
|
(start-tag "<script type=\"text/sx\" data-components>")
|
|
(end-tag "</script>"))
|
|
(let
|
|
loop
|
|
((s result))
|
|
(let
|
|
((start-idx (index-of s start-tag)))
|
|
(if
|
|
(= start-idx -1)
|
|
(set! result s)
|
|
(let
|
|
((after-start (+ start-idx (len start-tag)))
|
|
(rest-str (slice s (+ start-idx (len start-tag)))))
|
|
(let
|
|
((end-offset (index-of rest-str end-tag)))
|
|
(if
|
|
(= end-offset -1)
|
|
(set! result s)
|
|
(let
|
|
((comp-text (slice rest-str 0 end-offset))
|
|
(before (slice s 0 start-idx))
|
|
(after (slice rest-str (+ end-offset (len end-tag)))))
|
|
(sx-load-components comp-text)
|
|
(loop (str before after)))))))))
|
|
result)))
|
|
|
|
(define
|
|
extract-response-css
|
|
(fn
|
|
(text)
|
|
(let
|
|
((result text) (start-tag "<style data-sx-css>") (end-tag "</style>"))
|
|
(let
|
|
loop
|
|
((s result))
|
|
(let
|
|
((start-idx (index-of s start-tag)))
|
|
(if
|
|
(= start-idx -1)
|
|
(set! result s)
|
|
(let
|
|
((rest-str (slice s (+ start-idx (len start-tag)))))
|
|
(let
|
|
((end-offset (index-of rest-str end-tag)))
|
|
(if
|
|
(= end-offset -1)
|
|
(set! result s)
|
|
(let
|
|
((css-text (slice rest-str 0 end-offset))
|
|
(before (slice s 0 start-idx))
|
|
(after (slice rest-str (+ end-offset (len end-tag)))))
|
|
(let
|
|
((doc (host-global "document"))
|
|
(style-el (host-call doc "createElement" "style")))
|
|
(host-set! style-el "textContent" css-text)
|
|
(dom-append-to-head style-el))
|
|
(loop (str before after)))))))))
|
|
result)))
|
|
|
|
(define
|
|
sx-render
|
|
(fn
|
|
(text)
|
|
(let
|
|
((doc (host-global "document"))
|
|
(frag (host-call doc "createDocumentFragment"))
|
|
(exprs (sx-parse text)))
|
|
(scope-push! "sx-render-markers" true)
|
|
(for-each
|
|
(fn
|
|
(expr)
|
|
(let
|
|
((result (try-catch (fn () (render-to-dom expr (get-render-env nil) nil)) (fn (err) (log-error (str "sx-render: " err)) (let ((el (dom-create-element "div" nil))) (dom-set-attr el "class" "sx-render-error") (dom-set-attr el "style" "color:red;font-size:0.875rem;padding:0.5rem;border:1px solid red;border-radius:4px;margin:0.25rem 0;") (dom-set-text-content el (str "Render error: " err)) el)))))
|
|
(when result (dom-append frag result))))
|
|
exprs)
|
|
(scope-pop! "sx-render-markers")
|
|
frag)))
|
|
|
|
(define sx-hydrate (fn (root) (sx-hydrate-elements (or root (dom-body)))))
|
|
|
|
(define
|
|
sx-process-scripts
|
|
(fn
|
|
(root)
|
|
(let
|
|
((scripts (dom-query-all (or root (dom-body)) "script[type=\"text/sx\"]")))
|
|
(for-each
|
|
(fn
|
|
(s)
|
|
(when
|
|
(not (is-processed? s "sx-script"))
|
|
(mark-processed! s "sx-script")
|
|
(let
|
|
((text (host-get s "textContent")))
|
|
(when
|
|
(and text (> (len text) 0))
|
|
(let
|
|
((exprs (sx-parse text)))
|
|
(for-each
|
|
(fn
|
|
(expr)
|
|
(try-catch
|
|
(fn () (cek-eval expr))
|
|
(fn
|
|
(err)
|
|
(log-error (str "sx-process-scripts: " err)))))
|
|
exprs))))))
|
|
scripts))))
|
|
|
|
(define
|
|
select-from-container
|
|
(fn
|
|
(container selector)
|
|
(if
|
|
selector
|
|
(let
|
|
((selected (dom-query container selector)))
|
|
(if
|
|
selected
|
|
(children-to-fragment selected)
|
|
(children-to-fragment container)))
|
|
(children-to-fragment container))))
|
|
|
|
(define
|
|
children-to-fragment
|
|
(fn
|
|
(el)
|
|
(let
|
|
((doc (host-global "document"))
|
|
(frag (host-call doc "createDocumentFragment")))
|
|
(let
|
|
loop
|
|
()
|
|
(let
|
|
((child (dom-first-child el)))
|
|
(when child (dom-append frag child) (loop))))
|
|
frag)))
|
|
|
|
(define
|
|
select-html-from-doc
|
|
(fn
|
|
(doc selector)
|
|
(if
|
|
selector
|
|
(let
|
|
((el (dom-query doc selector)))
|
|
(if el (dom-inner-html el) (dom-body-inner-html doc)))
|
|
(dom-body-inner-html doc))))
|
|
|
|
(define register-io-deps (fn (deps) nil))
|
|
|
|
(define resolve-page-data (fn (page-name params callback) nil))
|
|
|
|
(define
|
|
parse-sx-data
|
|
(fn
|
|
(text)
|
|
(if
|
|
(and text (> (len text) 0))
|
|
(let
|
|
((exprs (sx-parse text)))
|
|
(if (not (empty? exprs)) (first exprs) nil))
|
|
nil)))
|
|
|
|
(define
|
|
try-eval-content
|
|
(fn
|
|
(content-src env)
|
|
(let
|
|
((exprs (sx-parse content-src)))
|
|
(if
|
|
(empty? exprs)
|
|
nil
|
|
(let
|
|
((frag (create-fragment)))
|
|
(for-each
|
|
(fn
|
|
(expr)
|
|
(let
|
|
((result (render-to-dom expr env nil)))
|
|
(when result (dom-append frag result))))
|
|
exprs)
|
|
frag)))))
|
|
|
|
(define
|
|
try-async-eval-content
|
|
(fn (content-src env callback) (try-eval-content content-src env)))
|
|
|
|
(define try-rerender-page (fn () nil))
|
|
|
|
(define execute-action (fn () nil))
|
|
|
|
(define bind-preload (fn () nil))
|
|
|
|
(define persist-offline-data (fn () nil))
|
|
|
|
(define retrieve-offline-data (fn () nil))
|