···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
odoc-scrollycode-extension/test/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
···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
odoc-scrollycode-extension/test/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-}