Fix WASM browser click handlers: 8 bugs, 50 new VM tests
The sx-get links were doing full page refreshes because click handlers never attached. Root causes: VM frame management bug, missing primitives, CEK/VM type dispatch mismatch, and silent error swallowing. Fixes: - VM frame exhaustion: frames <- [] now properly pops to rest_frames - length primitive: add alias for len in OCaml primitives - call_sx_fn: use sx_call directly instead of eval_expr (CEK checks for type "lambda" but VmClosure reports "function") - Boot error surfacing: Sx.init() now has try/catch + failure summary - Callback error surfacing: catch-all handler for non-Eval_error exceptions - Silent JIT failures: log before CEK fallback instead of swallowing - vm→env sync: loadModule now calls sync_vm_to_env() - sx_build_bytecode MCP tool added for bytecode compilation Tests: 50 new tests across test-vm.sx and test-vm-primitives.sx covering nested VM calls, frame integrity, CEK bridge, primitive availability, cross-module symbol resolution, and callback dispatch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -519,6 +519,21 @@ let rec handle_tool name args =
|
||||
| Unix.WEXITED 0 -> text_result (Printf.sprintf "OK — %s build succeeded\n%s" target (String.trim output))
|
||||
| _ -> error_result (Printf.sprintf "%s build failed:\n%s" target output))
|
||||
|
||||
| "sx_build_bytecode" ->
|
||||
let project_dir = try Sys.getenv "SX_PROJECT_DIR" with Not_found ->
|
||||
let spec_dir = try Sys.getenv "SX_SPEC_DIR" with Not_found -> "spec" in
|
||||
Filename.dirname spec_dir
|
||||
in
|
||||
let cmd = Printf.sprintf "cd %s && node hosts/ocaml/browser/compile-modules.js shared/static/wasm 2>&1" project_dir in
|
||||
let ic = Unix.open_process_in cmd in
|
||||
let lines = ref [] in
|
||||
(try while true do lines := input_line ic :: !lines done with End_of_file -> ());
|
||||
let status = Unix.close_process_in ic in
|
||||
let output = String.concat "\n" (List.rev !lines) in
|
||||
(match status with
|
||||
| Unix.WEXITED 0 -> text_result (Printf.sprintf "OK — bytecode compilation succeeded\n%s" (String.trim output))
|
||||
| _ -> error_result (Printf.sprintf "Bytecode compilation failed:\n%s" output))
|
||||
|
||||
| "sx_test" ->
|
||||
let host = args |> member "host" |> to_string_option |> Option.value ~default:"js" in
|
||||
let full = args |> member "full" |> to_bool_option |> Option.value ~default:false in
|
||||
@@ -748,8 +763,10 @@ let rec handle_tool name args =
|
||||
(match actions with Some a -> Some ("actions", `String a) | None -> None);
|
||||
(match island with Some i -> Some ("island", `String i) | None -> None);
|
||||
]) in
|
||||
let args_json = Yojson.Safe.to_string (Yojson.Safe.from_string (Yojson.Basic.to_string inspector_args)) in
|
||||
let cmd = Printf.sprintf "cd %s && node tests/playwright/sx-inspect.js '%s' 2>&1" project_dir (String.escaped args_json) in
|
||||
let args_json = Yojson.Basic.to_string inspector_args in
|
||||
(* Single-quote shell wrapping — escape any literal single quotes in JSON *)
|
||||
let shell_safe = String.concat "'\\''" (String.split_on_char '\'' args_json) in
|
||||
let cmd = Printf.sprintf "cd %s && node tests/playwright/sx-inspect.js '%s' 2>&1" project_dir shell_safe in
|
||||
let ic = Unix.open_process_in cmd in
|
||||
let lines = ref [] in
|
||||
(try while true do lines := input_line ic :: !lines done with End_of_file -> ());
|
||||
@@ -1304,6 +1321,8 @@ let tool_definitions = `List [
|
||||
[("target", `Assoc [("type", `String "string"); ("description", `String "Build target: \"js\" (default) or \"ocaml\"")]);
|
||||
("full", `Assoc [("type", `String "boolean"); ("description", `String "Include extensions and type system (default: false)")])]
|
||||
[];
|
||||
tool "sx_build_bytecode" "Compile all web .sx files to pre-compiled .sxbc.json bytecode modules for the WASM browser kernel."
|
||||
[] [];
|
||||
tool "sx_test" "Run SX test suite. Returns pass/fail summary and any failures."
|
||||
[("host", `Assoc [("type", `String "string"); ("description", `String "Test host: \"js\" (default) or \"ocaml\"")]);
|
||||
("full", `Assoc [("type", `String "boolean"); ("description", `String "Run full test suite including extensions (default: false)")])]
|
||||
@@ -1354,13 +1373,13 @@ let tool_definitions = `List [
|
||||
("files", `Assoc [("type", `String "array"); ("items", `Assoc [("type", `String "string")]); ("description", `String "Multiple .sx files to load in order")]);
|
||||
("setup", `Assoc [("type", `String "string"); ("description", `String "SX setup expression to run before main evaluation")])]
|
||||
["expr"];
|
||||
tool "sx_playwright" "Run Playwright browser tests or inspect SX pages interactively. Modes: run (spec files), inspect (page/island report with leak detection and handler audit), diff (full SSR vs hydrated DOM), hydrate (lake-focused SSR vs hydrated comparison — detects clobbering), eval (JS expression), interact (action sequence), screenshot."
|
||||
tool "sx_playwright" "Run Playwright browser tests or inspect SX pages interactively. Modes: run (spec files), inspect (page/island report with leak detection and handler audit), diff (full SSR vs hydrated DOM), hydrate (lake-focused SSR vs hydrated comparison — detects clobbering), eval (JS expression), interact (action sequence), screenshot, listeners (CDP event listener inspection), trace (click + capture console/network/pushState), cdp (raw CDP command)."
|
||||
[("spec", `Assoc [("type", `String "string"); ("description", `String "Spec file to run (run mode). e.g. stepper.spec.js")]);
|
||||
("mode", `Assoc [("type", `String "string"); ("description", `String "Mode: run, inspect, diff, hydrate, eval, interact, screenshot")]);
|
||||
("mode", `Assoc [("type", `String "string"); ("description", `String "Mode: run, inspect, diff, hydrate, eval, interact, screenshot, listeners, trace, cdp")]);
|
||||
("url", `Assoc [("type", `String "string"); ("description", `String "URL path to navigate to (default: /)")]);
|
||||
("island", `Assoc [("type", `String "string"); ("description", `String "Filter inspect to a specific island by name (e.g. home/stepper)")]);
|
||||
("selector", `Assoc [("type", `String "string"); ("description", `String "CSS selector to focus on (screenshot mode)")]);
|
||||
("expr", `Assoc [("type", `String "string"); ("description", `String "JS expression to evaluate (eval mode)")]);
|
||||
("selector", `Assoc [("type", `String "string"); ("description", `String "CSS selector for screenshot/listeners/trace modes")]);
|
||||
("expr", `Assoc [("type", `String "string"); ("description", `String "JS expression (eval mode), selector (listeners/trace), or CDP command (cdp mode)")]);
|
||||
("actions", `Assoc [("type", `String "string"); ("description", `String "Semicolon-separated action sequence (interact mode). Actions: click:sel, fill:sel:val, wait:ms, text:sel, html:sel, attrs:sel, screenshot, screenshot:sel, count:sel, visible:sel")])]
|
||||
[];
|
||||
]
|
||||
|
||||
@@ -58,7 +58,8 @@ let global_env = make_env ()
|
||||
let _sx_render_mode = ref false
|
||||
|
||||
let call_sx_fn (fn : value) (args : value list) : value =
|
||||
Sx_ref.eval_expr (List (fn :: args)) (Env global_env)
|
||||
let result = Sx_runtime.sx_call fn args in
|
||||
!Sx_primitives._sx_trampoline_fn result
|
||||
|
||||
(* ================================================================== *)
|
||||
(* Value conversion: OCaml <-> JS *)
|
||||
@@ -107,10 +108,18 @@ let rec value_to_js (v : value) : Js.Unsafe.any =
|
||||
let args = match arg with Nil -> [] | _ -> [arg] in
|
||||
let result = call_sx_fn v args in
|
||||
value_to_js result
|
||||
with Eval_error msg ->
|
||||
with
|
||||
| Eval_error msg ->
|
||||
let fn_info = Printf.sprintf " [callback %s handle=%d]" (type_of v) handle in
|
||||
ignore (Js.Unsafe.meth_call
|
||||
(Js.Unsafe.get Js.Unsafe.global (Js.string "console"))
|
||||
"error" [| Js.Unsafe.inject (Js.string ("[sx] " ^ msg)) |]);
|
||||
"error" [| Js.Unsafe.inject (Js.string ("[sx] " ^ msg ^ fn_info)) |]);
|
||||
Js.Unsafe.inject Js.null
|
||||
| exn ->
|
||||
let fn_info = Printf.sprintf " [callback %s handle=%d]" (type_of v) handle in
|
||||
ignore (Js.Unsafe.meth_call
|
||||
(Js.Unsafe.get Js.Unsafe.global (Js.string "console"))
|
||||
"error" [| Js.Unsafe.inject (Js.string ("[sx] UNCAUGHT: " ^ Printexc.to_string exn ^ fn_info)) |]);
|
||||
Js.Unsafe.inject Js.null) in
|
||||
Js.Unsafe.fun_call _tag_fn [|
|
||||
Js.Unsafe.inject inner;
|
||||
@@ -189,6 +198,31 @@ and js_to_value (js : Js.Unsafe.any) : value =
|
||||
let return_via_side_channel (v : Js.Unsafe.any) : Js.Unsafe.any =
|
||||
Js.Unsafe.set Js.Unsafe.global (Js.string "__sxR") v; v
|
||||
|
||||
(* ================================================================== *)
|
||||
(* Persistent VM globals — synced with global_env *)
|
||||
(* ================================================================== *)
|
||||
|
||||
(* String-keyed mirror of global_env.bindings for VmClosures.
|
||||
VmClosures from bytecode modules hold vm_env_ref pointing here.
|
||||
Must stay in sync so VmClosures see post-boot definitions. *)
|
||||
let _vm_globals : (string, value) Hashtbl.t = Hashtbl.create 512
|
||||
let _in_batch = ref false
|
||||
|
||||
(* Sync env→VM: copy all bindings from global_env.bindings to _vm_globals.
|
||||
Called after CEK eval/load so VmClosures can see new definitions. *)
|
||||
let sync_env_to_vm () =
|
||||
Hashtbl.iter (fun id v ->
|
||||
Hashtbl.replace _vm_globals (unintern id) v
|
||||
) global_env.bindings
|
||||
|
||||
(* Hook: intercept env_bind on global_env to also update _vm_globals.
|
||||
This ensures VmClosures see new definitions immediately, even during
|
||||
a single boot-init call that loads page scripts and components. *)
|
||||
let () =
|
||||
Sx_types._env_bind_hook := Some (fun env name v ->
|
||||
if env == global_env then
|
||||
Hashtbl.replace _vm_globals name v)
|
||||
|
||||
(* ================================================================== *)
|
||||
(* Core API *)
|
||||
(* ================================================================== *)
|
||||
@@ -207,6 +241,7 @@ let api_eval src_js =
|
||||
let exprs = Sx_parser.parse_all src in
|
||||
let env = Env global_env in
|
||||
let result = List.fold_left (fun _acc expr -> Sx_ref.eval_expr expr env) Nil exprs in
|
||||
sync_env_to_vm ();
|
||||
return_via_side_channel (value_to_js result)
|
||||
with
|
||||
| Eval_error msg -> Js.Unsafe.inject (Js.string ("Error: " ^ msg))
|
||||
@@ -215,7 +250,9 @@ let api_eval src_js =
|
||||
let api_eval_expr expr_js _env_js =
|
||||
let expr = js_to_value expr_js in
|
||||
try
|
||||
return_via_side_channel (value_to_js (Sx_ref.eval_expr expr (Env global_env)))
|
||||
let result = Sx_ref.eval_expr expr (Env global_env) in
|
||||
sync_env_to_vm ();
|
||||
return_via_side_channel (value_to_js result)
|
||||
with Eval_error msg ->
|
||||
Js.Unsafe.inject (Js.string ("Error: " ^ msg))
|
||||
|
||||
@@ -226,56 +263,66 @@ let api_load src_js =
|
||||
let env = Env global_env in
|
||||
let count = ref 0 in
|
||||
List.iter (fun expr -> ignore (Sx_ref.eval_expr expr env); incr count) exprs;
|
||||
sync_env_to_vm ();
|
||||
Js.Unsafe.inject !count
|
||||
with
|
||||
| Eval_error msg -> Js.Unsafe.inject (Js.string ("Error: " ^ msg))
|
||||
| Parse_error msg -> Js.Unsafe.inject (Js.string ("Parse error: " ^ msg))
|
||||
|
||||
(* Shared globals table for batch module loading.
|
||||
Created by beginModuleLoad, accumulated across loadModule calls,
|
||||
flushed to env by endModuleLoad. Ensures closures from early
|
||||
modules can see definitions from later modules. *)
|
||||
let _module_globals : (string, value) Hashtbl.t option ref = ref None
|
||||
|
||||
let api_begin_module_load () =
|
||||
let g = Hashtbl.create 512 in
|
||||
Hashtbl.iter (fun id v -> Hashtbl.replace g (unintern id) v) global_env.bindings;
|
||||
_module_globals := Some g;
|
||||
(* Snapshot current env into the persistent VM globals table *)
|
||||
Hashtbl.clear _vm_globals;
|
||||
Hashtbl.iter (fun id v -> Hashtbl.replace _vm_globals (unintern id) v) global_env.bindings;
|
||||
_in_batch := true;
|
||||
Js.Unsafe.inject true
|
||||
|
||||
let api_end_module_load () =
|
||||
(match !_module_globals with
|
||||
| Some g ->
|
||||
Hashtbl.iter (fun k v ->
|
||||
Hashtbl.replace global_env.bindings (intern k) v
|
||||
) g;
|
||||
_module_globals := None
|
||||
| None -> ());
|
||||
if !_in_batch then begin
|
||||
(* Copy VM globals back to env (bytecode modules defined new symbols) *)
|
||||
Hashtbl.iter (fun k v ->
|
||||
Hashtbl.replace global_env.bindings (intern k) v
|
||||
) _vm_globals;
|
||||
_in_batch := false
|
||||
end;
|
||||
Js.Unsafe.inject true
|
||||
|
||||
let sync_vm_to_env () =
|
||||
Hashtbl.iter (fun name v ->
|
||||
let id = intern name in
|
||||
if not (Hashtbl.mem global_env.bindings id) then
|
||||
Hashtbl.replace global_env.bindings id v
|
||||
else begin
|
||||
(* Update existing binding if the VM has a newer value *)
|
||||
let existing = Hashtbl.find global_env.bindings id in
|
||||
match existing, v with
|
||||
| VmClosure _, VmClosure _ -> Hashtbl.replace global_env.bindings id v
|
||||
| _, VmClosure _ -> Hashtbl.replace global_env.bindings id v
|
||||
| _ -> ()
|
||||
end
|
||||
) _vm_globals
|
||||
|
||||
let api_load_module module_js =
|
||||
try
|
||||
let code_val = js_to_value module_js in
|
||||
let code = Sx_vm.code_from_value code_val in
|
||||
let globals = match !_module_globals with
|
||||
| Some g -> g (* use shared table *)
|
||||
| None ->
|
||||
(* standalone mode: create temp table *)
|
||||
let g = Hashtbl.create 256 in
|
||||
Hashtbl.iter (fun id v -> Hashtbl.replace g (unintern id) v) global_env.bindings;
|
||||
g
|
||||
in
|
||||
let _result = Sx_vm.execute_module code globals in
|
||||
(* If standalone (no batch), copy back immediately *)
|
||||
if !_module_globals = None then
|
||||
Hashtbl.iter (fun k v ->
|
||||
Hashtbl.replace global_env.bindings (intern k) v
|
||||
) globals;
|
||||
Js.Unsafe.inject (Hashtbl.length globals)
|
||||
let _result = Sx_vm.execute_module code _vm_globals in
|
||||
sync_vm_to_env ();
|
||||
Js.Unsafe.inject (Hashtbl.length _vm_globals)
|
||||
with
|
||||
| Eval_error msg -> Js.Unsafe.inject (Js.string ("Error: " ^ msg))
|
||||
| exn -> Js.Unsafe.inject (Js.string ("Error: " ^ Printexc.to_string exn))
|
||||
|
||||
let api_debug_env name_js =
|
||||
let name = Js.to_string name_js in
|
||||
let id = intern name in
|
||||
let found_env = Hashtbl.find_opt global_env.bindings id in
|
||||
let found_vm = Hashtbl.find_opt _vm_globals name in
|
||||
let total_env = Hashtbl.length global_env.bindings in
|
||||
let total_vm = Hashtbl.length _vm_globals in
|
||||
let env_s = match found_env with Some v -> "env:" ^ type_of v | None -> "env:MISSING" in
|
||||
let vm_s = match found_vm with Some v -> "vm:" ^ type_of v | None -> "vm:MISSING" in
|
||||
Js.Unsafe.inject (Js.string (Printf.sprintf "%s %s (env=%d vm=%d)" env_s vm_s total_env total_vm))
|
||||
|
||||
let api_compile_module src_js =
|
||||
let src = Js.to_string src_js in
|
||||
try
|
||||
@@ -318,7 +365,9 @@ let api_register_native name_js callback_js =
|
||||
let js_args = args |> List.map value_to_js |> Array.of_list in
|
||||
js_to_value (Js.Unsafe.fun_call callback_js [| Js.Unsafe.inject (Js.array js_args) |])
|
||||
in
|
||||
ignore (env_bind global_env name (NativeFn (name, native_fn)));
|
||||
let v = NativeFn (name, native_fn) in
|
||||
ignore (env_bind global_env name v);
|
||||
Hashtbl.replace _vm_globals name v;
|
||||
Js.Unsafe.inject Js.null
|
||||
|
||||
let api_call_fn fn_js args_js =
|
||||
@@ -627,18 +676,12 @@ let _jit_compiling = ref false
|
||||
let _jit_enabled = ref false
|
||||
|
||||
let () =
|
||||
(* Convert int-keyed env.bindings to string-keyed Hashtbl for VM globals *)
|
||||
let env_to_vm_globals env =
|
||||
let g = Hashtbl.create (Hashtbl.length env.bindings) in
|
||||
Hashtbl.iter (fun id v -> Hashtbl.replace g (unintern id) v) env.bindings;
|
||||
g
|
||||
in
|
||||
Sx_ref.jit_call_hook := Some (fun f args ->
|
||||
match f with
|
||||
| Lambda l when !_jit_enabled ->
|
||||
(match l.l_compiled with
|
||||
| Some cl when not (Sx_vm.is_jit_failed cl) ->
|
||||
(try Some (Sx_vm.call_closure cl args cl.vm_env_ref)
|
||||
(try Some (Sx_vm.call_closure cl args _vm_globals)
|
||||
with Eval_error msg ->
|
||||
let fn_name = match l.l_name with Some n -> n | None -> "?" in
|
||||
Printf.eprintf "[jit] DISABLED %s — %s\n%!" fn_name msg;
|
||||
@@ -649,13 +692,12 @@ let () =
|
||||
if !_jit_compiling then None
|
||||
else begin
|
||||
_jit_compiling := true;
|
||||
let globals = env_to_vm_globals global_env in
|
||||
let compiled = Sx_vm.jit_compile_lambda l globals in
|
||||
let compiled = Sx_vm.jit_compile_lambda l _vm_globals in
|
||||
_jit_compiling := false;
|
||||
(match compiled with
|
||||
| Some cl ->
|
||||
l.l_compiled <- Some cl;
|
||||
(try Some (Sx_vm.call_closure cl args cl.vm_env_ref)
|
||||
(try Some (Sx_vm.call_closure cl args _vm_globals)
|
||||
with _ ->
|
||||
l.l_compiled <- Some Sx_vm.jit_failed_sentinel;
|
||||
None)
|
||||
@@ -693,5 +735,6 @@ let () =
|
||||
Js.Unsafe.set sx (Js.string "callFn") (wrap api_call_fn);
|
||||
Js.Unsafe.set sx (Js.string "isCallable") (Js.wrap_callback api_is_callable);
|
||||
Js.Unsafe.set sx (Js.string "fnArity") (Js.wrap_callback api_fn_arity);
|
||||
Js.Unsafe.set sx (Js.string "debugEnv") (Js.wrap_callback api_debug_env);
|
||||
|
||||
Js.Unsafe.set Js.Unsafe.global (Js.string "SxKernel") sx
|
||||
|
||||
@@ -362,6 +362,7 @@ let () =
|
||||
| [Macro _] | [Thunk _] | [Keyword _] | [Symbol _] -> Number 0.0
|
||||
| _ -> raise (Eval_error (Printf.sprintf "len: %d args"
|
||||
(List.length args))));
|
||||
register "length" (Hashtbl.find primitives "len");
|
||||
register "first" (fun args ->
|
||||
match args with
|
||||
| [List (x :: _)] | [ListRef { contents = x :: _ }] -> x
|
||||
|
||||
@@ -148,7 +148,11 @@ and vm_call vm f args =
|
||||
(* Cached bytecode — run on VM using the closure's captured env,
|
||||
not the caller's globals. Closure vars were merged at compile time. *)
|
||||
(try push vm (call_closure cl args cl.vm_env_ref)
|
||||
with _ -> push vm (Sx_ref.cek_call f (List args)))
|
||||
with e ->
|
||||
let msg = match e with Eval_error m -> m | e -> Printexc.to_string e in
|
||||
Printf.eprintf "[vm] JIT call failed for %s: %s — falling back to CEK\n%!"
|
||||
(match l.l_name with Some n -> n | None -> "<anon>") msg;
|
||||
push vm (Sx_ref.cek_call f (List args)))
|
||||
| Some _ ->
|
||||
(* Compile failed — CEK *)
|
||||
push vm (Sx_ref.cek_call f (List args))
|
||||
@@ -161,7 +165,10 @@ and vm_call vm f args =
|
||||
| Some cl ->
|
||||
l.l_compiled <- Some cl;
|
||||
(try push vm (call_closure cl args cl.vm_env_ref)
|
||||
with _ ->
|
||||
with e ->
|
||||
let msg = match e with Eval_error m -> m | e -> Printexc.to_string e in
|
||||
Printf.eprintf "[vm] JIT first-call failed for %s: %s — marking failed, falling back to CEK\n%!"
|
||||
(match l.l_name with Some n -> n | None -> "<anon>") msg;
|
||||
l.l_compiled <- Some jit_failed_sentinel;
|
||||
push vm (Sx_ref.cek_call f (List args)))
|
||||
| None ->
|
||||
@@ -187,8 +194,17 @@ and run vm =
|
||||
| frame :: rest_frames ->
|
||||
let bc = frame.closure.vm_code.vc_bytecode in
|
||||
let consts = frame.closure.vm_code.vc_constants in
|
||||
if frame.ip >= Array.length bc then
|
||||
vm.frames <- [] (* bytecode exhausted — stop *)
|
||||
if frame.ip >= Array.length bc then begin
|
||||
(* Bytecode exhausted without explicit RETURN — pop frame like RETURN *)
|
||||
let fn_name = match frame.closure.vm_name with Some n -> n | None -> "?" in
|
||||
Printf.eprintf "[vm] WARN: bytecode exhausted without RETURN in %s (base=%d sp=%d frames=%d)\n%!"
|
||||
fn_name frame.base vm.sp (List.length rest_frames);
|
||||
let result = if vm.sp > frame.base then pop vm else Nil in
|
||||
vm.frames <- rest_frames;
|
||||
vm.sp <- frame.base;
|
||||
if rest_frames <> [] then push vm result
|
||||
(* If no more frames, result stays on stack for call_closure to pop *)
|
||||
end
|
||||
else begin
|
||||
let saved_ip = frame.ip in
|
||||
let op = bc.(frame.ip) in
|
||||
@@ -612,4 +628,5 @@ let jit_compile_lambda (l : lambda) globals =
|
||||
|
||||
(* Wire up forward references *)
|
||||
let () = jit_compile_ref := jit_compile_lambda
|
||||
let () = _vm_call_closure_ref := (fun cl args -> call_closure cl args cl.vm_env_ref)
|
||||
let () = _vm_call_closure_ref := (fun cl args ->
|
||||
call_closure cl args cl.vm_env_ref)
|
||||
|
||||
172
lib/tests/test-vm-primitives.sx
Normal file
172
lib/tests/test-vm-primitives.sx
Normal file
@@ -0,0 +1,172 @@
|
||||
(define
|
||||
vm-eval
|
||||
(fn
|
||||
(expr)
|
||||
(let
|
||||
((code (compile expr)))
|
||||
(vm-execute-module (code-from-value code) {}))))
|
||||
|
||||
(define
|
||||
vm-eval-with
|
||||
(fn
|
||||
(expr globals)
|
||||
(let
|
||||
((code (compile expr)))
|
||||
(vm-execute-module (code-from-value code) globals))))
|
||||
|
||||
(define
|
||||
try-eval
|
||||
(fn
|
||||
(expr)
|
||||
(try-catch (fn () (vm-eval expr)) (fn (err) (str "Error: " err)))))
|
||||
|
||||
(defsuite
|
||||
"vm-primitive-availability"
|
||||
(deftest
|
||||
"length is available"
|
||||
(assert-equal 3 (vm-eval (quote (length (list 1 2 3))))))
|
||||
(deftest
|
||||
"len is available"
|
||||
(assert-equal 3 (vm-eval (quote (len (list 1 2 3))))))
|
||||
(deftest
|
||||
"first is available"
|
||||
(assert-equal 1 (vm-eval (quote (first (list 1 2 3))))))
|
||||
(deftest
|
||||
"rest is available"
|
||||
(assert-equal (list 2 3) (vm-eval (quote (rest (list 1 2 3))))))
|
||||
(deftest
|
||||
"nth is available"
|
||||
(assert-equal 2 (vm-eval (quote (nth (list 1 2 3) 1)))))
|
||||
(deftest
|
||||
"get is available"
|
||||
(assert-equal "v" (vm-eval (quote (get {:k "v"} :k)))))
|
||||
(deftest
|
||||
"type-of is available"
|
||||
(assert-equal "number" (vm-eval (quote (type-of 42)))))
|
||||
(deftest
|
||||
"str concatenation works"
|
||||
(assert-equal "ab" (vm-eval (quote (str "a" "b")))))
|
||||
(deftest
|
||||
"empty? is available"
|
||||
(assert-equal true (vm-eval (quote (empty? (list))))))
|
||||
(deftest
|
||||
"not is available"
|
||||
(assert-equal true (vm-eval (quote (not false)))))
|
||||
(deftest
|
||||
"append is available"
|
||||
(assert-equal
|
||||
(list 1 2 3)
|
||||
(vm-eval (quote (append (list 1 2) (list 3))))))
|
||||
(deftest
|
||||
"assoc is available"
|
||||
(assert-equal "b" (vm-eval (quote (get (assoc {:a "a"} :a "b") :a)))))
|
||||
(deftest
|
||||
"keys is available"
|
||||
(assert-equal 1 (vm-eval (quote (len (keys {:a 1}))))))
|
||||
(deftest
|
||||
"contains? is available"
|
||||
(assert-equal true (vm-eval (quote (contains? "hello" "ell")))))
|
||||
(deftest
|
||||
"starts-with? is available"
|
||||
(assert-equal true (vm-eval (quote (starts-with? "hello" "hel")))))
|
||||
(deftest
|
||||
"split is available"
|
||||
(assert-equal 3 (vm-eval (quote (len (split "a,b,c" ","))))))
|
||||
(deftest
|
||||
"join is available"
|
||||
(assert-equal "a,b" (vm-eval (quote (join "," (list "a" "b"))))))
|
||||
(deftest
|
||||
"trim is available"
|
||||
(assert-equal "x" (vm-eval (quote (trim " x ")))))
|
||||
(deftest
|
||||
"upper is available"
|
||||
(assert-equal "ABC" (vm-eval (quote (upper "abc"))))))
|
||||
|
||||
(defsuite
|
||||
"vm-cross-module-symbols"
|
||||
(deftest
|
||||
"module B sees module A definition"
|
||||
(let
|
||||
((g (dict)))
|
||||
(vm-eval-with (quote (define helper (fn (x) (+ x 100)))) g)
|
||||
(vm-eval-with (quote (define user (fn (x) (helper x)))) g)
|
||||
(assert-equal 142 (vm-eval-with (quote (user 42)) g))))
|
||||
(deftest
|
||||
"module C sees definitions from both A and B"
|
||||
(let
|
||||
((g (dict)))
|
||||
(vm-eval-with (quote (define a (fn (x) (* x 2)))) g)
|
||||
(vm-eval-with (quote (define b (fn (x) (+ (a x) 1)))) g)
|
||||
(vm-eval-with (quote (define c (fn (x) (b (a x))))) g)
|
||||
(assert-equal 21 (vm-eval-with (quote (c 5)) g))))
|
||||
(deftest
|
||||
"late-defined symbol visible to earlier closure"
|
||||
(let
|
||||
((g (dict)))
|
||||
(vm-eval-with (quote (define caller (fn () (callee 10)))) g)
|
||||
(vm-eval-with (quote (define callee (fn (x) (* x 3)))) g)
|
||||
(assert-equal 30 (vm-eval-with (quote (caller)) g)))))
|
||||
|
||||
(defsuite
|
||||
"vm-callback-dispatch"
|
||||
(deftest
|
||||
"simple closure callback"
|
||||
(let
|
||||
((g (dict)))
|
||||
(vm-eval-with
|
||||
(quote (define make-fn (fn (n) (fn (x) (+ n x)))))
|
||||
g)
|
||||
(let
|
||||
((result (vm-eval-with (quote (let ((f (make-fn 10))) (f 5))) g)))
|
||||
(assert-equal 15 result))))
|
||||
(deftest
|
||||
"closure capturing multiple vars"
|
||||
(let
|
||||
((g (dict)))
|
||||
(vm-eval-with
|
||||
(quote (define make-adder (fn (a b) (fn (x) (+ a b x)))))
|
||||
g)
|
||||
(assert-equal 60 (vm-eval-with (quote ((make-adder 10 20) 30)) g))))
|
||||
(deftest
|
||||
"nested closure callback"
|
||||
(let
|
||||
((g (dict)))
|
||||
(vm-eval-with
|
||||
(quote (define wrap (fn (f) (fn (x) (f (f x))))))
|
||||
g)
|
||||
(vm-eval-with (quote (define inc (fn (x) (+ x 1)))) g)
|
||||
(assert-equal 12 (vm-eval-with (quote ((wrap inc) 10)) g)))))
|
||||
|
||||
(defsuite
|
||||
"vm-error-surfacing"
|
||||
(deftest
|
||||
"undefined symbol in compiled fn raises error"
|
||||
(let
|
||||
((g (dict)) (ok false))
|
||||
(vm-eval-with (quote (define good-fn (fn () 42))) g)
|
||||
(set! ok (= 42 (vm-eval-with (quote (good-fn)) g)))
|
||||
(assert-equal true ok)))
|
||||
(deftest
|
||||
"compiled fn calling another compiled fn works"
|
||||
(let
|
||||
((g (dict)))
|
||||
(vm-eval-with (quote (define a (fn (x) (+ x 1)))) g)
|
||||
(vm-eval-with (quote (define b (fn (x) (a x)))) g)
|
||||
(assert-equal 6 (vm-eval-with (quote (b 5)) g))))
|
||||
(deftest
|
||||
"when block executes all body forms"
|
||||
(assert-equal
|
||||
30
|
||||
(vm-eval
|
||||
(quote
|
||||
(let ((x 0)) (when true (set! x 10) (set! x (+ x 20))) x)))))
|
||||
(deftest
|
||||
"when block: function call then side effect"
|
||||
(assert-equal
|
||||
50
|
||||
(vm-eval
|
||||
(quote
|
||||
(let
|
||||
((x 0) (f (fn (n) (* n 10))))
|
||||
(when true (f 1) (set! x (f 5)))
|
||||
x))))))
|
||||
1199
lib/tests/test-vm.sx
1199
lib/tests/test-vm.sx
File diff suppressed because it is too large
Load Diff
@@ -257,6 +257,7 @@
|
||||
console.log("[sx-platform] ok " + path + " (bytecode)");
|
||||
return true;
|
||||
} catch(e) {
|
||||
console.error("[sx-platform] bytecode EXCEPTION " + path + ":", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -329,18 +330,23 @@
|
||||
];
|
||||
|
||||
var loaded = 0, bcCount = 0, srcCount = 0;
|
||||
if (K.beginModuleLoad) K.beginModuleLoad();
|
||||
var t0 = performance.now();
|
||||
var forceSrc = (typeof window !== "undefined" && window.location.search.indexOf("nosxbc") !== -1);
|
||||
if (!forceSrc && K.beginModuleLoad) K.beginModuleLoad();
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var r = loadBytecodeFile(files[i]);
|
||||
if (r) { bcCount++; continue; }
|
||||
if (!forceSrc) {
|
||||
var r = loadBytecodeFile(files[i]);
|
||||
if (r) { bcCount++; continue; }
|
||||
}
|
||||
// Bytecode not available — end batch, load source, restart batch
|
||||
if (K.endModuleLoad) K.endModuleLoad();
|
||||
if (!forceSrc && K.endModuleLoad) K.endModuleLoad();
|
||||
r = loadSxFile(files[i]);
|
||||
if (typeof r === "number") { loaded += r; srcCount++; }
|
||||
if (K.beginModuleLoad) K.beginModuleLoad();
|
||||
if (!forceSrc && K.beginModuleLoad) K.beginModuleLoad();
|
||||
}
|
||||
if (K.endModuleLoad) K.endModuleLoad();
|
||||
console.log("[sx-platform] Loaded " + files.length + " files (" + bcCount + " bytecode, " + srcCount + " source, " + loaded + " exprs)");
|
||||
if (!forceSrc && K.endModuleLoad) K.endModuleLoad();
|
||||
var elapsed = Math.round(performance.now() - t0);
|
||||
console.log("[sx-platform] Loaded " + files.length + " files (" + bcCount + " bytecode, " + srcCount + " source, " + loaded + " exprs) in " + elapsed + "ms");
|
||||
return loaded;
|
||||
}
|
||||
|
||||
@@ -358,46 +364,33 @@
|
||||
engine: function() { return K.engine(); },
|
||||
// Boot entry point (called by auto-init or manually)
|
||||
init: function() {
|
||||
if (typeof K.eval === "function") {
|
||||
// Check boot-init exists
|
||||
// Step through boot manually
|
||||
console.log("[sx] init-css-tracking...");
|
||||
K.eval("(init-css-tracking)");
|
||||
console.log("[sx] process-page-scripts...");
|
||||
K.eval("(process-page-scripts)");
|
||||
console.log("[sx] routes after pages:", K.eval("(len _page-routes)"));
|
||||
console.log("[sx] process-sx-scripts...");
|
||||
K.eval("(process-sx-scripts nil)");
|
||||
console.log("[sx] sx-hydrate-elements...");
|
||||
K.eval("(sx-hydrate-elements nil)");
|
||||
console.log("[sx] sx-hydrate-islands...");
|
||||
K.eval("(sx-hydrate-islands nil)");
|
||||
console.log("[sx] process-elements...");
|
||||
K.eval("(process-elements nil)");
|
||||
// Debug islands
|
||||
console.log("[sx] ~home/stepper defined?", K.eval("(type-of ~home/stepper)"));
|
||||
console.log("[sx] ~layouts/header defined?", K.eval("(type-of ~layouts/header)"));
|
||||
// Try manual island query
|
||||
console.log("[sx] manual island query:", K.eval("(len (dom-query-all (dom-body) \"[data-sx-island]\"))"));
|
||||
// Try hydrating again
|
||||
console.log("[sx] retry hydrate-islands...");
|
||||
K.eval("(sx-hydrate-islands nil)");
|
||||
// Check if links are boosted
|
||||
var links = document.querySelectorAll("a[href]");
|
||||
var boosted = 0;
|
||||
for (var i = 0; i < links.length; i++) {
|
||||
if (links[i]._sxBoundboost) boosted++;
|
||||
if (typeof K.eval !== "function") return;
|
||||
var steps = [
|
||||
'(log-info (str "sx-browser " SX_VERSION))',
|
||||
'(init-css-tracking)',
|
||||
'(process-page-scripts)',
|
||||
'(process-sx-scripts nil)',
|
||||
'(sx-hydrate-elements nil)',
|
||||
'(sx-hydrate-islands nil)',
|
||||
'(run-post-render-hooks)',
|
||||
'(process-elements nil)',
|
||||
'(dom-listen (dom-window) "popstate" (fn (e) (handle-popstate 0)))'
|
||||
];
|
||||
var failures = [];
|
||||
for (var i = 0; i < steps.length; i++) {
|
||||
try {
|
||||
var r = K.eval(steps[i]);
|
||||
if (typeof r === "string" && r.indexOf("Error") === 0) {
|
||||
console.error("[sx] boot step " + i + " FAILED: " + steps[i].substring(0, 60), r);
|
||||
failures.push({ step: i, expr: steps[i], error: r });
|
||||
}
|
||||
} catch(e) {
|
||||
console.error("[sx] boot step " + i + " THREW: " + steps[i].substring(0, 60), e);
|
||||
failures.push({ step: i, expr: steps[i], error: String(e) });
|
||||
}
|
||||
console.log("[sx] boosted links:", boosted, "/", links.length);
|
||||
// Check island state
|
||||
var islands = document.querySelectorAll("[data-sx-island]");
|
||||
console.log("[sx] islands:", islands.length);
|
||||
for (var j = 0; j < islands.length; j++) {
|
||||
console.log("[sx] island:", islands[j].getAttribute("data-sx-island"),
|
||||
"hydrated:", !!islands[j]._sxBoundislandhydrated || !!islands[j]["_sxBound" + "island-hydrated"],
|
||||
"children:", islands[j].children.length);
|
||||
}
|
||||
console.log("[sx] boot done");
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
console.error("[sx] BOOT FAILED: " + failures.length + " step(s) errored:", failures.map(function(f) { return "step " + f.step + ": " + f.error; }).join("; "));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -410,8 +403,8 @@
|
||||
var _doInit = function() {
|
||||
loadWebStack();
|
||||
Sx.init();
|
||||
// Enable JIT after all boot code has run
|
||||
setTimeout(function() { K.eval('(enable-jit!)'); }, 0);
|
||||
// JIT disabled for debugging
|
||||
// setTimeout(function() { K.eval('(enable-jit!)'); }, 0);
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,45 +1,434 @@
|
||||
(define HEAD_HOIST_SELECTOR "meta, title, link[rel='canonical'], script[type='application/ld+json']")
|
||||
(define
|
||||
HEAD_HOIST_SELECTOR
|
||||
"meta, title, link[rel='canonical'], script[type='application/ld+json']")
|
||||
|
||||
(define hoist-head-elements-full :effects (mutation io) (fn (root) (let ((els (dom-query-all root HEAD_HOIST_SELECTOR))) (for-each (fn (el) (let ((tag (lower (dom-tag-name el)))) (cond (= tag "title") (do (set-document-title (dom-text-content el)) (dom-remove-child (dom-parent el) el)) (= tag "meta") (do (let ((name (dom-get-attr el "name")) (prop (dom-get-attr el "property"))) (when name (remove-head-element (str "meta[name=\"" name "\"]"))) (when prop (remove-head-element (str "meta[property=\"" prop "\"]")))) (dom-remove-child (dom-parent el) el) (dom-append-to-head el)) (and (= tag "link") (= (dom-get-attr el "rel") "canonical")) (do (remove-head-element "link[rel=\"canonical\"]") (dom-remove-child (dom-parent el) el) (dom-append-to-head el)) :else (do (dom-remove-child (dom-parent el) el) (dom-append-to-head el))))) els))))
|
||||
(define
|
||||
hoist-head-elements-full
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(root)
|
||||
(let
|
||||
((els (dom-query-all root HEAD_HOIST_SELECTOR)))
|
||||
(for-each
|
||||
(fn
|
||||
(el)
|
||||
(let
|
||||
((tag (lower (dom-tag-name el))))
|
||||
(cond
|
||||
(= tag "title")
|
||||
(do
|
||||
(set-document-title (dom-text-content el))
|
||||
(dom-remove-child (dom-parent el) el))
|
||||
(= tag "meta")
|
||||
(do
|
||||
(let
|
||||
((name (dom-get-attr el "name"))
|
||||
(prop (dom-get-attr el "property")))
|
||||
(when
|
||||
name
|
||||
(remove-head-element (str "meta[name=\"" name "\"]")))
|
||||
(when
|
||||
prop
|
||||
(remove-head-element (str "meta[property=\"" prop "\"]"))))
|
||||
(dom-remove-child (dom-parent el) el)
|
||||
(dom-append-to-head el))
|
||||
(and (= tag "link") (= (dom-get-attr el "rel") "canonical"))
|
||||
(do
|
||||
(remove-head-element "link[rel=\"canonical\"]")
|
||||
(dom-remove-child (dom-parent el) el)
|
||||
(dom-append-to-head el))
|
||||
:else (do
|
||||
(dom-remove-child (dom-parent el) el)
|
||||
(dom-append-to-head el)))))
|
||||
els))))
|
||||
|
||||
(define sx-mount :effects (mutation io) (fn (target (source :as string) (extra-env :as dict)) (let ((el (resolve-mount-target target))) (when el (when (empty? (dom-child-list el)) (let ((node (sx-render-with-env source extra-env))) (dom-set-text-content el "") (dom-append el node) (hoist-head-elements-full el))) (process-elements el) (sx-hydrate-elements el) (sx-hydrate-islands el) (run-post-render-hooks)))))
|
||||
(define
|
||||
sx-mount
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(target (source :as string) (extra-env :as dict))
|
||||
(let
|
||||
((el (resolve-mount-target target)))
|
||||
(when
|
||||
el
|
||||
(when
|
||||
(empty? (dom-child-list el))
|
||||
(let
|
||||
((node (sx-render-with-env source extra-env)))
|
||||
(dom-set-text-content el "")
|
||||
(dom-append el node)
|
||||
(hoist-head-elements-full el)))
|
||||
(process-elements el)
|
||||
(sx-hydrate-elements el)
|
||||
(sx-hydrate-islands el)
|
||||
(run-post-render-hooks)))))
|
||||
|
||||
(define resolve-suspense :effects (mutation io) (fn ((id :as string) (sx :as string)) (process-sx-scripts nil) (let ((el (dom-query (str "[data-suspense=\"" id "\"]")))) (if el (do (let ((exprs (parse sx)) (env (get-render-env nil))) (dom-set-text-content el "") (for-each (fn (expr) (dom-append el (render-to-dom expr env nil))) exprs) (process-elements el) (sx-hydrate-elements el) (sx-hydrate-islands el) (run-post-render-hooks) (dom-dispatch el "sx:resolved" {:id id}))) (log-warn (str "resolveSuspense: no element for id=" id))))))
|
||||
(define
|
||||
resolve-suspense
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
((id :as string) (sx :as string))
|
||||
(process-sx-scripts nil)
|
||||
(let
|
||||
((el (dom-query (str "[data-suspense=\"" id "\"]"))))
|
||||
(if
|
||||
el
|
||||
(do
|
||||
(let
|
||||
((exprs (parse sx)) (env (get-render-env nil)))
|
||||
(dom-set-text-content el "")
|
||||
(for-each
|
||||
(fn (expr) (dom-append el (render-to-dom expr env nil)))
|
||||
exprs)
|
||||
(process-elements el)
|
||||
(sx-hydrate-elements el)
|
||||
(sx-hydrate-islands el)
|
||||
(run-post-render-hooks)
|
||||
(dom-dispatch el "sx:resolved" {:id id})))
|
||||
(log-warn (str "resolveSuspense: no element for id=" id))))))
|
||||
|
||||
(define sx-hydrate-elements :effects (mutation io) (fn (root) (let ((els (dom-query-all (or root (dom-body)) "[data-sx]"))) (for-each (fn (el) (when (not (is-processed? el "hydrated")) (mark-processed! el "hydrated") (sx-update-element el nil))) els))))
|
||||
(define
|
||||
sx-hydrate-elements
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(root)
|
||||
(let
|
||||
((els (dom-query-all (or root (dom-body)) "[data-sx]")))
|
||||
(for-each
|
||||
(fn
|
||||
(el)
|
||||
(when
|
||||
(not (is-processed? el "hydrated"))
|
||||
(mark-processed! el "hydrated")
|
||||
(sx-update-element el nil)))
|
||||
els))))
|
||||
|
||||
(define sx-update-element :effects (mutation io) (fn (el new-env) (let ((target (resolve-mount-target el))) (when target (let ((source (dom-get-attr target "data-sx"))) (when source (let ((base-env (parse-env-attr target)) (env (merge-envs base-env new-env))) (let ((node (sx-render-with-env source env))) (dom-set-text-content target "") (dom-append target node) (when new-env (store-env-attr target base-env new-env))))))))))
|
||||
(define
|
||||
sx-update-element
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(el new-env)
|
||||
(let
|
||||
((target (resolve-mount-target el)))
|
||||
(when
|
||||
target
|
||||
(let
|
||||
((source (dom-get-attr target "data-sx")))
|
||||
(when
|
||||
source
|
||||
(let
|
||||
((base-env (parse-env-attr target))
|
||||
(env (merge-envs base-env new-env)))
|
||||
(let
|
||||
((node (sx-render-with-env source env)))
|
||||
(dom-set-text-content target "")
|
||||
(dom-append target node)
|
||||
(when new-env (store-env-attr target base-env new-env))))))))))
|
||||
|
||||
(define sx-render-component :effects (mutation io) (fn ((name :as string) (kwargs :as dict) (extra-env :as dict)) (let ((full-name (if (starts-with? name "~") name (str "~" name)))) (let ((env (get-render-env extra-env)) (comp (env-get env full-name))) (if (not (component? comp)) (error (str "Unknown component: " full-name)) (let ((call-expr (list (make-symbol full-name)))) (for-each (fn ((k :as string)) (append! call-expr (make-keyword (to-kebab k))) (append! call-expr (dict-get kwargs k))) (keys kwargs)) (render-to-dom call-expr env nil)))))))
|
||||
(define
|
||||
sx-render-component
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
((name :as string) (kwargs :as dict) (extra-env :as dict))
|
||||
(let
|
||||
((full-name (if (starts-with? name "~") name (str "~" name))))
|
||||
(let
|
||||
((env (get-render-env extra-env)) (comp (env-get env full-name)))
|
||||
(if
|
||||
(not (component? comp))
|
||||
(error (str "Unknown component: " full-name))
|
||||
(let
|
||||
((call-expr (list (make-symbol full-name))))
|
||||
(for-each
|
||||
(fn
|
||||
((k :as string))
|
||||
(append! call-expr (make-keyword (to-kebab k)))
|
||||
(append! call-expr (dict-get kwargs k)))
|
||||
(keys kwargs))
|
||||
(render-to-dom call-expr env nil)))))))
|
||||
|
||||
(define process-sx-scripts :effects (mutation io) (fn (root) (let ((scripts (query-sx-scripts root))) (for-each (fn (s) (when (not (is-processed? s "script")) (mark-processed! s "script") (let ((text (dom-text-content s))) (cond (dom-has-attr? s "data-components") (process-component-script s text) (or (nil? text) (empty? (trim text))) nil (dom-has-attr? s "data-init") (let ((exprs (sx-parse text))) (for-each (fn (expr) (eval-expr expr (env-extend (dict)))) exprs)) (dom-has-attr? s "data-mount") (let ((mount-sel (dom-get-attr s "data-mount")) (target (dom-query mount-sel))) (when target (sx-mount target text nil))) :else (sx-load-components text))))) scripts))))
|
||||
(define
|
||||
process-sx-scripts
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(root)
|
||||
(let
|
||||
((scripts (query-sx-scripts root)))
|
||||
(for-each
|
||||
(fn
|
||||
(s)
|
||||
(when
|
||||
(not (is-processed? s "script"))
|
||||
(mark-processed! s "script")
|
||||
(let
|
||||
((text (dom-text-content s)))
|
||||
(cond
|
||||
(dom-has-attr? s "data-components")
|
||||
(process-component-script s text)
|
||||
(or (nil? text) (empty? (trim text)))
|
||||
nil
|
||||
(dom-has-attr? s "data-init")
|
||||
(let
|
||||
((exprs (sx-parse text)))
|
||||
(for-each (fn (expr) (cek-eval expr)) exprs))
|
||||
(dom-has-attr? s "data-mount")
|
||||
(let
|
||||
((mount-sel (dom-get-attr s "data-mount"))
|
||||
(target (dom-query mount-sel)))
|
||||
(when target (sx-mount target text nil)))
|
||||
:else (sx-load-components text)))))
|
||||
scripts))))
|
||||
|
||||
(define process-component-script :effects (mutation io) (fn (script (text :as string)) (let ((hash (dom-get-attr script "data-hash"))) (if (nil? hash) (when (and text (not (empty? (trim text)))) (sx-load-components text)) (let ((has-inline (and text (not (empty? (trim text)))))) (let ((cached-hash (local-storage-get "sx-components-hash"))) (if (= cached-hash hash) (if has-inline (do (local-storage-set "sx-components-hash" hash) (local-storage-set "sx-components-src" text) (sx-load-components text) (log-info "components: downloaded (cookie stale)")) (let ((cached (local-storage-get "sx-components-src"))) (if cached (do (sx-load-components cached) (log-info (str "components: cached (" hash ")"))) (do (clear-sx-comp-cookie) (browser-reload))))) (if has-inline (do (local-storage-set "sx-components-hash" hash) (local-storage-set "sx-components-src" text) (sx-load-components text) (log-info (str "components: downloaded (" hash ")"))) (do (local-storage-remove "sx-components-hash") (local-storage-remove "sx-components-src") (clear-sx-comp-cookie) (browser-reload))))) (set-sx-comp-cookie hash))))))
|
||||
(define
|
||||
process-component-script
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(script (text :as string))
|
||||
(let
|
||||
((hash (dom-get-attr script "data-hash")))
|
||||
(if
|
||||
(nil? hash)
|
||||
(when
|
||||
(and text (not (empty? (trim text))))
|
||||
(sx-load-components text))
|
||||
(let
|
||||
((has-inline (and text (not (empty? (trim text))))))
|
||||
(let
|
||||
((cached-hash (local-storage-get "sx-components-hash")))
|
||||
(if
|
||||
(= cached-hash hash)
|
||||
(if
|
||||
has-inline
|
||||
(do
|
||||
(local-storage-set "sx-components-hash" hash)
|
||||
(local-storage-set "sx-components-src" text)
|
||||
(sx-load-components text)
|
||||
(log-info "components: downloaded (cookie stale)"))
|
||||
(let
|
||||
((cached (local-storage-get "sx-components-src")))
|
||||
(if
|
||||
cached
|
||||
(do
|
||||
(sx-load-components cached)
|
||||
(log-info (str "components: cached (" hash ")")))
|
||||
(do (clear-sx-comp-cookie) (browser-reload)))))
|
||||
(if
|
||||
has-inline
|
||||
(do
|
||||
(local-storage-set "sx-components-hash" hash)
|
||||
(local-storage-set "sx-components-src" text)
|
||||
(sx-load-components text)
|
||||
(log-info (str "components: downloaded (" hash ")")))
|
||||
(do
|
||||
(local-storage-remove "sx-components-hash")
|
||||
(local-storage-remove "sx-components-src")
|
||||
(clear-sx-comp-cookie)
|
||||
(browser-reload)))))
|
||||
(set-sx-comp-cookie hash))))))
|
||||
|
||||
(define _page-routes (list))
|
||||
|
||||
(define process-page-scripts :effects (mutation io) (fn () (let ((scripts (query-page-scripts))) (log-info (str "pages: found " (len scripts) " script tags")) (for-each (fn (s) (when (not (is-processed? s "pages")) (mark-processed! s "pages") (let ((text (dom-text-content s))) (log-info (str "pages: script text length=" (if text (len text) 0))) (if (and text (not (empty? (trim text)))) (let ((pages (parse text))) (log-info (str "pages: parsed " (len pages) " entries")) (for-each (fn ((page :as dict)) (append! _page-routes (merge page {:parsed (parse-route-pattern (get page "path"))}))) pages)) (log-warn "pages: script tag is empty"))))) scripts) (log-info (str "pages: " (len _page-routes) " routes loaded")))))
|
||||
(define
|
||||
process-page-scripts
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((scripts (query-page-scripts)))
|
||||
(log-info (str "pages: found " (len scripts) " script tags"))
|
||||
(for-each
|
||||
(fn
|
||||
(s)
|
||||
(when
|
||||
(not (is-processed? s "pages"))
|
||||
(mark-processed! s "pages")
|
||||
(let
|
||||
((text (dom-text-content s)))
|
||||
(log-info
|
||||
(str "pages: script text length=" (if text (len text) 0)))
|
||||
(if
|
||||
(and text (not (empty? (trim text))))
|
||||
(let
|
||||
((pages (parse text)))
|
||||
(log-info (str "pages: parsed " (len pages) " entries"))
|
||||
(for-each
|
||||
(fn
|
||||
((page :as dict))
|
||||
(append! _page-routes (merge page {:parsed (parse-route-pattern (get page "path"))})))
|
||||
pages))
|
||||
(log-warn "pages: script tag is empty")))))
|
||||
scripts)
|
||||
(log-info (str "pages: " (len _page-routes) " routes loaded")))))
|
||||
|
||||
(define sx-hydrate-islands :effects (mutation io) (fn (root) (let ((els (dom-query-all (or root (dom-body)) "[data-sx-island]"))) (log-info (str "sx-hydrate-islands: " (len els) " island(s) in " (if root "subtree" "document"))) (for-each (fn (el) (if (is-processed? el "island-hydrated") (log-info (str " skip (already hydrated): " (dom-get-attr el "data-sx-island"))) (do (log-info (str " hydrating: " (dom-get-attr el "data-sx-island"))) (mark-processed! el "island-hydrated") (hydrate-island el)))) els))))
|
||||
(define
|
||||
sx-hydrate-islands
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(root)
|
||||
(let
|
||||
((els (dom-query-all (or root (dom-body)) "[data-sx-island]")))
|
||||
(log-info
|
||||
(str
|
||||
"sx-hydrate-islands: "
|
||||
(len els)
|
||||
" island(s) in "
|
||||
(if root "subtree" "document")))
|
||||
(for-each
|
||||
(fn
|
||||
(el)
|
||||
(if
|
||||
(is-processed? el "island-hydrated")
|
||||
(log-info
|
||||
(str
|
||||
" skip (already hydrated): "
|
||||
(dom-get-attr el "data-sx-island")))
|
||||
(do
|
||||
(log-info
|
||||
(str " hydrating: " (dom-get-attr el "data-sx-island")))
|
||||
(mark-processed! el "island-hydrated")
|
||||
(hydrate-island el))))
|
||||
els))))
|
||||
|
||||
(define hydrate-island :effects (mutation io) (fn (el) (let ((name (dom-get-attr el "data-sx-island")) (state-sx (or (dom-get-attr el "data-sx-state") "{}"))) (let ((comp-name (str "~" name)) (env (get-render-env nil))) (let ((comp (env-get env comp-name))) (if (not (or (component? comp) (island? comp))) (log-warn (str "hydrate-island: unknown island " comp-name)) (let ((kwargs (or (first (sx-parse state-sx)) {})) (disposers (list)) (local (env-merge (component-closure comp) env))) (for-each (fn ((p :as string)) (env-bind! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil))) (component-params comp)) (let ((body-dom (cek-try (fn () (with-island-scope (fn (disposable) (append! disposers disposable)) (fn () (render-to-dom (component-body comp) local nil)))) (fn (err) (log-warn (str "hydrate-island FAILED: " comp-name " — " err)) (let ((error-el (dom-create-element "div" nil))) (dom-set-attr error-el "class" "sx-island-error") (dom-set-attr error-el "style" "padding:8px;margin:4px 0;border:1px solid #ef4444;border-radius:4px;background:#fef2f2;color:#b91c1c;font-family:monospace;font-size:12px;white-space:pre-wrap") (dom-set-text-content error-el (str "Island error: " comp-name "\n" err)) error-el))))) (dom-set-text-content el "") (dom-append el body-dom) (dom-set-data el "sx-disposers" disposers) (process-elements el) (log-info (str "hydrated island: " comp-name " (" (len disposers) " disposers)"))))))))))
|
||||
(define
|
||||
hydrate-island
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(el)
|
||||
(let
|
||||
((name (dom-get-attr el "data-sx-island"))
|
||||
(state-sx (or (dom-get-attr el "data-sx-state") "{}")))
|
||||
(let
|
||||
((comp-name (str "~" name)) (env (get-render-env nil)))
|
||||
(let
|
||||
((comp (env-get env comp-name)))
|
||||
(if
|
||||
(not (or (component? comp) (island? comp)))
|
||||
(log-warn (str "hydrate-island: unknown island " comp-name))
|
||||
(let
|
||||
((kwargs (or (first (sx-parse state-sx)) {}))
|
||||
(disposers (list))
|
||||
(local (env-merge (component-closure comp) env)))
|
||||
(for-each
|
||||
(fn
|
||||
((p :as string))
|
||||
(env-bind!
|
||||
local
|
||||
p
|
||||
(if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(component-params comp))
|
||||
(let
|
||||
((body-dom (cek-try (fn () (with-island-scope (fn (disposable) (append! disposers disposable)) (fn () (render-to-dom (component-body comp) local nil)))) (fn (err) (log-warn (str "hydrate-island FAILED: " comp-name " — " err)) (let ((error-el (dom-create-element "div" nil))) (dom-set-attr error-el "class" "sx-island-error") (dom-set-attr error-el "style" "padding:8px;margin:4px 0;border:1px solid #ef4444;border-radius:4px;background:#fef2f2;color:#b91c1c;font-family:monospace;font-size:12px;white-space:pre-wrap") (dom-set-text-content error-el (str "Island error: " comp-name "\n" err)) error-el)))))
|
||||
(dom-set-text-content el "")
|
||||
(dom-append el body-dom)
|
||||
(dom-set-data el "sx-disposers" disposers)
|
||||
(process-elements el)
|
||||
(log-info
|
||||
(str
|
||||
"hydrated island: "
|
||||
comp-name
|
||||
" ("
|
||||
(len disposers)
|
||||
" disposers)"))))))))))
|
||||
|
||||
(define dispose-island :effects (mutation io) (fn (el) (let ((disposers (dom-get-data el "sx-disposers"))) (when disposers (for-each (fn ((d :as lambda)) (when (callable? d) (d))) disposers) (dom-set-data el "sx-disposers" nil))) (clear-processed! el "island-hydrated")))
|
||||
(define
|
||||
dispose-island
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(el)
|
||||
(let
|
||||
((disposers (dom-get-data el "sx-disposers")))
|
||||
(when
|
||||
disposers
|
||||
(for-each
|
||||
(fn ((d :as lambda)) (when (callable? d) (d)))
|
||||
disposers)
|
||||
(dom-set-data el "sx-disposers" nil)))
|
||||
(clear-processed! el "island-hydrated")))
|
||||
|
||||
(define dispose-islands-in :effects (mutation io) (fn (root) (when root (let ((islands (dom-query-all root "[data-sx-island]"))) (when (and islands (not (empty? islands))) (let ((to-dispose (filter (fn (el) (not (is-processed? el "island-hydrated"))) islands))) (when (not (empty? to-dispose)) (log-info (str "disposing " (len to-dispose) " island(s)")) (for-each dispose-island to-dispose))))))))
|
||||
(define
|
||||
dispose-islands-in
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(root)
|
||||
(when
|
||||
root
|
||||
(let
|
||||
((islands (dom-query-all root "[data-sx-island]")))
|
||||
(when
|
||||
(and islands (not (empty? islands)))
|
||||
(let
|
||||
((to-dispose (filter (fn (el) (not (is-processed? el "island-hydrated"))) islands)))
|
||||
(when
|
||||
(not (empty? to-dispose))
|
||||
(log-info (str "disposing " (len to-dispose) " island(s)"))
|
||||
(for-each dispose-island to-dispose))))))))
|
||||
|
||||
(define force-dispose-islands-in :effects (mutation io) (fn (root) (when root (let ((islands (dom-query-all root "[data-sx-island]"))) (when (and islands (not (empty? islands))) (log-info (str "force-disposing " (len islands) " island(s)")) (for-each dispose-island islands))))))
|
||||
(define
|
||||
force-dispose-islands-in
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(root)
|
||||
(when
|
||||
root
|
||||
(let
|
||||
((islands (dom-query-all root "[data-sx-island]")))
|
||||
(when
|
||||
(and islands (not (empty? islands)))
|
||||
(log-info (str "force-disposing " (len islands) " island(s)"))
|
||||
(for-each dispose-island islands))))))
|
||||
|
||||
(define *pre-render-hooks* (list))
|
||||
|
||||
(define *post-render-hooks* (list))
|
||||
|
||||
(define register-pre-render-hook :effects (mutation) (fn ((hook-fn :as lambda)) (append! *pre-render-hooks* hook-fn)))
|
||||
(define
|
||||
register-pre-render-hook
|
||||
:effects (mutation)
|
||||
(fn ((hook-fn :as lambda)) (append! *pre-render-hooks* hook-fn)))
|
||||
|
||||
(define register-post-render-hook :effects (mutation) (fn ((hook-fn :as lambda)) (append! *post-render-hooks* hook-fn)))
|
||||
(define
|
||||
register-post-render-hook
|
||||
:effects (mutation)
|
||||
(fn ((hook-fn :as lambda)) (append! *post-render-hooks* hook-fn)))
|
||||
|
||||
(define run-pre-render-hooks :effects (mutation io) (fn () (for-each (fn (hook) (cek-call hook nil)) *pre-render-hooks*)))
|
||||
(define
|
||||
run-pre-render-hooks
|
||||
:effects (mutation io)
|
||||
(fn () (for-each (fn (hook) (cek-call hook nil)) *pre-render-hooks*)))
|
||||
|
||||
(define run-post-render-hooks :effects (mutation io) (fn () (log-info (str "run-post-render-hooks: " (len *post-render-hooks*) " hooks")) (for-each (fn (hook) (log-info (str " hook type: " (type-of hook) " callable: " (callable? hook) " lambda: " (lambda? hook))) (cek-call hook nil)) *post-render-hooks*)))
|
||||
(define
|
||||
run-post-render-hooks
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
()
|
||||
(log-info
|
||||
(str "run-post-render-hooks: " (len *post-render-hooks*) " hooks"))
|
||||
(for-each
|
||||
(fn
|
||||
(hook)
|
||||
(log-info
|
||||
(str
|
||||
" hook type: "
|
||||
(type-of hook)
|
||||
" callable: "
|
||||
(callable? hook)
|
||||
" lambda: "
|
||||
(lambda? hook)))
|
||||
(cek-call hook nil))
|
||||
*post-render-hooks*)))
|
||||
|
||||
(define boot-init :effects (mutation io) (fn () (do (log-info (str "sx-browser " SX_VERSION)) (init-css-tracking) (process-page-scripts) (process-sx-scripts nil) (sx-hydrate-elements nil) (sx-hydrate-islands nil) (run-post-render-hooks) (process-elements nil) (dom-listen (dom-window) "popstate" (fn (e) (handle-popstate 0))))))
|
||||
(define
|
||||
boot-init
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
()
|
||||
(do
|
||||
(log-info (str "sx-browser " SX_VERSION))
|
||||
(init-css-tracking)
|
||||
(process-page-scripts)
|
||||
(process-sx-scripts nil)
|
||||
(sx-hydrate-elements nil)
|
||||
(sx-hydrate-islands nil)
|
||||
(run-post-render-hooks)
|
||||
(process-elements nil)
|
||||
(dom-listen (dom-window) "popstate" (fn (e) (handle-popstate 0))))))
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,425 +1,418 @@
|
||||
;; ==========================================================================
|
||||
;; dom.sx — DOM library functions
|
||||
;;
|
||||
;; All DOM operations expressed using the host FFI primitives:
|
||||
;; host-get — read property from host object
|
||||
;; host-set! — write property on host object
|
||||
;; host-call — call method on host object
|
||||
;; host-new — construct host object
|
||||
;; host-global — access global (window/document/etc.)
|
||||
;; host-callback — wrap SX function as host callback
|
||||
;; host-typeof — check host object type
|
||||
;;
|
||||
;; These are LIBRARY FUNCTIONS — portable, auditable, in-band SX.
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Globals
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define dom-document (fn () (host-global "document")))
|
||||
(define dom-window (fn () (host-global "window")))
|
||||
(define dom-body (fn () (host-get (dom-document) "body")))
|
||||
(define dom-head (fn () (host-get (dom-document) "head")))
|
||||
|
||||
(define dom-window (fn () (host-global "window")))
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Node creation
|
||||
;; --------------------------------------------------------------------------
|
||||
(define dom-body (fn () (host-get (dom-document) "body")))
|
||||
|
||||
(define dom-create-element
|
||||
(fn (tag &rest ns-arg)
|
||||
(let ((ns (if (and ns-arg (not (empty? ns-arg))) (first ns-arg) nil)))
|
||||
(if ns
|
||||
(define dom-head (fn () (host-get (dom-document) "head")))
|
||||
|
||||
(define
|
||||
dom-create-element
|
||||
(fn
|
||||
(tag &rest ns-arg)
|
||||
(let
|
||||
((ns (if (and ns-arg (not (empty? ns-arg))) (first ns-arg) nil)))
|
||||
(if
|
||||
ns
|
||||
(host-call (dom-document) "createElementNS" ns tag)
|
||||
(host-call (dom-document) "createElement" tag)))))
|
||||
|
||||
(define create-text-node
|
||||
(fn (s)
|
||||
(host-call (dom-document) "createTextNode" s)))
|
||||
(define
|
||||
create-text-node
|
||||
(fn (s) (host-call (dom-document) "createTextNode" s)))
|
||||
|
||||
(define create-fragment
|
||||
(fn ()
|
||||
(host-call (dom-document) "createDocumentFragment")))
|
||||
(define
|
||||
create-fragment
|
||||
(fn () (host-call (dom-document) "createDocumentFragment")))
|
||||
|
||||
(define create-comment
|
||||
(fn (text)
|
||||
(host-call (dom-document) "createComment" (or text ""))))
|
||||
(define
|
||||
create-comment
|
||||
(fn (text) (host-call (dom-document) "createComment" (or text ""))))
|
||||
|
||||
(define
|
||||
dom-append
|
||||
(fn
|
||||
(parent child)
|
||||
(when (and parent child) (host-call parent "appendChild" child))))
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Tree manipulation
|
||||
;; --------------------------------------------------------------------------
|
||||
(define
|
||||
dom-prepend
|
||||
(fn
|
||||
(parent child)
|
||||
(when (and parent child) (host-call parent "prepend" child))))
|
||||
|
||||
(define dom-append
|
||||
(fn (parent child)
|
||||
(when (and parent child)
|
||||
(host-call parent "appendChild" child))))
|
||||
(define
|
||||
dom-insert-before
|
||||
(fn
|
||||
(parent child ref)
|
||||
(when (and parent child) (host-call parent "insertBefore" child ref))))
|
||||
|
||||
(define dom-prepend
|
||||
(fn (parent child)
|
||||
(when (and parent child)
|
||||
(host-call parent "prepend" child))))
|
||||
|
||||
(define dom-insert-before
|
||||
(fn (parent child ref)
|
||||
(when (and parent child)
|
||||
(host-call parent "insertBefore" child ref))))
|
||||
|
||||
(define dom-insert-after
|
||||
(fn (ref node)
|
||||
(define
|
||||
dom-insert-after
|
||||
(fn
|
||||
(ref node)
|
||||
"Insert node after ref in the same parent."
|
||||
(let ((parent (host-get ref "parentNode"))
|
||||
(next (host-get ref "nextSibling")))
|
||||
(when parent
|
||||
(if next
|
||||
(let
|
||||
((parent (host-get ref "parentNode"))
|
||||
(next (host-get ref "nextSibling")))
|
||||
(when
|
||||
parent
|
||||
(if
|
||||
next
|
||||
(host-call parent "insertBefore" node next)
|
||||
(host-call parent "appendChild" node))))))
|
||||
|
||||
(define dom-remove
|
||||
(fn (el)
|
||||
(when el (host-call el "remove"))))
|
||||
(define dom-remove (fn (el) (when el (host-call el "remove"))))
|
||||
|
||||
(define dom-is-active-element?
|
||||
(fn (el)
|
||||
(let ((active (host-get (dom-document) "activeElement")))
|
||||
(if (and active el)
|
||||
(identical? el active)
|
||||
false))))
|
||||
(define
|
||||
dom-is-active-element?
|
||||
(fn
|
||||
(el)
|
||||
(let
|
||||
((active (host-get (dom-document) "activeElement")))
|
||||
(if (and active el) (identical? el active) false))))
|
||||
|
||||
(define dom-is-input-element?
|
||||
(fn (el)
|
||||
(let ((tag (upper (or (dom-tag-name el) ""))))
|
||||
(define
|
||||
dom-is-input-element?
|
||||
(fn
|
||||
(el)
|
||||
(let
|
||||
((tag (upper (or (dom-tag-name el) ""))))
|
||||
(or (= tag "INPUT") (= tag "TEXTAREA") (= tag "SELECT")))))
|
||||
|
||||
(define dom-is-child-of?
|
||||
(fn (child parent)
|
||||
(and child parent (host-call parent "contains" child))))
|
||||
(define
|
||||
dom-is-child-of?
|
||||
(fn (child parent) (and child parent (host-call parent "contains" child))))
|
||||
|
||||
(define dom-attr-list
|
||||
(fn (el)
|
||||
;; Return list of (name value) pairs for all attributes on the element.
|
||||
(let ((attrs (host-get el "attributes"))
|
||||
(result (list)))
|
||||
(when attrs
|
||||
(let ((n (host-get attrs "length")))
|
||||
(let loop ((i 0))
|
||||
(when (< i n)
|
||||
(let ((attr (host-call attrs "item" i)))
|
||||
(append! result (list (host-get attr "name") (host-get attr "value"))))
|
||||
(define
|
||||
dom-attr-list
|
||||
(fn
|
||||
(el)
|
||||
(let
|
||||
((attrs (host-get el "attributes")) (result (list)))
|
||||
(when
|
||||
attrs
|
||||
(let
|
||||
((n (host-get attrs "length")))
|
||||
(let
|
||||
loop
|
||||
((i 0))
|
||||
(when
|
||||
(< i n)
|
||||
(let
|
||||
((attr (host-call attrs "item" i)))
|
||||
(append!
|
||||
result
|
||||
(list (host-get attr "name") (host-get attr "value"))))
|
||||
(loop (+ i 1))))))
|
||||
result)))
|
||||
|
||||
(define dom-remove-child
|
||||
(fn (parent child)
|
||||
(when (and parent child)
|
||||
(host-call parent "removeChild" child))))
|
||||
(define
|
||||
dom-remove-child
|
||||
(fn
|
||||
(parent child)
|
||||
(when (and parent child) (host-call parent "removeChild" child))))
|
||||
|
||||
(define dom-replace-child
|
||||
(fn (parent new-child old-child)
|
||||
(when (and parent new-child old-child)
|
||||
(define
|
||||
dom-replace-child
|
||||
(fn
|
||||
(parent new-child old-child)
|
||||
(when
|
||||
(and parent new-child old-child)
|
||||
(host-call parent "replaceChild" new-child old-child))))
|
||||
|
||||
(define dom-clone
|
||||
(fn (node deep)
|
||||
(host-call node "cloneNode" (if (nil? deep) true deep))))
|
||||
(define
|
||||
dom-clone
|
||||
(fn (node deep) (host-call node "cloneNode" (if (nil? deep) true deep))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Queries
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define dom-query
|
||||
(fn (root-or-sel &rest rest)
|
||||
(if (empty? rest)
|
||||
;; Single arg: selector on document
|
||||
(define
|
||||
dom-query
|
||||
(fn
|
||||
(root-or-sel &rest rest)
|
||||
(if
|
||||
(empty? rest)
|
||||
(host-call (dom-document) "querySelector" root-or-sel)
|
||||
;; Two args: root element + selector
|
||||
(host-call root-or-sel "querySelector" (first rest)))))
|
||||
|
||||
(define dom-query-all
|
||||
(fn (root sel)
|
||||
(define
|
||||
dom-query-all
|
||||
(fn
|
||||
(root sel)
|
||||
"Query DOM and return an SX list (not a host NodeList)."
|
||||
(let ((node-list (if (nil? sel)
|
||||
(host-call (dom-document) "querySelectorAll" root)
|
||||
(host-call root "querySelectorAll" sel))))
|
||||
;; Convert NodeList → SX list by indexing
|
||||
(if (nil? node-list)
|
||||
(let
|
||||
((node-list (if (nil? sel) (host-call (dom-document) "querySelectorAll" root) (host-call root "querySelectorAll" sel))))
|
||||
(if
|
||||
(nil? node-list)
|
||||
(list)
|
||||
(let ((n (host-get node-list "length"))
|
||||
(result (list)))
|
||||
(let loop ((i 0))
|
||||
(when (< i n)
|
||||
(let
|
||||
((n (host-get node-list "length")) (result (list)))
|
||||
(let
|
||||
loop
|
||||
((i 0))
|
||||
(when
|
||||
(< i n)
|
||||
(append! result (host-call node-list "item" i))
|
||||
(loop (+ i 1))))
|
||||
result)))))
|
||||
|
||||
(define dom-query-by-id
|
||||
(fn (id)
|
||||
(host-call (dom-document) "getElementById" id)))
|
||||
(define
|
||||
dom-query-by-id
|
||||
(fn (id) (host-call (dom-document) "getElementById" id)))
|
||||
|
||||
(define dom-closest
|
||||
(fn (el sel)
|
||||
(when el (host-call el "closest" sel))))
|
||||
(define dom-closest (fn (el sel) (when el (host-call el "closest" sel))))
|
||||
|
||||
(define dom-matches?
|
||||
(fn (el sel)
|
||||
(if (and el (host-get el "matches"))
|
||||
(host-call el "matches" sel)
|
||||
false)))
|
||||
(define
|
||||
dom-matches?
|
||||
(fn
|
||||
(el sel)
|
||||
(if (and el (host-get el "matches")) (host-call el "matches" sel) false)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Attributes
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define dom-get-attr
|
||||
(fn (el name)
|
||||
(if (and el (host-get el "getAttribute"))
|
||||
(let ((v (host-call el "getAttribute" name)))
|
||||
(if (nil? v) nil v))
|
||||
(define
|
||||
dom-get-attr
|
||||
(fn
|
||||
(el name)
|
||||
(if
|
||||
(and el (host-get el "getAttribute"))
|
||||
(let ((v (host-call el "getAttribute" name))) (if (nil? v) nil v))
|
||||
nil)))
|
||||
|
||||
(define dom-set-attr
|
||||
(fn (el name val)
|
||||
(when (and el (host-get el "setAttribute"))
|
||||
(define
|
||||
dom-set-attr
|
||||
(fn
|
||||
(el name val)
|
||||
(when
|
||||
(and el (host-get el "setAttribute"))
|
||||
(host-call el "setAttribute" name val))))
|
||||
|
||||
(define dom-remove-attr
|
||||
(fn (el name)
|
||||
(when (and el (host-get el "removeAttribute"))
|
||||
(define
|
||||
dom-remove-attr
|
||||
(fn
|
||||
(el name)
|
||||
(when
|
||||
(and el (host-get el "removeAttribute"))
|
||||
(host-call el "removeAttribute" name))))
|
||||
|
||||
(define dom-has-attr?
|
||||
(fn (el name)
|
||||
(if (and el (host-get el "hasAttribute"))
|
||||
(define
|
||||
dom-has-attr?
|
||||
(fn
|
||||
(el name)
|
||||
(if
|
||||
(and el (host-get el "hasAttribute"))
|
||||
(host-call el "hasAttribute" name)
|
||||
false)))
|
||||
|
||||
(define
|
||||
dom-add-class
|
||||
(fn (el cls) (when el (host-call (host-get el "classList") "add" cls))))
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Classes
|
||||
;; --------------------------------------------------------------------------
|
||||
(define
|
||||
dom-remove-class
|
||||
(fn
|
||||
(el cls)
|
||||
(when el (host-call (host-get el "classList") "remove" cls))))
|
||||
|
||||
(define dom-add-class
|
||||
(fn (el cls)
|
||||
(when el
|
||||
(host-call (host-get el "classList") "add" cls))))
|
||||
(define
|
||||
dom-has-class?
|
||||
(fn
|
||||
(el cls)
|
||||
(if el (host-call (host-get el "classList") "contains" cls) false)))
|
||||
|
||||
(define dom-remove-class
|
||||
(fn (el cls)
|
||||
(when el
|
||||
(host-call (host-get el "classList") "remove" cls))))
|
||||
(define dom-text-content (fn (el) (host-get el "textContent")))
|
||||
|
||||
(define dom-has-class?
|
||||
(fn (el cls)
|
||||
(if el
|
||||
(host-call (host-get el "classList") "contains" cls)
|
||||
false)))
|
||||
(define dom-set-text-content (fn (el val) (host-set! el "textContent" val)))
|
||||
|
||||
(define dom-inner-html (fn (el) (host-get el "innerHTML")))
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Content
|
||||
;; --------------------------------------------------------------------------
|
||||
(define dom-set-inner-html (fn (el val) (host-set! el "innerHTML" val)))
|
||||
|
||||
(define dom-text-content
|
||||
(fn (el) (host-get el "textContent")))
|
||||
(define dom-outer-html (fn (el) (host-get el "outerHTML")))
|
||||
|
||||
(define dom-set-text-content
|
||||
(fn (el val) (host-set! el "textContent" val)))
|
||||
(define
|
||||
dom-insert-adjacent-html
|
||||
(fn (el position html) (host-call el "insertAdjacentHTML" position html)))
|
||||
|
||||
(define dom-inner-html
|
||||
(fn (el) (host-get el "innerHTML")))
|
||||
(define dom-get-style (fn (el prop) (host-get (host-get el "style") prop)))
|
||||
|
||||
(define dom-set-inner-html
|
||||
(fn (el val) (host-set! el "innerHTML" val)))
|
||||
|
||||
(define dom-outer-html
|
||||
(fn (el) (host-get el "outerHTML")))
|
||||
|
||||
(define dom-insert-adjacent-html
|
||||
(fn (el position html)
|
||||
(host-call el "insertAdjacentHTML" position html)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Style & properties
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define dom-get-style
|
||||
(fn (el prop)
|
||||
(host-get (host-get el "style") prop)))
|
||||
|
||||
(define dom-set-style
|
||||
(fn (el prop val)
|
||||
(define
|
||||
dom-set-style
|
||||
(fn
|
||||
(el prop val)
|
||||
(host-call (host-get el "style") "setProperty" prop val)))
|
||||
|
||||
(define dom-get-prop
|
||||
(fn (el name) (host-get el name)))
|
||||
(define dom-get-prop (fn (el name) (host-get el name)))
|
||||
|
||||
(define dom-set-prop
|
||||
(fn (el name val) (host-set! el name val)))
|
||||
(define dom-set-prop (fn (el name val) (host-set! el name val)))
|
||||
|
||||
(define
|
||||
dom-tag-name
|
||||
(fn (el) (if el (lower (or (host-get el "tagName") "")) "")))
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Node info
|
||||
;; --------------------------------------------------------------------------
|
||||
(define dom-node-type (fn (el) (host-get el "nodeType")))
|
||||
|
||||
(define dom-tag-name
|
||||
(fn (el)
|
||||
(if el (lower (or (host-get el "tagName") "")) "")))
|
||||
(define dom-node-name (fn (el) (host-get el "nodeName")))
|
||||
|
||||
(define dom-node-type
|
||||
(fn (el) (host-get el "nodeType")))
|
||||
(define dom-id (fn (el) (host-get el "id")))
|
||||
|
||||
(define dom-node-name
|
||||
(fn (el) (host-get el "nodeName")))
|
||||
(define dom-parent (fn (el) (host-get el "parentNode")))
|
||||
|
||||
(define dom-id
|
||||
(fn (el) (host-get el "id")))
|
||||
(define dom-first-child (fn (el) (host-get el "firstChild")))
|
||||
|
||||
(define dom-parent
|
||||
(fn (el) (host-get el "parentNode")))
|
||||
(define dom-next-sibling (fn (el) (host-get el "nextSibling")))
|
||||
|
||||
(define dom-first-child
|
||||
(fn (el) (host-get el "firstChild")))
|
||||
|
||||
(define dom-next-sibling
|
||||
(fn (el) (host-get el "nextSibling")))
|
||||
|
||||
(define dom-child-list
|
||||
(fn (el)
|
||||
(define
|
||||
dom-child-list
|
||||
(fn
|
||||
(el)
|
||||
"Return child nodes as an SX list."
|
||||
(if el
|
||||
(let ((nl (host-get el "childNodes"))
|
||||
(n (host-get nl "length"))
|
||||
(result (list)))
|
||||
(let loop ((i 0))
|
||||
(when (< i n)
|
||||
(if
|
||||
el
|
||||
(let
|
||||
((nl (host-get el "childNodes"))
|
||||
(n (host-get nl "length"))
|
||||
(result (list)))
|
||||
(let
|
||||
loop
|
||||
((i 0))
|
||||
(when
|
||||
(< i n)
|
||||
(append! result (host-call nl "item" i))
|
||||
(loop (+ i 1))))
|
||||
result)
|
||||
(list))))
|
||||
|
||||
(define dom-is-fragment?
|
||||
(fn (el) (= (host-get el "nodeType") 11)))
|
||||
(define dom-is-fragment? (fn (el) (= (host-get el "nodeType") 11)))
|
||||
|
||||
(define dom-child-nodes
|
||||
(fn (el)
|
||||
(define
|
||||
dom-child-nodes
|
||||
(fn
|
||||
(el)
|
||||
"Return child nodes as an SX list."
|
||||
(if el
|
||||
(let ((nl (host-get el "childNodes"))
|
||||
(n (host-get nl "length"))
|
||||
(result (list)))
|
||||
(let loop ((i 0))
|
||||
(when (< i n)
|
||||
(if
|
||||
el
|
||||
(let
|
||||
((nl (host-get el "childNodes"))
|
||||
(n (host-get nl "length"))
|
||||
(result (list)))
|
||||
(let
|
||||
loop
|
||||
((i 0))
|
||||
(when
|
||||
(< i n)
|
||||
(append! result (host-call nl "item" i))
|
||||
(loop (+ i 1))))
|
||||
result)
|
||||
(list))))
|
||||
|
||||
(define dom-remove-children-after
|
||||
(fn (marker)
|
||||
(define
|
||||
dom-remove-children-after
|
||||
(fn
|
||||
(marker)
|
||||
"Remove all siblings after marker node."
|
||||
(let ((parent (dom-parent marker)))
|
||||
(when parent
|
||||
(let loop ()
|
||||
(let ((next (dom-next-sibling marker)))
|
||||
(when next
|
||||
(host-call parent "removeChild" next)
|
||||
(loop))))))))
|
||||
(let
|
||||
((parent (dom-parent marker)))
|
||||
(when
|
||||
parent
|
||||
(let
|
||||
loop
|
||||
()
|
||||
(let
|
||||
((next (dom-next-sibling marker)))
|
||||
(when next (host-call parent "removeChild" next) (loop))))))))
|
||||
|
||||
(define dom-focus
|
||||
(fn (el) (when el (host-call el "focus"))))
|
||||
(define dom-focus (fn (el) (when el (host-call el "focus"))))
|
||||
|
||||
(define dom-parse-html
|
||||
(fn (html)
|
||||
(let ((parser (host-new "DOMParser"))
|
||||
(doc (host-call parser "parseFromString" html "text/html")))
|
||||
(define
|
||||
dom-parse-html
|
||||
(fn
|
||||
(html)
|
||||
(let
|
||||
((parser (host-new "DOMParser"))
|
||||
(doc (host-call parser "parseFromString" html "text/html")))
|
||||
(host-get (host-get doc "body") "childNodes"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Events
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define dom-listen
|
||||
(fn (el event-name handler)
|
||||
(let ((cb (host-callback handler)))
|
||||
(define
|
||||
dom-listen
|
||||
(fn
|
||||
(el event-name handler)
|
||||
(let
|
||||
((cb (host-callback handler)))
|
||||
(host-call el "addEventListener" event-name cb)
|
||||
;; Return cleanup function
|
||||
(fn () (host-call el "removeEventListener" event-name cb)))))
|
||||
|
||||
;; dom-add-listener — addEventListener with optional options
|
||||
;; Used by orchestration.sx: (dom-add-listener el event handler opts)
|
||||
(define dom-add-listener
|
||||
(fn (el event-name handler &rest opts)
|
||||
(let ((cb (host-callback handler)))
|
||||
(if (and opts (not (empty? opts)))
|
||||
(define
|
||||
dom-add-listener
|
||||
(fn
|
||||
(el event-name handler &rest opts)
|
||||
(let
|
||||
((cb (host-callback handler)))
|
||||
(if
|
||||
(and opts (not (empty? opts)))
|
||||
(host-call el "addEventListener" event-name cb (first opts))
|
||||
(host-call el "addEventListener" event-name cb))
|
||||
;; Return cleanup function
|
||||
(fn () (host-call el "removeEventListener" event-name cb)))))
|
||||
|
||||
(define dom-dispatch
|
||||
(fn (el event-name detail)
|
||||
(let ((evt (host-new "CustomEvent" event-name
|
||||
(dict "detail" detail "bubbles" true))))
|
||||
(define
|
||||
dom-dispatch
|
||||
(fn
|
||||
(el event-name detail)
|
||||
(let
|
||||
((evt (host-new "CustomEvent" event-name (dict "detail" detail "bubbles" true))))
|
||||
(host-call el "dispatchEvent" evt))))
|
||||
|
||||
(define event-detail
|
||||
(fn (evt) (host-get evt "detail")))
|
||||
(define event-detail (fn (evt) (host-get evt "detail")))
|
||||
|
||||
(define prevent-default
|
||||
(fn (e) (when e (host-call e "preventDefault"))))
|
||||
(define prevent-default (fn (e) (when e (host-call e "preventDefault"))))
|
||||
|
||||
(define stop-propagation
|
||||
(fn (e) (when e (host-call e "stopPropagation"))))
|
||||
(define stop-propagation (fn (e) (when e (host-call e "stopPropagation"))))
|
||||
|
||||
(define event-modifier-key?
|
||||
(fn (e)
|
||||
(and e (or (host-get e "ctrlKey") (host-get e "metaKey")
|
||||
(host-get e "shiftKey") (host-get e "altKey")))))
|
||||
(define
|
||||
event-modifier-key?
|
||||
(fn
|
||||
(e)
|
||||
(and
|
||||
e
|
||||
(or
|
||||
(host-get e "ctrlKey")
|
||||
(host-get e "metaKey")
|
||||
(host-get e "shiftKey")
|
||||
(host-get e "altKey")))))
|
||||
|
||||
(define element-value
|
||||
(fn (el)
|
||||
(if (and el (not (nil? (host-get el "value"))))
|
||||
(define
|
||||
element-value
|
||||
(fn
|
||||
(el)
|
||||
(if
|
||||
(and el (not (nil? (host-get el "value"))))
|
||||
(host-get el "value")
|
||||
nil)))
|
||||
|
||||
(define error-message
|
||||
(fn (e)
|
||||
(if (and e (host-get e "message"))
|
||||
(host-get e "message")
|
||||
(str e))))
|
||||
(define
|
||||
error-message
|
||||
(fn
|
||||
(e)
|
||||
(if (and e (host-get e "message")) (host-get e "message") (str e))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; DOM data storage
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define dom-get-data
|
||||
(fn (el key)
|
||||
(let ((store (host-get el "__sx_data")))
|
||||
(define
|
||||
dom-get-data
|
||||
(fn
|
||||
(el key)
|
||||
(let
|
||||
((store (host-get el "__sx_data")))
|
||||
(if store (host-get store key) nil))))
|
||||
|
||||
(define dom-set-data
|
||||
(fn (el key val)
|
||||
(when (not (host-get el "__sx_data"))
|
||||
(define
|
||||
dom-set-data
|
||||
(fn
|
||||
(el key val)
|
||||
(when
|
||||
(not (host-get el "__sx_data"))
|
||||
(host-set! el "__sx_data" (dict)))
|
||||
(host-set! (host-get el "__sx_data") key val)))
|
||||
|
||||
(define
|
||||
dom-append-to-head
|
||||
(fn (el) (when (dom-head) (host-call (dom-head) "appendChild" el))))
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Head manipulation
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define dom-append-to-head
|
||||
(fn (el)
|
||||
(when (dom-head)
|
||||
(host-call (dom-head) "appendChild" el))))
|
||||
|
||||
(define set-document-title
|
||||
(fn (title)
|
||||
(host-set! (dom-document) "title" title)))
|
||||
(define
|
||||
set-document-title
|
||||
(fn (title) (host-set! (dom-document) "title" title)))
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"magic":"SXBC","version":1,"hash":"19bca721c37b25b6","module":{"bytecode":[52,1,0,0,128,0,0,5,51,3,0,128,2,0,5,51,5,0,128,4,0,5,51,7,0,128,6,0,5,51,9,0,128,8,0,5,51,11,0,128,10,0,5,51,13,0,128,12,0,5,51,15,0,128,14,0,5,51,17,0,128,16,0,50],"constants":[{"t":"s","v":"freeze-registry"},{"t":"s","v":"dict"},{"t":"s","v":"freeze-signal"},{"t":"code","v":{"bytecode":[1,1,0,2,52,0,0,2,17,2,16,2,33,56,0,20,3,0,16,2,52,2,0,2,6,34,5,0,5,52,4,0,0,17,3,20,5,0,16,3,1,7,0,16,0,1,8,0,16,1,52,6,0,4,48,2,5,20,3,0,16,2,16,3,52,9,0,3,32,1,0,2,50],"constants":[{"t":"s","v":"context"},{"t":"s","v":"sx-freeze-scope"},{"t":"s","v":"get"},{"t":"s","v":"freeze-registry"},{"t":"s","v":"list"},{"t":"s","v":"append!"},{"t":"s","v":"dict"},{"t":"s","v":"name"},{"t":"s","v":"signal"},{"t":"s","v":"dict-set!"}]}},{"t":"s","v":"freeze-scope"},{"t":"code","v":{"bytecode":[1,1,0,16,0,52,0,0,2,5,20,3,0,16,0,52,4,0,0,52,2,0,3,5,20,5,0,16,1,2,48,2,5,1,1,0,52,6,0,1,5,2,50],"constants":[{"t":"s","v":"scope-push!"},{"t":"s","v":"sx-freeze-scope"},{"t":"s","v":"dict-set!"},{"t":"s","v":"freeze-registry"},{"t":"s","v":"list"},{"t":"s","v":"cek-call"},{"t":"s","v":"scope-pop!"}]}},{"t":"s","v":"cek-freeze-scope"},{"t":"code","v":{"bytecode":[20,1,0,16,0,52,0,0,2,6,34,5,0,5,52,2,0,0,17,1,52,3,0,0,17,2,51,5,0,1,2,16,1,52,4,0,2,5,1,6,0,16,0,1,7,0,16,2,52,3,0,4,50],"constants":[{"t":"s","v":"get"},{"t":"s","v":"freeze-registry"},{"t":"s","v":"list"},{"t":"s","v":"dict"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[18,0,16,0,1,2,0,52,1,0,2,20,3,0,16,0,1,4,0,52,1,0,2,48,1,52,0,0,3,50],"constants":[{"t":"s","v":"dict-set!"},{"t":"s","v":"get"},{"t":"s","v":"name"},{"t":"s","v":"signal-value"},{"t":"s","v":"signal"}]}},{"t":"s","v":"name"},{"t":"s","v":"signals"}]}},{"t":"s","v":"cek-freeze-all"},{"t":"code","v":{"bytecode":[51,1,0,20,3,0,52,2,0,1,52,0,0,2,50],"constants":[{"t":"s","v":"map"},{"t":"code","v":{"bytecode":[20,0,0,16,0,49,1,50],"constants":[{"t":"s","v":"cek-freeze-scope"}]}},{"t":"s","v":"keys"},{"t":"s","v":"freeze-registry"}]}},{"t":"s","v":"cek-thaw-scope"},{"t":"code","v":{"bytecode":[20,1,0,16,0,52,0,0,2,6,34,5,0,5,52,2,0,0,17,2,16,1,1,3,0,52,0,0,2,17,3,16,3,33,14,0,51,5,0,1,3,16,2,52,4,0,2,32,1,0,2,50],"constants":[{"t":"s","v":"get"},{"t":"s","v":"freeze-registry"},{"t":"s","v":"list"},{"t":"s","v":"signals"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[16,0,1,1,0,52,0,0,2,17,1,16,0,1,2,0,52,0,0,2,17,2,18,0,16,1,52,0,0,2,17,3,16,3,52,4,0,1,52,3,0,1,33,12,0,20,5,0,16,2,16,3,49,2,32,1,0,2,50],"constants":[{"t":"s","v":"get"},{"t":"s","v":"name"},{"t":"s","v":"signal"},{"t":"s","v":"not"},{"t":"s","v":"nil?"},{"t":"s","v":"reset!"}]}}]}},{"t":"s","v":"cek-thaw-all"},{"t":"code","v":{"bytecode":[51,1,0,16,0,52,0,0,2,50],"constants":[{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,2,0,52,1,0,2,16,0,49,2,50],"constants":[{"t":"s","v":"cek-thaw-scope"},{"t":"s","v":"get"},{"t":"s","v":"name"}]}}]}},{"t":"s","v":"freeze-to-sx"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,16,0,48,1,49,1,50],"constants":[{"t":"s","v":"sx-serialize"},{"t":"s","v":"cek-freeze-scope"}]}},{"t":"s","v":"thaw-from-sx"},{"t":"code","v":{"bytecode":[20,0,0,16,0,48,1,17,1,16,1,52,2,0,1,52,1,0,1,33,27,0,16,1,52,3,0,1,17,2,20,4,0,16,2,1,6,0,52,5,0,2,16,2,49,2,32,1,0,2,50],"constants":[{"t":"s","v":"sx-parse"},{"t":"s","v":"not"},{"t":"s","v":"empty?"},{"t":"s","v":"first"},{"t":"s","v":"cek-thaw-scope"},{"t":"s","v":"get"},{"t":"s","v":"name"}]}}]}}
|
||||
{"magic":"SXBC","version":1,"hash":"19bca721c37b25b6","module":{"bytecode":[52,1,0,0,128,0,0,5,51,3,0,128,2,0,5,51,5,0,128,4,0,5,51,7,0,128,6,0,5,51,9,0,128,8,0,5,51,11,0,128,10,0,5,51,13,0,128,12,0,5,51,15,0,128,14,0,5,51,17,0,128,16,0,50],"constants":[{"t":"s","v":"freeze-registry"},{"t":"s","v":"dict"},{"t":"s","v":"freeze-signal"},{"t":"code","v":{"bytecode":[1,1,0,2,52,0,0,2,17,2,16,2,33,56,0,20,3,0,16,2,52,2,0,2,6,34,5,0,5,52,4,0,0,17,3,20,5,0,16,3,1,7,0,16,0,1,8,0,16,1,52,6,0,4,48,2,5,20,3,0,16,2,16,3,52,9,0,3,32,1,0,2,50],"constants":[{"t":"s","v":"context"},{"t":"s","v":"sx-freeze-scope"},{"t":"s","v":"get"},{"t":"s","v":"freeze-registry"},{"t":"s","v":"list"},{"t":"s","v":"append!"},{"t":"s","v":"dict"},{"t":"s","v":"name"},{"t":"s","v":"signal"},{"t":"s","v":"dict-set!"}],"arity":2}},{"t":"s","v":"freeze-scope"},{"t":"code","v":{"bytecode":[1,1,0,16,0,52,0,0,2,5,20,3,0,16,0,52,4,0,0,52,2,0,3,5,20,5,0,16,1,2,48,2,5,1,1,0,52,6,0,1,5,2,50],"constants":[{"t":"s","v":"scope-push!"},{"t":"s","v":"sx-freeze-scope"},{"t":"s","v":"dict-set!"},{"t":"s","v":"freeze-registry"},{"t":"s","v":"list"},{"t":"s","v":"cek-call"},{"t":"s","v":"scope-pop!"}],"arity":2}},{"t":"s","v":"cek-freeze-scope"},{"t":"code","v":{"bytecode":[20,1,0,16,0,52,0,0,2,6,34,5,0,5,52,2,0,0,17,1,52,3,0,0,17,2,51,5,0,1,2,16,1,52,4,0,2,5,1,6,0,16,0,1,7,0,16,2,52,3,0,4,50],"constants":[{"t":"s","v":"get"},{"t":"s","v":"freeze-registry"},{"t":"s","v":"list"},{"t":"s","v":"dict"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[18,0,16,0,1,2,0,52,1,0,2,20,3,0,16,0,1,4,0,52,1,0,2,48,1,52,0,0,3,50],"constants":[{"t":"s","v":"dict-set!"},{"t":"s","v":"get"},{"t":"s","v":"name"},{"t":"s","v":"signal-value"},{"t":"s","v":"signal"}],"arity":1,"upvalue-count":1}},{"t":"s","v":"name"},{"t":"s","v":"signals"}],"arity":1}},{"t":"s","v":"cek-freeze-all"},{"t":"code","v":{"bytecode":[51,1,0,20,3,0,52,2,0,1,52,0,0,2,50],"constants":[{"t":"s","v":"map"},{"t":"code","v":{"bytecode":[20,0,0,16,0,49,1,50],"constants":[{"t":"s","v":"cek-freeze-scope"}],"arity":1}},{"t":"s","v":"keys"},{"t":"s","v":"freeze-registry"}]}},{"t":"s","v":"cek-thaw-scope"},{"t":"code","v":{"bytecode":[20,1,0,16,0,52,0,0,2,6,34,5,0,5,52,2,0,0,17,2,16,1,1,3,0,52,0,0,2,17,3,16,3,33,14,0,51,5,0,1,3,16,2,52,4,0,2,32,1,0,2,50],"constants":[{"t":"s","v":"get"},{"t":"s","v":"freeze-registry"},{"t":"s","v":"list"},{"t":"s","v":"signals"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[16,0,1,1,0,52,0,0,2,17,1,16,0,1,2,0,52,0,0,2,17,2,18,0,16,1,52,0,0,2,17,3,16,3,52,4,0,1,52,3,0,1,33,12,0,20,5,0,16,2,16,3,49,2,32,1,0,2,50],"constants":[{"t":"s","v":"get"},{"t":"s","v":"name"},{"t":"s","v":"signal"},{"t":"s","v":"not"},{"t":"s","v":"nil?"},{"t":"s","v":"reset!"}],"arity":1,"upvalue-count":1}}],"arity":2}},{"t":"s","v":"cek-thaw-all"},{"t":"code","v":{"bytecode":[51,1,0,16,0,52,0,0,2,50],"constants":[{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,2,0,52,1,0,2,16,0,49,2,50],"constants":[{"t":"s","v":"cek-thaw-scope"},{"t":"s","v":"get"},{"t":"s","v":"name"}],"arity":1}}],"arity":1}},{"t":"s","v":"freeze-to-sx"},{"t":"code","v":{"bytecode":[20,0,0,20,1,0,16,0,48,1,49,1,50],"constants":[{"t":"s","v":"sx-serialize"},{"t":"s","v":"cek-freeze-scope"}],"arity":1}},{"t":"s","v":"thaw-from-sx"},{"t":"code","v":{"bytecode":[20,0,0,16,0,48,1,17,1,16,1,52,2,0,1,52,1,0,1,33,27,0,16,1,52,3,0,1,17,2,20,4,0,16,2,1,6,0,52,5,0,2,16,2,49,2,32,1,0,2,50],"constants":[{"t":"s","v":"sx-parse"},{"t":"s","v":"not"},{"t":"s","v":"empty?"},{"t":"s","v":"first"},{"t":"s","v":"cek-thaw-scope"},{"t":"s","v":"get"},{"t":"s","v":"name"}],"arity":1}}]}}
|
||||
@@ -1 +1 @@
|
||||
{"magic":"SXBC","version":1,"hash":"93780bb9539e858f","module":{"bytecode":[51,1,0,128,0,0,5,51,3,0,128,2,0,5,51,5,0,128,4,0,5,51,7,0,128,6,0,5,51,9,0,128,8,0,5,51,11,0,128,10,0,5,51,13,0,128,12,0,5,51,15,0,128,14,0,5,51,17,0,128,16,0,5,51,19,0,128,18,0,5,51,21,0,128,20,0,50],"constants":[{"t":"s","v":"assert-signal-value"},{"t":"code","v":{"bytecode":[20,0,0,16,0,48,1,17,2,20,1,0,16,2,16,1,1,3,0,16,1,1,4,0,16,2,52,2,0,4,49,3,50],"constants":[{"t":"s","v":"deref"},{"t":"s","v":"assert="},{"t":"s","v":"str"},{"t":"s","v":"Expected signal value "},{"t":"s","v":", got "}]}},{"t":"s","v":"assert-signal-has-subscribers"},{"t":"code","v":{"bytecode":[20,0,0,20,3,0,16,0,48,1,52,2,0,1,1,4,0,52,1,0,2,1,5,0,49,2,50],"constants":[{"t":"s","v":"assert"},{"t":"s","v":">"},{"t":"s","v":"len"},{"t":"s","v":"signal-subscribers"},{"t":"n","v":0},{"t":"s","v":"Expected signal to have subscribers"}]}},{"t":"s","v":"assert-signal-no-subscribers"},{"t":"code","v":{"bytecode":[20,0,0,20,3,0,16,0,48,1,52,2,0,1,1,4,0,52,1,0,2,1,5,0,49,2,50],"constants":[{"t":"s","v":"assert"},{"t":"s","v":"="},{"t":"s","v":"len"},{"t":"s","v":"signal-subscribers"},{"t":"n","v":0},{"t":"s","v":"Expected signal to have no subscribers"}]}},{"t":"s","v":"assert-signal-subscriber-count"},{"t":"code","v":{"bytecode":[20,1,0,16,0,48,1,52,0,0,1,17,2,20,2,0,16,2,16,1,1,4,0,16,1,1,5,0,16,2,52,3,0,4,49,3,50],"constants":[{"t":"s","v":"len"},{"t":"s","v":"signal-subscribers"},{"t":"s","v":"assert="},{"t":"s","v":"str"},{"t":"s","v":"Expected "},{"t":"s","v":" subscribers, got "}]}},{"t":"s","v":"simulate-signal-set!"},{"t":"code","v":{"bytecode":[20,0,0,16,0,16,1,49,2,50],"constants":[{"t":"s","v":"reset!"}]}},{"t":"s","v":"simulate-signal-swap!"},{"t":"code","v":{"bytecode":[20,1,0,16,0,16,1,16,2,52,2,0,2,52,2,0,2,52,0,0,2,50],"constants":[{"t":"s","v":"apply"},{"t":"s","v":"swap!"},{"t":"s","v":"cons"}]}},{"t":"s","v":"assert-computed-dep-count"},{"t":"code","v":{"bytecode":[20,1,0,16,0,48,1,52,0,0,1,17,2,20,2,0,16,2,16,1,1,4,0,16,1,1,5,0,16,2,52,3,0,4,49,3,50],"constants":[{"t":"s","v":"len"},{"t":"s","v":"signal-deps"},{"t":"s","v":"assert="},{"t":"s","v":"str"},{"t":"s","v":"Expected "},{"t":"s","v":" deps, got "}]}},{"t":"s","v":"assert-computed-depends-on"},{"t":"code","v":{"bytecode":[20,0,0,20,2,0,16,0,48,1,16,1,52,1,0,2,1,3,0,49,2,50],"constants":[{"t":"s","v":"assert"},{"t":"s","v":"contains?"},{"t":"s","v":"signal-deps"},{"t":"s","v":"Expected computed to depend on the given signal"}]}},{"t":"s","v":"count-effect-runs"},{"t":"code","v":{"bytecode":[20,0,0,1,1,0,48,1,17,1,20,2,0,51,3,0,1,1,48,1,5,1,1,0,17,2,20,2,0,51,4,0,1,2,1,0,48,1,17,3,16,2,50],"constants":[{"t":"s","v":"signal"},{"t":"n","v":0},{"t":"s","v":"effect"},{"t":"code","v":{"bytecode":[20,0,0,18,0,49,1,50],"constants":[{"t":"s","v":"deref"}]}},{"t":"code","v":{"bytecode":[18,0,1,1,0,52,0,0,2,19,0,5,20,2,0,18,1,2,49,2,50],"constants":[{"t":"s","v":"+"},{"t":"n","v":1},{"t":"s","v":"cek-call"}]}}]}},{"t":"s","v":"make-test-signal"},{"t":"code","v":{"bytecode":[20,0,0,16,0,48,1,17,1,52,1,0,0,17,2,20,2,0,51,3,0,1,2,1,1,48,1,5,1,0,0,16,1,1,4,0,16,2,65,2,0,50],"constants":[{"t":"s","v":"signal"},{"t":"s","v":"list"},{"t":"s","v":"effect"},{"t":"code","v":{"bytecode":[20,0,0,18,0,20,1,0,18,1,48,1,49,2,50],"constants":[{"t":"s","v":"append!"},{"t":"s","v":"deref"}]}},{"t":"s","v":"history"}]}},{"t":"s","v":"assert-batch-coalesces"},{"t":"code","v":{"bytecode":[1,0,0,17,2,20,1,0,1,0,0,48,1,17,3,20,2,0,51,3,0,1,3,1,2,48,1,5,1,0,0,17,2,5,20,4,0,16,0,48,1,5,20,5,0,16,2,16,1,1,7,0,16,1,1,8,0,16,2,52,6,0,4,49,3,50],"constants":[{"t":"n","v":0},{"t":"s","v":"signal"},{"t":"s","v":"effect"},{"t":"code","v":{"bytecode":[20,0,0,18,0,48,1,5,18,1,1,2,0,52,1,0,2,19,1,50],"constants":[{"t":"s","v":"deref"},{"t":"s","v":"+"},{"t":"n","v":1}]}},{"t":"s","v":"batch"},{"t":"s","v":"assert="},{"t":"s","v":"str"},{"t":"s","v":"Expected "},{"t":"s","v":" notifications, got "}]}}]}}
|
||||
{"magic":"SXBC","version":1,"hash":"93780bb9539e858f","module":{"bytecode":[51,1,0,128,0,0,5,51,3,0,128,2,0,5,51,5,0,128,4,0,5,51,7,0,128,6,0,5,51,9,0,128,8,0,5,51,11,0,128,10,0,5,51,13,0,128,12,0,5,51,15,0,128,14,0,5,51,17,0,128,16,0,5,51,19,0,128,18,0,5,51,21,0,128,20,0,50],"constants":[{"t":"s","v":"assert-signal-value"},{"t":"code","v":{"bytecode":[20,0,0,16,0,48,1,17,2,20,1,0,16,2,16,1,1,3,0,16,1,1,4,0,16,2,52,2,0,4,49,3,50],"constants":[{"t":"s","v":"deref"},{"t":"s","v":"assert="},{"t":"s","v":"str"},{"t":"s","v":"Expected signal value "},{"t":"s","v":", got "}],"arity":2}},{"t":"s","v":"assert-signal-has-subscribers"},{"t":"code","v":{"bytecode":[20,0,0,20,3,0,16,0,48,1,52,2,0,1,1,4,0,52,1,0,2,1,5,0,49,2,50],"constants":[{"t":"s","v":"assert"},{"t":"s","v":">"},{"t":"s","v":"len"},{"t":"s","v":"signal-subscribers"},{"t":"n","v":0},{"t":"s","v":"Expected signal to have subscribers"}],"arity":1}},{"t":"s","v":"assert-signal-no-subscribers"},{"t":"code","v":{"bytecode":[20,0,0,20,3,0,16,0,48,1,52,2,0,1,1,4,0,52,1,0,2,1,5,0,49,2,50],"constants":[{"t":"s","v":"assert"},{"t":"s","v":"="},{"t":"s","v":"len"},{"t":"s","v":"signal-subscribers"},{"t":"n","v":0},{"t":"s","v":"Expected signal to have no subscribers"}],"arity":1}},{"t":"s","v":"assert-signal-subscriber-count"},{"t":"code","v":{"bytecode":[20,1,0,16,0,48,1,52,0,0,1,17,2,20,2,0,16,2,16,1,1,4,0,16,1,1,5,0,16,2,52,3,0,4,49,3,50],"constants":[{"t":"s","v":"len"},{"t":"s","v":"signal-subscribers"},{"t":"s","v":"assert="},{"t":"s","v":"str"},{"t":"s","v":"Expected "},{"t":"s","v":" subscribers, got "}],"arity":2}},{"t":"s","v":"simulate-signal-set!"},{"t":"code","v":{"bytecode":[20,0,0,16,0,16,1,49,2,50],"constants":[{"t":"s","v":"reset!"}],"arity":2}},{"t":"s","v":"simulate-signal-swap!"},{"t":"code","v":{"bytecode":[20,1,0,16,0,16,1,16,2,52,2,0,2,52,2,0,2,52,0,0,2,50],"constants":[{"t":"s","v":"apply"},{"t":"s","v":"swap!"},{"t":"s","v":"cons"}],"arity":3}},{"t":"s","v":"assert-computed-dep-count"},{"t":"code","v":{"bytecode":[20,1,0,16,0,48,1,52,0,0,1,17,2,20,2,0,16,2,16,1,1,4,0,16,1,1,5,0,16,2,52,3,0,4,49,3,50],"constants":[{"t":"s","v":"len"},{"t":"s","v":"signal-deps"},{"t":"s","v":"assert="},{"t":"s","v":"str"},{"t":"s","v":"Expected "},{"t":"s","v":" deps, got "}],"arity":2}},{"t":"s","v":"assert-computed-depends-on"},{"t":"code","v":{"bytecode":[20,0,0,20,2,0,16,0,48,1,16,1,52,1,0,2,1,3,0,49,2,50],"constants":[{"t":"s","v":"assert"},{"t":"s","v":"contains?"},{"t":"s","v":"signal-deps"},{"t":"s","v":"Expected computed to depend on the given signal"}],"arity":2}},{"t":"s","v":"count-effect-runs"},{"t":"code","v":{"bytecode":[20,0,0,1,1,0,48,1,17,1,20,2,0,51,3,0,1,1,48,1,5,1,1,0,17,2,20,2,0,51,4,0,1,2,1,0,48,1,17,3,16,2,50],"constants":[{"t":"s","v":"signal"},{"t":"n","v":0},{"t":"s","v":"effect"},{"t":"code","v":{"bytecode":[20,0,0,18,0,49,1,50],"constants":[{"t":"s","v":"deref"}],"upvalue-count":1}},{"t":"code","v":{"bytecode":[18,0,1,1,0,52,0,0,2,19,0,5,20,2,0,18,1,2,49,2,50],"constants":[{"t":"s","v":"+"},{"t":"n","v":1},{"t":"s","v":"cek-call"}],"upvalue-count":2}}],"arity":1}},{"t":"s","v":"make-test-signal"},{"t":"code","v":{"bytecode":[20,0,0,16,0,48,1,17,1,52,1,0,0,17,2,20,2,0,51,3,0,1,2,1,1,48,1,5,1,0,0,16,1,1,4,0,16,2,65,2,0,50],"constants":[{"t":"s","v":"signal"},{"t":"s","v":"list"},{"t":"s","v":"effect"},{"t":"code","v":{"bytecode":[20,0,0,18,0,20,1,0,18,1,48,1,49,2,50],"constants":[{"t":"s","v":"append!"},{"t":"s","v":"deref"}],"upvalue-count":2}},{"t":"s","v":"history"}],"arity":1}},{"t":"s","v":"assert-batch-coalesces"},{"t":"code","v":{"bytecode":[1,0,0,17,2,20,1,0,1,0,0,48,1,17,3,20,2,0,51,3,0,1,3,1,2,48,1,5,1,0,0,17,2,5,20,4,0,16,0,48,1,5,20,5,0,16,2,16,1,1,7,0,16,1,1,8,0,16,2,52,6,0,4,49,3,50],"constants":[{"t":"n","v":0},{"t":"s","v":"signal"},{"t":"s","v":"effect"},{"t":"code","v":{"bytecode":[20,0,0,18,0,48,1,5,18,1,1,2,0,52,1,0,2,19,1,50],"constants":[{"t":"s","v":"deref"},{"t":"s","v":"+"},{"t":"n","v":1}],"upvalue-count":2}},{"t":"s","v":"batch"},{"t":"s","v":"assert="},{"t":"s","v":"str"},{"t":"s","v":"Expected "},{"t":"s","v":" notifications, got "}],"arity":2}}]}}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"magic":"SXBC","version":1,"hash":"1e908c466d2b8c22","module":{"bytecode":[51,1,0,128,0,0,5,51,3,0,128,2,0,5,52,5,0,0,128,4,0,5,51,7,0,128,6,0,5,51,9,0,128,8,0,5,51,11,0,128,10,0,5,51,13,0,128,12,0,5,51,15,0,128,14,0,5,51,17,0,128,16,0,5,51,19,0,128,18,0,50],"constants":[{"t":"s","v":"with-marsh-scope"},{"t":"code","v":{"bytecode":[52,0,0,0,17,2,20,1,0,51,2,0,1,2,16,1,48,2,5,20,3,0,16,0,1,4,0,16,2,49,3,50],"constants":[{"t":"s","v":"list"},{"t":"s","v":"with-island-scope"},{"t":"code","v":{"bytecode":[20,0,0,18,0,16,0,49,2,50],"constants":[{"t":"s","v":"append!"}]}},{"t":"s","v":"dom-set-data"},{"t":"s","v":"sx-marsh-disposers"}]}},{"t":"s","v":"dispose-marsh-scope"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,1,16,1,33,24,0,51,3,0,16,1,52,2,0,2,5,20,4,0,16,0,1,1,0,2,49,3,32,1,0,2,50],"constants":[{"t":"s","v":"dom-get-data"},{"t":"s","v":"sx-marsh-disposers"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,16,0,2,49,2,50],"constants":[{"t":"s","v":"cek-call"}]}},{"t":"s","v":"dom-set-data"}]}},{"t":"s","v":"*store-registry*"},{"t":"s","v":"dict"},{"t":"s","v":"def-store"},{"t":"code","v":{"bytecode":[20,0,0,17,2,16,2,16,0,52,2,0,2,52,1,0,1,33,22,0,16,2,16,0,20,4,0,16,1,2,48,2,52,3,0,3,21,0,0,32,1,0,2,5,20,0,0,16,0,52,5,0,2,50],"constants":[{"t":"s","v":"*store-registry*"},{"t":"s","v":"not"},{"t":"s","v":"has-key?"},{"t":"s","v":"assoc"},{"t":"s","v":"cek-call"},{"t":"s","v":"get"}]}},{"t":"s","v":"use-store"},{"t":"code","v":{"bytecode":[20,1,0,16,0,52,0,0,2,33,12,0,20,1,0,16,0,52,2,0,2,32,16,0,1,5,0,16,0,1,6,0,52,4,0,3,52,3,0,1,50],"constants":[{"t":"s","v":"has-key?"},{"t":"s","v":"*store-registry*"},{"t":"s","v":"get"},{"t":"s","v":"error"},{"t":"s","v":"str"},{"t":"s","v":"Store not found: "},{"t":"s","v":". Call (def-store ...) before (use-store ...)."}]}},{"t":"s","v":"clear-stores"},{"t":"code","v":{"bytecode":[52,0,0,0,21,1,0,50],"constants":[{"t":"s","v":"dict"},{"t":"s","v":"*store-registry*"}]}},{"t":"s","v":"emit-event"},{"t":"code","v":{"bytecode":[20,0,0,16,0,16,1,16,2,49,3,50],"constants":[{"t":"s","v":"dom-dispatch"}]}},{"t":"s","v":"on-event"},{"t":"code","v":{"bytecode":[20,0,0,16,0,16,1,16,2,49,3,50],"constants":[{"t":"s","v":"dom-on"}]}},{"t":"s","v":"bridge-event"},{"t":"code","v":{"bytecode":[20,0,0,51,1,0,1,0,1,1,1,3,1,2,49,1,50],"constants":[{"t":"s","v":"effect"},{"t":"code","v":{"bytecode":[20,0,0,18,0,18,1,51,1,0,0,2,0,3,48,3,17,0,16,0,50],"constants":[{"t":"s","v":"dom-on"},{"t":"code","v":{"bytecode":[20,0,0,16,0,48,1,17,1,18,0,33,16,0,20,1,0,18,0,16,1,52,2,0,1,48,2,32,2,0,16,1,17,2,20,3,0,18,1,16,2,49,2,50],"constants":[{"t":"s","v":"event-detail"},{"t":"s","v":"cek-call"},{"t":"s","v":"list"},{"t":"s","v":"reset!"}]}}]}}]}},{"t":"s","v":"resource"},{"t":"code","v":{"bytecode":[20,0,0,1,2,0,3,1,3,0,2,1,4,0,2,52,1,0,6,48,1,17,1,20,5,0,20,6,0,16,0,2,48,2,51,7,0,1,1,51,8,0,1,1,48,3,5,16,1,50],"constants":[{"t":"s","v":"signal"},{"t":"s","v":"dict"},{"t":"s","v":"loading"},{"t":"s","v":"data"},{"t":"s","v":"error"},{"t":"s","v":"promise-then"},{"t":"s","v":"cek-call"},{"t":"code","v":{"bytecode":[20,0,0,18,0,1,2,0,4,1,3,0,16,0,1,4,0,2,52,1,0,6,49,2,50],"constants":[{"t":"s","v":"reset!"},{"t":"s","v":"dict"},{"t":"s","v":"loading"},{"t":"s","v":"data"},{"t":"s","v":"error"}]}},{"t":"code","v":{"bytecode":[20,0,0,18,0,1,2,0,4,1,3,0,2,1,4,0,16,0,52,1,0,6,49,2,50],"constants":[{"t":"s","v":"reset!"},{"t":"s","v":"dict"},{"t":"s","v":"loading"},{"t":"s","v":"data"},{"t":"s","v":"error"}]}}]}}]}}
|
||||
{"magic":"SXBC","version":1,"hash":"1e908c466d2b8c22","module":{"bytecode":[51,1,0,128,0,0,5,51,3,0,128,2,0,5,52,5,0,0,128,4,0,5,51,7,0,128,6,0,5,51,9,0,128,8,0,5,51,11,0,128,10,0,5,51,13,0,128,12,0,5,51,15,0,128,14,0,5,51,17,0,128,16,0,5,51,19,0,128,18,0,50],"constants":[{"t":"s","v":"with-marsh-scope"},{"t":"code","v":{"bytecode":[52,0,0,0,17,2,20,1,0,51,2,0,1,2,16,1,48,2,5,20,3,0,16,0,1,4,0,16,2,49,3,50],"constants":[{"t":"s","v":"list"},{"t":"s","v":"with-island-scope"},{"t":"code","v":{"bytecode":[20,0,0,18,0,16,0,49,2,50],"constants":[{"t":"s","v":"append!"}],"arity":1,"upvalue-count":1}},{"t":"s","v":"dom-set-data"},{"t":"s","v":"sx-marsh-disposers"}],"arity":2}},{"t":"s","v":"dispose-marsh-scope"},{"t":"code","v":{"bytecode":[20,0,0,16,0,1,1,0,48,2,17,1,16,1,33,24,0,51,3,0,16,1,52,2,0,2,5,20,4,0,16,0,1,1,0,2,49,3,32,1,0,2,50],"constants":[{"t":"s","v":"dom-get-data"},{"t":"s","v":"sx-marsh-disposers"},{"t":"s","v":"for-each"},{"t":"code","v":{"bytecode":[20,0,0,16,0,2,49,2,50],"constants":[{"t":"s","v":"cek-call"}],"arity":1}},{"t":"s","v":"dom-set-data"}],"arity":1}},{"t":"s","v":"*store-registry*"},{"t":"s","v":"dict"},{"t":"s","v":"def-store"},{"t":"code","v":{"bytecode":[20,0,0,17,2,16,2,16,0,52,2,0,2,52,1,0,1,33,22,0,16,2,16,0,20,4,0,16,1,2,48,2,52,3,0,3,21,0,0,32,1,0,2,5,20,0,0,16,0,52,5,0,2,50],"constants":[{"t":"s","v":"*store-registry*"},{"t":"s","v":"not"},{"t":"s","v":"has-key?"},{"t":"s","v":"assoc"},{"t":"s","v":"cek-call"},{"t":"s","v":"get"}],"arity":2}},{"t":"s","v":"use-store"},{"t":"code","v":{"bytecode":[20,1,0,16,0,52,0,0,2,33,12,0,20,1,0,16,0,52,2,0,2,32,16,0,1,5,0,16,0,1,6,0,52,4,0,3,52,3,0,1,50],"constants":[{"t":"s","v":"has-key?"},{"t":"s","v":"*store-registry*"},{"t":"s","v":"get"},{"t":"s","v":"error"},{"t":"s","v":"str"},{"t":"s","v":"Store not found: "},{"t":"s","v":". Call (def-store ...) before (use-store ...)."}],"arity":1}},{"t":"s","v":"clear-stores"},{"t":"code","v":{"bytecode":[52,0,0,0,21,1,0,50],"constants":[{"t":"s","v":"dict"},{"t":"s","v":"*store-registry*"}]}},{"t":"s","v":"emit-event"},{"t":"code","v":{"bytecode":[20,0,0,16,0,16,1,16,2,49,3,50],"constants":[{"t":"s","v":"dom-dispatch"}],"arity":3}},{"t":"s","v":"on-event"},{"t":"code","v":{"bytecode":[20,0,0,16,0,16,1,16,2,49,3,50],"constants":[{"t":"s","v":"dom-on"}],"arity":3}},{"t":"s","v":"bridge-event"},{"t":"code","v":{"bytecode":[20,0,0,51,1,0,1,0,1,1,1,3,1,2,49,1,50],"constants":[{"t":"s","v":"effect"},{"t":"code","v":{"bytecode":[20,0,0,18,0,18,1,51,1,0,0,2,0,3,48,3,17,0,16,0,50],"constants":[{"t":"s","v":"dom-on"},{"t":"code","v":{"bytecode":[20,0,0,16,0,48,1,17,1,18,0,33,16,0,20,1,0,18,0,16,1,52,2,0,1,48,2,32,2,0,16,1,17,2,20,3,0,18,1,16,2,49,2,50],"constants":[{"t":"s","v":"event-detail"},{"t":"s","v":"cek-call"},{"t":"s","v":"list"},{"t":"s","v":"reset!"}],"arity":1,"upvalue-count":2}}],"upvalue-count":4}}],"arity":4}},{"t":"s","v":"resource"},{"t":"code","v":{"bytecode":[20,0,0,1,2,0,3,1,3,0,2,1,4,0,2,52,1,0,6,48,1,17,1,20,5,0,20,6,0,16,0,2,48,2,51,7,0,1,1,51,8,0,1,1,48,3,5,16,1,50],"constants":[{"t":"s","v":"signal"},{"t":"s","v":"dict"},{"t":"s","v":"loading"},{"t":"s","v":"data"},{"t":"s","v":"error"},{"t":"s","v":"promise-then"},{"t":"s","v":"cek-call"},{"t":"code","v":{"bytecode":[20,0,0,18,0,1,2,0,4,1,3,0,16,0,1,4,0,2,52,1,0,6,49,2,50],"constants":[{"t":"s","v":"reset!"},{"t":"s","v":"dict"},{"t":"s","v":"loading"},{"t":"s","v":"data"},{"t":"s","v":"error"}],"arity":1,"upvalue-count":1}},{"t":"code","v":{"bytecode":[20,0,0,18,0,1,2,0,4,1,3,0,2,1,4,0,16,0,52,1,0,6,49,2,50],"constants":[{"t":"s","v":"reset!"},{"t":"s","v":"dict"},{"t":"s","v":"loading"},{"t":"s","v":"data"},{"t":"s","v":"error"}],"arity":1,"upvalue-count":1}}],"arity":1}}]}}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1792,7 +1792,7 @@
|
||||
blake2_js_for_wasm_create: blake2_js_for_wasm_create};
|
||||
}
|
||||
(globalThis))
|
||||
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["sx-ea36a0db",[2]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,4]],["dune__exe__Sx_browser-6b9c6428",[2,3,5]],["std_exit-10fb8830",[2]],["start-29cf9a72",0]],"generated":(b=>{var
|
||||
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["sx-31fbd690",[2]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,4]],["dune__exe__Sx_browser-c7255f12",[2,3,5]],["std_exit-10fb8830",[2]],["start-29cf9a72",0]],"generated":(b=>{var
|
||||
c=b,a=b?.module?.export||b;return{"env":{"caml_ba_kind_of_typed_array":()=>{throw new
|
||||
Error("caml_ba_kind_of_typed_array not implemented")},"caml_exn_with_js_backtrace":()=>{throw new
|
||||
Error("caml_exn_with_js_backtrace not implemented")},"caml_int64_create_lo_mi_hi":()=>{throw new
|
||||
@@ -1818,4 +1818,4 @@ a()},"Js_of_ocaml__Json.fragments":{"get_JSON":a=>a.JSON,"get_constructor":a=>a.
|
||||
a(b)},"Js_of_ocaml__Dom_svg.fragments":{"get_SVGElement":a=>a.SVGElement,"get_document":a=>a.document,"get_tagName":a=>a.tagName,"meth_call_0_toLowerCase":a=>a.toLowerCase(),"meth_call_1_getElementById":(a,b)=>a.getElementById(b),"meth_call_2_createElementNS":(a,b,c)=>a.createElementNS(b,c)},"Js_of_ocaml__EventSource.fragments":{"get_EventSource":a=>a.EventSource,"obj_9":()=>({}),"set_withCredentials":(a,b)=>a.withCredentials=b},"Js_of_ocaml__Geolocation.fragments":{"get_geolocation":a=>a.geolocation,"get_navigator":a=>a.navigator,"obj_10":()=>({})},"Js_of_ocaml__IntersectionObserver.fragments":{"get_IntersectionObserver":a=>a.IntersectionObserver,"obj_11":()=>({})},"Js_of_ocaml__Intl.fragments":{"get_Collator":a=>a.Collator,"get_DateTimeFormat":a=>a.DateTimeFormat,"get_Intl":a=>a.Intl,"get_NumberFormat":a=>a.NumberFormat,"get_PluralRules":a=>a.PluralRules,"obj_12":a=>({localeMatcher:a}),"obj_13":(a,b,c,d,e,f)=>({localeMatcher:a,usage:b,sensitivity:c,ignorePunctuation:d,numeric:e,caseFirst:f}),"obj_14":(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t)=>({dateStyle:a,timeStyle:b,calendar:c,dayPeriod:d,numberingSystem:e,localeMatcher:f,timeZone:g,hour12:h,hourCycle:i,formatMatcher:j,weekday:k,era:l,year:m,month:n,day:o,hour:p,minute:q,second:r,fractionalSecondDigits:s,timeZoneName:t}),"obj_15":(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u)=>({compactDisplay:a,currency:b,currencyDisplay:c,currencySign:d,localeMatcher:e,notation:f,numberingSystem:g,signDisplay:h,style:i,unit:j,unitDisplay:k,useGrouping:l,roundingMode:m,roundingPriority:n,roundingIncrement:o,trailingZeroDisplay:p,minimumIntegerDigits:q,minimumFractionDigits:r,maximumFractionDigits:s,minimumSignificantDigits:t,maximumSignificantDigits:u}),"obj_16":(a,b)=>({localeMatcher:a,type:b})},"Dune__exe__Sx_browser.fragments":{"fun_call_1":(a,b)=>a(b),"fun_call_3":(a,b,c,d)=>a(b,c,d),"get_Array":a=>a.Array,"get_Object":a=>a.Object,"get___sx_handle":a=>a.__sx_handle,"get__type":a=>a._type,"get_console":a=>a.console,"get_items":a=>a.items,"get_length":a=>a.length,"get_name":a=>a.name,"js_expr_10d25c5c":()=>function(a){return function(){b.__sxR=undefined;var
|
||||
c=a.apply(null,arguments);return b.__sxR!==undefined?b.__sxR:c}},"js_expr_1ab4fffb":()=>function(){var
|
||||
b={},d=0;return{put:function(a){var
|
||||
c=d++;b[c]=a;return c},get:function(a){return b[a]}}}(),"js_expr_36506fc1":()=>function(a,b,c){a.__sx_handle=b;a._type=c;return a},"meth_call_1_error":(a,b)=>a.error(b),"meth_call_1_get":(a,b)=>a.get(b),"meth_call_1_isArray":(a,b)=>a.isArray(b),"meth_call_1_keys":(a,b)=>a.keys(b),"meth_call_1_put":(a,b)=>a.put(b),"obj_0":()=>({}),"obj_1":()=>({}),"obj_2":(a,b)=>({_type:a,items:b}),"obj_3":(a,b)=>({_type:a,name:b}),"obj_4":(a,b)=>({_type:a,name:b}),"obj_5":(a,b)=>({_type:a,__sx_handle:b}),"set_SxKernel":(a,b)=>a.SxKernel=b,"set___sxR":(a,b)=>a.__sxR=b,"set__type":(a,b)=>a._type=b,"set_beginModuleLoad":(a,b)=>a.beginModuleLoad=b,"set_callFn":(a,b)=>a.callFn=b,"set_compileModule":(a,b)=>a.compileModule=b,"set_endModuleLoad":(a,b)=>a.endModuleLoad=b,"set_engine":(a,b)=>a.engine=b,"set_eval":(a,b)=>a.eval=b,"set_evalExpr":(a,b)=>a.evalExpr=b,"set_fnArity":(a,b)=>a.fnArity=b,"set_inspect":(a,b)=>a.inspect=b,"set_isCallable":(a,b)=>a.isCallable=b,"set_load":(a,b)=>a.load=b,"set_loadModule":(a,b)=>a.loadModule=b,"set_loadSource":(a,b)=>a.loadSource=b,"set_parse":(a,b)=>a.parse=b,"set_registerNative":(a,b)=>a.registerNative=b,"set_renderToHtml":(a,b)=>a.renderToHtml=b,"set_stringify":(a,b)=>a.stringify=b,"set_typeOf":(a,b)=>a.typeOf=b}}})(globalThis),"src":"sx_browser.bc.wasm.assets"});
|
||||
c=d++;b[c]=a;return c},get:function(a){return b[a]}}}(),"js_expr_36506fc1":()=>function(a,b,c){a.__sx_handle=b;a._type=c;return a},"meth_call_1_error":(a,b)=>a.error(b),"meth_call_1_get":(a,b)=>a.get(b),"meth_call_1_isArray":(a,b)=>a.isArray(b),"meth_call_1_keys":(a,b)=>a.keys(b),"meth_call_1_put":(a,b)=>a.put(b),"obj_0":()=>({}),"obj_1":()=>({}),"obj_2":(a,b)=>({_type:a,items:b}),"obj_3":(a,b)=>({_type:a,name:b}),"obj_4":(a,b)=>({_type:a,name:b}),"obj_5":(a,b)=>({_type:a,__sx_handle:b}),"set_SxKernel":(a,b)=>a.SxKernel=b,"set___sxR":(a,b)=>a.__sxR=b,"set__type":(a,b)=>a._type=b,"set_beginModuleLoad":(a,b)=>a.beginModuleLoad=b,"set_callFn":(a,b)=>a.callFn=b,"set_compileModule":(a,b)=>a.compileModule=b,"set_debugEnv":(a,b)=>a.debugEnv=b,"set_endModuleLoad":(a,b)=>a.endModuleLoad=b,"set_engine":(a,b)=>a.engine=b,"set_eval":(a,b)=>a.eval=b,"set_evalExpr":(a,b)=>a.evalExpr=b,"set_fnArity":(a,b)=>a.fnArity=b,"set_inspect":(a,b)=>a.inspect=b,"set_isCallable":(a,b)=>a.isCallable=b,"set_load":(a,b)=>a.load=b,"set_loadModule":(a,b)=>a.loadModule=b,"set_loadSource":(a,b)=>a.loadSource=b,"set_parse":(a,b)=>a.parse=b,"set_registerNative":(a,b)=>a.registerNative=b,"set_renderToHtml":(a,b)=>a.renderToHtml=b,"set_stringify":(a,b)=>a.stringify=b,"set_typeOf":(a,b)=>a.typeOf=b}}})(globalThis),"src":"sx_browser.bc.wasm.assets"});
|
||||
|
||||
429
web/boot.sx
429
web/boot.sx
@@ -1,45 +1,434 @@
|
||||
(define HEAD_HOIST_SELECTOR "meta, title, link[rel='canonical'], script[type='application/ld+json']")
|
||||
(define
|
||||
HEAD_HOIST_SELECTOR
|
||||
"meta, title, link[rel='canonical'], script[type='application/ld+json']")
|
||||
|
||||
(define hoist-head-elements-full :effects (mutation io) (fn (root) (let ((els (dom-query-all root HEAD_HOIST_SELECTOR))) (for-each (fn (el) (let ((tag (lower (dom-tag-name el)))) (cond (= tag "title") (do (set-document-title (dom-text-content el)) (dom-remove-child (dom-parent el) el)) (= tag "meta") (do (let ((name (dom-get-attr el "name")) (prop (dom-get-attr el "property"))) (when name (remove-head-element (str "meta[name=\"" name "\"]"))) (when prop (remove-head-element (str "meta[property=\"" prop "\"]")))) (dom-remove-child (dom-parent el) el) (dom-append-to-head el)) (and (= tag "link") (= (dom-get-attr el "rel") "canonical")) (do (remove-head-element "link[rel=\"canonical\"]") (dom-remove-child (dom-parent el) el) (dom-append-to-head el)) :else (do (dom-remove-child (dom-parent el) el) (dom-append-to-head el))))) els))))
|
||||
(define
|
||||
hoist-head-elements-full
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(root)
|
||||
(let
|
||||
((els (dom-query-all root HEAD_HOIST_SELECTOR)))
|
||||
(for-each
|
||||
(fn
|
||||
(el)
|
||||
(let
|
||||
((tag (lower (dom-tag-name el))))
|
||||
(cond
|
||||
(= tag "title")
|
||||
(do
|
||||
(set-document-title (dom-text-content el))
|
||||
(dom-remove-child (dom-parent el) el))
|
||||
(= tag "meta")
|
||||
(do
|
||||
(let
|
||||
((name (dom-get-attr el "name"))
|
||||
(prop (dom-get-attr el "property")))
|
||||
(when
|
||||
name
|
||||
(remove-head-element (str "meta[name=\"" name "\"]")))
|
||||
(when
|
||||
prop
|
||||
(remove-head-element (str "meta[property=\"" prop "\"]"))))
|
||||
(dom-remove-child (dom-parent el) el)
|
||||
(dom-append-to-head el))
|
||||
(and (= tag "link") (= (dom-get-attr el "rel") "canonical"))
|
||||
(do
|
||||
(remove-head-element "link[rel=\"canonical\"]")
|
||||
(dom-remove-child (dom-parent el) el)
|
||||
(dom-append-to-head el))
|
||||
:else (do
|
||||
(dom-remove-child (dom-parent el) el)
|
||||
(dom-append-to-head el)))))
|
||||
els))))
|
||||
|
||||
(define sx-mount :effects (mutation io) (fn (target (source :as string) (extra-env :as dict)) (let ((el (resolve-mount-target target))) (when el (when (empty? (dom-child-list el)) (let ((node (sx-render-with-env source extra-env))) (dom-set-text-content el "") (dom-append el node) (hoist-head-elements-full el))) (process-elements el) (sx-hydrate-elements el) (sx-hydrate-islands el) (run-post-render-hooks)))))
|
||||
(define
|
||||
sx-mount
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(target (source :as string) (extra-env :as dict))
|
||||
(let
|
||||
((el (resolve-mount-target target)))
|
||||
(when
|
||||
el
|
||||
(when
|
||||
(empty? (dom-child-list el))
|
||||
(let
|
||||
((node (sx-render-with-env source extra-env)))
|
||||
(dom-set-text-content el "")
|
||||
(dom-append el node)
|
||||
(hoist-head-elements-full el)))
|
||||
(process-elements el)
|
||||
(sx-hydrate-elements el)
|
||||
(sx-hydrate-islands el)
|
||||
(run-post-render-hooks)))))
|
||||
|
||||
(define resolve-suspense :effects (mutation io) (fn ((id :as string) (sx :as string)) (process-sx-scripts nil) (let ((el (dom-query (str "[data-suspense=\"" id "\"]")))) (if el (do (let ((exprs (parse sx)) (env (get-render-env nil))) (dom-set-text-content el "") (for-each (fn (expr) (dom-append el (render-to-dom expr env nil))) exprs) (process-elements el) (sx-hydrate-elements el) (sx-hydrate-islands el) (run-post-render-hooks) (dom-dispatch el "sx:resolved" {:id id}))) (log-warn (str "resolveSuspense: no element for id=" id))))))
|
||||
(define
|
||||
resolve-suspense
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
((id :as string) (sx :as string))
|
||||
(process-sx-scripts nil)
|
||||
(let
|
||||
((el (dom-query (str "[data-suspense=\"" id "\"]"))))
|
||||
(if
|
||||
el
|
||||
(do
|
||||
(let
|
||||
((exprs (parse sx)) (env (get-render-env nil)))
|
||||
(dom-set-text-content el "")
|
||||
(for-each
|
||||
(fn (expr) (dom-append el (render-to-dom expr env nil)))
|
||||
exprs)
|
||||
(process-elements el)
|
||||
(sx-hydrate-elements el)
|
||||
(sx-hydrate-islands el)
|
||||
(run-post-render-hooks)
|
||||
(dom-dispatch el "sx:resolved" {:id id})))
|
||||
(log-warn (str "resolveSuspense: no element for id=" id))))))
|
||||
|
||||
(define sx-hydrate-elements :effects (mutation io) (fn (root) (let ((els (dom-query-all (or root (dom-body)) "[data-sx]"))) (for-each (fn (el) (when (not (is-processed? el "hydrated")) (mark-processed! el "hydrated") (sx-update-element el nil))) els))))
|
||||
(define
|
||||
sx-hydrate-elements
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(root)
|
||||
(let
|
||||
((els (dom-query-all (or root (dom-body)) "[data-sx]")))
|
||||
(for-each
|
||||
(fn
|
||||
(el)
|
||||
(when
|
||||
(not (is-processed? el "hydrated"))
|
||||
(mark-processed! el "hydrated")
|
||||
(sx-update-element el nil)))
|
||||
els))))
|
||||
|
||||
(define sx-update-element :effects (mutation io) (fn (el new-env) (let ((target (resolve-mount-target el))) (when target (let ((source (dom-get-attr target "data-sx"))) (when source (let ((base-env (parse-env-attr target)) (env (merge-envs base-env new-env))) (let ((node (sx-render-with-env source env))) (dom-set-text-content target "") (dom-append target node) (when new-env (store-env-attr target base-env new-env))))))))))
|
||||
(define
|
||||
sx-update-element
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(el new-env)
|
||||
(let
|
||||
((target (resolve-mount-target el)))
|
||||
(when
|
||||
target
|
||||
(let
|
||||
((source (dom-get-attr target "data-sx")))
|
||||
(when
|
||||
source
|
||||
(let
|
||||
((base-env (parse-env-attr target))
|
||||
(env (merge-envs base-env new-env)))
|
||||
(let
|
||||
((node (sx-render-with-env source env)))
|
||||
(dom-set-text-content target "")
|
||||
(dom-append target node)
|
||||
(when new-env (store-env-attr target base-env new-env))))))))))
|
||||
|
||||
(define sx-render-component :effects (mutation io) (fn ((name :as string) (kwargs :as dict) (extra-env :as dict)) (let ((full-name (if (starts-with? name "~") name (str "~" name)))) (let ((env (get-render-env extra-env)) (comp (env-get env full-name))) (if (not (component? comp)) (error (str "Unknown component: " full-name)) (let ((call-expr (list (make-symbol full-name)))) (for-each (fn ((k :as string)) (append! call-expr (make-keyword (to-kebab k))) (append! call-expr (dict-get kwargs k))) (keys kwargs)) (render-to-dom call-expr env nil)))))))
|
||||
(define
|
||||
sx-render-component
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
((name :as string) (kwargs :as dict) (extra-env :as dict))
|
||||
(let
|
||||
((full-name (if (starts-with? name "~") name (str "~" name))))
|
||||
(let
|
||||
((env (get-render-env extra-env)) (comp (env-get env full-name)))
|
||||
(if
|
||||
(not (component? comp))
|
||||
(error (str "Unknown component: " full-name))
|
||||
(let
|
||||
((call-expr (list (make-symbol full-name))))
|
||||
(for-each
|
||||
(fn
|
||||
((k :as string))
|
||||
(append! call-expr (make-keyword (to-kebab k)))
|
||||
(append! call-expr (dict-get kwargs k)))
|
||||
(keys kwargs))
|
||||
(render-to-dom call-expr env nil)))))))
|
||||
|
||||
(define process-sx-scripts :effects (mutation io) (fn (root) (let ((scripts (query-sx-scripts root))) (for-each (fn (s) (when (not (is-processed? s "script")) (mark-processed! s "script") (let ((text (dom-text-content s))) (cond (dom-has-attr? s "data-components") (process-component-script s text) (or (nil? text) (empty? (trim text))) nil (dom-has-attr? s "data-init") (let ((exprs (sx-parse text))) (for-each (fn (expr) (eval-expr expr (env-extend (dict)))) exprs)) (dom-has-attr? s "data-mount") (let ((mount-sel (dom-get-attr s "data-mount")) (target (dom-query mount-sel))) (when target (sx-mount target text nil))) :else (sx-load-components text))))) scripts))))
|
||||
(define
|
||||
process-sx-scripts
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(root)
|
||||
(let
|
||||
((scripts (query-sx-scripts root)))
|
||||
(for-each
|
||||
(fn
|
||||
(s)
|
||||
(when
|
||||
(not (is-processed? s "script"))
|
||||
(mark-processed! s "script")
|
||||
(let
|
||||
((text (dom-text-content s)))
|
||||
(cond
|
||||
(dom-has-attr? s "data-components")
|
||||
(process-component-script s text)
|
||||
(or (nil? text) (empty? (trim text)))
|
||||
nil
|
||||
(dom-has-attr? s "data-init")
|
||||
(let
|
||||
((exprs (sx-parse text)))
|
||||
(for-each (fn (expr) (cek-eval expr)) exprs))
|
||||
(dom-has-attr? s "data-mount")
|
||||
(let
|
||||
((mount-sel (dom-get-attr s "data-mount"))
|
||||
(target (dom-query mount-sel)))
|
||||
(when target (sx-mount target text nil)))
|
||||
:else (sx-load-components text)))))
|
||||
scripts))))
|
||||
|
||||
(define process-component-script :effects (mutation io) (fn (script (text :as string)) (let ((hash (dom-get-attr script "data-hash"))) (if (nil? hash) (when (and text (not (empty? (trim text)))) (sx-load-components text)) (let ((has-inline (and text (not (empty? (trim text)))))) (let ((cached-hash (local-storage-get "sx-components-hash"))) (if (= cached-hash hash) (if has-inline (do (local-storage-set "sx-components-hash" hash) (local-storage-set "sx-components-src" text) (sx-load-components text) (log-info "components: downloaded (cookie stale)")) (let ((cached (local-storage-get "sx-components-src"))) (if cached (do (sx-load-components cached) (log-info (str "components: cached (" hash ")"))) (do (clear-sx-comp-cookie) (browser-reload))))) (if has-inline (do (local-storage-set "sx-components-hash" hash) (local-storage-set "sx-components-src" text) (sx-load-components text) (log-info (str "components: downloaded (" hash ")"))) (do (local-storage-remove "sx-components-hash") (local-storage-remove "sx-components-src") (clear-sx-comp-cookie) (browser-reload))))) (set-sx-comp-cookie hash))))))
|
||||
(define
|
||||
process-component-script
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(script (text :as string))
|
||||
(let
|
||||
((hash (dom-get-attr script "data-hash")))
|
||||
(if
|
||||
(nil? hash)
|
||||
(when
|
||||
(and text (not (empty? (trim text))))
|
||||
(sx-load-components text))
|
||||
(let
|
||||
((has-inline (and text (not (empty? (trim text))))))
|
||||
(let
|
||||
((cached-hash (local-storage-get "sx-components-hash")))
|
||||
(if
|
||||
(= cached-hash hash)
|
||||
(if
|
||||
has-inline
|
||||
(do
|
||||
(local-storage-set "sx-components-hash" hash)
|
||||
(local-storage-set "sx-components-src" text)
|
||||
(sx-load-components text)
|
||||
(log-info "components: downloaded (cookie stale)"))
|
||||
(let
|
||||
((cached (local-storage-get "sx-components-src")))
|
||||
(if
|
||||
cached
|
||||
(do
|
||||
(sx-load-components cached)
|
||||
(log-info (str "components: cached (" hash ")")))
|
||||
(do (clear-sx-comp-cookie) (browser-reload)))))
|
||||
(if
|
||||
has-inline
|
||||
(do
|
||||
(local-storage-set "sx-components-hash" hash)
|
||||
(local-storage-set "sx-components-src" text)
|
||||
(sx-load-components text)
|
||||
(log-info (str "components: downloaded (" hash ")")))
|
||||
(do
|
||||
(local-storage-remove "sx-components-hash")
|
||||
(local-storage-remove "sx-components-src")
|
||||
(clear-sx-comp-cookie)
|
||||
(browser-reload)))))
|
||||
(set-sx-comp-cookie hash))))))
|
||||
|
||||
(define _page-routes (list))
|
||||
|
||||
(define process-page-scripts :effects (mutation io) (fn () (let ((scripts (query-page-scripts))) (log-info (str "pages: found " (len scripts) " script tags")) (for-each (fn (s) (when (not (is-processed? s "pages")) (mark-processed! s "pages") (let ((text (dom-text-content s))) (log-info (str "pages: script text length=" (if text (len text) 0))) (if (and text (not (empty? (trim text)))) (let ((pages (parse text))) (log-info (str "pages: parsed " (len pages) " entries")) (for-each (fn ((page :as dict)) (append! _page-routes (merge page {:parsed (parse-route-pattern (get page "path"))}))) pages)) (log-warn "pages: script tag is empty"))))) scripts) (log-info (str "pages: " (len _page-routes) " routes loaded")))))
|
||||
(define
|
||||
process-page-scripts
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((scripts (query-page-scripts)))
|
||||
(log-info (str "pages: found " (len scripts) " script tags"))
|
||||
(for-each
|
||||
(fn
|
||||
(s)
|
||||
(when
|
||||
(not (is-processed? s "pages"))
|
||||
(mark-processed! s "pages")
|
||||
(let
|
||||
((text (dom-text-content s)))
|
||||
(log-info
|
||||
(str "pages: script text length=" (if text (len text) 0)))
|
||||
(if
|
||||
(and text (not (empty? (trim text))))
|
||||
(let
|
||||
((pages (parse text)))
|
||||
(log-info (str "pages: parsed " (len pages) " entries"))
|
||||
(for-each
|
||||
(fn
|
||||
((page :as dict))
|
||||
(append! _page-routes (merge page {:parsed (parse-route-pattern (get page "path"))})))
|
||||
pages))
|
||||
(log-warn "pages: script tag is empty")))))
|
||||
scripts)
|
||||
(log-info (str "pages: " (len _page-routes) " routes loaded")))))
|
||||
|
||||
(define sx-hydrate-islands :effects (mutation io) (fn (root) (let ((els (dom-query-all (or root (dom-body)) "[data-sx-island]"))) (log-info (str "sx-hydrate-islands: " (len els) " island(s) in " (if root "subtree" "document"))) (for-each (fn (el) (if (is-processed? el "island-hydrated") (log-info (str " skip (already hydrated): " (dom-get-attr el "data-sx-island"))) (do (log-info (str " hydrating: " (dom-get-attr el "data-sx-island"))) (mark-processed! el "island-hydrated") (hydrate-island el)))) els))))
|
||||
(define
|
||||
sx-hydrate-islands
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(root)
|
||||
(let
|
||||
((els (dom-query-all (or root (dom-body)) "[data-sx-island]")))
|
||||
(log-info
|
||||
(str
|
||||
"sx-hydrate-islands: "
|
||||
(len els)
|
||||
" island(s) in "
|
||||
(if root "subtree" "document")))
|
||||
(for-each
|
||||
(fn
|
||||
(el)
|
||||
(if
|
||||
(is-processed? el "island-hydrated")
|
||||
(log-info
|
||||
(str
|
||||
" skip (already hydrated): "
|
||||
(dom-get-attr el "data-sx-island")))
|
||||
(do
|
||||
(log-info
|
||||
(str " hydrating: " (dom-get-attr el "data-sx-island")))
|
||||
(mark-processed! el "island-hydrated")
|
||||
(hydrate-island el))))
|
||||
els))))
|
||||
|
||||
(define hydrate-island :effects (mutation io) (fn (el) (let ((name (dom-get-attr el "data-sx-island")) (state-sx (or (dom-get-attr el "data-sx-state") "{}"))) (let ((comp-name (str "~" name)) (env (get-render-env nil))) (let ((comp (env-get env comp-name))) (if (not (or (component? comp) (island? comp))) (log-warn (str "hydrate-island: unknown island " comp-name)) (let ((kwargs (or (first (sx-parse state-sx)) {})) (disposers (list)) (local (env-merge (component-closure comp) env))) (for-each (fn ((p :as string)) (env-bind! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil))) (component-params comp)) (let ((body-dom (cek-try (fn () (with-island-scope (fn (disposable) (append! disposers disposable)) (fn () (render-to-dom (component-body comp) local nil)))) (fn (err) (log-warn (str "hydrate-island FAILED: " comp-name " — " err)) (let ((error-el (dom-create-element "div" nil))) (dom-set-attr error-el "class" "sx-island-error") (dom-set-attr error-el "style" "padding:8px;margin:4px 0;border:1px solid #ef4444;border-radius:4px;background:#fef2f2;color:#b91c1c;font-family:monospace;font-size:12px;white-space:pre-wrap") (dom-set-text-content error-el (str "Island error: " comp-name "\n" err)) error-el))))) (dom-set-text-content el "") (dom-append el body-dom) (dom-set-data el "sx-disposers" disposers) (process-elements el) (log-info (str "hydrated island: " comp-name " (" (len disposers) " disposers)"))))))))))
|
||||
(define
|
||||
hydrate-island
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(el)
|
||||
(let
|
||||
((name (dom-get-attr el "data-sx-island"))
|
||||
(state-sx (or (dom-get-attr el "data-sx-state") "{}")))
|
||||
(let
|
||||
((comp-name (str "~" name)) (env (get-render-env nil)))
|
||||
(let
|
||||
((comp (env-get env comp-name)))
|
||||
(if
|
||||
(not (or (component? comp) (island? comp)))
|
||||
(log-warn (str "hydrate-island: unknown island " comp-name))
|
||||
(let
|
||||
((kwargs (or (first (sx-parse state-sx)) {}))
|
||||
(disposers (list))
|
||||
(local (env-merge (component-closure comp) env)))
|
||||
(for-each
|
||||
(fn
|
||||
((p :as string))
|
||||
(env-bind!
|
||||
local
|
||||
p
|
||||
(if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(component-params comp))
|
||||
(let
|
||||
((body-dom (cek-try (fn () (with-island-scope (fn (disposable) (append! disposers disposable)) (fn () (render-to-dom (component-body comp) local nil)))) (fn (err) (log-warn (str "hydrate-island FAILED: " comp-name " — " err)) (let ((error-el (dom-create-element "div" nil))) (dom-set-attr error-el "class" "sx-island-error") (dom-set-attr error-el "style" "padding:8px;margin:4px 0;border:1px solid #ef4444;border-radius:4px;background:#fef2f2;color:#b91c1c;font-family:monospace;font-size:12px;white-space:pre-wrap") (dom-set-text-content error-el (str "Island error: " comp-name "\n" err)) error-el)))))
|
||||
(dom-set-text-content el "")
|
||||
(dom-append el body-dom)
|
||||
(dom-set-data el "sx-disposers" disposers)
|
||||
(process-elements el)
|
||||
(log-info
|
||||
(str
|
||||
"hydrated island: "
|
||||
comp-name
|
||||
" ("
|
||||
(len disposers)
|
||||
" disposers)"))))))))))
|
||||
|
||||
(define dispose-island :effects (mutation io) (fn (el) (let ((disposers (dom-get-data el "sx-disposers"))) (when disposers (for-each (fn ((d :as lambda)) (when (callable? d) (d))) disposers) (dom-set-data el "sx-disposers" nil))) (clear-processed! el "island-hydrated")))
|
||||
(define
|
||||
dispose-island
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(el)
|
||||
(let
|
||||
((disposers (dom-get-data el "sx-disposers")))
|
||||
(when
|
||||
disposers
|
||||
(for-each
|
||||
(fn ((d :as lambda)) (when (callable? d) (d)))
|
||||
disposers)
|
||||
(dom-set-data el "sx-disposers" nil)))
|
||||
(clear-processed! el "island-hydrated")))
|
||||
|
||||
(define dispose-islands-in :effects (mutation io) (fn (root) (when root (let ((islands (dom-query-all root "[data-sx-island]"))) (when (and islands (not (empty? islands))) (let ((to-dispose (filter (fn (el) (not (is-processed? el "island-hydrated"))) islands))) (when (not (empty? to-dispose)) (log-info (str "disposing " (len to-dispose) " island(s)")) (for-each dispose-island to-dispose))))))))
|
||||
(define
|
||||
dispose-islands-in
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(root)
|
||||
(when
|
||||
root
|
||||
(let
|
||||
((islands (dom-query-all root "[data-sx-island]")))
|
||||
(when
|
||||
(and islands (not (empty? islands)))
|
||||
(let
|
||||
((to-dispose (filter (fn (el) (not (is-processed? el "island-hydrated"))) islands)))
|
||||
(when
|
||||
(not (empty? to-dispose))
|
||||
(log-info (str "disposing " (len to-dispose) " island(s)"))
|
||||
(for-each dispose-island to-dispose))))))))
|
||||
|
||||
(define force-dispose-islands-in :effects (mutation io) (fn (root) (when root (let ((islands (dom-query-all root "[data-sx-island]"))) (when (and islands (not (empty? islands))) (log-info (str "force-disposing " (len islands) " island(s)")) (for-each dispose-island islands))))))
|
||||
(define
|
||||
force-dispose-islands-in
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
(root)
|
||||
(when
|
||||
root
|
||||
(let
|
||||
((islands (dom-query-all root "[data-sx-island]")))
|
||||
(when
|
||||
(and islands (not (empty? islands)))
|
||||
(log-info (str "force-disposing " (len islands) " island(s)"))
|
||||
(for-each dispose-island islands))))))
|
||||
|
||||
(define *pre-render-hooks* (list))
|
||||
|
||||
(define *post-render-hooks* (list))
|
||||
|
||||
(define register-pre-render-hook :effects (mutation) (fn ((hook-fn :as lambda)) (append! *pre-render-hooks* hook-fn)))
|
||||
(define
|
||||
register-pre-render-hook
|
||||
:effects (mutation)
|
||||
(fn ((hook-fn :as lambda)) (append! *pre-render-hooks* hook-fn)))
|
||||
|
||||
(define register-post-render-hook :effects (mutation) (fn ((hook-fn :as lambda)) (append! *post-render-hooks* hook-fn)))
|
||||
(define
|
||||
register-post-render-hook
|
||||
:effects (mutation)
|
||||
(fn ((hook-fn :as lambda)) (append! *post-render-hooks* hook-fn)))
|
||||
|
||||
(define run-pre-render-hooks :effects (mutation io) (fn () (for-each (fn (hook) (cek-call hook nil)) *pre-render-hooks*)))
|
||||
(define
|
||||
run-pre-render-hooks
|
||||
:effects (mutation io)
|
||||
(fn () (for-each (fn (hook) (cek-call hook nil)) *pre-render-hooks*)))
|
||||
|
||||
(define run-post-render-hooks :effects (mutation io) (fn () (log-info (str "run-post-render-hooks: " (len *post-render-hooks*) " hooks")) (for-each (fn (hook) (log-info (str " hook type: " (type-of hook) " callable: " (callable? hook) " lambda: " (lambda? hook))) (cek-call hook nil)) *post-render-hooks*)))
|
||||
(define
|
||||
run-post-render-hooks
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
()
|
||||
(log-info
|
||||
(str "run-post-render-hooks: " (len *post-render-hooks*) " hooks"))
|
||||
(for-each
|
||||
(fn
|
||||
(hook)
|
||||
(log-info
|
||||
(str
|
||||
" hook type: "
|
||||
(type-of hook)
|
||||
" callable: "
|
||||
(callable? hook)
|
||||
" lambda: "
|
||||
(lambda? hook)))
|
||||
(cek-call hook nil))
|
||||
*post-render-hooks*)))
|
||||
|
||||
(define boot-init :effects (mutation io) (fn () (do (log-info (str "sx-browser " SX_VERSION)) (init-css-tracking) (process-page-scripts) (process-sx-scripts nil) (sx-hydrate-elements nil) (sx-hydrate-islands nil) (run-post-render-hooks) (process-elements nil) (dom-listen (dom-window) "popstate" (fn (e) (handle-popstate 0))))))
|
||||
(define
|
||||
boot-init
|
||||
:effects (mutation io)
|
||||
(fn
|
||||
()
|
||||
(do
|
||||
(log-info (str "sx-browser " SX_VERSION))
|
||||
(init-css-tracking)
|
||||
(process-page-scripts)
|
||||
(process-sx-scripts nil)
|
||||
(sx-hydrate-elements nil)
|
||||
(sx-hydrate-islands nil)
|
||||
(run-post-render-hooks)
|
||||
(process-elements nil)
|
||||
(dom-listen (dom-window) "popstate" (fn (e) (handle-popstate 0))))))
|
||||
|
||||
619
web/lib/dom.sx
619
web/lib/dom.sx
@@ -1,425 +1,418 @@
|
||||
;; ==========================================================================
|
||||
;; dom.sx — DOM library functions
|
||||
;;
|
||||
;; All DOM operations expressed using the host FFI primitives:
|
||||
;; host-get — read property from host object
|
||||
;; host-set! — write property on host object
|
||||
;; host-call — call method on host object
|
||||
;; host-new — construct host object
|
||||
;; host-global — access global (window/document/etc.)
|
||||
;; host-callback — wrap SX function as host callback
|
||||
;; host-typeof — check host object type
|
||||
;;
|
||||
;; These are LIBRARY FUNCTIONS — portable, auditable, in-band SX.
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Globals
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define dom-document (fn () (host-global "document")))
|
||||
(define dom-window (fn () (host-global "window")))
|
||||
(define dom-body (fn () (host-get (dom-document) "body")))
|
||||
(define dom-head (fn () (host-get (dom-document) "head")))
|
||||
|
||||
(define dom-window (fn () (host-global "window")))
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Node creation
|
||||
;; --------------------------------------------------------------------------
|
||||
(define dom-body (fn () (host-get (dom-document) "body")))
|
||||
|
||||
(define dom-create-element
|
||||
(fn (tag &rest ns-arg)
|
||||
(let ((ns (if (and ns-arg (not (empty? ns-arg))) (first ns-arg) nil)))
|
||||
(if ns
|
||||
(define dom-head (fn () (host-get (dom-document) "head")))
|
||||
|
||||
(define
|
||||
dom-create-element
|
||||
(fn
|
||||
(tag &rest ns-arg)
|
||||
(let
|
||||
((ns (if (and ns-arg (not (empty? ns-arg))) (first ns-arg) nil)))
|
||||
(if
|
||||
ns
|
||||
(host-call (dom-document) "createElementNS" ns tag)
|
||||
(host-call (dom-document) "createElement" tag)))))
|
||||
|
||||
(define create-text-node
|
||||
(fn (s)
|
||||
(host-call (dom-document) "createTextNode" s)))
|
||||
(define
|
||||
create-text-node
|
||||
(fn (s) (host-call (dom-document) "createTextNode" s)))
|
||||
|
||||
(define create-fragment
|
||||
(fn ()
|
||||
(host-call (dom-document) "createDocumentFragment")))
|
||||
(define
|
||||
create-fragment
|
||||
(fn () (host-call (dom-document) "createDocumentFragment")))
|
||||
|
||||
(define create-comment
|
||||
(fn (text)
|
||||
(host-call (dom-document) "createComment" (or text ""))))
|
||||
(define
|
||||
create-comment
|
||||
(fn (text) (host-call (dom-document) "createComment" (or text ""))))
|
||||
|
||||
(define
|
||||
dom-append
|
||||
(fn
|
||||
(parent child)
|
||||
(when (and parent child) (host-call parent "appendChild" child))))
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Tree manipulation
|
||||
;; --------------------------------------------------------------------------
|
||||
(define
|
||||
dom-prepend
|
||||
(fn
|
||||
(parent child)
|
||||
(when (and parent child) (host-call parent "prepend" child))))
|
||||
|
||||
(define dom-append
|
||||
(fn (parent child)
|
||||
(when (and parent child)
|
||||
(host-call parent "appendChild" child))))
|
||||
(define
|
||||
dom-insert-before
|
||||
(fn
|
||||
(parent child ref)
|
||||
(when (and parent child) (host-call parent "insertBefore" child ref))))
|
||||
|
||||
(define dom-prepend
|
||||
(fn (parent child)
|
||||
(when (and parent child)
|
||||
(host-call parent "prepend" child))))
|
||||
|
||||
(define dom-insert-before
|
||||
(fn (parent child ref)
|
||||
(when (and parent child)
|
||||
(host-call parent "insertBefore" child ref))))
|
||||
|
||||
(define dom-insert-after
|
||||
(fn (ref node)
|
||||
(define
|
||||
dom-insert-after
|
||||
(fn
|
||||
(ref node)
|
||||
"Insert node after ref in the same parent."
|
||||
(let ((parent (host-get ref "parentNode"))
|
||||
(next (host-get ref "nextSibling")))
|
||||
(when parent
|
||||
(if next
|
||||
(let
|
||||
((parent (host-get ref "parentNode"))
|
||||
(next (host-get ref "nextSibling")))
|
||||
(when
|
||||
parent
|
||||
(if
|
||||
next
|
||||
(host-call parent "insertBefore" node next)
|
||||
(host-call parent "appendChild" node))))))
|
||||
|
||||
(define dom-remove
|
||||
(fn (el)
|
||||
(when el (host-call el "remove"))))
|
||||
(define dom-remove (fn (el) (when el (host-call el "remove"))))
|
||||
|
||||
(define dom-is-active-element?
|
||||
(fn (el)
|
||||
(let ((active (host-get (dom-document) "activeElement")))
|
||||
(if (and active el)
|
||||
(identical? el active)
|
||||
false))))
|
||||
(define
|
||||
dom-is-active-element?
|
||||
(fn
|
||||
(el)
|
||||
(let
|
||||
((active (host-get (dom-document) "activeElement")))
|
||||
(if (and active el) (identical? el active) false))))
|
||||
|
||||
(define dom-is-input-element?
|
||||
(fn (el)
|
||||
(let ((tag (upper (or (dom-tag-name el) ""))))
|
||||
(define
|
||||
dom-is-input-element?
|
||||
(fn
|
||||
(el)
|
||||
(let
|
||||
((tag (upper (or (dom-tag-name el) ""))))
|
||||
(or (= tag "INPUT") (= tag "TEXTAREA") (= tag "SELECT")))))
|
||||
|
||||
(define dom-is-child-of?
|
||||
(fn (child parent)
|
||||
(and child parent (host-call parent "contains" child))))
|
||||
(define
|
||||
dom-is-child-of?
|
||||
(fn (child parent) (and child parent (host-call parent "contains" child))))
|
||||
|
||||
(define dom-attr-list
|
||||
(fn (el)
|
||||
;; Return list of (name value) pairs for all attributes on the element.
|
||||
(let ((attrs (host-get el "attributes"))
|
||||
(result (list)))
|
||||
(when attrs
|
||||
(let ((n (host-get attrs "length")))
|
||||
(let loop ((i 0))
|
||||
(when (< i n)
|
||||
(let ((attr (host-call attrs "item" i)))
|
||||
(append! result (list (host-get attr "name") (host-get attr "value"))))
|
||||
(define
|
||||
dom-attr-list
|
||||
(fn
|
||||
(el)
|
||||
(let
|
||||
((attrs (host-get el "attributes")) (result (list)))
|
||||
(when
|
||||
attrs
|
||||
(let
|
||||
((n (host-get attrs "length")))
|
||||
(let
|
||||
loop
|
||||
((i 0))
|
||||
(when
|
||||
(< i n)
|
||||
(let
|
||||
((attr (host-call attrs "item" i)))
|
||||
(append!
|
||||
result
|
||||
(list (host-get attr "name") (host-get attr "value"))))
|
||||
(loop (+ i 1))))))
|
||||
result)))
|
||||
|
||||
(define dom-remove-child
|
||||
(fn (parent child)
|
||||
(when (and parent child)
|
||||
(host-call parent "removeChild" child))))
|
||||
(define
|
||||
dom-remove-child
|
||||
(fn
|
||||
(parent child)
|
||||
(when (and parent child) (host-call parent "removeChild" child))))
|
||||
|
||||
(define dom-replace-child
|
||||
(fn (parent new-child old-child)
|
||||
(when (and parent new-child old-child)
|
||||
(define
|
||||
dom-replace-child
|
||||
(fn
|
||||
(parent new-child old-child)
|
||||
(when
|
||||
(and parent new-child old-child)
|
||||
(host-call parent "replaceChild" new-child old-child))))
|
||||
|
||||
(define dom-clone
|
||||
(fn (node deep)
|
||||
(host-call node "cloneNode" (if (nil? deep) true deep))))
|
||||
(define
|
||||
dom-clone
|
||||
(fn (node deep) (host-call node "cloneNode" (if (nil? deep) true deep))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Queries
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define dom-query
|
||||
(fn (root-or-sel &rest rest)
|
||||
(if (empty? rest)
|
||||
;; Single arg: selector on document
|
||||
(define
|
||||
dom-query
|
||||
(fn
|
||||
(root-or-sel &rest rest)
|
||||
(if
|
||||
(empty? rest)
|
||||
(host-call (dom-document) "querySelector" root-or-sel)
|
||||
;; Two args: root element + selector
|
||||
(host-call root-or-sel "querySelector" (first rest)))))
|
||||
|
||||
(define dom-query-all
|
||||
(fn (root sel)
|
||||
(define
|
||||
dom-query-all
|
||||
(fn
|
||||
(root sel)
|
||||
"Query DOM and return an SX list (not a host NodeList)."
|
||||
(let ((node-list (if (nil? sel)
|
||||
(host-call (dom-document) "querySelectorAll" root)
|
||||
(host-call root "querySelectorAll" sel))))
|
||||
;; Convert NodeList → SX list by indexing
|
||||
(if (nil? node-list)
|
||||
(let
|
||||
((node-list (if (nil? sel) (host-call (dom-document) "querySelectorAll" root) (host-call root "querySelectorAll" sel))))
|
||||
(if
|
||||
(nil? node-list)
|
||||
(list)
|
||||
(let ((n (host-get node-list "length"))
|
||||
(result (list)))
|
||||
(let loop ((i 0))
|
||||
(when (< i n)
|
||||
(let
|
||||
((n (host-get node-list "length")) (result (list)))
|
||||
(let
|
||||
loop
|
||||
((i 0))
|
||||
(when
|
||||
(< i n)
|
||||
(append! result (host-call node-list "item" i))
|
||||
(loop (+ i 1))))
|
||||
result)))))
|
||||
|
||||
(define dom-query-by-id
|
||||
(fn (id)
|
||||
(host-call (dom-document) "getElementById" id)))
|
||||
(define
|
||||
dom-query-by-id
|
||||
(fn (id) (host-call (dom-document) "getElementById" id)))
|
||||
|
||||
(define dom-closest
|
||||
(fn (el sel)
|
||||
(when el (host-call el "closest" sel))))
|
||||
(define dom-closest (fn (el sel) (when el (host-call el "closest" sel))))
|
||||
|
||||
(define dom-matches?
|
||||
(fn (el sel)
|
||||
(if (and el (host-get el "matches"))
|
||||
(host-call el "matches" sel)
|
||||
false)))
|
||||
(define
|
||||
dom-matches?
|
||||
(fn
|
||||
(el sel)
|
||||
(if (and el (host-get el "matches")) (host-call el "matches" sel) false)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Attributes
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define dom-get-attr
|
||||
(fn (el name)
|
||||
(if (and el (host-get el "getAttribute"))
|
||||
(let ((v (host-call el "getAttribute" name)))
|
||||
(if (nil? v) nil v))
|
||||
(define
|
||||
dom-get-attr
|
||||
(fn
|
||||
(el name)
|
||||
(if
|
||||
(and el (host-get el "getAttribute"))
|
||||
(let ((v (host-call el "getAttribute" name))) (if (nil? v) nil v))
|
||||
nil)))
|
||||
|
||||
(define dom-set-attr
|
||||
(fn (el name val)
|
||||
(when (and el (host-get el "setAttribute"))
|
||||
(define
|
||||
dom-set-attr
|
||||
(fn
|
||||
(el name val)
|
||||
(when
|
||||
(and el (host-get el "setAttribute"))
|
||||
(host-call el "setAttribute" name val))))
|
||||
|
||||
(define dom-remove-attr
|
||||
(fn (el name)
|
||||
(when (and el (host-get el "removeAttribute"))
|
||||
(define
|
||||
dom-remove-attr
|
||||
(fn
|
||||
(el name)
|
||||
(when
|
||||
(and el (host-get el "removeAttribute"))
|
||||
(host-call el "removeAttribute" name))))
|
||||
|
||||
(define dom-has-attr?
|
||||
(fn (el name)
|
||||
(if (and el (host-get el "hasAttribute"))
|
||||
(define
|
||||
dom-has-attr?
|
||||
(fn
|
||||
(el name)
|
||||
(if
|
||||
(and el (host-get el "hasAttribute"))
|
||||
(host-call el "hasAttribute" name)
|
||||
false)))
|
||||
|
||||
(define
|
||||
dom-add-class
|
||||
(fn (el cls) (when el (host-call (host-get el "classList") "add" cls))))
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Classes
|
||||
;; --------------------------------------------------------------------------
|
||||
(define
|
||||
dom-remove-class
|
||||
(fn
|
||||
(el cls)
|
||||
(when el (host-call (host-get el "classList") "remove" cls))))
|
||||
|
||||
(define dom-add-class
|
||||
(fn (el cls)
|
||||
(when el
|
||||
(host-call (host-get el "classList") "add" cls))))
|
||||
(define
|
||||
dom-has-class?
|
||||
(fn
|
||||
(el cls)
|
||||
(if el (host-call (host-get el "classList") "contains" cls) false)))
|
||||
|
||||
(define dom-remove-class
|
||||
(fn (el cls)
|
||||
(when el
|
||||
(host-call (host-get el "classList") "remove" cls))))
|
||||
(define dom-text-content (fn (el) (host-get el "textContent")))
|
||||
|
||||
(define dom-has-class?
|
||||
(fn (el cls)
|
||||
(if el
|
||||
(host-call (host-get el "classList") "contains" cls)
|
||||
false)))
|
||||
(define dom-set-text-content (fn (el val) (host-set! el "textContent" val)))
|
||||
|
||||
(define dom-inner-html (fn (el) (host-get el "innerHTML")))
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Content
|
||||
;; --------------------------------------------------------------------------
|
||||
(define dom-set-inner-html (fn (el val) (host-set! el "innerHTML" val)))
|
||||
|
||||
(define dom-text-content
|
||||
(fn (el) (host-get el "textContent")))
|
||||
(define dom-outer-html (fn (el) (host-get el "outerHTML")))
|
||||
|
||||
(define dom-set-text-content
|
||||
(fn (el val) (host-set! el "textContent" val)))
|
||||
(define
|
||||
dom-insert-adjacent-html
|
||||
(fn (el position html) (host-call el "insertAdjacentHTML" position html)))
|
||||
|
||||
(define dom-inner-html
|
||||
(fn (el) (host-get el "innerHTML")))
|
||||
(define dom-get-style (fn (el prop) (host-get (host-get el "style") prop)))
|
||||
|
||||
(define dom-set-inner-html
|
||||
(fn (el val) (host-set! el "innerHTML" val)))
|
||||
|
||||
(define dom-outer-html
|
||||
(fn (el) (host-get el "outerHTML")))
|
||||
|
||||
(define dom-insert-adjacent-html
|
||||
(fn (el position html)
|
||||
(host-call el "insertAdjacentHTML" position html)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Style & properties
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define dom-get-style
|
||||
(fn (el prop)
|
||||
(host-get (host-get el "style") prop)))
|
||||
|
||||
(define dom-set-style
|
||||
(fn (el prop val)
|
||||
(define
|
||||
dom-set-style
|
||||
(fn
|
||||
(el prop val)
|
||||
(host-call (host-get el "style") "setProperty" prop val)))
|
||||
|
||||
(define dom-get-prop
|
||||
(fn (el name) (host-get el name)))
|
||||
(define dom-get-prop (fn (el name) (host-get el name)))
|
||||
|
||||
(define dom-set-prop
|
||||
(fn (el name val) (host-set! el name val)))
|
||||
(define dom-set-prop (fn (el name val) (host-set! el name val)))
|
||||
|
||||
(define
|
||||
dom-tag-name
|
||||
(fn (el) (if el (lower (or (host-get el "tagName") "")) "")))
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Node info
|
||||
;; --------------------------------------------------------------------------
|
||||
(define dom-node-type (fn (el) (host-get el "nodeType")))
|
||||
|
||||
(define dom-tag-name
|
||||
(fn (el)
|
||||
(if el (lower (or (host-get el "tagName") "")) "")))
|
||||
(define dom-node-name (fn (el) (host-get el "nodeName")))
|
||||
|
||||
(define dom-node-type
|
||||
(fn (el) (host-get el "nodeType")))
|
||||
(define dom-id (fn (el) (host-get el "id")))
|
||||
|
||||
(define dom-node-name
|
||||
(fn (el) (host-get el "nodeName")))
|
||||
(define dom-parent (fn (el) (host-get el "parentNode")))
|
||||
|
||||
(define dom-id
|
||||
(fn (el) (host-get el "id")))
|
||||
(define dom-first-child (fn (el) (host-get el "firstChild")))
|
||||
|
||||
(define dom-parent
|
||||
(fn (el) (host-get el "parentNode")))
|
||||
(define dom-next-sibling (fn (el) (host-get el "nextSibling")))
|
||||
|
||||
(define dom-first-child
|
||||
(fn (el) (host-get el "firstChild")))
|
||||
|
||||
(define dom-next-sibling
|
||||
(fn (el) (host-get el "nextSibling")))
|
||||
|
||||
(define dom-child-list
|
||||
(fn (el)
|
||||
(define
|
||||
dom-child-list
|
||||
(fn
|
||||
(el)
|
||||
"Return child nodes as an SX list."
|
||||
(if el
|
||||
(let ((nl (host-get el "childNodes"))
|
||||
(n (host-get nl "length"))
|
||||
(result (list)))
|
||||
(let loop ((i 0))
|
||||
(when (< i n)
|
||||
(if
|
||||
el
|
||||
(let
|
||||
((nl (host-get el "childNodes"))
|
||||
(n (host-get nl "length"))
|
||||
(result (list)))
|
||||
(let
|
||||
loop
|
||||
((i 0))
|
||||
(when
|
||||
(< i n)
|
||||
(append! result (host-call nl "item" i))
|
||||
(loop (+ i 1))))
|
||||
result)
|
||||
(list))))
|
||||
|
||||
(define dom-is-fragment?
|
||||
(fn (el) (= (host-get el "nodeType") 11)))
|
||||
(define dom-is-fragment? (fn (el) (= (host-get el "nodeType") 11)))
|
||||
|
||||
(define dom-child-nodes
|
||||
(fn (el)
|
||||
(define
|
||||
dom-child-nodes
|
||||
(fn
|
||||
(el)
|
||||
"Return child nodes as an SX list."
|
||||
(if el
|
||||
(let ((nl (host-get el "childNodes"))
|
||||
(n (host-get nl "length"))
|
||||
(result (list)))
|
||||
(let loop ((i 0))
|
||||
(when (< i n)
|
||||
(if
|
||||
el
|
||||
(let
|
||||
((nl (host-get el "childNodes"))
|
||||
(n (host-get nl "length"))
|
||||
(result (list)))
|
||||
(let
|
||||
loop
|
||||
((i 0))
|
||||
(when
|
||||
(< i n)
|
||||
(append! result (host-call nl "item" i))
|
||||
(loop (+ i 1))))
|
||||
result)
|
||||
(list))))
|
||||
|
||||
(define dom-remove-children-after
|
||||
(fn (marker)
|
||||
(define
|
||||
dom-remove-children-after
|
||||
(fn
|
||||
(marker)
|
||||
"Remove all siblings after marker node."
|
||||
(let ((parent (dom-parent marker)))
|
||||
(when parent
|
||||
(let loop ()
|
||||
(let ((next (dom-next-sibling marker)))
|
||||
(when next
|
||||
(host-call parent "removeChild" next)
|
||||
(loop))))))))
|
||||
(let
|
||||
((parent (dom-parent marker)))
|
||||
(when
|
||||
parent
|
||||
(let
|
||||
loop
|
||||
()
|
||||
(let
|
||||
((next (dom-next-sibling marker)))
|
||||
(when next (host-call parent "removeChild" next) (loop))))))))
|
||||
|
||||
(define dom-focus
|
||||
(fn (el) (when el (host-call el "focus"))))
|
||||
(define dom-focus (fn (el) (when el (host-call el "focus"))))
|
||||
|
||||
(define dom-parse-html
|
||||
(fn (html)
|
||||
(let ((parser (host-new "DOMParser"))
|
||||
(doc (host-call parser "parseFromString" html "text/html")))
|
||||
(define
|
||||
dom-parse-html
|
||||
(fn
|
||||
(html)
|
||||
(let
|
||||
((parser (host-new "DOMParser"))
|
||||
(doc (host-call parser "parseFromString" html "text/html")))
|
||||
(host-get (host-get doc "body") "childNodes"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Events
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define dom-listen
|
||||
(fn (el event-name handler)
|
||||
(let ((cb (host-callback handler)))
|
||||
(define
|
||||
dom-listen
|
||||
(fn
|
||||
(el event-name handler)
|
||||
(let
|
||||
((cb (host-callback handler)))
|
||||
(host-call el "addEventListener" event-name cb)
|
||||
;; Return cleanup function
|
||||
(fn () (host-call el "removeEventListener" event-name cb)))))
|
||||
|
||||
;; dom-add-listener — addEventListener with optional options
|
||||
;; Used by orchestration.sx: (dom-add-listener el event handler opts)
|
||||
(define dom-add-listener
|
||||
(fn (el event-name handler &rest opts)
|
||||
(let ((cb (host-callback handler)))
|
||||
(if (and opts (not (empty? opts)))
|
||||
(define
|
||||
dom-add-listener
|
||||
(fn
|
||||
(el event-name handler &rest opts)
|
||||
(let
|
||||
((cb (host-callback handler)))
|
||||
(if
|
||||
(and opts (not (empty? opts)))
|
||||
(host-call el "addEventListener" event-name cb (first opts))
|
||||
(host-call el "addEventListener" event-name cb))
|
||||
;; Return cleanup function
|
||||
(fn () (host-call el "removeEventListener" event-name cb)))))
|
||||
|
||||
(define dom-dispatch
|
||||
(fn (el event-name detail)
|
||||
(let ((evt (host-new "CustomEvent" event-name
|
||||
(dict "detail" detail "bubbles" true))))
|
||||
(define
|
||||
dom-dispatch
|
||||
(fn
|
||||
(el event-name detail)
|
||||
(let
|
||||
((evt (host-new "CustomEvent" event-name (dict "detail" detail "bubbles" true))))
|
||||
(host-call el "dispatchEvent" evt))))
|
||||
|
||||
(define event-detail
|
||||
(fn (evt) (host-get evt "detail")))
|
||||
(define event-detail (fn (evt) (host-get evt "detail")))
|
||||
|
||||
(define prevent-default
|
||||
(fn (e) (when e (host-call e "preventDefault"))))
|
||||
(define prevent-default (fn (e) (when e (host-call e "preventDefault"))))
|
||||
|
||||
(define stop-propagation
|
||||
(fn (e) (when e (host-call e "stopPropagation"))))
|
||||
(define stop-propagation (fn (e) (when e (host-call e "stopPropagation"))))
|
||||
|
||||
(define event-modifier-key?
|
||||
(fn (e)
|
||||
(and e (or (host-get e "ctrlKey") (host-get e "metaKey")
|
||||
(host-get e "shiftKey") (host-get e "altKey")))))
|
||||
(define
|
||||
event-modifier-key?
|
||||
(fn
|
||||
(e)
|
||||
(and
|
||||
e
|
||||
(or
|
||||
(host-get e "ctrlKey")
|
||||
(host-get e "metaKey")
|
||||
(host-get e "shiftKey")
|
||||
(host-get e "altKey")))))
|
||||
|
||||
(define element-value
|
||||
(fn (el)
|
||||
(if (and el (not (nil? (host-get el "value"))))
|
||||
(define
|
||||
element-value
|
||||
(fn
|
||||
(el)
|
||||
(if
|
||||
(and el (not (nil? (host-get el "value"))))
|
||||
(host-get el "value")
|
||||
nil)))
|
||||
|
||||
(define error-message
|
||||
(fn (e)
|
||||
(if (and e (host-get e "message"))
|
||||
(host-get e "message")
|
||||
(str e))))
|
||||
(define
|
||||
error-message
|
||||
(fn
|
||||
(e)
|
||||
(if (and e (host-get e "message")) (host-get e "message") (str e))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; DOM data storage
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define dom-get-data
|
||||
(fn (el key)
|
||||
(let ((store (host-get el "__sx_data")))
|
||||
(define
|
||||
dom-get-data
|
||||
(fn
|
||||
(el key)
|
||||
(let
|
||||
((store (host-get el "__sx_data")))
|
||||
(if store (host-get store key) nil))))
|
||||
|
||||
(define dom-set-data
|
||||
(fn (el key val)
|
||||
(when (not (host-get el "__sx_data"))
|
||||
(define
|
||||
dom-set-data
|
||||
(fn
|
||||
(el key val)
|
||||
(when
|
||||
(not (host-get el "__sx_data"))
|
||||
(host-set! el "__sx_data" (dict)))
|
||||
(host-set! (host-get el "__sx_data") key val)))
|
||||
|
||||
(define
|
||||
dom-append-to-head
|
||||
(fn (el) (when (dom-head) (host-call (dom-head) "appendChild" el))))
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Head manipulation
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define dom-append-to-head
|
||||
(fn (el)
|
||||
(when (dom-head)
|
||||
(host-call (dom-head) "appendChild" el))))
|
||||
|
||||
(define set-document-title
|
||||
(fn (title)
|
||||
(host-set! (dom-document) "title" title)))
|
||||
(define
|
||||
set-document-title
|
||||
(fn (title) (host-set! (dom-document) "title" title)))
|
||||
|
||||
2363
web/orchestration.sx
2363
web/orchestration.sx
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user