A fork of mtelver's day10 project
1# Interactive OCaml Tutorials — User's Guide
2
3This guide is for tutorial authors and web developers who want to create
4interactive OCaml content — live code cells, exercises, and widgets — served
5as static HTML pages with no server-side component.
6
7## How it works
8
9Authors write documentation in `.mld` files using odoc's tagged code block
10syntax. The odoc plugin translates `{@ocaml ...}` blocks into `<x-ocaml>`
11HTML elements. A WebComponent (`x-ocaml.js`) and a Web Worker (`worker.js`)
12handle all interactivity in the browser: editing, execution, autocompletion,
13and type feedback.
14
15```
16Author writes .mld odoc plugin x-ocaml + worker
17───────────────────── ──> ──────────────── ──> ─────────────────
18{@ocaml exercise <x-ocaml WebComponent reads
19 id=factorial mode="exercise" data attrs, manages
20 [let facr n = ...]} data-id="factorial"> UI, sends code to
21 ... worker for execution
22 </x-ocaml>
23```
24
25There is no server-side component beyond serving static files over HTTP.
26
27## Universes
28
29A **universe** is a self-consistent set of compiled OCaml packages. OCaml
30requires that all libraries in a universe are built with exactly the same
31versions of all transitive dependencies — you cannot mix libraries from
32different build environments.
33
34Each universe is identified by a content hash and has a single entry point:
35`findlib_index.json`. This file tells the runtime which compiler version to
36use and where to find every package's artifacts.
37
38### Content sources
39
40**The ocaml.org multiverse** — the OCaml package documentation site hosts
41pre-built universes for published opam packages. Each package version's
42documentation page links to the correct universe for that package and its
43dependencies.
44
45**Self-hosted** — you can generate and host your own universes for custom
46package sets or private libraries. See the Admin Guide for instructions on
47using `jtw opam` or `day10` to produce these artifacts. Serve the output
48directory over HTTP with appropriate CORS headers if loading cross-origin.
49
50### Directory layout
51
52```
53compiler/<ocaml-version>/<hash>/
54 worker.js -- the OCaml toplevel web worker
55 lib/ocaml/
56 *.cmi, stdlib.cma.js -- stdlib artifacts
57
58p/<package>/<version>/<hash>/
59 lib/<findlib-name>/
60 META, *.cmi, *.cma.js -- package artifacts
61
62u/<universe-hash>/
63 findlib_index.json -- entry point
64```
65
66All paths include a content hash, making them safe to cache indefinitely.
67
68## Authoring tutorials
69
70### Page-level configuration
71
72Custom tags at the top of the `.mld` file configure the page:
73
74```
75@x-ocaml.universe https://ocaml.org/universe/5.3.0
76@x-ocaml.requires cmdliner, astring
77@x-ocaml.auto-execute false
78@x-ocaml.merlin false
79```
80
81| Tag | Default | Purpose |
82|-----|---------|---------|
83| `universe` | `./universe/` | URL where `findlib_index.json` lives |
84| `requires` | none | Packages to preload before any cells run |
85| `auto-execute` | `true` | Whether cells run automatically on page load |
86| `merlin` | `true` | Whether Merlin-based LSP feedback is enabled |
87
88### Cell types
89
90Code blocks use odoc's tagged code block syntax:
91`{@ocaml <attributes> [...code...]}`.
92
93| Attribute | Purpose | Editable? | Visible? |
94|---------------|---------------------------------|-----------|----------|
95| `interactive` | Demo or example cell | No | Yes |
96| `exercise` | Skeleton for the reader to edit | Yes | Yes |
97| `test` | Immutable test assertions | No | Yes |
98| `hidden` | Setup code, runs but not shown | No | No |
99
100### Per-cell attributes
101
102| Attribute | Purpose |
103|------------|----------------------------------------|
104| `id=name` | Name this cell for explicit linking |
105| `for=name` | Link a test cell to a specific exercise |
106| `env=name` | Named execution environment |
107| `merlin` | Override page-level merlin setting |
108
109### Execution environments
110
111Cells sharing an `env` attribute see each other's definitions. By default,
112all cells on a page share one environment. Named environments allow
113isolation when needed:
114
115```
116{@ocaml hidden env=greetings [
117let greeting = "Hello"
118]}
119
120{@ocaml interactive env=greetings [
121Printf.printf "%s, world!\n" greeting
122]}
123
124{@ocaml interactive env=math [
125(* This cell cannot see 'greeting' *)
126let pi = Float.pi
127]}
128```
129
130### Exercise linking
131
132Test cells are linked to exercise cells by two mechanisms:
133
134- **Positional (default)** — a test cell applies to the nearest preceding
135 exercise cell.
136- **Explicit** — use `id` and `for` attributes when the test is distant or
137 ambiguous.
138
139## Examples
140
141### Interactive tutorial
142
143A step-by-step walkthrough where cells build on each other:
144
145```
146@x-ocaml.universe https://ocaml.org/universe/5.4.0
147@x-ocaml.requires fmt
148
149{1 Working with Fmt}
150
151The [Fmt] library provides composable pretty-printing combinators.
152
153{@ocaml interactive [
154let pp_greeting ppf name =
155 Fmt.pf ppf "Hello, %s!" name
156]}
157
158Try it:
159
160{@ocaml interactive [
161Fmt.pr "%a@." pp_greeting "world"
162]}
163```
164
165### Assessment worksheet
166
167An exercise with hidden setup, editable skeleton, and visible tests:
168
169```
170{@ocaml hidden [
171(* Setup code the student doesn't see *)
172let check_positive f =
173 assert (f 0 = 1);
174 assert (f 1 = 1)
175]}
176
177Write an OCaml function [facr] to compute the factorial by recursion.
178
179{@ocaml exercise id=factorial [
180let rec facr n =
181 (* YOUR CODE HERE *)
182 failwith "Not implemented"
183]}
184
185{@ocaml test for=factorial [
186assert (facr 10 = 3628800);;
187assert (facr 11 = 39916800);;
188]}
189```
190
191### Generated HTML
192
193The odoc plugin maps attributes directly to HTML data attributes:
194
195```html
196<x-ocaml mode="hidden">
197(* Setup code the student doesn't see *)
198let check_positive f = ...
199</x-ocaml>
200
201<p>Write an OCaml function <code>facr</code> to compute the factorial
202by recursion.</p>
203
204<x-ocaml mode="exercise" data-id="factorial">
205let rec facr n =
206 (* YOUR CODE HERE *)
207 failwith "Not implemented"
208</x-ocaml>
209
210<x-ocaml mode="test" data-for="factorial">
211assert (facr 10 = 3628800);;
212assert (facr 11 = 39916800);;
213</x-ocaml>
214```
215
216The plugin also injects a `<script>` tag for `x-ocaml.js` (once per page)
217and `<meta>` tags for page-level configuration.
218
219## Using x-ocaml outside odoc
220
221The `<x-ocaml>` WebComponent works in any HTML page — it doesn't require
222odoc. You can write the elements by hand:
223
224```html
225<!DOCTYPE html>
226<html>
227<head>
228 <meta name="x-ocaml-universe"
229 content="https://ocaml.org/universe/5.4.0">
230 <meta name="x-ocaml-requires" content="str">
231 <script src="https://ocaml.org/jtw/x-ocaml.js" type="module"></script>
232</head>
233<body>
234 <x-ocaml mode="interactive">
235 let words = Str.split (Str.regexp " ") "hello world"
236 </x-ocaml>
237
238 <x-ocaml mode="exercise" data-id="reverse">
239 let reverse lst =
240 (* YOUR CODE HERE *)
241 failwith "todo"
242 </x-ocaml>
243
244 <x-ocaml mode="test" data-for="reverse">
245 assert (reverse [1;2;3] = [3;2;1]);;
246 </x-ocaml>
247</body>
248</html>
249```
250
251This makes the system usable with other documentation generators, static
252site builders, or hand-written HTML.
253
254## Client library (advanced)
255
256For custom integrations that need direct control over the Web Worker, the
257`OcamlWorker` JavaScript class provides a programmatic API.
258
259### Creating a worker
260
261```javascript
262import { OcamlWorker } from './ocaml-worker.js';
263
264const indexUrl = './u/UNIVERSE_HASH/findlib_index.json';
265const { worker, stdlib_dcs, findlib_index } =
266 await OcamlWorker.fromIndex(indexUrl, '.', { timeout: 120000 });
267
268await worker.init({
269 findlib_requires: ['fmt'],
270 stdlib_dcs,
271 findlib_index,
272});
273```
274
275### Evaluating code
276
277```javascript
278const result = await worker.eval('List.map (fun x -> x * 2) [1;2;3];;');
279console.log(result.caml_ppf);
280// val x : int list = [2; 4; 6]
281```
282
283The result object contains:
284
285| Field | Type | Description |
286|-------------|-------------|------------------------------------------------|
287| `caml_ppf` | `string` | Toplevel-style output (e.g. `val x : int = 3`) |
288| `stdout` | `string` | Anything printed to stdout |
289| `stderr` | `string` | Warnings and errors |
290| `mime_vals` | `MimeVal[]` | Rich output (HTML, SVG, images) |
291
292### Other methods
293
294- **`complete(code, pos)`** — autocompletion suggestions at a cursor position
295- **`typeAt(code, pos)`** — type of the expression at a position
296- **`errors(code)`** — check code for errors without executing it
297- **`createEnv(name)` / `destroyEnv(name)`** — manage isolated execution
298 environments
299- **`terminate()`** — shut down the web worker
300
301### Rich output (MIME values)
302
303`eval()` results may include `mime_vals` — an array of objects with
304`mime_type` and `data`. Libraries that produce graphical output (plotting,
305diagrams) use this mechanism to display results in the browser. Common types:
306`text/html`, `image/svg+xml`, `image/png`.
307
308### Loading libraries at runtime
309
310In addition to preloading via `findlib_requires`, users can load libraries
311dynamically:
312
313```ocaml
314#require "str";;
315Str.split (Str.regexp " ") "hello world";;
316```
317
318This works for any library present in the universe's `findlib_index.json`.
319
320## What's next
321
322- **Scrollycode tutorials** — scroll-driven code walkthroughs using
323 `odoc-scrollycode-extension` (already prototyped)
324- **Interactive widgets** — reactive UI elements (sliders, plots, mini-apps)
325 driven by an FRP library running in the Worker (experimental)