diff --git a/hosts/ocaml/bin/run_tests.ml b/hosts/ocaml/bin/run_tests.ml
index d8c7f544..14e2563e 100644
--- a/hosts/ocaml/bin/run_tests.ml
+++ b/hosts/ocaml/bin/run_tests.ml
@@ -1985,9 +1985,10 @@ let run_spec_tests env test_files =
Hashtbl.replace r "right" (Number 100.0); Hashtbl.replace r "bottom" (Number 100.0);
Dict r
| "insertAdjacentHTML" ->
- (* Simplified: just append text to innerHTML *)
+ (* Simplified: coerce value to string and append to innerHTML *)
(match rest with
- | [String _pos; String html] ->
+ | [String _pos; value] ->
+ let html = match dom_stringify value with String s -> s | _ -> "" in
let cur = match Hashtbl.find_opt d "innerHTML" with Some (String s) -> s | _ -> "" in
Hashtbl.replace d "innerHTML" (String (cur ^ html)); Nil
| _ -> Nil)
diff --git a/lib/hyperscript/compiler.sx b/lib/hyperscript/compiler.sx
index 3ae4c062..85148e37 100644
--- a/lib/hyperscript/compiler.sx
+++ b/lib/hyperscript/compiler.sx
@@ -267,10 +267,10 @@
(if
(and (> (len ast) 4) (= (nth ast 4) :index))
(list
- (quote for-each)
+ (quote map-indexed)
(list
(quote fn)
- (list (make-symbol var-name) (make-symbol (nth ast 5)))
+ (list (make-symbol (nth ast 5)) (make-symbol var-name))
body)
collection)
(list
diff --git a/lib/hyperscript/parser.sx b/lib/hyperscript/parser.sx
index 8e5d3607..8bb9546a 100644
--- a/lib/hyperscript/parser.sx
+++ b/lib/hyperscript/parser.sx
@@ -1223,10 +1223,10 @@
(let
((prop (cond ((= (tp-type) "style") (get (adv!) "value")) ((= (tp-val) "my") (do (adv!) (if (= (tp-type) "style") (get (adv!) "value") (get (adv!) "value")))) (true (get (adv!) "value")))))
(let
- ((from-val (if (match-kw "from") (let ((v (parse-atom))) (if (and (number? v) (= (tp-type) "ident") (not (hs-keyword? (tp-val)))) (let ((unit (get (adv!) "value"))) (list (quote string-postfix) v unit)) v)) nil)))
+ ((from-val (if (match-kw "from") (let ((v (parse-atom))) (if (and v (= (tp-type) "ident") (not (hs-keyword? (tp-val)))) (let ((unit (get (adv!) "value"))) (list (quote string-postfix) v unit)) v)) nil)))
(expect-kw! "to")
(let
- ((value (let ((v (parse-atom))) (if (and (number? v) (= (tp-type) "ident") (not (hs-keyword? (tp-val)))) (let ((unit (get (adv!) "value"))) (list (quote string-postfix) v unit)) v))))
+ ((value (let ((v (parse-atom))) (if (and v (= (tp-type) "ident") (not (hs-keyword? (tp-val)))) (let ((unit (get (adv!) "value"))) (list (quote string-postfix) v unit)) v))))
(let
((dur (if (match-kw "over") (let ((v (parse-atom))) (if (and (number? v) (= (tp-type) "ident") (not (hs-keyword? (tp-val)))) (let ((unit (get (adv!) "value"))) (list (quote string-postfix) v unit)) v)) nil)))
(let
@@ -1521,9 +1521,9 @@
(let
((collection (parse-expr)))
(let
- ((idx (if (match-kw "index") (let ((iname (tp-val))) (adv!) iname) nil)))
+ ((idx (cond ((match-kw "index") (let ((iname (tp-val))) (adv!) iname)) ((match-kw "indexed") (do (match-kw "by") (let ((iname (tp-val))) (adv!) iname))) (true nil))))
(let
- ((body (parse-cmd-list)))
+ ((body (do (match-kw "then") (parse-cmd-list))))
(match-kw "end")
(if
idx
diff --git a/lib/hyperscript/tokenizer.sx b/lib/hyperscript/tokenizer.sx
index 8c473703..2f7707d7 100644
--- a/lib/hyperscript/tokenizer.sx
+++ b/lib/hyperscript/tokenizer.sx
@@ -104,6 +104,7 @@
"detail"
"sender"
"index"
+ "indexed"
"increment"
"decrement"
"append"
diff --git a/spec/tests/test-hyperscript-behavioral.sx b/spec/tests/test-hyperscript-behavioral.sx
index 354a536d..026e64b1 100644
--- a/spec/tests/test-hyperscript-behavioral.sx
+++ b/spec/tests/test-hyperscript-behavioral.sx
@@ -638,10 +638,11 @@
(deftest "can toggle between two attribute values"
(hs-cleanup!)
(let ((_el-div (dom-create-element "div")))
- ;; HS source has bare quotes or embedded HTML
+ (dom-set-attr _el-div "_" "\\\"on")
(dom-set-attr _el-div "data-state" "active")
;; SKIP attr [@data-state (contains special chars)
(dom-append (dom-body) _el-div)
+ (hs-activate! _el-div)
(dom-dispatch _el-div "click" nil)
(assert= (dom-get-attr _el-div "data-state") "inactive")
(dom-dispatch _el-div "click" nil)
@@ -650,11 +651,12 @@
(deftest "can toggle between different attributes"
(hs-cleanup!)
(let ((_el-div (dom-create-element "div")))
- ;; HS source has bare quotes or embedded HTML
+ (dom-set-attr _el-div "_" "\\\"on")
(dom-set-attr _el-div "enabled" "true")
;; SKIP attr [@enabled (contains special chars)
;; SKIP attr [@disabled (contains special chars)
(dom-append (dom-body) _el-div)
+ (hs-activate! _el-div)
(dom-dispatch _el-div "click" nil)
(assert= (dom-get-attr _el-div "disabled") "true")
(dom-dispatch _el-div "click" nil)
@@ -722,9 +724,10 @@
(deftest "can toggle *display between two values"
(hs-cleanup!)
(let ((_el-div (dom-create-element "div")))
- ;; HS source has bare quotes or embedded HTML
+ (dom-set-attr _el-div "_" "\\\"on")
(dom-set-attr _el-div "style" "display:none")
(dom-append (dom-body) _el-div)
+ (hs-activate! _el-div)
(dom-dispatch _el-div "click" nil)
(assert= (dom-get-style _el-div "display") "flex")
(dom-dispatch _el-div "click" nil)
@@ -733,9 +736,10 @@
(deftest "can toggle *opacity between three values"
(hs-cleanup!)
(let ((_el-div (dom-create-element "div")))
- ;; HS source has bare quotes or embedded HTML
+ (dom-set-attr _el-div "_" "\\\"on")
(dom-set-attr _el-div "style" "opacity:0")
(dom-append (dom-body) _el-div)
+ (hs-activate! _el-div)
(dom-dispatch _el-div "click" nil)
(assert= (dom-get-style _el-div "opacity") "0.5")
(dom-dispatch _el-div "click" nil)
@@ -746,8 +750,9 @@
(deftest "can toggle a global variable between two values"
(hs-cleanup!)
(let ((_el-div (dom-create-element "div")))
- ;; HS source has bare quotes or embedded HTML
+ (dom-set-attr _el-div "_" "\\\"on")
(dom-append (dom-body) _el-div)
+ (hs-activate! _el-div)
(dom-dispatch _el-div "click" nil)
(dom-dispatch _el-div "click" nil)
(dom-dispatch _el-div "click" nil)
@@ -755,8 +760,9 @@
(deftest "can toggle a global variable between three values"
(hs-cleanup!)
(let ((_el-div (dom-create-element "div")))
- ;; HS source has bare quotes or embedded HTML
+ (dom-set-attr _el-div "_" "\\\"on")
(dom-append (dom-body) _el-div)
+ (hs-activate! _el-div)
(dom-dispatch _el-div "click" nil)
(dom-dispatch _el-div "click" nil)
(dom-dispatch _el-div "click" nil)
@@ -1351,7 +1357,7 @@
(deftest "properly processes hyperscript in new content in a symbol write"
(hs-cleanup!)
(let ((_el-div (dom-create-element "div")))
- (dom-set-attr _el-div "_" "on click put \"\" into me")
+ (dom-set-attr _el-div "_" "on click put \"\" into me")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
(dom-dispatch _el-div "click" nil)
@@ -1362,7 +1368,7 @@
(hs-cleanup!)
(let ((_el-d1 (dom-create-element "div")))
(dom-set-attr _el-d1 "id" "d1")
- (dom-set-attr _el-d1 "_" "on click put \"\" into
")
+ (dom-set-attr _el-d1 "_" "on click put \"\" into ")
(dom-append (dom-body) _el-d1)
(hs-activate! _el-d1)
(dom-dispatch _el-d1 "click" nil)
@@ -1373,7 +1379,7 @@
(hs-cleanup!)
(let ((_el-d1 (dom-create-element "div")))
(dom-set-attr _el-d1 "id" "d1")
- (dom-set-attr _el-d1 "_" "on click put \"\" before me")
+ (dom-set-attr _el-d1 "_" "on click put \"\" before me")
(dom-append (dom-body) _el-d1)
(hs-activate! _el-d1)
(dom-dispatch _el-d1 "click" nil)
@@ -1384,7 +1390,7 @@
(hs-cleanup!)
(let ((_el-d1 (dom-create-element "div")))
(dom-set-attr _el-d1 "id" "d1")
- (dom-set-attr _el-d1 "_" "on click put \"\" at the start of me")
+ (dom-set-attr _el-d1 "_" "on click put \"\" at the start of me")
(dom-append (dom-body) _el-d1)
(hs-activate! _el-d1)
(dom-dispatch _el-d1 "click" nil)
@@ -1395,7 +1401,7 @@
(hs-cleanup!)
(let ((_el-d1 (dom-create-element "div")))
(dom-set-attr _el-d1 "id" "d1")
- (dom-set-attr _el-d1 "_" "on click put \"\" at the end of me")
+ (dom-set-attr _el-d1 "_" "on click put \"\" at the end of me")
(dom-append (dom-body) _el-d1)
(hs-activate! _el-d1)
(dom-dispatch _el-d1 "click" nil)
@@ -1406,7 +1412,7 @@
(hs-cleanup!)
(let ((_el-d1 (dom-create-element "div")))
(dom-set-attr _el-d1 "id" "d1")
- (dom-set-attr _el-d1 "_" "on click put \"\" after me")
+ (dom-set-attr _el-d1 "_" "on click put \"\" after me")
(dom-append (dom-body) _el-d1)
(hs-activate! _el-d1)
(dom-dispatch _el-d1 "click" nil)
@@ -2047,7 +2053,7 @@
(deftest "for loop over undefined skips without error"
(hs-cleanup!)
(let ((_el-div (dom-create-element "div")))
- (dom-set-attr _el-div "_" "on click repeat for x in doesNotExist put x at end of me end put \"done\" into me")
+ (dom-set-attr _el-div "_" "on click repeat for x in doesNotExist put x at end of me end put \\\"done\\\" into me")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
(dom-dispatch _el-div "click" nil)
@@ -2704,9 +2710,10 @@
(deftest "can transition on query ref with of syntax"
(hs-cleanup!)
(let ((_el-div (dom-create-element "div")) (_el-span (dom-create-element "span")))
- ;; HS source has bare quotes or embedded HTML
+ (dom-set-attr _el-div "_" "\\\"on")
(dom-append (dom-body) _el-div)
(dom-append (dom-body) _el-span)
+ (hs-activate! _el-div)
(dom-dispatch _el-div "click" nil)
(assert= (dom-get-style _el-span "width") "100px")
))
@@ -2980,7 +2987,7 @@
(deftest "throws on non-2xx response by default"
(hs-cleanup!)
(let ((_el-div (dom-create-element "div")))
- (dom-set-attr _el-div "_" "on click fetch /test catch e put \"caught\" into me")
+ (dom-set-attr _el-div "_" "on click fetch /test catch e put \\\"caught\\\" into me")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
(dom-dispatch _el-div "click" nil)
@@ -3016,8 +3023,9 @@
(deftest "Response can be converted to JSON via as JSON"
(hs-cleanup!)
(let ((_el-div (dom-create-element "div")))
- ;; HS source has bare quotes or embedded HTML
+ (dom-set-attr _el-div "_" "\\\"on")
(dom-append (dom-body) _el-div)
+ (hs-activate! _el-div)
(dom-dispatch _el-div "click" nil)
(assert= (dom-text-content _el-div) "Joe")
))
@@ -4184,7 +4192,7 @@
(hs-cleanup!)
(let ((_el-d1 (dom-create-element "div")))
(dom-set-attr _el-d1 "id" "d1")
- (dom-set-attr _el-d1 "_" "on myEvent(foo) if foo put foo into me else put \"no-detail\" into me")
+ (dom-set-attr _el-d1 "_" "on myEvent(foo) if foo put foo into me else put \\\"no-detail\\\" into me")
(dom-append (dom-body) _el-d1)
(hs-activate! _el-d1)
))
@@ -4203,7 +4211,7 @@
(deftest "caught exceptions do not trigger 'exception' event"
(hs-cleanup!)
(let ((_el-button (dom-create-element "button")))
- (dom-set-attr _el-button "_" "on click put \"foo\" into me then throw \"bar\" catch e log e on exception(error) put error into me")
+ (dom-set-attr _el-button "_" "on click put \\\"foo\\\" into me then throw \\\"bar\\\" catch e log e on exception(error) put error into me")
(dom-append (dom-body) _el-button)
(hs-activate! _el-button)
(dom-dispatch _el-button "click" nil)
@@ -4212,7 +4220,7 @@
(deftest "rethrown exceptions trigger 'exception' event"
(hs-cleanup!)
(let ((_el-button (dom-create-element "button")))
- (dom-set-attr _el-button "_" "on click put \"foo\" into me then throw \"bar\" catch e throw e on exception(error) put error into me")
+ (dom-set-attr _el-button "_" "on click put \\\"foo\\\" into me then throw \\\"bar\\\" catch e throw e on exception(error) put error into me")
(dom-append (dom-body) _el-button)
(hs-activate! _el-button)
(dom-dispatch _el-button "click" nil)
@@ -4221,7 +4229,7 @@
(deftest "can ignore when target doesn\'t exist"
(hs-cleanup!)
(let ((_el-div (dom-create-element "div")))
- (dom-set-attr _el-div "_" "on click from #doesntExist then throw \"bar\" on click put \"clicked\" into me")
+ (dom-set-attr _el-div "_" "on click from #doesntExist then throw \\\"bar\\\" on click put \\\"clicked\\\" into me")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
(dom-dispatch _el-div "click" nil)
@@ -4423,7 +4431,7 @@
(deftest "prompts and puts result in it"
(hs-cleanup!)
(let ((_el-button (dom-create-element "button")) (_el-out (dom-create-element "div")))
- (dom-set-attr _el-button "_" "on click ask \"What is your name?\" then put it into #out")
+ (dom-set-attr _el-button "_" "on click ask \\\"What is your name?\\\" then put it into #out")
(dom-set-inner-html _el-button "Ask")
(dom-set-attr _el-out "id" "out")
(dom-append (dom-body) _el-button)
@@ -4435,7 +4443,7 @@
(deftest "returns null on cancel"
(hs-cleanup!)
(let ((_el-button (dom-create-element "button")) (_el-out (dom-create-element "div")))
- (dom-set-attr _el-button "_" "on click ask \"Name?\" then put it into #out")
+ (dom-set-attr _el-button "_" "on click ask \\\"Name?\\\" then put it into #out")
(dom-set-inner-html _el-button "Ask")
(dom-set-attr _el-out "id" "out")
(dom-append (dom-body) _el-button)
@@ -4447,7 +4455,7 @@
(deftest "shows an alert"
(hs-cleanup!)
(let ((_el-button (dom-create-element "button")) (_el-out (dom-create-element "div")))
- (dom-set-attr _el-button "_" "on click answer \"Hello!\" then put \"done\" into #out")
+ (dom-set-attr _el-button "_" "on click answer \\\"Hello!\\\" then put \\\"done\\\" into #out")
(dom-set-inner-html _el-button "Go")
(dom-set-attr _el-out "id" "out")
(dom-append (dom-body) _el-button)
@@ -4459,7 +4467,7 @@
(deftest "confirm returns first choice on OK"
(hs-cleanup!)
(let ((_el-button (dom-create-element "button")) (_el-out (dom-create-element "div")))
- (dom-set-attr _el-button "_" "on click answer \"Save?\" with \"Yes\" or \"No\" then put it into #out")
+ (dom-set-attr _el-button "_" "on click answer \\\"Save?\\\" with \\\"Yes\\\" or \\\"No\\\" then put it into #out")
(dom-set-inner-html _el-button "Go")
(dom-set-attr _el-out "id" "out")
(dom-append (dom-body) _el-button)
@@ -4471,7 +4479,7 @@
(deftest "confirm returns second choice on cancel"
(hs-cleanup!)
(let ((_el-button (dom-create-element "button")) (_el-out (dom-create-element "div")))
- (dom-set-attr _el-button "_" "on click answer \"Save?\" with \"Yes\" or \"No\" then put it into #out")
+ (dom-set-attr _el-button "_" "on click answer \\\"Save?\\\" with \\\"Yes\\\" or \\\"No\\\" then put it into #out")
(dom-set-inner-html _el-button "Go")
(dom-set-attr _el-out "id" "out")
(dom-append (dom-body) _el-button)
@@ -4850,7 +4858,7 @@
(deftest "can parse go to with string URL"
(hs-cleanup!)
(let ((_el-div (dom-create-element "div")))
- (dom-set-attr _el-div "_" "on click go to \"#test-hash\"")
+ (dom-set-attr _el-div "_" "on click go to \\\"#test-hash\\\"")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
))
@@ -4946,11 +4954,12 @@
(dom-set-attr _el-outer "id" "outer")
(dom-set-attr _el-outer "_" "on click add .outer-clicked")
(dom-set-attr _el-inner "id" "inner")
- ;; HS source has bare quotes or embedded HTML
+ (dom-set-attr _el-inner "_" "\\\"on")
(dom-set-inner-html _el-inner "click me")
(dom-append (dom-body) _el-outer)
(dom-append _el-outer _el-inner)
(hs-activate! _el-outer)
+ (hs-activate! _el-inner)
(dom-dispatch (dom-query-by-id "inner") "click" nil)
(assert (not (dom-has-class? (dom-query-by-id "outer") "outer-clicked")))
(assert (dom-has-class? (dom-query-by-id "inner") "continued"))
@@ -5036,7 +5045,7 @@
(dom-set-attr _el-target "id" "target")
(dom-set-inner-html _el-span "first")
(dom-set-attr _el-target2 "id" "target")
- ;; HS source has bare quotes or embedded HTML
+ (dom-set-attr _el-target2 "_" "\\\"on")
(dom-set-inner-html _el-span3 "first")
(dom-set-inner-html _el-span4 "second")
(dom-append (dom-body) _el-target)
@@ -5044,6 +5053,7 @@
(dom-append (dom-body) _el-target2)
(dom-append _el-target2 _el-span3)
(dom-append _el-target2 _el-span4)
+ (hs-activate! _el-target2)
(dom-dispatch (dom-query-by-id "go") "click" nil)
))
(deftest "morph removes old children"
@@ -5053,13 +5063,14 @@
(dom-set-inner-html _el-span "first")
(dom-set-inner-html _el-span2 "second")
(dom-set-attr _el-target3 "id" "target")
- ;; HS source has bare quotes or embedded HTML
+ (dom-set-attr _el-target3 "_" "\\\"on")
(dom-set-inner-html _el-span4 "first")
(dom-append (dom-body) _el-target)
(dom-append _el-target _el-span)
(dom-append _el-target _el-span2)
(dom-append (dom-body) _el-target3)
(dom-append _el-target3 _el-span4)
+ (hs-activate! _el-target3)
(dom-dispatch (dom-query-by-id "go") "click" nil)
))
(deftest "morph initializes hyperscript on new elements"
@@ -5068,7 +5079,7 @@
(dom-set-attr _el-target "id" "target")
(dom-set-inner-html _el-p "old")
(dom-set-attr _el-target2 "id" "target")
- ;; HS source has bare quotes or embedded HTML
+ (dom-set-attr _el-target2 "_" "\\\"on")
(dom-set-attr _el-inner "id" "inner")
;; HS source has bare quotes or embedded HTML
(dom-set-inner-html _el-inner "new")
@@ -5076,6 +5087,7 @@
(dom-append _el-target _el-p)
(dom-append (dom-body) _el-target2)
(dom-append _el-target2 _el-inner)
+ (hs-activate! _el-target2)
(dom-dispatch (dom-query-by-id "go") "click" nil)
(assert= (dom-text-content (dom-query-by-id "inner")) "new")
(dom-dispatch (dom-query-by-id "inner") "click" nil)
@@ -5086,7 +5098,7 @@
(let ((_el-target (dom-create-element "div")) (_el-child (dom-create-element "div")) (_el-button (dom-create-element "button")))
(dom-set-attr _el-target "id" "target")
(dom-set-attr _el-child "id" "child")
- (dom-set-attr _el-child "_" "on click put \"alive\" into me")
+ (dom-set-attr _el-child "_" "on click put \\\"alive\\\" into me")
(dom-set-inner-html _el-child "child")
;; HS source has bare quotes or embedded HTML
(dom-set-inner-html _el-button "go")
@@ -5132,7 +5144,7 @@
(dom-set-attr _el-target "id" "target")
(dom-set-inner-html _el-target "original")
(dom-set-attr _el-go "id" "go")
- (dom-set-attr _el-go "_" "on click set content to \"morphed
\" then morph #target to content")
+ (dom-set-attr _el-go "_" "on click set content to \\\"morphed
\\\" then morph #target to content")
(dom-set-inner-html _el-go "go")
(dom-append (dom-body) _el-target)
(dom-append (dom-body) _el-go)
@@ -7255,7 +7267,7 @@
(let ((_el-target (dom-create-element "div")) (_el-button (dom-create-element "button")))
(dom-set-attr _el-target "id" "target")
(dom-set-inner-html _el-target "old")
- (dom-set-attr _el-button "_" "on click make a then put \"moved\" into it then set #target to it")
+ (dom-set-attr _el-button "_" "on click make a then put \\\"moved\\\" into it then set #target to it")
(dom-set-inner-html _el-button "go")
(dom-append (dom-body) _el-target)
(dom-append (dom-body) _el-button)
@@ -7343,7 +7355,7 @@
(let ((_el-target (dom-create-element "div")) (_el-button (dom-create-element "button")))
(dom-set-attr _el-target "id" "target")
(dom-set-inner-html _el-target "old")
- (dom-set-attr _el-button "_" "on click put \"new\" into #target")
+ (dom-set-attr _el-button "_" "on click put \\\"new\\\" into #target")
(dom-set-inner-html _el-button "go")
(dom-append (dom-body) _el-target)
(dom-append (dom-body) _el-button)
@@ -7464,15 +7476,17 @@
(dom-set-inner-html _el-span2 "B")
(dom-add-class _el-span3 "a")
(dom-set-inner-html _el-span3 "C")
- ;; HS source has bare quotes or embedded HTML
+ (dom-set-attr _el-button "_" "\\\"on")
(dom-set-attr _el-b2 "id" "b2")
- ;; HS source has bare quotes or embedded HTML
+ (dom-set-attr _el-b2 "_" "\\\"on")
(dom-append (dom-body) _el-box)
(dom-append _el-box _el-span)
(dom-append _el-box _el-span2)
(dom-append _el-box _el-span3)
(dom-append (dom-body) _el-button)
(dom-append (dom-body) _el-b2)
+ (hs-activate! _el-button)
+ (hs-activate! _el-b2)
(dom-dispatch _el-button "click" nil)
(assert= (dom-text-content _el-button) "2")
(dom-dispatch (dom-query-by-id "b2") "click" nil)
@@ -7486,11 +7500,12 @@
(dom-set-inner-html _el-span "A")
(dom-add-class _el-span2 "b")
(dom-set-inner-html _el-span2 "B")
- ;; HS source has bare quotes or embedded HTML
+ (dom-set-attr _el-button "_" "\\\"set")
(dom-append (dom-body) _el-box)
(dom-append _el-box _el-span)
(dom-append _el-box _el-span2)
(dom-append (dom-body) _el-button)
+ (hs-activate! _el-button)
(dom-dispatch _el-button "click" nil)
(assert= (dom-text-content _el-button) "1")
))
@@ -7911,8 +7926,9 @@
(hs-cleanup!)
(let ((_el-d1 (dom-create-element "div")))
(dom-set-attr _el-d1 "id" "d1")
- ;; HS source has bare quotes or embedded HTML
+ (dom-set-attr _el-d1 "_" "\\\"on")
(dom-append (dom-body) _el-d1)
+ (hs-activate! _el-d1)
(dom-dispatch (dom-query-by-id "d1") "click" nil)
(assert= (dom-text-content (dom-query-by-id "d1")) "bar")
))
@@ -7920,8 +7936,9 @@
(hs-cleanup!)
(let ((_el-d1 (dom-create-element "div")))
(dom-set-attr _el-d1 "id" "d1")
- ;; HS source has bare quotes or embedded HTML
+ (dom-set-attr _el-d1 "_" "\\\"on")
(dom-append (dom-body) _el-d1)
+ (hs-activate! _el-d1)
(dom-dispatch (dom-query-by-id "d1") "click" nil)
(assert= (dom-text-content (dom-query-by-id "d1")) "bar")
))
@@ -7996,7 +8013,7 @@
(deftest "handles rejected promises without hanging"
(hs-cleanup!)
(let ((_el-div (dom-create-element "div")))
- (dom-set-attr _el-div "_" "on click js return Promise.reject(\"boom\") end catch e put e into my.innerHTML")
+ (dom-set-attr _el-div "_" "on click js return Promise.reject(\\\"boom\\\") end catch e put e into my.innerHTML")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
(dom-dispatch _el-div "click" nil)
@@ -8078,7 +8095,7 @@
(deftest "the result in a when clause refers to previous command result, not element being tested"
(hs-cleanup!)
(let ((_el-div (dom-create-element "div")) (_el-s1 (dom-create-element "span")) (_el-s2 (dom-create-element "span")))
- ;; HS source has bare quotes or embedded HTML
+ (dom-set-attr _el-div "_" "\\\"on")
(dom-set-attr _el-s1 "id" "s1")
(dom-set-attr _el-s1 "style" "display:none")
(dom-set-inner-html _el-s1 "A")
@@ -8088,6 +8105,7 @@
(dom-append (dom-body) _el-div)
(dom-append (dom-body) _el-s1)
(dom-append (dom-body) _el-s2)
+ (hs-activate! _el-div)
(dom-dispatch _el-div "click" nil)
(assert (dom-visible? (dom-query-by-id "s1")))
(assert (dom-visible? (dom-query-by-id "s2")))
@@ -8095,7 +8113,7 @@
(deftest "the result after show...when is the matched elements"
(hs-cleanup!)
(let ((_el-div (dom-create-element "div")) (_el-p (dom-create-element "p")) (_el-p2 (dom-create-element "p")) (_el-out (dom-create-element "span")))
- ;; HS source has bare quotes or embedded HTML
+ (dom-set-attr _el-div "_" "\\\"on")
(dom-set-attr _el-p "style" "display:none")
(dom-set-inner-html _el-p "yes")
(dom-set-attr _el-p2 "style" "display:none")
@@ -8106,6 +8124,7 @@
(dom-append (dom-body) _el-p)
(dom-append (dom-body) _el-p2)
(dom-append (dom-body) _el-out)
+ (hs-activate! _el-div)
(dom-dispatch _el-div "click" nil)
(assert= (dom-text-content (dom-query-by-id "out")) "some")
))
@@ -8234,7 +8253,7 @@
(deftest "can have comments in attributes (triple dash)"
(hs-cleanup!)
(let ((_el-div (dom-create-element "div")))
- (dom-set-attr _el-div "_" "on click put \"clicked\" into my.innerHTML ---put some content into the div...")
+ (dom-set-attr _el-div "_" "on click put \\\"clicked\\\" into my.innerHTML ---put some content into the div...")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
(dom-dispatch _el-div "click" nil)
@@ -8244,7 +8263,7 @@
(hs-cleanup!)
(let ((_el-d1 (dom-create-element "div")))
(dom-set-attr _el-d1 "id" "d1")
- (dom-set-attr _el-d1 "_" "on click blargh end on mouseenter put \"hovered\" into my.innerHTML")
+ (dom-set-attr _el-d1 "_" "on click blargh end on mouseenter put \\\"hovered\\\" into my.innerHTML")
(dom-append (dom-body) _el-d1)
(hs-activate! _el-d1)
))
@@ -8252,7 +8271,7 @@
(hs-cleanup!)
(let ((_el-d1 (dom-create-element "div")))
(dom-set-attr _el-d1 "id" "d1")
- (dom-set-attr _el-d1 "_" "on click blargh end on mouseenter also_bad end on focus put \"focused\" into my.innerHTML")
+ (dom-set-attr _el-d1 "_" "on click blargh end on mouseenter also_bad end on focus put \\\"focused\\\" into my.innerHTML")
(dom-append (dom-body) _el-d1)
(hs-activate! _el-d1)
))
@@ -8264,7 +8283,7 @@
(dom-set-attr _el-d1 "id" "d1")
(dom-set-attr _el-d1 "_" "on click blargh end on mouseenter also_bad")
(dom-set-attr _el-d2 "id" "d2")
- (dom-set-attr _el-d2 "_" "on click put \"clicked\" into my.innerHTML")
+ (dom-set-attr _el-d2 "_" "on click put \\\"clicked\\\" into my.innerHTML")
(dom-append (dom-body) _el-div)
(dom-append _el-div _el-d1)
(dom-append _el-div _el-d2)
@@ -8351,7 +8370,7 @@
(hs-cleanup!)
(let ((_el-arDiv (dom-create-element "div")))
(dom-set-attr _el-arDiv "id" "arDiv")
- (dom-set-attr _el-arDiv "_" "on click set my @data-foo to \"blue\"")
+ (dom-set-attr _el-arDiv "_" "on click set my @data-foo to \\\"blue\\\"")
(dom-set-attr _el-arDiv "data-foo" "red")
(dom-append (dom-body) _el-arDiv)
(hs-activate! _el-arDiv)
@@ -8367,7 +8386,7 @@
(dom-set-attr _el-outerDiv2 "id" "outerDiv2")
(dom-set-attr _el-outerDiv2 "foo" "bar")
(dom-set-attr _el-d1b "id" "d1b")
- (dom-set-attr _el-d1b "_" "on click set closest @foo to \"doh\"")
+ (dom-set-attr _el-d1b "_" "on click set closest @foo to \\\"doh\\\"")
(dom-append (dom-body) _el-outerDiv2)
(dom-append _el-outerDiv2 _el-d1b)
(hs-activate! _el-d1b)
@@ -8381,7 +8400,7 @@
(dom-add-class _el-input4 "cb")
(dom-set-attr _el-input4 "type" "checkbox")
(dom-set-attr _el-master "id" "master")
- ;; HS source has bare quotes or embedded HTML
+ (dom-set-attr _el-master "_" "\\\"set")
(dom-set-attr _el-master "type" "checkbox")
(dom-set-attr _el-master "-)), not nested.
+# The directory named "tests" is also in the server's skip_dirs list, so we
+# couldn't use /tests/ anyway.
+PAGES_DIR = os.path.join(PROJECT_ROOT, 'sx/sx/applications/hyperscript')
+GALLERY_SLUG = 'gallery'
+
+
+def page_slug(parts):
+ """Build a dash-joined slug from path parts (theme, category, ...)."""
+ return '-'.join([GALLERY_SLUG] + [p for p in parts if p])
+
+
+def page_url(parts):
+ """Build the full /sx/... URL for a gallery slug."""
+ return f'/sx/(applications.(hyperscript.{page_slug(parts)}))'
+
+# Six themes for grouping categories on the live gallery pages.
+# Any category not listed here gets bucketed into 'misc'.
+TEST_THEMES = {
+ 'dom': ['add', 'remove', 'toggle', 'set', 'put', 'append', 'hide', 'empty',
+ 'take', 'morph', 'show', 'measure', 'swap', 'focus', 'scroll', 'reset'],
+ 'events': ['on', 'when', 'send', 'tell', 'init', 'bootstrap', 'socket',
+ 'dialog', 'wait', 'halt', 'pick', 'fetch', 'asyncError'],
+ 'expressions': ['comparisonOperator', 'mathOperator', 'logicalOperator',
+ 'asExpression', 'collectionExpressions', 'closest', 'increment',
+ 'queryRef', 'attributeRef', 'objectLiteral', 'no', 'default',
+ 'in', 'splitJoin', 'select'],
+ 'control': ['if', 'repeat', 'go', 'call', 'log', 'settle'],
+ 'reactivity': ['bind', 'live', 'liveTemplate', 'reactive-properties',
+ 'transition', 'resize'],
+ 'language': ['def', 'component', 'parser', 'js', 'scoping', 'evalStatically',
+ 'askAnswer', 'assignableElements',
+ 'relativePositionalExpression', 'cookies', 'dom-scope'],
+}
+
+
+def theme_for_category(category):
+ for theme, cats in TEST_THEMES.items():
+ if category in cats:
+ return theme
+ return 'misc'
+
+
+def sx_str(s):
+ """Escape a Python string for inclusion as an SX string literal."""
+ return '"' + s.replace('\\', '\\\\').replace('"', '\\"') + '"'
+
with open(INPUT) as f:
raw_tests = json.load(f)
@@ -530,9 +581,12 @@ def parse_dev_body(body, elements, var_names):
def process_hs_val(hs_val):
"""Process a raw HS attribute value: collapse whitespace, insert 'then' separators."""
- # Convert escaped newlines/tabs to real whitespace before stripping backslashes
+ # Convert escaped newlines/tabs to real whitespace
hs_val = hs_val.replace('\\n', '\n').replace('\\t', ' ')
+ # Preserve escaped quotes (\" → placeholder), strip remaining backslashes, restore
+ hs_val = hs_val.replace('\\"', '\x00QUOT\x00')
hs_val = hs_val.replace('\\', '')
+ hs_val = hs_val.replace('\x00QUOT\x00', '\\"')
cmd_kws = r'(?:set|put|get|add|remove|toggle|hide|show|if|repeat|for|wait|send|trigger|log|call|take|throw|return|append|tell|go|halt|settle|increment|decrement|fetch|make|install|measure|empty|reset|swap|default|morph|render|scroll|focus|select|pick|beep!)'
hs_val = re.sub(r'\s{2,}(?=' + cmd_kws + r'\b)', ' then ', hs_val)
hs_val = re.sub(r'\s*[\n\r]\s*', ' then ', hs_val)
@@ -545,12 +599,16 @@ def process_hs_val(hs_val):
return hs_val.strip()
-def emit_element_setup(lines, elements, var_names):
+def emit_element_setup(lines, elements, var_names, root='(dom-body)', indent=' '):
"""Emit SX for creating elements, setting attributes, appending to DOM, and activating.
+ root — where top-level elements get appended. Default (dom-body); for the gallery
+ card, callers pass a sandbox variable name so the HS runs inside the card, not on
+ the page body.
+
Three phases to ensure correct ordering:
1. Set attributes/content on all elements
- 2. Append elements to their parents (children first, then parents to body)
+ 2. Append elements to their parents (children first, then roots to root)
3. Activate HS handlers (all elements in DOM)
"""
hs_elements = [] # indices of elements with valid HS
@@ -559,41 +617,41 @@ def emit_element_setup(lines, elements, var_names):
for i, el in enumerate(elements):
var = var_names[i]
if el['id']:
- lines.append(f' (dom-set-attr {var} "id" "{el["id"]}")')
+ lines.append(f'{indent}(dom-set-attr {var} "id" "{el["id"]}")')
for cls in el['classes']:
- lines.append(f' (dom-add-class {var} "{cls}")')
+ lines.append(f'{indent}(dom-add-class {var} "{cls}")')
if el['hs']:
hs_val = process_hs_val(el['hs'])
if not hs_val:
pass # no HS to set
elif hs_val.startswith('"') or (hs_val.endswith('"') and '<' in hs_val):
- lines.append(f' ;; HS source has bare quotes or embedded HTML')
+ lines.append(f'{indent};; HS source has bare quotes or embedded HTML')
else:
hs_escaped = hs_val.replace('\\', '\\\\').replace('"', '\\"')
- lines.append(f' (dom-set-attr {var} "_" "{hs_escaped}")')
+ lines.append(f'{indent}(dom-set-attr {var} "_" "{hs_escaped}")')
hs_elements.append(i)
for aname, aval in el['attrs'].items():
if '\\' in aval or '\n' in aval or aname.startswith('['):
- lines.append(f' ;; SKIP attr {aname} (contains special chars)')
+ lines.append(f'{indent};; SKIP attr {aname} (contains special chars)')
continue
aval_escaped = aval.replace('"', '\\"')
- lines.append(f' (dom-set-attr {var} "{aname}" "{aval_escaped}")')
+ lines.append(f'{indent}(dom-set-attr {var} "{aname}" "{aval_escaped}")')
if el['inner']:
inner_escaped = el['inner'].replace('\\', '\\\\').replace('"', '\\"')
- lines.append(f' (dom-set-inner-html {var} "{inner_escaped}")')
+ lines.append(f'{indent}(dom-set-inner-html {var} "{inner_escaped}")')
- # Phase 2: Append elements (children to parents, roots to body)
+ # Phase 2: Append elements (children to parents, roots to `root`)
for i, el in enumerate(elements):
var = var_names[i]
if el['parent_idx'] is not None:
parent_var = var_names[el['parent_idx']]
- lines.append(f' (dom-append {parent_var} {var})')
+ lines.append(f'{indent}(dom-append {parent_var} {var})')
else:
- lines.append(f' (dom-append (dom-body) {var})')
+ lines.append(f'{indent}(dom-append {root} {var})')
# Phase 3: Activate HS handlers (all elements now in DOM)
for i in hs_elements:
- lines.append(f' (hs-activate! {var_names[i]})')
+ lines.append(f'{indent}(hs-activate! {var_names[i]})')
def generate_test_chai(test, elements, var_names, idx):
@@ -905,6 +963,215 @@ def generate_test(test, idx):
return generate_test_chai(test, elements, var_names, idx)
+# ── Live gallery pages ────────────────────────────────────────────
+
+PAGE_HEADER = (
+ ';; AUTO-GENERATED from spec/tests/hyperscript-upstream-tests.json\n'
+ ';; DO NOT EDIT — regenerate with:\n'
+ ';; python3 tests/playwright/generate-sx-tests.py --emit-pages\n'
+)
+
+# Actions/checks that we can't yet compile into a runner body emit a placeholder
+# runner that throws; the card still renders so users can see the source. This
+# keeps gallery coverage 1:1 with the JSON source of truth.
+NOT_DEMONSTRABLE = '(error "not yet runnable in gallery — see test suite")'
+
+
+def emit_runner_body(test, elements, var_names):
+ """Emit the body of the runner lambda that runs inside a sandbox element.
+
+ Returns an SX expression string or None if the test can't be reproduced
+ (no HTML, unparseable action, etc.)."""
+ if not elements:
+ return None
+
+ ref = make_ref_fn(elements, var_names)
+ actions = parse_action(test.get('action', ''), ref)
+ checks_parsed = parse_checks(test.get('check', ''))
+
+ # Skip-only action list (no real action) → nothing to demonstrate
+ real_actions = [a for a in actions if not a.startswith(';;')]
+ if not real_actions:
+ return None
+
+ lines = []
+ bindings = ' '.join(
+ f'({var_names[i]} (dom-create-element "{el["tag"]}"))'
+ for i, el in enumerate(elements)
+ )
+ lines.append(f'(fn (sandbox)')
+ lines.append(f' (let ({bindings})')
+ emit_element_setup(lines, elements, var_names, root='sandbox', indent=' ')
+ for a in actions:
+ lines.append(f' {a}')
+ for c in checks_parsed:
+ sx = check_to_sx(c, ref)
+ lines.append(f' {sx}')
+ lines.append(' ))')
+ return '\n'.join(lines)
+
+
+def emit_card(test):
+ """Return an SX (~hyperscript/hs-test-card ...) call for one test."""
+ name_sx = sx_str(test['name'])
+ html_sx = sx_str(test.get('html', '') or '')
+ action_sx = sx_str(test.get('action', '') or '')
+ check_sx = sx_str(test.get('check', '') or '')
+
+ elements = parse_html(test.get('html', ''))
+ var_names = assign_var_names(elements) if elements else []
+ runner = emit_runner_body(test, elements, var_names)
+ if runner is None:
+ runner = f'(fn (sandbox) {NOT_DEMONSTRABLE})'
+
+ # :run-src is SX SOURCE TEXT — a string the island parses + evals at Run
+ # time. Ordinary lambda kwargs (and even bare quoted `(fn ...)` lists)
+ # end up lambda-ified by the prop pipeline and print as ""
+ # through aser, which can't round-trip. Strings do.
+ run_src = sx_str(runner)
+ return (
+ f'(~hyperscript/hs-test-card\n'
+ f' :name {name_sx}\n'
+ f' :html {html_sx}\n'
+ f' :action {action_sx}\n'
+ f' :check {check_sx}\n'
+ f' :run-src {run_src})'
+ )
+
+
+def emit_category_page(theme, category, tests):
+ """Return SX source for one category page (all tests in that category)."""
+ total = len(tests)
+ runnable = sum(
+ 1 for t in tests
+ if parse_html(t.get('html', '')) and
+ any(not a.startswith(';;') for a in
+ parse_action(t.get('action', ''),
+ make_ref_fn(parse_html(t.get('html', '')),
+ assign_var_names(parse_html(t.get('html', ''))))))
+ )
+ cards = '\n'.join(emit_card(t) for t in tests)
+ title = f'Hyperscript: {category} ({total} tests — {runnable} runnable)'
+ intro = (
+ f'Live cards for the upstream {category} tests. '
+ f'{runnable} of {total} are reproducible in-browser; '
+ f'the remainder show their source for reference.'
+ )
+ return (
+ PAGE_HEADER + '\n'
+ f'(defcomp ()\n'
+ f' (~docs/page :title {sx_str(title)}\n'
+ f' (p :style "color:#57534e;margin-bottom:1rem" {sx_str(intro)})\n'
+ f' (p :style "color:#78716c;font-size:0.875rem;margin-bottom:1rem"\n'
+ f' "Theme: " (a :href {sx_str(page_url([theme]))}\n'
+ f' :style "color:#7c3aed" {sx_str(theme)}))\n'
+ f' (div :style "display:flex;flex-direction:column"\n'
+ f' {cards})))\n'
+ )
+
+
+def emit_theme_index(theme, cats_in_theme, cats_to_tests):
+ """Return SX source for a theme index page (list of its categories)."""
+ total = sum(len(cats_to_tests.get(c, [])) for c in cats_in_theme)
+ links = []
+ for cat in cats_in_theme:
+ if cat not in cats_to_tests:
+ continue
+ n = len(cats_to_tests[cat])
+ href = page_url([theme, cat])
+ links.append(
+ f' (li :style "margin-bottom:0.25rem"\n'
+ f' (a :href {sx_str(href)} :style "color:#7c3aed;text-decoration:underline"\n'
+ f' {sx_str(cat)})\n'
+ f' (span :style "color:#78716c;margin-left:0.5rem;font-size:0.875rem"\n'
+ f' {sx_str(f"({n} tests)")}))'
+ )
+ title = f'Hyperscript tests: {theme} ({total} tests)'
+ return (
+ PAGE_HEADER + '\n'
+ f'(defcomp ()\n'
+ f' (~docs/page :title {sx_str(title)}\n'
+ f' (p :style "color:#57534e;margin-bottom:1rem"\n'
+ f' "Pick a category to see its live test cards.")\n'
+ f' (ul :style "list-style:disc;padding-left:1.5rem"\n'
+ + '\n'.join(links) + '\n'
+ f' )))\n'
+ )
+
+
+def emit_top_index(themes_with_counts):
+ """Return SX source for the top-level /tests index page."""
+ links = []
+ for theme, count in themes_with_counts:
+ href = page_url([theme])
+ links.append(
+ f' (li :style "margin-bottom:0.25rem"\n'
+ f' (a :href {sx_str(href)} :style "color:#7c3aed;text-decoration:underline;font-weight:500"\n'
+ f' {sx_str(theme)})\n'
+ f' (span :style "color:#78716c;margin-left:0.5rem;font-size:0.875rem"\n'
+ f' {sx_str(f"({count} tests)")}))'
+ )
+ grand_total = sum(c for _, c in themes_with_counts)
+ title = f'Hyperscript test gallery ({grand_total} tests)'
+ return (
+ PAGE_HEADER + '\n'
+ f'(defcomp ()\n'
+ f' (~docs/page :title {sx_str(title)}\n'
+ f' (p :style "color:#57534e;margin-bottom:1rem"\n'
+ f' "Live cards for every upstream _hyperscript behavioural test. "\n'
+ f' "Each card renders the HTML into a sandbox, activates the hyperscript, "\n'
+ f' "dispatches the action, and runs the assertion. Pass/fail is shown "\n'
+ f' "with the same runtime path as the SX test suite.")\n'
+ f' (ul :style "list-style:disc;padding-left:1.5rem"\n'
+ + '\n'.join(links) + '\n'
+ f' )))\n'
+ )
+
+
+def write_page_files(categories):
+ """Write gallery files. Everything is flat in applications/hyperscript/ —
+ gallery.sx (top), gallery-.sx, gallery--.sx —
+ because the /sx/ router only dispatches one level per page-fn call."""
+ # Bucket categories by theme
+ themed = OrderedDict() # theme -> [(cat, tests)]
+ for cat, tests in categories.items():
+ theme = theme_for_category(cat)
+ themed.setdefault(theme, []).append((cat, tests))
+
+ # Remove any previous gallery-*.sx files so stale themes don't linger
+ if os.path.isdir(PAGES_DIR):
+ for fname in os.listdir(PAGES_DIR):
+ if fname == f'{GALLERY_SLUG}.sx' or fname.startswith(f'{GALLERY_SLUG}-'):
+ try: os.remove(os.path.join(PAGES_DIR, fname))
+ except OSError: pass
+
+ themes_with_counts = []
+ written = []
+ for theme, cat_pairs in themed.items():
+ cats_in_theme = [c for c, _ in cat_pairs]
+ cats_to_tests = {c: ts for c, ts in cat_pairs}
+
+ for cat, tests in cat_pairs:
+ fname = f'{page_slug([theme, cat])}.sx'
+ with open(os.path.join(PAGES_DIR, fname), 'w') as f:
+ f.write(emit_category_page(theme, cat, tests))
+ written.append(fname)
+
+ fname = f'{page_slug([theme])}.sx'
+ with open(os.path.join(PAGES_DIR, fname), 'w') as f:
+ f.write(emit_theme_index(theme, cats_in_theme, cats_to_tests))
+ written.append(fname)
+
+ themes_with_counts.append((theme, sum(len(ts) for _, ts in cat_pairs)))
+
+ fname = f'{GALLERY_SLUG}.sx'
+ with open(os.path.join(PAGES_DIR, fname), 'w') as f:
+ f.write(emit_top_index(themes_with_counts))
+ written.append(fname)
+
+ return themed, written
+
+
# ── Output generation ─────────────────────────────────────────────
output = []
@@ -989,3 +1256,15 @@ print(f' Categories: {len(categories)}')
for cat, (gen, stub) in generated_counts.items():
marker = '' if stub == 0 else f' ({stub} stubs)'
print(f' {cat}: {gen}{marker}')
+
+
+# ── Optional: live gallery pages ──────────────────────────────────
+
+import sys
+if '--emit-pages' in sys.argv:
+ themed, written = write_page_files(categories)
+ print(f'\nGallery pages written under {PAGES_DIR} ({len(written)} files)')
+ for theme, pairs in themed.items():
+ cats = ', '.join(c for c, _ in pairs)
+ total_t = sum(len(ts) for _, ts in pairs)
+ print(f' {theme} ({total_t} tests, {len(pairs)} categories): {cats}')