Hyperscript conformance: 341→372 (45%) — parser, compiler, runtime, generator

Parser: increment/decrement "by N", then-less command chaining, scroll/select/
reset/default/halt commands, toggle style/attr/between, repeat for-loop
delegation, number fix for repeat N times, take with from/for scope.

Compiler: emit-inc/emit-dec with amount + property/style targets, 12 new
dispatch entries (scroll, select, reset, default, halt, toggle-style,
toggle-style-between, toggle-attr, toggle-attr-between, take rewrite).

Runtime: hs-scroll!, hs-halt!, hs-select!, hs-reset!, hs-query-all,
hs-toggle-style!, hs-toggle-style-between!, hs-toggle-attr!,
hs-toggle-attr-between!, hs-take! rewrite with kind/name/scope.

Generator: handle backtick strings, two-line run()/expect() patterns,
toEqual with arrays, toThrow — unlocks 34 more eval-only tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 10:00:51 +00:00
parent 56855eee7f
commit 3dbbe7e1d1
3 changed files with 220 additions and 78 deletions

View File

@@ -508,37 +508,116 @@ def generate_test_pw(test, elements, var_names, idx):
return '\n'.join(lines)
def js_val_to_sx(val):
"""Convert a JS literal value to SX."""
val = val.strip()
if val == 'true': return 'true'
if val == 'false': return 'false'
if val in ('null', 'undefined'): return 'nil'
if val.startswith('"') or val.startswith("'"):
return '"' + val.strip("\"'") + '"'
# Arrays: [1, 2, 3] → (list 1 2 3)
if val.startswith('[') and val.endswith(']'):
inner = val[1:-1].strip()
if not inner:
return '(list)'
items = [js_val_to_sx(x.strip()) for x in split_top_level(inner)]
return '(list ' + ' '.join(items) + ')'
try:
float(val)
return val
except ValueError:
return f'"{val}"'
def split_top_level(s):
"""Split a string by commas, respecting brackets/quotes."""
parts = []
depth = 0
current = []
in_str = None
for ch in s:
if in_str:
current.append(ch)
if ch == in_str:
in_str = None
elif ch in ('"', "'"):
in_str = ch
current.append(ch)
elif ch in ('(', '[', '{'):
depth += 1
current.append(ch)
elif ch in (')', ']', '}'):
depth -= 1
current.append(ch)
elif ch == ',' and depth == 0:
parts.append(''.join(current))
current = []
else:
current.append(ch)
if current:
parts.append(''.join(current))
return parts
def extract_hs_expr(raw):
"""Clean a HS expression extracted from run() call."""
# Remove surrounding whitespace and newlines
expr = raw.strip().replace('\n', ' ').replace('\t', ' ')
# Collapse multiple spaces
expr = re.sub(r'\s+', ' ', expr)
# Escape quotes for SX string
expr = expr.replace('\\', '').replace('"', '\\"')
return expr
def generate_eval_only_test(test, idx):
"""Generate SX deftest for no-HTML tests using eval-hs.
Parses body field for run("expr").toBe(val) / expect(run("expr")).toBe(val) patterns."""
Handles patterns:
- run("expr").toBe(val)
- expect(run("expr")).toBe(val)
- var result = await run(`expr`); expect(result).toBe(val)
- run("expr").toEqual([...])
- run("expr").toThrow()
"""
body = test.get('body', '')
lines = []
lines.append(f' (deftest "{test["name"]}"')
safe_name = test["name"].replace('"', "'")
lines.append(f' (deftest "{safe_name}"')
# Extract run("expr").toBe(val) or expect(await run("expr")).toBe(val) patterns
assertions = []
for m in re.finditer(r'(?:expect\()?(?:await\s+)?run\(["\x27]([^"\x27]+)["\x27]\)\)?\.toBe\(([^)]+)\)', body):
hs_expr = m.group(1).replace('\\', '').replace('"', '\\"')
expected = m.group(2).strip()
# Convert JS values to SX
if expected == 'true': expected_sx = 'true'
elif expected == 'false': expected_sx = 'false'
elif expected == 'null' or expected == 'undefined': expected_sx = 'nil'
elif expected.startswith('"') or expected.startswith("'"):
expected_sx = '"' + expected.strip("\"'") + '"'
else:
try:
float(expected)
expected_sx = expected
except ValueError:
expected_sx = f'"{expected}"'
# Pattern 1: Inline — run("expr").toBe(val) or expect(run("expr")).toBe(val)
for m in re.finditer(
r'(?:expect\()?(?:await\s+)?run\((["\x27`])(.+?)\1\)\)?\.toBe\(([^)]+)\)',
body, re.DOTALL
):
hs_expr = extract_hs_expr(m.group(2))
expected_sx = js_val_to_sx(m.group(3))
assertions.append(f' (assert= {expected_sx} (eval-hs "{hs_expr}"))')
# Also handle toEqual patterns
for m in re.finditer(r'(?:expect\()?(?:await\s+)?run\(["\x27]([^"\x27]+)["\x27]\)\)?\.toEqual\(([^)]+)\)', body):
hs_expr = m.group(1).replace('\\', '').replace('"', '\\"')
expected = m.group(2).strip()
assertions.append(f' ;; toEqual: {expected[:40]}')
# Pattern 2: Two-line — var result = await run(`expr`); expect(result).toBe(val)
if not assertions:
run_match = re.search(
r'(?:var|let|const)\s+\w+\s*=\s*(?:await\s+)?run\((["\x27`])(.+?)\1\)',
body, re.DOTALL
)
if run_match:
hs_expr = extract_hs_expr(run_match.group(2))
for m in re.finditer(r'\.toBe\(([^)]+)\)', body):
expected_sx = js_val_to_sx(m.group(1))
assertions.append(f' (assert= {expected_sx} (eval-hs "{hs_expr}"))')
for m in re.finditer(r'\.toEqual\((\[.*?\])\)', body, re.DOTALL):
expected_sx = js_val_to_sx(m.group(1))
assertions.append(f' (assert= {expected_sx} (eval-hs "{hs_expr}"))')
# Pattern 3: toThrow — expect(() => run("expr")).toThrow()
for m in re.finditer(
r'run\((["\x27`])(.+?)\1\).*?\.toThrow\(\)',
body, re.DOTALL
):
hs_expr = extract_hs_expr(m.group(2))
assertions.append(f' (assert-throws (eval-hs "{hs_expr}"))')
if not assertions:
return None # Can't convert this body pattern