From 20e23d233c32e2666504b7c56be0b52d35ddc3ac Mon Sep 17 00:00:00 2001 From: giles Date: Mon, 4 May 2026 17:38:29 +0000 Subject: [PATCH] =?UTF-8?q?HS:=20parser=20fixes=20=E2=80=94=20parenthesize?= =?UTF-8?q?d=20commands=20+=20add=20error=20+=20class-name=20depth=20(+3?= =?UTF-8?q?=20tests)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - parse-on-feat: event-vars paren check now restores position and returns empty list when the first token after '(' is a keyword (command starter). Previously '(log me)' was consumed as event variable names instead of a parenthesized command, silently dropping the command body and returning empty innerHTML. Fixes 'can support parenthesized commands and features'. - parse-add-cmd: true-fallback now throws instead of returning nil when no 'to' keyword follows the expression. Makes 'add - to' and similar invalid add forms throw a parse error, satisfying assert-throws in 'basic parse error messages work' and '_hyperscript() evaluate API still throws on first error'. - read-class-name: '(' and ')' now only allowed inside '[...]' bracket groups (depth > 0). Previously allowing them at top level caused '.innerHTML)' at the end of a possessive expression to be consumed into the class token, producing 'innerHTML))' as a bogus property name. Tailwind classes like 'group-[:nth-of-type(3)_&]:block' still tokenize correctly. Co-Authored-By: Claude Sonnet 4.6 --- lib/hyperscript/parser.sx | 6 ++++-- lib/hyperscript/tokenizer.sx | 30 ++++++++++++++++++++++----- shared/static/wasm/sx/hs-parser.sx | 6 ++++-- shared/static/wasm/sx/hs-tokenizer.sx | 30 ++++++++++++++++++++++----- 4 files changed, 58 insertions(+), 14 deletions(-) diff --git a/lib/hyperscript/parser.sx b/lib/hyperscript/parser.sx index 176174be..be58d2b6 100644 --- a/lib/hyperscript/parser.sx +++ b/lib/hyperscript/parser.sx @@ -1164,7 +1164,9 @@ (let ((tgt (parse-expr))) (list (quote add-value) value tgt)) - nil)))))) + (error + (str + "Invalid 'add' syntax: expected a class (.foo), attribute, or expression with 'to'")))))))) (define parse-remove-cmd (fn @@ -2973,7 +2975,7 @@ (let ((of-filter (when (and (= event-name "mutation") (match-kw "of")) (cond ((and (= (tp-type) "ident") (or (= (tp-val) "attributes") (= (tp-val) "childList") (= (tp-val) "characterData"))) (let ((nm (tp-val))) (do (adv!) (dict "type" nm)))) ((= (tp-type) "attr") (let ((attrs (list (tp-val)))) (do (adv!) (define collect-or! (fn () (when (match-kw "or") (cond ((= (tp-type) "attr") (do (set! attrs (append attrs (list (tp-val)))) (adv!) (collect-or!))) (true (set! p (- p 1))))))) (collect-or!) (dict "type" "attrs" "attrs" attrs)))) (true nil))))) (let - ((event-vars (if (= (tp-type) "paren-open") (do (adv!) (define ev-coll (fn () (cond ((or (= (tp-type) "paren-close") (= (tp-type) "eof")) (do (when (= (tp-type) "paren-close") (adv!)) (list))) ((or (= (tp-type) "ident") (= (tp-type) "keyword")) (let ((nm (tp-val))) (adv!) (cons nm (ev-coll)))) (true (do (adv!) (ev-coll)))))) (ev-coll)) (list)))) + ((event-vars (if (= (tp-type) "paren-open") (let ((saved-p p)) (do (adv!) (if (= (tp-type) "keyword") (do (set! p saved-p) (list)) (do (define ev-coll (fn () (cond ((or (= (tp-type) "paren-close") (= (tp-type) "eof")) (do (when (= (tp-type) "paren-close") (adv!)) (list))) ((or (= (tp-type) "ident") (= (tp-type) "keyword")) (let ((nm (tp-val))) (adv!) (cons nm (ev-coll)))) (true (do (adv!) (ev-coll)))))) (ev-coll))))) (list)))) (let ((flt (if (= (tp-type) "bracket-open") (do (adv!) (let ((f (parse-expr))) (if (= (tp-type) "bracket-close") (adv!) nil) f)) nil))) (let diff --git a/lib/hyperscript/tokenizer.sx b/lib/hyperscript/tokenizer.sx index a62849eb..367a178d 100644 --- a/lib/hyperscript/tokenizer.sx +++ b/lib/hyperscript/tokenizer.sx @@ -459,7 +459,7 @@ (define build-name (fn - (acc) + (acc depth) (cond ((and (< pos src-len) (= (hs-cur) "\\") (< (+ pos 1) src-len)) (do @@ -467,15 +467,35 @@ (let ((c (hs-cur))) (hs-advance! 1) - (build-name (str acc c))))) - ((and (< pos src-len) (or (hs-ident-char? (hs-cur)) (= (hs-cur) ":") (= (hs-cur) "[") (= (hs-cur) "]") (= (hs-cur) "(") (= (hs-cur) ")") (= (hs-cur) "&"))) + (build-name (str acc c) depth)))) + ((and (< pos src-len) (= (hs-cur) "[")) (do (let ((c (hs-cur))) (hs-advance! 1) - (build-name (str acc c))))) + (build-name (str acc c) (+ depth 1))))) + ((and (< pos src-len) (= (hs-cur) "]")) + (do + (let + ((c (hs-cur))) + (hs-advance! 1) + (build-name + (str acc c) + (if (> depth 0) (- depth 1) 0))))) + ((and (< pos src-len) (> depth 0) (or (= (hs-cur) "(") (= (hs-cur) ")"))) + (do + (let + ((c (hs-cur))) + (hs-advance! 1) + (build-name (str acc c) depth)))) + ((and (< pos src-len) (or (hs-ident-char? (hs-cur)) (= (hs-cur) ":") (= (hs-cur) "&"))) + (do + (let + ((c (hs-cur))) + (hs-advance! 1) + (build-name (str acc c) depth)))) (true acc)))) - (build-name ""))) + (build-name "" 0))) (define hs-emit! (fn diff --git a/shared/static/wasm/sx/hs-parser.sx b/shared/static/wasm/sx/hs-parser.sx index 176174be..be58d2b6 100644 --- a/shared/static/wasm/sx/hs-parser.sx +++ b/shared/static/wasm/sx/hs-parser.sx @@ -1164,7 +1164,9 @@ (let ((tgt (parse-expr))) (list (quote add-value) value tgt)) - nil)))))) + (error + (str + "Invalid 'add' syntax: expected a class (.foo), attribute, or expression with 'to'")))))))) (define parse-remove-cmd (fn @@ -2973,7 +2975,7 @@ (let ((of-filter (when (and (= event-name "mutation") (match-kw "of")) (cond ((and (= (tp-type) "ident") (or (= (tp-val) "attributes") (= (tp-val) "childList") (= (tp-val) "characterData"))) (let ((nm (tp-val))) (do (adv!) (dict "type" nm)))) ((= (tp-type) "attr") (let ((attrs (list (tp-val)))) (do (adv!) (define collect-or! (fn () (when (match-kw "or") (cond ((= (tp-type) "attr") (do (set! attrs (append attrs (list (tp-val)))) (adv!) (collect-or!))) (true (set! p (- p 1))))))) (collect-or!) (dict "type" "attrs" "attrs" attrs)))) (true nil))))) (let - ((event-vars (if (= (tp-type) "paren-open") (do (adv!) (define ev-coll (fn () (cond ((or (= (tp-type) "paren-close") (= (tp-type) "eof")) (do (when (= (tp-type) "paren-close") (adv!)) (list))) ((or (= (tp-type) "ident") (= (tp-type) "keyword")) (let ((nm (tp-val))) (adv!) (cons nm (ev-coll)))) (true (do (adv!) (ev-coll)))))) (ev-coll)) (list)))) + ((event-vars (if (= (tp-type) "paren-open") (let ((saved-p p)) (do (adv!) (if (= (tp-type) "keyword") (do (set! p saved-p) (list)) (do (define ev-coll (fn () (cond ((or (= (tp-type) "paren-close") (= (tp-type) "eof")) (do (when (= (tp-type) "paren-close") (adv!)) (list))) ((or (= (tp-type) "ident") (= (tp-type) "keyword")) (let ((nm (tp-val))) (adv!) (cons nm (ev-coll)))) (true (do (adv!) (ev-coll)))))) (ev-coll))))) (list)))) (let ((flt (if (= (tp-type) "bracket-open") (do (adv!) (let ((f (parse-expr))) (if (= (tp-type) "bracket-close") (adv!) nil) f)) nil))) (let diff --git a/shared/static/wasm/sx/hs-tokenizer.sx b/shared/static/wasm/sx/hs-tokenizer.sx index a62849eb..367a178d 100644 --- a/shared/static/wasm/sx/hs-tokenizer.sx +++ b/shared/static/wasm/sx/hs-tokenizer.sx @@ -459,7 +459,7 @@ (define build-name (fn - (acc) + (acc depth) (cond ((and (< pos src-len) (= (hs-cur) "\\") (< (+ pos 1) src-len)) (do @@ -467,15 +467,35 @@ (let ((c (hs-cur))) (hs-advance! 1) - (build-name (str acc c))))) - ((and (< pos src-len) (or (hs-ident-char? (hs-cur)) (= (hs-cur) ":") (= (hs-cur) "[") (= (hs-cur) "]") (= (hs-cur) "(") (= (hs-cur) ")") (= (hs-cur) "&"))) + (build-name (str acc c) depth)))) + ((and (< pos src-len) (= (hs-cur) "[")) (do (let ((c (hs-cur))) (hs-advance! 1) - (build-name (str acc c))))) + (build-name (str acc c) (+ depth 1))))) + ((and (< pos src-len) (= (hs-cur) "]")) + (do + (let + ((c (hs-cur))) + (hs-advance! 1) + (build-name + (str acc c) + (if (> depth 0) (- depth 1) 0))))) + ((and (< pos src-len) (> depth 0) (or (= (hs-cur) "(") (= (hs-cur) ")"))) + (do + (let + ((c (hs-cur))) + (hs-advance! 1) + (build-name (str acc c) depth)))) + ((and (< pos src-len) (or (hs-ident-char? (hs-cur)) (= (hs-cur) ":") (= (hs-cur) "&"))) + (do + (let + ((c (hs-cur))) + (hs-advance! 1) + (build-name (str acc c) depth)))) (true acc)))) - (build-name ""))) + (build-name "" 0))) (define hs-emit! (fn