HS runtime + generator: make, Values, toggle styles, scoped storage, array ops, fetch coercion, scripts in PW bodies
Runtime (lib/hyperscript/ + shared/static/wasm/sx/hs-*.sx):
- make: parser accepts `<tag.class#id/>` selectors and `from <expr>,…`; compiler
emits via scoped-set so `called <name>` persists; `called $X` lands on
window; runtime dispatches element vs host-new constructor by type.
- Values: `x as Values` walks form inputs/selects/textareas, producing
{name: value | [value,…]}; duplicates promote to array; multi-select and
checkbox/radio handled.
- toggle *display/*visibility/*opacity: paired with sensible inline defaults
in the mock DOM so toggle flips block/visible/1 ↔ none/hidden/0.
- add/remove/put at array: emit-set paths route list mutations back through
the scoped binding; add hs-put-at! / hs-splice-at! / hs-dict-without.
- remove OBJ.KEY / KEY of OBJ: rebuild dict via hs-dict-without and reassign,
since SX dicts are copy-on-read across the bridge.
- dom-set-data: use (host-new "Object") rather than (dict) so element-local
storage actually persists between reads.
- fetch: hs-fetch normalizes JSON/Object/Text/Response format aliases;
compiler sets `the-result` when wrapping a fetch in the `let ((it …))`
chain, and __get-cmd shares one evaluation via __hs-g.
Mock DOM (tests/hs-run-filtered.js):
- parseHTMLFragments accepts void elements (<input>, <br>, …);
- setAttribute tracks name/type/checked/selected/multiple;
- select.options populated on appendChild;
- insertAdjacentHTML parses fragments and inserts real El children into the
parent so HS-activated handlers attach.
Generator (tests/playwright/generate-sx-tests.py):
- process_hs_val strips `//` / `--` line comments before newline→then
collapse, and strips spurious `then` before else/end/catch/finally.
- parse_dev_body interleaves window-setup ops and DOM resets between
actions/assertions; pre-html setups still emit up front.
- generate_test_pw compiles any `<script type=text/hyperscript>` (flattened
across JS string-concat) under guard, exposing def blocks.
- Ordered ops for `run()`-style tests check window.obj.prop via new
_js_window_expr_to_sx; add DOM-constructing evaluate + _hyperscript
pattern for `as Values` tests (result.key[i].toBe(…)).
- js_val_to_sx handles backticks and escapes embedded quotes.
Net delta across suites:
- if 16→18, make 0→8, toggle 12→21, add 9→10, remove 11→16, put 29→31,
fetch 11→15, repeat 14→26, expressions/asExpression 20→25, set 27→28,
core/scoping 12→14, when 39→39 (no regression).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -353,16 +353,41 @@
|
|||||||
emit-make
|
emit-make
|
||||||
(fn
|
(fn
|
||||||
(ast)
|
(ast)
|
||||||
(if
|
(let
|
||||||
(= (len ast) 3)
|
((type-name (nth ast 1))
|
||||||
(list
|
(called (if (>= (len ast) 3) (nth ast 2) nil))
|
||||||
(quote let)
|
(args (if (>= (len ast) 4) (nth ast 3) nil))
|
||||||
(list
|
(kind (if (>= (len ast) 5) (nth ast 4) (quote auto))))
|
||||||
(list
|
(let
|
||||||
(make-symbol (nth ast 2))
|
((make-call (cond ((nil? args) (list (quote hs-make) type-name)) (true (cons (quote hs-make) (cons type-name (map hs-to-sx args)))))))
|
||||||
(list (quote hs-make) (nth ast 1))))
|
(cond
|
||||||
(make-symbol (nth ast 2)))
|
((and called (> (len called) 1) (= (substring called 0 1) "$"))
|
||||||
(list (quote hs-make) (nth ast 1)))))
|
(list
|
||||||
|
(quote let)
|
||||||
|
(list (list (quote __hs-mk) make-call))
|
||||||
|
(list
|
||||||
|
(quote do)
|
||||||
|
(list
|
||||||
|
(quote host-set!)
|
||||||
|
(list (quote host-global) "window")
|
||||||
|
called
|
||||||
|
(quote __hs-mk))
|
||||||
|
(list (quote set!) (quote it) (quote __hs-mk))
|
||||||
|
(quote __hs-mk))))
|
||||||
|
(called
|
||||||
|
(list
|
||||||
|
(quote do)
|
||||||
|
(list (quote set!) (make-symbol called) make-call)
|
||||||
|
(list (quote set!) (quote it) (make-symbol called))
|
||||||
|
(make-symbol called)))
|
||||||
|
(true
|
||||||
|
(list
|
||||||
|
(quote let)
|
||||||
|
(list (list (quote __hs-mk) make-call))
|
||||||
|
(list
|
||||||
|
(quote do)
|
||||||
|
(list (quote set!) (quote it) (quote __hs-mk))
|
||||||
|
(quote __hs-mk)))))))))
|
||||||
(define
|
(define
|
||||||
emit-inc
|
emit-inc
|
||||||
(fn
|
(fn
|
||||||
@@ -1182,13 +1207,38 @@
|
|||||||
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt))
|
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt))
|
||||||
(nth ast 1)))))
|
(nth ast 1)))))
|
||||||
((= head (quote remove-element))
|
((= head (quote remove-element))
|
||||||
(list (quote dom-remove) (hs-to-sx (nth ast 1))))
|
(let
|
||||||
|
((tgt (nth ast 1)))
|
||||||
|
(cond
|
||||||
|
((and (list? tgt) (= (first tgt) (quote array-index)))
|
||||||
|
(let
|
||||||
|
((coll (nth tgt 1)) (idx (hs-to-sx (nth tgt 2))))
|
||||||
|
(emit-set
|
||||||
|
coll
|
||||||
|
(list (quote hs-splice-at!) (hs-to-sx coll) idx))))
|
||||||
|
((and (list? tgt) (= (first tgt) dot-sym))
|
||||||
|
(let
|
||||||
|
((obj (nth tgt 1)) (prop (nth tgt 2)))
|
||||||
|
(emit-set
|
||||||
|
obj
|
||||||
|
(list (quote hs-dict-without) (hs-to-sx obj) prop))))
|
||||||
|
((and (list? tgt) (= (first tgt) (quote of)))
|
||||||
|
(let
|
||||||
|
((prop-ast (nth tgt 1)) (obj-ast (nth tgt 2)))
|
||||||
|
(let
|
||||||
|
((prop (cond ((string? prop-ast) prop-ast) ((and (list? prop-ast) (= (first prop-ast) (quote ref))) (nth prop-ast 1)) (true (hs-to-sx prop-ast)))))
|
||||||
|
(emit-set
|
||||||
|
obj-ast
|
||||||
|
(list
|
||||||
|
(quote hs-dict-without)
|
||||||
|
(hs-to-sx obj-ast)
|
||||||
|
prop)))))
|
||||||
|
(true (list (quote dom-remove) (hs-to-sx tgt))))))
|
||||||
((= head (quote add-value))
|
((= head (quote add-value))
|
||||||
(let
|
(let
|
||||||
((val (hs-to-sx (nth ast 1))) (tgt (nth ast 2)))
|
((val (hs-to-sx (nth ast 1))) (tgt (nth ast 2)))
|
||||||
(list
|
(emit-set
|
||||||
(quote set!)
|
tgt
|
||||||
(hs-to-sx tgt)
|
|
||||||
(list (quote hs-add-to!) val (hs-to-sx tgt)))))
|
(list (quote hs-add-to!) val (hs-to-sx tgt)))))
|
||||||
((= head (quote add-attr))
|
((= head (quote add-attr))
|
||||||
(let
|
(let
|
||||||
@@ -1201,9 +1251,8 @@
|
|||||||
((= head (quote remove-value))
|
((= head (quote remove-value))
|
||||||
(let
|
(let
|
||||||
((val (hs-to-sx (nth ast 1))) (tgt (nth ast 2)))
|
((val (hs-to-sx (nth ast 1))) (tgt (nth ast 2)))
|
||||||
(list
|
(emit-set
|
||||||
(quote set!)
|
tgt
|
||||||
(hs-to-sx tgt)
|
|
||||||
(list (quote hs-remove-from!) val (hs-to-sx tgt)))))
|
(list (quote hs-remove-from!) val (hs-to-sx tgt)))))
|
||||||
((= head (quote empty-target))
|
((= head (quote empty-target))
|
||||||
(let
|
(let
|
||||||
@@ -1348,11 +1397,16 @@
|
|||||||
((= head (quote set!))
|
((= head (quote set!))
|
||||||
(emit-set (nth ast 1) (hs-to-sx (nth ast 2))))
|
(emit-set (nth ast 1) (hs-to-sx (nth ast 2))))
|
||||||
((= head (quote put!))
|
((= head (quote put!))
|
||||||
(list
|
(let
|
||||||
(quote hs-put!)
|
((val (hs-to-sx (nth ast 1)))
|
||||||
(hs-to-sx (nth ast 1))
|
(pos (nth ast 2))
|
||||||
(nth ast 2)
|
(raw-tgt (nth ast 3)))
|
||||||
(hs-to-sx (nth ast 3))))
|
(cond
|
||||||
|
((and (or (= pos "end") (= pos "start")) (list? raw-tgt) (or (= (first raw-tgt) (quote local)) (= (first raw-tgt) (quote ref))))
|
||||||
|
(emit-set
|
||||||
|
raw-tgt
|
||||||
|
(list (quote hs-put-at!) val pos (hs-to-sx raw-tgt))))
|
||||||
|
(true (list (quote hs-put!) val pos (hs-to-sx raw-tgt))))))
|
||||||
((= head (quote if))
|
((= head (quote if))
|
||||||
(if
|
(if
|
||||||
(> (len ast) 3)
|
(> (len ast) 3)
|
||||||
@@ -1387,10 +1441,24 @@
|
|||||||
(reduce
|
(reduce
|
||||||
(fn
|
(fn
|
||||||
(body cmd)
|
(body cmd)
|
||||||
(list
|
(if
|
||||||
(quote let)
|
(and
|
||||||
(list (list (quote it) cmd))
|
(list? cmd)
|
||||||
body))
|
(= (first cmd) (quote hs-fetch)))
|
||||||
|
(list
|
||||||
|
(quote let)
|
||||||
|
(list (list (quote it) cmd))
|
||||||
|
(list
|
||||||
|
(quote begin)
|
||||||
|
(list
|
||||||
|
(quote set!)
|
||||||
|
(quote the-result)
|
||||||
|
(quote it))
|
||||||
|
body))
|
||||||
|
(list
|
||||||
|
(quote let)
|
||||||
|
(list (list (quote it) cmd))
|
||||||
|
body)))
|
||||||
(nth compiled (- (len compiled) 1))
|
(nth compiled (- (len compiled) 1))
|
||||||
(rest (reverse compiled)))
|
(rest (reverse compiled)))
|
||||||
(cons (quote do) compiled))))
|
(cons (quote do) compiled))))
|
||||||
@@ -1512,10 +1580,13 @@
|
|||||||
(let
|
(let
|
||||||
((val (hs-to-sx (nth ast 1))))
|
((val (hs-to-sx (nth ast 1))))
|
||||||
(list
|
(list
|
||||||
(quote begin)
|
(quote let)
|
||||||
(list (quote set!) (quote the-result) val)
|
(list (list (quote __hs-g) val))
|
||||||
(list (quote set!) (quote it) val)
|
(list
|
||||||
val)))
|
(quote begin)
|
||||||
|
(list (quote set!) (quote the-result) (quote __hs-g))
|
||||||
|
(list (quote set!) (quote it) (quote __hs-g))
|
||||||
|
(quote __hs-g)))))
|
||||||
((= head (quote append!))
|
((= head (quote append!))
|
||||||
(let
|
(let
|
||||||
((tgt (hs-to-sx (nth ast 2)))
|
((tgt (hs-to-sx (nth ast 2)))
|
||||||
|
|||||||
@@ -1975,14 +1975,29 @@
|
|||||||
()
|
()
|
||||||
(if (= (tp-val) "a") (adv!) nil)
|
(if (= (tp-val) "a") (adv!) nil)
|
||||||
(let
|
(let
|
||||||
((type-name (tp-val)))
|
((kind (if (= (tp-type) "selector") (quote element) (quote object)))
|
||||||
|
(type-name (tp-val)))
|
||||||
(adv!)
|
(adv!)
|
||||||
(let
|
(let
|
||||||
((called (if (match-kw "called") (let ((n (tp-val))) (adv!) n) nil)))
|
((called nil) (args nil))
|
||||||
(if
|
(define
|
||||||
called
|
parse-from-args
|
||||||
(list (quote make) type-name called)
|
(fn
|
||||||
(list (quote make) type-name))))))
|
()
|
||||||
|
(set! args (append args (list (parse-expr))))
|
||||||
|
(when (= (tp-type) "comma") (adv!) (parse-from-args))))
|
||||||
|
(define
|
||||||
|
parse-clauses
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(cond
|
||||||
|
((match-kw "from")
|
||||||
|
(do (parse-from-args) (parse-clauses)))
|
||||||
|
((match-kw "called")
|
||||||
|
(do (set! called (tp-val)) (adv!) (parse-clauses)))
|
||||||
|
(true nil))))
|
||||||
|
(parse-clauses)
|
||||||
|
(list (quote make) type-name called args kind)))))
|
||||||
(define
|
(define
|
||||||
parse-install-cmd
|
parse-install-cmd
|
||||||
(fn
|
(fn
|
||||||
|
|||||||
@@ -293,18 +293,91 @@
|
|||||||
(filter (fn (x) (not (= x value))) target)
|
(filter (fn (x) (not (= x value))) target)
|
||||||
(host-call target "splice" (host-call target "indexOf" value) 1))))
|
(host-call target "splice" (host-call target "indexOf" value) 1))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hs-splice-at!
|
||||||
|
(fn
|
||||||
|
(target idx)
|
||||||
|
(if
|
||||||
|
(list? target)
|
||||||
|
(let
|
||||||
|
((n (len target)))
|
||||||
|
(let
|
||||||
|
((i (if (< idx 0) (+ n idx) idx)))
|
||||||
|
(cond
|
||||||
|
((or (< i 0) (>= i n)) target)
|
||||||
|
(true (concat (slice target 0 i) (slice target (+ i 1) n))))))
|
||||||
|
(do
|
||||||
|
(when
|
||||||
|
target
|
||||||
|
(let
|
||||||
|
((n (host-get target "length")))
|
||||||
|
(let
|
||||||
|
((i (if (< idx 0) (+ (if (nil? n) 0 n) idx) idx)))
|
||||||
|
(host-call target "splice" i 1))))
|
||||||
|
target))))
|
||||||
|
|
||||||
|
;; ── Iteration ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
;; Repeat a thunk N times.
|
||||||
|
(define
|
||||||
|
hs-put-at!
|
||||||
|
(fn
|
||||||
|
(value pos target)
|
||||||
|
(cond
|
||||||
|
((nil? target) (list value))
|
||||||
|
((list? target)
|
||||||
|
(if
|
||||||
|
(= pos "start")
|
||||||
|
(cons value target)
|
||||||
|
(append target (list value))))
|
||||||
|
(true
|
||||||
|
(do
|
||||||
|
(cond
|
||||||
|
((= pos "end") (host-call target "push" value))
|
||||||
|
((= pos "start") (host-call target "unshift" value)))
|
||||||
|
target)))))
|
||||||
|
|
||||||
|
;; Repeat forever (until break — relies on exception/continuation).
|
||||||
|
(define
|
||||||
|
hs-dict-without
|
||||||
|
(fn
|
||||||
|
(obj key)
|
||||||
|
(cond
|
||||||
|
((nil? obj) (dict))
|
||||||
|
((dict? obj)
|
||||||
|
(let
|
||||||
|
((out (dict)))
|
||||||
|
(for-each
|
||||||
|
(fn (k) (when (not (= k key)) (dict-set! out k (get obj k))))
|
||||||
|
(keys obj))
|
||||||
|
out))
|
||||||
|
(true
|
||||||
|
(let
|
||||||
|
((out (dict)))
|
||||||
|
(host-call (host-global "Object") "assign" out obj)
|
||||||
|
(host-call (host-global "Reflect") "deleteProperty" out key)
|
||||||
|
out)))))
|
||||||
|
|
||||||
|
;; ── Fetch ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
;; Fetch a URL, parse response according to format.
|
||||||
|
;; (hs-fetch url format) — format is "json" | "text" | "html"
|
||||||
(define
|
(define
|
||||||
hs-set-on!
|
hs-set-on!
|
||||||
(fn
|
(fn
|
||||||
(props target)
|
(props target)
|
||||||
(for-each (fn (k) (host-set! target k (get props k))) (keys props))))
|
(for-each (fn (k) (host-set! target k (get props k))) (keys props))))
|
||||||
|
|
||||||
;; ── Iteration ───────────────────────────────────────────────────
|
;; ── Type coercion ───────────────────────────────────────────────
|
||||||
|
|
||||||
;; Repeat a thunk N times.
|
;; Coerce a value to a type by name.
|
||||||
|
;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc.
|
||||||
(define hs-navigate! (fn (url) (perform (list (quote io-navigate) url))))
|
(define hs-navigate! (fn (url) (perform (list (quote io-navigate) url))))
|
||||||
|
|
||||||
;; Repeat forever (until break — relies on exception/continuation).
|
;; ── Object creation ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
;; Make a new object of a given type.
|
||||||
|
;; (hs-make type-name) — creates empty object/collection
|
||||||
(define
|
(define
|
||||||
hs-scroll!
|
hs-scroll!
|
||||||
(fn
|
(fn
|
||||||
@@ -317,10 +390,11 @@
|
|||||||
((= position "bottom") (dict :block "end"))
|
((= position "bottom") (dict :block "end"))
|
||||||
(true (dict :block "start")))))))
|
(true (dict :block "start")))))))
|
||||||
|
|
||||||
;; ── Fetch ───────────────────────────────────────────────────────
|
;; ── Behavior installation ───────────────────────────────────────
|
||||||
|
|
||||||
;; Fetch a URL, parse response according to format.
|
;; Install a behavior on an element.
|
||||||
;; (hs-fetch url format) — format is "json" | "text" | "html"
|
;; A behavior is a function that takes (me ...params) and sets up features.
|
||||||
|
;; (hs-install behavior-fn me ...args)
|
||||||
(define
|
(define
|
||||||
hs-halt!
|
hs-halt!
|
||||||
(fn
|
(fn
|
||||||
@@ -341,16 +415,16 @@
|
|||||||
(host-call ev "stopPropagation")))))
|
(host-call ev "stopPropagation")))))
|
||||||
(when (not (= mode "the-event")) (raise (list "hs-return" nil))))))
|
(when (not (= mode "the-event")) (raise (list "hs-return" nil))))))
|
||||||
|
|
||||||
;; ── Type coercion ───────────────────────────────────────────────
|
;; ── Measurement ─────────────────────────────────────────────────
|
||||||
|
|
||||||
;; Coerce a value to a type by name.
|
;; Measure an element's bounding rect, store as local variables.
|
||||||
;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc.
|
;; Returns a dict with x, y, width, height, top, left, right, bottom.
|
||||||
(define hs-select! (fn (target) (host-call target "select" (list))))
|
(define hs-select! (fn (target) (host-call target "select" (list))))
|
||||||
|
|
||||||
;; ── Object creation ─────────────────────────────────────────────
|
;; ── Transition ──────────────────────────────────────────────────
|
||||||
|
|
||||||
;; Make a new object of a given type.
|
;; Transition a CSS property to a value, optionally with duration.
|
||||||
;; (hs-make type-name) — creates empty object/collection
|
;; (hs-transition target prop value duration)
|
||||||
(define
|
(define
|
||||||
hs-reset!
|
hs-reset!
|
||||||
(fn
|
(fn
|
||||||
@@ -397,11 +471,6 @@
|
|||||||
(when default-val (dom-set-prop target "value" default-val)))))
|
(when default-val (dom-set-prop target "value" default-val)))))
|
||||||
(true nil)))))))
|
(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
|
(define
|
||||||
hs-next
|
hs-next
|
||||||
(fn
|
(fn
|
||||||
@@ -421,10 +490,6 @@
|
|||||||
(true (find-next (dom-next-sibling el))))))
|
(true (find-next (dom-next-sibling el))))))
|
||||||
(find-next sibling)))))
|
(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
|
(define
|
||||||
hs-previous
|
hs-previous
|
||||||
(fn
|
(fn
|
||||||
@@ -444,10 +509,6 @@
|
|||||||
(true (find-prev (dom-get-prop el "previousElementSibling"))))))
|
(true (find-prev (dom-get-prop el "previousElementSibling"))))))
|
||||||
(find-prev sibling)))))
|
(find-prev sibling)))))
|
||||||
|
|
||||||
;; ── Transition ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
;; Transition a CSS property to a value, optionally with duration.
|
|
||||||
;; (hs-transition target prop value duration)
|
|
||||||
(define
|
(define
|
||||||
hs-query-all
|
hs-query-all
|
||||||
(fn (sel) (host-call (dom-body) "querySelectorAll" sel)))
|
(fn (sel) (host-call (dom-body) "querySelectorAll" sel)))
|
||||||
@@ -493,6 +554,10 @@
|
|||||||
((all (dom-query-all scope sel)))
|
((all (dom-query-all scope sel)))
|
||||||
(if (> (len all) 0) (nth all (- (len all) 1)) nil))))
|
(if (> (len all) 0) (nth all (- (len all) 1)) nil))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(define
|
(define
|
||||||
hs-repeat-times
|
hs-repeat-times
|
||||||
(fn
|
(fn
|
||||||
@@ -526,7 +591,8 @@
|
|||||||
((= signal "hs-continue") (do-forever))
|
((= signal "hs-continue") (do-forever))
|
||||||
(true (do-forever))))))
|
(true (do-forever))))))
|
||||||
(do-forever)))
|
(do-forever)))
|
||||||
|
;; ── Sandbox/test runtime additions ──────────────────────────────
|
||||||
|
;; Property access — dot notation and .length
|
||||||
(define
|
(define
|
||||||
hs-repeat-while
|
hs-repeat-while
|
||||||
(fn
|
(fn
|
||||||
@@ -539,11 +605,7 @@
|
|||||||
((= signal "hs-break") nil)
|
((= signal "hs-break") nil)
|
||||||
((= signal "hs-continue") (hs-repeat-while cond-fn thunk))
|
((= signal "hs-continue") (hs-repeat-while cond-fn thunk))
|
||||||
(true (hs-repeat-while cond-fn thunk)))))))
|
(true (hs-repeat-while cond-fn thunk)))))))
|
||||||
|
;; DOM query stub — sandbox returns empty list
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(define
|
(define
|
||||||
hs-repeat-until
|
hs-repeat-until
|
||||||
(fn
|
(fn
|
||||||
@@ -555,7 +617,7 @@
|
|||||||
((= signal "hs-continue")
|
((= signal "hs-continue")
|
||||||
(if (cond-fn) nil (hs-repeat-until cond-fn thunk)))
|
(if (cond-fn) nil (hs-repeat-until cond-fn thunk)))
|
||||||
(true (if (cond-fn) nil (hs-repeat-until cond-fn thunk)))))))
|
(true (if (cond-fn) nil (hs-repeat-until cond-fn thunk)))))))
|
||||||
|
;; Method dispatch — obj.method(args)
|
||||||
(define
|
(define
|
||||||
hs-for-each
|
hs-for-each
|
||||||
(fn
|
(fn
|
||||||
@@ -575,8 +637,9 @@
|
|||||||
((= signal "hs-continue") (do-loop (rest remaining)))
|
((= signal "hs-continue") (do-loop (rest remaining)))
|
||||||
(true (do-loop (rest remaining))))))))
|
(true (do-loop (rest remaining))))))))
|
||||||
(do-loop items))))
|
(do-loop items))))
|
||||||
;; ── Sandbox/test runtime additions ──────────────────────────────
|
|
||||||
;; Property access — dot notation and .length
|
;; ── 0.9.90 features ─────────────────────────────────────────────
|
||||||
|
;; beep! — debug logging, returns value unchanged
|
||||||
(begin
|
(begin
|
||||||
(define
|
(define
|
||||||
hs-append
|
hs-append
|
||||||
@@ -600,13 +663,15 @@
|
|||||||
((hs-element? target)
|
((hs-element? target)
|
||||||
(dom-insert-adjacent-html target "beforeend" (str value)))
|
(dom-insert-adjacent-html target "beforeend" (str value)))
|
||||||
(true nil)))))
|
(true nil)))))
|
||||||
;; DOM query stub — sandbox returns empty list
|
;; Property-based is — check obj.key truthiness
|
||||||
(define
|
(define
|
||||||
hs-fetch
|
hs-fetch
|
||||||
(fn
|
(fn
|
||||||
(url format)
|
(url format)
|
||||||
(perform (list "io-fetch" url (if format format "text")))))
|
(let
|
||||||
;; Method dispatch — obj.method(args)
|
((fmt (cond ((nil? format) "text") ((or (= format "JSON") (= format "json") (= format "Object") (= format "object")) "json") ((or (= format "HTML") (= format "html")) "html") ((or (= format "Response") (= format "response")) "response") ((or (= format "Text") (= format "text")) "text") (true format))))
|
||||||
|
(perform (list "io-fetch" url fmt)))))
|
||||||
|
;; Array slicing (inclusive both ends)
|
||||||
(define
|
(define
|
||||||
hs-coerce
|
hs-coerce
|
||||||
(fn
|
(fn
|
||||||
@@ -649,11 +714,7 @@
|
|||||||
(str (/ (floor (+ (* num factor) 0.5)) factor))))))
|
(str (/ (floor (+ (* num factor) 0.5)) factor))))))
|
||||||
((= type-name "Selector") (str value))
|
((= type-name "Selector") (str value))
|
||||||
((= type-name "Fragment") value)
|
((= type-name "Fragment") value)
|
||||||
((= type-name "Values")
|
((= type-name "Values") (hs-as-values value))
|
||||||
(if
|
|
||||||
(dict? value)
|
|
||||||
(map (fn (k) (get value k)) (keys value))
|
|
||||||
value))
|
|
||||||
((= type-name "Keys") (if (dict? value) (sort (keys value)) value))
|
((= type-name "Keys") (if (dict? value) (sort (keys value)) value))
|
||||||
((= type-name "Entries")
|
((= type-name "Entries")
|
||||||
(if
|
(if
|
||||||
@@ -697,9 +758,126 @@
|
|||||||
(map (fn (k) (list k (get value k))) (keys value))
|
(map (fn (k) (list k (get value k))) (keys value))
|
||||||
value))
|
value))
|
||||||
(true value))))
|
(true value))))
|
||||||
|
;; Collection: sorted by
|
||||||
|
(define
|
||||||
|
hs-gather-form-nodes
|
||||||
|
(fn
|
||||||
|
(root)
|
||||||
|
(let
|
||||||
|
((acc (list)))
|
||||||
|
(define
|
||||||
|
walk
|
||||||
|
(fn
|
||||||
|
(node)
|
||||||
|
(let
|
||||||
|
((tag (host-get node "tagName")))
|
||||||
|
(cond
|
||||||
|
((or (= tag "INPUT") (= tag "SELECT") (= tag "TEXTAREA"))
|
||||||
|
(set! acc (append acc (list node))))
|
||||||
|
(true
|
||||||
|
(let
|
||||||
|
((kids (host-get node "children")))
|
||||||
|
(when
|
||||||
|
(and (not (nil? kids)) (list? kids))
|
||||||
|
(let
|
||||||
|
((n (len kids)))
|
||||||
|
(define
|
||||||
|
each
|
||||||
|
(fn
|
||||||
|
(i)
|
||||||
|
(when
|
||||||
|
(< i n)
|
||||||
|
(walk (nth kids i))
|
||||||
|
(each (+ i 1)))))
|
||||||
|
(each 0)))))))))
|
||||||
|
(walk root)
|
||||||
|
acc)))
|
||||||
|
;; Collection: sorted by descending
|
||||||
|
(define
|
||||||
|
hs-values-from-nodes
|
||||||
|
(fn (nodes) (reduce hs-values-absorb (dict) nodes)))
|
||||||
|
;; Collection: split by
|
||||||
|
(define
|
||||||
|
hs-value-of-node
|
||||||
|
(fn
|
||||||
|
(node)
|
||||||
|
(let
|
||||||
|
((tag (host-get node "tagName")) (typ (host-get node "type")))
|
||||||
|
(cond
|
||||||
|
((= tag "SELECT")
|
||||||
|
(if
|
||||||
|
(host-get node "multiple")
|
||||||
|
(hs-select-multi-values node)
|
||||||
|
(host-get node "value")))
|
||||||
|
((or (= typ "checkbox") (= typ "radio"))
|
||||||
|
(if (host-get node "checked") (host-get node "value") nil))
|
||||||
|
(true (host-get node "value"))))))
|
||||||
|
;; Collection: joined by
|
||||||
|
(define
|
||||||
|
hs-select-multi-values
|
||||||
|
(fn
|
||||||
|
(node)
|
||||||
|
(let
|
||||||
|
((options (host-get node "options")) (acc (list)))
|
||||||
|
(if
|
||||||
|
(or (nil? options) (not (list? options)))
|
||||||
|
acc
|
||||||
|
(let
|
||||||
|
((n (len options)))
|
||||||
|
(define
|
||||||
|
each
|
||||||
|
(fn
|
||||||
|
(i)
|
||||||
|
(when
|
||||||
|
(< i n)
|
||||||
|
(let
|
||||||
|
((opt (nth options i)))
|
||||||
|
(when
|
||||||
|
(host-get opt "selected")
|
||||||
|
(set! acc (append acc (list (host-get opt "value"))))))
|
||||||
|
(each (+ i 1)))))
|
||||||
|
(each 0)
|
||||||
|
acc)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hs-values-absorb
|
||||||
|
(fn
|
||||||
|
(acc node)
|
||||||
|
(let
|
||||||
|
((name (host-get node "name")))
|
||||||
|
(if
|
||||||
|
(or (nil? name) (= name ""))
|
||||||
|
acc
|
||||||
|
(let
|
||||||
|
((v (hs-value-of-node node)))
|
||||||
|
(cond
|
||||||
|
((nil? v) acc)
|
||||||
|
((has-key? acc name)
|
||||||
|
(let
|
||||||
|
((existing (get acc name)))
|
||||||
|
(do
|
||||||
|
(if
|
||||||
|
(list? existing)
|
||||||
|
(dict-set! acc name (append existing (list v)))
|
||||||
|
(dict-set! acc name (list existing v)))
|
||||||
|
acc)))
|
||||||
|
(true (do (dict-set! acc name v) acc))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hs-as-values
|
||||||
|
(fn
|
||||||
|
(value)
|
||||||
|
(cond
|
||||||
|
((nil? value) (dict))
|
||||||
|
((list? value) (hs-values-from-nodes value))
|
||||||
|
(true
|
||||||
|
(let
|
||||||
|
((tag (host-get value "tagName")))
|
||||||
|
(cond
|
||||||
|
((or (= tag "INPUT") (= tag "SELECT") (= tag "TEXTAREA"))
|
||||||
|
(hs-values-from-nodes (list value)))
|
||||||
|
(true (hs-values-from-nodes (hs-gather-form-nodes value)))))))))
|
||||||
|
|
||||||
;; ── 0.9.90 features ─────────────────────────────────────────────
|
|
||||||
;; beep! — debug logging, returns value unchanged
|
|
||||||
(define
|
(define
|
||||||
hs-default?
|
hs-default?
|
||||||
(fn
|
(fn
|
||||||
@@ -708,13 +886,13 @@
|
|||||||
((nil? v) true)
|
((nil? v) true)
|
||||||
((and (string? v) (= v "")) true)
|
((and (string? v) (= v "")) true)
|
||||||
(true false))))
|
(true false))))
|
||||||
;; Property-based is — check obj.key truthiness
|
|
||||||
(define
|
(define
|
||||||
hs-array-set!
|
hs-array-set!
|
||||||
(fn
|
(fn
|
||||||
(arr i v)
|
(arr i v)
|
||||||
(if (list? arr) (do (set-nth! arr i v) v) (host-set! arr i v))))
|
(if (list? arr) (do (set-nth! arr i v) v) (host-set! arr i v))))
|
||||||
;; Array slicing (inclusive both ends)
|
|
||||||
(define
|
(define
|
||||||
hs-add
|
hs-add
|
||||||
(fn
|
(fn
|
||||||
@@ -724,24 +902,117 @@
|
|||||||
((list? b) (cons a b))
|
((list? b) (cons a b))
|
||||||
((or (string? a) (string? b)) (str a b))
|
((or (string? a) (string? b)) (str a b))
|
||||||
(true (+ a b)))))
|
(true (+ a b)))))
|
||||||
;; Collection: sorted by
|
|
||||||
(define
|
(begin
|
||||||
hs-make
|
(define
|
||||||
(fn
|
hs-make
|
||||||
(type-name)
|
(fn
|
||||||
(cond
|
(type-name &rest args)
|
||||||
((= type-name "Object") (dict))
|
(if
|
||||||
((= type-name "Array") (list))
|
(hs-make-element? type-name)
|
||||||
((= type-name "Set") (list))
|
(hs-make-element type-name)
|
||||||
((= type-name "Map") (dict))
|
(let
|
||||||
(true (dict)))))
|
((ctor (host-global type-name)))
|
||||||
;; Collection: sorted by descending
|
(if
|
||||||
|
(nil? ctor)
|
||||||
|
(cond
|
||||||
|
((= type-name "Object") (dict))
|
||||||
|
((= type-name "Array") (list))
|
||||||
|
((= type-name "Set") (list))
|
||||||
|
((= type-name "Map") (dict))
|
||||||
|
(true (dict)))
|
||||||
|
(apply host-new (cons type-name args)))))))
|
||||||
|
(define
|
||||||
|
hs-make-element?
|
||||||
|
(fn
|
||||||
|
(s)
|
||||||
|
(and
|
||||||
|
(string? s)
|
||||||
|
(> (len s) 0)
|
||||||
|
(let
|
||||||
|
((c (substring s 0 1)))
|
||||||
|
(or
|
||||||
|
(= c ".")
|
||||||
|
(= c "#")
|
||||||
|
(contains? s ".")
|
||||||
|
(contains? s "#")
|
||||||
|
(and (hs-lower-letter? c) (not (any-upper? s))))))))
|
||||||
|
(define hs-lower-letter? (fn (c) (and (>= c "a") (<= c "z"))))
|
||||||
|
(define
|
||||||
|
any-upper?
|
||||||
|
(fn
|
||||||
|
(s)
|
||||||
|
(let
|
||||||
|
((n (len s)))
|
||||||
|
(define
|
||||||
|
scan
|
||||||
|
(fn
|
||||||
|
(i)
|
||||||
|
(cond
|
||||||
|
((>= i n) false)
|
||||||
|
((and (>= (substring s i (+ i 1)) "A") (<= (substring s i (+ i 1)) "Z"))
|
||||||
|
true)
|
||||||
|
(true (scan (+ i 1))))))
|
||||||
|
(scan 0))))
|
||||||
|
(define
|
||||||
|
hs-make-element
|
||||||
|
(fn
|
||||||
|
(sel)
|
||||||
|
(let
|
||||||
|
((parsed (hs-parse-element-selector sel)))
|
||||||
|
(let
|
||||||
|
((tag (get parsed "tag"))
|
||||||
|
(id (get parsed "id"))
|
||||||
|
(classes (get parsed "classes")))
|
||||||
|
(let
|
||||||
|
((el (dom-create-element (if (= tag "") "div" tag))))
|
||||||
|
(when (and id (not (= id ""))) (dom-set-attr el "id" id))
|
||||||
|
(for-each (fn (c) (dom-add-class el c)) classes)
|
||||||
|
el)))))
|
||||||
|
(define
|
||||||
|
hs-parse-element-selector
|
||||||
|
(fn
|
||||||
|
(sel)
|
||||||
|
(let
|
||||||
|
((n (len sel))
|
||||||
|
(tag "")
|
||||||
|
(id "")
|
||||||
|
(classes (list))
|
||||||
|
(cur "")
|
||||||
|
(mode "tag"))
|
||||||
|
(define
|
||||||
|
flush!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(cond
|
||||||
|
((= mode "tag") (set! tag cur))
|
||||||
|
((= mode "id") (set! id cur))
|
||||||
|
((= mode "class") (set! classes (append classes (list cur)))))
|
||||||
|
(set! cur "")))
|
||||||
|
(define
|
||||||
|
walk
|
||||||
|
(fn
|
||||||
|
(i)
|
||||||
|
(when
|
||||||
|
(< i n)
|
||||||
|
(let
|
||||||
|
((ch (substring sel i (+ i 1))))
|
||||||
|
(cond
|
||||||
|
((= ch ".")
|
||||||
|
(do (flush!) (set! mode "class") (walk (+ i 1))))
|
||||||
|
((= ch "#")
|
||||||
|
(do (flush!) (set! mode "id") (walk (+ i 1))))
|
||||||
|
(true (do (set! cur (str cur ch)) (walk (+ i 1)))))))))
|
||||||
|
(walk 0)
|
||||||
|
(flush!)
|
||||||
|
{:tag tag :classes classes :id id}))))
|
||||||
|
|
||||||
(define hs-install (fn (behavior-fn) (behavior-fn me)))
|
(define hs-install (fn (behavior-fn) (behavior-fn me)))
|
||||||
;; Collection: split by
|
|
||||||
(define
|
(define
|
||||||
hs-measure
|
hs-measure
|
||||||
(fn (target) (perform (list (quote io-measure) target))))
|
(fn (target) (perform (list (quote io-measure) target))))
|
||||||
;; Collection: joined by
|
|
||||||
(define
|
(define
|
||||||
hs-transition
|
hs-transition
|
||||||
(fn
|
(fn
|
||||||
|
|||||||
@@ -435,7 +435,7 @@
|
|||||||
(el key val)
|
(el key val)
|
||||||
(when
|
(when
|
||||||
(not (host-get el "__sx_data"))
|
(not (host-get el "__sx_data"))
|
||||||
(host-set! el "__sx_data" (dict)))
|
(host-set! el "__sx_data" (host-new "Object")))
|
||||||
(host-set! (host-get el "__sx_data") key val)))
|
(host-set! (host-get el "__sx_data") key val)))
|
||||||
(define
|
(define
|
||||||
dom-append-to-head
|
dom-append-to-head
|
||||||
|
|||||||
@@ -353,16 +353,41 @@
|
|||||||
emit-make
|
emit-make
|
||||||
(fn
|
(fn
|
||||||
(ast)
|
(ast)
|
||||||
(if
|
(let
|
||||||
(= (len ast) 3)
|
((type-name (nth ast 1))
|
||||||
(list
|
(called (if (>= (len ast) 3) (nth ast 2) nil))
|
||||||
(quote let)
|
(args (if (>= (len ast) 4) (nth ast 3) nil))
|
||||||
(list
|
(kind (if (>= (len ast) 5) (nth ast 4) (quote auto))))
|
||||||
(list
|
(let
|
||||||
(make-symbol (nth ast 2))
|
((make-call (cond ((nil? args) (list (quote hs-make) type-name)) (true (cons (quote hs-make) (cons type-name (map hs-to-sx args)))))))
|
||||||
(list (quote hs-make) (nth ast 1))))
|
(cond
|
||||||
(make-symbol (nth ast 2)))
|
((and called (> (len called) 1) (= (substring called 0 1) "$"))
|
||||||
(list (quote hs-make) (nth ast 1)))))
|
(list
|
||||||
|
(quote let)
|
||||||
|
(list (list (quote __hs-mk) make-call))
|
||||||
|
(list
|
||||||
|
(quote do)
|
||||||
|
(list
|
||||||
|
(quote host-set!)
|
||||||
|
(list (quote host-global) "window")
|
||||||
|
called
|
||||||
|
(quote __hs-mk))
|
||||||
|
(list (quote set!) (quote it) (quote __hs-mk))
|
||||||
|
(quote __hs-mk))))
|
||||||
|
(called
|
||||||
|
(list
|
||||||
|
(quote do)
|
||||||
|
(list (quote set!) (make-symbol called) make-call)
|
||||||
|
(list (quote set!) (quote it) (make-symbol called))
|
||||||
|
(make-symbol called)))
|
||||||
|
(true
|
||||||
|
(list
|
||||||
|
(quote let)
|
||||||
|
(list (list (quote __hs-mk) make-call))
|
||||||
|
(list
|
||||||
|
(quote do)
|
||||||
|
(list (quote set!) (quote it) (quote __hs-mk))
|
||||||
|
(quote __hs-mk)))))))))
|
||||||
(define
|
(define
|
||||||
emit-inc
|
emit-inc
|
||||||
(fn
|
(fn
|
||||||
@@ -1182,13 +1207,38 @@
|
|||||||
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt))
|
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt))
|
||||||
(nth ast 1)))))
|
(nth ast 1)))))
|
||||||
((= head (quote remove-element))
|
((= head (quote remove-element))
|
||||||
(list (quote dom-remove) (hs-to-sx (nth ast 1))))
|
(let
|
||||||
|
((tgt (nth ast 1)))
|
||||||
|
(cond
|
||||||
|
((and (list? tgt) (= (first tgt) (quote array-index)))
|
||||||
|
(let
|
||||||
|
((coll (nth tgt 1)) (idx (hs-to-sx (nth tgt 2))))
|
||||||
|
(emit-set
|
||||||
|
coll
|
||||||
|
(list (quote hs-splice-at!) (hs-to-sx coll) idx))))
|
||||||
|
((and (list? tgt) (= (first tgt) dot-sym))
|
||||||
|
(let
|
||||||
|
((obj (nth tgt 1)) (prop (nth tgt 2)))
|
||||||
|
(emit-set
|
||||||
|
obj
|
||||||
|
(list (quote hs-dict-without) (hs-to-sx obj) prop))))
|
||||||
|
((and (list? tgt) (= (first tgt) (quote of)))
|
||||||
|
(let
|
||||||
|
((prop-ast (nth tgt 1)) (obj-ast (nth tgt 2)))
|
||||||
|
(let
|
||||||
|
((prop (cond ((string? prop-ast) prop-ast) ((and (list? prop-ast) (= (first prop-ast) (quote ref))) (nth prop-ast 1)) (true (hs-to-sx prop-ast)))))
|
||||||
|
(emit-set
|
||||||
|
obj-ast
|
||||||
|
(list
|
||||||
|
(quote hs-dict-without)
|
||||||
|
(hs-to-sx obj-ast)
|
||||||
|
prop)))))
|
||||||
|
(true (list (quote dom-remove) (hs-to-sx tgt))))))
|
||||||
((= head (quote add-value))
|
((= head (quote add-value))
|
||||||
(let
|
(let
|
||||||
((val (hs-to-sx (nth ast 1))) (tgt (nth ast 2)))
|
((val (hs-to-sx (nth ast 1))) (tgt (nth ast 2)))
|
||||||
(list
|
(emit-set
|
||||||
(quote set!)
|
tgt
|
||||||
(hs-to-sx tgt)
|
|
||||||
(list (quote hs-add-to!) val (hs-to-sx tgt)))))
|
(list (quote hs-add-to!) val (hs-to-sx tgt)))))
|
||||||
((= head (quote add-attr))
|
((= head (quote add-attr))
|
||||||
(let
|
(let
|
||||||
@@ -1201,9 +1251,8 @@
|
|||||||
((= head (quote remove-value))
|
((= head (quote remove-value))
|
||||||
(let
|
(let
|
||||||
((val (hs-to-sx (nth ast 1))) (tgt (nth ast 2)))
|
((val (hs-to-sx (nth ast 1))) (tgt (nth ast 2)))
|
||||||
(list
|
(emit-set
|
||||||
(quote set!)
|
tgt
|
||||||
(hs-to-sx tgt)
|
|
||||||
(list (quote hs-remove-from!) val (hs-to-sx tgt)))))
|
(list (quote hs-remove-from!) val (hs-to-sx tgt)))))
|
||||||
((= head (quote empty-target))
|
((= head (quote empty-target))
|
||||||
(let
|
(let
|
||||||
@@ -1348,11 +1397,16 @@
|
|||||||
((= head (quote set!))
|
((= head (quote set!))
|
||||||
(emit-set (nth ast 1) (hs-to-sx (nth ast 2))))
|
(emit-set (nth ast 1) (hs-to-sx (nth ast 2))))
|
||||||
((= head (quote put!))
|
((= head (quote put!))
|
||||||
(list
|
(let
|
||||||
(quote hs-put!)
|
((val (hs-to-sx (nth ast 1)))
|
||||||
(hs-to-sx (nth ast 1))
|
(pos (nth ast 2))
|
||||||
(nth ast 2)
|
(raw-tgt (nth ast 3)))
|
||||||
(hs-to-sx (nth ast 3))))
|
(cond
|
||||||
|
((and (or (= pos "end") (= pos "start")) (list? raw-tgt) (or (= (first raw-tgt) (quote local)) (= (first raw-tgt) (quote ref))))
|
||||||
|
(emit-set
|
||||||
|
raw-tgt
|
||||||
|
(list (quote hs-put-at!) val pos (hs-to-sx raw-tgt))))
|
||||||
|
(true (list (quote hs-put!) val pos (hs-to-sx raw-tgt))))))
|
||||||
((= head (quote if))
|
((= head (quote if))
|
||||||
(if
|
(if
|
||||||
(> (len ast) 3)
|
(> (len ast) 3)
|
||||||
@@ -1387,10 +1441,24 @@
|
|||||||
(reduce
|
(reduce
|
||||||
(fn
|
(fn
|
||||||
(body cmd)
|
(body cmd)
|
||||||
(list
|
(if
|
||||||
(quote let)
|
(and
|
||||||
(list (list (quote it) cmd))
|
(list? cmd)
|
||||||
body))
|
(= (first cmd) (quote hs-fetch)))
|
||||||
|
(list
|
||||||
|
(quote let)
|
||||||
|
(list (list (quote it) cmd))
|
||||||
|
(list
|
||||||
|
(quote begin)
|
||||||
|
(list
|
||||||
|
(quote set!)
|
||||||
|
(quote the-result)
|
||||||
|
(quote it))
|
||||||
|
body))
|
||||||
|
(list
|
||||||
|
(quote let)
|
||||||
|
(list (list (quote it) cmd))
|
||||||
|
body)))
|
||||||
(nth compiled (- (len compiled) 1))
|
(nth compiled (- (len compiled) 1))
|
||||||
(rest (reverse compiled)))
|
(rest (reverse compiled)))
|
||||||
(cons (quote do) compiled))))
|
(cons (quote do) compiled))))
|
||||||
@@ -1512,10 +1580,13 @@
|
|||||||
(let
|
(let
|
||||||
((val (hs-to-sx (nth ast 1))))
|
((val (hs-to-sx (nth ast 1))))
|
||||||
(list
|
(list
|
||||||
(quote begin)
|
(quote let)
|
||||||
(list (quote set!) (quote the-result) val)
|
(list (list (quote __hs-g) val))
|
||||||
(list (quote set!) (quote it) val)
|
(list
|
||||||
val)))
|
(quote begin)
|
||||||
|
(list (quote set!) (quote the-result) (quote __hs-g))
|
||||||
|
(list (quote set!) (quote it) (quote __hs-g))
|
||||||
|
(quote __hs-g)))))
|
||||||
((= head (quote append!))
|
((= head (quote append!))
|
||||||
(let
|
(let
|
||||||
((tgt (hs-to-sx (nth ast 2)))
|
((tgt (hs-to-sx (nth ast 2)))
|
||||||
|
|||||||
@@ -1975,14 +1975,29 @@
|
|||||||
()
|
()
|
||||||
(if (= (tp-val) "a") (adv!) nil)
|
(if (= (tp-val) "a") (adv!) nil)
|
||||||
(let
|
(let
|
||||||
((type-name (tp-val)))
|
((kind (if (= (tp-type) "selector") (quote element) (quote object)))
|
||||||
|
(type-name (tp-val)))
|
||||||
(adv!)
|
(adv!)
|
||||||
(let
|
(let
|
||||||
((called (if (match-kw "called") (let ((n (tp-val))) (adv!) n) nil)))
|
((called nil) (args nil))
|
||||||
(if
|
(define
|
||||||
called
|
parse-from-args
|
||||||
(list (quote make) type-name called)
|
(fn
|
||||||
(list (quote make) type-name))))))
|
()
|
||||||
|
(set! args (append args (list (parse-expr))))
|
||||||
|
(when (= (tp-type) "comma") (adv!) (parse-from-args))))
|
||||||
|
(define
|
||||||
|
parse-clauses
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(cond
|
||||||
|
((match-kw "from")
|
||||||
|
(do (parse-from-args) (parse-clauses)))
|
||||||
|
((match-kw "called")
|
||||||
|
(do (set! called (tp-val)) (adv!) (parse-clauses)))
|
||||||
|
(true nil))))
|
||||||
|
(parse-clauses)
|
||||||
|
(list (quote make) type-name called args kind)))))
|
||||||
(define
|
(define
|
||||||
parse-install-cmd
|
parse-install-cmd
|
||||||
(fn
|
(fn
|
||||||
|
|||||||
@@ -293,18 +293,91 @@
|
|||||||
(filter (fn (x) (not (= x value))) target)
|
(filter (fn (x) (not (= x value))) target)
|
||||||
(host-call target "splice" (host-call target "indexOf" value) 1))))
|
(host-call target "splice" (host-call target "indexOf" value) 1))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hs-splice-at!
|
||||||
|
(fn
|
||||||
|
(target idx)
|
||||||
|
(if
|
||||||
|
(list? target)
|
||||||
|
(let
|
||||||
|
((n (len target)))
|
||||||
|
(let
|
||||||
|
((i (if (< idx 0) (+ n idx) idx)))
|
||||||
|
(cond
|
||||||
|
((or (< i 0) (>= i n)) target)
|
||||||
|
(true (concat (slice target 0 i) (slice target (+ i 1) n))))))
|
||||||
|
(do
|
||||||
|
(when
|
||||||
|
target
|
||||||
|
(let
|
||||||
|
((n (host-get target "length")))
|
||||||
|
(let
|
||||||
|
((i (if (< idx 0) (+ (if (nil? n) 0 n) idx) idx)))
|
||||||
|
(host-call target "splice" i 1))))
|
||||||
|
target))))
|
||||||
|
|
||||||
|
;; ── Iteration ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
;; Repeat a thunk N times.
|
||||||
|
(define
|
||||||
|
hs-put-at!
|
||||||
|
(fn
|
||||||
|
(value pos target)
|
||||||
|
(cond
|
||||||
|
((nil? target) (list value))
|
||||||
|
((list? target)
|
||||||
|
(if
|
||||||
|
(= pos "start")
|
||||||
|
(cons value target)
|
||||||
|
(append target (list value))))
|
||||||
|
(true
|
||||||
|
(do
|
||||||
|
(cond
|
||||||
|
((= pos "end") (host-call target "push" value))
|
||||||
|
((= pos "start") (host-call target "unshift" value)))
|
||||||
|
target)))))
|
||||||
|
|
||||||
|
;; Repeat forever (until break — relies on exception/continuation).
|
||||||
|
(define
|
||||||
|
hs-dict-without
|
||||||
|
(fn
|
||||||
|
(obj key)
|
||||||
|
(cond
|
||||||
|
((nil? obj) (dict))
|
||||||
|
((dict? obj)
|
||||||
|
(let
|
||||||
|
((out (dict)))
|
||||||
|
(for-each
|
||||||
|
(fn (k) (when (not (= k key)) (dict-set! out k (get obj k))))
|
||||||
|
(keys obj))
|
||||||
|
out))
|
||||||
|
(true
|
||||||
|
(let
|
||||||
|
((out (dict)))
|
||||||
|
(host-call (host-global "Object") "assign" out obj)
|
||||||
|
(host-call (host-global "Reflect") "deleteProperty" out key)
|
||||||
|
out)))))
|
||||||
|
|
||||||
|
;; ── Fetch ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
;; Fetch a URL, parse response according to format.
|
||||||
|
;; (hs-fetch url format) — format is "json" | "text" | "html"
|
||||||
(define
|
(define
|
||||||
hs-set-on!
|
hs-set-on!
|
||||||
(fn
|
(fn
|
||||||
(props target)
|
(props target)
|
||||||
(for-each (fn (k) (host-set! target k (get props k))) (keys props))))
|
(for-each (fn (k) (host-set! target k (get props k))) (keys props))))
|
||||||
|
|
||||||
;; ── Iteration ───────────────────────────────────────────────────
|
;; ── Type coercion ───────────────────────────────────────────────
|
||||||
|
|
||||||
;; Repeat a thunk N times.
|
;; Coerce a value to a type by name.
|
||||||
|
;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc.
|
||||||
(define hs-navigate! (fn (url) (perform (list (quote io-navigate) url))))
|
(define hs-navigate! (fn (url) (perform (list (quote io-navigate) url))))
|
||||||
|
|
||||||
;; Repeat forever (until break — relies on exception/continuation).
|
;; ── Object creation ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
;; Make a new object of a given type.
|
||||||
|
;; (hs-make type-name) — creates empty object/collection
|
||||||
(define
|
(define
|
||||||
hs-scroll!
|
hs-scroll!
|
||||||
(fn
|
(fn
|
||||||
@@ -317,10 +390,11 @@
|
|||||||
((= position "bottom") (dict :block "end"))
|
((= position "bottom") (dict :block "end"))
|
||||||
(true (dict :block "start")))))))
|
(true (dict :block "start")))))))
|
||||||
|
|
||||||
;; ── Fetch ───────────────────────────────────────────────────────
|
;; ── Behavior installation ───────────────────────────────────────
|
||||||
|
|
||||||
;; Fetch a URL, parse response according to format.
|
;; Install a behavior on an element.
|
||||||
;; (hs-fetch url format) — format is "json" | "text" | "html"
|
;; A behavior is a function that takes (me ...params) and sets up features.
|
||||||
|
;; (hs-install behavior-fn me ...args)
|
||||||
(define
|
(define
|
||||||
hs-halt!
|
hs-halt!
|
||||||
(fn
|
(fn
|
||||||
@@ -341,16 +415,16 @@
|
|||||||
(host-call ev "stopPropagation")))))
|
(host-call ev "stopPropagation")))))
|
||||||
(when (not (= mode "the-event")) (raise (list "hs-return" nil))))))
|
(when (not (= mode "the-event")) (raise (list "hs-return" nil))))))
|
||||||
|
|
||||||
;; ── Type coercion ───────────────────────────────────────────────
|
;; ── Measurement ─────────────────────────────────────────────────
|
||||||
|
|
||||||
;; Coerce a value to a type by name.
|
;; Measure an element's bounding rect, store as local variables.
|
||||||
;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc.
|
;; Returns a dict with x, y, width, height, top, left, right, bottom.
|
||||||
(define hs-select! (fn (target) (host-call target "select" (list))))
|
(define hs-select! (fn (target) (host-call target "select" (list))))
|
||||||
|
|
||||||
;; ── Object creation ─────────────────────────────────────────────
|
;; ── Transition ──────────────────────────────────────────────────
|
||||||
|
|
||||||
;; Make a new object of a given type.
|
;; Transition a CSS property to a value, optionally with duration.
|
||||||
;; (hs-make type-name) — creates empty object/collection
|
;; (hs-transition target prop value duration)
|
||||||
(define
|
(define
|
||||||
hs-reset!
|
hs-reset!
|
||||||
(fn
|
(fn
|
||||||
@@ -397,11 +471,6 @@
|
|||||||
(when default-val (dom-set-prop target "value" default-val)))))
|
(when default-val (dom-set-prop target "value" default-val)))))
|
||||||
(true nil)))))))
|
(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
|
(define
|
||||||
hs-next
|
hs-next
|
||||||
(fn
|
(fn
|
||||||
@@ -421,10 +490,6 @@
|
|||||||
(true (find-next (dom-next-sibling el))))))
|
(true (find-next (dom-next-sibling el))))))
|
||||||
(find-next sibling)))))
|
(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
|
(define
|
||||||
hs-previous
|
hs-previous
|
||||||
(fn
|
(fn
|
||||||
@@ -444,10 +509,6 @@
|
|||||||
(true (find-prev (dom-get-prop el "previousElementSibling"))))))
|
(true (find-prev (dom-get-prop el "previousElementSibling"))))))
|
||||||
(find-prev sibling)))))
|
(find-prev sibling)))))
|
||||||
|
|
||||||
;; ── Transition ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
;; Transition a CSS property to a value, optionally with duration.
|
|
||||||
;; (hs-transition target prop value duration)
|
|
||||||
(define
|
(define
|
||||||
hs-query-all
|
hs-query-all
|
||||||
(fn (sel) (host-call (dom-body) "querySelectorAll" sel)))
|
(fn (sel) (host-call (dom-body) "querySelectorAll" sel)))
|
||||||
@@ -493,6 +554,10 @@
|
|||||||
((all (dom-query-all scope sel)))
|
((all (dom-query-all scope sel)))
|
||||||
(if (> (len all) 0) (nth all (- (len all) 1)) nil))))
|
(if (> (len all) 0) (nth all (- (len all) 1)) nil))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(define
|
(define
|
||||||
hs-repeat-times
|
hs-repeat-times
|
||||||
(fn
|
(fn
|
||||||
@@ -526,7 +591,8 @@
|
|||||||
((= signal "hs-continue") (do-forever))
|
((= signal "hs-continue") (do-forever))
|
||||||
(true (do-forever))))))
|
(true (do-forever))))))
|
||||||
(do-forever)))
|
(do-forever)))
|
||||||
|
;; ── Sandbox/test runtime additions ──────────────────────────────
|
||||||
|
;; Property access — dot notation and .length
|
||||||
(define
|
(define
|
||||||
hs-repeat-while
|
hs-repeat-while
|
||||||
(fn
|
(fn
|
||||||
@@ -539,11 +605,7 @@
|
|||||||
((= signal "hs-break") nil)
|
((= signal "hs-break") nil)
|
||||||
((= signal "hs-continue") (hs-repeat-while cond-fn thunk))
|
((= signal "hs-continue") (hs-repeat-while cond-fn thunk))
|
||||||
(true (hs-repeat-while cond-fn thunk)))))))
|
(true (hs-repeat-while cond-fn thunk)))))))
|
||||||
|
;; DOM query stub — sandbox returns empty list
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(define
|
(define
|
||||||
hs-repeat-until
|
hs-repeat-until
|
||||||
(fn
|
(fn
|
||||||
@@ -555,7 +617,7 @@
|
|||||||
((= signal "hs-continue")
|
((= signal "hs-continue")
|
||||||
(if (cond-fn) nil (hs-repeat-until cond-fn thunk)))
|
(if (cond-fn) nil (hs-repeat-until cond-fn thunk)))
|
||||||
(true (if (cond-fn) nil (hs-repeat-until cond-fn thunk)))))))
|
(true (if (cond-fn) nil (hs-repeat-until cond-fn thunk)))))))
|
||||||
|
;; Method dispatch — obj.method(args)
|
||||||
(define
|
(define
|
||||||
hs-for-each
|
hs-for-each
|
||||||
(fn
|
(fn
|
||||||
@@ -575,8 +637,9 @@
|
|||||||
((= signal "hs-continue") (do-loop (rest remaining)))
|
((= signal "hs-continue") (do-loop (rest remaining)))
|
||||||
(true (do-loop (rest remaining))))))))
|
(true (do-loop (rest remaining))))))))
|
||||||
(do-loop items))))
|
(do-loop items))))
|
||||||
;; ── Sandbox/test runtime additions ──────────────────────────────
|
|
||||||
;; Property access — dot notation and .length
|
;; ── 0.9.90 features ─────────────────────────────────────────────
|
||||||
|
;; beep! — debug logging, returns value unchanged
|
||||||
(begin
|
(begin
|
||||||
(define
|
(define
|
||||||
hs-append
|
hs-append
|
||||||
@@ -600,13 +663,15 @@
|
|||||||
((hs-element? target)
|
((hs-element? target)
|
||||||
(dom-insert-adjacent-html target "beforeend" (str value)))
|
(dom-insert-adjacent-html target "beforeend" (str value)))
|
||||||
(true nil)))))
|
(true nil)))))
|
||||||
;; DOM query stub — sandbox returns empty list
|
;; Property-based is — check obj.key truthiness
|
||||||
(define
|
(define
|
||||||
hs-fetch
|
hs-fetch
|
||||||
(fn
|
(fn
|
||||||
(url format)
|
(url format)
|
||||||
(perform (list "io-fetch" url (if format format "text")))))
|
(let
|
||||||
;; Method dispatch — obj.method(args)
|
((fmt (cond ((nil? format) "text") ((or (= format "JSON") (= format "json") (= format "Object") (= format "object")) "json") ((or (= format "HTML") (= format "html")) "html") ((or (= format "Response") (= format "response")) "response") ((or (= format "Text") (= format "text")) "text") (true format))))
|
||||||
|
(perform (list "io-fetch" url fmt)))))
|
||||||
|
;; Array slicing (inclusive both ends)
|
||||||
(define
|
(define
|
||||||
hs-coerce
|
hs-coerce
|
||||||
(fn
|
(fn
|
||||||
@@ -649,11 +714,7 @@
|
|||||||
(str (/ (floor (+ (* num factor) 0.5)) factor))))))
|
(str (/ (floor (+ (* num factor) 0.5)) factor))))))
|
||||||
((= type-name "Selector") (str value))
|
((= type-name "Selector") (str value))
|
||||||
((= type-name "Fragment") value)
|
((= type-name "Fragment") value)
|
||||||
((= type-name "Values")
|
((= type-name "Values") (hs-as-values value))
|
||||||
(if
|
|
||||||
(dict? value)
|
|
||||||
(map (fn (k) (get value k)) (keys value))
|
|
||||||
value))
|
|
||||||
((= type-name "Keys") (if (dict? value) (sort (keys value)) value))
|
((= type-name "Keys") (if (dict? value) (sort (keys value)) value))
|
||||||
((= type-name "Entries")
|
((= type-name "Entries")
|
||||||
(if
|
(if
|
||||||
@@ -697,9 +758,126 @@
|
|||||||
(map (fn (k) (list k (get value k))) (keys value))
|
(map (fn (k) (list k (get value k))) (keys value))
|
||||||
value))
|
value))
|
||||||
(true value))))
|
(true value))))
|
||||||
|
;; Collection: sorted by
|
||||||
|
(define
|
||||||
|
hs-gather-form-nodes
|
||||||
|
(fn
|
||||||
|
(root)
|
||||||
|
(let
|
||||||
|
((acc (list)))
|
||||||
|
(define
|
||||||
|
walk
|
||||||
|
(fn
|
||||||
|
(node)
|
||||||
|
(let
|
||||||
|
((tag (host-get node "tagName")))
|
||||||
|
(cond
|
||||||
|
((or (= tag "INPUT") (= tag "SELECT") (= tag "TEXTAREA"))
|
||||||
|
(set! acc (append acc (list node))))
|
||||||
|
(true
|
||||||
|
(let
|
||||||
|
((kids (host-get node "children")))
|
||||||
|
(when
|
||||||
|
(and (not (nil? kids)) (list? kids))
|
||||||
|
(let
|
||||||
|
((n (len kids)))
|
||||||
|
(define
|
||||||
|
each
|
||||||
|
(fn
|
||||||
|
(i)
|
||||||
|
(when
|
||||||
|
(< i n)
|
||||||
|
(walk (nth kids i))
|
||||||
|
(each (+ i 1)))))
|
||||||
|
(each 0)))))))))
|
||||||
|
(walk root)
|
||||||
|
acc)))
|
||||||
|
;; Collection: sorted by descending
|
||||||
|
(define
|
||||||
|
hs-values-from-nodes
|
||||||
|
(fn (nodes) (reduce hs-values-absorb (dict) nodes)))
|
||||||
|
;; Collection: split by
|
||||||
|
(define
|
||||||
|
hs-value-of-node
|
||||||
|
(fn
|
||||||
|
(node)
|
||||||
|
(let
|
||||||
|
((tag (host-get node "tagName")) (typ (host-get node "type")))
|
||||||
|
(cond
|
||||||
|
((= tag "SELECT")
|
||||||
|
(if
|
||||||
|
(host-get node "multiple")
|
||||||
|
(hs-select-multi-values node)
|
||||||
|
(host-get node "value")))
|
||||||
|
((or (= typ "checkbox") (= typ "radio"))
|
||||||
|
(if (host-get node "checked") (host-get node "value") nil))
|
||||||
|
(true (host-get node "value"))))))
|
||||||
|
;; Collection: joined by
|
||||||
|
(define
|
||||||
|
hs-select-multi-values
|
||||||
|
(fn
|
||||||
|
(node)
|
||||||
|
(let
|
||||||
|
((options (host-get node "options")) (acc (list)))
|
||||||
|
(if
|
||||||
|
(or (nil? options) (not (list? options)))
|
||||||
|
acc
|
||||||
|
(let
|
||||||
|
((n (len options)))
|
||||||
|
(define
|
||||||
|
each
|
||||||
|
(fn
|
||||||
|
(i)
|
||||||
|
(when
|
||||||
|
(< i n)
|
||||||
|
(let
|
||||||
|
((opt (nth options i)))
|
||||||
|
(when
|
||||||
|
(host-get opt "selected")
|
||||||
|
(set! acc (append acc (list (host-get opt "value"))))))
|
||||||
|
(each (+ i 1)))))
|
||||||
|
(each 0)
|
||||||
|
acc)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hs-values-absorb
|
||||||
|
(fn
|
||||||
|
(acc node)
|
||||||
|
(let
|
||||||
|
((name (host-get node "name")))
|
||||||
|
(if
|
||||||
|
(or (nil? name) (= name ""))
|
||||||
|
acc
|
||||||
|
(let
|
||||||
|
((v (hs-value-of-node node)))
|
||||||
|
(cond
|
||||||
|
((nil? v) acc)
|
||||||
|
((has-key? acc name)
|
||||||
|
(let
|
||||||
|
((existing (get acc name)))
|
||||||
|
(do
|
||||||
|
(if
|
||||||
|
(list? existing)
|
||||||
|
(dict-set! acc name (append existing (list v)))
|
||||||
|
(dict-set! acc name (list existing v)))
|
||||||
|
acc)))
|
||||||
|
(true (do (dict-set! acc name v) acc))))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hs-as-values
|
||||||
|
(fn
|
||||||
|
(value)
|
||||||
|
(cond
|
||||||
|
((nil? value) (dict))
|
||||||
|
((list? value) (hs-values-from-nodes value))
|
||||||
|
(true
|
||||||
|
(let
|
||||||
|
((tag (host-get value "tagName")))
|
||||||
|
(cond
|
||||||
|
((or (= tag "INPUT") (= tag "SELECT") (= tag "TEXTAREA"))
|
||||||
|
(hs-values-from-nodes (list value)))
|
||||||
|
(true (hs-values-from-nodes (hs-gather-form-nodes value)))))))))
|
||||||
|
|
||||||
;; ── 0.9.90 features ─────────────────────────────────────────────
|
|
||||||
;; beep! — debug logging, returns value unchanged
|
|
||||||
(define
|
(define
|
||||||
hs-default?
|
hs-default?
|
||||||
(fn
|
(fn
|
||||||
@@ -708,13 +886,13 @@
|
|||||||
((nil? v) true)
|
((nil? v) true)
|
||||||
((and (string? v) (= v "")) true)
|
((and (string? v) (= v "")) true)
|
||||||
(true false))))
|
(true false))))
|
||||||
;; Property-based is — check obj.key truthiness
|
|
||||||
(define
|
(define
|
||||||
hs-array-set!
|
hs-array-set!
|
||||||
(fn
|
(fn
|
||||||
(arr i v)
|
(arr i v)
|
||||||
(if (list? arr) (do (set-nth! arr i v) v) (host-set! arr i v))))
|
(if (list? arr) (do (set-nth! arr i v) v) (host-set! arr i v))))
|
||||||
;; Array slicing (inclusive both ends)
|
|
||||||
(define
|
(define
|
||||||
hs-add
|
hs-add
|
||||||
(fn
|
(fn
|
||||||
@@ -724,24 +902,117 @@
|
|||||||
((list? b) (cons a b))
|
((list? b) (cons a b))
|
||||||
((or (string? a) (string? b)) (str a b))
|
((or (string? a) (string? b)) (str a b))
|
||||||
(true (+ a b)))))
|
(true (+ a b)))))
|
||||||
;; Collection: sorted by
|
|
||||||
(define
|
(begin
|
||||||
hs-make
|
(define
|
||||||
(fn
|
hs-make
|
||||||
(type-name)
|
(fn
|
||||||
(cond
|
(type-name &rest args)
|
||||||
((= type-name "Object") (dict))
|
(if
|
||||||
((= type-name "Array") (list))
|
(hs-make-element? type-name)
|
||||||
((= type-name "Set") (list))
|
(hs-make-element type-name)
|
||||||
((= type-name "Map") (dict))
|
(let
|
||||||
(true (dict)))))
|
((ctor (host-global type-name)))
|
||||||
;; Collection: sorted by descending
|
(if
|
||||||
|
(nil? ctor)
|
||||||
|
(cond
|
||||||
|
((= type-name "Object") (dict))
|
||||||
|
((= type-name "Array") (list))
|
||||||
|
((= type-name "Set") (list))
|
||||||
|
((= type-name "Map") (dict))
|
||||||
|
(true (dict)))
|
||||||
|
(apply host-new (cons type-name args)))))))
|
||||||
|
(define
|
||||||
|
hs-make-element?
|
||||||
|
(fn
|
||||||
|
(s)
|
||||||
|
(and
|
||||||
|
(string? s)
|
||||||
|
(> (len s) 0)
|
||||||
|
(let
|
||||||
|
((c (substring s 0 1)))
|
||||||
|
(or
|
||||||
|
(= c ".")
|
||||||
|
(= c "#")
|
||||||
|
(contains? s ".")
|
||||||
|
(contains? s "#")
|
||||||
|
(and (hs-lower-letter? c) (not (any-upper? s))))))))
|
||||||
|
(define hs-lower-letter? (fn (c) (and (>= c "a") (<= c "z"))))
|
||||||
|
(define
|
||||||
|
any-upper?
|
||||||
|
(fn
|
||||||
|
(s)
|
||||||
|
(let
|
||||||
|
((n (len s)))
|
||||||
|
(define
|
||||||
|
scan
|
||||||
|
(fn
|
||||||
|
(i)
|
||||||
|
(cond
|
||||||
|
((>= i n) false)
|
||||||
|
((and (>= (substring s i (+ i 1)) "A") (<= (substring s i (+ i 1)) "Z"))
|
||||||
|
true)
|
||||||
|
(true (scan (+ i 1))))))
|
||||||
|
(scan 0))))
|
||||||
|
(define
|
||||||
|
hs-make-element
|
||||||
|
(fn
|
||||||
|
(sel)
|
||||||
|
(let
|
||||||
|
((parsed (hs-parse-element-selector sel)))
|
||||||
|
(let
|
||||||
|
((tag (get parsed "tag"))
|
||||||
|
(id (get parsed "id"))
|
||||||
|
(classes (get parsed "classes")))
|
||||||
|
(let
|
||||||
|
((el (dom-create-element (if (= tag "") "div" tag))))
|
||||||
|
(when (and id (not (= id ""))) (dom-set-attr el "id" id))
|
||||||
|
(for-each (fn (c) (dom-add-class el c)) classes)
|
||||||
|
el)))))
|
||||||
|
(define
|
||||||
|
hs-parse-element-selector
|
||||||
|
(fn
|
||||||
|
(sel)
|
||||||
|
(let
|
||||||
|
((n (len sel))
|
||||||
|
(tag "")
|
||||||
|
(id "")
|
||||||
|
(classes (list))
|
||||||
|
(cur "")
|
||||||
|
(mode "tag"))
|
||||||
|
(define
|
||||||
|
flush!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(cond
|
||||||
|
((= mode "tag") (set! tag cur))
|
||||||
|
((= mode "id") (set! id cur))
|
||||||
|
((= mode "class") (set! classes (append classes (list cur)))))
|
||||||
|
(set! cur "")))
|
||||||
|
(define
|
||||||
|
walk
|
||||||
|
(fn
|
||||||
|
(i)
|
||||||
|
(when
|
||||||
|
(< i n)
|
||||||
|
(let
|
||||||
|
((ch (substring sel i (+ i 1))))
|
||||||
|
(cond
|
||||||
|
((= ch ".")
|
||||||
|
(do (flush!) (set! mode "class") (walk (+ i 1))))
|
||||||
|
((= ch "#")
|
||||||
|
(do (flush!) (set! mode "id") (walk (+ i 1))))
|
||||||
|
(true (do (set! cur (str cur ch)) (walk (+ i 1)))))))))
|
||||||
|
(walk 0)
|
||||||
|
(flush!)
|
||||||
|
{:tag tag :classes classes :id id}))))
|
||||||
|
|
||||||
(define hs-install (fn (behavior-fn) (behavior-fn me)))
|
(define hs-install (fn (behavior-fn) (behavior-fn me)))
|
||||||
;; Collection: split by
|
|
||||||
(define
|
(define
|
||||||
hs-measure
|
hs-measure
|
||||||
(fn (target) (perform (list (quote io-measure) target))))
|
(fn (target) (perform (list (quote io-measure) target))))
|
||||||
;; Collection: joined by
|
|
||||||
(define
|
(define
|
||||||
hs-transition
|
hs-transition
|
||||||
(fn
|
(fn
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -21,17 +21,27 @@ 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; }
|
// Default CSS values for unset inline styles (matches browser UA defaults
|
||||||
|
// for block-level elements — our tests mostly exercise divs).
|
||||||
|
function mkStyle(tag) {
|
||||||
|
const inline = ['SPAN','A','B','I','EM','STRONG','CODE','LABEL','SMALL','SUB','SUP','U','MARK'];
|
||||||
|
const display = (tag && inline.includes(tag.toUpperCase())) ? 'inline' : 'block';
|
||||||
|
const s = { display, visibility: 'visible', opacity: '1' };
|
||||||
|
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=mkStyle(); this.attributes={}; this.children=[]; this.childNodes=[]; this.childNodes.item=function(i){return this[i]||null;}; 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.tagName); this.attributes={}; this.children=[]; this.childNodes=[]; this.childNodes.item=function(i){return this[i]||null;}; 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; if(n==='style'){const s=String(v);for(const d of s.split(';')){const c=d.indexOf(':');if(c>0){const k=d.slice(0,c).trim();const val=d.slice(c+1).trim();if(k)this.style.setProperty(k,val);}} } }
|
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==='name')this.name=v; if(n==='type')this.type=v; if(n==='checked')this.checked=true; if(n==='selected')this.selected=true; if(n==='multiple')this.multiple=true; if(n==='disabled')this.disabled=true; if(n==='style'){const s=String(v);for(const d of s.split(';')){const c=d.indexOf(':');if(c>0){const k=d.slice(0,c).trim();const val=d.slice(c+1).trim();if(k)this.style.setProperty(k,val);}} } }
|
||||||
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; }
|
||||||
hasAttribute(n) { return n in this.attributes; }
|
hasAttribute(n) { return n in this.attributes; }
|
||||||
addEventListener(e,f) { if(!this._listeners[e])this._listeners[e]=[]; this._listeners[e].push(f); }
|
addEventListener(e,f) { if(!this._listeners[e])this._listeners[e]=[]; this._listeners[e].push(f); }
|
||||||
removeEventListener(e,f) { if(this._listeners[e])this._listeners[e]=this._listeners[e].filter(x=>x!==f); }
|
removeEventListener(e,f) { if(this._listeners[e])this._listeners[e]=this._listeners[e].filter(x=>x!==f); }
|
||||||
dispatchEvent(ev) { ev.target=ev.target||this; ev.currentTarget=this; const fns=[...(this._listeners[ev.type]||[])]; for(const f of fns){if(ev._si)break;try{f.call(this,ev);}catch(e){}} if(ev.bubbles&&!ev._sp&&this.parentElement){this.parentElement.dispatchEvent(ev);} return !ev.defaultPrevented; }
|
dispatchEvent(ev) { ev.target=ev.target||this; ev.currentTarget=this; const fns=[...(this._listeners[ev.type]||[])]; for(const f of fns){if(ev._si)break;try{f.call(this,ev);}catch(e){}} if(ev.bubbles&&!ev._sp&&this.parentElement){this.parentElement.dispatchEvent(ev);} return !ev.defaultPrevented; }
|
||||||
appendChild(c) { if(c.parentElement)c.parentElement.removeChild(c); c.parentElement=this; c.parentNode=this; this.children.push(c); this.childNodes.push(c); this._syncText(); return c; }
|
appendChild(c) { if(c.parentElement)c.parentElement.removeChild(c); c.parentElement=this; c.parentNode=this; this.children.push(c); this.childNodes.push(c); if(this.tagName==='SELECT'&&c.tagName==='OPTION'){this.options.push(c);if(c.selected&&this.selectedIndex<0)this.selectedIndex=this.options.length-1;} this._syncText(); return c; }
|
||||||
removeChild(c) { this.children=this.children.filter(x=>x!==c); this.childNodes=this.childNodes.filter(x=>x!==c); c.parentElement=null; c.parentNode=null; this._syncText(); return c; }
|
removeChild(c) { this.children=this.children.filter(x=>x!==c); this.childNodes=this.childNodes.filter(x=>x!==c); c.parentElement=null; c.parentNode=null; this._syncText(); return c; }
|
||||||
insertBefore(n,r) { if(n.parentElement)n.parentElement.removeChild(n); const i=this.children.indexOf(r); if(i>=0){this.children.splice(i,0,n);this.childNodes.splice(i,0,n);}else{this.children.push(n);this.childNodes.push(n);} n.parentElement=this;n.parentNode=this; this._syncText(); return n; }
|
insertBefore(n,r) { if(n.parentElement)n.parentElement.removeChild(n); const i=this.children.indexOf(r); if(i>=0){this.children.splice(i,0,n);this.childNodes.splice(i,0,n);}else{this.children.push(n);this.childNodes.push(n);} n.parentElement=this;n.parentNode=this; this._syncText(); return n; }
|
||||||
replaceChild(n,o) { const i=this.children.indexOf(o); if(i>=0){this.children[i]=n;this.childNodes[i]=n;} n.parentElement=this;n.parentNode=this; o.parentElement=null;o.parentNode=null; this._syncText(); return o; }
|
replaceChild(n,o) { const i=this.children.indexOf(o); if(i>=0){this.children[i]=n;this.childNodes[i]=n;} n.parentElement=this;n.parentNode=this; o.parentElement=null;o.parentNode=null; this._syncText(); return o; }
|
||||||
@@ -74,27 +84,44 @@ class El {
|
|||||||
get scrollHeight() { return 100; } get scrollWidth() { return 100; }
|
get scrollHeight() { return 100; } get scrollWidth() { return 100; }
|
||||||
get clientHeight() { return 100; } get clientWidth() { return 100; }
|
get clientHeight() { return 100; } get clientWidth() { return 100; }
|
||||||
insertAdjacentHTML(pos, html) {
|
insertAdjacentHTML(pos, html) {
|
||||||
// For non-HTML content (plain text/numbers), just append to innerHTML
|
// Parse the HTML into real El instances so they can be queried, clicked,
|
||||||
|
// and HS-activated. Text-only content becomes a textContent append.
|
||||||
if (typeof html !== 'string') html = String(html);
|
if (typeof html !== 'string') html = String(html);
|
||||||
if (pos === 'beforeend' || pos === 'beforeEnd') {
|
const parsed = parseHTMLFragments(html);
|
||||||
|
const p = (pos || '').toLowerCase();
|
||||||
|
if (parsed.length === 0) {
|
||||||
|
if (p === 'beforeend' || p === 'afterbegin') this.textContent = (this.textContent || '') + html;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (p === 'beforeend') {
|
||||||
|
for (const c of parsed) this.appendChild(c);
|
||||||
this.innerHTML = (this.innerHTML || '') + html;
|
this.innerHTML = (this.innerHTML || '') + html;
|
||||||
this.textContent = (this.textContent || '') + html.replace(/<[^>]*>/g, '');
|
} else if (p === 'afterbegin') {
|
||||||
} else if (pos === 'afterbegin' || pos === 'afterBegin') {
|
const first = this.children[0] || null;
|
||||||
|
for (const c of parsed) { if (first) this.insertBefore(c, first); else this.appendChild(c); }
|
||||||
this.innerHTML = html + (this.innerHTML || '');
|
this.innerHTML = html + (this.innerHTML || '');
|
||||||
this.textContent = html.replace(/<[^>]*>/g, '') + (this.textContent || '');
|
} else if (p === 'beforebegin' && this.parentElement) {
|
||||||
} else if (pos === 'beforebegin' || pos === 'beforeBegin') {
|
for (const c of parsed) this.parentElement.insertBefore(c, this);
|
||||||
if (this.parentElement) { this.parentElement.insertAdjacentHTML('beforeend', html); }
|
} else if (p === 'afterend' && this.parentElement) {
|
||||||
} else if (pos === 'afterend' || pos === 'afterEnd') {
|
const parent = this.parentElement;
|
||||||
if (this.parentElement) { this.parentElement.insertAdjacentHTML('beforeend', html); }
|
const idx = parent.children.indexOf(this);
|
||||||
|
const nextSibling = parent.children[idx + 1] || null;
|
||||||
|
for (const c of parsed) {
|
||||||
|
if (nextSibling) parent.insertBefore(c, nextSibling);
|
||||||
|
else parent.appendChild(c);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class CL { constructor(e){this._el=e;this._set=new Set();} _sync(str){this._set=new Set((str||'').split(/\s+/).filter(Boolean));} add(...c){for(const x of c)this._set.add(x);this._el.className=[...this._set].join(' ');this._el.attributes['class']=this._el.className;} remove(...c){for(const x of c)this._set.delete(x);this._el.className=[...this._set].join(' ');this._el.attributes['class']=this._el.className;} toggle(c,f){if(f!==undefined){if(f)this.add(c);else this.remove(c);return f;} if(this._set.has(c)){this.remove(c);return false;}else{this.add(c);return true;}} contains(c){return this._set.has(c);} get length(){return this._set.size;} [Symbol.iterator](){return this._set[Symbol.iterator]();} }
|
class CL { constructor(e){this._el=e;this._set=new Set();} _sync(str){this._set=new Set((str||'').split(/\s+/).filter(Boolean));} add(...c){for(const x of c)this._set.add(x);this._el.className=[...this._set].join(' ');this._el.attributes['class']=this._el.className;} remove(...c){for(const x of c)this._set.delete(x);this._el.className=[...this._set].join(' ');this._el.attributes['class']=this._el.className;} toggle(c,f){if(f!==undefined){if(f)this.add(c);else this.remove(c);return f;} if(this._set.has(c)){this.remove(c);return false;}else{this.add(c);return true;}} contains(c){return this._set.has(c);} get length(){return this._set.size;} [Symbol.iterator](){return this._set[Symbol.iterator]();} }
|
||||||
class Ev { constructor(t,o={}){this.type=t;this.bubbles=o.bubbles||false;this.cancelable=o.cancelable!==false;this.defaultPrevented=false;this._sp=false;this._si=false;this.target=null;this.currentTarget=null;this.detail=o.detail||null;} preventDefault(){this.defaultPrevented=true;} stopPropagation(){this._sp=true;} stopImmediatePropagation(){this._sp=true;this._si=true;} }
|
class Ev { constructor(t,o={}){this.type=t;this.bubbles=o.bubbles||false;this.cancelable=o.cancelable!==false;this.defaultPrevented=false;this._sp=false;this._si=false;this.target=null;this.currentTarget=null;this.detail=o.detail||null;} preventDefault(){this.defaultPrevented=true;} stopPropagation(){this._sp=true;} stopImmediatePropagation(){this._sp=true;this._si=true;} }
|
||||||
|
|
||||||
|
const VOID_TAGS = new Set(['input','br','hr','img','meta','link','area','base','col','embed','source','track','wbr']);
|
||||||
function parseHTMLFragments(html) {
|
function parseHTMLFragments(html) {
|
||||||
const results = [];
|
const results = [];
|
||||||
const re = /<(\w+)([^>]*?)(?:\/>|>([\s\S]*?)<\/\1>)/g;
|
// Match self-closing `<tag .../>`, paired `<tag>...</tag>`, or void
|
||||||
|
// elements with no close tag (input, br, hr, etc.).
|
||||||
|
const re = /<(\w+)([^>]*?)(?:\/>|>([\s\S]*?)<\/\1>|>)/g;
|
||||||
let m;
|
let m;
|
||||||
let lastIndex = 0;
|
let lastIndex = 0;
|
||||||
while ((m = re.exec(html)) !== null) {
|
while ((m = re.exec(html)) !== null) {
|
||||||
@@ -106,10 +133,20 @@ function parseHTMLFragments(html) {
|
|||||||
// Can't push text nodes directly to El children; wrap if needed
|
// Can't push text nodes directly to El children; wrap if needed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const tag = m[1]; const attrs = m[2]; const inner = m[3] || '';
|
const tag = m[1]; const attrs = m[2]; let inner = m[3] || '';
|
||||||
|
// If the regex matched a void-style `<tag ...>` with no close tag,
|
||||||
|
// ensure we don't treat subsequent markup as inner content.
|
||||||
|
if (inner === '' && !m[0].endsWith('/>') && !VOID_TAGS.has(tag.toLowerCase()) && m[0].endsWith('>')) {
|
||||||
|
// Generic `<tag ...>` with no close tag — still leave inner empty;
|
||||||
|
// this keeps behaviour lenient without running past the next tag.
|
||||||
|
}
|
||||||
const el = new El(tag);
|
const el = new El(tag);
|
||||||
const attrRe = /([\w-]+)="([^"]*)"/g; let am;
|
const attrRe = /([\w-]+)(?:="([^"]*)")?/g; let am;
|
||||||
while ((am = attrRe.exec(attrs))) el.setAttribute(am[1], am[2]);
|
while ((am = attrRe.exec(attrs))) {
|
||||||
|
const nm = am[1]; const val = am[2];
|
||||||
|
if (val !== undefined) el.setAttribute(nm, val);
|
||||||
|
else el.setAttribute(nm, '');
|
||||||
|
}
|
||||||
// Also handle boolean attrs like disabled
|
// Also handle boolean attrs like disabled
|
||||||
const boolRe = /\s(\w+)(?=\s|\/|>|$)/g;
|
const boolRe = /\s(\w+)(?=\s|\/|>|$)/g;
|
||||||
if (inner) {
|
if (inner) {
|
||||||
|
|||||||
@@ -193,11 +193,20 @@ with open(INPUT) as f:
|
|||||||
# ── HTML parsing ──────────────────────────────────────────────────
|
# ── HTML parsing ──────────────────────────────────────────────────
|
||||||
|
|
||||||
def extract_hs_scripts(html):
|
def extract_hs_scripts(html):
|
||||||
"""Extract <script type='text/hyperscript'>...</script> content blocks."""
|
"""Extract <script type='text/hyperscript'>...</script> content blocks.
|
||||||
|
|
||||||
|
For PW-style bodies, script markup may be spread across `"..." + "..."`
|
||||||
|
string-concat segments inside `html(...)`. First inline those segments
|
||||||
|
so the direct regex catches the opening + closing tag pair.
|
||||||
|
"""
|
||||||
|
flattened = re.sub(
|
||||||
|
r'(["\x27`])\s*\+\s*(?:\n\s*)?(["\x27`])',
|
||||||
|
'', html,
|
||||||
|
)
|
||||||
scripts = []
|
scripts = []
|
||||||
for m in re.finditer(
|
for m in re.finditer(
|
||||||
r"<script\s+type=['\"]text/hyperscript['\"]>(.*?)</script>",
|
r"<script\s+type=['\"]text/hyperscript['\"]>(.*?)</script>",
|
||||||
html, re.DOTALL
|
flattened, re.DOTALL,
|
||||||
):
|
):
|
||||||
scripts.append(m.group(1).strip())
|
scripts.append(m.group(1).strip())
|
||||||
return scripts
|
return scripts
|
||||||
@@ -723,75 +732,172 @@ def pw_assertion_to_sx(target, negated, assert_type, args_str):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _body_statements(body):
|
||||||
|
"""Yield top-level statements from a JS test body, split on `;` at
|
||||||
|
depth 0, respecting string/backtick/paren/brace nesting."""
|
||||||
|
depth, in_str, esc, buf = 0, None, False, []
|
||||||
|
for ch in body:
|
||||||
|
if in_str:
|
||||||
|
buf.append(ch)
|
||||||
|
if esc:
|
||||||
|
esc = False
|
||||||
|
elif ch == '\\':
|
||||||
|
esc = True
|
||||||
|
elif ch == in_str:
|
||||||
|
in_str = None
|
||||||
|
continue
|
||||||
|
if ch in ('"', "'", '`'):
|
||||||
|
in_str = ch
|
||||||
|
buf.append(ch)
|
||||||
|
continue
|
||||||
|
if ch in '([{':
|
||||||
|
depth += 1
|
||||||
|
elif ch in ')]}':
|
||||||
|
depth -= 1
|
||||||
|
if ch == ';' and depth == 0:
|
||||||
|
s = ''.join(buf).strip()
|
||||||
|
if s:
|
||||||
|
yield s
|
||||||
|
buf = []
|
||||||
|
else:
|
||||||
|
buf.append(ch)
|
||||||
|
last = ''.join(buf).strip()
|
||||||
|
if last:
|
||||||
|
yield last
|
||||||
|
|
||||||
|
|
||||||
|
def _window_setup_ops(assign_body):
|
||||||
|
"""Parse `window.X = Y[; window.Z = W; ...]` into (name, sx_val) tuples."""
|
||||||
|
out = []
|
||||||
|
for substmt in split_top_level_chars(assign_body, ';'):
|
||||||
|
sm = re.match(r'\s*window\.(\w+)\s*=\s*(.+?)\s*$', substmt, re.DOTALL)
|
||||||
|
if not sm:
|
||||||
|
continue
|
||||||
|
sx_val = js_expr_to_sx(sm.group(2).strip())
|
||||||
|
if sx_val is not None:
|
||||||
|
out.append((sm.group(1), sx_val))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def parse_dev_body(body, elements, var_names):
|
def parse_dev_body(body, elements, var_names):
|
||||||
"""Parse Playwright test body to extract actions and post-action assertions.
|
"""Parse Playwright test body into ordered SX ops.
|
||||||
|
|
||||||
Returns a single ordered list of SX expression strings (actions and assertions
|
Returns (pre_setups, ops) where:
|
||||||
interleaved in their original order). Pre-action assertions are skipped.
|
- pre_setups: list of (name, sx_val) for `window.X = Y` setups that
|
||||||
|
appear BEFORE the first `html(...)` call; these should be emitted
|
||||||
|
before element creation so activation can see them.
|
||||||
|
- ops: ordered list of SX expression strings — setups, actions, and
|
||||||
|
assertions interleaved in their original body order, starting after
|
||||||
|
the first `html(...)` call.
|
||||||
"""
|
"""
|
||||||
|
pre_setups = []
|
||||||
ops = []
|
ops = []
|
||||||
found_first_action = False
|
seen_html = False
|
||||||
|
|
||||||
for line in body.split('\n'):
|
def add_action(stmt):
|
||||||
line = line.strip()
|
am = re.search(
|
||||||
|
r"find\((['\"])(.+?)\1\)(?:\.(?:first|last)\(\))?"
|
||||||
|
r"\.(click|dispatchEvent|fill|check|uncheck|focus|selectOption)\(([^)]*)\)",
|
||||||
|
stmt,
|
||||||
|
)
|
||||||
|
if not am or 'expect' in stmt:
|
||||||
|
return False
|
||||||
|
selector = am.group(2)
|
||||||
|
action_type = am.group(3)
|
||||||
|
action_arg = am.group(4).strip("'\"")
|
||||||
|
target = selector_to_sx(selector, elements, var_names)
|
||||||
|
if action_type == 'click':
|
||||||
|
ops.append(f'(dom-dispatch {target} "click" nil)')
|
||||||
|
elif action_type == 'dispatchEvent':
|
||||||
|
ops.append(f'(dom-dispatch {target} "{action_arg}" nil)')
|
||||||
|
elif action_type == 'fill':
|
||||||
|
escaped = action_arg.replace('\\', '\\\\').replace('"', '\\"')
|
||||||
|
ops.append(f'(dom-set-prop {target} "value" "{escaped}")')
|
||||||
|
ops.append(f'(dom-dispatch {target} "input" nil)')
|
||||||
|
elif action_type == 'check':
|
||||||
|
ops.append(f'(dom-set-prop {target} "checked" true)')
|
||||||
|
ops.append(f'(dom-dispatch {target} "change" nil)')
|
||||||
|
elif action_type == 'uncheck':
|
||||||
|
ops.append(f'(dom-set-prop {target} "checked" false)')
|
||||||
|
ops.append(f'(dom-dispatch {target} "change" nil)')
|
||||||
|
elif action_type == 'focus':
|
||||||
|
ops.append(f'(dom-focus {target})')
|
||||||
|
elif action_type == 'selectOption':
|
||||||
|
escaped = action_arg.replace('\\', '\\\\').replace('"', '\\"')
|
||||||
|
ops.append(f'(dom-set-prop {target} "value" "{escaped}")')
|
||||||
|
ops.append(f'(dom-dispatch {target} "change" nil)')
|
||||||
|
return True
|
||||||
|
|
||||||
# Skip comments
|
def add_assertion(stmt):
|
||||||
if line.startswith('//'):
|
em = re.search(
|
||||||
continue
|
|
||||||
|
|
||||||
# Action: find('selector')[.first()/.last()].click/dispatchEvent/fill/check/uncheck/focus()
|
|
||||||
m = re.search(r"find\((['\"])(.+?)\1\)(?:\.(?:first|last)\(\))?\.(click|dispatchEvent|fill|check|uncheck|focus|selectOption)\(([^)]*)\)", line)
|
|
||||||
if m and 'expect' not in line:
|
|
||||||
found_first_action = True
|
|
||||||
selector = m.group(2)
|
|
||||||
action_type = m.group(3)
|
|
||||||
action_arg = m.group(4).strip("'\"")
|
|
||||||
target = selector_to_sx(selector, elements, var_names)
|
|
||||||
if action_type == 'click':
|
|
||||||
ops.append(f'(dom-dispatch {target} "click" nil)')
|
|
||||||
elif action_type == 'dispatchEvent':
|
|
||||||
ops.append(f'(dom-dispatch {target} "{action_arg}" nil)')
|
|
||||||
elif action_type == 'fill':
|
|
||||||
escaped = action_arg.replace('\\', '\\\\').replace('"', '\\"')
|
|
||||||
ops.append(f'(dom-set-prop {target} "value" "{escaped}")')
|
|
||||||
ops.append(f'(dom-dispatch {target} "input" nil)')
|
|
||||||
elif action_type == 'check':
|
|
||||||
ops.append(f'(dom-set-prop {target} "checked" true)')
|
|
||||||
ops.append(f'(dom-dispatch {target} "change" nil)')
|
|
||||||
elif action_type == 'uncheck':
|
|
||||||
ops.append(f'(dom-set-prop {target} "checked" false)')
|
|
||||||
ops.append(f'(dom-dispatch {target} "change" nil)')
|
|
||||||
elif action_type == 'focus':
|
|
||||||
ops.append(f'(dom-focus {target})')
|
|
||||||
elif action_type == 'selectOption':
|
|
||||||
escaped = action_arg.replace('\\', '\\\\').replace('"', '\\"')
|
|
||||||
ops.append(f'(dom-set-prop {target} "value" "{escaped}")')
|
|
||||||
ops.append(f'(dom-dispatch {target} "change" nil)')
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Skip lines before first action (pre-checks, setup)
|
|
||||||
if not found_first_action:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Assertion: expect(find('selector')[.first()/.last()]).[not.]toHaveText("value")
|
|
||||||
m = re.search(
|
|
||||||
r"expect\(find\((['\"])(.+?)\1\)(?:\.(?:first|last)\(\))?\)\.(not\.)?"
|
r"expect\(find\((['\"])(.+?)\1\)(?:\.(?:first|last)\(\))?\)\.(not\.)?"
|
||||||
r"(toHaveText|toHaveClass|toHaveCSS|toHaveAttribute|toHaveValue|toBeVisible|toBeHidden|toBeChecked)"
|
r"(toHaveText|toHaveClass|toHaveCSS|toHaveAttribute|toHaveValue|toBeVisible|toBeHidden|toBeChecked)"
|
||||||
r"\(((?:[^()]|\([^()]*\))*)\)",
|
r"\(((?:[^()]|\([^()]*\))*)\)",
|
||||||
line
|
stmt,
|
||||||
)
|
)
|
||||||
if m:
|
if not em:
|
||||||
selector = m.group(2)
|
return False
|
||||||
negated = bool(m.group(3))
|
selector = em.group(2)
|
||||||
assert_type = m.group(4)
|
negated = bool(em.group(3))
|
||||||
args_str = m.group(5)
|
assert_type = em.group(4)
|
||||||
target = selector_to_sx(selector, elements, var_names)
|
args_str = em.group(5)
|
||||||
sx = pw_assertion_to_sx(target, negated, assert_type, args_str)
|
target = selector_to_sx(selector, elements, var_names)
|
||||||
if sx:
|
sx = pw_assertion_to_sx(target, negated, assert_type, args_str)
|
||||||
ops.append(sx)
|
if sx:
|
||||||
|
ops.append(sx)
|
||||||
|
return True
|
||||||
|
|
||||||
|
for stmt in _body_statements(body):
|
||||||
|
stmt_na = re.sub(r'^(?:await\s+)+', '', stmt).strip()
|
||||||
|
|
||||||
|
# html(...) — marks the DOM-built boundary. Setups after this go inline.
|
||||||
|
if re.match(r'html\s*\(', stmt_na):
|
||||||
|
seen_html = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return ops
|
# evaluate(() => window.X = Y) — single-expression window setup.
|
||||||
|
m = re.match(
|
||||||
|
r'evaluate\(\s*\(\)\s*=>\s*(window\.\w+\s*=\s*.+?)\s*\)\s*$',
|
||||||
|
stmt_na, re.DOTALL,
|
||||||
|
)
|
||||||
|
if m:
|
||||||
|
for name, sx_val in _window_setup_ops(m.group(1)):
|
||||||
|
if seen_html:
|
||||||
|
ops.append(f'(host-set! (host-global "window") "{name}" {sx_val})')
|
||||||
|
else:
|
||||||
|
pre_setups.append((name, sx_val))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# evaluate(() => { window.X = Y; ... }) — block window setup.
|
||||||
|
m = re.match(r'evaluate\(\s*\(\)\s*=>\s*\{(.+)\}\s*\)\s*$', stmt_na, re.DOTALL)
|
||||||
|
if m:
|
||||||
|
for name, sx_val in _window_setup_ops(m.group(1)):
|
||||||
|
if seen_html:
|
||||||
|
ops.append(f'(host-set! (host-global "window") "{name}" {sx_val})')
|
||||||
|
else:
|
||||||
|
pre_setups.append((name, sx_val))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# evaluate(() => document.querySelector(SEL).innerHTML = VAL) — DOM reset.
|
||||||
|
m = re.match(
|
||||||
|
r"evaluate\(\s*\(\)\s*=>\s*document\.querySelector\(\s*(['\"])([^'\"]+)\1\s*\)"
|
||||||
|
r"\.innerHTML\s*=\s*(['\"])(.*?)\3\s*\)\s*$",
|
||||||
|
stmt_na, re.DOTALL,
|
||||||
|
)
|
||||||
|
if m and seen_html:
|
||||||
|
sel = re.sub(r'^#work-area\s+', '', m.group(2))
|
||||||
|
target = selector_to_sx(sel, elements, var_names)
|
||||||
|
val = m.group(4).replace('\\', '\\\\').replace('"', '\\"')
|
||||||
|
ops.append(f'(dom-set-inner-html {target} "{val}")')
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not seen_html:
|
||||||
|
continue
|
||||||
|
if add_action(stmt_na):
|
||||||
|
continue
|
||||||
|
add_assertion(stmt_na)
|
||||||
|
|
||||||
|
return pre_setups, ops
|
||||||
|
|
||||||
|
|
||||||
# ── Test generation ───────────────────────────────────────────────
|
# ── Test generation ───────────────────────────────────────────────
|
||||||
@@ -804,6 +910,10 @@ def process_hs_val(hs_val):
|
|||||||
hs_val = hs_val.replace('\\"', '\x00QUOT\x00')
|
hs_val = hs_val.replace('\\"', '\x00QUOT\x00')
|
||||||
hs_val = hs_val.replace('\\', '')
|
hs_val = hs_val.replace('\\', '')
|
||||||
hs_val = hs_val.replace('\x00QUOT\x00', '\\"')
|
hs_val = hs_val.replace('\x00QUOT\x00', '\\"')
|
||||||
|
# Strip line comments BEFORE newline collapse — once newlines become `then`,
|
||||||
|
# an unterminated `//` / ` --` comment would consume the rest of the input.
|
||||||
|
hs_val = re.sub(r'//[^\n]*', '', hs_val)
|
||||||
|
hs_val = re.sub(r'(^|\s)--[^\n]*', r'\1', hs_val)
|
||||||
cmd_kws = r'(?:set|put|get|add|remove|toggle|hide|show|if|repeat|for|wait|send|trigger|log|call|take|throw|return|append|tell|go|halt|settle|increment|decrement|fetch|make|install|measure|empty|reset|swap|default|morph|render|scroll|focus|select|pick|beep!)'
|
cmd_kws = r'(?:set|put|get|add|remove|toggle|hide|show|if|repeat|for|wait|send|trigger|log|call|take|throw|return|append|tell|go|halt|settle|increment|decrement|fetch|make|install|measure|empty|reset|swap|default|morph|render|scroll|focus|select|pick|beep!)'
|
||||||
hs_val = re.sub(r'\s{2,}(?=' + cmd_kws + r'\b)', ' then ', hs_val)
|
hs_val = re.sub(r'\s{2,}(?=' + cmd_kws + r'\b)', ' then ', hs_val)
|
||||||
hs_val = re.sub(r'\s*[\n\r]\s*', ' then ', hs_val)
|
hs_val = re.sub(r'\s*[\n\r]\s*', ' then ', hs_val)
|
||||||
@@ -817,6 +927,12 @@ def process_hs_val(hs_val):
|
|||||||
hs_val = re.sub(r'\belse then\b', 'else ', hs_val)
|
hs_val = re.sub(r'\belse then\b', 'else ', hs_val)
|
||||||
# Same for `catch <name> then` (try/catch syntax).
|
# Same for `catch <name> then` (try/catch syntax).
|
||||||
hs_val = re.sub(r'\bcatch (\w+) then\b', r'catch \1 ', hs_val)
|
hs_val = re.sub(r'\bcatch (\w+) then\b', r'catch \1 ', hs_val)
|
||||||
|
# Also strip stray `then` BEFORE else/end/catch/finally — they're closers,
|
||||||
|
# not commands, so the separator is spurious (cl-collect tolerates but other
|
||||||
|
# sub-parsers like parse-expr may not).
|
||||||
|
hs_val = re.sub(r'\bthen\s+(?=else\b|end\b|catch\b|finally\b|otherwise\b)', '', hs_val)
|
||||||
|
# Collapse any residual double spaces from above transforms.
|
||||||
|
hs_val = re.sub(r' +', ' ', hs_val)
|
||||||
return hs_val.strip()
|
return hs_val.strip()
|
||||||
|
|
||||||
|
|
||||||
@@ -945,16 +1061,34 @@ def generate_test_pw(test, elements, var_names, idx):
|
|||||||
if test['name'] in SKIP_TEST_NAMES:
|
if test['name'] in SKIP_TEST_NAMES:
|
||||||
return emit_skip_test(test)
|
return emit_skip_test(test)
|
||||||
|
|
||||||
ops = parse_dev_body(test['body'], elements, var_names)
|
pre_setups, ops = parse_dev_body(test['body'], elements, var_names)
|
||||||
|
|
||||||
|
# `<script type="text/hyperscript">` blocks appear in both the
|
||||||
|
# upstream html field AND inside the body's `html(...)` string literal.
|
||||||
|
# Extract from both so def blocks are compiled before the first action.
|
||||||
|
hs_scripts = list(extract_hs_scripts(test.get('html', '')))
|
||||||
|
hs_scripts.extend(extract_hs_scripts(test.get('body', '') or ''))
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
lines.append(f' (deftest "{sx_name(test["name"])}"')
|
lines.append(f' (deftest "{sx_name(test["name"])}"')
|
||||||
lines.append(' (hs-cleanup!)')
|
lines.append(' (hs-cleanup!)')
|
||||||
|
|
||||||
# `evaluate(() => window.X = Y)` setups — see generate_test_chai.
|
# Pre-`html(...)` setups — emit before element creation so activation
|
||||||
for name, sx_val in extract_window_setups(test.get('body', '') or ''):
|
# (init handlers etc.) sees the expected globals.
|
||||||
|
for name, sx_val in pre_setups:
|
||||||
lines.append(f' (host-set! (host-global "window") "{name}" {sx_val})')
|
lines.append(f' (host-set! (host-global "window") "{name}" {sx_val})')
|
||||||
|
|
||||||
|
# Compile script blocks so `def X()` functions are available. Wrap in
|
||||||
|
# guard because not all script forms (e.g. `behavior`) are implemented
|
||||||
|
# and their parse/compile errors would crash the whole test.
|
||||||
|
for script in hs_scripts:
|
||||||
|
clean = clean_hs_script(script)
|
||||||
|
escaped = clean.replace('\\', '\\\\').replace('"', '\\"')
|
||||||
|
lines.append(
|
||||||
|
f' (guard (_e (true nil))'
|
||||||
|
f' (eval-expr-cek (hs-to-sx (hs-compile "{escaped}"))))'
|
||||||
|
)
|
||||||
|
|
||||||
bindings = [f'({var_names[i]} (dom-create-element "{el["tag"]}"))' for i, el in enumerate(elements)]
|
bindings = [f'({var_names[i]} (dom-create-element "{el["tag"]}"))' for i, el in enumerate(elements)]
|
||||||
lines.append(f' (let ({" ".join(bindings)})')
|
lines.append(f' (let ({" ".join(bindings)})')
|
||||||
|
|
||||||
@@ -974,7 +1108,10 @@ def js_val_to_sx(val):
|
|||||||
if val == 'false': return 'false'
|
if val == 'false': return 'false'
|
||||||
if val in ('null', 'undefined'): return 'nil'
|
if val in ('null', 'undefined'): return 'nil'
|
||||||
if val.startswith('"') or val.startswith("'"):
|
if val.startswith('"') or val.startswith("'"):
|
||||||
return '"' + val.strip("\"'") + '"'
|
return '"' + val.strip("\"'").replace('\\', '\\\\').replace('"', '\\"') + '"'
|
||||||
|
if val.startswith('`') and val.endswith('`'):
|
||||||
|
inner = val[1:-1]
|
||||||
|
return '"' + inner.replace('\\', '\\\\').replace('"', '\\"') + '"'
|
||||||
# Arrays: [1, 2, 3] → (list 1 2 3)
|
# Arrays: [1, 2, 3] → (list 1 2 3)
|
||||||
if val.startswith('[') and val.endswith(']'):
|
if val.startswith('[') and val.endswith(']'):
|
||||||
inner = val[1:-1].strip()
|
inner = val[1:-1].strip()
|
||||||
@@ -1006,7 +1143,8 @@ def js_val_to_sx(val):
|
|||||||
float(val)
|
float(val)
|
||||||
return val
|
return val
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return f'"{val}"'
|
escaped = val.replace('\\', '\\\\').replace('"', '\\"')
|
||||||
|
return f'"{escaped}"'
|
||||||
|
|
||||||
|
|
||||||
def split_top_level(s):
|
def split_top_level(s):
|
||||||
@@ -1252,6 +1390,43 @@ def split_top_level_chars(s, sep_char):
|
|||||||
return parts
|
return parts
|
||||||
|
|
||||||
|
|
||||||
|
def _js_window_expr_to_sx(expr):
|
||||||
|
"""Translate a narrow slice of JS into SX for the `make`-style tests.
|
||||||
|
|
||||||
|
Only patterns that read state stored on `window` by a prior run() call.
|
||||||
|
Returns None if the shape isn't covered.
|
||||||
|
"""
|
||||||
|
expr = expr.strip().rstrip(';').strip()
|
||||||
|
|
||||||
|
# `window.X instanceof TYPE` or `window['X'] instanceof TYPE` — check non-null.
|
||||||
|
m = re.match(r"window(?:\.(\w+)|\[\s*['\"]([^'\"]+)['\"]\s*\])\s+instanceof\s+\w+$", expr)
|
||||||
|
if m:
|
||||||
|
key = m.group(1) or m.group(2)
|
||||||
|
return f'(not (nil? (host-get (host-global "window") "{key}")))'
|
||||||
|
|
||||||
|
# `window.X.classList.contains("Y")` → dom-has-class?
|
||||||
|
m = re.match(
|
||||||
|
r"window(?:\.(\w+)|\[\s*['\"]([^'\"]+)['\"]\s*\])\.classList\.contains\(\s*['\"]([^'\"]+)['\"]\s*\)$",
|
||||||
|
expr,
|
||||||
|
)
|
||||||
|
if m:
|
||||||
|
key = m.group(1) or m.group(2)
|
||||||
|
cls = m.group(3)
|
||||||
|
return f'(dom-has-class? (host-get (host-global "window") "{key}") "{cls}")'
|
||||||
|
|
||||||
|
# `window.X.Y.Z...` or `window['X'].Y.Z` — chained host-get.
|
||||||
|
m = re.match(r"window(?:\.(\w+)|\[\s*['\"]([^'\"]+)['\"]\s*\])((?:\.\w+)*)$", expr)
|
||||||
|
if m:
|
||||||
|
key = m.group(1) or m.group(2)
|
||||||
|
rest = m.group(3) or ''
|
||||||
|
sx = f'(host-get (host-global "window") "{key}")'
|
||||||
|
for prop in re.findall(r'\.(\w+)', rest):
|
||||||
|
sx = f'(host-get {sx} "{prop}")'
|
||||||
|
return sx
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def extract_hs_expr(raw):
|
def extract_hs_expr(raw):
|
||||||
"""Clean a HS expression extracted from run() call."""
|
"""Clean a HS expression extracted from run() call."""
|
||||||
# Remove surrounding whitespace and newlines
|
# Remove surrounding whitespace and newlines
|
||||||
@@ -1382,12 +1557,108 @@ def generate_eval_only_test(test, idx):
|
|||||||
obj_str = re.sub(r'\s+', ' ', m.group(3)).strip()
|
obj_str = re.sub(r'\s+', ' ', m.group(3)).strip()
|
||||||
assertions.append(f' ;; TODO: assert= (eval-hs "{hs_expr}") against {obj_str}')
|
assertions.append(f' ;; TODO: assert= (eval-hs "{hs_expr}") against {obj_str}')
|
||||||
|
|
||||||
|
# Pattern 2-values: DOM-constructing evaluate returning _hyperscript result.
|
||||||
|
# const result = await evaluate(() => {
|
||||||
|
# const node = document.createElement("<tag>")
|
||||||
|
# node.innerHTML = `<html>` (or direct property assignments)
|
||||||
|
# return _hyperscript("<hs-expr>", { locals: { <name>: node } })
|
||||||
|
# })
|
||||||
|
# expect(result.<prop>).toBe/toEqual(<val>)
|
||||||
|
if not assertions:
|
||||||
|
pv = re.search(
|
||||||
|
r'const\s+\w+\s*=\s*(?:await\s+)?evaluate\(\s*\(\)\s*=>\s*\{'
|
||||||
|
r'(.*?)'
|
||||||
|
r'return\s+_hyperscript\(\s*(["\x27`])(.+?)\2'
|
||||||
|
r'(?:\s*,\s*\{\s*locals:\s*\{\s*(\w+)\s*:\s*(\w+)\s*\}\s*\})?'
|
||||||
|
r'\s*\)\s*\}\s*\)\s*;?',
|
||||||
|
body, re.DOTALL,
|
||||||
|
)
|
||||||
|
if pv:
|
||||||
|
setup_block = pv.group(1)
|
||||||
|
hs_src = extract_hs_expr(pv.group(3))
|
||||||
|
local_name = pv.group(4)
|
||||||
|
# node variable from createElement
|
||||||
|
cm = re.search(
|
||||||
|
r'const\s+(\w+)\s*=\s*document\.createElement\(\s*["\x27](\w+)["\x27]\s*\)',
|
||||||
|
setup_block,
|
||||||
|
)
|
||||||
|
if cm:
|
||||||
|
node_tag = cm.group(2)
|
||||||
|
setup_lines = [f'(let ((_node (dom-create-element "{node_tag}")))']
|
||||||
|
# node.innerHTML = `...`
|
||||||
|
ih = re.search(
|
||||||
|
r'\w+\.innerHTML\s*=\s*(["\x27`])((?:\\.|[^\\])*?)\1',
|
||||||
|
setup_block, re.DOTALL,
|
||||||
|
)
|
||||||
|
if ih:
|
||||||
|
raw = ih.group(2)
|
||||||
|
clean = re.sub(r'\s+', ' ', raw).strip()
|
||||||
|
esc = clean.replace('\\', '\\\\').replace('"', '\\"')
|
||||||
|
setup_lines.append(f' (dom-set-inner-html _node "{esc}")')
|
||||||
|
# node.prop = val (e.g. node.name = "x", node.value = "y")
|
||||||
|
for pm in re.finditer(
|
||||||
|
r'\w+\.(\w+)\s*=\s*(["\x27])(.*?)\2\s*;?', setup_block, re.DOTALL,
|
||||||
|
):
|
||||||
|
prop = pm.group(1)
|
||||||
|
if prop == 'innerHTML':
|
||||||
|
continue
|
||||||
|
val = pm.group(3).replace('\\', '\\\\').replace('"', '\\"')
|
||||||
|
setup_lines.append(f' (host-set! _node "{prop}" "{val}")')
|
||||||
|
# Collect post-return expressions that modify node (e.g. `select.value = 'cat'`)
|
||||||
|
# We cover the simple `var select = node.querySelector("select")`
|
||||||
|
# followed by `select.value = "X"` pattern.
|
||||||
|
local_sx = (
|
||||||
|
'(list '
|
||||||
|
+ (f'(list (quote {local_name}) _node)' if local_name else '')
|
||||||
|
+ ')'
|
||||||
|
)
|
||||||
|
call = f'(eval-hs-locals "{hs_src}" {local_sx})' if local_name else f'(eval-hs "{hs_src}")'
|
||||||
|
setup_lines.append(f' (let ((_result {call}))')
|
||||||
|
|
||||||
|
# Find expect assertions tied to `result`. Allow hyphens in
|
||||||
|
# bracket keys (e.g. result["test-name"]) and numeric index
|
||||||
|
# access (result.gender[0]).
|
||||||
|
extra = []
|
||||||
|
for em in re.finditer(
|
||||||
|
r'expect\(\s*result'
|
||||||
|
r'(?:\.(\w+)(?:\[(\d+)\])?'
|
||||||
|
r'|\[\s*["\x27]([\w-]+)["\x27]\s*\](?:\[(\d+)\])?)?'
|
||||||
|
r'\s*\)\.(toBe|toEqual)\(([^)]+)\)',
|
||||||
|
body,
|
||||||
|
):
|
||||||
|
key = em.group(1) or em.group(3)
|
||||||
|
idx = em.group(2) or em.group(4)
|
||||||
|
val_raw = em.group(6).strip()
|
||||||
|
target = '_result' if not key else f'(host-get _result "{key}")'
|
||||||
|
if idx is not None:
|
||||||
|
target = f'(nth {target} {idx})'
|
||||||
|
expected_sx = js_val_to_sx(val_raw)
|
||||||
|
extra.append(f' (assert= {target} {expected_sx})')
|
||||||
|
# Also handle toEqual([list]) where the regex's [^)] stops
|
||||||
|
# at the first `]` inside the brackets. Re-scan for arrays.
|
||||||
|
for em in re.finditer(
|
||||||
|
r'expect\(\s*result(?:\.(\w+)|\[\s*["\x27]([\w-]+)["\x27]\s*\])?\s*\)\.toEqual\((\[.*?\])\)',
|
||||||
|
body, re.DOTALL,
|
||||||
|
):
|
||||||
|
key = em.group(1) or em.group(2)
|
||||||
|
target = '_result' if not key else f'(host-get _result "{key}")'
|
||||||
|
expected_sx = js_val_to_sx(em.group(3))
|
||||||
|
extra.append(f' (assert= {target} {expected_sx})')
|
||||||
|
if extra:
|
||||||
|
for a in extra:
|
||||||
|
setup_lines.append(a)
|
||||||
|
setup_lines.append(' ))')
|
||||||
|
assertions.append(' ' + '\n '.join(setup_lines))
|
||||||
|
|
||||||
# Pattern 2: var result = await run(`expr`, opts); expect(result...).toBe/toEqual(val)
|
# Pattern 2: var result = await run(`expr`, opts); expect(result...).toBe/toEqual(val)
|
||||||
# Reassignments are common (`result = await run(...)` repeated for multiple
|
# Reassignments are common (`result = await run(...)` repeated for multiple
|
||||||
# checks). Walk the body in order, pairing each expect(result) with the
|
# checks). Walk the body in order, pairing each expect(result) with the
|
||||||
# most recent preceding run().
|
# most recent preceding run().
|
||||||
if not assertions:
|
if not assertions:
|
||||||
decl_match = re.search(r'(?:var|let|const)\s+(\w+)', body)
|
# Only match when the declared var is actually bound to a run() call —
|
||||||
|
# otherwise tests that bind to `evaluate(...)` (e.g. window-mutating
|
||||||
|
# make tests) would be mis-paired to the run() return value.
|
||||||
|
decl_match = re.search(r'(?:var|let|const)\s+(\w+)\s*=\s*(?:await\s+)?run\(', body)
|
||||||
if decl_match:
|
if decl_match:
|
||||||
var_name = decl_match.group(1)
|
var_name = decl_match.group(1)
|
||||||
# Find every run() occurrence (with or without var = prefix), and
|
# Find every run() occurrence (with or without var = prefix), and
|
||||||
@@ -1586,6 +1857,61 @@ def generate_eval_only_test(test, idx):
|
|||||||
expected_sx = js_val_to_sx(be_match.group(1))
|
expected_sx = js_val_to_sx(be_match.group(1))
|
||||||
assertions.append(f' (assert= (eval-hs "{hs_expr}") {expected_sx})')
|
assertions.append(f' (assert= (eval-hs "{hs_expr}") {expected_sx})')
|
||||||
|
|
||||||
|
# Pattern 2e: run() with side-effects on window, checked via
|
||||||
|
# const X = await evaluate(() => <js-expr>); expect(X).toBe(val)
|
||||||
|
# The const holds the evaluated JS expr, not the run() return value,
|
||||||
|
# so we need to translate <js-expr> into SX and assert against that.
|
||||||
|
if not assertions:
|
||||||
|
run_iter = list(re.finditer(
|
||||||
|
r'(?:await\s+)?run\((?:String\.raw)?(' + _Q + r')(.+?)\1' + _RUN_ARGS + r'\)',
|
||||||
|
body, re.DOTALL,
|
||||||
|
))
|
||||||
|
if run_iter:
|
||||||
|
# Map `const X = await evaluate(() => EXPR)` assignments by name.
|
||||||
|
eval_binds = {}
|
||||||
|
for em in re.finditer(
|
||||||
|
r'(?:var|let|const)\s+(\w+)\s*=\s*(?:await\s+)?evaluate\('
|
||||||
|
r'\s*\(\)\s*=>\s*(.+?)\)\s*;',
|
||||||
|
body, re.DOTALL,
|
||||||
|
):
|
||||||
|
eval_binds[em.group(1)] = em.group(2).strip()
|
||||||
|
|
||||||
|
# Inline pattern: expect(await evaluate(() => EXPR)).toBe(val)
|
||||||
|
inline_matches = list(re.finditer(
|
||||||
|
r'expect\(\s*(?:await\s+)?evaluate\(\s*\(\)\s*=>\s*(.+?)\)\s*\)'
|
||||||
|
r'\s*\.toBe\(([^)]+)\)',
|
||||||
|
body, re.DOTALL,
|
||||||
|
))
|
||||||
|
|
||||||
|
name_matches = list(re.finditer(
|
||||||
|
r'expect\((\w+)\)\.toBe\(([^)]+)\)', body,
|
||||||
|
))
|
||||||
|
|
||||||
|
hs_exprs_emitted = set()
|
||||||
|
for rm in run_iter:
|
||||||
|
hs_src = extract_hs_expr(rm.group(2))
|
||||||
|
if hs_src in hs_exprs_emitted:
|
||||||
|
continue
|
||||||
|
hs_exprs_emitted.add(hs_src)
|
||||||
|
assertions.append(f' (eval-hs "{hs_src}")')
|
||||||
|
|
||||||
|
for em in inline_matches:
|
||||||
|
sx_expr = _js_window_expr_to_sx(em.group(1).strip())
|
||||||
|
if sx_expr is None:
|
||||||
|
continue
|
||||||
|
expected_sx = js_val_to_sx(em.group(2))
|
||||||
|
assertions.append(f' (assert= {sx_expr} {expected_sx})')
|
||||||
|
|
||||||
|
for em in name_matches:
|
||||||
|
name = em.group(1)
|
||||||
|
if name not in eval_binds:
|
||||||
|
continue
|
||||||
|
sx_expr = _js_window_expr_to_sx(eval_binds[name])
|
||||||
|
if sx_expr is None:
|
||||||
|
continue
|
||||||
|
expected_sx = js_val_to_sx(em.group(2))
|
||||||
|
assertions.append(f' (assert= {sx_expr} {expected_sx})')
|
||||||
|
|
||||||
# Pattern 3: toThrow — expect(() => run("expr")).toThrow()
|
# Pattern 3: toThrow — expect(() => run("expr")).toThrow()
|
||||||
for m in re.finditer(
|
for m in re.finditer(
|
||||||
r'run\((?:String\.raw)?(["\x27`])(.+?)\1\).*?\.toThrow\(\)',
|
r'run\((?:String\.raw)?(["\x27`])(.+?)\1\).*?\.toThrow\(\)',
|
||||||
|
|||||||
Reference in New Issue
Block a user