Hyperscript: focus command, diagnostic test output, blur keyword

Parser/compiler/runtime for focus command. Tokenizer: focus, blur,
precedes, follows, ignoring, case keywords. Test spec: per-test
failure output for diagnosis.

374/831 (45%)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 12:38:05 +00:00
parent 1783f4805a
commit f60d22e86e
12 changed files with 277 additions and 190 deletions

View File

@@ -1838,6 +1838,47 @@ let parse_http_headers data =
) (match lines with _ :: rest -> rest | [] -> []);
!headers
(* IO-aware eval for rendering — handles perform (text-measure, sleep, import).
Used by aser and SSR so components can call measure-text via perform. *)
let eval_with_io_render expr env =
let state = ref (Sx_ref.cek_step_loop (Sx_ref.make_cek_state expr (Env env) Nil)) in
while sx_truthy (Sx_ref.cek_suspended_p !state) do
let request = Sx_ref.cek_io_request !state in
let op = match request with
| Dict d -> (match Hashtbl.find_opt d "op" with Some (String s) -> s | Some (Symbol s) -> s | _ -> "")
| _ -> "" in
let args = match request with
| Dict d -> (match Hashtbl.find_opt d "args" with Some v -> v | None -> Nil)
| _ -> Nil in
let result = match op with
| "text-measure" ->
let size = match args with
| List [_font; Number sz; _text] -> sz
| List [_font; Number sz] -> sz
| _ -> 16.0 in
let text = match args with
| List [_font; _sz; String t] -> t
| _ -> "" in
let char_width = size *. 0.6 in
let width = char_width *. (float_of_int (String.length text)) in
let d = Hashtbl.create 4 in
Hashtbl.replace d "width" (Number width);
Hashtbl.replace d "height" (Number size);
Hashtbl.replace d "ascent" (Number (size *. 0.8));
Hashtbl.replace d "descent" (Number (size *. 0.2));
Dict d
| "io-sleep" | "sleep" ->
let ms = match args with
| List (Number n :: _) -> n | Number n -> n | _ -> 0.0 in
Unix.sleepf (ms /. 1000.0); Nil
| "import" -> Nil
| _ -> Nil
in
state := Sx_ref.cek_step_loop (Sx_ref.cek_resume !state result)
done;
if sx_truthy (Sx_ref.cek_terminal_p !state) then Sx_ref.cek_value !state
else Nil
(** Render a page. Routing + AJAX detection in SX (request-handler.sx),
render pipeline (aser → SSR → shell) in OCaml for reliable env access. *)
let http_render_page env path headers =
@@ -1889,7 +1930,7 @@ let http_render_page env path headers =
(* AJAX: return SX wire format (aser output) with text/sx content type *)
let body_result =
let call = List [Symbol "aser"; List [Symbol "quote"; wrapped]; Env env] in
Sx_ref.eval_expr call (Env env) in
eval_with_io_render call env in
let body_str = match body_result with
| String s | SxExpr s -> s | _ -> serialize_value body_result in
let t1 = Unix.gettimeofday () in
@@ -1903,7 +1944,7 @@ let http_render_page env path headers =
let t1 = Unix.gettimeofday () in
let body_result =
let call = List [Symbol "aser"; List [Symbol "quote"; full_ast]; Env env] in
Sx_ref.eval_expr call (Env env) in
eval_with_io_render call env in
let body_str = match body_result with
| String s | SxExpr s -> s | _ -> serialize_value body_result in
let t2 = Unix.gettimeofday () in
@@ -1913,7 +1954,7 @@ let http_render_page env path headers =
if env_has env "render-to-html" then
let render_call = List [Symbol "render-to-html";
List [Symbol "quote"; body_expr]; Env env] in
(match Sx_ref.eval_expr render_call (Env env) with
(match eval_with_io_render render_call env with
| String s | RawHTML s -> s | v -> Sx_runtime.value_to_str v)
else Sx_render.sx_render_to_html env body_expr env
with e -> Printf.eprintf "[http-ssr] failed for %s: %s\n%!" path (Printexc.to_string e); "" in

View File

@@ -1061,6 +1061,8 @@
(list (quote nil?) t)
(list (quote set!) t v))))
((= head (quote halt!)) (list (quote hs-halt!) (nth ast 1)))
((= head (quote focus!))
(list (quote dom-focus) (hs-to-sx (nth ast 1))))
(true ast))))))))
;; ── Convenience: source → SX ─────────────────────────────────

View File

@@ -1266,6 +1266,13 @@
(define
parse-param-list
(fn () (if (= (tp-type) "paren-open") (parse-call-args) (list))))
(define
parse-focus-cmd
(fn
()
(let
((tgt (cond ((at-end?) (list (quote me))) ((and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end"))) (list (quote me))) (true (parse-expr)))))
(list (quote focus!) tgt))))
(define
parse-feat-body
(fn
@@ -1452,6 +1459,8 @@
(do (adv!) (parse-default-cmd)))
((and (= typ "keyword") (= val "halt"))
(do (adv!) (parse-halt-cmd)))
((and (= typ "keyword") (= val "focus"))
(do (adv!) (parse-focus-cmd)))
(true (parse-expr))))))
(define
parse-cmd-list
@@ -1496,7 +1505,8 @@
(= v "default")
(= v "scroll")
(= v "select")
(= v "reset"))))
(= v "reset")
(= v "focus"))))
(define
cl-collect
(fn

View File

@@ -170,7 +170,9 @@
"precedes"
"follows"
"ignoring"
"case"))
"case"
"focus"
"blur"))
(define hs-keyword? (fn (word) (some (fn (k) (= k word)) hs-keywords)))

View File

@@ -1061,6 +1061,8 @@
(list (quote nil?) t)
(list (quote set!) t v))))
((= head (quote halt!)) (list (quote hs-halt!) (nth ast 1)))
((= head (quote focus!))
(list (quote dom-focus) (hs-to-sx (nth ast 1))))
(true ast))))))))
;; ── Convenience: source → SX ─────────────────────────────────

File diff suppressed because one or more lines are too long

View File

@@ -1266,6 +1266,13 @@
(define
parse-param-list
(fn () (if (= (tp-type) "paren-open") (parse-call-args) (list))))
(define
parse-focus-cmd
(fn
()
(let
((tgt (cond ((at-end?) (list (quote me))) ((and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end"))) (list (quote me))) (true (parse-expr)))))
(list (quote focus!) tgt))))
(define
parse-feat-body
(fn
@@ -1452,6 +1459,8 @@
(do (adv!) (parse-default-cmd)))
((and (= typ "keyword") (= val "halt"))
(do (adv!) (parse-halt-cmd)))
((and (= typ "keyword") (= val "focus"))
(do (adv!) (parse-focus-cmd)))
(true (parse-expr))))))
(define
parse-cmd-list
@@ -1496,7 +1505,8 @@
(= v "default")
(= v "scroll")
(= v "select")
(= v "reset"))))
(= v "reset")
(= v "focus"))))
(define
cl-collect
(fn

File diff suppressed because one or more lines are too long

View File

@@ -170,7 +170,9 @@
"precedes"
"follows"
"ignoring"
"case"))
"case"
"focus"
"blur"))
(define hs-keyword? (fn (word) (some (fn (k) (= k word)) hs-keywords)))

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +1,9 @@
;; Pretext demo — DOM-free text layout
;;
;; Visual-first: shows typeset text, then explains how.
;; All layout computed as data, then rendered.
;; Uses measure-text (perform) for real glyph measurement.
;; Compute positioned word data for one line.
;; Returns list of {:word :x :width} dicts.
(define
pretext-position-line
(fn
@@ -21,7 +20,6 @@
(append acc (list {:width (nth widths i) :x x :word (nth words i)})))))))
;; Compute all positioned lines for a paragraph.
;; Returns list of {:y :words [{:word :x :width}...]} dicts.
(define
pretext-layout-lines
(fn
@@ -47,6 +45,15 @@
{:y y :words (pretext-position-line lw lwid gap)}))))))
(range n-lines)))))
;; Measure all words and return widths list
(define
pretext-measure-words
(fn
(words font size)
(map
(fn (w) (let ((m (measure-text font size w))) (get m :width)))
words)))
;; Render pre-computed positioned lines
(defcomp
~pretext-demo/render-paragraph
@@ -99,187 +106,193 @@
()
(let
((sample-words (split "In the beginning was the Word, and the Word was with God, and the Word was God. The same was in the beginning with God. All things were made by him; and without him was not any thing made that was made. In him was life; and the life was the light of men." " "))
(char-w 9.6)
(space-w 9.6))
(font "serif")
(size 15))
(let
((sw (map (fn (w) (* (len w) char-w)) sample-words))
((sw (pretext-measure-words sample-words font size))
(space-m (measure-text font size " "))
(n-words (len sample-words)))
(div
(~tw :tokens "space-y-10")
(let
((space-w (get space-m :width)))
(div
(~tw :tokens "space-y-4")
(~tw :tokens "space-y-10")
(div
(h1
(~tw :tokens "text-3xl font-bold text-stone-900 tracking-tight")
"Pretext")
(~tw :tokens "space-y-4")
(div
(h1
(~tw
:tokens "text-3xl font-bold text-stone-900 tracking-tight")
"Pretext")
(p
(~tw :tokens "mt-1 text-lg text-stone-500")
"DOM-free text layout. One IO boundary. Pure arithmetic."))
(let
((hero-max 520) (hero-ranges (break-lines sw space-w 520)))
(div
(~tw :tokens "max-w-xl mx-auto mt-6")
(~pretext-demo/render-paragraph
:lines (pretext-layout-lines
sample-words
sw
hero-ranges
space-w
hero-max
24)
:max-width hero-max
:n-words n-words
:label "Knuth-Plass optimal line breaking — John 1:14"))))
(div
(~tw
:tokens "rounded-lg border border-violet-200 bg-violet-50 p-5")
(p
(~tw :tokens "mt-1 text-lg text-stone-500")
"DOM-free text layout. One IO boundary. Pure arithmetic."))
(let
((hero-max 520) (hero-ranges (break-lines sw space-w 520)))
(div
(~tw :tokens "max-w-xl mx-auto mt-6")
(~pretext-demo/render-paragraph
:lines (pretext-layout-lines
sample-words
sw
hero-ranges
space-w
hero-max
24)
:max-width hero-max
:n-words n-words
:label "Knuth-Plass optimal line breaking — John 1:14"))))
(div
(~tw :tokens "rounded-lg border border-violet-200 bg-violet-50 p-5")
(p
(~tw :tokens "text-sm text-violet-800")
(strong "One ")
(code (~tw :tokens "bg-violet-100 px-1 rounded") "perform")
" for glyph measurement. Everything else — line breaking, positioning, hyphenation, justification — is pure SX functions over numbers. "
"Server renders with font-table lookups. Browser uses "
(code "canvas.measureText")
". Same algorithm, same output."))
(div
(~tw :tokens "space-y-3")
(h2
(~tw :tokens "text-xl font-semibold text-stone-800")
"Greedy vs optimal")
(p
(~tw :tokens "text-sm text-stone-500")
"Most web text uses greedy word wrap — break when the next word doesn't fit. "
"Knuth-Plass considers all possible breaks simultaneously, minimizing total raggedness.")
(let
((nw (map (fn (w) (* (len w) 7.8)) sample-words))
(ns 7.8)
(nm 340)
(nlh 22))
(div
(~tw :tokens "grid grid-cols-1 md:grid-cols-2 gap-4")
(~pretext-demo/render-paragraph
:lines (pretext-layout-lines
sample-words
nw
(break-lines-greedy nw ns nm)
ns
nm
nlh)
:max-width nm
:line-height nlh
:n-words n-words
:label "Greedy (browser default)")
(~pretext-demo/render-paragraph
:lines (pretext-layout-lines
sample-words
nw
(break-lines nw ns nm)
ns
nm
nlh)
:max-width nm
:line-height nlh
:n-words n-words
:label "Knuth-Plass optimal"))))
(div
(~tw :tokens "space-y-3")
(h2
(~tw :tokens "text-xl font-semibold text-stone-800")
"How lines are scored")
(p
(~tw :tokens "text-sm text-stone-500")
"Each line gets a badness score — how far it deviates from ideal width. "
"The algorithm minimizes total demerits (1 + badness)² across all lines.")
(~tw :tokens "text-sm text-violet-800")
(strong "One ")
(code (~tw :tokens "bg-violet-100 px-1 rounded") "perform")
" for glyph measurement. Everything else — line breaking, positioning, hyphenation, justification — is pure SX functions over numbers. "
"Server renders with font-table lookups. Browser uses "
(code "canvas.measureText")
". Same algorithm, same output."))
(div
(~tw :tokens "grid grid-cols-4 md:grid-cols-8 gap-2")
(map
(fn
(used)
(let
((bad (line-badness used 100))
(pct (str (min used 100) "%")))
(div
(~tw
:tokens "rounded border border-stone-200 p-2 text-center")
(div
:style (str
"height:4px;background:linear-gradient(90deg,hsl(263,70%,50%) "
pct
",#e7e5e4 "
pct
");border-radius:2px;margin-bottom:6px;")
"")
(div
(~tw :tokens "text-sm font-mono font-bold")
(if
(>= bad 100000)
(span (~tw :tokens "text-red-500") "∞")
(span (~tw :tokens "text-stone-700") (str bad))))
(div
(~tw :tokens "text-xs text-stone-400 mt-0.5")
(str used "%")))))
(list 100 95 90 85 80 70 50 110))))
(div
(~tw :tokens "space-y-3")
(h2
(~tw :tokens "text-xl font-semibold text-stone-800")
"Hyphenation")
(p
(~tw :tokens "text-sm text-stone-500")
"Liang's algorithm: a trie of character patterns with numeric levels. "
"Odd levels mark valid break points.")
(let
((trie (make-hyphenation-trie (list "hy1p" "he2n" "hen3at" "hena4t" "1na" "n2at" "1tio" "2io" "o2i" "1tic" "1mo" "4m1p" "1pu" "put1" "1er" "pro1g" "1gram" "2gra" "program5" "pro3" "ty1" "1graph" "2ph"))))
(~tw :tokens "space-y-3")
(h2
(~tw :tokens "text-xl font-semibold text-stone-800")
"Greedy vs optimal")
(p
(~tw :tokens "text-sm text-stone-500")
"Most web text uses greedy word wrap — break when the next word doesn't fit. "
"Knuth-Plass considers all possible breaks simultaneously, minimizing total raggedness.")
(let
((nm 340))
(div
(~tw :tokens "grid grid-cols-1 md:grid-cols-2 gap-4")
(~pretext-demo/render-paragraph
:words sample-words
:widths sw
:space-width space-w
:max-width nm
:line-height 22
:lines (pretext-layout-lines
sample-words
sw
(break-lines-greedy sw space-w nm)
space-w
nm
22)
:n-words n-words
:label "Greedy (browser default)")
(~pretext-demo/render-paragraph
:words sample-words
:widths sw
:space-width space-w
:max-width nm
:line-height 22
:lines (pretext-layout-lines
sample-words
sw
(break-lines sw space-w nm)
space-w
nm
22)
:n-words n-words
:label "Knuth-Plass optimal"))))
(div
(~tw :tokens "space-y-3")
(h2
(~tw :tokens "text-xl font-semibold text-stone-800")
"How lines are scored")
(p
(~tw :tokens "text-sm text-stone-500")
"Each line gets a badness score — how far it deviates from ideal width. "
"The algorithm minimizes total demerits (1 + badness)² across all lines.")
(div
(~tw :tokens "flex flex-wrap gap-3")
(~tw :tokens "grid grid-cols-4 md:grid-cols-8 gap-2")
(map
(fn
(word)
(used)
(let
((syllables (hyphenate-word trie word)))
((bad (line-badness used 100))
(pct (str (min used 100) "%")))
(div
(~tw
:tokens "rounded-lg border border-stone-200 bg-white px-4 py-3 text-center")
:tokens "rounded border border-stone-200 p-2 text-center")
(div
:style (str
"height:4px;background:linear-gradient(90deg,hsl(263,70%,50%) "
pct
",#e7e5e4 "
pct
");border-radius:2px;margin-bottom:6px;")
"")
(div
(~tw :tokens "text-sm font-mono font-bold")
(if
(>= bad 100000)
(span (~tw :tokens "text-red-500") "∞")
(span (~tw :tokens "text-stone-700") (str bad))))
(div
(~tw :tokens "text-xs text-stone-400 mt-0.5")
(str used "%")))))
(list 100 95 90 85 80 70 50 110))))
(div
(~tw :tokens "space-y-3")
(h2
(~tw :tokens "text-xl font-semibold text-stone-800")
"Hyphenation")
(p
(~tw :tokens "text-sm text-stone-500")
"Liang's algorithm: a trie of character patterns with numeric levels. "
"Odd levels mark valid break points.")
(let
((trie (make-hyphenation-trie (list "hy1p" "he2n" "hen3at" "hena4t" "1na" "n2at" "1tio" "2io" "o2i" "1tic" "1mo" "4m1p" "1pu" "put1" "1er" "pro1g" "1gram" "2gra" "program5" "pro3" "ty1" "1graph" "2ph"))))
(div
(~tw :tokens "flex flex-wrap gap-3")
(map
(fn
(word)
(let
((syllables (hyphenate-word trie word)))
(div
(~tw
:tokens "text-lg font-mono font-semibold text-stone-800 tracking-wide")
(map-indexed
(fn
(i syl)
(if
(= i 0)
(span syl)
(list
(span
(~tw :tokens "text-violet-400 mx-0.5")
"·")
(span syl))))
syllables))
(div (~tw :tokens "text-xs text-stone-400 mt-1") word))))
(list "hyphen" "computation" "programming" "typography")))))
(div
(~tw
:tokens "rounded-lg border border-stone-200 bg-stone-50 p-5 space-y-2")
(h3
:tokens "rounded-lg border border-stone-200 bg-white px-4 py-3 text-center")
(div
(~tw
:tokens "text-lg font-mono font-semibold text-stone-800 tracking-wide")
(map-indexed
(fn
(i syl)
(if
(= i 0)
(span syl)
(<>
(span :class "text-violet-400 mx-0.5" "·")
(span syl))))
syllables))
(div (~tw :tokens "text-xs text-stone-400 mt-1") word))))
(list "hyphen" "computation" "programming" "typography")))))
(div
(~tw
:tokens "text-sm font-semibold text-stone-600 uppercase tracking-wide")
"The pipeline")
(ol
(~tw
:tokens "list-decimal list-inside text-sm text-stone-600 space-y-1")
(li
(code "measure-text")
" — the only IO. Server: font tables. Browser: "
(code "canvas.measureText"))
(li
(code "break-lines")
" — Knuth-Plass DP over word widths → optimal break points")
(li
(code "position-lines")
" — pure arithmetic: widths + breaks → x,y coordinates")
(li
(code "hyphenate-word")
" — Liang's trie: character patterns → syllable boundaries")
(li
"All layout is "
(strong "deterministic")
" — same widths → same positions, every time")))))))
:tokens "rounded-lg border border-stone-200 bg-stone-50 p-5 space-y-2")
(h3
(~tw
:tokens "text-sm font-semibold text-stone-600 uppercase tracking-wide")
"The pipeline")
(ol
(~tw
:tokens "list-decimal list-inside text-sm text-stone-600 space-y-1")
(li
(code "measure-text")
" — the only IO. Server: font tables. Browser: "
(code "canvas.measureText"))
(li
(code "break-lines")
" — Knuth-Plass DP over word widths → optimal break points")
(li
(code "position-lines")
" — pure arithmetic: widths + breaks → x,y coordinates")
(li
(code "hyphenate-word")
" — Liang's trie: character patterns → syllable boundaries")
(li
"All layout is "
(strong "deterministic")
" — same widths → same positions, every time"))))))))

View File

@@ -322,11 +322,16 @@ test.describe('Hyperscript behavioral tests', () => {
for (const [e, info] of Object.entries(uniqueErrors).sort((a,b) => b[1].count - a[1].count).slice(0, 25)) {
console.log(` [${info.count}x] ${e}`);
}
// Show samples of "bar" error specifically
const barSamples = results.filter(r => !r.p && (r.e||'').includes('Expected , got')).slice(0, 15);
if (barSamples.length > 0) {
console.log(` Expected-got failures (${barSamples.length}):`);
for (const s of barSamples) console.log(` ${s.s}/${s.n}`);
// Show ALL failing tests with errors (for diagnosis)
const failsByCategory = {};
for (const r of results.filter(r => !r.p)) {
if (!failsByCategory[r.s]) failsByCategory[r.s] = [];
failsByCategory[r.s].push(r);
}
for (const [cat, fails] of Object.entries(failsByCategory).sort((a,b) => a[0].localeCompare(b[0]))) {
for (const f of fails.slice(0, 5)) {
console.log(` FAIL ${f.s}/${f.n}: ${(f.e||'').slice(0, 100)}`);
}
}
expect(results.length).toBeGreaterThanOrEqual(830);