HS: hide strategy config (+3 tests)

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 06:25:58 +00:00
parent 65d4c70638
commit beb120baf7
4 changed files with 224 additions and 69 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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