···11+{0 Building a REPL}
22+33+@scrolly.dark Building a REPL in OCaml
44+{ol
55+{li
66+ {b The Expression Type}
77+88+ A REPL evaluates expressions. We start with a tiny language:
99+ integer literals, addition, let-bindings, and variables.
1010+ Four constructors is all we need.
1111+1212+ {[
1313+type expr =
1414+ | Lit of int
1515+ | Add of expr * expr
1616+ | Let of string * expr * expr
1717+ | Var of string
1818+ ]}
1919+}
2020+{li
2121+ {b Values and Environments}
2222+2323+ Evaluation produces values. For now, just integers. An
2424+ environment maps variable names to their values using a
2525+ simple association list.
2626+2727+ {[
2828+type expr =
2929+ | Lit of int
3030+ | Add of expr * expr
3131+ | Let of string * expr * expr
3232+ | Var of string
3333+3434+type value = Int of int
3535+3636+type env = (string * value) list
3737+3838+let empty_env : env = []
3939+4040+let extend env name v = (name, v) :: env
4141+4242+let lookup env name =
4343+ match List.assoc_opt name env with
4444+ | Some v -> v
4545+ | None -> failwith ("unbound: " ^ name)
4646+ ]}
4747+}
4848+{li
4949+ {b The Evaluator}
5050+5151+ Pattern matching makes the evaluator beautifully direct.
5252+ Each expression form maps to a straightforward computation.
5353+ Let-bindings extend the environment for the body expression.
5454+5555+ {[
5656+type expr =
5757+ | Lit of int
5858+ | Add of expr * expr
5959+ | Let of string * expr * expr
6060+ | Var of string
6161+6262+type value = Int of int
6363+6464+type env = (string * value) list
6565+6666+let empty_env : env = []
6767+6868+let extend env name v = (name, v) :: env
6969+7070+let lookup env name =
7171+ match List.assoc_opt name env with
7272+ | Some v -> v
7373+ | None -> failwith ("unbound: " ^ name)
7474+7575+let rec eval env = function
7676+ | Lit n -> Int n
7777+ | Add (a, b) ->
7878+ let (Int x) = eval env a in
7979+ let (Int y) = eval env b in
8080+ Int (x + y)
8181+ | Let (name, rhs, body) ->
8282+ let v = eval env rhs in
8383+ eval (extend env name v) body
8484+ | Var name -> lookup env name
8585+ ]}
8686+}
8787+{li
8888+ {b A Tiny Tokenizer}
8989+9090+ To read user input, we need a tokenizer. It splits a string
9191+ into meaningful chunks: numbers, identifiers, operators, and
9292+ parentheses. Whitespace is consumed but not produced.
9393+9494+ {[
9595+type expr =
9696+ | Lit of int
9797+ | Add of expr * expr
9898+ | Let of string * expr * expr
9999+ | Var of string
100100+101101+type value = Int of int
102102+type env = (string * value) list
103103+let empty_env : env = []
104104+let extend env name v = (name, v) :: env
105105+let lookup env name =
106106+ match List.assoc_opt name env with
107107+ | Some v -> v
108108+ | None -> failwith ("unbound: " ^ name)
109109+110110+let rec eval env = function
111111+ | Lit n -> Int n
112112+ | Add (a, b) ->
113113+ let (Int x) = eval env a in
114114+ let (Int y) = eval env b in
115115+ Int (x + y)
116116+ | Let (name, rhs, body) ->
117117+ let v = eval env rhs in
118118+ eval (extend env name v) body
119119+ | Var name -> lookup env name
120120+121121+type token =
122122+ | TNum of int
123123+ | TIdent of string
124124+ | TPlus | TEqual
125125+ | TLParen | TRParen
126126+ | TLet | TIn
127127+128128+let is_alpha c =
129129+ (c >= 'a' && c <= 'z')
130130+ || (c >= 'A' && c <= 'Z')
131131+ || c = '_'
132132+133133+let is_digit c = c >= '0' && c <= '9'
134134+135135+let tokenize input =
136136+ let len = String.length input in
137137+ let pos = ref 0 in
138138+ let tokens = ref [] in
139139+ while !pos < len do
140140+ let c = input.[!pos] in
141141+ if c = ' ' || c = '\t' || c = '\n' then
142142+ incr pos
143143+ else if is_digit c then begin
144144+ let start = !pos in
145145+ while !pos < len && is_digit input.[!pos] do
146146+ incr pos done;
147147+ let s = String.sub input start (!pos - start) in
148148+ tokens := TNum (int_of_string s) :: !tokens
149149+ end else if is_alpha c then begin
150150+ let start = !pos in
151151+ while !pos < len && is_alpha input.[!pos] do
152152+ incr pos done;
153153+ let s = String.sub input start (!pos - start) in
154154+ let tok = match s with
155155+ | "let" -> TLet | "in" -> TIn
156156+ | _ -> TIdent s in
157157+ tokens := tok :: !tokens
158158+ end else begin
159159+ let tok = match c with
160160+ | '+' -> TPlus | '=' -> TEqual
161161+ | '(' -> TLParen | ')' -> TRParen
162162+ | _ -> failwith "unexpected char" in
163163+ tokens := tok :: !tokens;
164164+ incr pos
165165+ end
166166+ done;
167167+ List.rev !tokens
168168+ ]}
169169+}
170170+{li
171171+ {b The Parser}
172172+173173+ A recursive descent parser turns tokens into our expression AST.
174174+ It handles operator precedence naturally: addition is parsed as
175175+ a left-associative chain of atoms.
176176+177177+ {[
178178+type expr =
179179+ | Lit of int
180180+ | Add of expr * expr
181181+ | Let of string * expr * expr
182182+ | Var of string
183183+184184+type value = Int of int
185185+type env = (string * value) list
186186+let empty_env : env = []
187187+let extend env name v = (name, v) :: env
188188+let lookup env name =
189189+ match List.assoc_opt name env with
190190+ | Some v -> v
191191+ | None -> failwith ("unbound: " ^ name)
192192+193193+let rec eval env = function
194194+ | Lit n -> Int n
195195+ | Add (a, b) ->
196196+ let (Int x) = eval env a in
197197+ let (Int y) = eval env b in
198198+ Int (x + y)
199199+ | Let (name, rhs, body) ->
200200+ let v = eval env rhs in
201201+ eval (extend env name v) body
202202+ | Var name -> lookup env name
203203+204204+type token =
205205+ | TNum of int | TIdent of string
206206+ | TPlus | TEqual
207207+ | TLParen | TRParen
208208+ | TLet | TIn
209209+210210+let is_alpha c =
211211+ (c >= 'a' && c <= 'z')
212212+ || (c >= 'A' && c <= 'Z') || c = '_'
213213+let is_digit c = c >= '0' && c <= '9'
214214+215215+let tokenize input =
216216+ let len = String.length input in
217217+ let pos = ref 0 in
218218+ let tokens = ref [] in
219219+ while !pos < len do
220220+ let c = input.[!pos] in
221221+ if c = ' ' || c = '\t' || c = '\n' then
222222+ incr pos
223223+ else if is_digit c then begin
224224+ let start = !pos in
225225+ while !pos < len && is_digit input.[!pos]
226226+ do incr pos done;
227227+ let s = String.sub input start
228228+ (!pos - start) in
229229+ tokens := TNum (int_of_string s) :: !tokens
230230+ end else if is_alpha c then begin
231231+ let start = !pos in
232232+ while !pos < len && is_alpha input.[!pos]
233233+ do incr pos done;
234234+ let s = String.sub input start
235235+ (!pos - start) in
236236+ let tok = match s with
237237+ | "let" -> TLet | "in" -> TIn
238238+ | _ -> TIdent s in
239239+ tokens := tok :: !tokens
240240+ end else begin
241241+ let tok = match c with
242242+ | '+' -> TPlus | '=' -> TEqual
243243+ | '(' -> TLParen | ')' -> TRParen
244244+ | _ -> failwith "unexpected char" in
245245+ tokens := tok :: !tokens; incr pos
246246+ end
247247+ done;
248248+ List.rev !tokens
249249+250250+let parse tokens =
251251+ let toks = ref tokens in
252252+ let next () =
253253+ match !toks with
254254+ | [] -> failwith "unexpected end"
255255+ | t :: rest -> toks := rest; t in
256256+ let peek () =
257257+ match !toks with [] -> None | t :: _ -> Some t in
258258+ let rec parse_expr () =
259259+ let left = parse_atom () in
260260+ parse_add left
261261+ and parse_add left =
262262+ match peek () with
263263+ | Some TPlus ->
264264+ ignore (next ());
265265+ let right = parse_atom () in
266266+ parse_add (Add (left, right))
267267+ | _ -> left
268268+ and parse_atom () =
269269+ match next () with
270270+ | TNum n -> Lit n
271271+ | TIdent s -> Var s
272272+ | TLParen ->
273273+ let e = parse_expr () in
274274+ ignore (next ()); e
275275+ | TLet ->
276276+ let (TIdent name) = next () in
277277+ ignore (next ());
278278+ let rhs = parse_expr () in
279279+ ignore (next ());
280280+ let body = parse_expr () in
281281+ Let (name, rhs, body)
282282+ | _ -> failwith "unexpected token" in
283283+ parse_expr ()
284284+ ]}
285285+}
286286+{li
287287+ {b The Read-Eval-Print Loop}
288288+289289+ Now we connect all the pieces. The REPL reads a line,
290290+ tokenizes it, parses the tokens, evaluates the expression,
291291+ and prints the result. A persistent environment accumulates
292292+ bindings across interactions.
293293+294294+ {[
295295+type expr =
296296+ | Lit of int
297297+ | Add of expr * expr
298298+ | Let of string * expr * expr
299299+ | Var of string
300300+301301+type value = Int of int
302302+type env = (string * value) list
303303+let empty_env : env = []
304304+let extend env name v = (name, v) :: env
305305+let lookup env name =
306306+ match List.assoc_opt name env with
307307+ | Some v -> v
308308+ | None -> failwith ("unbound: " ^ name)
309309+310310+let rec eval env = function
311311+ | Lit n -> Int n
312312+ | Add (a, b) ->
313313+ let (Int x) = eval env a in
314314+ let (Int y) = eval env b in
315315+ Int (x + y)
316316+ | Let (name, rhs, body) ->
317317+ let v = eval env rhs in
318318+ eval (extend env name v) body
319319+ | Var name -> lookup env name
320320+321321+type token =
322322+ | TNum of int | TIdent of string
323323+ | TPlus | TEqual
324324+ | TLParen | TRParen
325325+ | TLet | TIn
326326+327327+let is_alpha c =
328328+ (c >= 'a' && c <= 'z')
329329+ || (c >= 'A' && c <= 'Z') || c = '_'
330330+let is_digit c = c >= '0' && c <= '9'
331331+332332+let tokenize input =
333333+ let len = String.length input in
334334+ let pos = ref 0 in
335335+ let tokens = ref [] in
336336+ while !pos < len do
337337+ let c = input.[!pos] in
338338+ if c = ' ' || c = '\t' || c = '\n' then
339339+ incr pos
340340+ else if is_digit c then begin
341341+ let start = !pos in
342342+ while !pos < len && is_digit input.[!pos]
343343+ do incr pos done;
344344+ tokens := TNum (int_of_string
345345+ (String.sub input start
346346+ (!pos - start))) :: !tokens
347347+ end else if is_alpha c then begin
348348+ let start = !pos in
349349+ while !pos < len && is_alpha input.[!pos]
350350+ do incr pos done;
351351+ let s = String.sub input start
352352+ (!pos - start) in
353353+ tokens := (match s with
354354+ | "let" -> TLet | "in" -> TIn
355355+ | _ -> TIdent s) :: !tokens
356356+ end else begin
357357+ tokens := (match c with
358358+ | '+' -> TPlus | '=' -> TEqual
359359+ | '(' -> TLParen | ')' -> TRParen
360360+ | _ -> failwith "unexpected") :: !tokens;
361361+ incr pos
362362+ end
363363+ done; List.rev !tokens
364364+365365+let parse tokens =
366366+ let toks = ref tokens in
367367+ let next () = match !toks with
368368+ | [] -> failwith "end"
369369+ | t :: r -> toks := r; t in
370370+ let peek () = match !toks with
371371+ | [] -> None | t :: _ -> Some t in
372372+ let rec expr () =
373373+ let l = atom () in add l
374374+ and add left = match peek () with
375375+ | Some TPlus ->
376376+ ignore (next ());
377377+ add (Add (left, atom ()))
378378+ | _ -> left
379379+ and atom () = match next () with
380380+ | TNum n -> Lit n
381381+ | TIdent s -> Var s
382382+ | TLParen ->
383383+ let e = expr () in
384384+ ignore (next ()); e
385385+ | TLet ->
386386+ let (TIdent name) = next () in
387387+ ignore (next ());
388388+ let rhs = expr () in
389389+ ignore (next ());
390390+ Let (name, rhs, expr ())
391391+ | _ -> failwith "unexpected" in
392392+ expr ()
393393+394394+let print_value = function
395395+ | Int n -> Printf.printf "=> %d\n" n
396396+397397+let repl () =
398398+ let env = ref empty_env in
399399+ try while true do
400400+ print_string "> ";
401401+ let line = input_line stdin in
402402+ let tokens = tokenize line in
403403+ let ast = parse tokens in
404404+ let result = eval !env ast in
405405+ print_value result
406406+ done with End_of_file ->
407407+ print_endline "Goodbye."
408408+409409+let () = repl ()
410410+ ]}
411411+}
412412+}
···11+{0 Building a Test Framework}
22+33+@scrolly.notebook Building a Test Framework in OCaml
44+{ol
55+{li
66+ {b A Single Assertion}
77+88+ The simplest possible test: check that a condition holds.
99+ If it fails, raise an exception with a message. This is
1010+ the foundation everything else builds on.
1111+1212+ {[
1313+exception Test_failure of string
1414+1515+let assert_equal ~expected ~actual msg =
1616+ if expected <> actual then
1717+ raise (Test_failure
1818+ (Printf.sprintf "%s: expected %s, got %s"
1919+ msg
2020+ (string_of_int expected)
2121+ (string_of_int actual)))
2222+ ]}
2323+}
2424+{li
2525+ {b Collecting Tests}
2626+2727+ A test is a named function. We store tests in a mutable list
2828+ so they can be registered declaratively with a simple helper.
2929+ Each test is just a unit function that might raise.
3030+3131+ {[
3232+exception Test_failure of string
3333+3434+let assert_equal ~expected ~actual msg =
3535+ if expected <> actual then
3636+ raise (Test_failure
3737+ (Printf.sprintf "%s: expected %s, got %s"
3838+ msg
3939+ (string_of_int expected)
4040+ (string_of_int actual)))
4141+4242+type test = {
4343+ name : string;
4444+ fn : unit -> unit;
4545+}
4646+4747+let tests : test list ref = ref []
4848+4949+let register name fn =
5050+ tests := { name; fn } :: !tests
5151+5252+let () = register "addition" (fun () ->
5353+ assert_equal ~expected:4 ~actual:(2 + 2)
5454+ "two plus two")
5555+5656+let () = register "multiplication" (fun () ->
5757+ assert_equal ~expected:6 ~actual:(2 * 3)
5858+ "two times three")
5959+ ]}
6060+}
6161+{li
6262+ {b A Test Runner}
6363+6464+ The runner iterates through registered tests, catching
6565+ exceptions to report pass or fail. It counts results
6666+ and prints a summary at the end.
6767+6868+ {[
6969+exception Test_failure of string
7070+7171+let assert_equal ~expected ~actual msg =
7272+ if expected <> actual then
7373+ raise (Test_failure
7474+ (Printf.sprintf "%s: expected %s, got %s"
7575+ msg
7676+ (string_of_int expected)
7777+ (string_of_int actual)))
7878+7979+type test = {
8080+ name : string;
8181+ fn : unit -> unit;
8282+}
8383+8484+let tests : test list ref = ref []
8585+8686+let register name fn =
8787+ tests := { name; fn } :: !tests
8888+8989+type result =
9090+ | Pass
9191+ | Fail of string
9292+9393+let run_one test =
9494+ try test.fn (); Pass
9595+ with
9696+ | Test_failure msg -> Fail msg
9797+ | exn -> Fail (Printexc.to_string exn)
9898+9999+let run_all () =
100100+ let results =
101101+ List.rev !tests
102102+ |> List.map (fun t -> (t.name, run_one t))
103103+ in
104104+ let passed =
105105+ List.length
106106+ (List.filter
107107+ (fun (_, r) -> r = Pass) results)
108108+ in
109109+ let total = List.length results in
110110+ List.iter (fun (name, result) ->
111111+ match result with
112112+ | Pass ->
113113+ Printf.printf " PASS %s\n" name
114114+ | Fail msg ->
115115+ Printf.printf " FAIL %s: %s\n" name msg
116116+ ) results;
117117+ Printf.printf "\n%d/%d tests passed\n"
118118+ passed total;
119119+ if passed < total then exit 1
120120+ ]}
121121+}
122122+{li
123123+ {b Better Assertions}
124124+125125+ Real frameworks need more than integer equality. We add
126126+ string comparison, boolean checks, and a generic raises
127127+ assertion that checks an exception is thrown.
128128+129129+ {[
130130+exception Test_failure of string
131131+132132+let assert_equal ~expected ~actual msg =
133133+ if expected <> actual then
134134+ raise (Test_failure
135135+ (Printf.sprintf "%s: expected %s, got %s"
136136+ msg
137137+ (string_of_int expected)
138138+ (string_of_int actual)))
139139+140140+let assert_string_equal ~expected ~actual msg =
141141+ if expected <> actual then
142142+ raise (Test_failure
143143+ (Printf.sprintf
144144+ "%s: expected %S, got %S"
145145+ msg expected actual))
146146+147147+let assert_true condition msg =
148148+ if not condition then
149149+ raise (Test_failure msg)
150150+151151+let assert_raises fn msg =
152152+ try fn ();
153153+ raise (Test_failure
154154+ (msg ^ ": expected exception"))
155155+ with
156156+ | Test_failure _ as e -> raise e
157157+ | _ -> ()
158158+159159+type test = {
160160+ name : string;
161161+ fn : unit -> unit;
162162+}
163163+164164+let tests : test list ref = ref []
165165+166166+let register name fn =
167167+ tests := { name; fn } :: !tests
168168+169169+type result = Pass | Fail of string
170170+171171+let run_one test =
172172+ try test.fn (); Pass
173173+ with
174174+ | Test_failure msg -> Fail msg
175175+ | exn -> Fail (Printexc.to_string exn)
176176+177177+let run_all () =
178178+ let results =
179179+ List.rev !tests
180180+ |> List.map (fun t -> (t.name, run_one t))
181181+ in
182182+ let passed = List.length
183183+ (List.filter
184184+ (fun (_, r) -> r = Pass) results) in
185185+ let total = List.length results in
186186+ List.iter (fun (name, result) ->
187187+ match result with
188188+ | Pass ->
189189+ Printf.printf " PASS %s\n" name
190190+ | Fail msg ->
191191+ Printf.printf " FAIL %s: %s\n"
192192+ name msg
193193+ ) results;
194194+ Printf.printf "\n%d/%d tests passed\n"
195195+ passed total;
196196+ if passed < total then exit 1
197197+ ]}
198198+}
199199+{li
200200+ {b Test Suites}
201201+202202+ As projects grow, tests need organization. We add a suite
203203+ concept that groups related tests under a name. Suites
204204+ can be nested and run independently.
205205+206206+ {[
207207+exception Test_failure of string
208208+209209+let assert_equal ~expected ~actual msg =
210210+ if expected <> actual then
211211+ raise (Test_failure
212212+ (Printf.sprintf "%s: expected %s, got %s"
213213+ msg
214214+ (string_of_int expected)
215215+ (string_of_int actual)))
216216+217217+let assert_string_equal ~expected ~actual msg =
218218+ if expected <> actual then
219219+ raise (Test_failure
220220+ (Printf.sprintf "%s: expected %S, got %S"
221221+ msg expected actual))
222222+223223+let assert_true condition msg =
224224+ if not condition then
225225+ raise (Test_failure msg)
226226+227227+let assert_raises fn msg =
228228+ try fn ();
229229+ raise (Test_failure
230230+ (msg ^ ": expected exception"))
231231+ with Test_failure _ as e -> raise e | _ -> ()
232232+233233+type test = { name : string; fn : unit -> unit }
234234+type result = Pass | Fail of string
235235+236236+type suite = {
237237+ suite_name : string;
238238+ mutable suite_tests : test list;
239239+}
240240+241241+let suites : suite list ref = ref []
242242+243243+let create_suite name =
244244+ let s = { suite_name = name;
245245+ suite_tests = [] } in
246246+ suites := s :: !suites; s
247247+248248+let add_test suite name fn =
249249+ suite.suite_tests <-
250250+ { name; fn } :: suite.suite_tests
251251+252252+let run_one test =
253253+ try test.fn (); Pass
254254+ with
255255+ | Test_failure msg -> Fail msg
256256+ | exn -> Fail (Printexc.to_string exn)
257257+258258+let run_suite suite =
259259+ Printf.printf "Suite: %s\n" suite.suite_name;
260260+ let results =
261261+ List.rev suite.suite_tests
262262+ |> List.map (fun t ->
263263+ (t.name, run_one t)) in
264264+ let passed = List.length
265265+ (List.filter
266266+ (fun (_, r) -> r = Pass) results) in
267267+ let total = List.length results in
268268+ List.iter (fun (name, result) ->
269269+ match result with
270270+ | Pass ->
271271+ Printf.printf " PASS %s\n" name
272272+ | Fail msg ->
273273+ Printf.printf " FAIL %s: %s\n"
274274+ name msg
275275+ ) results;
276276+ Printf.printf " %d/%d passed\n\n"
277277+ passed total;
278278+ passed = total
279279+280280+let run_all_suites () =
281281+ let all_ok = List.for_all run_suite
282282+ (List.rev !suites) in
283283+ if not all_ok then exit 1
284284+ ]}
285285+}
286286+{li
287287+ {b Expect Tests}
288288+289289+ The crown jewel: expect tests capture actual output and
290290+ compare it to an expected snapshot. On first run, they
291291+ record the output. On later runs, they detect regressions.
292292+ This is how tools like ppx_expect and Cram tests work.
293293+294294+ {[
295295+exception Test_failure of string
296296+297297+let assert_equal ~expected ~actual msg =
298298+ if expected <> actual then
299299+ raise (Test_failure
300300+ (Printf.sprintf "%s: expected %s, got %s"
301301+ msg
302302+ (string_of_int expected)
303303+ (string_of_int actual)))
304304+305305+let assert_string_equal ~expected ~actual msg =
306306+ if expected <> actual then
307307+ raise (Test_failure
308308+ (Printf.sprintf "%s: expected %S, got %S"
309309+ msg expected actual))
310310+311311+let assert_true condition msg =
312312+ if not condition then
313313+ raise (Test_failure msg)
314314+315315+let assert_raises fn msg =
316316+ try fn ();
317317+ raise (Test_failure
318318+ (msg ^ ": expected exception"))
319319+ with Test_failure _ as e -> raise e | _ -> ()
320320+321321+type test = { name : string; fn : unit -> unit }
322322+type result = Pass | Fail of string
323323+324324+type suite = {
325325+ suite_name : string;
326326+ mutable suite_tests : test list;
327327+}
328328+329329+let suites : suite list ref = ref []
330330+331331+let create_suite name =
332332+ let s = { suite_name = name;
333333+ suite_tests = [] } in
334334+ suites := s :: !suites; s
335335+336336+let add_test suite name fn =
337337+ suite.suite_tests <-
338338+ { name; fn } :: suite.suite_tests
339339+340340+let run_one test =
341341+ try test.fn (); Pass
342342+ with
343343+ | Test_failure msg -> Fail msg
344344+ | exn -> Fail (Printexc.to_string exn)
345345+346346+(* Expect test infrastructure *)
347347+let expect_dir = "_expect"
348348+349349+let expect_test suite name fn =
350350+ add_test suite name (fun () ->
351351+ let buf = Buffer.create 256 in
352352+ fn (Buffer.add_string buf);
353353+ let actual = Buffer.contents buf in
354354+ let path = Printf.sprintf "%s/%s/%s.expected"
355355+ expect_dir suite.suite_name name in
356356+ if Sys.file_exists path then begin
357357+ let ic = open_in path in
358358+ let expected = really_input_string ic
359359+ (in_channel_length ic) in
360360+ close_in ic;
361361+ assert_string_equal
362362+ ~expected ~actual
363363+ (name ^ " snapshot")
364364+ end else begin
365365+ let dir = Filename.dirname path in
366366+ ignore (Sys.command
367367+ ("mkdir -p " ^ dir));
368368+ let oc = open_out path in
369369+ output_string oc actual;
370370+ close_out oc;
371371+ Printf.printf
372372+ " NEW %s (snapshot saved)\n" name
373373+ end)
374374+375375+let run_suite suite =
376376+ Printf.printf "Suite: %s\n" suite.suite_name;
377377+ let results =
378378+ List.rev suite.suite_tests
379379+ |> List.map (fun t ->
380380+ (t.name, run_one t)) in
381381+ let passed = List.length
382382+ (List.filter
383383+ (fun (_, r) -> r = Pass) results) in
384384+ let total = List.length results in
385385+ List.iter (fun (name, result) ->
386386+ match result with
387387+ | Pass ->
388388+ Printf.printf " PASS %s\n" name
389389+ | Fail msg ->
390390+ Printf.printf " FAIL %s: %s\n"
391391+ name msg
392392+ ) results;
393393+ Printf.printf " %d/%d passed\n\n"
394394+ passed total;
395395+ passed = total
396396+397397+let run_all_suites () =
398398+ let all_ok = List.for_all run_suite
399399+ (List.rev !suites) in
400400+ if not all_ok then exit 1
401401+ ]}
402402+}
403403+}
+14
test/scrollycode-demos/odoc_scrolly.ml
···11+(* Custom odoc binary with the scrollycode extension statically linked.
22+33+ The scrollycode extension registers itself when this module is loaded,
44+ via the [let () = ...] at the bottom of scrollycode_extension.ml.
55+66+ We force it to be linked by referencing it, then invoke the standard
77+ odoc CLI entry point. *)
88+99+(* Force-link the extension module *)
1010+let () =
1111+ ignore (Scrollycode_extension.Scrolly.prefix : string)
1212+1313+(* Include the full odoc CLI - this is main.ml without the dune-site loading *)
1414+include Odoc_scrolly_main
+1858
test/scrollycode-demos/odoc_scrolly_main.ml
···11+(* CR-someday trefis: the "deps" and "targets" subcommands currently output
22+ their result on stdout.
33+ It would make the interaction with jenga nicer if we could specify a file to
44+ output the result to. *)
55+66+open Odoc_utils
77+open ResultMonad
88+module List = ListLabels
99+open Odoc_odoc
1010+open Cmdliner
1111+1212+(* Load all installed extensions at startup *)
1313+1414+1515+let convert_syntax : Odoc_document.Renderer.syntax Arg.conv =
1616+ let syntax_parser str =
1717+ match str with
1818+ | "ml" | "ocaml" -> Ok Odoc_document.Renderer.OCaml
1919+ | "re" | "reason" -> Ok Odoc_document.Renderer.Reason
2020+ | s -> Error (Printf.sprintf "Unknown syntax '%s'" s)
2121+ in
2222+ let syntax_printer fmt syntax =
2323+ Format.pp_print_string fmt (Odoc_document.Renderer.string_of_syntax syntax)
2424+ in
2525+ Arg.conv' (syntax_parser, syntax_printer)
2626+2727+let convert_directory ?(create = false) () : Fs.Directory.t Arg.conv =
2828+ let dir_parser, dir_printer =
2929+ (Arg.conv_parser Arg.string, Arg.conv_printer Arg.string)
3030+ in
3131+ let odoc_dir_parser str =
3232+ let () = if create then Fs.Directory.(mkdir_p (of_string str)) in
3333+ match dir_parser str with
3434+ | Ok res -> Ok (Fs.Directory.of_string res)
3535+ | Error (`Msg e) -> Error e
3636+ in
3737+ let odoc_dir_printer fmt dir = dir_printer fmt (Fs.Directory.to_string dir) in
3838+ Arg.conv' (odoc_dir_parser, odoc_dir_printer)
3939+4040+(** On top of the conversion 'file' that checks that the passed file exists. *)
4141+let convert_fpath =
4242+ let parse inp =
4343+ match Arg.(conv_parser file) inp with
4444+ | Ok s -> Ok (Fs.File.of_string s)
4545+ | Error _ as e -> e
4646+ and print = Fpath.pp in
4747+ Arg.conv (parse, print)
4848+4949+let convert_named_root =
5050+ let parse inp =
5151+ match String.cuts inp ~sep:":" with
5252+ | [ s1; s2 ] -> Ok (s1, Fs.Directory.of_string s2)
5353+ | _ -> Error (`Msg "")
5454+ in
5555+ let print ppf (s, t) =
5656+ Format.fprintf ppf "%s:%s" s (Fs.Directory.to_string t)
5757+ in
5858+ Arg.conv (parse, print)
5959+6060+let handle_error = function
6161+ | Ok () -> ()
6262+ | Error (`Cli_error msg) ->
6363+ Printf.eprintf "%s\n%!" msg;
6464+ exit 2
6565+ | Error (`Msg msg) ->
6666+ Printf.eprintf "ERROR: %s\n%!" msg;
6767+ exit 1
6868+6969+module Antichain = struct
7070+ let absolute_normalization p =
7171+ let p =
7272+ if Fpath.is_rel p then Fpath.( // ) (Fpath.v (Sys.getcwd ())) p else p
7373+ in
7474+ Fpath.normalize p
7575+7676+ (** Check that a list of directories form an antichain: they are all disjoints
7777+ *)
7878+ let check ~opt l =
7979+ let l =
8080+ List.map
8181+ ~f:(fun p -> p |> Fs.Directory.to_fpath |> absolute_normalization)
8282+ l
8383+ in
8484+ let rec check = function
8585+ | [] -> true
8686+ | p1 :: rest ->
8787+ List.for_all
8888+ ~f:(fun p2 ->
8989+ (not (Fpath.is_prefix p1 p2)) && not (Fpath.is_prefix p2 p1))
9090+ rest
9191+ && check rest
9292+ in
9393+ if check l then Ok ()
9494+ else
9595+ let msg =
9696+ Format.sprintf "Paths given to all %s options must be disjoint" opt
9797+ in
9898+ Error (`Msg msg)
9999+end
100100+101101+let docs = "ARGUMENTS"
102102+103103+let odoc_file_directories =
104104+ let doc =
105105+ "Where to look for required $(i,.odoc) files. Can be present several times."
106106+ in
107107+ Arg.(
108108+ value
109109+ & opt_all (convert_directory ()) []
110110+ & info ~docs ~docv:"DIR" ~doc [ "I" ])
111111+112112+let hidden =
113113+ let doc =
114114+ "Mark the unit as hidden. (Useful for files included in module packs)."
115115+ in
116116+ Arg.(value & flag & info ~docs ~doc [ "hidden" ])
117117+118118+let extra_suffix =
119119+ let doc =
120120+ "Extra suffix to append to generated filenames. This is intended for \
121121+ expect tests to use."
122122+ in
123123+ let default = None in
124124+ Arg.(
125125+ value
126126+ & opt (some string) default
127127+ & info ~docv:"SUFFIX" ~doc [ "extra-suffix" ])
128128+129129+let warnings_options =
130130+ let warn_error =
131131+ let doc = "Turn warnings into errors." in
132132+ let env =
133133+ Cmd.Env.info "ODOC_WARN_ERROR" ~doc:(doc ^ " See option $(opt).")
134134+ in
135135+ Arg.(value & flag & info ~docs ~doc ~env [ "warn-error" ])
136136+ in
137137+ let print_warnings =
138138+ let doc =
139139+ "Whether warnings should be printed to stderr. See the $(b,errors) \
140140+ command."
141141+ in
142142+ let env = Cmd.Env.info "ODOC_PRINT_WARNINGS" ~doc in
143143+ Arg.(value & opt bool true & info ~docs ~doc ~env [ "print-warnings" ])
144144+ in
145145+ let enable_missing_root_warning =
146146+ let doc =
147147+ "Produce a warning when a root is missing. This is usually a build \
148148+ system problem so is disabled for users by default."
149149+ in
150150+ let env = Cmd.Env.info "ODOC_ENABLE_MISSING_ROOT_WARNING" ~doc in
151151+ Arg.(value & flag & info ~docs ~doc ~env [ "enable-missing-root-warning" ])
152152+ in
153153+ let warnings_tag =
154154+ let doc =
155155+ "Warnings tag. This is useful when you want to declare that warnings \
156156+ that would be generated resolving the references defined in this unit \
157157+ should be ignored if they end up in expansions in other units. If this \
158158+ option is passed, link-time warnings will be suppressed unless the link \
159159+ command is passed the tag via the --warnings-tags parameter. A suitable \
160160+ tag would be the name of the package."
161161+ in
162162+ let env = Cmd.Env.info "ODOC_WARNINGS_TAG" ~doc in
163163+ Arg.(
164164+ value & opt (some string) None & info ~docs ~doc ~env [ "warnings-tag" ])
165165+ in
166166+ Term.(
167167+ const
168168+ (fun warn_error print_warnings enable_missing_root_warning warnings_tag ->
169169+ Odoc_model.Error.enable_missing_root_warning :=
170170+ enable_missing_root_warning;
171171+ { Odoc_model.Error.warn_error; print_warnings; warnings_tag })
172172+ $ warn_error $ print_warnings $ enable_missing_root_warning $ warnings_tag)
173173+174174+let dst ?create () =
175175+ let doc = "Output directory where the HTML tree is expected to be saved." in
176176+ Arg.(
177177+ required
178178+ & opt (some (convert_directory ?create ())) None
179179+ & info ~docs ~docv:"DIR" ~doc [ "o"; "output-dir" ])
180180+181181+let open_modules =
182182+ let doc =
183183+ "Initially open module. Can be used more than once. Defaults to 'Stdlib'"
184184+ in
185185+ let default = [ "Stdlib" ] in
186186+ Arg.(value & opt_all string default & info ~docv:"MODULE" ~doc [ "open" ])
187187+188188+module Compile : sig
189189+ val output_file : dst:string option -> input:Fs.file -> Fs.file
190190+191191+ val input : string Term.t
192192+193193+ val dst : string option Term.t
194194+195195+ val cmd : unit Term.t
196196+197197+ val info : docs:string -> Cmd.info
198198+end = struct
199199+ let has_page_prefix file =
200200+ file |> Fs.File.basename |> Fs.File.to_string
201201+ |> String.is_prefix ~affix:"page-"
202202+203203+ let unique_id =
204204+ let doc = "For debugging use" in
205205+ Arg.(value & opt (some string) None & info ~doc ~docv:"ID" [ "unique-id" ])
206206+207207+ let output_file ~dst ~input =
208208+ match dst with
209209+ | Some file ->
210210+ let output = Fs.File.of_string file in
211211+ if Fs.File.has_ext ".mld" input && not (has_page_prefix output) then (
212212+ Printf.eprintf
213213+ "ERROR: the name of the .odoc file produced from a .mld must start \
214214+ with 'page-'\n\
215215+ %!";
216216+ exit 1);
217217+ output
218218+ | None ->
219219+ let output =
220220+ if Fs.File.has_ext ".mld" input && not (has_page_prefix input) then
221221+ let directory = Fs.File.dirname input in
222222+ let name = Fs.File.basename input in
223223+ let name = "page-" ^ Fs.File.to_string name in
224224+ Fs.File.create ~directory ~name
225225+ else input
226226+ in
227227+ Fs.File.(set_ext ".odoc" output)
228228+229229+ let compile hidden directories resolve_fwd_refs dst output_dir package_opt
230230+ parent_name_opt parent_id_opt open_modules children input warnings_options
231231+ unique_id short_title =
232232+ let _ =
233233+ match unique_id with
234234+ | Some id -> Odoc_model.Names.set_unique_ident id
235235+ | None -> ()
236236+ in
237237+ let resolver =
238238+ Resolver.create ~important_digests:(not resolve_fwd_refs) ~directories
239239+ ~open_modules ~roots:None
240240+ in
241241+ let input = Fs.File.of_string input in
242242+ let output = output_file ~dst ~input in
243243+ let cli_spec =
244244+ let error message = Error (`Cli_error message) in
245245+ match
246246+ (parent_name_opt, package_opt, parent_id_opt, children, output_dir)
247247+ with
248248+ | Some _, None, None, _, None ->
249249+ Ok (Compile.CliParent { parent = parent_name_opt; children; output })
250250+ | None, Some p, None, [], None ->
251251+ Ok (Compile.CliPackage { package = p; output })
252252+ | None, None, Some p, [], Some output_dir ->
253253+ Ok (Compile.CliParentId { parent_id = p; output_dir })
254254+ | None, None, None, _ :: _, None ->
255255+ Ok (Compile.CliParent { parent = None; output; children })
256256+ | None, None, None, [], None -> Ok (Compile.CliNoParent output)
257257+ | Some _, Some _, _, _, _ ->
258258+ error "Either --package or --parent should be specified, not both."
259259+ | _, Some _, Some _, _, _ ->
260260+ error "Either --package or --parent-id should be specified, not both."
261261+ | Some _, _, Some _, _, _ ->
262262+ error "Either --parent or --parent-id should be specified, not both."
263263+ | _, _, None, _, Some _ ->
264264+ error "--output-dir can only be passed with --parent-id."
265265+ | None, Some _, _, _ :: _, _ ->
266266+ error "--child cannot be passed with --package."
267267+ | None, _, Some _, _ :: _, _ ->
268268+ error "--child cannot be passed with --parent-id."
269269+ | _, _, Some _, _, None ->
270270+ error "--output-dir is required when passing --parent-id."
271271+ in
272272+ cli_spec >>= fun cli_spec ->
273273+ Fs.Directory.mkdir_p (Fs.File.dirname output);
274274+ Compile.compile ~resolver ~cli_spec ~hidden ~warnings_options ~short_title
275275+ input
276276+277277+ let input =
278278+ let doc = "Input $(i,.cmti), $(i,.cmt), $(i,.cmi) or $(i,.mld) file." in
279279+ Arg.(required & pos 0 (some file) None & info ~doc ~docv:"FILE" [])
280280+281281+ let dst =
282282+ let doc =
283283+ "Output file path. Non-existing intermediate directories are created. If \
284284+ absent outputs a $(i,BASE.odoc) file in the same directory as the input \
285285+ file where $(i,BASE) is the basename of the input file. For mld files \
286286+ the \"page-\" prefix will be added if not already present in the input \
287287+ basename."
288288+ in
289289+ Arg.(value & opt (some string) None & info ~docs ~docv:"PATH" ~doc [ "o" ])
290290+291291+ let output_dir =
292292+ let doc = "Output file directory. " in
293293+ Arg.(
294294+ value
295295+ & opt (some string) None
296296+ & info ~docs ~docv:"PATH" ~doc [ "output-dir" ])
297297+298298+ let children =
299299+ let doc =
300300+ "Specify the $(i,.odoc) file as a child. Can be used multiple times. \
301301+ Only applies to mld files."
302302+ in
303303+ let default = [] in
304304+ Arg.(
305305+ value & opt_all string default & info ~docv:"CHILD" ~doc [ "c"; "child" ])
306306+307307+ let cmd =
308308+ let package_opt =
309309+ let doc =
310310+ "Package the input is part of. Deprecated: use '--parent' instead."
311311+ in
312312+ Arg.(
313313+ value
314314+ & opt (some string) None
315315+ & info ~docs ~docv:"PKG" ~doc [ "package"; "pkg" ])
316316+ in
317317+ let parent_opt =
318318+ let doc = "Parent page or subpage." in
319319+ Arg.(
320320+ value
321321+ & opt (some string) None
322322+ & info ~docs ~docv:"PARENT" ~doc [ "parent" ])
323323+ in
324324+ let parent_id_opt =
325325+ let doc = "Parent id." in
326326+ Arg.(
327327+ value
328328+ & opt (some string) None
329329+ & info ~docs ~docv:"PARENT" ~doc [ "parent-id" ])
330330+ in
331331+ let short_title =
332332+ let doc = "Override short_title of an mld file" in
333333+ Arg.(
334334+ value
335335+ & opt (some string) None
336336+ & info ~docs ~docv:"TITLE" ~doc [ "short-title" ])
337337+ in
338338+ let resolve_fwd_refs =
339339+ let doc = "Try resolving forward references." in
340340+ Arg.(value & flag & info ~doc [ "r"; "resolve-fwd-refs" ])
341341+ in
342342+ Term.(
343343+ const handle_error
344344+ $ (const compile $ hidden $ odoc_file_directories $ resolve_fwd_refs $ dst
345345+ $ output_dir $ package_opt $ parent_opt $ parent_id_opt $ open_modules
346346+ $ children $ input $ warnings_options $ unique_id $ short_title))
347347+348348+ let info ~docs =
349349+ let man =
350350+ [
351351+ `S "DEPENDENCIES";
352352+ `P
353353+ "Dependencies between compilation units is the same as while \
354354+ compiling the initial OCaml modules.";
355355+ `P "Mld pages don't have any dependency.";
356356+ ]
357357+ in
358358+ let doc =
359359+ "Compile a $(i,.cmti), $(i,.cmt), $(i,.cmi) or $(i,.mld) file to an \
360360+ $(i,.odoc) file."
361361+ in
362362+ Cmd.info "compile" ~docs ~doc ~man
363363+end
364364+365365+module Compile_asset = struct
366366+ let compile_asset parent_id name output_dir =
367367+ Odoc_odoc.Asset.compile ~parent_id ~name ~output_dir
368368+369369+ let output_dir =
370370+ let doc = "Output file directory. " in
371371+ Arg.(
372372+ required
373373+ & opt (some string) None
374374+ & info ~docs ~docv:"PATH" ~doc [ "output-dir" ])
375375+376376+ let cmd =
377377+ let asset_name =
378378+ let doc = "Name of the asset." in
379379+ Arg.(
380380+ required
381381+ & opt (some string) None
382382+ & info ~docs ~docv:"NAME" ~doc [ "name" ])
383383+ in
384384+ let parent_id =
385385+ let doc = "Parent id." in
386386+ Arg.(
387387+ required
388388+ & opt (some string) None
389389+ & info ~docs ~docv:"PARENT" ~doc [ "parent-id" ])
390390+ in
391391+ Term.(
392392+ const handle_error
393393+ $ (const compile_asset $ parent_id $ asset_name $ output_dir))
394394+395395+ let info ~docs =
396396+ let man =
397397+ [
398398+ `S "DEPENDENCIES";
399399+ `P
400400+ "There are no dependency for compile assets, in particular you do \
401401+ not need the asset itself at this stage.";
402402+ ]
403403+ in
404404+ let doc = "Declare the name of an asset." in
405405+ Cmd.info "compile-asset" ~docs ~doc ~man
406406+end
407407+408408+module Compile_impl = struct
409409+ let prefix = "impl-"
410410+411411+ let output_dir =
412412+ let doc = "Output file directory. " in
413413+ Arg.(
414414+ value
415415+ & opt (some string) None
416416+ & info ~docs ~docv:"PATH" ~doc [ "output-dir" ])
417417+418418+ let output_file output_dir parent_id input =
419419+ let name =
420420+ Fs.File.basename input |> Fpath.set_ext "odoc" |> Fs.File.to_string
421421+ |> String.Ascii.uncapitalize
422422+ in
423423+ let name = prefix ^ name in
424424+425425+ let dir = Fpath.(append output_dir parent_id) in
426426+ Fs.File.create
427427+ ~directory:(Fpath.to_string dir |> Fs.Directory.of_string)
428428+ ~name
429429+430430+ let compile_impl directories output_dir parent_id source_id input
431431+ warnings_options =
432432+ let input = Fs.File.of_string input in
433433+ let output_dir =
434434+ match output_dir with Some x -> Fpath.v x | None -> Fpath.v "."
435435+ in
436436+ let output =
437437+ output_file output_dir
438438+ (match parent_id with Some x -> Fpath.v x | None -> Fpath.v ".")
439439+ input
440440+ in
441441+ let resolver =
442442+ Resolver.create ~important_digests:true ~directories ~open_modules:[]
443443+ ~roots:None
444444+ in
445445+ Source.compile ~resolver ~source_id ~output ~warnings_options input
446446+447447+ let cmd =
448448+ let input =
449449+ let doc = "Input $(i,.cmt) file." in
450450+ Arg.(required & pos 0 (some file) None & info ~doc ~docv:"FILE" [])
451451+ in
452452+ let source_id =
453453+ let doc = "The id of the source file" in
454454+ Arg.(
455455+ value
456456+ & opt (some string) None
457457+ & info [ "source-id" ] ~doc ~docv:"/path/to/source.ml")
458458+ in
459459+ let parent_id =
460460+ let doc = "The parent id of the implementation" in
461461+ Arg.(
462462+ value
463463+ & opt (some string) None
464464+ & info [ "parent-id" ] ~doc ~docv:"/path/to/library")
465465+ in
466466+467467+ Term.(
468468+ const handle_error
469469+ $ (const compile_impl $ odoc_file_directories $ output_dir $ parent_id
470470+ $ source_id $ input $ warnings_options))
471471+472472+ let info ~docs =
473473+ let doc =
474474+ "(EXPERIMENTAL) Compile a $(i,NAME.cmt) file to a $(i,src-NAME.odoc) \
475475+ containing the implementation information needed by odoc for the \
476476+ compilation unit."
477477+ in
478478+ Cmd.info "compile-impl" ~docs ~doc
479479+end
480480+481481+module Indexing = struct
482482+ let output_file ~dst marshall =
483483+ match (dst, marshall) with
484484+ | Some file, `JSON
485485+ when not
486486+ (Fpath.has_ext "json" (Fpath.v file)
487487+ || Fpath.has_ext "js" (Fpath.v file)) ->
488488+ Error
489489+ (`Msg
490490+ "When generating a json index, the output must have a .json or \
491491+ .js file extension")
492492+ | Some file, `Marshall when not (Fpath.has_ext "odoc-index" (Fpath.v file))
493493+ ->
494494+ Error
495495+ (`Msg
496496+ "When generating a binary index, the output must have a \
497497+ .odoc-index file extension")
498498+ | Some file, _ -> Ok (Fs.File.of_string file)
499499+ | None, `JSON -> Ok (Fs.File.of_string "index.json")
500500+ | None, `Marshall -> Ok (Fs.File.of_string "index.odoc-index")
501501+502502+ let index dst json warnings_options roots inputs_in_file inputs occurrences
503503+ simplified_json wrap_json =
504504+ let marshall = if json then `JSON else `Marshall in
505505+ output_file ~dst marshall >>= fun output ->
506506+ Indexing.compile marshall ~output ~warnings_options ~roots ~occurrences
507507+ ~inputs_in_file ~simplified_json ~wrap_json ~odocls:inputs
508508+509509+ let cmd =
510510+ let dst =
511511+ let doc =
512512+ "Output file path. Non-existing intermediate directories are created. \
513513+ Defaults to index.odoc-index, or index.json if --json is passed (in \
514514+ which case, the .odoc-index file extension is mandatory)."
515515+ in
516516+ Arg.(
517517+ value & opt (some string) None & info ~docs ~docv:"PATH" ~doc [ "o" ])
518518+ in
519519+ let occurrences =
520520+ let doc = "Occurrence file." in
521521+ Arg.(
522522+ value
523523+ & opt (some convert_fpath) None
524524+ & info ~docs ~docv:"PATH" ~doc [ "occurrences" ])
525525+ in
526526+ let inputs_in_file =
527527+ let doc =
528528+ "Input text file containing a line-separated list of paths to .odocl \
529529+ files to index."
530530+ in
531531+ Arg.(
532532+ value & opt_all convert_fpath []
533533+ & info ~doc ~docv:"FILE" [ "file-list" ])
534534+ in
535535+ let json =
536536+ let doc = "whether to output a json file, or a binary .odoc-index file" in
537537+ Arg.(value & flag & info ~doc [ "json" ])
538538+ in
539539+ let simplified_json =
540540+ let doc =
541541+ "whether to simplify the json file. Only has an effect in json output \
542542+ mode."
543543+ in
544544+ Arg.(value & flag & info ~doc [ "simplified-json" ])
545545+ in
546546+ let wrap_json =
547547+ let doc =
548548+ "Not intended for general use. Wraps the json output in a JavaScript \
549549+ variable assignment, and assumes the use of fuse.js"
550550+ in
551551+ Arg.(value & flag & info ~doc [ "wrap-json" ])
552552+ in
553553+554554+ let inputs =
555555+ let doc = ".odocl file to index" in
556556+ Arg.(value & pos_all convert_fpath [] & info ~doc ~docv:"FILE" [])
557557+ in
558558+ let roots =
559559+ let doc =
560560+ "Specifies a directory PATH containing pages or units that should be \
561561+ included in the sidebar."
562562+ in
563563+ Arg.(
564564+ value
565565+ & opt_all (convert_directory ()) []
566566+ & info ~docs ~docv:"NAME:PATH" ~doc [ "root" ])
567567+ in
568568+ Term.(
569569+ const handle_error
570570+ $ (const index $ dst $ json $ warnings_options $ roots $ inputs_in_file
571571+ $ inputs $ occurrences $ simplified_json $ wrap_json))
572572+573573+ let info ~docs =
574574+ let doc =
575575+ "Generate an index of all identified entries in the .odocl files found \
576576+ in the given directories."
577577+ in
578578+ Cmd.info "compile-index" ~docs ~doc
579579+end
580580+581581+module Sidebar = struct
582582+ let output_file ~dst marshall =
583583+ match (dst, marshall) with
584584+ | Some file, `JSON when not (Fpath.has_ext "json" (Fpath.v file)) ->
585585+ Error
586586+ (`Msg
587587+ "When generating a sidebar with --json, the output must have a \
588588+ .json file extension")
589589+ | Some file, `Marshall
590590+ when not (Fpath.has_ext "odoc-sidebar" (Fpath.v file)) ->
591591+ Error
592592+ (`Msg
593593+ "When generating sidebar, the output must have a .odoc-sidebar \
594594+ file extension")
595595+ | Some file, _ -> Ok (Fs.File.of_string file)
596596+ | None, `JSON -> Ok (Fs.File.of_string "sidebar.json")
597597+ | None, `Marshall -> Ok (Fs.File.of_string "sidebar.odoc-sidebar")
598598+599599+ let generate dst json warnings_options input =
600600+ let marshall = if json then `JSON else `Marshall in
601601+ output_file ~dst marshall >>= fun output ->
602602+ Sidebar.generate ~marshall ~output ~warnings_options ~index:input
603603+604604+ let cmd =
605605+ let dst =
606606+ let doc =
607607+ "Output file path. Non-existing intermediate directories are created. \
608608+ Defaults to sidebar.odoc-sidebar, or sidebar.json if --json is \
609609+ passed."
610610+ in
611611+ Arg.(
612612+ value & opt (some string) None & info ~docs ~docv:"PATH" ~doc [ "o" ])
613613+ in
614614+ let json =
615615+ let doc = "whether to output a json file, or a binary .odoc-index file" in
616616+ Arg.(value & flag & info ~doc [ "json" ])
617617+ in
618618+ let inputs =
619619+ let doc = ".odoc-index file to generate a value from" in
620620+ Arg.(
621621+ required & pos 0 (some convert_fpath) None & info ~doc ~docv:"FILE" [])
622622+ in
623623+ Term.(
624624+ const handle_error
625625+ $ (const generate $ dst $ json $ warnings_options $ inputs))
626626+627627+ let info ~docs =
628628+ let doc = "Generate a sidebar from an index file." in
629629+ Cmd.info "sidebar-generate" ~docs ~doc
630630+end
631631+632632+module Support_files_command = struct
633633+ let support_files without_theme output_dir =
634634+ Support_files.write ~without_theme output_dir
635635+636636+ let without_theme =
637637+ let doc = "Don't copy the default theme to output directory." in
638638+ Arg.(value & flag & info ~doc [ "without-theme" ])
639639+640640+ let cmd = Term.(const support_files $ without_theme $ dst ~create:true ())
641641+642642+ let info ~docs =
643643+ let doc =
644644+ "Copy the support files (e.g. default theme, JavaScript files) to the \
645645+ output directory."
646646+ in
647647+ Cmd.info ~docs ~doc "support-files"
648648+end
649649+650650+module Css = struct
651651+ let cmd = Support_files_command.cmd
652652+653653+ let info ~docs =
654654+ let doc =
655655+ "DEPRECATED: Use $(i,odoc support-files) to copy the CSS file for the \
656656+ default theme."
657657+ in
658658+ Cmd.info ~docs ~doc "css"
659659+end
660660+661661+module Odoc_link : sig
662662+ val cmd : unit Term.t
663663+664664+ val info : docs:string -> Cmd.info
665665+end = struct
666666+ let get_output_file ~output_file ~input =
667667+ match output_file with
668668+ | Some file -> Fs.File.of_string file
669669+ | None -> Fs.File.(set_ext ".odocl" input)
670670+671671+ (** Find the package/library name the output is part of *)
672672+ let find_root_of_input l o =
673673+ let l =
674674+ List.map
675675+ ~f:(fun (x, p) ->
676676+ (x, p, p |> Fs.Directory.to_fpath |> Antichain.absolute_normalization))
677677+ l
678678+ in
679679+ let o = Antichain.absolute_normalization o in
680680+ match l with
681681+ | [] -> None
682682+ | _ ->
683683+ Odoc_utils.List.find_map
684684+ (fun (root, orig_path, norm_path) ->
685685+ if Fpath.is_prefix norm_path o then Some (root, orig_path) else None)
686686+ l
687687+688688+ let current_library_of_input lib_roots input =
689689+ find_root_of_input lib_roots input
690690+691691+ (** Checks if the package specified with [--current-package] is consistent
692692+ with the pages roots and with the output path for pages. *)
693693+ let validate_current_package ?detected_package page_roots current_package =
694694+ match (current_package, detected_package) with
695695+ | Some curpkgnane, Some (detected_package, _)
696696+ when detected_package <> curpkgnane ->
697697+ Error
698698+ (`Msg
699699+ "The package name specified with --current-package is not \
700700+ consistent with the packages passed as a -P")
701701+ | _, (Some _ as r) (* we have equality or only detected package *) -> Ok r
702702+ | None, None -> Ok None
703703+ | Some given, None -> (
704704+ try Ok (Some (given, List.assoc given page_roots))
705705+ with Not_found ->
706706+ Error
707707+ (`Msg
708708+ "The package name specified with --current-package do not match \
709709+ any package passed as a -P"))
710710+711711+ let find_current_package ~current_package page_roots input =
712712+ let detected_package = find_root_of_input page_roots input in
713713+ validate_current_package ?detected_package page_roots current_package
714714+715715+ let warnings_tags =
716716+ let doc =
717717+ "Filter warnings that were compiled with a tag that is not in the list \
718718+ of --warnings-tags passed."
719719+ in
720720+ let env = Cmd.Env.info "ODOC_WARNINGS_TAGS" ~doc in
721721+ Arg.(value & opt_all string [] & info ~docs ~doc ~env [ "warnings-tags" ])
722722+723723+ let link directories page_roots lib_roots input_file output_file
724724+ current_package warnings_options open_modules custom_layout warnings_tags
725725+ =
726726+ let input = Fs.File.of_string input_file in
727727+ let output = get_output_file ~output_file ~input in
728728+ let check () =
729729+ if not custom_layout then
730730+ Antichain.check (page_roots |> List.map ~f:snd) ~opt:"-P" >>= fun () ->
731731+ Antichain.check (lib_roots |> List.map ~f:snd) ~opt:"-L"
732732+ else Ok ()
733733+ in
734734+ check () >>= fun () ->
735735+ let current_lib = current_library_of_input lib_roots input in
736736+ find_current_package ~current_package page_roots input
737737+ >>= fun current_package ->
738738+ let current_dir = Fs.File.dirname input in
739739+ let roots =
740740+ Some
741741+ {
742742+ Resolver.page_roots;
743743+ lib_roots;
744744+ current_lib;
745745+ current_package;
746746+ current_dir;
747747+ }
748748+ in
749749+750750+ let resolver =
751751+ Resolver.create ~important_digests:false ~directories ~open_modules ~roots
752752+ in
753753+ match
754754+ Odoc_link.from_odoc ~resolver ~warnings_options ~warnings_tags input
755755+ output
756756+ with
757757+ | Error _ as e -> e
758758+ | Ok _ -> Ok ()
759759+760760+ let dst =
761761+ let doc =
762762+ "Output file path. Non-existing intermediate directories are created. If \
763763+ absent outputs a $(i,.odocl) file in the same directory as the input \
764764+ file with the same basename."
765765+ in
766766+ Arg.(
767767+ value
768768+ & opt (some string) None
769769+ & info ~docs ~docv:"PATH.odocl" ~doc [ "o" ])
770770+771771+ let page_roots =
772772+ let doc =
773773+ "Specifies a directory DIR containing pages that can be referenced by \
774774+ {!/pkgname/pagename}. A pkgname can be specified in the -P command only \
775775+ once. All the trees specified by this option and -L must be disjoint."
776776+ in
777777+ Arg.(
778778+ value
779779+ & opt_all convert_named_root []
780780+ & info ~docs ~docv:"pkgname:DIR" ~doc [ "P" ])
781781+782782+ let lib_roots =
783783+ let doc =
784784+ "Specifies a library called libname containing the modules in directory \
785785+ DIR. Modules can be referenced both using the flat module namespace \
786786+ {!Module} and the absolute reference {!/libname/Module}. All the trees \
787787+ specified by this option and -P must be disjoint."
788788+ in
789789+ Arg.(
790790+ value
791791+ & opt_all convert_named_root []
792792+ & info ~docs ~docv:"libname:DIR" ~doc [ "L" ])
793793+794794+ let current_package =
795795+ let doc =
796796+ "Specify the current package name. The matching page root specified with \
797797+ -P is used to resolve references using the '//' syntax. A \
798798+ corresponding -P option must be passed."
799799+ in
800800+ Arg.(
801801+ value
802802+ & opt (some string) None
803803+ & info ~docs ~docv:"pkgname" ~doc [ "current-package" ])
804804+805805+ let custom_layout =
806806+ let doc =
807807+ "Signal that a custom layout is being used. This disables the checks \
808808+ that the library and package paths are disjoint."
809809+ in
810810+ Arg.(value & flag (info ~doc [ "custom-layout" ]))
811811+812812+ let cmd =
813813+ let input =
814814+ let doc = "Input file" in
815815+ Arg.(required & pos 0 (some file) None & info ~doc ~docv:"FILE.odoc" [])
816816+ in
817817+ Term.(
818818+ const handle_error
819819+ $ (const link $ odoc_file_directories $ page_roots $ lib_roots $ input
820820+ $ dst $ current_package $ warnings_options $ open_modules $ custom_layout
821821+ $ warnings_tags))
822822+823823+ let info ~docs =
824824+ let man =
825825+ [
826826+ `S "DEPENDENCIES";
827827+ `P
828828+ "Any link step depends on the result of all the compile results that \
829829+ could potentially be needed to resolve forward references. A \
830830+ correct approximation is to start linking only after every compile \
831831+ steps are done, passing everything that's possible to $(i,-I). Link \
832832+ steps don't have dependencies between them.";
833833+ ]
834834+ in
835835+ let doc =
836836+ "Second stage of compilation. Link a $(i,.odoc) into a $(i,.odocl)."
837837+ in
838838+ Cmd.info ~docs ~doc ~man "link"
839839+end
840840+841841+module type S = sig
842842+ type args
843843+844844+ val renderer : args Odoc_document.Renderer.t
845845+846846+ val extra_args : args Cmdliner.Term.t
847847+end
848848+849849+module Make_renderer (R : S) : sig
850850+ val process : docs:string -> unit Term.t * Cmd.info
851851+852852+ val targets : docs:string -> unit Term.t * Cmd.info
853853+854854+ val targets_source : docs:string -> unit Term.t * Cmd.info
855855+856856+ val generate : docs:string -> unit Term.t * Cmd.info
857857+858858+ val generate_source : docs:string -> unit Term.t * Cmd.info
859859+860860+ val generate_asset : docs:string -> unit Term.t * Cmd.info
861861+end = struct
862862+ let input_odoc =
863863+ let doc = "Input file." in
864864+ Arg.(required & pos 0 (some file) None & info ~doc ~docv:"FILE.odoc" [])
865865+866866+ let input_odocl =
867867+ let doc = "Input file." in
868868+ Arg.(required & pos 0 (some file) None & info ~doc ~docv:"FILE.odocl" [])
869869+870870+ let input_odocl_list =
871871+ let doc = "Input file(s)." in
872872+ Arg.(non_empty & pos_all file [] & info ~doc ~docv:"FILE.odocl" [])
873873+874874+ module Process = struct
875875+ let process extra _hidden directories output_dir syntax input_file
876876+ warnings_options =
877877+ let resolver =
878878+ Resolver.create ~important_digests:false ~directories ~open_modules:[]
879879+ ~roots:None
880880+ in
881881+ let file = Fs.File.of_string input_file in
882882+ Rendering.render_odoc ~renderer:R.renderer ~resolver ~warnings_options
883883+ ~syntax ~output:output_dir extra file
884884+885885+ let cmd =
886886+ let syntax =
887887+ let doc = "Available options: ml | re" in
888888+ let env = Cmd.Env.info "ODOC_SYNTAX" in
889889+ Arg.(
890890+ value
891891+ & opt convert_syntax Odoc_document.Renderer.OCaml
892892+ @@ info ~docv:"SYNTAX" ~doc ~env [ "syntax" ])
893893+ in
894894+ Term.(
895895+ const handle_error
896896+ $ (const process $ R.extra_args $ hidden $ odoc_file_directories
897897+ $ dst ~create:true () $ syntax $ input_odoc $ warnings_options))
898898+899899+ let info ~docs =
900900+ let doc =
901901+ Format.sprintf
902902+ "Render %s files from a $(i,.odoc). $(i,link) then $(i,%s-generate) \
903903+ should be used instead."
904904+ R.renderer.name R.renderer.name
905905+ in
906906+ Cmd.info ~docs ~doc R.renderer.name
907907+ end
908908+909909+ let process ~docs = Process.(cmd, info ~docs)
910910+911911+ module Generate = struct
912912+ let generate extra _hidden output_dir syntax extra_suffix input_files
913913+ warnings_options sidebar =
914914+ let process_file input_file =
915915+ let file = Fs.File.of_string input_file in
916916+ Rendering.generate_odoc ~renderer:R.renderer ~warnings_options ~syntax
917917+ ~output:output_dir ~extra_suffix ~sidebar extra file
918918+ in
919919+ List.fold_left
920920+ ~f:(fun acc input_file -> acc >>= fun () -> process_file input_file)
921921+ ~init:(Ok ()) input_files
922922+923923+ let sidebar =
924924+ let doc = "A .odoc-index file, used eg to generate the sidebar." in
925925+ Arg.(
926926+ value
927927+ & opt (some convert_fpath) None
928928+ & info [ "sidebar" ] ~doc ~docv:"FILE.odoc-sidebar")
929929+930930+ let cmd =
931931+ let syntax =
932932+ let doc = "Available options: ml | re" in
933933+ let env = Cmd.Env.info "ODOC_SYNTAX" in
934934+ Arg.(
935935+ value
936936+ & opt convert_syntax Odoc_document.Renderer.OCaml
937937+ @@ info ~docv:"SYNTAX" ~doc ~env [ "syntax" ])
938938+ in
939939+ Term.(
940940+ const handle_error
941941+ $ (const generate $ R.extra_args $ hidden $ dst ~create:true () $ syntax
942942+ $ extra_suffix $ input_odocl_list $ warnings_options $ sidebar))
943943+944944+ let info ~docs =
945945+ let doc =
946946+ Format.sprintf "Generate %s files from one or more $(i,.odocl) files."
947947+ R.renderer.name
948948+ in
949949+ Cmd.info ~docs ~doc (R.renderer.name ^ "-generate")
950950+ end
951951+952952+ let generate ~docs = Generate.(cmd, info ~docs)
953953+954954+ module Generate_source = struct
955955+ let generate extra output_dir syntax extra_suffix input_file
956956+ warnings_options source_file sidebar =
957957+ Rendering.generate_source_odoc ~renderer:R.renderer ~warnings_options
958958+ ~syntax ~output:output_dir ~extra_suffix ~source_file ~sidebar extra
959959+ input_file
960960+961961+ let input_odocl =
962962+ let doc = "Linked implementation file." in
963963+ Arg.(
964964+ required
965965+ & opt (some convert_fpath) None
966966+ & info [ "impl" ] ~doc ~docv:"impl-FILE.odocl")
967967+968968+ let source_file =
969969+ let doc = "Source code for the implementation unit." in
970970+ Arg.(
971971+ required
972972+ & pos 0 (some convert_fpath) None
973973+ & info ~doc ~docv:"FILE.ml" [])
974974+975975+ let cmd =
976976+ let syntax =
977977+ let doc = "Available options: ml | re" in
978978+ let env = Cmd.Env.info "ODOC_SYNTAX" in
979979+ Arg.(
980980+ value
981981+ & opt convert_syntax Odoc_document.Renderer.OCaml
982982+ @@ info ~docv:"SYNTAX" ~doc ~env [ "syntax" ])
983983+ in
984984+ let sidebar = Generate.sidebar in
985985+ Term.(
986986+ const handle_error
987987+ $ (const generate $ R.extra_args $ dst ~create:true () $ syntax
988988+ $ extra_suffix $ input_odocl $ warnings_options $ source_file $ sidebar
989989+ ))
990990+991991+ let info ~docs =
992992+ let doc =
993993+ Format.sprintf "Generate %s files from a $(i,impl-*.odocl)."
994994+ R.renderer.name
995995+ in
996996+ Cmd.info ~docs ~doc (R.renderer.name ^ "-generate-source")
997997+ end
998998+999999+ let generate_source ~docs = Generate_source.(cmd, info ~docs)
10001000+10011001+ module Generate_asset = struct
10021002+ let generate extra output_dir extra_suffix input_file warnings_options
10031003+ asset_file =
10041004+ Rendering.generate_asset_odoc ~renderer:R.renderer ~warnings_options
10051005+ ~output:output_dir ~extra_suffix ~asset_file extra input_file
10061006+10071007+ let input_odocl =
10081008+ let doc = "Odoc asset unit." in
10091009+ Arg.(
10101010+ required
10111011+ & opt (some convert_fpath) None
10121012+ & info [ "asset-unit" ] ~doc ~docv:"asset-FILE.odocl")
10131013+10141014+ let asset_file =
10151015+ let doc = "The asset file" in
10161016+ Arg.(
10171017+ required
10181018+ & pos 0 (some convert_fpath) None
10191019+ & info ~doc ~docv:"FILE.ext" [])
10201020+10211021+ let cmd =
10221022+ Term.(
10231023+ const handle_error
10241024+ $ (const generate $ R.extra_args $ dst ~create:true () $ extra_suffix
10251025+ $ input_odocl $ warnings_options $ asset_file))
10261026+10271027+ let info ~docs =
10281028+ let doc =
10291029+ Format.sprintf "Generate %s files from a $(i,impl-*.odocl)."
10301030+ R.renderer.name
10311031+ in
10321032+ Cmd.info ~docs ~doc (R.renderer.name ^ "-generate-asset")
10331033+ end
10341034+10351035+ let generate_asset ~docs = Generate_asset.(cmd, info ~docs)
10361036+10371037+ module Targets = struct
10381038+ let list_targets output_dir directories extra odoc_file =
10391039+ let odoc_file = Fs.File.of_string odoc_file in
10401040+ let resolver =
10411041+ Resolver.create ~important_digests:false ~directories ~open_modules:[]
10421042+ ~roots:None
10431043+ in
10441044+ let warnings_options =
10451045+ {
10461046+ Odoc_model.Error.warn_error = false;
10471047+ print_warnings = false;
10481048+ warnings_tag = None;
10491049+ }
10501050+ in
10511051+ Rendering.targets_odoc ~resolver ~warnings_options ~syntax:OCaml
10521052+ ~renderer:R.renderer ~output:output_dir ~extra odoc_file
10531053+10541054+ let back_compat =
10551055+ let doc =
10561056+ "For backwards compatibility when processing $(i,.odoc) rather than \
10571057+ $(i,.odocl) files."
10581058+ in
10591059+ Arg.(
10601060+ value
10611061+ & opt_all (convert_directory ()) []
10621062+ & info ~docs ~docv:"DIR" ~doc [ "I" ])
10631063+10641064+ let cmd =
10651065+ Term.(
10661066+ const handle_error
10671067+ $ (const list_targets $ dst () $ back_compat $ R.extra_args
10681068+ $ input_odocl))
10691069+10701070+ let info ~docs =
10711071+ let doc =
10721072+ Format.sprintf
10731073+ "Print the files that would be generated by $(i,%s-generate)."
10741074+ R.renderer.name
10751075+ in
10761076+ Cmd.info (R.renderer.name ^ "-targets") ~docs ~doc
10771077+ end
10781078+10791079+ let targets ~docs = Targets.(cmd, info ~docs)
10801080+10811081+ module Targets_source = struct
10821082+ let list_targets output_dir source_file extra odoc_file =
10831083+ let warnings_options =
10841084+ {
10851085+ Odoc_model.Error.warn_error = false;
10861086+ print_warnings = false;
10871087+ warnings_tag = None;
10881088+ }
10891089+ in
10901090+ Rendering.targets_source_odoc ~warnings_options ~syntax:OCaml
10911091+ ~renderer:R.renderer ~output:output_dir ~extra ~source_file odoc_file
10921092+10931093+ let source_file = Generate_source.source_file
10941094+ let input_odocl = Generate_source.input_odocl
10951095+10961096+ let cmd =
10971097+ Term.(
10981098+ const handle_error
10991099+ $ (const list_targets $ dst () $ source_file $ R.extra_args
11001100+ $ input_odocl))
11011101+11021102+ let info ~docs =
11031103+ let doc =
11041104+ Format.sprintf
11051105+ "Print the files that would be generated by $(i,%s-generate-source)."
11061106+ R.renderer.name
11071107+ in
11081108+ Cmd.info (R.renderer.name ^ "-targets-source") ~docs ~doc
11091109+ end
11101110+11111111+ let targets_source ~docs = Targets_source.(cmd, info ~docs)
11121112+end
11131113+11141114+module Odoc_latex_url : sig
11151115+ val cmd : unit Term.t
11161116+11171117+ val info : docs:string -> Cmd.info
11181118+end = struct
11191119+ let reference =
11201120+ let doc = "The reference to be resolved and whose url to be generated." in
11211121+ Arg.(required & pos 0 (some string) None & info ~doc ~docv:"REF" [])
11221122+11231123+ let reference_to_url = Url.reference_to_url_latex
11241124+11251125+ let cmd =
11261126+ Term.(
11271127+ const handle_error
11281128+ $ (const reference_to_url $ odoc_file_directories $ reference))
11291129+11301130+ let info ~docs =
11311131+ Cmd.info ~docs ~doc:"Resolve a reference and output its corresponding url."
11321132+ "latex-url"
11331133+end
11341134+11351135+module Odoc_html_args = struct
11361136+ include Html_page
11371137+11381138+ let semantic_uris =
11391139+ let doc = "Generate pretty (semantic) links." in
11401140+ Arg.(value & flag (info ~doc [ "semantic-uris"; "pretty-uris" ]))
11411141+11421142+ let closed_details =
11431143+ let doc =
11441144+ "If this flag is passed <details> tags (used for includes) will be \
11451145+ closed by default."
11461146+ in
11471147+ Arg.(value & flag (info ~doc [ "closed-details" ]))
11481148+11491149+ let indent =
11501150+ let doc = "Format the output HTML files with indentation." in
11511151+ Arg.(value & flag (info ~doc [ "indent" ]))
11521152+11531153+ module Uri = struct
11541154+ (* Very basic validation and normalization for URI paths. *)
11551155+11561156+ open Odoc_html.Types
11571157+11581158+ let is_absolute str =
11591159+ List.exists [ "http"; "https"; "file"; "data"; "ftp" ] ~f:(fun scheme ->
11601160+ Astring.String.is_prefix ~affix:(scheme ^ ":") str)
11611161+ || str.[0] = '/'
11621162+11631163+ let conv_rel_dir rel =
11641164+ let l = String.cuts ~sep:"/" rel in
11651165+ List.fold_left
11661166+ ~f:(fun acc seg ->
11671167+ Some Odoc_document.Url.Path.{ kind = `Page; parent = acc; name = seg })
11681168+ l ~init:None
11691169+11701170+ let convert_dir : uri Arg.conv =
11711171+ let parser str =
11721172+ if String.length str = 0 then Error "invalid URI"
11731173+ else
11741174+ (* The URI is absolute if it starts with a scheme or with '/'. *)
11751175+ let last_char = str.[String.length str - 1] in
11761176+ let str =
11771177+ if last_char <> '/' then str
11781178+ else String.with_range ~len:(String.length str - 1) str
11791179+ in
11801180+ Ok
11811181+ (if is_absolute str then (Absolute str : uri)
11821182+ else
11831183+ Relative
11841184+ (let u = conv_rel_dir str in
11851185+ match u with
11861186+ | None -> None
11871187+ | Some u -> Some { u with kind = `Page }))
11881188+ in
11891189+ let printer ppf = function
11901190+ | (Absolute uri : uri) -> Format.pp_print_string ppf uri
11911191+ | Relative _uri -> Format.pp_print_string ppf ""
11921192+ in
11931193+ Arg.conv' (parser, printer)
11941194+11951195+ let convert_file_uri : Odoc_html.Types.file_uri Arg.conv =
11961196+ let parser str =
11971197+ if String.length str = 0 then Error "invalid URI"
11981198+ else
11991199+ let conv_rel_file rel =
12001200+ match String.cut ~rev:true ~sep:"/" rel with
12011201+ | Some (before, after) ->
12021202+ let base = conv_rel_dir before in
12031203+ Odoc_document.Url.Path.
12041204+ { kind = `File; parent = base; name = after }
12051205+ | None ->
12061206+ Odoc_document.Url.Path.
12071207+ { kind = `File; parent = None; name = rel }
12081208+ in
12091209+ Ok
12101210+ (if is_absolute str then (Absolute str : file_uri)
12111211+ else Relative (conv_rel_file str))
12121212+ in
12131213+ let printer ppf = function
12141214+ | Odoc_html.Types.Absolute uri -> Format.pp_print_string ppf uri
12151215+ | Odoc_html.Types.Relative _uri -> Format.pp_print_string ppf ""
12161216+ in
12171217+ Arg.conv' (parser, printer)
12181218+ end
12191219+12201220+ let home_breadcrumb =
12211221+ let doc =
12221222+ "Name for a 'Home' breadcrumb to go up the root of the given sidebar."
12231223+ in
12241224+ Arg.(
12251225+ value
12261226+ & opt (some string) None
12271227+ & info ~docv:"escape" ~doc [ "home-breadcrumb" ])
12281228+12291229+ let theme_uri =
12301230+ let doc =
12311231+ "Where to look for theme files (e.g. `URI/odoc.css'). Relative URIs are \
12321232+ resolved using `--output-dir' as a target."
12331233+ in
12341234+ let default : Odoc_html.Types.uri = Odoc_html.Types.Relative None in
12351235+ Arg.(
12361236+ value
12371237+ & opt Uri.convert_dir default
12381238+ & info ~docv:"URI" ~doc [ "theme-uri" ])
12391239+12401240+ let support_uri =
12411241+ let doc =
12421242+ "Where to look for support files (e.g. `URI/highlite.pack.js'). Relative \
12431243+ URIs are resolved using `--output-dir' as a target."
12441244+ in
12451245+ let default : Odoc_html.Types.uri = Odoc_html.Types.Relative None in
12461246+ Arg.(
12471247+ value
12481248+ & opt Uri.convert_dir default
12491249+ & info ~docv:"URI" ~doc [ "support-uri" ])
12501250+12511251+ let search_uri =
12521252+ let doc =
12531253+ "Where to look for search scripts. Relative URIs are resolved using \
12541254+ `--output-dir' as a target."
12551255+ in
12561256+ Arg.(
12571257+ value
12581258+ & opt_all Uri.convert_file_uri []
12591259+ & info ~docv:"URI" ~doc [ "search-uri" ])
12601260+12611261+ let flat =
12621262+ let doc =
12631263+ "Output HTML files in 'flat' mode, where the hierarchy of modules / \
12641264+ module types / classes and class types are reflected in the filenames \
12651265+ rather than in the directory structure."
12661266+ in
12671267+ Arg.(value & flag & info ~docs ~doc [ "flat" ])
12681268+12691269+ let as_json =
12701270+ let doc =
12711271+ "EXPERIMENTAL: Output HTML files in 'embeddable json' mode, where HTML \
12721272+ fragments (preamble, content) together with metadata (uses_katex, \
12731273+ breadcrumbs, table of contents) are emitted in JSON format. The \
12741274+ structure of the output should be considered unstable and no guarantees \
12751275+ are made about backward compatibility."
12761276+ in
12771277+ Arg.(value & flag & info ~doc [ "as-json" ])
12781278+12791279+ let remap =
12801280+ let convert_remap =
12811281+ let parse inp =
12821282+ match String.cut ~sep:":" inp with
12831283+ | Some (orig, mapped) -> Ok (orig, mapped)
12841284+ | _ -> Error (`Msg "Map must be of the form '<orig>:https://...'")
12851285+ and print fmt (orig, mapped) = Format.fprintf fmt "%s:%s" orig mapped in
12861286+ Arg.conv (parse, print)
12871287+ in
12881288+ let doc = "Remap an identifier to an external URL." in
12891289+ Arg.(value & opt_all convert_remap [] & info [ "R" ] ~doc)
12901290+12911291+ let remap_file =
12921292+ let doc = "File containing remap rules." in
12931293+ Arg.(value & opt (some file) None & info ~docv:"FILE" ~doc [ "remap-file" ])
12941294+12951295+ let extra_args =
12961296+ let config semantic_uris closed_details indent theme_uri support_uri
12971297+ search_uris flat as_json remap remap_file home_breadcrumb =
12981298+ let open_details = not closed_details in
12991299+ let remap =
13001300+ match remap_file with
13011301+ | None -> remap
13021302+ | Some f ->
13031303+ Io_utils.fold_lines f
13041304+ (fun line acc ->
13051305+ match String.cut ~sep:":" line with
13061306+ | Some (orig, mapped) -> (orig, mapped) :: acc
13071307+ | None -> acc)
13081308+ []
13091309+ in
13101310+ let html_config =
13111311+ Odoc_html.Config.v ~theme_uri ~support_uri ~search_uris ~semantic_uris
13121312+ ~indent ~flat ~open_details ~as_json ~remap ?home_breadcrumb ()
13131313+ in
13141314+ { Html_page.html_config }
13151315+ in
13161316+ Term.(
13171317+ const config $ semantic_uris $ closed_details $ indent $ theme_uri
13181318+ $ support_uri $ search_uri $ flat $ as_json $ remap $ remap_file
13191319+ $ home_breadcrumb)
13201320+end
13211321+13221322+module Odoc_html = Make_renderer (Odoc_html_args)
13231323+13241324+module Odoc_markdown_cmd = Make_renderer (struct
13251325+ type args = Odoc_markdown.Config.t
13261326+13271327+ let render config _sidebar page = Odoc_markdown.Generator.render ~config page
13281328+13291329+ let filepath config url = Odoc_markdown.Generator.filepath ~config url
13301330+13311331+ let extra_args =
13321332+ Term.const { Odoc_markdown.Config.root_url = None; allow_html = true }
13331333+ let renderer = { Odoc_document.Renderer.name = "markdown"; render; filepath }
13341334+end)
13351335+13361336+module Odoc_html_url : sig
13371337+ val cmd : unit Term.t
13381338+13391339+ val info : docs:string -> Cmd.info
13401340+end = struct
13411341+ let root_url =
13421342+ let doc =
13431343+ "A string to prepend to the generated relative url. A separating / is \
13441344+ added if needed."
13451345+ in
13461346+ Arg.(value & opt (some string) None & info [ "r"; "root-url" ] ~doc)
13471347+13481348+ let reference =
13491349+ let doc = "The reference to be resolved and whose url to be generated." in
13501350+ Arg.(required & pos 0 (some string) None & info ~doc ~docv:"REF" [])
13511351+13521352+ let reference_to_url = Url.reference_to_url_html
13531353+13541354+ let cmd =
13551355+ Term.(
13561356+ const handle_error
13571357+ $ (const reference_to_url $ Odoc_html_args.extra_args $ root_url
13581358+ $ odoc_file_directories $ reference))
13591359+13601360+ let info ~docs =
13611361+ Cmd.info ~docs ~doc:"Resolve a reference and output its corresponding url."
13621362+ "html-url"
13631363+end
13641364+13651365+module Html_fragment : sig
13661366+ val cmd : unit Term.t
13671367+13681368+ val info : docs:string -> Cmd.info
13691369+end = struct
13701370+ let html_fragment directories xref_base_uri output_file input_file
13711371+ warnings_options =
13721372+ let resolver =
13731373+ Resolver.create ~important_digests:false ~directories ~open_modules:[]
13741374+ ~roots:None
13751375+ in
13761376+ let input_file = Fs.File.of_string input_file in
13771377+ let output_file = Fs.File.of_string output_file in
13781378+ let xref_base_uri =
13791379+ if xref_base_uri = "" then xref_base_uri
13801380+ else
13811381+ let last_char = xref_base_uri.[String.length xref_base_uri - 1] in
13821382+ if last_char <> '/' then xref_base_uri ^ "/" else xref_base_uri
13831383+ in
13841384+ Html_fragment.from_mld ~resolver ~xref_base_uri ~output:output_file
13851385+ ~warnings_options input_file
13861386+13871387+ let cmd =
13881388+ let output =
13891389+ let doc = "Output HTML fragment file." in
13901390+ Arg.(
13911391+ value & opt string "/dev/stdout"
13921392+ & info ~docs ~docv:"file.html" ~doc [ "o"; "output-file" ])
13931393+ in
13941394+ let input =
13951395+ let doc = "Input documentation page file." in
13961396+ Arg.(required & pos 0 (some file) None & info ~doc ~docv:"file.mld" [])
13971397+ in
13981398+ let xref_base_uri =
13991399+ let doc =
14001400+ "Base URI used to resolve cross-references. Set this to the root of \
14011401+ the global docset during local development. By default `.' is used."
14021402+ in
14031403+ Arg.(value & opt string "" & info ~docv:"URI" ~doc [ "xref-base-uri" ])
14041404+ in
14051405+ Term.(
14061406+ const handle_error
14071407+ $ (const html_fragment $ odoc_file_directories $ xref_base_uri $ output
14081408+ $ input $ warnings_options))
14091409+14101410+ let info ~docs =
14111411+ Cmd.info ~docs ~doc:"Generates an html fragment file from an mld one."
14121412+ "html-fragment"
14131413+end
14141414+14151415+module Odoc_manpage = Make_renderer (struct
14161416+ type args = unit
14171417+14181418+ let renderer = Man_page.renderer
14191419+14201420+ let extra_args = Term.const ()
14211421+end)
14221422+14231423+module Odoc_latex = Make_renderer (struct
14241424+ type args = Latex.args
14251425+14261426+ let renderer = Latex.renderer
14271427+14281428+ let with_children =
14291429+ let doc = "Include children at the end of the page." in
14301430+ Arg.(value & opt bool true & info ~docv:"BOOL" ~doc [ "with-children" ])
14311431+14321432+ let shorten_beyond_depth =
14331433+ let doc = "Shorten items beyond the given depth." in
14341434+ Arg.(
14351435+ value
14361436+ & opt (some' int) None
14371437+ & info ~docv:"INT" ~doc [ "shorten-beyond-depth" ])
14381438+14391439+ let remove_functor_arg_link =
14401440+ let doc = "Remove link to functor argument." in
14411441+ Arg.(
14421442+ value & opt bool false
14431443+ & info ~docv:"BOOL" ~doc [ "remove-functor-arg-link" ])
14441444+14451445+ let extra_args =
14461446+ let f with_children shorten_beyond_depth remove_functor_arg_link =
14471447+ { Latex.with_children; shorten_beyond_depth; remove_functor_arg_link }
14481448+ in
14491449+ Term.(
14501450+ const f $ with_children $ shorten_beyond_depth $ remove_functor_arg_link)
14511451+end)
14521452+14531453+module Depends = struct
14541454+ module Compile = struct
14551455+ let list_dependencies input_files =
14561456+ try
14571457+ let deps =
14581458+ Depends.for_compile_step (List.map ~f:Fs.File.of_string input_files)
14591459+ in
14601460+ List.iter
14611461+ ~f:(fun t ->
14621462+ Printf.printf "%s %s\n" (Depends.Compile.name t)
14631463+ (Digest.to_hex @@ Depends.Compile.digest t))
14641464+ deps;
14651465+ flush stdout
14661466+ with Cmi_format.Error e ->
14671467+ let msg =
14681468+ match e with
14691469+ | Not_an_interface file ->
14701470+ Printf.sprintf "File %S is not an interface" file
14711471+ | Wrong_version_interface (file, v) ->
14721472+ Printf.sprintf "File %S is compiled for %s version of OCaml" file
14731473+ v
14741474+ | Corrupted_interface file ->
14751475+ Printf.sprintf "File %S is corrupted" file
14761476+ in
14771477+ Printf.eprintf "ERROR: %s\n%!" msg;
14781478+ exit 1
14791479+14801480+ let cmd =
14811481+ let input =
14821482+ let doc = "Input files" in
14831483+ Arg.(non_empty & pos_all file [] & info ~doc ~docv:"file.cm{i,t,ti}" [])
14841484+ in
14851485+ Term.(const list_dependencies $ input)
14861486+14871487+ let info ~docs =
14881488+ Cmd.info "compile-deps" ~docs
14891489+ ~doc:
14901490+ "List units (with their digest) which needs to be compiled in order \
14911491+ to compile this one. The unit itself and its digest is also \
14921492+ reported in the output.\n\
14931493+ Dependencies between compile steps are the same as when compiling \
14941494+ the ocaml modules."
14951495+ end
14961496+14971497+ module Link = struct
14981498+ let rec fmt_page pp page =
14991499+ match page.Odoc_model.Paths.Identifier.iv with
15001500+ | `Page (parent_opt, name) ->
15011501+ Format.fprintf pp "%a%a" fmt_parent_opt parent_opt
15021502+ Odoc_model.Names.PageName.fmt name
15031503+ | `LeafPage (parent_opt, name) ->
15041504+ Format.fprintf pp "%a%a" fmt_parent_opt parent_opt
15051505+ Odoc_model.Names.PageName.fmt name
15061506+15071507+ and fmt_parent_opt pp parent_opt =
15081508+ match parent_opt with
15091509+ | None -> ()
15101510+ | Some p -> Format.fprintf pp "%a/" fmt_page p
15111511+15121512+ let list_dependencies input_file =
15131513+ Depends.for_rendering_step (Fs.Directory.of_string input_file)
15141514+ >>= fun depends ->
15151515+ List.iter depends ~f:(fun (root : Odoc_model.Root.t) ->
15161516+ match root.id.iv with
15171517+ | `Root (Some p, _) ->
15181518+ Format.printf "%a %s %s\n" fmt_page p
15191519+ (Odoc_model.Root.Odoc_file.name root.file)
15201520+ (Digest.to_hex root.digest)
15211521+ | _ ->
15221522+ Format.printf "none %s %s\n"
15231523+ (Odoc_model.Root.Odoc_file.name root.file)
15241524+ (Digest.to_hex root.digest));
15251525+ Ok ()
15261526+15271527+ let cmd =
15281528+ let input =
15291529+ let doc = "Input directory" in
15301530+ Arg.(required & pos 0 (some file) None & info ~doc ~docv:"PKG_DIR" [])
15311531+ in
15321532+ Term.(const handle_error $ (const list_dependencies $ input))
15331533+15341534+ let info ~docs =
15351535+ Cmd.info "link-deps" ~docs
15361536+ ~doc:
15371537+ "Lists a subset of the packages and modules which need to be in \
15381538+ odoc's load path to link the $(i, odoc) files in the given \
15391539+ directory. Additional packages may be required to resolve all \
15401540+ references."
15411541+ end
15421542+15431543+ module Odoc_html = struct
15441544+ let includes =
15451545+ let doc = "For backwards compatibility. Ignored." in
15461546+ Arg.(
15471547+ value
15481548+ & opt_all (convert_directory ()) []
15491549+ & info ~docs ~docv:"DIR" ~doc [ "I" ])
15501550+15511551+ let cmd =
15521552+ let input =
15531553+ let doc = "Input directory" in
15541554+ Arg.(required & pos 0 (some file) None & info ~doc ~docv:"PKG_DIR" [])
15551555+ in
15561556+ let cmd _ = Link.list_dependencies in
15571557+ Term.(const handle_error $ (const cmd $ includes $ input))
15581558+15591559+ let info ~docs =
15601560+ Cmd.info "html-deps" ~docs ~doc:"DEPRECATED: alias for link-deps"
15611561+ end
15621562+end
15631563+15641564+module Targets = struct
15651565+ module Compile = struct
15661566+ let list_targets dst input =
15671567+ let input = Fs.File.of_string input in
15681568+ let output = Compile.output_file ~dst ~input in
15691569+ Printf.printf "%s\n" (Fs.File.to_string output);
15701570+ flush stdout
15711571+15721572+ let cmd = Term.(const list_targets $ Compile.dst $ Compile.input)
15731573+15741574+ let info ~docs =
15751575+ Cmd.info "compile-targets" ~docs
15761576+ ~doc:
15771577+ "Print the name of the file produced by $(i,compile). If $(i,-o) is \
15781578+ passed, the same path is printed but error checking is performed."
15791579+ end
15801580+15811581+ module Support_files = struct
15821582+ let list_targets without_theme output_directory =
15831583+ Support_files.print_filenames ~without_theme output_directory
15841584+15851585+ let cmd =
15861586+ Term.(const list_targets $ Support_files_command.without_theme $ dst ())
15871587+15881588+ let info ~docs =
15891589+ Cmd.info "support-files-targets" ~docs
15901590+ ~doc:
15911591+ "Lists the names of the files that $(i,odoc support-files) outputs."
15921592+ end
15931593+end
15941594+15951595+module Occurrences = struct
15961596+ let dst_of_string s =
15971597+ let f = Fs.File.of_string s in
15981598+ if not (Fs.File.has_ext ".odoc-occurrences" f) then
15991599+ Error (`Msg "Output file must have '.odoc-occurrences' extension.")
16001600+ else Ok f
16011601+16021602+ module Count = struct
16031603+ let count directories dst warnings_options include_hidden =
16041604+ dst_of_string dst >>= fun dst ->
16051605+ Occurrences.count ~dst ~warnings_options directories include_hidden
16061606+16071607+ let cmd =
16081608+ let dst =
16091609+ let doc = "Output file path." in
16101610+ Arg.(
16111611+ required
16121612+ & opt (some string) None
16131613+ & info ~docs ~docv:"PATH" ~doc [ "o" ])
16141614+ in
16151615+ let include_hidden =
16161616+ let doc = "Include hidden identifiers in the table" in
16171617+ Arg.(value & flag & info ~docs ~doc [ "include-hidden" ])
16181618+ in
16191619+ let input =
16201620+ let doc =
16211621+ "Directories to recursively traverse, agregating occurrences from \
16221622+ $(i,impl-*.odocl) files. Can be present several times."
16231623+ in
16241624+ Arg.(
16251625+ value
16261626+ & pos_all (convert_directory ()) []
16271627+ & info ~docs ~docv:"DIR" ~doc [])
16281628+ in
16291629+ Term.(
16301630+ const handle_error
16311631+ $ (const count $ input $ dst $ warnings_options $ include_hidden))
16321632+16331633+ let info ~docs =
16341634+ let doc =
16351635+ "Generate a hashtable mapping identifiers to number of occurrences, as \
16361636+ computed from the implementations of .odocl files found in the given \
16371637+ directories."
16381638+ in
16391639+ Cmd.info "count-occurrences" ~docs ~doc
16401640+ end
16411641+ module Aggregate = struct
16421642+ let index dst files file_list strip_path warnings_options =
16431643+ match (files, file_list) with
16441644+ | [], [] ->
16451645+ Error
16461646+ (`Msg
16471647+ "At least one of --file-list or a path to a file must be passed \
16481648+ to odoc aggregate-occurrences")
16491649+ | _ ->
16501650+ dst_of_string dst >>= fun dst ->
16511651+ Occurrences.aggregate ~dst ~warnings_options ~strip_path files
16521652+ file_list
16531653+16541654+ let cmd =
16551655+ let dst =
16561656+ let doc = "Output file path." in
16571657+ Arg.(
16581658+ required
16591659+ & opt (some string) None
16601660+ & info ~docs ~docv:"PATH" ~doc [ "o" ])
16611661+ in
16621662+ let inputs_in_file =
16631663+ let doc =
16641664+ "Input text file containing a line-separated list of paths to files \
16651665+ created with count-occurrences."
16661666+ in
16671667+ Arg.(
16681668+ value & opt_all convert_fpath []
16691669+ & info ~doc ~docv:"FILE" [ "file-list" ])
16701670+ in
16711671+ let inputs =
16721672+ let doc = "file created with count-occurrences" in
16731673+ Arg.(value & pos_all convert_fpath [] & info ~doc ~docv:"FILE" [])
16741674+ in
16751675+ let strip_path =
16761676+ let doc = "Strip package/version information from paths" in
16771677+ Arg.(value & flag & info ~doc [ "strip-path" ])
16781678+ in
16791679+ Term.(
16801680+ const handle_error
16811681+ $ (const index $ dst $ inputs $ inputs_in_file $ strip_path
16821682+ $ warnings_options))
16831683+16841684+ let info ~docs =
16851685+ let doc = "Aggregate hashtables created with odoc count-occurrences." in
16861686+ Cmd.info "aggregate-occurrences" ~docs ~doc
16871687+ end
16881688+end
16891689+16901690+module Odoc_error = struct
16911691+ let errors input =
16921692+ let open Odoc_odoc in
16931693+ let input = Fs.File.of_string input in
16941694+ Odoc_file.load input >>= fun unit ->
16951695+ Odoc_model.Error.print_errors unit.warnings;
16961696+ Ok ()
16971697+16981698+ let input =
16991699+ let doc = "Input $(i,.odoc) or $(i,.odocl) file" in
17001700+ Arg.(required & pos 0 (some file) None & info ~doc ~docv:"FILE" [])
17011701+17021702+ let cmd = Term.(const handle_error $ (const errors $ input))
17031703+17041704+ let info ~docs =
17051705+ Cmd.info "errors" ~docs
17061706+ ~doc:"Print errors that occurred while compiling or linking."
17071707+end
17081708+17091709+module Classify = struct
17101710+ let libdirs =
17111711+ let doc = "The directories containing the libraries" in
17121712+ Arg.(value & pos_all string [] & info ~doc ~docv:"DIR" [])
17131713+17141714+ let cmd = Term.(const handle_error $ (const Classify.classify $ libdirs))
17151715+17161716+ let info ~docs =
17171717+ Cmd.info "classify" ~docs
17181718+ ~doc:
17191719+ "Classify the modules into libraries based on heuristics. Libraries \
17201720+ are specified by the --library option."
17211721+end
17221722+17231723+module Extract_code = struct
17241724+ let extract dst input line_directives names warnings_options =
17251725+ Extract_code.extract ~dst ~input ~line_directives ~names ~warnings_options
17261726+17271727+ let line_directives =
17281728+ let doc = "Whether to include line directives in the output file" in
17291729+ Arg.(value & flag & info ~doc [ "line-directives" ])
17301730+17311731+ let names =
17321732+ let doc =
17331733+ "From which name(s) of code blocks to extract content. When no names are \
17341734+ provided, extract all OCaml code blocks."
17351735+ in
17361736+ Arg.(value & opt_all string [] & info ~doc [ "name" ])
17371737+17381738+ let input =
17391739+ let doc = "Input $(i,.mld) file." in
17401740+ Arg.(required & pos 0 (some file) None & info ~doc ~docv:"FILE" [])
17411741+17421742+ let dst =
17431743+ let doc = "Output file path." in
17441744+ Arg.(
17451745+ value
17461746+ & opt (some string) None
17471747+ & info ~docs ~docv:"PATH" ~doc [ "o"; "output" ])
17481748+17491749+ let cmd =
17501750+ Term.(
17511751+ const handle_error
17521752+ $ (const extract $ dst $ input $ line_directives $ names
17531753+ $ warnings_options))
17541754+17551755+ let info ~docs =
17561756+ Cmd.info "extract-code" ~docs
17571757+ ~doc:
17581758+ "Extract code blocks from mld files in order to be able to execute them"
17591759+end
17601760+17611761+let section_pipeline = "COMMANDS: Compilation pipeline"
17621762+let section_generators = "COMMANDS: Alternative generators"
17631763+let section_support = "COMMANDS: Scripting"
17641764+let section_legacy = "COMMANDS: Legacy pipeline"
17651765+let section_deprecated = "COMMANDS: Deprecated"
17661766+17671767+module Extensions = struct
17681768+ let run () =
17691769+ let prefixes = Odoc_extension_api.Registry.list_prefixes () in
17701770+ match prefixes with
17711771+ | [] ->
17721772+ Printf.printf "No extensions installed.\n%!";
17731773+ Printf.printf "Extensions can be installed as opam packages that register with odoc.\n%!"
17741774+ | _ ->
17751775+ Printf.printf "Installed extensions:\n%!";
17761776+ List.iter ~f:(fun prefix -> Printf.printf " @%s\n%!" prefix) prefixes
17771777+17781778+ let cmd = Term.(const run $ const ())
17791779+ let info ~docs = Cmd.info "extensions" ~docs ~doc:"List installed odoc extensions"
17801780+end
17811781+17821782+(** Sections in the order they should appear. *)
17831783+let main_page_sections =
17841784+ [
17851785+ section_pipeline;
17861786+ section_generators;
17871787+ section_support;
17881788+ section_legacy;
17891789+ section_deprecated;
17901790+ ]
17911791+17921792+let () =
17931793+ Printexc.record_backtrace true;
17941794+ let cmd_make (term, info) = Cmd.v info term in
17951795+ let subcommands =
17961796+ List.map ~f:cmd_make
17971797+ @@ [
17981798+ Occurrences.Count.(cmd, info ~docs:section_pipeline);
17991799+ Occurrences.Aggregate.(cmd, info ~docs:section_pipeline);
18001800+ Compile.(cmd, info ~docs:section_pipeline);
18011801+ Compile_asset.(cmd, info ~docs:section_pipeline);
18021802+ Odoc_link.(cmd, info ~docs:section_pipeline);
18031803+ Odoc_html.generate ~docs:section_pipeline;
18041804+ Odoc_html.generate_source ~docs:section_pipeline;
18051805+ Odoc_html.generate_asset ~docs:section_pipeline;
18061806+ Support_files_command.(cmd, info ~docs:section_pipeline);
18071807+ Compile_impl.(cmd, info ~docs:section_pipeline);
18081808+ Indexing.(cmd, info ~docs:section_pipeline);
18091809+ Sidebar.(cmd, info ~docs:section_pipeline);
18101810+ Odoc_markdown_cmd.generate ~docs:section_generators;
18111811+ Odoc_markdown_cmd.generate_source ~docs:section_generators;
18121812+ Odoc_markdown_cmd.targets ~docs:section_support;
18131813+ Odoc_manpage.generate ~docs:section_generators;
18141814+ Odoc_latex.generate ~docs:section_generators;
18151815+ Odoc_html_url.(cmd, info ~docs:section_support);
18161816+ Odoc_latex_url.(cmd, info ~docs:section_support);
18171817+ Targets.Support_files.(cmd, info ~docs:section_support);
18181818+ Odoc_error.(cmd, info ~docs:section_support);
18191819+ Odoc_html.targets ~docs:section_support;
18201820+ Odoc_html.targets_source ~docs:section_support;
18211821+ Odoc_manpage.targets ~docs:section_support;
18221822+ Odoc_latex.targets ~docs:section_support;
18231823+ Depends.Compile.(cmd, info ~docs:section_support);
18241824+ Targets.Compile.(cmd, info ~docs:section_support);
18251825+ Html_fragment.(cmd, info ~docs:section_legacy);
18261826+ Odoc_html.process ~docs:section_legacy;
18271827+ Odoc_manpage.process ~docs:section_legacy;
18281828+ Odoc_latex.process ~docs:section_legacy;
18291829+ Depends.Link.(cmd, info ~docs:section_legacy);
18301830+ Css.(cmd, info ~docs:section_deprecated);
18311831+ Depends.Odoc_html.(cmd, info ~docs:section_deprecated);
18321832+ Classify.(cmd, info ~docs:section_pipeline);
18331833+ Extract_code.(cmd, info ~docs:section_pipeline);
18341834+ Extensions.(cmd, info ~docs:section_support);
18351835+ ]
18361836+ in
18371837+ let main =
18381838+ let print_default () =
18391839+ let available_subcommands =
18401840+ List.map subcommands ~f:(fun cmd -> Cmd.name cmd)
18411841+ in
18421842+ Printf.printf
18431843+ "Available subcommands: %s\nSee --help for more information.\n%!"
18441844+ (String.concat ~sep:", " available_subcommands)
18451845+ in
18461846+ let man =
18471847+ (* Show sections in a defined order. *)
18481848+ List.map ~f:(fun s -> `S s) main_page_sections
18491849+ in
18501850+ let default = Term.(const print_default $ const ()) in
18511851+ let info = Cmd.info ~man ~version:"%%VERSION%%" "odoc" in
18521852+ Cmd.group ~default info subcommands
18531853+ in
18541854+ match Cmd.eval_value ~err:Format.err_formatter main with
18551855+ | Error _ ->
18561856+ Format.pp_print_flush Format.err_formatter ();
18571857+ exit 2
18581858+ | _ -> ()
+384
test/scrollycode-demos/warm_parser.mld
···11+{0 Building a JSON Parser}
22+33+@scrolly.warm Building a JSON Parser in OCaml
44+{ol
55+{li
66+ {b Defining the Value Type}
77+88+ Every parser starts with a type. JSON has six kinds of values:
99+ null, booleans, numbers, strings, arrays, and objects.
1010+ We encode this directly as an OCaml variant.
1111+1212+ {[
1313+type json =
1414+ | Null
1515+ | Bool of bool
1616+ | Number of float
1717+ | String of string
1818+ | Array of json list
1919+ | Object of (string * json) list
2020+ ]}
2121+}
2222+{li
2323+ {b A Simple Scanner}
2424+2525+ Before parsing structure, we need to skip whitespace and
2626+ peek at the next meaningful character. Our scanner works
2727+ on a string with a mutable position index.
2828+2929+ {[
3030+type json =
3131+ | Null
3232+ | Bool of bool
3333+ | Number of float
3434+ | String of string
3535+ | Array of json list
3636+ | Object of (string * json) list
3737+3838+type scanner = {
3939+ input : string;
4040+ mutable pos : int;
4141+}
4242+4343+let peek s =
4444+ while s.pos < String.length s.input
4545+ && s.input.[s.pos] = ' ' do
4646+ s.pos <- s.pos + 1
4747+ done;
4848+ if s.pos < String.length s.input
4949+ then Some s.input.[s.pos]
5050+ else None
5151+5252+let advance s = s.pos <- s.pos + 1
5353+ ]}
5454+}
5555+{li
5656+ {b Parsing Strings}
5757+5858+ JSON strings are delimited by double quotes. We scan character
5959+ by character, collecting into a buffer. This handles the simple
6060+ case without escape sequences.
6161+6262+ {[
6363+type json =
6464+ | Null
6565+ | Bool of bool
6666+ | Number of float
6767+ | String of string
6868+ | Array of json list
6969+ | Object of (string * json) list
7070+7171+type scanner = {
7272+ input : string;
7373+ mutable pos : int;
7474+}
7575+7676+let peek s =
7777+ while s.pos < String.length s.input
7878+ && s.input.[s.pos] = ' ' do
7979+ s.pos <- s.pos + 1
8080+ done;
8181+ if s.pos < String.length s.input
8282+ then Some s.input.[s.pos]
8383+ else None
8484+8585+let advance s = s.pos <- s.pos + 1
8686+8787+let parse_string s =
8888+ advance s;
8989+ let buf = Buffer.create 64 in
9090+ while s.pos < String.length s.input
9191+ && s.input.[s.pos] <> '"' do
9292+ Buffer.add_char buf s.input.[s.pos];
9393+ advance s
9494+ done;
9595+ advance s;
9696+ Buffer.contents buf
9797+ ]}
9898+}
9999+{li
100100+ {b Parsing Numbers}
101101+102102+ Numbers in JSON can be integers or floats. We collect consecutive
103103+ digit and dot characters, then use float_of_string to parse them.
104104+ A production parser would handle exponents too.
105105+106106+ {[
107107+type json =
108108+ | Null
109109+ | Bool of bool
110110+ | Number of float
111111+ | String of string
112112+ | Array of json list
113113+ | Object of (string * json) list
114114+115115+type scanner = {
116116+ input : string;
117117+ mutable pos : int;
118118+}
119119+120120+let peek s =
121121+ while s.pos < String.length s.input
122122+ && s.input.[s.pos] = ' ' do
123123+ s.pos <- s.pos + 1
124124+ done;
125125+ if s.pos < String.length s.input
126126+ then Some s.input.[s.pos]
127127+ else None
128128+129129+let advance s = s.pos <- s.pos + 1
130130+131131+let parse_string s =
132132+ advance s;
133133+ let buf = Buffer.create 64 in
134134+ while s.pos < String.length s.input
135135+ && s.input.[s.pos] <> '"' do
136136+ Buffer.add_char buf s.input.[s.pos];
137137+ advance s
138138+ done;
139139+ advance s;
140140+ Buffer.contents buf
141141+142142+let is_digit c = c >= '0' && c <= '9'
143143+144144+let parse_number s =
145145+ let start = s.pos in
146146+ while s.pos < String.length s.input
147147+ && (is_digit s.input.[s.pos]
148148+ || s.input.[s.pos] = '.'
149149+ || s.input.[s.pos] = '-') do
150150+ advance s
151151+ done;
152152+ float_of_string
153153+ (String.sub s.input start (s.pos - start))
154154+ ]}
155155+}
156156+{li
157157+ {b The Recursive Core}
158158+159159+ Now the magic: parse_value dispatches on the next character
160160+ to decide what kind of JSON value to parse. For atoms like
161161+ null, true, false we match literal strings. For compound
162162+ structures, we recurse.
163163+164164+ {[
165165+type json =
166166+ | Null
167167+ | Bool of bool
168168+ | Number of float
169169+ | String of string
170170+ | Array of json list
171171+ | Object of (string * json) list
172172+173173+type scanner = {
174174+ input : string;
175175+ mutable pos : int;
176176+}
177177+178178+let peek s =
179179+ while s.pos < String.length s.input
180180+ && s.input.[s.pos] = ' ' do
181181+ s.pos <- s.pos + 1
182182+ done;
183183+ if s.pos < String.length s.input
184184+ then Some s.input.[s.pos]
185185+ else None
186186+187187+let advance s = s.pos <- s.pos + 1
188188+189189+let parse_string s =
190190+ advance s;
191191+ let buf = Buffer.create 64 in
192192+ while s.pos < String.length s.input
193193+ && s.input.[s.pos] <> '"' do
194194+ Buffer.add_char buf s.input.[s.pos];
195195+ advance s
196196+ done;
197197+ advance s;
198198+ Buffer.contents buf
199199+200200+let is_digit c = c >= '0' && c <= '9'
201201+202202+let parse_number s =
203203+ let start = s.pos in
204204+ while s.pos < String.length s.input
205205+ && (is_digit s.input.[s.pos]
206206+ || s.input.[s.pos] = '.'
207207+ || s.input.[s.pos] = '-') do
208208+ advance s
209209+ done;
210210+ float_of_string
211211+ (String.sub s.input start (s.pos - start))
212212+213213+let expect s c =
214214+ match peek s with
215215+ | Some c' when c' = c -> advance s
216216+ | _ -> failwith "unexpected character"
217217+218218+let rec parse_value s =
219219+ match peek s with
220220+ | Some '"' -> String (parse_string s)
221221+ | Some c when is_digit c || c = '-' ->
222222+ Number (parse_number s)
223223+ | Some 't' ->
224224+ s.pos <- s.pos + 4; Bool true
225225+ | Some 'f' ->
226226+ s.pos <- s.pos + 5; Bool false
227227+ | Some 'n' ->
228228+ s.pos <- s.pos + 4; Null
229229+ | Some '[' -> parse_array s
230230+ | Some '{' -> parse_object s
231231+ | _ -> failwith "unexpected token"
232232+233233+and parse_array s =
234234+ advance s;
235235+ let items = ref [] in
236236+ (match peek s with
237237+ | Some ']' -> advance s
238238+ | _ ->
239239+ items := [parse_value s];
240240+ while peek s = Some ',' do
241241+ advance s;
242242+ items := parse_value s :: !items
243243+ done;
244244+ expect s ']');
245245+ Array (List.rev !items)
246246+247247+and parse_object s =
248248+ advance s;
249249+ let pairs = ref [] in
250250+ (match peek s with
251251+ | Some '}' -> advance s
252252+ | _ ->
253253+ let key = parse_string s in
254254+ expect s ':';
255255+ let value = parse_value s in
256256+ pairs := [(key, value)];
257257+ while peek s = Some ',' do
258258+ advance s;
259259+ let k = parse_string s in
260260+ expect s ':';
261261+ let v = parse_value s in
262262+ pairs := (k, v) :: !pairs
263263+ done;
264264+ expect s '}');
265265+ Object (List.rev !pairs)
266266+ ]}
267267+}
268268+{li
269269+ {b The Public API}
270270+271271+ Finally we wrap the scanner in a clean top-level function.
272272+ Pass a string in, get a JSON value out. The entire parser
273273+ is about 80 lines of OCaml — no dependencies, no magic.
274274+275275+ {[
276276+type json =
277277+ | Null
278278+ | Bool of bool
279279+ | Number of float
280280+ | String of string
281281+ | Array of json list
282282+ | Object of (string * json) list
283283+284284+type scanner = {
285285+ input : string;
286286+ mutable pos : int;
287287+}
288288+289289+let peek s =
290290+ while s.pos < String.length s.input
291291+ && s.input.[s.pos] = ' ' do
292292+ s.pos <- s.pos + 1
293293+ done;
294294+ if s.pos < String.length s.input
295295+ then Some s.input.[s.pos]
296296+ else None
297297+298298+let advance s = s.pos <- s.pos + 1
299299+300300+let parse_string s =
301301+ advance s;
302302+ let buf = Buffer.create 64 in
303303+ while s.pos < String.length s.input
304304+ && s.input.[s.pos] <> '"' do
305305+ Buffer.add_char buf s.input.[s.pos];
306306+ advance s
307307+ done;
308308+ advance s;
309309+ Buffer.contents buf
310310+311311+let is_digit c = c >= '0' && c <= '9'
312312+313313+let parse_number s =
314314+ let start = s.pos in
315315+ while s.pos < String.length s.input
316316+ && (is_digit s.input.[s.pos]
317317+ || s.input.[s.pos] = '.'
318318+ || s.input.[s.pos] = '-') do
319319+ advance s
320320+ done;
321321+ float_of_string
322322+ (String.sub s.input start (s.pos - start))
323323+324324+let expect s c =
325325+ match peek s with
326326+ | Some c' when c' = c -> advance s
327327+ | _ -> failwith "unexpected character"
328328+329329+let rec parse_value s =
330330+ match peek s with
331331+ | Some '"' -> String (parse_string s)
332332+ | Some c when is_digit c || c = '-' ->
333333+ Number (parse_number s)
334334+ | Some 't' ->
335335+ s.pos <- s.pos + 4; Bool true
336336+ | Some 'f' ->
337337+ s.pos <- s.pos + 5; Bool false
338338+ | Some 'n' ->
339339+ s.pos <- s.pos + 4; Null
340340+ | Some '[' -> parse_array s
341341+ | Some '{' -> parse_object s
342342+ | _ -> failwith "unexpected token"
343343+344344+and parse_array s =
345345+ advance s;
346346+ let items = ref [] in
347347+ (match peek s with
348348+ | Some ']' -> advance s
349349+ | _ ->
350350+ items := [parse_value s];
351351+ while peek s = Some ',' do
352352+ advance s;
353353+ items := parse_value s :: !items
354354+ done;
355355+ expect s ']');
356356+ Array (List.rev !items)
357357+358358+and parse_object s =
359359+ advance s;
360360+ let pairs = ref [] in
361361+ (match peek s with
362362+ | Some '}' -> advance s
363363+ | _ ->
364364+ let key = parse_string s in
365365+ expect s ':';
366366+ let value = parse_value s in
367367+ pairs := [(key, value)];
368368+ while peek s = Some ',' do
369369+ advance s;
370370+ let k = parse_string s in
371371+ expect s ':';
372372+ let v = parse_value s in
373373+ pairs := (k, v) :: !pairs
374374+ done;
375375+ expect s '}');
376376+ Object (List.rev !pairs)
377377+378378+let parse input =
379379+ let s = { input; pos = 0 } in
380380+ let v = parse_value s in
381381+ v
382382+ ]}
383383+}
384384+}