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

@@ -56,32 +56,39 @@
(hs-to-sx (nth target 2)) (hs-to-sx (nth target 2))
value)) value))
((= th (quote of)) ((= th (quote of))
;; Decompose (of prop-expr target) into a set operation (let
;; e.g. (of (. (ref "parentNode") "innerHTML") (query "#d1")) ((prop-ast (nth target 1)) (obj-ast (nth target 2)))
;; → set parentNode.innerHTML of #d1 → need to navigate target, then set final prop (if
(let ((prop-ast (nth target 1)) (and (list? prop-ast) (= (first prop-ast) dot-sym))
(obj-ast (nth target 2))) (let
(if (and (list? prop-ast) (= (first prop-ast) dot-sym)) ((base (nth prop-ast 1))
;; (. base "prop") of obj → (dom-set-prop (host-get (compiled-obj) (compiled-base-name)) "prop" value) (prop-name (nth prop-ast 2)))
(let ((base (nth prop-ast 1)) (list
(prop-name (nth prop-ast 2))) (quote dom-set-prop)
(list (quote dom-set-prop) (list
(list (quote host-get) (hs-to-sx obj-ast) (nth base 1)) (quote host-get)
(hs-to-sx obj-ast)
(nth base 1))
prop-name prop-name
value)) value))
;; (attr "name") of obj → (dom-set-attr (compiled-obj) "name" value) (if
(if (and (list? prop-ast) (= (first prop-ast) (quote attr))) (and
(list (quote dom-set-attr) (list? prop-ast)
(= (first prop-ast) (quote attr)))
(list
(quote dom-set-attr)
(hs-to-sx obj-ast) (hs-to-sx obj-ast)
(nth prop-ast 1) (nth prop-ast 1)
value) value)
;; Simple: (ref "prop") of obj → (dom-set-prop (compiled-obj) "prop" value) (if
(if (and (list? prop-ast) (= (first prop-ast) (quote ref))) (and
(list (quote dom-set-prop) (list? prop-ast)
(= (first prop-ast) (quote ref)))
(list
(quote dom-set-prop)
(hs-to-sx obj-ast) (hs-to-sx obj-ast)
(nth prop-ast 1) (nth prop-ast 1)
value) value)
;; Fallback
(list (quote set!) (hs-to-sx target) value)))))) (list (quote set!) (hs-to-sx target) value))))))
(true (list (quote set!) (hs-to-sx target) value))))))) (true (list (quote set!) (hs-to-sx target) value)))))))
(define (define
@@ -427,7 +434,7 @@
((= head (quote null-literal)) nil) ((= head (quote null-literal)) nil)
((= head (quote not)) ((= head (quote not))
(list (quote not) (hs-to-sx (nth ast 1)))) (list (quote not) (hs-to-sx (nth ast 1))))
((or (= head (quote starts-with?)) (= head (quote ends-with?)) (= head (quote contains?)) (= head (quote matches?)) (= head (quote precedes?)) (= head (quote follows?)) (= head (quote exists?))) ((or (= head (quote starts-with?)) (= head (quote ends-with?)) (= head (quote contains?)) (= head (quote precedes?)) (= head (quote follows?)) (= head (quote exists?)))
(cons head (map hs-to-sx (rest ast)))) (cons head (map hs-to-sx (rest ast))))
((= head (quote object-literal)) ((= head (quote object-literal))
(let (let
@@ -656,6 +663,11 @@
(quote dom-get-style) (quote dom-get-style)
(hs-to-sx (nth ast 2)) (hs-to-sx (nth ast 2))
(nth ast 1))) (nth ast 1)))
((= head (quote has-class?))
(list
(quote dom-has-class?)
(hs-to-sx (nth ast 1))
(nth ast 2)))
((= head (quote local)) (make-symbol (nth ast 1))) ((= head (quote local)) (make-symbol (nth ast 1)))
((= head (quote array)) ((= head (quote array))
(cons (quote list) (map hs-to-sx (rest ast)))) (cons (quote list) (map hs-to-sx (rest ast))))
@@ -713,15 +725,30 @@
(quote not) (quote not)
(list (quote nil?) (hs-to-sx (nth ast 1))))) (list (quote nil?) (hs-to-sx (nth ast 1)))))
((= head (quote matches?)) ((= head (quote matches?))
(list (let
(quote hs-matches?) ((left (nth ast 1)) (right (nth ast 2)))
(hs-to-sx (nth ast 1)) (if
(hs-to-sx (nth ast 2)))) (and (list? right) (= (first right) (quote query)))
(list (quote hs-matches?) (hs-to-sx left) (nth right 1))
(list
(quote hs-matches?)
(hs-to-sx left)
(hs-to-sx right)))))
((= head (quote matches-ignore-case?)) ((= head (quote matches-ignore-case?))
(list (list
(quote hs-matches-ignore-case?) (quote hs-matches-ignore-case?)
(hs-to-sx (nth ast 1)) (hs-to-sx (nth ast 1))
(hs-to-sx (nth ast 2)))) (hs-to-sx (nth ast 2))))
((= head (quote starts-with-ic?))
(list
(quote hs-starts-with-ic?)
(hs-to-sx (nth ast 1))
(hs-to-sx (nth ast 2))))
((= head (quote ends-with-ic?))
(list
(quote hs-ends-with-ic?)
(hs-to-sx (nth ast 1))
(hs-to-sx (nth ast 2))))
((= head (quote contains?)) ((= head (quote contains?))
(list (list
(quote hs-contains?) (quote hs-contains?)
@@ -824,6 +851,23 @@
(map (map
(fn (cls) (list (quote dom-add-class) target cls)) (fn (cls) (list (quote dom-add-class) target cls))
classes)))) classes))))
((= head (quote add-class-when))
(let
((cls (nth ast 1))
(raw-tgt (nth ast 2))
(when-cond (nth ast 3)))
(let
((tgt-expr (cond ((and (list? raw-tgt) (= (first raw-tgt) (quote query))) (list (quote hs-query-all) (nth raw-tgt 1))) ((and (list? raw-tgt) (= (first raw-tgt) (quote in?)) (list? (nth raw-tgt 1)) (= (first (nth raw-tgt 1)) (quote query))) (list (quote host-call) (hs-to-sx (nth raw-tgt 2)) "querySelectorAll" (nth (nth raw-tgt 1) 1))) (true (hs-to-sx raw-tgt)))))
(list
(quote for-each)
(list
(quote fn)
(list (quote it))
(list
(quote when)
(hs-to-sx when-cond)
(list (quote dom-add-class) (quote it) cls)))
tgt-expr))))
((= head (quote multi-remove-class)) ((= head (quote multi-remove-class))
(let (let
((target (hs-to-sx (nth ast 1))) ((target (hs-to-sx (nth ast 1)))
@@ -895,10 +939,12 @@
(nth ast 1) (nth ast 1)
(nth ast 2))) (nth ast 2)))
((= head (quote toggle-style)) ((= head (quote toggle-style))
(list (let
(quote hs-toggle-style!) ((raw-tgt (nth ast 2)))
(hs-to-sx (nth ast 2)) (list
(nth ast 1))) (quote hs-toggle-style!)
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt))
(nth ast 1))))
((= head (quote toggle-style-between)) ((= head (quote toggle-style-between))
(list (list
(quote hs-toggle-style-between!) (quote hs-toggle-style-between!)

View File

@@ -513,12 +513,26 @@
(do (do
(adv!) (adv!)
(match-kw "with") (match-kw "with")
(list (quote starts-with?) left (parse-expr)))) (let
((rhs (parse-atom)))
(if
(match-kw "ignoring")
(do
(match-kw "case")
(list (quote starts-with-ic?) left rhs))
(list (quote starts-with?) left rhs)))))
((and (or (= typ "keyword") (= typ "ident")) (= val "ends")) ((and (or (= typ "keyword") (= typ "ident")) (= val "ends"))
(do (do
(adv!) (adv!)
(match-kw "with") (match-kw "with")
(list (quote ends-with?) left (parse-expr)))) (let
((rhs (parse-atom)))
(if
(match-kw "ignoring")
(do
(match-kw "case")
(list (quote ends-with-ic?) left rhs))
(list (quote ends-with?) left rhs)))))
((and (= typ "keyword") (= val "matches")) ((and (= typ "keyword") (= val "matches"))
(do (do
(adv!) (adv!)
@@ -607,11 +621,17 @@
(quote not) (quote not)
(list (quote contains?) left (parse-expr)))) (list (quote contains?) left (parse-expr))))
((match-kw "start") ((match-kw "start")
(do (match-kw "with") (do
(list (quote not) (list (quote starts-with?) left (parse-expr))))) (match-kw "with")
(list
(quote not)
(list (quote starts-with?) left (parse-expr)))))
((match-kw "end") ((match-kw "end")
(do (match-kw "with") (do
(list (quote not) (list (quote ends-with?) left (parse-expr))))) (match-kw "with")
(list
(quote not)
(list (quote ends-with?) left (parse-expr)))))
(true left)))) (true left))))
((and (= typ "keyword") (= val "equals")) ((and (= typ "keyword") (= val "equals"))
(do (adv!) (list (quote =) left (parse-expr)))) (do (adv!) (list (quote =) left (parse-expr))))
@@ -693,12 +713,20 @@
nil nil
(do (do
(when (when
(and (number? left) (= (tp-type) "ident") (and
(not (or (= (tp-val) "starts") (= (tp-val) "ends") (number? left)
(= (tp-val) "contains") (= (tp-val) "matches") (= (tp-type) "ident")
(= (tp-val) "is") (= (tp-val) "does") (not
(= (tp-val) "in") (= (tp-val) "precedes") (or
(= (tp-val) "follows")))) (= (tp-val) "starts")
(= (tp-val) "ends")
(= (tp-val) "contains")
(= (tp-val) "matches")
(= (tp-val) "is")
(= (tp-val) "does")
(= (tp-val) "in")
(= (tp-val) "precedes")
(= (tp-val) "follows"))))
(let (let
((unit (tp-val))) ((unit (tp-val)))
(do (do
@@ -757,12 +785,25 @@
(collect-classes!) (collect-classes!)
(let (let
((tgt (parse-tgt-kw "to" (list (quote me))))) ((tgt (parse-tgt-kw "to" (list (quote me)))))
(if (let
(empty? extra-classes) ((when-clause (if (match-kw "when") (parse-expr) nil)))
(list (quote add-class) cls tgt) (if
(cons (empty? extra-classes)
(quote multi-add-class) (if
(cons tgt (cons cls extra-classes)))))) when-clause
(list (quote add-class-when) cls tgt when-clause)
(list (quote add-class) cls tgt))
(if
when-clause
(list
(quote multi-add-class-when)
tgt
when-clause
cls
extra-classes)
(cons
(quote multi-add-class)
(cons tgt (cons cls extra-classes))))))))
nil))) nil)))
(define (define
parse-remove-cmd parse-remove-cmd
@@ -893,6 +934,70 @@
(let (let
((tgt (parse-tgt-kw "on" (list (quote me))))) ((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"))
(do
(adv!)
(cond
((= (tp-type) "style")
(let
((prop (get (adv!) "value")))
(if
(match-kw "between")
(let
((val1 (parse-expr)))
(expect-kw! "and")
(let
((val2 (parse-expr)))
(let
((tgt (if (match-kw "on") (parse-expr) nil)))
(list
(quote toggle-style-between)
prop
val1
val2
tgt))))
(let
((tgt (if (match-kw "on") (parse-expr) nil)))
(list (quote toggle-style) prop tgt)))))
((= (tp-type) "attr")
(let
((attr-name (get (adv!) "value")))
(let
((tgt (if (match-kw "on") (parse-expr) nil)))
(list (quote toggle-attr) attr-name tgt))))
(true nil))))
((and (= (tp-type) "keyword") (= (tp-val) "the"))
(do
(adv!)
(let
((expr (parse-the-expr)))
(cond
((and (list? expr) (= (first expr) (quote style)))
(let
((prop (nth expr 1)) (tgt (nth expr 2)))
(if
(match-kw "between")
(let
((val1 (parse-expr)))
(expect-kw! "and")
(let
((val2 (parse-expr)))
(list
(quote toggle-style-between)
prop
val1
val2
tgt)))
(list (quote toggle-style) prop tgt))))
((and (list? expr) (= (first expr) (quote attr)))
(let
((attr-name (nth expr 1)) (tgt (nth expr 2)))
(list (quote toggle-attr) attr-name tgt)))
((and (list? expr) (= (first expr) (quote has-class?)))
(let
((tgt (nth expr 1)) (cls (nth expr 2)))
(list (quote toggle-class) cls tgt)))
(true nil)))))
(true nil)))) (true nil))))
(define (define
parse-set-cmd parse-set-cmd
@@ -1080,21 +1185,26 @@
(fn (fn
() ()
(let (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"))))) ((tgt (cond ((and (= (tp-type) "ident") (= (tp-val) "element")) (do (adv!) (parse-atom))) ((= (tp-type) "id") (parse-atom)) ((= (tp-type) "class") (parse-atom)) ((= (tp-type) "selector") (parse-atom)) (true nil))))
(let (let
((from-val (if (match-kw "from") (parse-expr) nil))) ((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")))))
(expect-kw! "to")
(let (let
((value (parse-expr))) ((from-val (if (match-kw "from") (parse-expr) nil)))
(expect-kw! "to")
(let (let
((dur (if (match-kw "over") (parse-expr) nil))) ((value (parse-expr)))
(if (let
from-val ((dur (if (match-kw "over") (parse-expr) nil)))
(list (quote transition-from) prop from-val value dur)
(if (if
dur from-val
(list (quote transition) prop value dur nil) (list
(list (quote transition) prop value nil))))))))) (quote transition-from)
prop
from-val
value
dur
tgt)
(list (quote transition) prop value dur tgt)))))))))
(define (define
parse-repeat-cmd parse-repeat-cmd
(fn (fn
@@ -1223,22 +1333,53 @@
() ()
(let (let
((typ (tp-type)) (val (tp-val))) ((typ (tp-type)) (val (tp-val)))
(if (cond
(or (= typ "ident") (= typ "keyword")) ((= typ "style")
(do (do
(adv!) (adv!)
(if (if
(match-kw "of") (match-kw "of")
(list (make-symbol ".") (parse-expr) val) (list (quote style) val (parse-expr))
(cond (list (quote style) val (list (quote me))))))
((= val "result") (list (quote it))) ((= typ "attr")
((= val "first") (parse-pos-kw (quote first))) (do
((= val "last") (parse-pos-kw (quote last))) (adv!)
((= val "closest") (parse-trav (quote closest))) (if
((= val "next") (parse-trav (quote next))) (match-kw "of")
((= val "previous") (parse-trav (quote previous))) (list (quote attr) val (parse-expr))
(true (list (quote ref) val))))) (list (quote attr) val (list (quote me))))))
(parse-atom))))) ((= typ "class")
(do
(adv!)
(if
(match-kw "of")
(list (quote has-class?) (parse-expr) val)
(list (quote has-class?) (list (quote me)) val))))
((= typ "selector")
(do
(adv!)
(if
(match-kw "in")
(list
(quote in?)
(list (quote query) val)
(parse-expr))
(list (quote query) val))))
((or (= typ "ident") (= typ "keyword"))
(do
(adv!)
(if
(match-kw "of")
(list (make-symbol ".") (parse-expr) val)
(cond
((= val "result") (list (quote it)))
((= val "first") (parse-pos-kw (quote first)))
((= val "last") (parse-pos-kw (quote last)))
((= val "closest") (parse-trav (quote closest)))
((= val "next") (parse-trav (quote next)))
((= val "previous") (parse-trav (quote previous)))
(true (list (quote ref) val))))))
(true (parse-atom))))))
(define (define
parse-array-lit parse-array-lit
(fn (fn

View File

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

View File

@@ -56,32 +56,39 @@
(hs-to-sx (nth target 2)) (hs-to-sx (nth target 2))
value)) value))
((= th (quote of)) ((= th (quote of))
;; Decompose (of prop-expr target) into a set operation (let
;; e.g. (of (. (ref "parentNode") "innerHTML") (query "#d1")) ((prop-ast (nth target 1)) (obj-ast (nth target 2)))
;; → set parentNode.innerHTML of #d1 → need to navigate target, then set final prop (if
(let ((prop-ast (nth target 1)) (and (list? prop-ast) (= (first prop-ast) dot-sym))
(obj-ast (nth target 2))) (let
(if (and (list? prop-ast) (= (first prop-ast) dot-sym)) ((base (nth prop-ast 1))
;; (. base "prop") of obj → (dom-set-prop (host-get (compiled-obj) (compiled-base-name)) "prop" value) (prop-name (nth prop-ast 2)))
(let ((base (nth prop-ast 1)) (list
(prop-name (nth prop-ast 2))) (quote dom-set-prop)
(list (quote dom-set-prop) (list
(list (quote host-get) (hs-to-sx obj-ast) (nth base 1)) (quote host-get)
(hs-to-sx obj-ast)
(nth base 1))
prop-name prop-name
value)) value))
;; (attr "name") of obj → (dom-set-attr (compiled-obj) "name" value) (if
(if (and (list? prop-ast) (= (first prop-ast) (quote attr))) (and
(list (quote dom-set-attr) (list? prop-ast)
(= (first prop-ast) (quote attr)))
(list
(quote dom-set-attr)
(hs-to-sx obj-ast) (hs-to-sx obj-ast)
(nth prop-ast 1) (nth prop-ast 1)
value) value)
;; Simple: (ref "prop") of obj → (dom-set-prop (compiled-obj) "prop" value) (if
(if (and (list? prop-ast) (= (first prop-ast) (quote ref))) (and
(list (quote dom-set-prop) (list? prop-ast)
(= (first prop-ast) (quote ref)))
(list
(quote dom-set-prop)
(hs-to-sx obj-ast) (hs-to-sx obj-ast)
(nth prop-ast 1) (nth prop-ast 1)
value) value)
;; Fallback
(list (quote set!) (hs-to-sx target) value)))))) (list (quote set!) (hs-to-sx target) value))))))
(true (list (quote set!) (hs-to-sx target) value))))))) (true (list (quote set!) (hs-to-sx target) value)))))))
(define (define
@@ -427,7 +434,7 @@
((= head (quote null-literal)) nil) ((= head (quote null-literal)) nil)
((= head (quote not)) ((= head (quote not))
(list (quote not) (hs-to-sx (nth ast 1)))) (list (quote not) (hs-to-sx (nth ast 1))))
((or (= head (quote starts-with?)) (= head (quote ends-with?)) (= head (quote contains?)) (= head (quote matches?)) (= head (quote precedes?)) (= head (quote follows?)) (= head (quote exists?))) ((or (= head (quote starts-with?)) (= head (quote ends-with?)) (= head (quote contains?)) (= head (quote precedes?)) (= head (quote follows?)) (= head (quote exists?)))
(cons head (map hs-to-sx (rest ast)))) (cons head (map hs-to-sx (rest ast))))
((= head (quote object-literal)) ((= head (quote object-literal))
(let (let
@@ -656,6 +663,11 @@
(quote dom-get-style) (quote dom-get-style)
(hs-to-sx (nth ast 2)) (hs-to-sx (nth ast 2))
(nth ast 1))) (nth ast 1)))
((= head (quote has-class?))
(list
(quote dom-has-class?)
(hs-to-sx (nth ast 1))
(nth ast 2)))
((= head (quote local)) (make-symbol (nth ast 1))) ((= head (quote local)) (make-symbol (nth ast 1)))
((= head (quote array)) ((= head (quote array))
(cons (quote list) (map hs-to-sx (rest ast)))) (cons (quote list) (map hs-to-sx (rest ast))))
@@ -713,15 +725,30 @@
(quote not) (quote not)
(list (quote nil?) (hs-to-sx (nth ast 1))))) (list (quote nil?) (hs-to-sx (nth ast 1)))))
((= head (quote matches?)) ((= head (quote matches?))
(list (let
(quote hs-matches?) ((left (nth ast 1)) (right (nth ast 2)))
(hs-to-sx (nth ast 1)) (if
(hs-to-sx (nth ast 2)))) (and (list? right) (= (first right) (quote query)))
(list (quote hs-matches?) (hs-to-sx left) (nth right 1))
(list
(quote hs-matches?)
(hs-to-sx left)
(hs-to-sx right)))))
((= head (quote matches-ignore-case?)) ((= head (quote matches-ignore-case?))
(list (list
(quote hs-matches-ignore-case?) (quote hs-matches-ignore-case?)
(hs-to-sx (nth ast 1)) (hs-to-sx (nth ast 1))
(hs-to-sx (nth ast 2)))) (hs-to-sx (nth ast 2))))
((= head (quote starts-with-ic?))
(list
(quote hs-starts-with-ic?)
(hs-to-sx (nth ast 1))
(hs-to-sx (nth ast 2))))
((= head (quote ends-with-ic?))
(list
(quote hs-ends-with-ic?)
(hs-to-sx (nth ast 1))
(hs-to-sx (nth ast 2))))
((= head (quote contains?)) ((= head (quote contains?))
(list (list
(quote hs-contains?) (quote hs-contains?)
@@ -824,6 +851,23 @@
(map (map
(fn (cls) (list (quote dom-add-class) target cls)) (fn (cls) (list (quote dom-add-class) target cls))
classes)))) classes))))
((= head (quote add-class-when))
(let
((cls (nth ast 1))
(raw-tgt (nth ast 2))
(when-cond (nth ast 3)))
(let
((tgt-expr (cond ((and (list? raw-tgt) (= (first raw-tgt) (quote query))) (list (quote hs-query-all) (nth raw-tgt 1))) ((and (list? raw-tgt) (= (first raw-tgt) (quote in?)) (list? (nth raw-tgt 1)) (= (first (nth raw-tgt 1)) (quote query))) (list (quote host-call) (hs-to-sx (nth raw-tgt 2)) "querySelectorAll" (nth (nth raw-tgt 1) 1))) (true (hs-to-sx raw-tgt)))))
(list
(quote for-each)
(list
(quote fn)
(list (quote it))
(list
(quote when)
(hs-to-sx when-cond)
(list (quote dom-add-class) (quote it) cls)))
tgt-expr))))
((= head (quote multi-remove-class)) ((= head (quote multi-remove-class))
(let (let
((target (hs-to-sx (nth ast 1))) ((target (hs-to-sx (nth ast 1)))
@@ -895,10 +939,12 @@
(nth ast 1) (nth ast 1)
(nth ast 2))) (nth ast 2)))
((= head (quote toggle-style)) ((= head (quote toggle-style))
(list (let
(quote hs-toggle-style!) ((raw-tgt (nth ast 2)))
(hs-to-sx (nth ast 2)) (list
(nth ast 1))) (quote hs-toggle-style!)
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt))
(nth ast 1))))
((= head (quote toggle-style-between)) ((= head (quote toggle-style-between))
(list (list
(quote hs-toggle-style-between!) (quote hs-toggle-style-between!)

View File

@@ -513,12 +513,26 @@
(do (do
(adv!) (adv!)
(match-kw "with") (match-kw "with")
(list (quote starts-with?) left (parse-expr)))) (let
((rhs (parse-atom)))
(if
(match-kw "ignoring")
(do
(match-kw "case")
(list (quote starts-with-ic?) left rhs))
(list (quote starts-with?) left rhs)))))
((and (or (= typ "keyword") (= typ "ident")) (= val "ends")) ((and (or (= typ "keyword") (= typ "ident")) (= val "ends"))
(do (do
(adv!) (adv!)
(match-kw "with") (match-kw "with")
(list (quote ends-with?) left (parse-expr)))) (let
((rhs (parse-atom)))
(if
(match-kw "ignoring")
(do
(match-kw "case")
(list (quote ends-with-ic?) left rhs))
(list (quote ends-with?) left rhs)))))
((and (= typ "keyword") (= val "matches")) ((and (= typ "keyword") (= val "matches"))
(do (do
(adv!) (adv!)
@@ -607,11 +621,17 @@
(quote not) (quote not)
(list (quote contains?) left (parse-expr)))) (list (quote contains?) left (parse-expr))))
((match-kw "start") ((match-kw "start")
(do (match-kw "with") (do
(list (quote not) (list (quote starts-with?) left (parse-expr))))) (match-kw "with")
(list
(quote not)
(list (quote starts-with?) left (parse-expr)))))
((match-kw "end") ((match-kw "end")
(do (match-kw "with") (do
(list (quote not) (list (quote ends-with?) left (parse-expr))))) (match-kw "with")
(list
(quote not)
(list (quote ends-with?) left (parse-expr)))))
(true left)))) (true left))))
((and (= typ "keyword") (= val "equals")) ((and (= typ "keyword") (= val "equals"))
(do (adv!) (list (quote =) left (parse-expr)))) (do (adv!) (list (quote =) left (parse-expr))))
@@ -693,12 +713,20 @@
nil nil
(do (do
(when (when
(and (number? left) (= (tp-type) "ident") (and
(not (or (= (tp-val) "starts") (= (tp-val) "ends") (number? left)
(= (tp-val) "contains") (= (tp-val) "matches") (= (tp-type) "ident")
(= (tp-val) "is") (= (tp-val) "does") (not
(= (tp-val) "in") (= (tp-val) "precedes") (or
(= (tp-val) "follows")))) (= (tp-val) "starts")
(= (tp-val) "ends")
(= (tp-val) "contains")
(= (tp-val) "matches")
(= (tp-val) "is")
(= (tp-val) "does")
(= (tp-val) "in")
(= (tp-val) "precedes")
(= (tp-val) "follows"))))
(let (let
((unit (tp-val))) ((unit (tp-val)))
(do (do
@@ -757,12 +785,25 @@
(collect-classes!) (collect-classes!)
(let (let
((tgt (parse-tgt-kw "to" (list (quote me))))) ((tgt (parse-tgt-kw "to" (list (quote me)))))
(if (let
(empty? extra-classes) ((when-clause (if (match-kw "when") (parse-expr) nil)))
(list (quote add-class) cls tgt) (if
(cons (empty? extra-classes)
(quote multi-add-class) (if
(cons tgt (cons cls extra-classes)))))) when-clause
(list (quote add-class-when) cls tgt when-clause)
(list (quote add-class) cls tgt))
(if
when-clause
(list
(quote multi-add-class-when)
tgt
when-clause
cls
extra-classes)
(cons
(quote multi-add-class)
(cons tgt (cons cls extra-classes))))))))
nil))) nil)))
(define (define
parse-remove-cmd parse-remove-cmd
@@ -893,6 +934,70 @@
(let (let
((tgt (parse-tgt-kw "on" (list (quote me))))) ((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"))
(do
(adv!)
(cond
((= (tp-type) "style")
(let
((prop (get (adv!) "value")))
(if
(match-kw "between")
(let
((val1 (parse-expr)))
(expect-kw! "and")
(let
((val2 (parse-expr)))
(let
((tgt (if (match-kw "on") (parse-expr) nil)))
(list
(quote toggle-style-between)
prop
val1
val2
tgt))))
(let
((tgt (if (match-kw "on") (parse-expr) nil)))
(list (quote toggle-style) prop tgt)))))
((= (tp-type) "attr")
(let
((attr-name (get (adv!) "value")))
(let
((tgt (if (match-kw "on") (parse-expr) nil)))
(list (quote toggle-attr) attr-name tgt))))
(true nil))))
((and (= (tp-type) "keyword") (= (tp-val) "the"))
(do
(adv!)
(let
((expr (parse-the-expr)))
(cond
((and (list? expr) (= (first expr) (quote style)))
(let
((prop (nth expr 1)) (tgt (nth expr 2)))
(if
(match-kw "between")
(let
((val1 (parse-expr)))
(expect-kw! "and")
(let
((val2 (parse-expr)))
(list
(quote toggle-style-between)
prop
val1
val2
tgt)))
(list (quote toggle-style) prop tgt))))
((and (list? expr) (= (first expr) (quote attr)))
(let
((attr-name (nth expr 1)) (tgt (nth expr 2)))
(list (quote toggle-attr) attr-name tgt)))
((and (list? expr) (= (first expr) (quote has-class?)))
(let
((tgt (nth expr 1)) (cls (nth expr 2)))
(list (quote toggle-class) cls tgt)))
(true nil)))))
(true nil)))) (true nil))))
(define (define
parse-set-cmd parse-set-cmd
@@ -1080,21 +1185,26 @@
(fn (fn
() ()
(let (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"))))) ((tgt (cond ((and (= (tp-type) "ident") (= (tp-val) "element")) (do (adv!) (parse-atom))) ((= (tp-type) "id") (parse-atom)) ((= (tp-type) "class") (parse-atom)) ((= (tp-type) "selector") (parse-atom)) (true nil))))
(let (let
((from-val (if (match-kw "from") (parse-expr) nil))) ((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")))))
(expect-kw! "to")
(let (let
((value (parse-expr))) ((from-val (if (match-kw "from") (parse-expr) nil)))
(expect-kw! "to")
(let (let
((dur (if (match-kw "over") (parse-expr) nil))) ((value (parse-expr)))
(if (let
from-val ((dur (if (match-kw "over") (parse-expr) nil)))
(list (quote transition-from) prop from-val value dur)
(if (if
dur from-val
(list (quote transition) prop value dur nil) (list
(list (quote transition) prop value nil))))))))) (quote transition-from)
prop
from-val
value
dur
tgt)
(list (quote transition) prop value dur tgt)))))))))
(define (define
parse-repeat-cmd parse-repeat-cmd
(fn (fn
@@ -1223,22 +1333,53 @@
() ()
(let (let
((typ (tp-type)) (val (tp-val))) ((typ (tp-type)) (val (tp-val)))
(if (cond
(or (= typ "ident") (= typ "keyword")) ((= typ "style")
(do (do
(adv!) (adv!)
(if (if
(match-kw "of") (match-kw "of")
(list (make-symbol ".") (parse-expr) val) (list (quote style) val (parse-expr))
(cond (list (quote style) val (list (quote me))))))
((= val "result") (list (quote it))) ((= typ "attr")
((= val "first") (parse-pos-kw (quote first))) (do
((= val "last") (parse-pos-kw (quote last))) (adv!)
((= val "closest") (parse-trav (quote closest))) (if
((= val "next") (parse-trav (quote next))) (match-kw "of")
((= val "previous") (parse-trav (quote previous))) (list (quote attr) val (parse-expr))
(true (list (quote ref) val))))) (list (quote attr) val (list (quote me))))))
(parse-atom))))) ((= typ "class")
(do
(adv!)
(if
(match-kw "of")
(list (quote has-class?) (parse-expr) val)
(list (quote has-class?) (list (quote me)) val))))
((= typ "selector")
(do
(adv!)
(if
(match-kw "in")
(list
(quote in?)
(list (quote query) val)
(parse-expr))
(list (quote query) val))))
((or (= typ "ident") (= typ "keyword"))
(do
(adv!)
(if
(match-kw "of")
(list (make-symbol ".") (parse-expr) val)
(cond
((= val "result") (list (quote it)))
((= val "first") (parse-pos-kw (quote first)))
((= val "last") (parse-pos-kw (quote last)))
((= val "closest") (parse-trav (quote closest)))
((= val "next") (parse-trav (quote next)))
((= val "previous") (parse-trav (quote previous)))
(true (list (quote ref) val))))))
(true (parse-atom))))))
(define (define
parse-array-lit parse-array-lit
(fn (fn

View File

@@ -88,7 +88,7 @@
((or (= prop "display") (= prop "opacity")) ((or (= prop "display") (= prop "opacity"))
(if (if
(or (= cur "none") (= cur "0")) (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")))) (dom-set-style target prop (if (= prop "display") "none" "0"))))
(true (true
(if (if
@@ -102,17 +102,30 @@
(define (define
hs-take! hs-take!
(fn (fn
(target kind name scope) (target kind name scope &rest extra)
(let (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 (if
(= kind "class") (= kind "class")
(do (do
(for-each (fn (el) (dom-remove-class el name)) els) (for-each (fn (el) (dom-remove-class el name)) els)
(dom-add-class target name)) (dom-add-class target name))
(do (let
(for-each (fn (el) (dom-remove-attr el name)) els) ((attr-val (if (> (len extra) 0) (first extra) nil))
(dom-set-attr target name "true")))))) (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). ;; Find next sibling matching a selector (or any sibling).
(define (define
@@ -204,7 +217,9 @@
;; Fetch a URL, parse response according to format. ;; Fetch a URL, parse response according to format.
;; (hs-fetch url format) — format is "json" | "text" | "html" ;; (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 ─────────────────────────────────────────────── ;; ── Type coercion ───────────────────────────────────────────────
@@ -287,32 +302,75 @@
((= type-name "Float") (+ value 0)) ((= type-name "Float") (+ value 0))
((= type-name "Number") (+ value 0)) ((= type-name "Number") (+ value 0))
((= type-name "String") (str value)) ((= type-name "String") (str value))
((= type-name "Bool") (if value true false)) ((= type-name "Bool") (not (hs-falsy? value)))
((= type-name "Boolean") (if value true false)) ((= type-name "Boolean") (not (hs-falsy? value)))
((= type-name "Array") (if (list? value) value (list value))) ((= type-name "Array") (if (list? value) value (list value)))
((= type-name "JSON") (str value)) ((= type-name "HTML") (str value))
((= type-name "Object") (if (string? value) value value)) ((= type-name "JSON")
((or (= type-name "Fixed") (string-contains? type-name "Fixed:")) (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 (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))) (num (+ value 0)))
(if (if
(= digits 0) (= digits 0)
(str (floor num)) (str (floor num))
(let (let
((factor (reduce (fn (acc _) (* acc 10)) 1 (range 0 digits)))) ((factor (** 10 digits)))
(let (str (/ (floor (+ (* num factor) 0.5)) factor))))))
((rounded (/ (floor (+ (* num factor) 0.5)) factor))) ((= type-name "Selector") (str value))
(str rounded)))))) ((= type-name "Fragment") value)
((= type-name "HTML") (str value)) ((= type-name "Values")
((= type-name "Values") value) (if
((= type-name "Fragment") (str value)) (dict? value)
((= type-name "Date") (str 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)))) (true value))))
(define (define
hs-add 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 (define
hs-make hs-make
@@ -371,7 +429,12 @@
((= type-name "Boolean") (or (= value true) (= value false))) ((= type-name "Boolean") (or (= value true) (= value false)))
((= type-name "Array") (list? value)) ((= type-name "Array") (list? value))
((= type-name "Object") (dict? 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)))))))
@@ -388,6 +451,22 @@
(fn (a b) (and (= (type-of a) (type-of b)) (= a b)))) (fn (a b) (and (= (type-of a) (type-of b)) (= a b))))
;; ── Sandbox/test runtime additions ────────────────────────────── ;; ── Sandbox/test runtime additions ──────────────────────────────
;; Property access — dot notation and .length ;; Property access — dot notation and .length
(define
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)))))
;; ── 0.9.90 features ─────────────────────────────────────────────
;; beep! — debug logging, returns value unchanged
(define (define
hs-falsy? hs-falsy?
(fn (fn
@@ -399,16 +478,18 @@
((and (list? v) (= (len v) 0)) true) ((and (list? v) (= (len v) 0)) true)
((= v 0) true) ((= v 0) true)
(true false)))) (true false))))
;; DOM query stub — sandbox returns empty list ;; Property-based is — check obj.key truthiness
(define (define
hs-matches? hs-matches?
(fn (fn
(target pattern) (target pattern)
(if (cond
(string? target) ((string? target)
(if (= pattern ".*") true (string-contains? target pattern)) (if (= pattern ".*") true (string-contains? target pattern)))
false))) ((= (host-typeof target) "element")
;; Method dispatch — obj.method(args) (if (string? pattern) (host-call target "matches" pattern) false))
(true false))))
;; Array slicing (inclusive both ends)
(define (define
hs-contains? hs-contains?
(fn (fn
@@ -428,11 +509,9 @@
true true
(hs-contains? (rest collection) item))))) (hs-contains? (rest collection) item)))))
(true false)))) (true false))))
;; Collection: sorted by
;; ── 0.9.90 features ─────────────────────────────────────────────
;; beep! — debug logging, returns value unchanged
(define precedes? (fn (a b) (< (str a) (str b)))) (define precedes? (fn (a b) (< (str a) (str b))))
;; Property-based is — check obj.key truthiness ;; Collection: sorted by descending
(define (define
hs-empty? hs-empty?
(fn (fn
@@ -443,7 +522,7 @@
((list? v) (= (len v) 0)) ((list? v) (= (len v) 0))
((dict? v) (= (len (keys v)) 0)) ((dict? v) (= (len (keys v)) 0))
(true false)))) (true false))))
;; Array slicing (inclusive both ends) ;; Collection: split by
(define (define
hs-empty-target! hs-empty-target!
(fn (fn
@@ -464,11 +543,61 @@
(dom-set-prop target "value" "")))) (dom-set-prop target "value" ""))))
((= tag "FORM") (dom-set-inner-html target "")) ((= tag "FORM") (dom-set-inner-html target ""))
(true (dom-set-inner-html target "")))))))) (true (dom-set-inner-html target ""))))))))
;; Collection: sorted by ;; Collection: joined by
(define
hs-open!
(fn
(el)
(let
((tag (dom-get-prop el "tagName")))
(if
(= tag "DIALOG")
(host-call el "showModal")
(dom-set-prop el "open" true)))))
(define
hs-close!
(fn
(el)
(let
((tag (dom-get-prop el "tagName")))
(if
(= tag "DIALOG")
(host-call el "close")
(dom-set-prop el "open" false)))))
(define
hs-hide!
(fn
(el strategy)
(let
((tag (dom-get-prop el "tagName")))
(cond
((= tag "DIALOG")
(when (dom-has-attr? el "open") (host-call el "close")))
((= tag "DETAILS") (dom-set-prop el "open" false))
((= strategy "opacity") (dom-set-style el "opacity" "0"))
((= strategy "visibility") (dom-set-style el "visibility" "hidden"))
(true (dom-set-style el "display" "none"))))))
(define
hs-show!
(fn
(el strategy)
(let
((tag (dom-get-prop el "tagName")))
(cond
((= tag "DIALOG")
(when (not (dom-has-attr? el "open")) (host-call el "showModal")))
((= tag "DETAILS") (dom-set-prop el "open" true))
((= strategy "opacity") (dom-set-style el "opacity" "1"))
((= strategy "visibility") (dom-set-style el "visibility" "visible"))
(true (dom-set-style el "display" ""))))))
(define hs-first (fn (lst) (first lst))) (define hs-first (fn (lst) (first lst)))
;; Collection: sorted by descending
(define hs-last (fn (lst) (last lst))) (define hs-last (fn (lst) (last lst)))
;; Collection: split by
(define (define
hs-template hs-template
(fn (fn
@@ -554,7 +683,7 @@
(set! i (+ i 1)) (set! i (+ i 1))
(tpl-loop))))))) (tpl-loop)))))))
(do (tpl-loop) result)))) (do (tpl-loop) result))))
;; Collection: joined by
(define (define
hs-make-object hs-make-object
(fn (fn

View File

@@ -21,8 +21,9 @@ function setStepLimit(n) { K.setStepLimit(n); }
function resetStepCount() { K.resetStepCount(); } function resetStepCount() { K.resetStepCount(); }
// ─── DOM mock ────────────────────────────────────────────────── // ─── DOM mock ──────────────────────────────────────────────────
function mkStyle() { const s={}; s.setProperty=function(p,v){s[p]=v;}; s.getPropertyValue=function(p){return s[p]||'';}; s.removeProperty=function(p){delete s[p];}; return s; }
class El { class El {
constructor(t) { this.tagName=t.toUpperCase(); this.nodeName=this.tagName; this.nodeType=1; this.id=''; this.className=''; this.classList=new CL(this); this.style={}; this.attributes={}; this.children=[]; this.childNodes=[]; this.parentElement=null; this.parentNode=null; this.textContent=''; this.innerHTML=''; this._listeners={}; this.dataset={}; this.open=false; this.value=''; this.checked=false; this.disabled=false; this.type=''; this.name=''; this.selectedIndex=-1; this.options=[]; } constructor(t) { this.tagName=t.toUpperCase(); this.nodeName=this.tagName; this.nodeType=1; this.id=''; this.className=''; this.classList=new CL(this); this.style=mkStyle(); this.attributes={}; this.children=[]; this.childNodes=[]; this.parentElement=null; this.parentNode=null; this.textContent=''; this.innerHTML=''; this._listeners={}; this.dataset={}; this.open=false; this.value=''; this.checked=false; this.disabled=false; this.type=''; this.name=''; this.selectedIndex=-1; this.options=[]; }
setAttribute(n,v) { this.attributes[n]=String(v); if(n==='id')this.id=v; if(n==='class'){this.className=v;this.classList._sync(v);} if(n==='value')this.value=v; if(n==='disabled')this.disabled=true; } setAttribute(n,v) { this.attributes[n]=String(v); if(n==='id')this.id=v; if(n==='class'){this.className=v;this.classList._sync(v);} if(n==='value')this.value=v; if(n==='disabled')this.disabled=true; }
getAttribute(n) { return this.attributes[n]!==undefined?this.attributes[n]:null; } getAttribute(n) { return this.attributes[n]!==undefined?this.attributes[n]:null; }
removeAttribute(n) { delete this.attributes[n]; if(n==='disabled')this.disabled=false; } removeAttribute(n) { delete this.attributes[n]; if(n==='disabled')this.disabled=false; }
@@ -39,7 +40,7 @@ class El {
closest(s) { let e=this; while(e){if(mt(e,s))return e; e=e.parentElement;} return null; } closest(s) { let e=this; while(e){if(mt(e,s))return e; e=e.parentElement;} return null; }
matches(s) { return mt(this,s); } matches(s) { return mt(this,s); }
contains(o) { if(o===this)return true; for(const c of this.children)if(c===o||c.contains(o))return true; return false; } contains(o) { if(o===this)return true; for(const c of this.children)if(c===o||c.contains(o))return true; return false; }
cloneNode(d) { const e=new El(this.tagName.toLowerCase()); Object.assign(e.attributes,this.attributes); e.id=this.id; e.className=this.className; e.classList._sync(this.className); Object.assign(e.style,this.style); e.textContent=this.textContent; e.innerHTML=this.innerHTML; e.value=this.value; if(d)for(const c of this.children)e.appendChild(c.cloneNode(true)); return e; } cloneNode(d) { const e=new El(this.tagName.toLowerCase()); Object.assign(e.attributes,this.attributes); e.id=this.id; e.className=this.className; e.classList._sync(this.className); for(const k of Object.keys(this.style)){if(typeof this.style[k]!=='function')e.style[k]=this.style[k];} e.textContent=this.textContent; e.innerHTML=this.innerHTML; e.value=this.value; if(d)for(const c of this.children)e.appendChild(c.cloneNode(true)); return e; }
focus(){} blur(){} click(){this.dispatchEvent(new Ev('click',{bubbles:true}));} remove(){if(this.parentElement)this.parentElement.removeChild(this);} focus(){} blur(){} click(){this.dispatchEvent(new Ev('click',{bubbles:true}));} remove(){if(this.parentElement)this.parentElement.removeChild(this);}
_syncText() { _syncText() {
// Sync textContent from children // Sync textContent from children
@@ -149,7 +150,7 @@ function mt(e,s) {
return e.tagName.toLowerCase() === s.toLowerCase(); return e.tagName.toLowerCase() === s.toLowerCase();
} }
function fnd(e,s) { for(const c of(e.children||[])){if(mt(c,s))return c;const f=fnd(c,s);if(f)return f;} return null; } function fnd(e,s) { for(const c of(e.children||[])){if(mt(c,s))return c;const f=fnd(c,s);if(f)return f;} return null; }
function fndAll(e,s) { const r=[];for(const c of(e.children||[])){if(mt(c,s))r.push(c);r.push(...fndAll(c,s));}return r; } function fndAll(e,s) { const r=[];for(const c of(e.children||[])){if(mt(c,s))r.push(c);r.push(...fndAll(c,s));}r.item=function(i){return r[i]||null;};return r; }
const _body = new El('body'); const _body = new El('body');
const _html = new El('html'); const _html = new El('html');