HS behavioral tests: 478→509/831 (57%→61%), parser/compiler/runtime fixes

Parser: am-a/am-not-a type checks, transition element/selector targeting,
take @attr=value with replacement, toggle my/the possessive, <selector/>
syntax in parse-atom, the-of for style/attr/class/selector, when-clause
filtering for add, starts/ends-with ignoring case.

Compiler: take attr passthrough, toggle-style nil→me default, scoped
querySelectorAll for add/remove/toggle-class, has-class? entry, matches?
extracts selector from (query sel), add-class-when with for-each filter,
starts/ends-with-ic entries, hs-add replaces + for polymorphic add.

Runtime: hs-take! proper attr values, hs-type-check Element/Node via
host-typeof, hs-toggle-style! opacity 0↔1, hs-coerce +8 coercions
(Keys/Values/Entries/Reversed/Unique/Flat/JSON/Object), hs-query-all
bypasses broken dom-query-all (WASM auto-converts arrays), hs-matches?
handles DOM el.matches(selector), hs-add list+string+number polymorphic,
hs-starts/ends-with-ic for case-insensitive comparison.

DOM mock: mkStyle() with setProperty/getPropertyValue, fndAll.item().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 12:53:43 +00:00
parent 1e42451252
commit 684a46297d
7 changed files with 793 additions and 220 deletions

View File

@@ -88,7 +88,7 @@
((or (= prop "display") (= prop "opacity"))
(if
(or (= cur "none") (= cur "0"))
(dom-set-style target prop "")
(dom-set-style target prop (if (= prop "opacity") "1" ""))
(dom-set-style target prop (if (= prop "display") "none" "0"))))
(true
(if
@@ -102,17 +102,30 @@
(define
hs-take!
(fn
(target kind name scope)
(target kind name scope &rest extra)
(let
((els (if scope (if (list? scope) scope (list scope)) (let ((parent (host-get target "parentNode"))) (if parent (dom-child-list parent) (list))))))
((els (if scope (if (list? scope) scope (list scope)) (let ((parent (dom-parent target))) (if parent (dom-child-list parent) (list))))))
(if
(= kind "class")
(do
(for-each (fn (el) (dom-remove-class el name)) els)
(dom-add-class target name))
(do
(for-each (fn (el) (dom-remove-attr el name)) els)
(dom-set-attr target name "true"))))))
(let
((attr-val (if (> (len extra) 0) (first extra) nil))
(with-val (if (> (len extra) 1) (nth extra 1) nil)))
(do
(for-each
(fn
(el)
(if
with-val
(dom-set-attr el name with-val)
(dom-remove-attr el name)))
els)
(if
attr-val
(dom-set-attr target name attr-val)
(dom-set-attr target name ""))))))))
;; Find next sibling matching a selector (or any sibling).
(define
@@ -204,7 +217,9 @@
;; Fetch a URL, parse response according to format.
;; (hs-fetch url format) — format is "json" | "text" | "html"
(define hs-query-all (fn (sel) (dom-query-all (dom-body) sel)))
(define
hs-query-all
(fn (sel) (host-call (dom-body) "querySelectorAll" sel)))
;; ── Type coercion ───────────────────────────────────────────────
@@ -290,29 +305,72 @@
((= type-name "Bool") (not (hs-falsy? value)))
((= type-name "Boolean") (not (hs-falsy? value)))
((= type-name "Array") (if (list? value) value (list value)))
((= type-name "JSON") (str value))
((= type-name "Object") (if (string? value) value value))
((or (= type-name "Fixed") (string-contains? type-name "Fixed:"))
((= type-name "HTML") (str value))
((= type-name "JSON")
(if
(string? value)
value
(host-call (host-global "JSON") "stringify" value)))
((= type-name "Object")
(if
(string? value)
(host-call (host-global "JSON") "parse" value)
value))
((or (= type-name "Fixed") (= type-name "Fixed:"))
(let
((digits (if (string-contains? type-name ":") (parse-number (nth (split type-name ":") 1)) 0))
((digits (if (> (string-length type-name) 6) (+ (substring type-name 6 (string-length type-name)) 0) 0))
(num (+ value 0)))
(if
(= digits 0)
(str (floor num))
(let
((factor (reduce (fn (acc _) (* acc 10)) 1 (range 0 digits))))
(let
((rounded (/ (floor (+ (* num factor) 0.5)) factor)))
(str rounded))))))
((= type-name "HTML") (str value))
((= type-name "Values") value)
((= type-name "Fragment") (str value))
((= type-name "Date") (str value))
((factor (** 10 digits)))
(str (/ (floor (+ (* num factor) 0.5)) factor))))))
((= type-name "Selector") (str value))
((= type-name "Fragment") value)
((= type-name "Values")
(if
(dict? value)
(map (fn (k) (get value k)) (keys value))
value))
((= type-name "Keys") (if (dict? value) (keys value) value))
((= type-name "Entries")
(if
(dict? value)
(map (fn (k) (list k (get value k))) (keys value))
value))
((= type-name "Reversed") (if (list? value) (reverse value) value))
((= type-name "Unique")
(if
(list? value)
(reduce
(fn
(acc x)
(if (some (fn (a) (= a x)) acc) acc (append acc (list x))))
(list)
value)
value))
((or (= type-name "Flattened") (= type-name "Flat"))
(if
(list? value)
(reduce
(fn
(acc x)
(if (list? x) (append acc x) (append acc (list x))))
(list)
value)
value))
(true value))))
(define
hs-add
(fn (a b) (if (or (string? a) (string? b)) (str a b) (+ a b))))
(fn
(a b)
(cond
((list? a) (if (list? b) (append a b) (append a (list b))))
((list? b) (cons a b))
((or (string? a) (string? b)) (str a b))
(true (+ a b)))))
(define
hs-make
@@ -371,7 +429,12 @@
((= type-name "Boolean") (or (= value true) (= value false)))
((= type-name "Array") (list? value))
((= type-name "Object") (dict? value))
(true true)))))
((= type-name "Element") (= (host-typeof value) "element"))
((= type-name "Node")
(or
(= (host-typeof value) "element")
(= (host-typeof value) "text")))
(true (= (host-typeof value) (downcase type-name)))))))
@@ -392,12 +455,18 @@
hs-eq-ignore-case
(fn (a b) (= (downcase (str a)) (downcase (str b)))))
;; DOM query stub — sandbox returns empty list
(define
hs-starts-with-ic?
(fn (str prefix) (starts-with? (downcase str) (downcase prefix))))
;; Method dispatch — obj.method(args)
(define
hs-contains-ignore-case?
(fn
(haystack needle)
(contains? (downcase (str haystack)) (downcase (str needle)))))
;; Method dispatch — obj.method(args)
;; ── 0.9.90 features ─────────────────────────────────────────────
;; beep! — debug logging, returns value unchanged
(define
hs-falsy?
(fn
@@ -409,18 +478,18 @@
((and (list? v) (= (len v) 0)) true)
((= v 0) true)
(true false))))
;; ── 0.9.90 features ─────────────────────────────────────────────
;; beep! — debug logging, returns value unchanged
;; Property-based is — check obj.key truthiness
(define
hs-matches?
(fn
(target pattern)
(if
(string? target)
(if (= pattern ".*") true (string-contains? target pattern))
false)))
;; Property-based is — check obj.key truthiness
(cond
((string? target)
(if (= pattern ".*") true (string-contains? target pattern)))
((= (host-typeof target) "element")
(if (string? pattern) (host-call target "matches" pattern) false))
(true false))))
;; Array slicing (inclusive both ends)
(define
hs-contains?
(fn
@@ -440,9 +509,9 @@
true
(hs-contains? (rest collection) item)))))
(true false))))
;; Array slicing (inclusive both ends)
(define precedes? (fn (a b) (< (str a) (str b))))
;; Collection: sorted by
(define precedes? (fn (a b) (< (str a) (str b))))
;; Collection: sorted by descending
(define
hs-empty?
(fn
@@ -453,7 +522,7 @@
((list? v) (= (len v) 0))
((dict? v) (= (len (keys v)) 0))
(true false))))
;; Collection: sorted by descending
;; Collection: split by
(define
hs-empty-target!
(fn
@@ -474,7 +543,7 @@
(dom-set-prop target "value" ""))))
((= tag "FORM") (dom-set-inner-html target ""))
(true (dom-set-inner-html target ""))))))))
;; Collection: split by
;; Collection: joined by
(define
hs-open!
(fn
@@ -485,7 +554,7 @@
(= tag "DIALOG")
(host-call el "showModal")
(dom-set-prop el "open" true)))))
;; Collection: joined by
(define
hs-close!
(fn