HS: add CSS template interpolation fix (+1 test)

${}{"val"} pattern in add {prop: ${}{"val"}} uses two consecutive brace
groups: empty ${} followed by {"val"} for the actual expression. The prior
fix called parse-expr when already at the brace-close of the empty group,
returning nil. New fix: detect empty ${} (brace-open then brace-close),
skip the close, then read the actual value from the following {…} block.
Also handles non-empty ${expr} directly as before.
Suite hs-upstream-add: 17/19 → 18/19. Smoke 0-195: 174/195 → 175/195.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 14:42:36 +00:00
parent a0bbf74c01
commit 5a76a04010
2 changed files with 276 additions and 84 deletions

View File

@@ -23,13 +23,14 @@
(define at-end? (fn () (or (>= p tok-len) (= (tp-type) "eof"))))
(define cur-start (fn () (if (< p tok-len) (get (tp) "pos") 0)))
(define cur-line (fn () (if (< p tok-len) (get (tp) "line") 1)))
(define prev-end (fn () (if (> p 0) (get (nth tokens (- p 1)) "end") 0)))
(define hs-ast-wrap
(fn (raw kind start end-pos line fields)
(if hs-span-mode
{:hs-ast true :kind kind :start start :end end-pos :line line
:src src :children raw :fields fields}
raw)))
(define
prev-end
(fn () (if (> p 0) (get (nth tokens (- p 1)) "end") 0)))
(define
hs-ast-wrap
(fn
(raw kind start end-pos line fields)
(if hs-span-mode {:children raw :end end-pos :kind kind :line line :src src :start start :hs-ast true :fields fields} raw)))
(define
match-kw
(fn
@@ -80,7 +81,11 @@
(base)
(let
((base-start (if (and (dict? base) (get base :hs-ast)) (get base :start) (cur-start)))
(base-line (if (and (dict? base) (get base :hs-ast)) (get base :line) (cur-line))))
(base-line
(if
(and (dict? base) (get base :hs-ast))
(get base :line)
(cur-line))))
(if
(and (= (tp-type) "class") (not (at-end?)))
(let
@@ -90,7 +95,11 @@
(parse-prop-chain
(hs-ast-wrap
(list (make-symbol ".") base prop)
"member" base-start (prev-end) base-line {:root base}))))
"member"
base-start
(prev-end)
base-line
{:root base}))))
(if
(= (tp-type) "paren-open")
(let
@@ -98,7 +107,11 @@
(parse-prop-chain
(hs-ast-wrap
(list (quote method-call) base args)
"call" base-start (prev-end) base-line {:root base})))
"call"
base-start
(prev-end)
base-line
{:root base})))
base)))))
(define
parse-trav
@@ -143,11 +156,23 @@
((typ (tp-type)) (val (tp-val)))
(cond
((= typ "number")
(let ((s (cur-start)) (l (cur-line)))
(do (adv!) (hs-ast-wrap (parse-dur val) "number" s (prev-end) l {}))))
(let
((s (cur-start)) (l (cur-line)))
(do
(adv!)
(hs-ast-wrap
(parse-dur val)
"number"
s
(prev-end)
l
{}))))
((= typ "string")
(let ((s (cur-start)) (l (cur-line)))
(do (adv!) (hs-ast-wrap val "string" s (prev-end) l {}))))
(let
((s (cur-start)) (l (cur-line)))
(do
(adv!)
(hs-ast-wrap val "string" s (prev-end) l {}))))
((= typ "template") (do (adv!) (list (quote template) val)))
((and (= typ "keyword") (= val "true")) (do (adv!) true))
((and (= typ "keyword") (= val "false")) (do (adv!) false))
@@ -212,10 +237,20 @@
((and (= typ "keyword") (= val "last"))
(do (adv!) (parse-pos-kw (quote last))))
((= typ "id")
(let ((s (cur-start)) (l (cur-line)))
(do (adv!) (hs-ast-wrap (list (quote query) (str "#" val)) "selector" s (prev-end) l {}))))
(let
((s (cur-start)) (l (cur-line)))
(do
(adv!)
(hs-ast-wrap
(list (quote query) (str "#" val))
"selector"
s
(prev-end)
l
{}))))
((= typ "selector")
(let ((s (cur-start)) (l (cur-line)))
(let
((s (cur-start)) (l (cur-line)))
(do
(adv!)
(hs-ast-wrap
@@ -226,9 +261,14 @@
(list
(quote query-scoped)
val
(parse-cmp (parse-arith (parse-poss (parse-atom))))))
(parse-cmp
(parse-arith (parse-poss (parse-atom))))))
(list (quote query) val))
"selector" s (prev-end) l {}))))
"selector"
s
(prev-end)
l
{}))))
((= typ "attr")
(do (adv!) (list (quote attr) val (list (quote me)))))
((= typ "style")
@@ -245,11 +285,29 @@
(adv!)
(list (quote dom-ref) name (list (quote me)))))))
((= typ "class")
(let ((s (cur-start)) (l (cur-line)))
(do (adv!) (hs-ast-wrap (list (quote query) (str "." val)) "selector" s (prev-end) l {}))))
(let
((s (cur-start)) (l (cur-line)))
(do
(adv!)
(hs-ast-wrap
(list (quote query) (str "." val))
"selector"
s
(prev-end)
l
{}))))
((= typ "ident")
(let ((s (cur-start)) (l (cur-line)))
(do (adv!) (hs-ast-wrap (list (quote ref) val) "ref" s (prev-end) l {}))))
(let
((s (cur-start)) (l (cur-line)))
(do
(adv!)
(hs-ast-wrap
(list (quote ref) val)
"ref"
s
(prev-end)
l
{}))))
((= typ "paren-open")
(do
(adv!)
@@ -970,8 +1028,7 @@
((prop (get (adv!) "value")))
(when (= (tp-type) "colon") (adv!))
(let
((val (tp-val)))
(adv!)
((val (if (and (= (tp-type) "ident") (= (tp-val) "$")) (do (adv!) (when (= (tp-type) "brace-open") (adv!)) (if (= (tp-type) "brace-close") (do (adv!) (if (= (tp-type) "brace-open") (do (adv!) (let ((inner (parse-expr))) (when (= (tp-type) "brace-close") (adv!)) inner)) "")) (let ((expr (parse-expr))) (when (= (tp-type) "brace-close") (adv!)) expr))) (get (adv!) "value"))))
(set! pairs (cons (list prop val) pairs))
(collect-pairs!))))))
(collect-pairs!)
@@ -2052,9 +2109,19 @@
((right (let ((a (parse-atom))) (if (nil? a) a (parse-poss a)))))
(let
((lhs-start (if (and (dict? left) (get left :hs-ast)) (get left :start) 0))
(lhs-line (if (and (dict? left) (get left :hs-ast)) (get left :line) 1)))
(lhs-line
(if
(and (dict? left) (get left :hs-ast))
(get left :line)
1)))
(parse-arith
(hs-ast-wrap (list op left right) "arith" lhs-start (prev-end) lhs-line {:lhs left :rhs right}))))))
(hs-ast-wrap
(list op left right)
"arith"
lhs-start
(prev-end)
lhs-line
{:rhs right :lhs left}))))))
left))))
(define
parse-the-expr
@@ -2454,15 +2521,21 @@
((and (= typ "keyword") (= val "put"))
(do (adv!) (parse-put-cmd)))
((and (= typ "keyword") (= val "if"))
(let ((s (cur-start)) (l (cur-line)))
(let
((s (cur-start)) (l (cur-line)))
(do
(adv!)
(let ((r (parse-if-cmd)))
(let ((tb (if (and (list? r) (> (len r) 2)) (nth r 2) nil)))
(hs-ast-wrap r "if" s (prev-end) l
(if tb
{:true-branch (if (and (list? tb) (= (first tb) (quote do))) (nth tb 1) tb)}
{})))))))
(let
((r (parse-if-cmd)))
(let
((tb (if (and (list? r) (> (len r) 2)) (nth r 2) nil)))
(hs-ast-wrap
r
"if"
s
(prev-end)
l
(if tb {:true-branch (if (and (list? tb) (= (first tb) (quote do))) (nth tb 1) tb)} {})))))))
((and (= typ "keyword") (= val "wait"))
(do (adv!) (parse-wait-cmd)))
((and (= typ "keyword") (= val "send"))
@@ -2470,8 +2543,17 @@
((and (= typ "keyword") (= val "trigger"))
(do (adv!) (parse-trigger-cmd)))
((and (= typ "keyword") (= val "log"))
(let ((s (cur-start)) (l (cur-line)))
(do (adv!) (hs-ast-wrap (parse-log-cmd) "cmd" s (prev-end) l {}))))
(let
((s (cur-start)) (l (cur-line)))
(do
(adv!)
(hs-ast-wrap
(parse-log-cmd)
"cmd"
s
(prev-end)
l
{}))))
((and (= typ "keyword") (= val "increment"))
(do (adv!) (parse-inc-cmd)))
((and (= typ "keyword") (= val "decrement"))
@@ -2511,8 +2593,17 @@
((and (= typ "keyword") (= val "tell"))
(do (adv!) (parse-tell-cmd)))
((and (= typ "keyword") (= val "for"))
(let ((s (cur-start)) (l (cur-line)))
(do (adv!) (hs-ast-wrap (parse-for-cmd) "cmd" s (prev-end) l {}))))
(let
((s (cur-start)) (l (cur-line)))
(do
(adv!)
(hs-ast-wrap
(parse-for-cmd)
"cmd"
s
(prev-end)
l
{}))))
((and (= typ "keyword") (= val "make"))
(do (adv!) (parse-make-cmd)))
((and (= typ "keyword") (= val "install"))
@@ -2642,10 +2733,13 @@
loop
(fn
(i)
(when (< i (- (len cmds-list) 1))
(when
(< i (- (len cmds-list) 1))
(let
((cur-node (nth cmds-list i)) (nxt-node (nth cmds-list (+ i 1))))
(when (and (dict? cur-node) (get cur-node :hs-ast))
((cur-node (nth cmds-list i))
(nxt-node (nth cmds-list (+ i 1))))
(when
(and (dict? cur-node) (get cur-node :hs-ast))
(dict-set! (get cur-node :fields) "next" nxt-node)))
(loop (+ i 1)))))
(loop 0)
@@ -2810,7 +2904,9 @@
((= val "behavior") (do (adv!) (parse-behavior-feat)))
((= val "live") (do (adv!) (parse-live-feat)))
((= val "when") (do (adv!) (parse-when-feat)))
((= val "worker") (error "worker plugin is not installed — see https://hyperscript.org/features/worker"))
((= val "worker")
(error
"worker plugin is not installed — see https://hyperscript.org/features/worker"))
(true (parse-cmd-list))))))
(define
coll-feats

View File

@@ -23,13 +23,14 @@
(define at-end? (fn () (or (>= p tok-len) (= (tp-type) "eof"))))
(define cur-start (fn () (if (< p tok-len) (get (tp) "pos") 0)))
(define cur-line (fn () (if (< p tok-len) (get (tp) "line") 1)))
(define prev-end (fn () (if (> p 0) (get (nth tokens (- p 1)) "end") 0)))
(define hs-ast-wrap
(fn (raw kind start end-pos line fields)
(if hs-span-mode
{:hs-ast true :kind kind :start start :end end-pos :line line
:src src :children raw :fields fields}
raw)))
(define
prev-end
(fn () (if (> p 0) (get (nth tokens (- p 1)) "end") 0)))
(define
hs-ast-wrap
(fn
(raw kind start end-pos line fields)
(if hs-span-mode {:children raw :end end-pos :kind kind :line line :src src :start start :hs-ast true :fields fields} raw)))
(define
match-kw
(fn
@@ -80,7 +81,11 @@
(base)
(let
((base-start (if (and (dict? base) (get base :hs-ast)) (get base :start) (cur-start)))
(base-line (if (and (dict? base) (get base :hs-ast)) (get base :line) (cur-line))))
(base-line
(if
(and (dict? base) (get base :hs-ast))
(get base :line)
(cur-line))))
(if
(and (= (tp-type) "class") (not (at-end?)))
(let
@@ -90,7 +95,11 @@
(parse-prop-chain
(hs-ast-wrap
(list (make-symbol ".") base prop)
"member" base-start (prev-end) base-line {:root base}))))
"member"
base-start
(prev-end)
base-line
{:root base}))))
(if
(= (tp-type) "paren-open")
(let
@@ -98,7 +107,11 @@
(parse-prop-chain
(hs-ast-wrap
(list (quote method-call) base args)
"call" base-start (prev-end) base-line {:root base})))
"call"
base-start
(prev-end)
base-line
{:root base})))
base)))))
(define
parse-trav
@@ -143,11 +156,23 @@
((typ (tp-type)) (val (tp-val)))
(cond
((= typ "number")
(let ((s (cur-start)) (l (cur-line)))
(do (adv!) (hs-ast-wrap (parse-dur val) "number" s (prev-end) l {}))))
(let
((s (cur-start)) (l (cur-line)))
(do
(adv!)
(hs-ast-wrap
(parse-dur val)
"number"
s
(prev-end)
l
{}))))
((= typ "string")
(let ((s (cur-start)) (l (cur-line)))
(do (adv!) (hs-ast-wrap val "string" s (prev-end) l {}))))
(let
((s (cur-start)) (l (cur-line)))
(do
(adv!)
(hs-ast-wrap val "string" s (prev-end) l {}))))
((= typ "template") (do (adv!) (list (quote template) val)))
((and (= typ "keyword") (= val "true")) (do (adv!) true))
((and (= typ "keyword") (= val "false")) (do (adv!) false))
@@ -212,10 +237,20 @@
((and (= typ "keyword") (= val "last"))
(do (adv!) (parse-pos-kw (quote last))))
((= typ "id")
(let ((s (cur-start)) (l (cur-line)))
(do (adv!) (hs-ast-wrap (list (quote query) (str "#" val)) "selector" s (prev-end) l {}))))
(let
((s (cur-start)) (l (cur-line)))
(do
(adv!)
(hs-ast-wrap
(list (quote query) (str "#" val))
"selector"
s
(prev-end)
l
{}))))
((= typ "selector")
(let ((s (cur-start)) (l (cur-line)))
(let
((s (cur-start)) (l (cur-line)))
(do
(adv!)
(hs-ast-wrap
@@ -226,9 +261,14 @@
(list
(quote query-scoped)
val
(parse-cmp (parse-arith (parse-poss (parse-atom))))))
(parse-cmp
(parse-arith (parse-poss (parse-atom))))))
(list (quote query) val))
"selector" s (prev-end) l {}))))
"selector"
s
(prev-end)
l
{}))))
((= typ "attr")
(do (adv!) (list (quote attr) val (list (quote me)))))
((= typ "style")
@@ -245,11 +285,29 @@
(adv!)
(list (quote dom-ref) name (list (quote me)))))))
((= typ "class")
(let ((s (cur-start)) (l (cur-line)))
(do (adv!) (hs-ast-wrap (list (quote query) (str "." val)) "selector" s (prev-end) l {}))))
(let
((s (cur-start)) (l (cur-line)))
(do
(adv!)
(hs-ast-wrap
(list (quote query) (str "." val))
"selector"
s
(prev-end)
l
{}))))
((= typ "ident")
(let ((s (cur-start)) (l (cur-line)))
(do (adv!) (hs-ast-wrap (list (quote ref) val) "ref" s (prev-end) l {}))))
(let
((s (cur-start)) (l (cur-line)))
(do
(adv!)
(hs-ast-wrap
(list (quote ref) val)
"ref"
s
(prev-end)
l
{}))))
((= typ "paren-open")
(do
(adv!)
@@ -970,8 +1028,7 @@
((prop (get (adv!) "value")))
(when (= (tp-type) "colon") (adv!))
(let
((val (tp-val)))
(adv!)
((val (if (and (= (tp-type) "ident") (= (tp-val) "$")) (do (adv!) (when (= (tp-type) "brace-open") (adv!)) (if (= (tp-type) "brace-close") (do (adv!) (if (= (tp-type) "brace-open") (do (adv!) (let ((inner (parse-expr))) (when (= (tp-type) "brace-close") (adv!)) inner)) "")) (let ((expr (parse-expr))) (when (= (tp-type) "brace-close") (adv!)) expr))) (get (adv!) "value"))))
(set! pairs (cons (list prop val) pairs))
(collect-pairs!))))))
(collect-pairs!)
@@ -2052,9 +2109,19 @@
((right (let ((a (parse-atom))) (if (nil? a) a (parse-poss a)))))
(let
((lhs-start (if (and (dict? left) (get left :hs-ast)) (get left :start) 0))
(lhs-line (if (and (dict? left) (get left :hs-ast)) (get left :line) 1)))
(lhs-line
(if
(and (dict? left) (get left :hs-ast))
(get left :line)
1)))
(parse-arith
(hs-ast-wrap (list op left right) "arith" lhs-start (prev-end) lhs-line {:lhs left :rhs right}))))))
(hs-ast-wrap
(list op left right)
"arith"
lhs-start
(prev-end)
lhs-line
{:rhs right :lhs left}))))))
left))))
(define
parse-the-expr
@@ -2454,15 +2521,21 @@
((and (= typ "keyword") (= val "put"))
(do (adv!) (parse-put-cmd)))
((and (= typ "keyword") (= val "if"))
(let ((s (cur-start)) (l (cur-line)))
(let
((s (cur-start)) (l (cur-line)))
(do
(adv!)
(let ((r (parse-if-cmd)))
(let ((tb (if (and (list? r) (> (len r) 2)) (nth r 2) nil)))
(hs-ast-wrap r "if" s (prev-end) l
(if tb
{:true-branch (if (and (list? tb) (= (first tb) (quote do))) (nth tb 1) tb)}
{})))))))
(let
((r (parse-if-cmd)))
(let
((tb (if (and (list? r) (> (len r) 2)) (nth r 2) nil)))
(hs-ast-wrap
r
"if"
s
(prev-end)
l
(if tb {:true-branch (if (and (list? tb) (= (first tb) (quote do))) (nth tb 1) tb)} {})))))))
((and (= typ "keyword") (= val "wait"))
(do (adv!) (parse-wait-cmd)))
((and (= typ "keyword") (= val "send"))
@@ -2470,8 +2543,17 @@
((and (= typ "keyword") (= val "trigger"))
(do (adv!) (parse-trigger-cmd)))
((and (= typ "keyword") (= val "log"))
(let ((s (cur-start)) (l (cur-line)))
(do (adv!) (hs-ast-wrap (parse-log-cmd) "cmd" s (prev-end) l {}))))
(let
((s (cur-start)) (l (cur-line)))
(do
(adv!)
(hs-ast-wrap
(parse-log-cmd)
"cmd"
s
(prev-end)
l
{}))))
((and (= typ "keyword") (= val "increment"))
(do (adv!) (parse-inc-cmd)))
((and (= typ "keyword") (= val "decrement"))
@@ -2511,8 +2593,17 @@
((and (= typ "keyword") (= val "tell"))
(do (adv!) (parse-tell-cmd)))
((and (= typ "keyword") (= val "for"))
(let ((s (cur-start)) (l (cur-line)))
(do (adv!) (hs-ast-wrap (parse-for-cmd) "cmd" s (prev-end) l {}))))
(let
((s (cur-start)) (l (cur-line)))
(do
(adv!)
(hs-ast-wrap
(parse-for-cmd)
"cmd"
s
(prev-end)
l
{}))))
((and (= typ "keyword") (= val "make"))
(do (adv!) (parse-make-cmd)))
((and (= typ "keyword") (= val "install"))
@@ -2642,10 +2733,13 @@
loop
(fn
(i)
(when (< i (- (len cmds-list) 1))
(when
(< i (- (len cmds-list) 1))
(let
((cur-node (nth cmds-list i)) (nxt-node (nth cmds-list (+ i 1))))
(when (and (dict? cur-node) (get cur-node :hs-ast))
((cur-node (nth cmds-list i))
(nxt-node (nth cmds-list (+ i 1))))
(when
(and (dict? cur-node) (get cur-node :hs-ast))
(dict-set! (get cur-node :fields) "next" nxt-node)))
(loop (+ i 1)))))
(loop 0)
@@ -2810,7 +2904,9 @@
((= val "behavior") (do (adv!) (parse-behavior-feat)))
((= val "live") (do (adv!) (parse-live-feat)))
((= val "when") (do (adv!) (parse-when-feat)))
((= val "worker") (error "worker plugin is not installed — see https://hyperscript.org/features/worker"))
((= val "worker")
(error
"worker plugin is not installed — see https://hyperscript.org/features/worker"))
(true (parse-cmd-list))))))
(define
coll-feats