diff --git a/hosts/ocaml/bin/run_tests.ml b/hosts/ocaml/bin/run_tests.ml index 4db0399b..f97d897f 100644 --- a/hosts/ocaml/bin/run_tests.ml +++ b/hosts/ocaml/bin/run_tests.ml @@ -1313,6 +1313,7 @@ let run_spec_tests env test_files = load_module "parser.sx" hs_dir; load_module "compiler.sx" hs_dir; load_module "runtime.sx" hs_dir; + load_module "integration.sx" hs_dir; load_module "types.sx" lib_dir; load_module "sx-swap.sx" lib_dir; (* Shared templates: TW styling engine *) diff --git a/lib/hyperscript/integration.sx b/lib/hyperscript/integration.sx new file mode 100644 index 00000000..827c8dca --- /dev/null +++ b/lib/hyperscript/integration.sx @@ -0,0 +1,66 @@ +;; _hyperscript integration — wire _="..." attributes to compiled SX +;; +;; Entry points: +;; (hs-handler src) — compile source to callable (fn (me) ...) +;; (hs-activate! el) — activate hyperscript on a single element +;; (hs-boot!) — scan DOM, activate all _="..." elements +;; (hs-boot-subtree! root) — activate within a subtree (for HTMX swaps) + +;; ── Compile source to a handler function ──────────────────────── +;; Returns a function (fn (me) ...) that can be called with a DOM element. +;; Uses eval-expr-cek to turn the SX data structure into a live closure. + +(define + hs-handler + (fn + (src) + (let + ((sx (hs-to-sx-from-source src))) + (eval-expr-cek + (list + (quote fn) + (list (quote me)) + (list + (quote let) + (list (list (quote it) nil) (list (quote event) nil)) + sx)))))) + +;; ── Activate a single element ─────────────────────────────────── +;; Reads the _="..." attribute, compiles, and executes with me=element. +;; Marks the element to avoid double-activation. + +(define + hs-activate! + (fn + (el) + (let + ((src (dom-get-attr el "_"))) + (when + (and src (not (dom-get-data el "hs-active"))) + (dom-set-data el "hs-active" true) + (let ((handler (hs-handler src))) (handler el)))))) + +;; ── Boot: scan entire document ────────────────────────────────── +;; Called once at page load. Finds all elements with _ attribute, +;; compiles their hyperscript, and activates them. + +(define + hs-boot! + (fn + () + (let + ((elements (dom-query-all (dom-body) "[_]"))) + (for-each (fn (el) (hs-activate! el)) elements)))) + +;; ── Boot subtree: for dynamic content ─────────────────────────── +;; Called after HTMX swaps or dynamic DOM insertion. +;; Only activates elements within the given root. + +(define + hs-boot-subtree! + (fn + (root) + (let + ((elements (dom-query-all root "[_]"))) + (for-each (fn (el) (hs-activate! el)) elements)) + (when (dom-get-attr root "_") (hs-activate! root)))) \ No newline at end of file diff --git a/spec/tests/test-hyperscript-integration.sx b/spec/tests/test-hyperscript-integration.sx new file mode 100644 index 00000000..3eb68a00 --- /dev/null +++ b/spec/tests/test-hyperscript-integration.sx @@ -0,0 +1,76 @@ +;; _hyperscript integration tests +;; Tests hs-handler: source → callable function via eval-expr-cek. +;; DOM-dependent tests (hs-boot!, hs-activate!) deferred to Playwright. + +;; ── Handler compilation ─────────────────────────────────────── +(defsuite + "hs-handler-compilation" + (deftest + "handler returns a function" + (let ((h (hs-handler "log 'hello'"))) (assert= true (procedure? h)))) + (deftest + "handler accepts me argument" + (let + ((h (hs-handler "set x to 42"))) + (h "mock-element") + (assert= 42 x))) + (deftest + "handler can return a value" + (let ((h (hs-handler "return 99"))) (assert= 99 (h "el")))) + (deftest + "handler binds me from argument" + (let + ((h (hs-handler "set x to me"))) + (h "my-element") + (assert= "my-element" x))) + (deftest + "handler initializes it to nil" + (let ((h (hs-handler "set x to it"))) (h "el") (assert= nil x)))) + +;; ── Arithmetic in handler ───────────────────────────────────── +(defsuite + "hs-handler-expressions" + (deftest + "arithmetic works in handler" + (let ((h (hs-handler "set x to 2 + 3"))) (h "el") (assert= 5 x))) + (deftest + "string concatenation via set" + (let + ((h (hs-handler "set x to 'hello'"))) + (h "el") + (assert= "hello" x))) + (deftest + "conditional in handler" + (let + ((h (hs-handler "if true set x to 1 end"))) + (h "el") + (assert= 1 x))) + (deftest + "if-else in handler" + (let + ((h (hs-handler "if false set x to 1 else set x to 2 end"))) + (h "el") + (assert= 2 x)))) + +;; ── Sequence / control flow ─────────────────────────────────── +(defsuite + "hs-handler-control" + (deftest + "then chains commands" + (let + ((h (hs-handler "set x to 1 then set y to 2"))) + (h "el") + (assert= 1 x) + (assert= 2 y))) + (deftest + "for iterates" + (let + ((h (hs-handler "set total to 0 then for n in [1, 2, 3] set total to total + n end"))) + (h "el") + (assert= 6 total))) + (deftest + "repeat counts" + (let + ((h (hs-handler "set count to 0 then repeat 3 times set count to count + 1 end"))) + (h "el") + (assert= 3 count)))) \ No newline at end of file