HS toggle style: parse between/cycle, runtime, mock style dict

Parser:
- Reorder toggle style parsing: target before between clause
- Handle "indexed" keyword, "indexed by" syntax
- Use parse-atom (not parse-expr) for between values to avoid
  consuming "and" as boolean operator
- Support 3-4 value cycles via toggle-style-cycle

Compiler:
- Add toggle-style-cycle dispatch → hs-toggle-style-cycle!

Runtime:
- Add hs-toggle-style-between! (2-value toggle)
- Add hs-toggle-style-cycle! (N-value round-robin)

Mock DOM:
- Parse CSS strings from setAttribute "style" into style sub-dict
  so dom-get-style/dom-set-style work correctly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-18 21:35:00 +00:00
parent 06bed36272
commit 00bf13a230
4 changed files with 156 additions and 77 deletions

View File

@@ -1815,6 +1815,22 @@ let run_spec_tests env test_files =
Hashtbl.replace d "className" (String sv); Hashtbl.replace d "className" (String sv);
end; end;
if name = "disabled" then Hashtbl.replace d "disabled" (Bool true); if name = "disabled" then Hashtbl.replace d "disabled" (Bool true);
if name = "style" then begin
(* Parse CSS string into the style sub-dict *)
let style_d = match Hashtbl.find_opt d "style" with Some (Dict s) -> s | _ ->
let s = Hashtbl.create 4 in Hashtbl.replace d "style" (Dict s); s in
let parts = String.split_on_char ';' sv in
List.iter (fun part ->
let part = String.trim part in
if String.length part > 0 then
match String.index_opt part ':' with
| Some i ->
let prop = String.trim (String.sub part 0 i) in
let value = String.trim (String.sub part (i+1) (String.length part - i - 1)) in
Hashtbl.replace style_d prop (String value)
| None -> ()
) parts
end;
Nil Nil
| _ -> Nil) | _ -> Nil)
| "getAttribute" -> | "getAttribute" ->

View File

@@ -1008,6 +1008,14 @@
(nth ast 1) (nth ast 1)
(hs-to-sx (nth ast 2)) (hs-to-sx (nth ast 2))
(hs-to-sx (nth ast 3)))) (hs-to-sx (nth ast 3))))
((= head (quote toggle-style-cycle))
(list
(quote hs-toggle-style-cycle!)
(hs-to-sx (nth ast 2))
(nth ast 1)
(cons
(quote list)
(map hs-to-sx (slice ast 3 (len ast))))))
((= head (quote toggle-attr)) ((= head (quote toggle-attr))
(list (list
(quote hs-toggle-attr!) (quote hs-toggle-attr!)

View File

@@ -935,40 +935,64 @@
(list (quote toggle-class) cls tgt))))) (list (quote toggle-class) cls tgt)))))
((= (tp-type) "style") ((= (tp-type) "style")
(let (let
((prop (do (let ((v (tp-val))) (adv!) v)))) ((prop (get (adv!) "value")))
(if (let
(match-kw "between") ((tgt (if (match-kw "of") (parse-expr) (list (quote me)))))
(let (if
((val1 (parse-atom))) (match-kw "between")
(expect-kw! "and")
(let (let
((val2 (parse-atom))) ((val1 (parse-atom)))
(expect-kw! "and")
(let (let
((tgt (parse-tgt-kw "on" (list (quote me))))) ((val2 (parse-atom)))
(list (quote toggle-style-between) prop val1 val2 tgt)))) (if
(let (match-kw "and")
((tgt (parse-tgt-kw "on" (list (quote me))))) (let
((val3 (parse-atom)))
(if
(match-kw "and")
(let
((val4 (parse-atom)))
(list
(quote toggle-style-cycle)
prop
tgt
val1
val2
val3
val4))
(list
(quote toggle-style-cycle)
prop
tgt
val1
val2
val3)))
(list
(quote toggle-style-between)
prop
val1
val2
tgt))))
(list (quote toggle-style) prop tgt))))) (list (quote toggle-style) prop tgt)))))
((= (tp-type) "attr") ((= (tp-type) "attr")
(let (let
((attr-name (do (let ((v (tp-val))) (adv!) v)))) ((attr-name (get (adv!) "value")))
(if (let
(match-kw "between") ((tgt (if (match-kw "on") (parse-expr) (list (quote me)))))
(let (if
((val1 (parse-atom))) (match-kw "between")
(expect-kw! "and")
(let (let
((val2 (parse-atom))) ((val1 (parse-expr)))
(expect-kw! "and")
(let (let
((tgt (parse-tgt-kw "on" (list (quote me))))) ((val2 (parse-expr)))
(list (list
(quote toggle-attr-between) (quote toggle-attr-between)
attr-name attr-name
val1 val1
val2 val2
tgt)))) tgt)))
(let
((tgt (parse-tgt-kw "on" (list (quote me)))))
(list (quote toggle-attr) attr-name tgt))))) (list (quote toggle-attr) attr-name tgt)))))
((and (= (tp-type) "keyword") (= (tp-val) "my")) ((and (= (tp-type) "keyword") (= (tp-val) "my"))
(do (do

View File

@@ -99,6 +99,39 @@
;; ── Navigation / traversal ────────────────────────────────────── ;; ── Navigation / traversal ──────────────────────────────────────
;; Navigate to a URL. ;; Navigate to a URL.
(define
hs-toggle-style-between!
(fn
(target prop val1 val2)
(let
((cur (dom-get-style target prop)))
(if
(= cur val1)
(dom-set-style target prop val2)
(dom-set-style target prop val1)))))
;; Find next sibling matching a selector (or any sibling).
(define
hs-toggle-style-cycle!
(fn
(target prop vals)
(let
((cur (dom-get-style target prop)))
(define
find-next
(fn
(remaining)
(cond
((empty? remaining) (first vals))
((= cur (first remaining))
(if
(empty? (rest remaining))
(first vals)
(first (rest remaining))))
(true (find-next (rest remaining))))))
(dom-set-style target prop (find-next vals)))))
;; Find previous sibling matching a selector.
(define (define
hs-take! hs-take!
(fn (fn
@@ -122,7 +155,7 @@
(dom-set-attr target name attr-val) (dom-set-attr target name attr-val)
(dom-set-attr target name "")))))))) (dom-set-attr target name ""))))))))
;; Find next sibling matching a selector (or any sibling). ;; First element matching selector within a scope.
(define (define
hs-put! hs-put!
(fn (fn
@@ -135,7 +168,7 @@
((= pos "start") (dom-insert-adjacent-html target "afterbegin" value)) ((= pos "start") (dom-insert-adjacent-html target "afterbegin" value))
((= pos "end") (dom-insert-adjacent-html target "beforeend" value))))) ((= pos "end") (dom-insert-adjacent-html target "beforeend" value)))))
;; Find previous sibling matching a selector. ;; Last element matching selector.
(define (define
hs-add-to! hs-add-to!
(fn (fn
@@ -145,7 +178,7 @@
(append target (list value)) (append target (list value))
(host-call target "push" value)))) (host-call target "push" value))))
;; First element matching selector within a scope. ;; First/last within a specific scope.
(define (define
hs-remove-from! hs-remove-from!
(fn (fn
@@ -155,16 +188,18 @@
(filter (fn (x) (not (= x value))) target) (filter (fn (x) (not (= x value))) target)
(host-call target "splice" (host-call target "indexOf" value) 1)))) (host-call target "splice" (host-call target "indexOf" value) 1))))
;; Last element matching selector.
(define (define
hs-set-on! hs-set-on!
(fn (fn
(props target) (props target)
(for-each (fn (k) (host-set! target k (get props k))) (keys props)))) (for-each (fn (k) (host-set! target k (get props k))) (keys props))))
;; First/last within a specific scope. ;; ── Iteration ───────────────────────────────────────────────────
;; Repeat a thunk N times.
(define hs-navigate! (fn (url) (perform (list (quote io-navigate) url)))) (define hs-navigate! (fn (url) (perform (list (quote io-navigate) url))))
;; Repeat forever (until break — relies on exception/continuation).
(define (define
hs-scroll! hs-scroll!
(fn (fn
@@ -177,9 +212,10 @@
((= position "bottom") (dict :block "end")) ((= position "bottom") (dict :block "end"))
(true (dict :block "start"))))))) (true (dict :block "start")))))))
;; ── Iteration ─────────────────────────────────────────────────── ;; ── Fetch ───────────────────────────────────────────────────────
;; Repeat a thunk N times. ;; Fetch a URL, parse response according to format.
;; (hs-fetch url format) — format is "json" | "text" | "html"
(define (define
hs-halt! hs-halt!
(fn (fn
@@ -189,19 +225,23 @@
(host-call event "preventDefault" (list)) (host-call event "preventDefault" (list))
(when (= mode "event") (host-call event "stopPropagation" (list)))))) (when (= mode "event") (host-call event "stopPropagation" (list))))))
;; Repeat forever (until break — relies on exception/continuation).
(define hs-select! (fn (target) (host-call target "select" (list))))
;; ── Fetch ───────────────────────────────────────────────────────
;; Fetch a URL, parse response according to format.
;; (hs-fetch url format) — format is "json" | "text" | "html"
(define hs-reset! (fn (target) (host-call target "reset" (list))))
;; ── Type coercion ─────────────────────────────────────────────── ;; ── Type coercion ───────────────────────────────────────────────
;; Coerce a value to a type by name. ;; Coerce a value to a type by name.
;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc. ;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc.
(define hs-select! (fn (target) (host-call target "select" (list))))
;; ── Object creation ─────────────────────────────────────────────
;; Make a new object of a given type.
;; (hs-make type-name) — creates empty object/collection
(define hs-reset! (fn (target) (host-call target "reset" (list))))
;; ── Behavior installation ───────────────────────────────────────
;; Install a behavior on an element.
;; A behavior is a function that takes (me ...params) and sets up features.
;; (hs-install behavior-fn me ...args)
(define (define
hs-next hs-next
(fn (fn
@@ -221,10 +261,10 @@
(true (find-next (dom-next-sibling el)))))) (true (find-next (dom-next-sibling el))))))
(find-next sibling))))) (find-next sibling)))))
;; ── Object creation ───────────────────────────────────────────── ;; ── Measurement ─────────────────────────────────────────────────
;; Make a new object of a given type. ;; Measure an element's bounding rect, store as local variables.
;; (hs-make type-name) — creates empty object/collection ;; Returns a dict with x, y, width, height, top, left, right, bottom.
(define (define
hs-previous hs-previous
(fn (fn
@@ -244,27 +284,18 @@
(true (find-prev (dom-get-prop el "previousElementSibling")))))) (true (find-prev (dom-get-prop el "previousElementSibling"))))))
(find-prev sibling))))) (find-prev sibling)))))
;; ── Behavior installation ───────────────────────────────────────
;; Install a behavior on an element.
;; A behavior is a function that takes (me ...params) and sets up features.
;; (hs-install behavior-fn me ...args)
(define
hs-query-all
(fn (sel) (host-call (dom-body) "querySelectorAll" sel)))
;; ── Measurement ─────────────────────────────────────────────────
;; Measure an element's bounding rect, store as local variables.
;; Returns a dict with x, y, width, height, top, left, right, bottom.
(define
hs-query-first
(fn (sel) (host-call (host-global "document") "querySelector" sel)))
;; ── Transition ────────────────────────────────────────────────── ;; ── Transition ──────────────────────────────────────────────────
;; Transition a CSS property to a value, optionally with duration. ;; Transition a CSS property to a value, optionally with duration.
;; (hs-transition target prop value duration) ;; (hs-transition target prop value duration)
(define
hs-query-all
(fn (sel) (host-call (dom-body) "querySelectorAll" sel)))
(define
hs-query-first
(fn (sel) (host-call (host-global "document") "querySelector" sel)))
(define (define
hs-query-last hs-query-last
(fn (fn
@@ -410,6 +441,10 @@
((or (string? a) (string? b)) (str a b)) ((or (string? a) (string? b)) (str a b))
(true (+ a b))))) (true (+ a b)))))
(define (define
hs-make hs-make
(fn (fn
@@ -422,15 +457,12 @@
(true (dict))))) (true (dict)))))
(define hs-install (fn (behavior-fn) (behavior-fn me))) (define hs-install (fn (behavior-fn) (behavior-fn me)))
;; ── Sandbox/test runtime additions ──────────────────────────────
;; Property access — dot notation and .length
(define (define
hs-measure hs-measure
(fn (target) (perform (list (quote io-measure) target)))) (fn (target) (perform (list (quote io-measure) target))))
;; DOM query stub — sandbox returns empty list
(define (define
hs-transition hs-transition
(fn (fn
@@ -443,8 +475,7 @@
(str prop " " (/ duration 1000) "s"))) (str prop " " (/ duration 1000) "s")))
(dom-set-style target prop value) (dom-set-style target prop value)
(when duration (hs-settle target)))) (when duration (hs-settle target))))
;; ── Sandbox/test runtime additions ────────────────────────────── ;; Method dispatch — obj.method(args)
;; Property access — dot notation and .length
(define (define
hs-transition-from hs-transition-from
(fn (fn
@@ -458,7 +489,9 @@
(str prop " " (/ duration 1000) "s"))) (str prop " " (/ duration 1000) "s")))
(dom-set-style target prop (str to-val)) (dom-set-style target prop (str to-val))
(when duration (hs-settle target)))) (when duration (hs-settle target))))
;; DOM query stub — sandbox returns empty list
;; ── 0.9.90 features ─────────────────────────────────────────────
;; beep! — debug logging, returns value unchanged
(define (define
hs-type-check hs-type-check
(fn (fn
@@ -478,33 +511,31 @@
(= (host-typeof value) "element") (= (host-typeof value) "element")
(= (host-typeof value) "text"))) (= (host-typeof value) "text")))
(true (= (host-typeof value) (downcase type-name))))))) (true (= (host-typeof value) (downcase type-name)))))))
;; Method dispatch — obj.method(args) ;; Property-based is — check obj.key truthiness
(define (define
hs-type-check-strict hs-type-check-strict
(fn (fn
(value type-name) (value type-name)
(if (nil? value) false (hs-type-check value type-name)))) (if (nil? value) false (hs-type-check value type-name))))
;; Array slicing (inclusive both ends)
;; ── 0.9.90 features ─────────────────────────────────────────────
;; beep! — debug logging, returns value unchanged
(define (define
hs-strict-eq hs-strict-eq
(fn (a b) (and (= (type-of a) (type-of b)) (= a b)))) (fn (a b) (and (= (type-of a) (type-of b)) (= a b))))
;; Property-based is — check obj.key truthiness ;; Collection: sorted by
(define (define
hs-eq-ignore-case hs-eq-ignore-case
(fn (a b) (= (downcase (str a)) (downcase (str b))))) (fn (a b) (= (downcase (str a)) (downcase (str b)))))
;; Array slicing (inclusive both ends) ;; Collection: sorted by descending
(define (define
hs-starts-with-ic? hs-starts-with-ic?
(fn (str prefix) (starts-with? (downcase str) (downcase prefix)))) (fn (str prefix) (starts-with? (downcase str) (downcase prefix))))
;; Collection: sorted by ;; Collection: split by
(define (define
hs-contains-ignore-case? hs-contains-ignore-case?
(fn (fn
(haystack needle) (haystack needle)
(contains? (downcase (str haystack)) (downcase (str needle))))) (contains? (downcase (str haystack)) (downcase (str needle)))))
;; Collection: sorted by descending ;; Collection: joined by
(define (define
hs-falsy? hs-falsy?
(fn (fn
@@ -516,7 +547,7 @@
((and (list? v) (= (len v) 0)) true) ((and (list? v) (= (len v) 0)) true)
((= v 0) true) ((= v 0) true)
(true false)))) (true false))))
;; Collection: split by
(define (define
hs-matches? hs-matches?
(fn (fn
@@ -527,7 +558,7 @@
((= (host-typeof target) "element") ((= (host-typeof target) "element")
(if (string? pattern) (host-call target "matches" pattern) false)) (if (string? pattern) (host-call target "matches" pattern) false))
(true false)))) (true false))))
;; Collection: joined by
(define (define
hs-contains? hs-contains?
(fn (fn