From beb120baf7cc958917292206819cf76e7faf1109 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 24 Apr 2026 06:25:58 +0000 Subject: [PATCH] HS: hide strategy config (+3 tests) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three parts: (a) `runtime.sx` hs-hide-one!/hs-show-one! consult a new `_hs-hide-strategies` dict (and `_hs-default-hide-strategy` override) before falling through to the built-in display/opacity/etc. cases. The strategy fn is called directly with (op, el, arg). New setters `hs-set-hide-strategies!` and `hs-set-default-hide-strategy!`. (b) `generate-sx-tests.py` `_hs_config_setup_ops` recognises `_hyperscript.config.defaultHideShowStrategy = "X"`, `delete …default…`, and `hideShowStrategies = { NAME: function (op, el, arg) { if … classList.add/remove } }` with brace-matched function body extraction. (c) Pre-setup emitter handles `__hs_config__` pseudo-name by emitting the SX expression as-is (not a window.X = Y assignment). Suite hs-upstream-hide: 12/16 → 15/16. Remaining test (`hide element then show element retains original display`) needs `on click 1 hide` / `on click 2 show` count-filtered events — separate feature. Smoke 0-195: 162/195 unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/hyperscript/runtime.sx | 102 ++++++++++++++-------- shared/static/wasm/sx/hs-runtime.sx | 102 ++++++++++++++-------- spec/tests/test-hyperscript-behavioral.sx | 11 +++ tests/playwright/generate-sx-tests.py | 78 ++++++++++++++++- 4 files changed, 224 insertions(+), 69 deletions(-) diff --git a/lib/hyperscript/runtime.sx b/lib/hyperscript/runtime.sx index eb7103c6..125a2e4b 100644 --- a/lib/hyperscript/runtime.sx +++ b/lib/hyperscript/runtime.sx @@ -1722,28 +1722,55 @@ (dom-set-prop el "open" false))))))) (begin + (define _hs-hide-strategies (dict)) + (define _hs-default-hide-strategy nil) + (define + hs-set-hide-strategies! + (fn + (strategies) + (for-each + (fn (k) (dict-set! _hs-hide-strategies k (get strategies k))) + (keys strategies)))) + (define + hs-set-default-hide-strategy! + (fn (name) (set! _hs-default-hide-strategy name))) + (define + _hs-resolve-strategy + (fn + (strategy) + (cond + ((and (= strategy "display") _hs-default-hide-strategy) + _hs-default-hide-strategy) + (true strategy)))) (define hs-hide-one! (fn (el strategy) (let - ((parts (split strategy ":")) (tag (dom-get-prop el "tagName"))) + ((resolved (_hs-resolve-strategy strategy))) (let - ((prop (first parts)) - (val (if (> (len parts) 1) (nth parts 1) nil))) - (cond - ((= tag "DIALOG") - (when (dom-has-attr? el "open") (host-call el "close"))) - ((= tag "DETAILS") (dom-set-prop el "open" false)) - ((= prop "opacity") - (dom-set-style el "opacity" (if val val "0"))) - ((= prop "visibility") - (dom-set-style el "visibility" (if val val "hidden"))) - ((= prop "hidden") (dom-set-attr el "hidden" "")) - ((= prop "twDisplay") (dom-add-class el "hidden")) - ((= prop "twVisibility") (dom-add-class el "invisible")) - ((= prop "twOpacity") (dom-add-class el "opacity-0")) - (true (dom-set-style el "display" (if val val "none")))))))) + ((parts (split resolved ":"))) + (let + ((prop (first parts)) + (val (if (> (len parts) 1) (nth parts 1) nil))) + (cond + ((and (not (= prop "display")) (not (= prop "opacity")) (not (= prop "visibility")) (not (= prop "hidden")) (not (= prop "class-hidden")) (not (= prop "class-invisible")) (not (= prop "class-opacity")) (not (= prop "details")) (not (= prop "dialog")) (dict-has? _hs-hide-strategies prop)) + (let + ((fn-val (get _hs-hide-strategies prop))) + (fn-val "hide" el val))) + ((= (dom-get-prop el "tagName") "DIALOG") + (when (dom-has-attr? el "open") (host-call el "close"))) + ((= (dom-get-prop el "tagName") "DETAILS") + (dom-set-prop el "open" false)) + ((= prop "opacity") + (dom-set-style el "opacity" (if val val "0"))) + ((= prop "visibility") + (dom-set-style el "visibility" (if val val "hidden"))) + ((= prop "hidden") (dom-set-attr el "hidden" "")) + ((= prop "class-hidden") (dom-add-class el "hidden")) + ((= prop "class-invisible") (dom-add-class el "invisible")) + ((= prop "class-opacity") (dom-add-class el "opacity-0")) + (true (dom-set-style el "display" (if val val "none"))))))))) (define hs-hide! (fn @@ -1759,25 +1786,32 @@ (fn (el strategy) (let - ((parts (split strategy ":")) (tag (dom-get-prop el "tagName"))) + ((resolved (_hs-resolve-strategy strategy))) (let - ((prop (first parts)) - (val (if (> (len parts) 1) (nth parts 1) nil))) - (cond - ((= tag "DIALOG") - (when - (not (dom-has-attr? el "open")) - (host-call el "showModal"))) - ((= tag "DETAILS") (dom-set-prop el "open" true)) - ((= prop "opacity") - (dom-set-style el "opacity" (if val val "1"))) - ((= prop "visibility") - (dom-set-style el "visibility" (if val val "visible"))) - ((= prop "hidden") (dom-remove-attr el "hidden")) - ((= prop "twDisplay") (dom-remove-class el "hidden")) - ((= prop "twVisibility") (dom-remove-class el "invisible")) - ((= prop "twOpacity") (dom-remove-class el "opacity-0")) - (true (dom-set-style el "display" (if val val "block")))))))) + ((parts (split resolved ":"))) + (let + ((prop (first parts)) + (val (if (> (len parts) 1) (nth parts 1) nil))) + (cond + ((and (not (= prop "display")) (not (= prop "opacity")) (not (= prop "visibility")) (not (= prop "hidden")) (not (= prop "class-hidden")) (not (= prop "class-invisible")) (not (= prop "class-opacity")) (not (= prop "details")) (not (= prop "dialog")) (dict-has? _hs-hide-strategies prop)) + (let + ((fn-val (get _hs-hide-strategies prop))) + (fn-val "show" el val))) + ((= (dom-get-prop el "tagName") "DIALOG") + (when + (not (dom-has-attr? el "open")) + (host-call el "showModal"))) + ((= (dom-get-prop el "tagName") "DETAILS") + (dom-set-prop el "open" true)) + ((= prop "opacity") + (dom-set-style el "opacity" (if val val "1"))) + ((= prop "visibility") + (dom-set-style el "visibility" (if val val "visible"))) + ((= prop "hidden") (dom-remove-attr el "hidden")) + ((= prop "class-hidden") (dom-remove-class el "hidden")) + ((= prop "class-invisible") (dom-remove-class el "invisible")) + ((= prop "class-opacity") (dom-remove-class el "opacity-0")) + (true (dom-set-style el "display" (if val val "block"))))))))) (define hs-show! (fn diff --git a/shared/static/wasm/sx/hs-runtime.sx b/shared/static/wasm/sx/hs-runtime.sx index eb7103c6..125a2e4b 100644 --- a/shared/static/wasm/sx/hs-runtime.sx +++ b/shared/static/wasm/sx/hs-runtime.sx @@ -1722,28 +1722,55 @@ (dom-set-prop el "open" false))))))) (begin + (define _hs-hide-strategies (dict)) + (define _hs-default-hide-strategy nil) + (define + hs-set-hide-strategies! + (fn + (strategies) + (for-each + (fn (k) (dict-set! _hs-hide-strategies k (get strategies k))) + (keys strategies)))) + (define + hs-set-default-hide-strategy! + (fn (name) (set! _hs-default-hide-strategy name))) + (define + _hs-resolve-strategy + (fn + (strategy) + (cond + ((and (= strategy "display") _hs-default-hide-strategy) + _hs-default-hide-strategy) + (true strategy)))) (define hs-hide-one! (fn (el strategy) (let - ((parts (split strategy ":")) (tag (dom-get-prop el "tagName"))) + ((resolved (_hs-resolve-strategy strategy))) (let - ((prop (first parts)) - (val (if (> (len parts) 1) (nth parts 1) nil))) - (cond - ((= tag "DIALOG") - (when (dom-has-attr? el "open") (host-call el "close"))) - ((= tag "DETAILS") (dom-set-prop el "open" false)) - ((= prop "opacity") - (dom-set-style el "opacity" (if val val "0"))) - ((= prop "visibility") - (dom-set-style el "visibility" (if val val "hidden"))) - ((= prop "hidden") (dom-set-attr el "hidden" "")) - ((= prop "twDisplay") (dom-add-class el "hidden")) - ((= prop "twVisibility") (dom-add-class el "invisible")) - ((= prop "twOpacity") (dom-add-class el "opacity-0")) - (true (dom-set-style el "display" (if val val "none")))))))) + ((parts (split resolved ":"))) + (let + ((prop (first parts)) + (val (if (> (len parts) 1) (nth parts 1) nil))) + (cond + ((and (not (= prop "display")) (not (= prop "opacity")) (not (= prop "visibility")) (not (= prop "hidden")) (not (= prop "class-hidden")) (not (= prop "class-invisible")) (not (= prop "class-opacity")) (not (= prop "details")) (not (= prop "dialog")) (dict-has? _hs-hide-strategies prop)) + (let + ((fn-val (get _hs-hide-strategies prop))) + (fn-val "hide" el val))) + ((= (dom-get-prop el "tagName") "DIALOG") + (when (dom-has-attr? el "open") (host-call el "close"))) + ((= (dom-get-prop el "tagName") "DETAILS") + (dom-set-prop el "open" false)) + ((= prop "opacity") + (dom-set-style el "opacity" (if val val "0"))) + ((= prop "visibility") + (dom-set-style el "visibility" (if val val "hidden"))) + ((= prop "hidden") (dom-set-attr el "hidden" "")) + ((= prop "class-hidden") (dom-add-class el "hidden")) + ((= prop "class-invisible") (dom-add-class el "invisible")) + ((= prop "class-opacity") (dom-add-class el "opacity-0")) + (true (dom-set-style el "display" (if val val "none"))))))))) (define hs-hide! (fn @@ -1759,25 +1786,32 @@ (fn (el strategy) (let - ((parts (split strategy ":")) (tag (dom-get-prop el "tagName"))) + ((resolved (_hs-resolve-strategy strategy))) (let - ((prop (first parts)) - (val (if (> (len parts) 1) (nth parts 1) nil))) - (cond - ((= tag "DIALOG") - (when - (not (dom-has-attr? el "open")) - (host-call el "showModal"))) - ((= tag "DETAILS") (dom-set-prop el "open" true)) - ((= prop "opacity") - (dom-set-style el "opacity" (if val val "1"))) - ((= prop "visibility") - (dom-set-style el "visibility" (if val val "visible"))) - ((= prop "hidden") (dom-remove-attr el "hidden")) - ((= prop "twDisplay") (dom-remove-class el "hidden")) - ((= prop "twVisibility") (dom-remove-class el "invisible")) - ((= prop "twOpacity") (dom-remove-class el "opacity-0")) - (true (dom-set-style el "display" (if val val "block")))))))) + ((parts (split resolved ":"))) + (let + ((prop (first parts)) + (val (if (> (len parts) 1) (nth parts 1) nil))) + (cond + ((and (not (= prop "display")) (not (= prop "opacity")) (not (= prop "visibility")) (not (= prop "hidden")) (not (= prop "class-hidden")) (not (= prop "class-invisible")) (not (= prop "class-opacity")) (not (= prop "details")) (not (= prop "dialog")) (dict-has? _hs-hide-strategies prop)) + (let + ((fn-val (get _hs-hide-strategies prop))) + (fn-val "show" el val))) + ((= (dom-get-prop el "tagName") "DIALOG") + (when + (not (dom-has-attr? el "open")) + (host-call el "showModal"))) + ((= (dom-get-prop el "tagName") "DETAILS") + (dom-set-prop el "open" true)) + ((= prop "opacity") + (dom-set-style el "opacity" (if val val "1"))) + ((= prop "visibility") + (dom-set-style el "visibility" (if val val "visible"))) + ((= prop "hidden") (dom-remove-attr el "hidden")) + ((= prop "class-hidden") (dom-remove-class el "hidden")) + ((= prop "class-invisible") (dom-remove-class el "invisible")) + ((= prop "class-opacity") (dom-remove-class el "opacity-0")) + (true (dom-set-style el "display" (if val val "block"))))))))) (define hs-show! (fn diff --git a/spec/tests/test-hyperscript-behavioral.sx b/spec/tests/test-hyperscript-behavioral.sx index d830aae7..9cf2beb5 100644 --- a/spec/tests/test-hyperscript-behavioral.sx +++ b/spec/tests/test-hyperscript-behavioral.sx @@ -7003,6 +7003,7 @@ )) (deftest "can hide element, with tailwindcss hidden class default strategy" (hs-cleanup!) + (hs-set-default-hide-strategy! "twDisplay") (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "on click hide") (dom-append (dom-body) _el-div) @@ -7017,6 +7018,7 @@ )) (deftest "can hide element, with tailwindcss invisible class default strategy" (hs-cleanup!) + (hs-set-default-hide-strategy! "twVisibility") (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "on click hide") (dom-append (dom-body) _el-div) @@ -7031,6 +7033,7 @@ )) (deftest "can hide element, with tailwindcss opacity-0 class default strategy" (hs-cleanup!) + (hs-set-default-hide-strategy! "twOpacity") (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "on click hide") (dom-append (dom-body) _el-div) @@ -7046,6 +7049,7 @@ )) (deftest "can show element, with tailwindcss removing hidden class default strategy" (hs-cleanup!) + (hs-set-default-hide-strategy! "twDisplay") (let ((_el-div (dom-create-element "div"))) (dom-add-class _el-div "hidden") (dom-set-attr _el-div "_" "on click show") @@ -7062,6 +7066,7 @@ )) (deftest "can show element, with tailwindcss removing invisible class default strategy" (hs-cleanup!) + (hs-set-default-hide-strategy! "twVisibility") (let ((_el-div (dom-create-element "div"))) (dom-add-class _el-div "invisible") (dom-set-attr _el-div "_" "on click show") @@ -7078,6 +7083,7 @@ )) (deftest "can show element, with tailwindcss removing opacity-0 class default strategy" (hs-cleanup!) + (hs-set-default-hide-strategy! "twOpacity") (let ((_el-div (dom-create-element "div"))) (dom-add-class _el-div "opacity-0") (dom-set-attr _el-div "_" "on click show") @@ -7424,6 +7430,7 @@ (defsuite "hs-upstream-hide" (deftest "can configure hidden as the default hide strategy" (hs-cleanup!) + (hs-set-default-hide-strategy! "hidden") (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "on click hide") (dom-append (dom-body) _el-div) @@ -7431,6 +7438,7 @@ (assert (!= (dom-get-attr _el-div "hidden") "")) (dom-dispatch _el-div "click" nil) (assert= (dom-get-attr _el-div "hidden") "") + (hs-set-default-hide-strategy! nil) )) (deftest "can filter hide via the when clause" (hs-cleanup!) @@ -7567,6 +7575,7 @@ )) (deftest "can hide with custom strategy" (hs-cleanup!) + (hs-set-hide-strategies! {:myHide (fn (op el arg) (if (= op "hide") (dom-add-class el "foo") (dom-remove-class el "foo")))}) (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "on click hide with myHide") (dom-append (dom-body) _el-div) @@ -7577,6 +7586,8 @@ )) (deftest "can set default to custom strategy" (hs-cleanup!) + (hs-set-default-hide-strategy! "myHide") + (hs-set-hide-strategies! {:myHide (fn (op el arg) (if (= op "hide") (dom-add-class el "foo") (dom-remove-class el "foo")))}) (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "on click hide") (dom-append (dom-body) _el-div) diff --git a/tests/playwright/generate-sx-tests.py b/tests/playwright/generate-sx-tests.py index 7e0ef0d1..e765770a 100644 --- a/tests/playwright/generate-sx-tests.py +++ b/tests/playwright/generate-sx-tests.py @@ -783,6 +783,58 @@ def _window_setup_ops(assign_body): return out +def _hs_config_setup_ops(body): + """Translate `_hyperscript.config.X = ...` assignments into SX ops. + Recognises `defaultHideShowStrategy = "name"` and `hideShowStrategies = { NAME: fn }` + for simple classList.add/remove-based strategies. Returns list of SX expr strings. + Empty list means no recognised ops; caller should skip (don't drop the block).""" + ops = [] + # defaultHideShowStrategy = "name" + for dm in re.finditer( + r'_hyperscript\.config\.defaultHideShowStrategy\s*=\s*"([^"]+)"', + body, + ): + ops.append(f'(hs-set-default-hide-strategy! "{dm.group(1)}")') + for dm in re.finditer( + r"_hyperscript\.config\.defaultHideShowStrategy\s*=\s*'([^']+)'", + body, + ): + ops.append(f'(hs-set-default-hide-strategy! "{dm.group(1)}")') + # delete _hyperscript.config.defaultHideShowStrategy + if re.search(r'delete\s+_hyperscript\.config\.defaultHideShowStrategy', body): + ops.append('(hs-set-default-hide-strategy! nil)') + # hideShowStrategies = { NAME: function(op, element, arg) { IF-ELSE } } + # Nested braces — locate the function body by manual brace-matching. + sm = re.search( + r'_hyperscript\.config\.hideShowStrategies\s*=\s*\{\s*' + r'(\w+)\s*:\s*function\s*\(\s*\w+\s*,\s*\w+\s*,\s*\w+\s*\)\s*\{', + body, + ) + if sm: + name = sm.group(1) + start = sm.end() + depth = 1 + i = start + while i < len(body) and depth > 0: + if body[i] == '{': depth += 1 + elif body[i] == '}': depth -= 1 + i += 1 + fn_body = body[start:i - 1] if depth == 0 else '' + hm = re.search( + r'if\s*\(\s*\w+\s*==\s*"hide"\s*\)\s*\{\s*' + r'\w+\.classList\.add\(\s*"([^"]+)"\s*\)\s*;?\s*\}\s*' + r'else\s*\{\s*\w+\.classList\.remove\(\s*"([^"]+)"\s*\)\s*;?\s*\}', + fn_body, re.DOTALL, + ) + if hm: + cls = hm.group(1) + ops.append( + f'(hs-set-hide-strategies! {{:{name} ' + f'(fn (op el arg) (if (= op "hide") (dom-add-class el "{cls}") (dom-remove-class el "{cls}")))}})' + ) + return ops + + def _extract_detail_expr(opts_src): """Extract `detail: ...` from an event options block like `, { detail: X }`. Returns an SX expression string, defaulting to `nil`.""" @@ -920,8 +972,29 @@ def parse_dev_body(body, elements, var_names): else: pre_setups.append((name, sx_val)) continue + # _hyperscript.config.X = ... setups (hideShowStrategies etc.) + hs_config_ops = _hs_config_setup_ops(m.group(1)) + if hs_config_ops: + for op_expr in hs_config_ops: + if seen_html: + ops.append(op_expr) + else: + pre_setups.append(('__hs_config__', op_expr)) + continue # fall through + # evaluate(() => _hyperscript.config.X = ...) single-line variant. + m = re.match(r'evaluate\(\s*\(\)\s*=>\s*(_hyperscript\.config\..+?)\s*\)\s*$', stmt_na, re.DOTALL) + if m: + hs_config_ops = _hs_config_setup_ops(m.group(1)) + if hs_config_ops: + for op_expr in hs_config_ops: + if seen_html: + ops.append(op_expr) + else: + pre_setups.append(('__hs_config__', op_expr)) + continue + # evaluate(() => document.querySelector(SEL).innerHTML = VAL) — DOM reset. m = re.match( r"evaluate\(\s*\(\)\s*=>\s*document\.querySelector\(\s*(['\"])([^'\"]+)\1\s*\)" @@ -1215,7 +1288,10 @@ def generate_test_pw(test, elements, var_names, idx): # Pre-`html(...)` setups — emit before element creation so activation # (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})') + if name == '__hs_config__': + lines.append(f' {sx_val}') + else: + 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