Two bugs found by automated test suite: 1. compiler.sx: query → hs-query-first (was dom-query, a deleted stub) 2. compiler.sx: emit-set with query target → dom-set-inner-html (was set!) 3. runtime.sx: hs-query-first uses real document.querySelector 4. runtime.sx: delete hs-dom-query stub (returned empty list) All 8/8 HS elements pass: toggle, bounce+wait, count, add-class, toggle-between, set-innerHTML-eval, put-into-target, repeat-3-times. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
572 lines
18 KiB
Plaintext
572 lines
18 KiB
Plaintext
;; _hyperscript runtime shims
|
|
;;
|
|
;; Thin wrappers over web/lib/dom.sx and web/lib/browser.sx primitives
|
|
;; that implement hyperscript-specific semantics (async transparency,
|
|
;; class toggling, event waiting, iteration, type coercion).
|
|
;;
|
|
;; These are the functions that hs-to-sx (compiler.sx) emits calls to.
|
|
;; Each is a pure define — no platform dependency beyond the DOM/browser
|
|
;; primitives already available in the SX web platform.
|
|
|
|
;; ── Event handling ──────────────────────────────────────────────
|
|
|
|
;; Register an event listener. Returns unlisten function.
|
|
;; (hs-on target event-name handler) → unlisten-fn
|
|
(define
|
|
hs-on
|
|
(fn (target event-name handler) (dom-listen target event-name handler)))
|
|
|
|
;; Register for every occurrence (no queuing — each fires independently).
|
|
;; Stock hyperscript queues by default; "every" disables queuing.
|
|
(define
|
|
hs-on-every
|
|
(fn (target event-name handler) (dom-listen target event-name handler)))
|
|
|
|
;; Run an initializer function immediately.
|
|
;; (hs-init thunk) — called at element boot time
|
|
(define hs-init (fn (thunk) (thunk)))
|
|
|
|
;; ── Async / timing ──────────────────────────────────────────────
|
|
|
|
;; Wait for a duration in milliseconds.
|
|
;; In hyperscript, wait is async-transparent — execution pauses.
|
|
;; Here we use perform/IO suspension for true pause semantics.
|
|
(define hs-wait (fn (ms) (perform (list (quote io-sleep) ms))))
|
|
|
|
;; Wait for a DOM event on a target.
|
|
;; (hs-wait-for target event-name) — suspends until event fires
|
|
(define
|
|
hs-wait-for
|
|
(fn
|
|
(target event-name)
|
|
(perform (list (quote io-wait-event) target event-name))))
|
|
|
|
;; Wait for CSS transitions/animations to settle on an element.
|
|
(define hs-settle (fn (target) (perform (list (quote io-settle) target))))
|
|
|
|
;; ── Class manipulation ──────────────────────────────────────────
|
|
|
|
;; Toggle a single class on an element.
|
|
(define
|
|
hs-toggle-class!
|
|
(fn (target cls) (host-call (host-get target "classList") "toggle" cls)))
|
|
|
|
;; Toggle between two classes — exactly one is active at a time.
|
|
(define
|
|
hs-toggle-between!
|
|
(fn
|
|
(target cls1 cls2)
|
|
(if
|
|
(dom-has-class? target cls1)
|
|
(do (dom-remove-class target cls1) (dom-add-class target cls2))
|
|
(do (dom-remove-class target cls2) (dom-add-class target cls1)))))
|
|
|
|
;; Take a class from siblings — add to target, remove from others.
|
|
;; (hs-take! target cls) — like radio button class behavior
|
|
(define
|
|
hs-take!
|
|
(fn
|
|
(target cls)
|
|
(let
|
|
((parent (dom-parent target)))
|
|
(when
|
|
parent
|
|
(for-each
|
|
(fn (child) (dom-remove-class child cls))
|
|
(dom-child-list parent)))
|
|
(dom-add-class target cls))))
|
|
|
|
;; ── DOM insertion ───────────────────────────────────────────────
|
|
|
|
;; Put content at a position relative to a target.
|
|
;; pos: "into" | "before" | "after"
|
|
(define
|
|
hs-put!
|
|
(fn
|
|
(value pos target)
|
|
(cond
|
|
((= pos "into") (dom-set-inner-html target value))
|
|
((= pos "before")
|
|
(dom-insert-adjacent-html target "beforebegin" value))
|
|
((= pos "after") (dom-insert-adjacent-html target "afterend" value)))))
|
|
|
|
;; ── Navigation / traversal ──────────────────────────────────────
|
|
|
|
;; Navigate to a URL.
|
|
(define hs-navigate! (fn (url) (perform (list (quote io-navigate) url))))
|
|
|
|
;; Find next sibling matching a selector (or any sibling).
|
|
(define
|
|
hs-next
|
|
(fn
|
|
(target sel)
|
|
(if
|
|
(= sel "*")
|
|
(dom-next-sibling target)
|
|
(let
|
|
((sibling (dom-next-sibling target)))
|
|
(define
|
|
find-next
|
|
(fn
|
|
(el)
|
|
(cond
|
|
((nil? el) nil)
|
|
((dom-matches? el sel) el)
|
|
(true (find-next (dom-next-sibling el))))))
|
|
(find-next sibling)))))
|
|
|
|
;; Find previous sibling matching a selector.
|
|
(define
|
|
hs-previous
|
|
(fn
|
|
(target sel)
|
|
(if
|
|
(= sel "*")
|
|
(dom-get-prop target "previousElementSibling")
|
|
(let
|
|
((sibling (dom-get-prop target "previousElementSibling")))
|
|
(define
|
|
find-prev
|
|
(fn
|
|
(el)
|
|
(cond
|
|
((nil? el) nil)
|
|
((dom-matches? el sel) el)
|
|
(true (find-prev (dom-get-prop el "previousElementSibling"))))))
|
|
(find-prev sibling)))))
|
|
|
|
;; First element matching selector within a scope.
|
|
(define
|
|
hs-query-first
|
|
(fn (sel) (host-call (host-global "document") "querySelector" sel)))
|
|
|
|
;; Last element matching selector.
|
|
(define
|
|
hs-query-last
|
|
(fn
|
|
(sel)
|
|
(let
|
|
((all (dom-query-all (dom-body) sel)))
|
|
(if (> (len all) 0) (nth all (- (len all) 1)) nil))))
|
|
|
|
;; First/last within a specific scope.
|
|
(define hs-first (fn (scope sel) (dom-query-all scope sel)))
|
|
|
|
(define
|
|
hs-last
|
|
(fn
|
|
(scope sel)
|
|
(let
|
|
((all (dom-query-all scope sel)))
|
|
(if (> (len all) 0) (nth all (- (len all) 1)) nil))))
|
|
|
|
;; ── Iteration ───────────────────────────────────────────────────
|
|
|
|
;; Repeat a thunk N times.
|
|
(define
|
|
hs-repeat-times
|
|
(fn
|
|
(n thunk)
|
|
(define
|
|
do-repeat
|
|
(fn (i) (when (< i n) (do (thunk) (do-repeat (+ i 1))))))
|
|
(do-repeat 0)))
|
|
|
|
;; Repeat forever (until break — relies on exception/continuation).
|
|
(define
|
|
hs-repeat-forever
|
|
(fn
|
|
(thunk)
|
|
(define do-forever (fn () (thunk) (do-forever)))
|
|
(do-forever)))
|
|
|
|
;; ── Fetch ───────────────────────────────────────────────────────
|
|
|
|
;; Fetch a URL, parse response according to format.
|
|
;; (hs-fetch url format) — format is "json" | "text" | "html"
|
|
(define
|
|
hs-fetch
|
|
(fn
|
|
(url format)
|
|
(let
|
|
((response (perform (list (quote io-fetch) url))))
|
|
(cond
|
|
((= format "json") (perform (list (quote io-parse-json) response)))
|
|
((= format "text") (perform (list (quote io-parse-text) response)))
|
|
((= format "html") (perform (list (quote io-parse-html) response)))
|
|
(true response)))))
|
|
|
|
;; ── Type coercion ───────────────────────────────────────────────
|
|
|
|
;; Coerce a value to a type by name.
|
|
;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc.
|
|
(define
|
|
hs-coerce
|
|
(fn
|
|
(value type-name)
|
|
(cond
|
|
((= type-name "Int") (floor (+ value 0)))
|
|
((= type-name "Integer") (floor (+ value 0)))
|
|
((= type-name "Float") (+ value 0))
|
|
((= type-name "Number") (+ value 0))
|
|
((= type-name "String") (str value))
|
|
((= type-name "Bool") (if value true false))
|
|
((= type-name "Boolean") (if value true false))
|
|
((= 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:"))
|
|
(let
|
|
((digits (if (string-contains? type-name ":") (parse-number (nth (split type-name ":") 1)) 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))
|
|
(true value))))
|
|
|
|
;; ── Object creation ─────────────────────────────────────────────
|
|
|
|
;; Make a new object of a given type.
|
|
;; (hs-make type-name) — creates empty object/collection
|
|
(define
|
|
hs-add
|
|
(fn (a b) (if (or (string? a) (string? b)) (str a b) (+ a b))))
|
|
|
|
;; ── 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-make
|
|
(fn
|
|
(type-name)
|
|
(cond
|
|
((= type-name "Object") (dict))
|
|
((= type-name "Array") (list))
|
|
((= type-name "Set") (list))
|
|
((= type-name "Map") (dict))
|
|
(true (dict)))))
|
|
|
|
;; ── 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-install (fn (behavior-fn) (behavior-fn me)))
|
|
|
|
;; ── Transition ──────────────────────────────────────────────────
|
|
|
|
;; Transition a CSS property to a value, optionally with duration.
|
|
;; (hs-transition target prop value duration)
|
|
(define
|
|
hs-measure
|
|
(fn (target) (perform (list (quote io-measure) target))))
|
|
|
|
(define
|
|
hs-transition
|
|
(fn
|
|
(target prop value duration)
|
|
(when
|
|
duration
|
|
(dom-set-style
|
|
target
|
|
"transition"
|
|
(str prop " " (/ duration 1000) "s")))
|
|
(dom-set-style target prop value)
|
|
(when duration (hs-settle target))))
|
|
|
|
(define
|
|
hs-type-check
|
|
(fn
|
|
(value type-name)
|
|
(if
|
|
(nil? value)
|
|
true
|
|
(cond
|
|
((= type-name "Number") (number? value))
|
|
((= type-name "String") (string? value))
|
|
((= type-name "Boolean") (or (= value true) (= value false)))
|
|
((= type-name "Array") (list? value))
|
|
((= type-name "Object") (dict? value))
|
|
(true true)))))
|
|
|
|
(define
|
|
hs-type-check-strict
|
|
(fn
|
|
(value type-name)
|
|
(if (nil? value) false (hs-type-check value type-name))))
|
|
|
|
(define
|
|
hs-strict-eq
|
|
(fn (a b) (and (= (type-of a) (type-of b)) (= a b))))
|
|
|
|
(define
|
|
hs-falsy?
|
|
(fn
|
|
(v)
|
|
(cond
|
|
((nil? v) true)
|
|
((= v false) true)
|
|
((and (string? v) (= v "")) true)
|
|
((and (list? v) (= (len v) 0)) true)
|
|
((= v 0) true)
|
|
(true false))))
|
|
|
|
(define
|
|
hs-matches?
|
|
(fn
|
|
(target pattern)
|
|
(if
|
|
(string? target)
|
|
(if (= pattern ".*") true (string-contains? target pattern))
|
|
false)))
|
|
|
|
(define
|
|
hs-contains?
|
|
(fn
|
|
(collection item)
|
|
(cond
|
|
((nil? collection) false)
|
|
((string? collection) (string-contains? collection (str item)))
|
|
((list? collection)
|
|
(if
|
|
(list? item)
|
|
(filter (fn (x) (hs-contains? collection x)) item)
|
|
(if
|
|
(= (len collection) 0)
|
|
false
|
|
(if
|
|
(= (first collection) item)
|
|
true
|
|
(hs-contains? (rest collection) item)))))
|
|
(true false))))
|
|
|
|
(define
|
|
hs-empty?
|
|
(fn
|
|
(v)
|
|
(cond
|
|
((nil? v) true)
|
|
((string? v) (= (len v) 0))
|
|
((list? v) (= (len v) 0))
|
|
((dict? v) (= (len (keys v)) 0))
|
|
(true false))))
|
|
|
|
(define hs-first (fn (lst) (first lst)))
|
|
|
|
(define hs-last (fn (lst) (last lst)))
|
|
|
|
|
|
|
|
|
|
|
|
(define
|
|
hs-template
|
|
(fn
|
|
(raw)
|
|
(let
|
|
((result "") (i 0) (n (len raw)))
|
|
(define
|
|
tpl-loop
|
|
(fn
|
|
()
|
|
(when
|
|
(< i n)
|
|
(let
|
|
((ch (nth raw i)))
|
|
(if
|
|
(and (= ch "$") (< (+ i 1) n))
|
|
(if
|
|
(= (nth raw (+ i 1)) "{")
|
|
(let
|
|
((start (+ i 2)))
|
|
(define
|
|
find-close
|
|
(fn
|
|
(j depth)
|
|
(if
|
|
(>= j n)
|
|
j
|
|
(if
|
|
(= (nth raw j) "}")
|
|
(if
|
|
(= depth 1)
|
|
j
|
|
(find-close (+ j 1) (- depth 1)))
|
|
(if
|
|
(= (nth raw j) "{")
|
|
(find-close (+ j 1) (+ depth 1))
|
|
(find-close (+ j 1) depth))))))
|
|
(let
|
|
((close (find-close start 1)))
|
|
(let
|
|
((expr-src (slice raw start close)))
|
|
(do
|
|
(set!
|
|
result
|
|
(str
|
|
result
|
|
(cek-eval (hs-to-sx (hs-compile expr-src)))))
|
|
(set! i (+ close 1))
|
|
(tpl-loop)))))
|
|
(let
|
|
((start (+ i 1)))
|
|
(define
|
|
read-id
|
|
(fn
|
|
(j)
|
|
(if
|
|
(and
|
|
(< j n)
|
|
(let
|
|
((c (nth raw j)))
|
|
(or
|
|
(and (>= c "a") (<= c "z"))
|
|
(and (>= c "A") (<= c "Z"))
|
|
(and (>= c "0") (<= c "9"))
|
|
(= c "_")
|
|
(= c "."))))
|
|
(read-id (+ j 1))
|
|
j)))
|
|
(let
|
|
((end (read-id start)))
|
|
(let
|
|
((ident (slice raw start end)))
|
|
(do
|
|
(set!
|
|
result
|
|
(str
|
|
result
|
|
(cek-eval (hs-to-sx (hs-compile ident)))))
|
|
(set! i end)
|
|
(tpl-loop))))))
|
|
(do
|
|
(set! result (str result ch))
|
|
(set! i (+ i 1))
|
|
(tpl-loop)))))))
|
|
(do (tpl-loop) result))))
|
|
|
|
(define
|
|
hs-make-object
|
|
(fn
|
|
(pairs)
|
|
(let
|
|
((d {}))
|
|
(do
|
|
(for-each
|
|
(fn (pair) (dict-set! d (first pair) (nth pair 1)))
|
|
pairs)
|
|
d))))
|
|
;; ── Sandbox/test runtime additions ──────────────────────────────
|
|
;; Property access — dot notation and .length
|
|
(define
|
|
hs-method-call
|
|
(fn
|
|
(obj method &rest args)
|
|
(cond
|
|
((= method "map") (map (first args) obj))
|
|
((= method "push") (do (append! obj (first args)) obj))
|
|
((= method "filter") (filter (first args) obj))
|
|
((= method "join") (join obj (first args)))
|
|
((= method "indexOf")
|
|
(let
|
|
((item (first args)))
|
|
(define
|
|
idx-loop
|
|
(fn
|
|
(lst i)
|
|
(if
|
|
(= (len lst) 0)
|
|
-1
|
|
(if (= (first lst) item) i (idx-loop (rest lst) (+ i 1))))))
|
|
(idx-loop obj 0)))
|
|
(true nil))))
|
|
;; DOM query stub — sandbox returns empty list
|
|
(define hs-beep (fn (v) v))
|
|
;; Method dispatch — obj.method(args)
|
|
(define hs-prop-is (fn (obj key) (not (hs-falsy? (host-get obj key)))))
|
|
|
|
;; ── 0.9.90 features ─────────────────────────────────────────────
|
|
;; beep! — debug logging, returns value unchanged
|
|
(define
|
|
hs-slice
|
|
(fn
|
|
(col start end)
|
|
(let
|
|
((s (if (nil? start) 0 start))
|
|
(e (if (nil? end) (len col) (+ end 1))))
|
|
(slice col s e))))
|
|
;; Property-based is — check obj.key truthiness
|
|
(define
|
|
hs-sorted-by
|
|
(fn
|
|
(col key-fn)
|
|
(let
|
|
((pairs (map (fn (item) (list (key-fn item) item)) col)))
|
|
(map
|
|
(fn (p) (nth p 1))
|
|
(sort (fn (a b) (if (< (first a) (first b)) true false)) pairs)))))
|
|
;; Array slicing (inclusive both ends)
|
|
(define
|
|
hs-sorted-by-desc
|
|
(fn
|
|
(col key-fn)
|
|
(let
|
|
((pairs (map (fn (item) (list (key-fn item) item)) col)))
|
|
(map
|
|
(fn (p) (nth p 1))
|
|
(sort (fn (a b) (if (> (first a) (first b)) true false)) pairs)))))
|
|
;; Collection: sorted by
|
|
(define hs-split-by (fn (s sep) (split s sep)))
|
|
;; Collection: sorted by descending
|
|
(define hs-joined-by (fn (col sep) (join sep col)))
|
|
;; Collection: split by
|
|
(define
|
|
hs-sorted-by
|
|
(fn
|
|
(col key-fn)
|
|
(let
|
|
((decorated (map (fn (item) (list (key-fn item) item)) col)))
|
|
(let
|
|
((sorted-dec (sort (map first decorated))))
|
|
(define
|
|
reorder
|
|
(fn
|
|
(keys acc remaining)
|
|
(if
|
|
(= (len keys) 0)
|
|
acc
|
|
(let
|
|
((k (first keys)))
|
|
(define
|
|
find-item
|
|
(fn
|
|
(lst)
|
|
(if
|
|
(= (len lst) 0)
|
|
nil
|
|
(if
|
|
(= (first (first lst)) k)
|
|
(first lst)
|
|
(find-item (rest lst))))))
|
|
(let
|
|
((found (find-item remaining)))
|
|
(reorder
|
|
(rest keys)
|
|
(append acc (list (nth found 1)))
|
|
(filter (fn (x) (not (= x found))) remaining)))))))
|
|
(reorder sorted-dec (list) decorated)))))
|
|
;; Collection: joined by
|
|
(define
|
|
hs-sorted-by-desc
|
|
(fn (col key-fn) (reverse (hs-sorted-by col key-fn))))
|