Files
rose-ash/lib/hyperscript/runtime.sx
giles 802ccd23e8 HS: fix empty/halt/morph/reset/dialog — 17 upstream tests pass
- parser `empty` no-target → (ref "me") (was bogus (sym "me"))
- parser `halt` modes distinguish: "all"/"bubbling"/"default" halt execution
  (raise hs-return), "the-event"/"the event's" only stop propagation/default.
  "'s" now matched as op token, not keyword.
- parser `get` cmd: dispatch + cmd-kw list + parse-get-cmd (parses expr with
  optional `as TYPE`). Required for `get result as JSON` in fetch chains.
- compiler empty-target for (local X): emit (set! X (hs-empty-like X)) so
  arrays/sets/maps clear the variable, not call DOM empty on the value.
- runtime hs-empty-like: container-of-same-type empty value.
- runtime hs-empty-target!: drop dead FORM branch that was short-circuiting
  to innerHTML=""; the querySelectorAll-over-inputs branch now runs.
- runtime hs-halt!: take ev param (was free `event` lookup); raise hs-return
  to stop execution unless mode is "the-event".
- runtime hs-reset!: type-aware — FORM → reset, INPUT/TEXTAREA → value/checked
  from defaults, SELECT → defaultSelected option.
- runtime hs-open!/hs-close!: toggle `open` attribute on details elements
  (not just the prop) so dom-has-attr? assertions work.
- runtime hs-coerce JSON: json-stringify dict/list (was str).
- test-runner mock: host-get on List + "length"/"size" (was only Dict);
  dom-set-attr tracks defaultChecked / defaultSelected / defaultValue;
  mock_query_all supports comma-separated selector groups.
- generator: emit boolean attrs (checked/selected/etc) even with null value;
  drop overcautious "skip HS with bare quotes or embedded HTML" guard so
  morph tests (source contains embedded <div>) emit properly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:36:01 +00:00

1499 lines
43 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-each
(fn
(target action)
(if (list? target) (for-each action target) (action target))))
;; Register for every occurrence (no queuing — each fires independently).
;; Stock hyperscript queues by default; "every" disables queuing.
(define
hs-on
(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-on-every
(fn (target event-name handler) (dom-listen target event-name handler)))
;; ── 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-init (fn (thunk) (thunk)))
;; Wait for a DOM event on a target.
;; (hs-wait-for target event-name) — suspends until event fires
(define hs-wait (fn (ms) (perform (list (quote io-sleep) ms))))
;; Wait for CSS transitions/animations to settle on an element.
(define
hs-wait-for
(fn
(target event-name)
(perform (list (quote io-wait-event) target event-name))))
;; ── Class manipulation ──────────────────────────────────────────
;; Toggle a single class on an element.
(define hs-settle (fn (target) (perform (list (quote io-settle) target))))
;; Toggle between two classes — exactly one is active at a time.
(define
hs-toggle-class!
(fn (target cls) (host-call (host-get target "classList") "toggle" cls)))
;; Take a class from siblings — add to target, remove from others.
;; (hs-take! target cls) — like radio button class behavior
(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)))))
;; ── DOM insertion ───────────────────────────────────────────────
;; Put content at a position relative to a target.
;; pos: "into" | "before" | "after"
(define
hs-toggle-style!
(fn
(target prop)
(let
((cur (dom-get-style target prop)))
(cond
((= prop "visibility")
(if
(= cur "hidden")
(dom-set-style target prop "visible")
(dom-set-style target prop "hidden")))
((or (= prop "display") (= prop "opacity"))
(if
(or (= cur "none") (= cur "0"))
(dom-set-style target prop (if (= prop "opacity") "1" ""))
(dom-set-style target prop (if (= prop "display") "none" "0"))))
(true
(if
(or (= cur "") (= cur nil))
(dom-set-style target prop "hidden")
(dom-set-style target prop "")))))))
;; ── Navigation / traversal ──────────────────────────────────────
;; 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
hs-take!
(fn
(target kind name scope &rest extra)
(let
((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))
(let
((attr-val (if (> (len extra) 0) (first extra) nil))
(with-val (if (> (len extra) 1) (nth extra 1) nil)))
(do
(when
with-val
(for-each (fn (el) (dom-set-attr el name with-val)) els))
(if
attr-val
(dom-set-attr target name attr-val)
(dom-set-attr target name ""))))))))
;; First element matching selector within a scope.
(begin
(define
hs-element?
(fn
(v)
(and v (or (host-get v "nodeType") (host-get v "__mock_type")))))
(define
hs-set-attr!
(fn
(el name val)
(if (nil? val) (dom-remove-attr el name) (dom-set-attr el name val))))
(define
hs-put!
(fn
(value pos target)
(cond
((= pos "into")
(cond
((list? target) target)
((hs-element? value)
(do
(dom-set-inner-html target "")
(host-call target "appendChild" value)))
(true
(do
(dom-set-inner-html target value)
(hs-boot-subtree! target)))))
((= pos "before")
(if
(hs-element? value)
(let
((parent (dom-parent target)))
(when parent (host-call parent "insertBefore" value target)))
(let
((parent (dom-parent target)))
(do
(dom-insert-adjacent-html target "beforebegin" value)
(when parent (hs-boot-subtree! parent))))))
((= pos "after")
(if
(hs-element? value)
(let
((parent (dom-parent target))
(next (host-get target "nextSibling")))
(when
parent
(if
next
(host-call parent "insertBefore" value next)
(host-call parent "appendChild" value))))
(let
((parent (dom-parent target)))
(do
(dom-insert-adjacent-html target "afterend" value)
(when parent (hs-boot-subtree! parent))))))
((= pos "start")
(cond
((list? target) (append! target value 0))
((hs-element? value) (dom-prepend target value))
(true
(do
(dom-insert-adjacent-html target "afterbegin" value)
(hs-boot-subtree! target)))))
((= pos "end")
(cond
((list? target) (append! target value))
((hs-element? value) (dom-append target value))
(true
(do
(dom-insert-adjacent-html target "beforeend" value)
(hs-boot-subtree! target)))))))))
;; Last element matching selector.
(define
hs-add-to!
(fn
(value target)
(if
(list? target)
(append target (list value))
(host-call target "push" value))))
;; First/last within a specific scope.
(define
hs-remove-from!
(fn
(value target)
(if
(list? target)
(filter (fn (x) (not (= x value))) target)
(host-call target "splice" (host-call target "indexOf" value) 1))))
(define
hs-set-on!
(fn
(props target)
(for-each (fn (k) (host-set! target k (get props k))) (keys props))))
;; ── Iteration ───────────────────────────────────────────────────
;; Repeat a thunk N times.
(define hs-navigate! (fn (url) (perform (list (quote io-navigate) url))))
;; Repeat forever (until break — relies on exception/continuation).
(define
hs-scroll!
(fn
(target position)
(host-call
target
"scrollIntoView"
(list
(cond
((= position "bottom") (dict :block "end"))
(true (dict :block "start")))))))
;; ── Fetch ───────────────────────────────────────────────────────
;; Fetch a URL, parse response according to format.
;; (hs-fetch url format) — format is "json" | "text" | "html"
(define
hs-halt!
(fn
(ev mode)
(do
(when
ev
(cond
((= mode "default") (host-call ev "preventDefault"))
((= mode "bubbling") (host-call ev "stopPropagation"))
((= mode "the-event")
(do
(host-call ev "preventDefault")
(host-call ev "stopPropagation")))
(true
(do
(host-call ev "preventDefault")
(host-call ev "stopPropagation")))))
(when (not (= mode "the-event")) (raise (list "hs-return" nil))))))
;; ── Type coercion ───────────────────────────────────────────────
;; Coerce a value to a type by name.
;; (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)
(cond
((list? target) (for-each (fn (el) (hs-reset! el)) target))
((nil? target) nil)
(true
(let
((tag (dom-get-prop target "tagName")))
(cond
((= tag "FORM") (host-call target "reset" (list)))
((or (= tag "INPUT") (= tag "TEXTAREA"))
(let
((input-type (dom-get-prop target "type")))
(cond
((or (= input-type "checkbox") (= input-type "radio"))
(dom-set-prop
target
"checked"
(dom-get-prop target "defaultChecked")))
(true
(dom-set-prop
target
"value"
(dom-get-prop target "defaultValue"))))))
((= tag "SELECT")
(let
((options (host-call target "querySelectorAll" "option"))
(default-val nil))
(do
(for-each
(fn
(opt)
(when
(and
(nil? default-val)
(dom-get-prop opt "defaultSelected"))
(set! default-val (dom-get-prop opt "value"))))
options)
(when
(and (nil? default-val) (> (len options) 0))
(set! default-val (dom-get-prop (first options) "value")))
(when default-val (dom-set-prop target "value" default-val)))))
(true nil)))))))
;; ── 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-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)))))
;; ── 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-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)))))
;; ── Transition ──────────────────────────────────────────────────
;; Transition a CSS property to a value, optionally with duration.
;; (hs-transition target prop value duration)
(define
hs-query-all
(fn (sel) (host-call (dom-body) "querySelectorAll" sel)))
(define
hs-list-set
(fn (lst idx val) (map-indexed (fn (i x) (if (= i idx) val x)) lst)))
(define
hs-to-number
(fn
(v)
(cond
((number? v) v)
((string? v) (or (parse-number v) 0))
((nil? v) 0)
(true (or (parse-number (str v)) 0)))))
(define
hs-query-first
(fn (sel) (host-call (host-global "document") "querySelector" sel)))
(define
hs-query-last
(fn
(sel)
(let
((all (dom-query-all (dom-body) sel)))
(if (> (len all) 0) (nth all (- (len all) 1)) nil))))
(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))))
(define
hs-repeat-times
(fn
(n thunk)
(define
do-repeat
(fn
(i)
(when
(< i n)
(let
((signal (guard (e ((or (= (str e) "hs-break") (= (str e) "hs-continue")) (str e)) (true (raise e))) (do (thunk) nil))))
(cond
((= signal "hs-break") nil)
((= signal "hs-continue") (do-repeat (+ i 1)))
(true (do-repeat (+ i 1))))))))
(do-repeat 0)))
(define
hs-repeat-forever
(fn
(thunk)
(define
do-forever
(fn
()
(let
((signal (guard (e ((or (= (str e) "hs-break") (= (str e) "hs-continue")) (str e)) (true (raise e))) (do (thunk) nil))))
(cond
((= signal "hs-break") nil)
((= signal "hs-continue") (do-forever))
(true (do-forever))))))
(do-forever)))
(define
hs-repeat-while
(fn
(cond-fn thunk)
(when
(cond-fn)
(let
((signal (guard (e ((or (= (str e) "hs-break") (= (str e) "hs-continue")) (str e)) (true (raise e))) (do (thunk) nil))))
(cond
((= signal "hs-break") nil)
((= signal "hs-continue") (hs-repeat-while cond-fn thunk))
(true (hs-repeat-while cond-fn thunk)))))))
(define
hs-repeat-until
(fn
(cond-fn thunk)
(let
((signal (guard (e ((or (= (str e) "hs-break") (= (str e) "hs-continue")) (str e)) (true (raise e))) (do (thunk) nil))))
(cond
((= signal "hs-break") nil)
((= signal "hs-continue")
(if (cond-fn) nil (hs-repeat-until cond-fn thunk)))
(true (if (cond-fn) nil (hs-repeat-until cond-fn thunk)))))))
(define
hs-for-each
(fn
(fn-body collection)
(let
((items (cond ((list? collection) collection) ((dict? collection) (keys collection)) ((nil? collection) (list)) (true (list)))))
(define
do-loop
(fn
(remaining)
(when
(not (empty? remaining))
(let
((signal (guard (e ((or (= (str e) "hs-break") (= (str e) "hs-continue")) (str e)) (true (raise e))) (do (fn-body (first remaining)) nil))))
(cond
((= signal "hs-break") nil)
((= signal "hs-continue") (do-loop (rest remaining)))
(true (do-loop (rest remaining))))))))
(do-loop items))))
(begin
(define
hs-append
(fn
(target value)
(cond
((string? target) (str target value))
((list? target) (append target (list value)))
(true (str target value)))))
(define
hs-append!
(fn (value target) (dom-insert-adjacent-html target "beforeend" value))))
;; ── Sandbox/test runtime additions ──────────────────────────────
;; Property access — dot notation and .length
(define
hs-fetch
(fn
(url format)
(perform (list "io-fetch" url (if format format "text")))))
;; DOM query stub — sandbox returns empty list
(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")
(if
(list? value)
(join "," (map (fn (x) (str x)) value))
(str value)))
((= 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 "HTML") (str value))
((= type-name "JSON")
(cond
((string? value) (guard (_e (true value)) (json-parse value)))
((dict? value) (json-stringify value))
((list? value) (json-stringify value))
(true value)))
((= type-name "Object")
(if
(string? value)
(guard (_e (true value)) (json-parse value))
value))
((= type-name "JSONString") (json-stringify value))
((or (= type-name "Fixed") (= type-name "Fixed:") (starts-with? type-name "Fixed:"))
(let
((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 (pow 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) (sort (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))
((= type-name "Set")
(if
(list? value)
(reduce
(fn
(acc x)
(if (some (fn (a) (= a x)) acc) acc (append acc (list x))))
(list)
value)
value))
((= type-name "Map")
(if
(dict? value)
(map (fn (k) (list k (get value k))) (keys value))
value))
(true value))))
;; Method dispatch — obj.method(args)
(define
hs-add
(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)))))
;; ── 0.9.90 features ─────────────────────────────────────────────
;; beep! — debug logging, returns value unchanged
(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)))))
;; Property-based is — check obj.key truthiness
(define hs-install (fn (behavior-fn) (behavior-fn me)))
;; Array slicing (inclusive both ends)
(define
hs-measure
(fn (target) (perform (list (quote io-measure) target))))
;; Collection: sorted by
(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))))
;; Collection: sorted by descending
(define
hs-transition-from
(fn
(target prop from-val to-val duration)
(dom-set-style target prop (str from-val))
(when
duration
(dom-set-style
target
"transition"
(str prop " " (/ duration 1000) "s")))
(dom-set-style target prop (str to-val))
(when duration (hs-settle target))))
;; Collection: split by
(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))
((= 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)))))))
;; Collection: joined by
(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-eq-ignore-case
(fn (a b) (= (downcase (str a)) (downcase (str b)))))
(define
hs-starts-with?
(fn
(s prefix)
(cond
((nil? s) false)
((nil? prefix) false)
(true (starts-with? (str s) (str prefix))))))
(define
hs-ends-with?
(fn
(s suffix)
(cond
((nil? s) false)
((nil? suffix) false)
(true (ends-with? (str s) (str suffix))))))
(define
hs-precedes?
(fn
(a b)
(cond ((nil? a) false) ((nil? b) false) (true (< (str a) (str b))))))
(define
hs-follows?
(fn
(a b)
(cond ((nil? a) false) ((nil? b) false) (true (> (str a) (str b))))))
(define
hs-starts-with-ic?
(fn (str prefix) (starts-with? (downcase str) (downcase prefix))))
(define
hs-ends-with-ic?
(fn (str suffix) (ends-with? (downcase str) (downcase suffix))))
(define
hs-matches-ignore-case?
(fn
(target pattern)
(cond
((string? target)
(contains? (downcase (str target)) (downcase (str pattern))))
(true false))))
(define
hs-contains-ignore-case?
(fn
(haystack needle)
(contains? (downcase (str haystack)) (downcase (str needle)))))
(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)
(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))))
(define
hs-contains?
(fn
(collection item)
(cond
((nil? collection) false)
((string? collection) (string-contains? collection (str item)))
((list? collection)
(cond
((nil? item) (list))
((list? item)
(filter (fn (x) (hs-contains? collection x)) item))
(true
(if
(= (len collection) 0)
false
(if
(= (first collection) item)
true
(hs-contains? (rest collection) item))))))
(true false))))
(define
hs-is
(fn
(obj thunk prop)
(cond
((and (dict? obj) (some (fn (k) (= k prop)) (keys obj)))
(not (hs-falsy? (get obj prop))))
(true
(let
((r (cek-try thunk)))
(if
(and (list? r) (= (first r) (quote ok)))
(= obj (nth r 1))
(= obj nil)))))))
(define precedes? (fn (a b) (< (str a) (str b))))
(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-empty-like
(fn
(v)
(cond
((list? v) (list))
((dict? v) (dict))
((string? v) "")
((nil? v) nil)
(true v))))
(define
hs-empty-target!
(fn
(target)
(cond
((list? target) (for-each (fn (el) (hs-empty-target! el)) target))
((nil? target) nil)
(true
(let
((tag (dom-get-prop target "tagName")))
(cond
((or (= tag "INPUT") (= tag "TEXTAREA"))
(let
((input-type (dom-get-prop target "type")))
(if
(or (= input-type "checkbox") (= input-type "radio"))
(dom-set-prop target "checked" false)
(dom-set-prop target "value" ""))))
((= tag "FORM")
(let
((children (host-call target "querySelectorAll" "input, textarea, select")))
(for-each (fn (el) (hs-empty-target! el)) children)))
(true (dom-set-inner-html target ""))))))))
(define
hs-morph-char
(fn (s p) (if (or (< p 0) (>= p (string-length s))) nil (nth s p))))
(define
hs-morph-index-from
(fn
(s needle from)
(let
((r (index-of (substring s from (string-length s)) needle)))
(if (< r 0) -1 (+ from r)))))
(define
hs-morph-sws
(fn
(s p)
(let
((c (hs-morph-char s p)))
(if (and c (hs-ws? c)) (hs-morph-sws s (+ p 1)) p))))
(define
hs-morph-read-until
(fn
(s p stop)
(define
loop
(fn
(q)
(let
((c (hs-morph-char s q)))
(if (and c (< (index-of stop c) 0)) (loop (+ q 1)) q))))
(let ((e (loop p))) (list (substring s p e) e))))
(define
hs-morph-parse-attrs
(fn
(s p acc)
(let
((p (hs-morph-sws s p)))
(let
((c (hs-morph-char s p)))
(cond
((nil? c) (list acc p false))
((= c ">") (list acc (+ p 1) false))
((= c "/")
(if
(= (hs-morph-char s (+ p 1)) ">")
(list acc (+ p 2) true)
(list acc (+ p 1) false)))
(true
(let
((r (hs-morph-read-until s p " \t\n=/>")))
(let
((name (first r)) (p2 (nth r 1)))
(let
((p3 (hs-morph-sws s p2)))
(if
(= (hs-morph-char s p3) "=")
(let
((p4 (hs-morph-sws s (+ p3 1))))
(let
((c2 (hs-morph-char s p4)))
(cond
((= c2 "\"")
(let
((close (hs-morph-index-from s "\"" (+ p4 1))))
(hs-morph-parse-attrs
s
(+ close 1)
(append
acc
(list
(list name (substring s (+ p4 1) close)))))))
((= c2 "'")
(let
((close (hs-morph-index-from s "'" (+ p4 1))))
(hs-morph-parse-attrs
s
(+ close 1)
(append
acc
(list
(list name (substring s (+ p4 1) close)))))))
(true
(let
((r2 (hs-morph-read-until s p4 " \t\n/>")))
(hs-morph-parse-attrs
s
(nth r2 1)
(append acc (list (list name (first r2))))))))))
(hs-morph-parse-attrs
s
p3
(append acc (list (list name ""))))))))))))))
(define
hs-morph-parse-element
(fn
(s p)
(let
((p (hs-morph-sws s p)))
(if
(not (= (hs-morph-char s p) "<"))
nil
(let
((r (hs-morph-read-until s (+ p 1) " \t\n/>")))
(let
((tag (first r)) (p2 (nth r 1)))
(let
((ar (hs-morph-parse-attrs s p2 (list))))
(let
((attrs (first ar))
(p3 (nth ar 1))
(self-closing (nth ar 2)))
(if
self-closing
{:children (list) :end p3 :tag tag :type "element" :attrs attrs}
(let
((cr (hs-morph-parse-children s p3 (list))))
{:children (first cr) :end (nth cr 1) :tag tag :type "element" :attrs attrs}))))))))))
(define
hs-morph-parse-children
(fn
(s p acc)
(let
((c (hs-morph-char s p)))
(cond
((nil? c) (list acc p))
((= c "<")
(if
(= (hs-morph-char s (+ p 1)) "/")
(let
((close-gt (hs-morph-index-from s ">" (+ p 1))))
(list acc (+ close-gt 1)))
(let
((child (hs-morph-parse-element s p)))
(if
(nil? child)
(list acc p)
(hs-morph-parse-children
s
(get child :end)
(append acc (list child)))))))
(true
(let
((r (hs-morph-read-until s p "<")))
(hs-morph-parse-children
s
(nth r 1)
(append acc (list {:text (first r) :type "text"})))))))))
(define
hs-morph-apply-attrs
(fn
(el attrs keep-id)
(for-each
(fn
(av)
(let
((n (first av)) (v (nth av 1)))
(cond
((= n "class")
(for-each
(fn
(c)
(when (> (string-length c) 0) (dom-add-class el c)))
(split v " ")))
((and keep-id (= n "id")) nil)
(true (dom-set-attr el n v)))))
attrs)))
(define
hs-morph-build-children
(fn
(parent children)
(cond
((= (len children) 0) nil)
((and (= (len children) 1) (= (get (first children) :type) "text"))
(dom-set-inner-html parent (get (first children) :text)))
(true (for-each (fn (c) (hs-morph-build-child parent c)) children)))))
(define
hs-morph-build-child
(fn
(parent node)
(cond
((= (get node :type) "element")
(let
((el (dom-create-element (get node :tag))))
(do
(hs-morph-apply-attrs el (get node :attrs) false)
(hs-morph-build-children el (get node :children))
(dom-append parent el)
(hs-activate! el))))
(true nil))))
(define
hs-morph!
(fn
(target content)
(when
target
(let
((tree (hs-morph-parse-element content 0)))
(when
tree
(do
(hs-morph-apply-attrs target (get tree :attrs) true)
(dom-set-inner-html target "")
(hs-morph-build-children target (get tree :children))))))))
(define
hs-open!
(fn
(el)
(let
((tag (dom-get-prop el "tagName")))
(cond
((= tag "DIALOG") (host-call el "showModal"))
(true
(do (dom-set-attr el "open" "") (dom-set-prop el "open" true)))))))
(define
hs-close!
(fn
(el)
(let
((tag (dom-get-prop el "tagName")))
(cond
((= tag "DIALOG") (host-call el "close"))
(true
(do
(host-call el "removeAttribute" "open")
(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-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))))
(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))))
(define hs-beep (fn (v) v))
(define hs-prop-is (fn (obj key) (not (hs-falsy? (host-get obj key)))))
(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))))
(define
hs-pick-first
(fn
(col n)
(let ((m (if (< n (len col)) n (len col)))) (slice col 0 m))))
(define
hs-pick-last
(fn
(col n)
(let
((total (len col)))
(let
((start (if (< n total) (- total n) 0)))
(slice col start total)))))
(define
hs-pick-random
(fn
(col n)
(if
(nil? n)
(first col)
(let ((m (if (< n (len col)) n (len col)))) (slice col 0 m)))))
(define hs-pick-items (fn (col start end) (slice col start end)))
(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)))))
(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)))))
(define hs-split-by (fn (s sep) (split s sep)))
(define hs-joined-by (fn (col sep) (join sep col)))
(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)))))
(define
hs-sorted-by-desc
(fn (col key-fn) (reverse (hs-sorted-by col key-fn))))
(define
hs-dom-has-var?
(fn
(el name)
(if
(nil? el)
false
(let
((store (host-get el "__hs_vars")))
(if (nil? store) false (has-key? store name))))))
(define
hs-dom-get-var-raw
(fn
(el name)
(let
((store (host-get el "__hs_vars")))
(if (nil? store) nil (host-get store name)))))
(define
hs-dom-set-var-raw!
(fn
(el name val)
(let
((changed (not (and (hs-dom-has-var? el name) (= (hs-dom-get-var-raw el name) val)))))
(do
(when
(nil? (host-get el "__hs_vars"))
(host-set! el "__hs_vars" (dict)))
(host-set! (host-get el "__hs_vars") name val)
(when changed (hs-dom-fire-watchers! el name val))))))
(define
hs-dom-resolve-start
(fn
(el)
(if
(nil? el)
nil
(let
((scope (dom-get-attr el "dom-scope")))
(cond
((or (nil? scope) (= scope "") (= scope "isolated")) el)
((starts-with? scope "closest ")
(dom-closest el (slice scope 8 (len scope))))
((starts-with? scope "parent of ")
(let
((match (dom-closest el (slice scope 10 (len scope)))))
(if match (dom-parent match) nil)))
(true el))))))
(define
hs-dom-walk
(fn
(el name)
(cond
((nil? el) nil)
((hs-dom-has-var? el name) (hs-dom-get-var-raw el name))
((= (dom-get-attr el "dom-scope") "isolated") nil)
(true (hs-dom-walk (dom-parent el) name)))))
(define
hs-dom-find-owner
(fn
(el name)
(cond
((nil? el) nil)
((hs-dom-has-var? el name) el)
((= (dom-get-attr el "dom-scope") "isolated") nil)
(true (hs-dom-find-owner (dom-parent el) name)))))
(define
hs-dom-get
(fn (el name) (hs-dom-walk (hs-dom-resolve-start el) name)))
(define
hs-dom-set!
(fn
(el name val)
(let
((start (hs-dom-resolve-start el)))
(let
((owner (hs-dom-find-owner start name)))
(hs-dom-set-var-raw! (if owner owner start) name val)))))
(define _hs-dom-watchers (list))
(define
hs-dom-watch!
(fn
(el name handler)
(set! _hs-dom-watchers (cons (list el name handler) _hs-dom-watchers))))
(define
hs-dom-fire-watchers!
(fn
(el name val)
(for-each
(fn
(entry)
(when
(and
(= (nth entry 1) name)
(hs-dom-is-ancestor? el (nth entry 0)))
((nth entry 2) val)))
_hs-dom-watchers)))
(define
hs-dom-is-ancestor?
(fn
(a b)
(cond
((nil? b) false)
((= a b) true)
(true (hs-dom-is-ancestor? a (dom-parent b))))))