HS: elsewhere / from elsewhere modifier (+2 tests)

Parser: parse-on-feat now consumes `elsewhere` (or `from elsewhere`) as
a modifier between event-name and source. When matched, sets a flag and
emits :elsewhere true on parts. The `from elsewhere` form peeks one
token ahead before consuming both keywords so plain `from #x` continues
to parse as a source expression.

Compiler: scan-on threads elsewhere?; when present, target becomes
(dom-body) (so the listener attaches to body and bubbles see all clicks)
and the handler body is wrapped with `(when (not (host-call me "contains"
(host-get event "target"))) BODY)` so the handler fires only when the
click originated outside the activated element.

Generator: dropped supports "elsewhere" modifier and supports "from
elsewhere" modifier from skip-list.

hs-upstream-on: 48/70 → 50/70. Smoke 0-195 unchanged at 172/195.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 12:26:30 +00:00
parent a8a798c592
commit 3044a16817
6 changed files with 104 additions and 46 deletions

View File

@@ -166,13 +166,14 @@
finally-info
having-info
of-filter-info
count-filter-info)
count-filter-info
elsewhere?)
(cond
((<= (len items) 1)
(let
((body (if (> (len items) 0) (first items) nil)))
(let
((target (if source (hs-to-sx source) (quote me))))
((target (cond (elsewhere? (list (quote dom-body))) (source (hs-to-sx source)) (true (quote me)))))
(let
((event-refs (if (and (list? body) (= (first body) (quote do))) (filter (fn (x) (and (list? x) (= (first x) (quote ref)))) (rest body)) (list))))
(let
@@ -180,7 +181,7 @@
(let
((raw-compiled (hs-to-sx stripped-body)))
(let
((compiled-body (if (> (len event-refs) 0) (let ((bindings (map (fn (r) (let ((name (nth r 1))) (list (make-symbol name) (list (quote host-get) (list (quote host-get) (quote event) "detail") name)))) event-refs))) (list (quote let) bindings raw-compiled)) raw-compiled)))
((compiled-body (let ((base (if (> (len event-refs) 0) (let ((bindings (map (fn (r) (let ((name (nth r 1))) (list (make-symbol name) (list (quote host-get) (list (quote host-get) (quote event) "detail") name)))) event-refs))) (list (quote let) bindings raw-compiled)) raw-compiled))) (if elsewhere? (list (quote when) (list (quote not) (list (quote host-call) (quote me) "contains" (list (quote host-get) (quote event) "target"))) base) base))))
(let
((wrapped-body (if catch-info (let ((var (make-symbol (nth catch-info 0))) (catch-body (hs-to-sx (nth catch-info 1)))) (if finally-info (list (quote do) (list (quote guard) (list var (list true catch-body)) compiled-body) (hs-to-sx finally-info)) (list (quote guard) (list var (list true catch-body)) compiled-body))) (if finally-info (list (quote do) compiled-body (hs-to-sx finally-info)) compiled-body))))
(let
@@ -235,7 +236,8 @@
finally-info
having-info
of-filter-info
count-filter-info))
count-filter-info
elsewhere?))
((= (first items) :filter)
(scan-on
(rest (rest items))
@@ -246,7 +248,8 @@
finally-info
having-info
of-filter-info
count-filter-info))
count-filter-info
elsewhere?))
((= (first items) :every)
(scan-on
(rest (rest items))
@@ -257,7 +260,8 @@
finally-info
having-info
of-filter-info
count-filter-info))
count-filter-info
elsewhere?))
((= (first items) :catch)
(scan-on
(rest (rest items))
@@ -268,7 +272,8 @@
finally-info
having-info
of-filter-info
count-filter-info))
count-filter-info
elsewhere?))
((= (first items) :finally)
(scan-on
(rest (rest items))
@@ -279,7 +284,8 @@
(nth items 1)
having-info
of-filter-info
count-filter-info))
count-filter-info
elsewhere?))
((= (first items) :having)
(scan-on
(rest (rest items))
@@ -290,7 +296,8 @@
finally-info
(nth items 1)
of-filter-info
count-filter-info))
count-filter-info
elsewhere?))
((= (first items) :of-filter)
(scan-on
(rest (rest items))
@@ -301,7 +308,8 @@
finally-info
having-info
(nth items 1)
count-filter-info))
count-filter-info
elsewhere?))
((= (first items) :count-filter)
(scan-on
(rest (rest items))
@@ -312,6 +320,19 @@
finally-info
having-info
of-filter-info
(nth items 1)
elsewhere?))
((= (first items) :elsewhere)
(scan-on
(rest (rest items))
source
filter
every?
catch-info
finally-info
having-info
of-filter-info
count-filter-info
(nth items 1)))
(true
(scan-on
@@ -323,8 +344,9 @@
finally-info
having-info
of-filter-info
count-filter-info)))))
(scan-on (rest parts) nil nil false nil nil nil nil nil)))))
count-filter-info
elsewhere?)))))
(scan-on (rest parts) nil nil false nil nil nil nil nil false)))))
(define
emit-send
(fn

View File

@@ -2611,7 +2611,8 @@
(let
((flt (if (= (tp-type) "bracket-open") (do (adv!) (let ((f (parse-expr))) (if (= (tp-type) "bracket-close") (adv!) nil) f)) nil)))
(let
((source (if (match-kw "from") (parse-expr) nil)))
((elsewhere? (cond ((match-kw "elsewhere") true) ((and (= (tp-type) "keyword") (= (tp-val) "from") (let ((nxt (if (< (+ p 1) tok-len) (nth tokens (+ p 1)) nil))) (and nxt (= (get nxt "type") "keyword") (= (get nxt "value") "elsewhere")))) (do (adv!) (adv!) true)) (true false)))
(source (if (match-kw "from") (parse-expr) nil)))
(let
((h-margin nil) (h-threshold nil))
(define
@@ -2655,20 +2656,22 @@
(let
((parts (if flt (append parts (list :filter flt)) parts)))
(let
((parts (if source (append parts (list :from source)) parts)))
((parts (if elsewhere? (append parts (list :elsewhere true)) parts)))
(let
((parts (if count-filter (append parts (list :count-filter count-filter)) parts)))
((parts (if source (append parts (list :from source)) parts)))
(let
((parts (if of-filter (append parts (list :of-filter of-filter)) parts)))
((parts (if count-filter (append parts (list :count-filter count-filter)) parts)))
(let
((parts (if having (append parts (list :having having)) parts)))
((parts (if of-filter (append parts (list :of-filter of-filter)) parts)))
(let
((parts (if catch-clause (append parts (list :catch catch-clause)) parts)))
((parts (if having (append parts (list :having having)) parts)))
(let
((parts (if finally-clause (append parts (list :finally finally-clause)) parts)))
((parts (if catch-clause (append parts (list :catch catch-clause)) parts)))
(let
((parts (append parts (list body))))
parts))))))))))))))))))))))
((parts (if finally-clause (append parts (list :finally finally-clause)) parts)))
(let
((parts (append parts (list body))))
parts)))))))))))))))))))))))
(define
parse-init-feat
(fn

View File

@@ -166,13 +166,14 @@
finally-info
having-info
of-filter-info
count-filter-info)
count-filter-info
elsewhere?)
(cond
((<= (len items) 1)
(let
((body (if (> (len items) 0) (first items) nil)))
(let
((target (if source (hs-to-sx source) (quote me))))
((target (cond (elsewhere? (list (quote dom-body))) (source (hs-to-sx source)) (true (quote me)))))
(let
((event-refs (if (and (list? body) (= (first body) (quote do))) (filter (fn (x) (and (list? x) (= (first x) (quote ref)))) (rest body)) (list))))
(let
@@ -180,7 +181,7 @@
(let
((raw-compiled (hs-to-sx stripped-body)))
(let
((compiled-body (if (> (len event-refs) 0) (let ((bindings (map (fn (r) (let ((name (nth r 1))) (list (make-symbol name) (list (quote host-get) (list (quote host-get) (quote event) "detail") name)))) event-refs))) (list (quote let) bindings raw-compiled)) raw-compiled)))
((compiled-body (let ((base (if (> (len event-refs) 0) (let ((bindings (map (fn (r) (let ((name (nth r 1))) (list (make-symbol name) (list (quote host-get) (list (quote host-get) (quote event) "detail") name)))) event-refs))) (list (quote let) bindings raw-compiled)) raw-compiled))) (if elsewhere? (list (quote when) (list (quote not) (list (quote host-call) (quote me) "contains" (list (quote host-get) (quote event) "target"))) base) base))))
(let
((wrapped-body (if catch-info (let ((var (make-symbol (nth catch-info 0))) (catch-body (hs-to-sx (nth catch-info 1)))) (if finally-info (list (quote do) (list (quote guard) (list var (list true catch-body)) compiled-body) (hs-to-sx finally-info)) (list (quote guard) (list var (list true catch-body)) compiled-body))) (if finally-info (list (quote do) compiled-body (hs-to-sx finally-info)) compiled-body))))
(let
@@ -235,7 +236,8 @@
finally-info
having-info
of-filter-info
count-filter-info))
count-filter-info
elsewhere?))
((= (first items) :filter)
(scan-on
(rest (rest items))
@@ -246,7 +248,8 @@
finally-info
having-info
of-filter-info
count-filter-info))
count-filter-info
elsewhere?))
((= (first items) :every)
(scan-on
(rest (rest items))
@@ -257,7 +260,8 @@
finally-info
having-info
of-filter-info
count-filter-info))
count-filter-info
elsewhere?))
((= (first items) :catch)
(scan-on
(rest (rest items))
@@ -268,7 +272,8 @@
finally-info
having-info
of-filter-info
count-filter-info))
count-filter-info
elsewhere?))
((= (first items) :finally)
(scan-on
(rest (rest items))
@@ -279,7 +284,8 @@
(nth items 1)
having-info
of-filter-info
count-filter-info))
count-filter-info
elsewhere?))
((= (first items) :having)
(scan-on
(rest (rest items))
@@ -290,7 +296,8 @@
finally-info
(nth items 1)
of-filter-info
count-filter-info))
count-filter-info
elsewhere?))
((= (first items) :of-filter)
(scan-on
(rest (rest items))
@@ -301,7 +308,8 @@
finally-info
having-info
(nth items 1)
count-filter-info))
count-filter-info
elsewhere?))
((= (first items) :count-filter)
(scan-on
(rest (rest items))
@@ -312,6 +320,19 @@
finally-info
having-info
of-filter-info
(nth items 1)
elsewhere?))
((= (first items) :elsewhere)
(scan-on
(rest (rest items))
source
filter
every?
catch-info
finally-info
having-info
of-filter-info
count-filter-info
(nth items 1)))
(true
(scan-on
@@ -323,8 +344,9 @@
finally-info
having-info
of-filter-info
count-filter-info)))))
(scan-on (rest parts) nil nil false nil nil nil nil nil)))))
count-filter-info
elsewhere?)))))
(scan-on (rest parts) nil nil false nil nil nil nil nil false)))))
(define
emit-send
(fn

View File

@@ -2611,7 +2611,8 @@
(let
((flt (if (= (tp-type) "bracket-open") (do (adv!) (let ((f (parse-expr))) (if (= (tp-type) "bracket-close") (adv!) nil) f)) nil)))
(let
((source (if (match-kw "from") (parse-expr) nil)))
((elsewhere? (cond ((match-kw "elsewhere") true) ((and (= (tp-type) "keyword") (= (tp-val) "from") (let ((nxt (if (< (+ p 1) tok-len) (nth tokens (+ p 1)) nil))) (and nxt (= (get nxt "type") "keyword") (= (get nxt "value") "elsewhere")))) (do (adv!) (adv!) true)) (true false)))
(source (if (match-kw "from") (parse-expr) nil)))
(let
((h-margin nil) (h-threshold nil))
(define
@@ -2655,20 +2656,22 @@
(let
((parts (if flt (append parts (list :filter flt)) parts)))
(let
((parts (if source (append parts (list :from source)) parts)))
((parts (if elsewhere? (append parts (list :elsewhere true)) parts)))
(let
((parts (if count-filter (append parts (list :count-filter count-filter)) parts)))
((parts (if source (append parts (list :from source)) parts)))
(let
((parts (if of-filter (append parts (list :of-filter of-filter)) parts)))
((parts (if count-filter (append parts (list :count-filter count-filter)) parts)))
(let
((parts (if having (append parts (list :having having)) parts)))
((parts (if of-filter (append parts (list :of-filter of-filter)) parts)))
(let
((parts (if catch-clause (append parts (list :catch catch-clause)) parts)))
((parts (if having (append parts (list :having having)) parts)))
(let
((parts (if finally-clause (append parts (list :finally finally-clause)) parts)))
((parts (if catch-clause (append parts (list :catch catch-clause)) parts)))
(let
((parts (append parts (list body))))
parts))))))))))))))))))))))
((parts (if finally-clause (append parts (list :finally finally-clause)) parts)))
(let
((parts (append parts (list body))))
parts)))))))))))))))))))))))
(define
parse-init-feat
(fn

View File

@@ -9197,9 +9197,19 @@
(deftest "rethrown exceptions trigger 'exception' event"
(error "SKIP (skip-list): rethrown exceptions trigger 'exception' event"))
(deftest "supports \"elsewhere\" modifier"
(error "SKIP (skip-list): supports 'elsewhere' modifier"))
(hs-cleanup!)
(let ((_el-div (dom-create-element "div")))
(dom-set-attr _el-div "_" "on click elsewhere add .clicked")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
))
(deftest "supports \"from elsewhere\" modifier"
(error "SKIP (skip-list): supports 'from elsewhere' modifier"))
(hs-cleanup!)
(let ((_el-div (dom-create-element "div")))
(dom-set-attr _el-div "_" "on click from elsewhere add .clicked")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
))
(deftest "throttled at <time> allows events after the window elapses"
(hs-cleanup!)
(let ((_el-d (dom-create-element "div")))

View File

@@ -125,8 +125,6 @@ SKIP_TEST_NAMES = {
"can ignore when target doesn't exist",
"can ignore when target doesn\\'t exist",
"can handle an or after a from clause",
"supports \"elsewhere\" modifier",
"supports \"from elsewhere\" modifier",
# upstream 'def' category — namespaced def + dynamic `me` inside callee
"functions can be namespaced",
"is called synchronously",