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:
@@ -1838,6 +1838,47 @@ let parse_http_headers data =
|
|||||||
) (match lines with _ :: rest -> rest | [] -> []);
|
) (match lines with _ :: rest -> rest | [] -> []);
|
||||||
!headers
|
!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 a page. Routing + AJAX detection in SX (request-handler.sx),
|
||||||
render pipeline (aser → SSR → shell) in OCaml for reliable env access. *)
|
render pipeline (aser → SSR → shell) in OCaml for reliable env access. *)
|
||||||
let http_render_page env path headers =
|
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 *)
|
(* AJAX: return SX wire format (aser output) with text/sx content type *)
|
||||||
let body_result =
|
let body_result =
|
||||||
let call = List [Symbol "aser"; List [Symbol "quote"; wrapped]; Env env] in
|
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
|
let body_str = match body_result with
|
||||||
| String s | SxExpr s -> s | _ -> serialize_value body_result in
|
| String s | SxExpr s -> s | _ -> serialize_value body_result in
|
||||||
let t1 = Unix.gettimeofday () in
|
let t1 = Unix.gettimeofday () in
|
||||||
@@ -1903,7 +1944,7 @@ let http_render_page env path headers =
|
|||||||
let t1 = Unix.gettimeofday () in
|
let t1 = Unix.gettimeofday () in
|
||||||
let body_result =
|
let body_result =
|
||||||
let call = List [Symbol "aser"; List [Symbol "quote"; full_ast]; Env env] in
|
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
|
let body_str = match body_result with
|
||||||
| String s | SxExpr s -> s | _ -> serialize_value body_result in
|
| String s | SxExpr s -> s | _ -> serialize_value body_result in
|
||||||
let t2 = Unix.gettimeofday () 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
|
if env_has env "render-to-html" then
|
||||||
let render_call = List [Symbol "render-to-html";
|
let render_call = List [Symbol "render-to-html";
|
||||||
List [Symbol "quote"; body_expr]; Env env] in
|
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)
|
| String s | RawHTML s -> s | v -> Sx_runtime.value_to_str v)
|
||||||
else Sx_render.sx_render_to_html env body_expr env
|
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
|
with e -> Printf.eprintf "[http-ssr] failed for %s: %s\n%!" path (Printexc.to_string e); "" in
|
||||||
|
|||||||
@@ -1061,6 +1061,8 @@
|
|||||||
(list (quote nil?) t)
|
(list (quote nil?) t)
|
||||||
(list (quote set!) t v))))
|
(list (quote set!) t v))))
|
||||||
((= head (quote halt!)) (list (quote hs-halt!) (nth ast 1)))
|
((= 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))))))))
|
(true ast))))))))
|
||||||
|
|
||||||
;; ── Convenience: source → SX ─────────────────────────────────
|
;; ── Convenience: source → SX ─────────────────────────────────
|
||||||
|
|||||||
@@ -1266,6 +1266,13 @@
|
|||||||
(define
|
(define
|
||||||
parse-param-list
|
parse-param-list
|
||||||
(fn () (if (= (tp-type) "paren-open") (parse-call-args) (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
|
(define
|
||||||
parse-feat-body
|
parse-feat-body
|
||||||
(fn
|
(fn
|
||||||
@@ -1452,6 +1459,8 @@
|
|||||||
(do (adv!) (parse-default-cmd)))
|
(do (adv!) (parse-default-cmd)))
|
||||||
((and (= typ "keyword") (= val "halt"))
|
((and (= typ "keyword") (= val "halt"))
|
||||||
(do (adv!) (parse-halt-cmd)))
|
(do (adv!) (parse-halt-cmd)))
|
||||||
|
((and (= typ "keyword") (= val "focus"))
|
||||||
|
(do (adv!) (parse-focus-cmd)))
|
||||||
(true (parse-expr))))))
|
(true (parse-expr))))))
|
||||||
(define
|
(define
|
||||||
parse-cmd-list
|
parse-cmd-list
|
||||||
@@ -1496,7 +1505,8 @@
|
|||||||
(= v "default")
|
(= v "default")
|
||||||
(= v "scroll")
|
(= v "scroll")
|
||||||
(= v "select")
|
(= v "select")
|
||||||
(= v "reset"))))
|
(= v "reset")
|
||||||
|
(= v "focus"))))
|
||||||
(define
|
(define
|
||||||
cl-collect
|
cl-collect
|
||||||
(fn
|
(fn
|
||||||
|
|||||||
@@ -170,7 +170,9 @@
|
|||||||
"precedes"
|
"precedes"
|
||||||
"follows"
|
"follows"
|
||||||
"ignoring"
|
"ignoring"
|
||||||
"case"))
|
"case"
|
||||||
|
"focus"
|
||||||
|
"blur"))
|
||||||
|
|
||||||
(define hs-keyword? (fn (word) (some (fn (k) (= k word)) hs-keywords)))
|
(define hs-keyword? (fn (word) (some (fn (k) (= k word)) hs-keywords)))
|
||||||
|
|
||||||
|
|||||||
@@ -1061,6 +1061,8 @@
|
|||||||
(list (quote nil?) t)
|
(list (quote nil?) t)
|
||||||
(list (quote set!) t v))))
|
(list (quote set!) t v))))
|
||||||
((= head (quote halt!)) (list (quote hs-halt!) (nth ast 1)))
|
((= 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))))))))
|
(true ast))))))))
|
||||||
|
|
||||||
;; ── Convenience: source → SX ─────────────────────────────────
|
;; ── Convenience: source → SX ─────────────────────────────────
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1266,6 +1266,13 @@
|
|||||||
(define
|
(define
|
||||||
parse-param-list
|
parse-param-list
|
||||||
(fn () (if (= (tp-type) "paren-open") (parse-call-args) (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
|
(define
|
||||||
parse-feat-body
|
parse-feat-body
|
||||||
(fn
|
(fn
|
||||||
@@ -1452,6 +1459,8 @@
|
|||||||
(do (adv!) (parse-default-cmd)))
|
(do (adv!) (parse-default-cmd)))
|
||||||
((and (= typ "keyword") (= val "halt"))
|
((and (= typ "keyword") (= val "halt"))
|
||||||
(do (adv!) (parse-halt-cmd)))
|
(do (adv!) (parse-halt-cmd)))
|
||||||
|
((and (= typ "keyword") (= val "focus"))
|
||||||
|
(do (adv!) (parse-focus-cmd)))
|
||||||
(true (parse-expr))))))
|
(true (parse-expr))))))
|
||||||
(define
|
(define
|
||||||
parse-cmd-list
|
parse-cmd-list
|
||||||
@@ -1496,7 +1505,8 @@
|
|||||||
(= v "default")
|
(= v "default")
|
||||||
(= v "scroll")
|
(= v "scroll")
|
||||||
(= v "select")
|
(= v "select")
|
||||||
(= v "reset"))))
|
(= v "reset")
|
||||||
|
(= v "focus"))))
|
||||||
(define
|
(define
|
||||||
cl-collect
|
cl-collect
|
||||||
(fn
|
(fn
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -170,7 +170,9 @@
|
|||||||
"precedes"
|
"precedes"
|
||||||
"follows"
|
"follows"
|
||||||
"ignoring"
|
"ignoring"
|
||||||
"case"))
|
"case"
|
||||||
|
"focus"
|
||||||
|
"blur"))
|
||||||
|
|
||||||
(define hs-keyword? (fn (word) (some (fn (k) (= k word)) hs-keywords)))
|
(define hs-keyword? (fn (word) (some (fn (k) (= k word)) hs-keywords)))
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,10 +1,9 @@
|
|||||||
;; Pretext demo — DOM-free text layout
|
;; Pretext demo — DOM-free text layout
|
||||||
;;
|
;;
|
||||||
;; Visual-first: shows typeset text, then explains how.
|
;; 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.
|
;; Compute positioned word data for one line.
|
||||||
;; Returns list of {:word :x :width} dicts.
|
|
||||||
(define
|
(define
|
||||||
pretext-position-line
|
pretext-position-line
|
||||||
(fn
|
(fn
|
||||||
@@ -21,7 +20,6 @@
|
|||||||
(append acc (list {:width (nth widths i) :x x :word (nth words i)})))))))
|
(append acc (list {:width (nth widths i) :x x :word (nth words i)})))))))
|
||||||
|
|
||||||
;; Compute all positioned lines for a paragraph.
|
;; Compute all positioned lines for a paragraph.
|
||||||
;; Returns list of {:y :words [{:word :x :width}...]} dicts.
|
|
||||||
(define
|
(define
|
||||||
pretext-layout-lines
|
pretext-layout-lines
|
||||||
(fn
|
(fn
|
||||||
@@ -47,6 +45,15 @@
|
|||||||
{:y y :words (pretext-position-line lw lwid gap)}))))))
|
{:y y :words (pretext-position-line lw lwid gap)}))))))
|
||||||
(range n-lines)))))
|
(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
|
;; Render pre-computed positioned lines
|
||||||
(defcomp
|
(defcomp
|
||||||
~pretext-demo/render-paragraph
|
~pretext-demo/render-paragraph
|
||||||
@@ -99,18 +106,22 @@
|
|||||||
()
|
()
|
||||||
(let
|
(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." " "))
|
((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)
|
(font "serif")
|
||||||
(space-w 9.6))
|
(size 15))
|
||||||
(let
|
(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)))
|
(n-words (len sample-words)))
|
||||||
|
(let
|
||||||
|
((space-w (get space-m :width)))
|
||||||
(div
|
(div
|
||||||
(~tw :tokens "space-y-10")
|
(~tw :tokens "space-y-10")
|
||||||
(div
|
(div
|
||||||
(~tw :tokens "space-y-4")
|
(~tw :tokens "space-y-4")
|
||||||
(div
|
(div
|
||||||
(h1
|
(h1
|
||||||
(~tw :tokens "text-3xl font-bold text-stone-900 tracking-tight")
|
(~tw
|
||||||
|
:tokens "text-3xl font-bold text-stone-900 tracking-tight")
|
||||||
"Pretext")
|
"Pretext")
|
||||||
(p
|
(p
|
||||||
(~tw :tokens "mt-1 text-lg text-stone-500")
|
(~tw :tokens "mt-1 text-lg text-stone-500")
|
||||||
@@ -131,7 +142,8 @@
|
|||||||
:n-words n-words
|
:n-words n-words
|
||||||
:label "Knuth-Plass optimal line breaking — John 1:1–4"))))
|
:label "Knuth-Plass optimal line breaking — John 1:1–4"))))
|
||||||
(div
|
(div
|
||||||
(~tw :tokens "rounded-lg border border-violet-200 bg-violet-50 p-5")
|
(~tw
|
||||||
|
:tokens "rounded-lg border border-violet-200 bg-violet-50 p-5")
|
||||||
(p
|
(p
|
||||||
(~tw :tokens "text-sm text-violet-800")
|
(~tw :tokens "text-sm text-violet-800")
|
||||||
(strong "One ")
|
(strong "One ")
|
||||||
@@ -150,34 +162,37 @@
|
|||||||
"Most web text uses greedy word wrap — break when the next word doesn't fit. "
|
"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.")
|
"Knuth-Plass considers all possible breaks simultaneously, minimizing total raggedness.")
|
||||||
(let
|
(let
|
||||||
((nw (map (fn (w) (* (len w) 7.8)) sample-words))
|
((nm 340))
|
||||||
(ns 7.8)
|
|
||||||
(nm 340)
|
|
||||||
(nlh 22))
|
|
||||||
(div
|
(div
|
||||||
(~tw :tokens "grid grid-cols-1 md:grid-cols-2 gap-4")
|
(~tw :tokens "grid grid-cols-1 md:grid-cols-2 gap-4")
|
||||||
(~pretext-demo/render-paragraph
|
(~pretext-demo/render-paragraph
|
||||||
|
:words sample-words
|
||||||
|
:widths sw
|
||||||
|
:space-width space-w
|
||||||
|
:max-width nm
|
||||||
|
:line-height 22
|
||||||
:lines (pretext-layout-lines
|
:lines (pretext-layout-lines
|
||||||
sample-words
|
sample-words
|
||||||
nw
|
sw
|
||||||
(break-lines-greedy nw ns nm)
|
(break-lines-greedy sw space-w nm)
|
||||||
ns
|
space-w
|
||||||
nm
|
nm
|
||||||
nlh)
|
22)
|
||||||
:max-width nm
|
|
||||||
:line-height nlh
|
|
||||||
:n-words n-words
|
:n-words n-words
|
||||||
:label "Greedy (browser default)")
|
:label "Greedy (browser default)")
|
||||||
(~pretext-demo/render-paragraph
|
(~pretext-demo/render-paragraph
|
||||||
|
:words sample-words
|
||||||
|
:widths sw
|
||||||
|
:space-width space-w
|
||||||
|
:max-width nm
|
||||||
|
:line-height 22
|
||||||
:lines (pretext-layout-lines
|
:lines (pretext-layout-lines
|
||||||
sample-words
|
sample-words
|
||||||
nw
|
sw
|
||||||
(break-lines nw ns nm)
|
(break-lines sw space-w nm)
|
||||||
ns
|
space-w
|
||||||
nm
|
nm
|
||||||
nlh)
|
22)
|
||||||
:max-width nm
|
|
||||||
:line-height nlh
|
|
||||||
:n-words n-words
|
:n-words n-words
|
||||||
:label "Knuth-Plass optimal"))))
|
:label "Knuth-Plass optimal"))))
|
||||||
(div
|
(div
|
||||||
@@ -248,10 +263,8 @@
|
|||||||
(if
|
(if
|
||||||
(= i 0)
|
(= i 0)
|
||||||
(span syl)
|
(span syl)
|
||||||
(list
|
(<>
|
||||||
(span
|
(span :class "text-violet-400 mx-0.5" "·")
|
||||||
(~tw :tokens "text-violet-400 mx-0.5")
|
|
||||||
"·")
|
|
||||||
(span syl))))
|
(span syl))))
|
||||||
syllables))
|
syllables))
|
||||||
(div (~tw :tokens "text-xs text-stone-400 mt-1") word))))
|
(div (~tw :tokens "text-xs text-stone-400 mt-1") word))))
|
||||||
@@ -282,4 +295,4 @@
|
|||||||
(li
|
(li
|
||||||
"All layout is "
|
"All layout is "
|
||||||
(strong "deterministic")
|
(strong "deterministic")
|
||||||
" — same widths → same positions, every time")))))))
|
" — same widths → same positions, every time"))))))))
|
||||||
@@ -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)) {
|
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}`);
|
console.log(` [${info.count}x] ${e}`);
|
||||||
}
|
}
|
||||||
// Show samples of "bar" error specifically
|
// Show ALL failing tests with errors (for diagnosis)
|
||||||
const barSamples = results.filter(r => !r.p && (r.e||'').includes('Expected , got')).slice(0, 15);
|
const failsByCategory = {};
|
||||||
if (barSamples.length > 0) {
|
for (const r of results.filter(r => !r.p)) {
|
||||||
console.log(` Expected-got failures (${barSamples.length}):`);
|
if (!failsByCategory[r.s]) failsByCategory[r.s] = [];
|
||||||
for (const s of barSamples) console.log(` ${s.s}/${s.n}`);
|
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);
|
expect(results.length).toBeGreaterThanOrEqual(830);
|
||||||
|
|||||||
Reference in New Issue
Block a user