this repo has no description

Site redesign, new content, blog gen, E2E tests, and build improvements

New blog posts (monopam-madness, open-source-and-ai, weeknotes-2026-10),
notebook showcase with card layout and screenshots, Atom feed generator,
foundations notebook fixes, ONNX test improvements, widget interaction
tests, deploy script updates for oxcaml switch, and .gitignore for
build artifacts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+181 -87
+17 -13
doc/demo1.mld
··· 1 1 {0 Interactive OCaml Demo} 2 2 3 + @x-ocaml.universe ./universe 4 + @x-ocaml.worker ./universe/worker.js 5 + 3 6 This page demonstrates interactive OCaml code cells powered by 4 7 [x-ocaml] and [js_top_worker]. 5 8 ··· 7 10 8 11 Try evaluating some OCaml expressions: 9 12 10 - {@ocaml[ 13 + {@ocaml x[ 11 14 1 + 2 * 3 12 15 ]} 13 16 14 - {@ocaml[ 17 + {@ocaml x[ 15 18 let greet name = Printf.sprintf "Hello, %s!" name 16 19 17 20 let () = print_endline (greet "World") 18 21 ]} 19 22 20 - {1 Using Yojson} 23 + {1 Using Cmdliner} 21 24 22 - These cells use the [yojson] library loaded from the universe: 25 + These cells use the [cmdliner] library loaded from the universe: 23 26 24 - {@ocaml[ 25 - #require "yojson" 27 + {@ocaml x[ 28 + #require "cmdliner" 26 29 ]} 27 30 28 - {@ocaml[ 29 - let json = `Assoc [ 30 - ("name", `String "OCaml"); 31 - ("version", `Float 5.4); 32 - ("features", `List [`String "modules"; `String "types"]) 33 - ] 31 + {@ocaml x[ 32 + let greeting = 33 + Cmdliner.Arg.(value & opt string "World" & info ["name"] ~docv:"NAME") 34 + 35 + let greet_term = 36 + Cmdliner.Term.(const (Printf.printf "Hello, %s!\n") $ greeting) 34 37 35 - let () = print_endline (Yojson.Safe.pretty_to_string json) 38 + let cmd = Cmdliner.Cmd.v (Cmdliner.Cmd.info "greet") greet_term 39 + let () = Printf.printf "Command: %s\n" (Cmdliner.Cmd.name cmd) 36 40 ]}
+20 -14
doc/demo2_v2.mld
··· 1 - {0 Yojson v2 Demo} 1 + {0 Cmdliner v1 Demo} 2 2 3 3 @x-ocaml.universe ./universe-v2 4 4 @x-ocaml.worker ./universe-v2/worker.js 5 5 6 - This page uses {b yojson 2.2.2} from a separate universe directory. 6 + This page uses {b cmdliner 1.3.0} from a separate universe directory. 7 7 8 - {@ocaml[ 9 - #require "yojson" 8 + {@ocaml x[ 9 + #require "cmdliner" 10 10 ]} 11 11 12 - {@ocaml[ 13 - (* Yojson 2.x API *) 14 - let json = `Assoc [("key", `String "value")] 15 - let s = Yojson.Safe.to_string json 16 - let () = print_endline s 12 + {@ocaml x[ 13 + (* Cmdliner 1.x: define an argument and build a command *) 14 + let greeting = 15 + Cmdliner.Arg.(value & opt string "World" & info ["name"] ~docv:"NAME") 16 + 17 + let greet_term = 18 + Cmdliner.Term.(const (Printf.printf "Hello, %s!\n") $ greeting) 19 + 20 + let cmd = Cmdliner.Cmd.v (Cmdliner.Cmd.info "greet" ~doc:"A greeting command") greet_term 21 + let () = Printf.printf "Command: %s\n" (Cmdliner.Cmd.name cmd) 17 22 ]} 18 23 19 - {@ocaml[ 20 - (* Yojson 2.x: Yojson.Safe.prettify is a string->string function *) 21 - let ugly = Yojson.Safe.to_string (`Assoc [("compact", `Bool true); ("data", `List [`Int 1; `Int 2; `Int 3])]) 22 - let pretty = Yojson.Safe.prettify ugly 23 - let () = print_endline pretty 24 + {@ocaml x[ 25 + (* Cmdliner 1.x: multiple arguments *) 26 + let verbose = Cmdliner.Arg.(value & flag & info ["v"; "verbose"]) 27 + let count = Cmdliner.Arg.(value & opt int 1 & info ["count"] ~docv:"N") 28 + let term = Cmdliner.Term.(const (fun v n -> Printf.printf "verbose=%b count=%d\n" v n) $ verbose $ count) 29 + let () = print_endline "Term with multiple arguments defined" 24 30 ]}
+20 -14
doc/demo2_v3.mld
··· 1 - {0 Yojson v3 Demo} 1 + {0 Cmdliner v2 Demo} 2 2 3 3 @x-ocaml.universe ./universe-v3 4 4 @x-ocaml.worker ./universe-v3/worker.js 5 5 6 - This page uses {b yojson 3.0.0} from a separate universe directory. 6 + This page uses {b cmdliner 2.1.0} from a separate universe directory. 7 7 8 - {@ocaml[ 9 - #require "yojson" 8 + {@ocaml x[ 9 + #require "cmdliner" 10 10 ]} 11 11 12 - {@ocaml[ 13 - (* Yojson 3.0 API *) 14 - let json = `Assoc [("key", `String "value")] 15 - let s = Yojson.Safe.to_string json 16 - let () = print_endline s 12 + {@ocaml x[ 13 + (* Cmdliner 2.x: same core API for simple cases *) 14 + let greeting = 15 + Cmdliner.Arg.(value & opt string "World" & info ["name"] ~docv:"NAME") 16 + 17 + let greet_term = 18 + Cmdliner.Term.(const (Printf.printf "Hello, %s!\n") $ greeting) 19 + 20 + let cmd = Cmdliner.Cmd.v (Cmdliner.Cmd.info "greet" ~doc:"A greeting command") greet_term 21 + let () = Printf.printf "Command: %s\n" (Cmdliner.Cmd.name cmd) 17 22 ]} 18 23 19 - {@ocaml[ 20 - (* Build and query JSON *) 21 - let parsed = `Assoc [("x", `Int 42); ("y", `String "hello")] 22 - let x = Yojson.Safe.Util.member "x" parsed 23 - let () = Printf.printf "x = %s\n" (Yojson.Safe.to_string x) 24 + {@ocaml x[ 25 + (* Cmdliner 2.x: Cmd.group for subcommands *) 26 + let sub1 = Cmdliner.Cmd.v (Cmdliner.Cmd.info "sub1") (Cmdliner.Term.const ()) 27 + let sub2 = Cmdliner.Cmd.v (Cmdliner.Cmd.info "sub2") (Cmdliner.Term.const ()) 28 + let group = Cmdliner.Cmd.group (Cmdliner.Cmd.info "myapp") [sub1; sub2] 29 + let () = Printf.printf "Group: %s\n" (Cmdliner.Cmd.name group) 24 30 ]}
+7 -7
doc/demo3_oxcaml.mld
··· 10 10 11 11 OxCaml adds Python/Haskell-style list and array comprehensions: 12 12 13 - {@ocaml[ 13 + {@ocaml x[ 14 14 let squares = [ x * x for x = 1 to 10 ] 15 15 16 16 let () = List.iter (fun x -> Printf.printf "%d " x) squares 17 17 ]} 18 18 19 - {@ocaml[ 19 + {@ocaml x[ 20 20 let evens = [ x for x = 1 to 20 when x mod 2 = 0 ] 21 21 22 22 let () = Printf.printf "Evens: %s\n" ··· 25 25 26 26 Nested comprehensions produce the cartesian product: 27 27 28 - {@ocaml[ 28 + {@ocaml x[ 29 29 let pairs = [ (x, y) for x = 1 to 3 for y = 1 to 3 when x <> y ] 30 30 31 31 let () = List.iter (fun (x, y) -> Printf.printf "(%d,%d) " x y) pairs ··· 35 35 36 36 Array comprehensions create arrays using the same syntax as list comprehensions: 37 37 38 - {@ocaml[ 38 + {@ocaml x[ 39 39 let squares = [| x * x for x = 1 to 10 |] 40 40 41 41 let () = Array.iter (fun x -> Printf.printf "%d " x) squares 42 42 ]} 43 43 44 - {@ocaml[ 44 + {@ocaml x[ 45 45 let fibs = 46 46 let a = Array.make 10 0 in 47 47 a.(0) <- 1; a.(1) <- 1; ··· 55 55 56 56 [let mutable] provides mutable local variables without heap allocation: 57 57 58 - {@ocaml[ 58 + {@ocaml x[ 59 59 let triangle n = 60 60 let mutable total = 0 in 61 61 for i = 1 to n do ··· 66 66 let () = Printf.printf "triangle 10 = %d\n" (triangle 10) 67 67 ]} 68 68 69 - {@ocaml[ 69 + {@ocaml x[ 70 70 let fizzbuzz n = 71 71 let mutable result = [] in 72 72 for i = n downto 1 do
+16 -14
doc/demo4_crossorigin.mld
··· 1 1 {0 Cross-Origin Demo} 2 2 3 - @x-ocaml.universe http://localhost:9090/universe 4 - @x-ocaml.worker http://localhost:9090/universe/worker.js 3 + @x-ocaml.universe https://jon.ludl.am/universe 4 + @x-ocaml.worker https://jon.ludl.am/universe/worker.js 5 5 6 6 This page demonstrates {b cross-origin} loading of OCaml universes. 7 - The page is served from [localhost:8080] while the worker and libraries 8 - are loaded from [localhost:9090], exercising the blob: URL worker 7 + The page is served from the main site while the worker and libraries 8 + are loaded from [jon.ludl.am], exercising the blob: URL worker 9 9 creation and sync XHR + eval library loading code paths. 10 10 11 11 {1 Basic Expression} 12 12 13 - {@ocaml[ 13 + {@ocaml x[ 14 14 1 + 2 * 3 15 15 ]} 16 16 17 - {@ocaml[ 17 + {@ocaml x[ 18 18 let greet name = Printf.sprintf "Hello, %s!" name 19 19 20 20 let () = print_endline (greet "Cross-Origin World") ··· 22 22 23 23 {1 Loading a Library} 24 24 25 - {@ocaml[ 26 - #require "yojson" 25 + {@ocaml x[ 26 + #require "cmdliner" 27 27 ]} 28 28 29 - {@ocaml[ 30 - let json = `Assoc [ 31 - ("origin", `String "cross-origin"); 32 - ("port", `Int 9090) 33 - ] 29 + {@ocaml x[ 30 + let greeting = 31 + Cmdliner.Arg.(value & opt string "Cross-Origin" & info ["name"] ~docv:"NAME") 32 + 33 + let greet_term = 34 + Cmdliner.Term.(const (Printf.printf "Hello from %s!\n") $ greeting) 34 35 35 - let () = print_endline (Yojson.Safe.pretty_to_string json) 36 + let cmd = Cmdliner.Cmd.v (Cmdliner.Cmd.info "cross-greet") greet_term 37 + let () = Printf.printf "Cross-origin command: %s\n" (Cmdliner.Cmd.name cmd) 36 38 ]}
+4 -4
doc/demo5_multiverse.mld
··· 20 20 21 21 {1 Basic Expression} 22 22 23 - {@ocaml[ 23 + {@ocaml x[ 24 24 1 + 2 * 3 25 25 ]} 26 26 27 - {@ocaml[ 27 + {@ocaml x[ 28 28 let greet name = Printf.sprintf "Hello, %s!" name 29 29 30 30 let () = print_endline (greet "Multiverse World") ··· 32 32 33 33 {1 Loading a Library} 34 34 35 - {@ocaml[ 35 + {@ocaml x[ 36 36 #require "yojson" 37 37 ]} 38 38 39 - {@ocaml[ 39 + {@ocaml x[ 40 40 let json = `Assoc [ 41 41 ("source", `String "multiverse"); 42 42 ("linked_universes", `Int 2)
+4 -4
doc/demo7_oxcaml_porting_real.mld
··· 29 29 30 30 Runnable miniature: 31 31 32 - {@ocaml[ 32 + {@ocaml x[ 33 33 let unlink_like win32_unlink is_win = 34 34 if is_win then win32_unlink else fun s -> "unlink " ^ s 35 35 ··· 53 53 54 54 Pedagogical simplified version: 55 55 56 - {@ocaml[ 56 + {@ocaml x[ 57 57 type 'a folder = 'a -> local_ (int -> char -> 'a) 58 58 59 59 let fold_chars (f : local_ 'a folder) acc s = ··· 86 86 87 87 Teaching-scale analogue: 88 88 89 - {@ocaml[ 89 + {@ocaml x[ 90 90 type frac = { global_ num : int; global_ den : int } 91 91 92 92 let compare__local (local_ a) (local_ b) = compare (a.num * b.den) (b.num * a.den) ··· 107 107 108 108 Typical shape: runtime probing + portability-safe fallback. 109 109 110 - {@ocaml[ 110 + {@ocaml x[ 111 111 module Runtime = struct 112 112 let runtime5 = false 113 113 let recommended_domain_count () = if runtime5 then 8 else 1
+3 -3
doc/demo_map.mld
··· 3 3 This page demonstrates a managed Leaflet map widget with FRP signals 4 4 and commands. 5 5 6 - {@ocaml[ 6 + {@ocaml x[ 7 7 #require "note";; 8 8 #require "js_top_worker-widget";; 9 9 #require "js_top_worker-widget-leaflet";; ··· 15 15 Click on the map to see coordinates. The click position is captured 16 16 as a [Note] event and displayed as a signal: 17 17 18 - {@ocaml[ 18 + {@ocaml x[ 19 19 let click_e, send_click = Note.E.create () 20 20 let last_click = Note.S.hold "No clicks yet" click_e 21 21 ··· 44 44 45 45 This cell sends a command to the map — clicking the button flies to Paris: 46 46 47 - {@ocaml[ 47 + {@ocaml x[ 48 48 let fly_view = 49 49 let open Widget.View in 50 50 Element { tag = "button"; attrs = [Handler ("click", "fly")];
+61 -5
doc/demo_widgets.mld
··· 3 3 This page demonstrates interactive FRP widgets powered by 4 4 [Widget] and [Note]. 5 5 6 - {@ocaml[ 6 + {@ocaml x[ 7 7 #require "note";; 8 8 #require "js_top_worker-widget";; 9 9 ]} ··· 12 12 13 13 A simple widget that renders static HTML: 14 14 15 - {@ocaml[ 15 + {@ocaml x[ 16 16 let open Widget.View in 17 17 Widget.display ~id:"hello" ~handlers:[] 18 18 (Element { tag = "div"; attrs = []; ··· 24 24 A counter driven by [Note] signals. Pressing the buttons sends events 25 25 back to the worker, which updates the signal: 26 26 27 - {@ocaml[ 27 + {@ocaml x[ 28 28 let inc_e, send_inc = Note.E.create () 29 29 let dec_e, send_dec = Note.E.create () 30 30 ··· 68 68 An input slider that drives a signal. Moving the slider sends the value 69 69 back to the worker: 70 70 71 - {@ocaml[ 71 + {@ocaml x[ 72 72 let x_e, send_x = Note.E.create () 73 73 let x = Note.S.hold 50 x_e 74 74 ··· 103 103 This widget derives from the slider signal [x] defined above. Moving 104 104 the slider updates this widget too: 105 105 106 - {@ocaml[ 106 + {@ocaml x[ 107 107 let doubled_view v = 108 108 let open Widget.View in 109 109 Element { tag = "div"; attrs = []; children = [ ··· 120 120 (Widget.update ~id:"doubled") 121 121 let () = Note.Logr.hold _logr3 122 122 ]} 123 + 124 + {1 Text Entry} 125 + 126 + A text input with a button. Typing in the textarea fires [text_changed], 127 + which updates the signal. Clicking "Shout" reads the current text and 128 + displays it in uppercase: 129 + 130 + {@ocaml x[ 131 + let text_e, send_text = Note.E.create () 132 + let text_s = Note.S.hold "hello world" text_e 133 + 134 + let shout_e, send_shout = Note.E.create () 135 + let shouted = Note.S.hold "" shout_e 136 + 137 + let text_entry_view txt = 138 + let open Widget.View in 139 + Element { tag = "div"; attrs = []; children = [ 140 + Element { tag = "textarea"; attrs = [ 141 + Property ("rows", "2"); 142 + Handler ("input", "text_changed"); 143 + ]; children = [Text txt] }; 144 + Element { tag = "button"; attrs = [ 145 + Handler ("click", "shout"); 146 + ]; children = [Text "Shout"] }; 147 + ] } 148 + 149 + let result_view s = 150 + let open Widget.View in 151 + Element { tag = "div"; attrs = [ 152 + Style ("font-family", "monospace"); 153 + Style ("padding", "0.5em"); 154 + ]; children = [Text s] } 155 + 156 + let () = 157 + Widget.display ~id:"text-entry" 158 + ~handlers:[ 159 + "text_changed", (fun v -> send_text (Option.value ~default:"" v)); 160 + "shout", (fun _ -> 161 + let current = Note.S.value text_s in 162 + send_shout (String.uppercase_ascii current)); 163 + ] 164 + (text_entry_view "hello world") 165 + 166 + let () = 167 + Widget.display ~id:"text-result" 168 + ~handlers:[] 169 + (result_view "") 170 + 171 + let _logr_text = Note.S.log text_s (fun _ -> ()) 172 + let () = Note.Logr.hold _logr_text 173 + 174 + let _logr4 = Note.S.log 175 + (Note.S.map result_view shouted) 176 + (Widget.update ~id:"text-result") 177 + let () = Note.Logr.hold _logr4 178 + ]}
+2 -2
doc/focs_2020_q2.mld
··· 20 20 21 21 We will use the following type throughout: 22 22 23 - {@ocaml[ 23 + {@ocaml x[ 24 24 type colour = Red | Green | Blue 25 25 26 26 exception SizeMismatch ··· 82 82 83 83 Think about this, then check by evaluating: 84 84 85 - {@ocaml[ 85 + {@ocaml x[ 86 86 test 87 87 ]} 88 88
+4 -4
doc/focs_2024_q1.mld
··· 9 9 exam consisting of 10 questions, of which each student has answered 6. If a 10 10 student has not attempted a question, this is indicated by a zero in the list. 11 11 12 - {@ocaml[ 12 + {@ocaml x[ 13 13 type marks = int list 14 14 15 15 (* Some sample data to work with *) ··· 89 89 function that does this and returns an appropriate type. Remember that the 90 90 result is not defined for some [marks] values (e.g. all zeros). 91 91 92 - {@ocaml[ 92 + {@ocaml x[ 93 93 (* These are available for your use *) 94 94 let float_of_int = float_of_int 95 95 let sqrt = sqrt ··· 148 148 149 149 Now that you have the building blocks, try some explorations: 150 150 151 - {@ocaml[ 151 + {@ocaml x[ 152 152 (* Which question had the highest average mark? *) 153 153 let best_question = 154 154 let means = List.init 10 (fun q -> (q, qmean q results)) in ··· 159 159 Printf.printf "Best question: Q%d (mean %.1f)\n" (fst best) (snd best) 160 160 ]} 161 161 162 - {@ocaml[ 162 + {@ocaml x[ 163 163 (* Per-student statistics *) 164 164 let () = 165 165 List.iteri (fun i row ->
+2 -2
doc/focs_2025_q2.mld
··· 7 7 The following type definition allows the representation of some mathematical 8 8 expressions as an OCaml value: 9 9 10 - {@ocaml[ 10 + {@ocaml x[ 11 11 type expr = 12 12 | Add of expr * expr 13 13 | Mul of expr * expr ··· 95 95 96 96 Here is one possible type definition for [t] and a framework for [reduce]: 97 97 98 - {@ocaml[ 98 + {@ocaml x[ 99 99 (* A possible type definition -- adjust to match your own *) 100 100 type t = Plus | Times | Num of int 101 101 ]}
+21 -1
src/interactive_extension.ml
··· 89 89 (** Recognised cell modes — first bare tag matching one of these wins. *) 90 90 let mode_tags = [ "interactive"; "exercise"; "test"; "hidden" ] 91 91 92 + (** All tags recognised by this extension. A code block must carry at 93 + least one of these (or [x]) to opt in to interactive treatment. 94 + Plain [{[...]}] and [{@ocaml[...]}] without tags are left alone. *) 95 + let known_tags = 96 + [ "x"; "interactive"; "exercise"; "test"; "hidden"; 97 + "autorun"; "skip"; "no-merlin" ] 98 + 92 99 module X_ocaml_code : Api.Code_Block_Extension = struct 93 100 let prefix = "ocaml" 94 101 95 102 let to_document meta code = 96 103 let tags = meta.Api.tags in 104 + (* Opt-in: the block must carry at least one recognised bare tag or 105 + a known key=value binding (id, for, env, run-on) to be treated as 106 + interactive. This prevents plain {[...]} code blocks from being 107 + hijacked. *) 108 + let bare = Api.get_all_tags tags in 109 + let has_known_tag = 110 + List.exists (fun t -> List.mem t known_tags) bare 111 + in 112 + let has_known_binding = 113 + List.exists (fun k -> Api.get_binding k tags <> None) 114 + [ "id"; "for"; "env"; "run-on"; "kind" ] 115 + in 116 + if not (has_known_tag || has_known_binding) then None 117 + else 97 118 (* Mode: first bare tag in mode_tags, default "interactive" *) 98 119 let mode = 99 - let bare = Api.get_all_tags tags in 100 120 match List.find_opt (fun t -> List.mem t mode_tags) bare with 101 121 | Some m -> m 102 122 | None -> "interactive"