• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ocaml / odoc / 2735

15 Jan 2025 05:29PM UTC coverage: 73.399% (-0.07%) from 73.471%
2735

push

github

jonludlam
Update test results

10256 of 13973 relevant lines covered (73.4%)

9962.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.19
/src/odoc/bin/main.ml
1
(* CR-someday trefis: the "deps" and "targets" subcommands currently output
2
   their result on stdout.
3
   It would make the interaction with jenga nicer if we could specify a file to
4
   output the result to. *)
5

6
open Odoc_odoc
7
open Compatcmdliner
8

9
let convert_syntax : Odoc_document.Renderer.syntax Arg.conv =
10
  let syntax_parser str =
11
    match str with
×
12
    | "ml" | "ocaml" -> `Ok Odoc_document.Renderer.OCaml
×
13
    | "re" | "reason" -> `Ok Odoc_document.Renderer.Reason
×
14
    | s -> `Error (Printf.sprintf "Unknown syntax '%s'" s)
×
15
  in
16
  let syntax_printer fmt syntax =
17
    Format.pp_print_string fmt (Odoc_document.Renderer.string_of_syntax syntax)
×
18
  in
19
  (syntax_parser, syntax_printer)
20

21
let convert_directory ?(create = false) () : Fs.Directory.t Arg.conv =
15,652✔
22
  let dir_parser, dir_printer = Arg.string in
30,186✔
23
  let odoc_dir_parser str =
24
    let () = if create then Fs.Directory.(mkdir_p (of_string str)) in
294✔
25
    match dir_parser str with
26
    | `Ok res -> `Ok (Fs.Directory.of_string res)
774✔
27
    | `Error e -> `Error e
×
28
  in
29
  let odoc_dir_printer fmt dir = dir_printer fmt (Fs.Directory.to_string dir) in
×
30
  (odoc_dir_parser, odoc_dir_printer)
31

32
(** On top of the conversion 'file' that checks that the passed file exists. *)
33
let convert_fpath =
34
  let parse inp =
35
    match Arg.(conv_parser file) inp with
103✔
36
    | Ok s -> Result.Ok (Fs.File.of_string s)
102✔
37
    | Error _ as e -> e
1✔
38
  and print = Fpath.pp in
39
  Arg.conv (parse, print)
1,118✔
40

41
let convert_named_root =
42
  let parse inp =
43
    match Astring.String.cuts inp ~sep:":" with
94✔
44
    | [ s1; s2 ] -> Result.Ok (s1, Fs.Directory.of_string s2)
94✔
45
    | _ -> Error (`Msg "")
×
46
  in
47
  let print ppf (s, t) =
48
    Format.fprintf ppf "%s:%s" s (Fs.Directory.to_string t)
×
49
  in
50
  Arg.conv (parse, print)
1,118✔
51

52
let handle_error = function
53
  | Result.Ok () -> ()
1,060✔
54
  | Error (`Cli_error msg) ->
6✔
55
      Printf.eprintf "%s\n%!" msg;
56
      exit 2
6✔
57
  | Error (`Msg msg) ->
20✔
58
      Printf.eprintf "ERROR: %s\n%!" msg;
59
      exit 1
20✔
60

61
module Antichain = struct
62
  let absolute_normalization p =
63
    let p =
742✔
64
      if Fpath.is_rel p then Fpath.( // ) (Fpath.v (Sys.getcwd ())) p else p
9✔
65
    in
66
    Fpath.normalize p
67

68
  (** Check that a list of directories form an antichain: they are all disjoints *)
69
  let check ~opt l =
70
    let l =
569✔
71
      List.map
72
        ~f:(fun p -> p |> Fs.Directory.to_fpath |> absolute_normalization)
94✔
73
        l
74
    in
75
    let rec check = function
569✔
76
      | [] -> true
564✔
77
      | p1 :: rest ->
89✔
78
          List.for_all
89✔
79
            ~f:(fun p2 ->
80
              (not (Fpath.is_prefix p1 p2)) && not (Fpath.is_prefix p2 p1))
5✔
81
            rest
82
          && check rest
84✔
83
    in
84
    if check l then Result.Ok ()
564✔
85
    else
86
      let msg =
5✔
87
        Format.sprintf "Paths given to all %s options must be disjoint" opt
88
      in
89
      Error (`Msg msg)
5✔
90
end
91

92
let docs = "ARGUMENTS"
93

94
let odoc_file_directories =
95
  let doc =
96
    "Where to look for required $(i,.odoc) files. Can be present several times."
97
  in
98
  Arg.(
99
    value
100
    & opt_all (convert_directory ()) []
1,118✔
101
    & info ~docs ~docv:"DIR" ~doc [ "I" ])
1,118✔
102

103
let hidden =
104
  let doc =
105
    "Mark the unit as hidden. (Useful for files included in module packs)."
106
  in
107
  Arg.(value & flag & info ~docs ~doc [ "hidden" ])
1,118✔
108

109
let extra_suffix =
110
  let doc =
111
    "Extra suffix to append to generated filenames. This is intended for \
112
     expect tests to use."
113
  in
114
  let default = None in
115
  Arg.(
116
    value
117
    & opt (some string) default
1,118✔
118
    & info ~docv:"SUFFIX" ~doc [ "extra-suffix" ])
1,118✔
119

120
let warnings_options =
121
  let warn_error =
122
    let doc = "Turn warnings into errors." in
123
    let env =
124
      Arg.env_var "ODOC_WARN_ERROR" ~doc:(doc ^ " See option $(opt).")
125
    in
126
    Arg.(value & flag & info ~docs ~doc ~env [ "warn-error" ])
1,118✔
127
  in
128
  let print_warnings =
129
    let doc =
130
      "Whether warnings should be printed to stderr. See the $(b,errors) \
131
       command."
132
    in
133
    let env = Arg.env_var "ODOC_PRINT_WARNINGS" ~doc in
134
    Arg.(value & opt bool true & info ~docs ~doc ~env [ "print-warnings" ])
1,118✔
135
  in
136
  let enable_missing_root_warning =
137
    let doc =
138
      "Produce a warning when a root is missing. This is usually a build \
139
       system problem so is disabled for users by default."
140
    in
141
    let env = Arg.env_var "ODOC_ENABLE_MISSING_ROOT_WARNING" ~doc in
142
    Arg.(value & flag & info ~docs ~doc ~env [ "enable-missing-root-warning" ])
1,118✔
143
  in
144
  let suppress_warnings =
145
    let doc =
146
      "Suppress warnings. This is useful when you want to declare that \
147
       warnings that would be generated resolving the references defined in \
148
       this unit should be ignored if they end up in expansions in other \
149
       units."
150
    in
151
    let env = Arg.env_var "ODOC_SUPPRESS_WARNINGS" ~doc in
152
    Arg.(value & flag & info ~docs ~doc ~env [ "suppress-warnings" ])
1,118✔
153
  in
154
  Term.(
155
    const
1,118✔
156
      (fun
157
        warn_error
158
        print_warnings
159
        enable_missing_root_warning
160
        suppress_warnings
161
      ->
162
        Odoc_model.Error.enable_missing_root_warning :=
960✔
163
          enable_missing_root_warning;
164
        { Odoc_model.Error.warn_error; print_warnings; suppress_warnings })
165
    $ warn_error $ print_warnings $ enable_missing_root_warning
1,118✔
166
    $ suppress_warnings)
1,118✔
167

168
let dst ?create () =
169
  let doc = "Output directory where the HTML tree is expected to be saved." in
22,360✔
170
  Arg.(
171
    required
172
    & opt (some (convert_directory ?create ())) None
22,360✔
173
    & info ~docs ~docv:"DIR" ~doc [ "o"; "output-dir" ])
22,360✔
174

175
let open_modules =
176
  let doc =
177
    "Initially open module. Can be used more than once. Defaults to 'Stdlib'"
178
  in
179
  let default = [ "Stdlib" ] in
180
  Arg.(value & opt_all string default & info ~docv:"MODULE" ~doc [ "open" ])
1,118✔
181

182
module Compile : sig
183
  val output_file : dst:string option -> input:Fs.file -> Fs.file
184

185
  val input : string Term.t
186

187
  val dst : string option Term.t
188

189
  val cmd : unit Term.t
190

191
  val info : docs:string -> Term.info
192
end = struct
193
  let has_page_prefix file =
194
    file |> Fs.File.basename |> Fs.File.to_string
90✔
195
    |> Astring.String.is_prefix ~affix:"page-"
90✔
196

197
  let unique_id =
198
    let doc = "For debugging use" in
199
    Arg.(value & opt (some string) None & info ~doc ~docv:"ID" [ "unique-id" ])
1,118✔
200

201
  let output_file ~dst ~input =
202
    match dst with
332✔
203
    | Some file ->
41✔
204
        let output = Fs.File.of_string file in
205
        if Fs.File.has_ext ".mld" input && not (has_page_prefix output) then (
×
206
          Printf.eprintf
207
            "ERROR: the name of the .odoc file produced from a .mld must start \
208
             with 'page-'\n\
209
             %!";
210
          exit 1);
×
211
        output
41✔
212
    | None ->
291✔
213
        let output =
214
          if Fs.File.has_ext ".mld" input && not (has_page_prefix input) then
88✔
215
            let directory = Fs.File.dirname input in
88✔
216
            let name = Fs.File.basename input in
88✔
217
            let name = "page-" ^ Fs.File.to_string name in
88✔
218
            Fs.File.create ~directory ~name
219
          else input
203✔
220
        in
221
        Fs.File.(set_ext ".odoc" output)
222

223
  let compile hidden directories resolve_fwd_refs dst output_dir package_opt
224
      parent_name_opt parent_id_opt open_modules children input warnings_options
225
      unique_id short_title =
226
    let open Or_error in
332✔
227
    let _ =
228
      match unique_id with
229
      | Some id -> Odoc_model.Names.set_unique_ident id
7✔
230
      | None -> ()
325✔
231
    in
232
    let resolver =
233
      Resolver.create ~important_digests:(not resolve_fwd_refs) ~directories
234
        ~open_modules ~roots:None
235
    in
236
    let input = Fs.File.of_string input in
237
    let output = output_file ~dst ~input in
332✔
238
    let cli_spec =
239
      let error message = Error (`Cli_error message) in
6✔
240
      match
241
        (parent_name_opt, package_opt, parent_id_opt, children, output_dir)
242
      with
243
      | Some _, None, None, _, None ->
24✔
244
          Ok (Compile.CliParent { parent = parent_name_opt; children; output })
245
      | None, Some p, None, [], None ->
67✔
246
          Ok (Compile.CliPackage { package = p; output })
247
      | None, None, Some p, [], Some output_dir ->
67✔
248
          Ok (Compile.CliParentId { parent_id = p; output_dir })
249
      | None, None, None, _ :: _, None ->
13✔
250
          Ok (Compile.CliParent { parent = None; output; children })
251
      | None, None, None, [], None -> Ok (Compile.CliNoParent output)
155✔
252
      | Some _, Some _, _, _, _ ->
1✔
253
          error "Either --package or --parent should be specified, not both."
1✔
254
      | _, Some _, Some _, _, _ ->
×
255
          error "Either --package or --parent-id should be specified, not both."
×
256
      | Some _, _, Some _, _, _ ->
1✔
257
          error "Either --parent or --parent-id should be specified, not both."
1✔
258
      | _, _, None, _, Some _ ->
2✔
259
          error "--output-dir can only be passed with --parent-id."
2✔
260
      | None, Some _, _, _ :: _, _ ->
1✔
261
          error "--child cannot be passed with --package."
1✔
262
      | None, _, Some _, _ :: _, _ ->
1✔
263
          error "--child cannot be passed with --parent-id."
1✔
264
      | _, _, Some _, _, None ->
×
265
          error "--output-dir is required when passing --parent-id."
×
266
    in
267
    cli_spec >>= fun cli_spec ->
268
    Fs.Directory.mkdir_p (Fs.File.dirname output);
326✔
269
    Compile.compile ~resolver ~cli_spec ~hidden ~warnings_options ~short_title
326✔
270
      input
271

272
  let input =
273
    let doc = "Input $(i,.cmti), $(i,.cmt), $(i,.cmi) or $(i,.mld) file." in
274
    Arg.(required & pos 0 (some file) None & info ~doc ~docv:"FILE" [])
1,118✔
275

276
  let dst =
277
    let doc =
278
      "Output file path. Non-existing intermediate directories are created. If \
279
       absent outputs a $(i,BASE.odoc) file in the same directory as the input \
280
       file where $(i,BASE) is the basename of the input file. For mld files \
281
       the \"page-\" prefix will be added if not already present in the input \
282
       basename."
283
    in
284
    Arg.(value & opt (some string) None & info ~docs ~docv:"PATH" ~doc [ "o" ])
1,118✔
285

286
  let output_dir =
287
    let doc = "Output file directory. " in
288
    Arg.(
289
      value
290
      & opt (some string) None
1,118✔
291
      & info ~docs ~docv:"PATH" ~doc [ "output-dir" ])
1,118✔
292

293
  let children =
294
    let doc =
295
      "Specify the $(i,.odoc) file as a child. Can be used multiple times. \
296
       Only applies to mld files."
297
    in
298
    let default = [] in
299
    Arg.(
300
      value & opt_all string default & info ~docv:"CHILD" ~doc [ "c"; "child" ])
1,118✔
301

302
  let cmd =
303
    let package_opt =
304
      let doc =
305
        "Package the input is part of. Deprecated: use '--parent' instead."
306
      in
307
      Arg.(
308
        value
309
        & opt (some string) None
1,118✔
310
        & info ~docs ~docv:"PKG" ~doc [ "package"; "pkg" ])
1,118✔
311
    in
312
    let parent_opt =
313
      let doc = "Parent page or subpage." in
314
      Arg.(
315
        value
316
        & opt (some string) None
1,118✔
317
        & info ~docs ~docv:"PARENT" ~doc [ "parent" ])
1,118✔
318
    in
319
    let parent_id_opt =
320
      let doc = "Parent id." in
321
      Arg.(
322
        value
323
        & opt (some string) None
1,118✔
324
        & info ~docs ~docv:"PARENT" ~doc [ "parent-id" ])
1,118✔
325
    in
326
    let short_title =
327
      let doc = "Override short_title of an mld file" in
328
      Arg.(
329
        value
330
        & opt (some string) None
1,118✔
331
        & info ~docs ~docv:"TITLE" ~doc [ "short-title" ])
1,118✔
332
    in
333
    let resolve_fwd_refs =
334
      let doc = "Try resolving forward references." in
335
      Arg.(value & flag & info ~doc [ "r"; "resolve-fwd-refs" ])
1,118✔
336
    in
337
    Term.(
338
      const handle_error
1,118✔
339
      $ (const compile $ hidden $ odoc_file_directories $ resolve_fwd_refs $ dst
1,118✔
340
       $ output_dir $ package_opt $ parent_opt $ parent_id_opt $ open_modules
1,118✔
341
       $ children $ input $ warnings_options $ unique_id $ short_title))
1,118✔
342

343
  let info ~docs =
344
    let man =
1,118✔
345
      [
346
        `S "DEPENDENCIES";
347
        `P
348
          "Dependencies between compilation units is the same as while \
349
           compiling the initial OCaml modules.";
350
        `P "Mld pages don't have any dependency.";
351
      ]
352
    in
353
    let doc =
354
      "Compile a $(i,.cmti), $(i,.cmt), $(i,.cmi) or $(i,.mld) file to an \
355
       $(i,.odoc) file."
356
    in
357
    Term.info "compile" ~docs ~doc ~man
358
end
359

360
module Compile_asset = struct
361
  let compile_asset parent_id name output_dir =
362
    Odoc_odoc.Asset.compile ~parent_id ~name ~output_dir
5✔
363

364
  let output_dir =
365
    let doc = "Output file directory. " in
366
    Arg.(
367
      required
368
      & opt (some string) None
1,118✔
369
      & info ~docs ~docv:"PATH" ~doc [ "output-dir" ])
1,118✔
370

371
  let cmd =
372
    let asset_name =
373
      let doc = "Name of the asset." in
374
      Arg.(
375
        required
376
        & opt (some string) None
1,118✔
377
        & info ~docs ~docv:"NAME" ~doc [ "name" ])
1,118✔
378
    in
379
    let parent_id =
380
      let doc = "Parent id." in
381
      Arg.(
382
        required
383
        & opt (some string) None
1,118✔
384
        & info ~docs ~docv:"PARENT" ~doc [ "parent-id" ])
1,118✔
385
    in
386
    Term.(
387
      const handle_error
1,118✔
388
      $ (const compile_asset $ parent_id $ asset_name $ output_dir))
1,118✔
389

390
  let info ~docs =
391
    let man =
1,118✔
392
      [
393
        `S "DEPENDENCIES";
394
        `P
395
          "There are no dependency for compile assets, in particular you do \
396
           not need the asset itself at this stage.";
397
      ]
398
    in
399
    let doc = "Declare the name of an asset." in
400
    Term.info "compile-asset" ~docs ~doc ~man
401
end
402

403
module Compile_impl = struct
404
  let prefix = "impl-"
405

406
  let output_dir =
407
    let doc = "Output file directory. " in
408
    Arg.(
409
      value
410
      & opt (some string) None
1,118✔
411
      & info ~docs ~docv:"PATH" ~doc [ "output-dir" ])
1,118✔
412

413
  let output_file output_dir parent_id input =
414
    let name =
32✔
415
      Fs.File.basename input |> Fpath.set_ext "odoc" |> Fs.File.to_string
32✔
416
      |> Astring.String.Ascii.uncapitalize
32✔
417
    in
418
    let name = prefix ^ name in
32✔
419

420
    let dir = Fpath.(append output_dir parent_id) in
32✔
421
    Fs.File.create
422
      ~directory:(Fpath.to_string dir |> Fs.Directory.of_string)
32✔
423
      ~name
424

425
  let compile_impl directories output_dir parent_id source_id input
426
      warnings_options =
427
    let input = Fs.File.of_string input in
32✔
428
    let output_dir =
32✔
429
      match output_dir with Some x -> Fpath.v x | None -> Fpath.v "."
15✔
430
    in
431
    let output =
432
      output_file output_dir
433
        (match parent_id with Some x -> Fpath.v x | None -> Fpath.v ".")
1✔
434
        input
435
    in
436
    let resolver =
32✔
437
      Resolver.create ~important_digests:true ~directories ~open_modules:[]
438
        ~roots:None
439
    in
440
    Source.compile ~resolver ~source_id ~output ~warnings_options input
441

442
  let cmd =
443
    let input =
444
      let doc = "Input $(i,.cmt) file." in
445
      Arg.(required & pos 0 (some file) None & info ~doc ~docv:"FILE" [])
1,118✔
446
    in
447
    let source_id =
448
      let doc = "The id of the source file" in
449
      Arg.(
450
        value
451
        & opt (some string) None
1,118✔
452
        & info [ "source-id" ] ~doc ~docv:"/path/to/source.ml")
1,118✔
453
    in
454
    let parent_id =
455
      let doc = "The parent id of the implementation" in
456
      Arg.(
457
        value
458
        & opt (some string) None
1,118✔
459
        & info [ "parent-id" ] ~doc ~docv:"/path/to/library")
1,118✔
460
    in
461

462
    Term.(
463
      const handle_error
1,118✔
464
      $ (const compile_impl $ odoc_file_directories $ output_dir $ parent_id
1,118✔
465
       $ source_id $ input $ warnings_options))
1,118✔
466

467
  let info ~docs =
468
    let doc =
1,118✔
469
      "(EXPERIMENTAL) Compile a $(i,NAME.cmt) file to a $(i,src-NAME.odoc) \
470
       containing the implementation information needed by odoc for the \
471
       compilation unit."
472
    in
473
    Term.info "compile-impl" ~docs ~doc
474
end
475

476
module Indexing = struct
477
  open Or_error
478

479
  let output_file ~dst marshall =
480
    match (dst, marshall) with
21✔
481
    | Some file, `JSON when not (Fpath.has_ext "json" (Fpath.v file)) ->
4✔
482
        Error
1✔
483
          (`Msg
484
            "When generating a json index, the output must have a .json file \
485
             extension")
486
    | Some file, `Marshall when not (Fpath.has_ext "odoc-index" (Fpath.v file))
4✔
487
      ->
488
        Error
2✔
489
          (`Msg
490
            "When generating a binary index, the output must have a \
491
             .odoc-index file extension")
492
    | Some file, _ -> Ok (Fs.File.of_string file)
5✔
493
    | None, `JSON -> Ok (Fs.File.of_string "index.json")
2✔
494
    | None, `Marshall -> Ok (Fs.File.of_string "index.odoc-index")
11✔
495

496
  let index dst json warnings_options roots inputs_in_file inputs occurrences =
497
    let marshall = if json then `JSON else `Marshall in
6✔
498
    output_file ~dst marshall >>= fun output ->
21✔
499
    Indexing.compile marshall ~output ~warnings_options ~roots ~occurrences
18✔
500
      ~inputs_in_file ~odocls:inputs
501

502
  let cmd =
503
    let dst =
504
      let doc =
505
        "Output file path. Non-existing intermediate directories are created. \
506
         Defaults to index.odoc-index, or index.json if --json is passed (in \
507
         which case, the .odoc-index file extension is mandatory)."
508
      in
509
      Arg.(
510
        value & opt (some string) None & info ~docs ~docv:"PATH" ~doc [ "o" ])
1,118✔
511
    in
512
    let occurrences =
513
      let doc = "Occurrence file." in
514
      Arg.(
515
        value
516
        & opt (some convert_fpath) None
1,118✔
517
        & info ~docs ~docv:"PATH" ~doc [ "occurrences" ])
1,118✔
518
    in
519
    let inputs_in_file =
520
      let doc =
521
        "Input text file containing a line-separated list of paths to .odocl \
522
         files to index."
523
      in
524
      Arg.(
525
        value & opt_all convert_fpath []
1,118✔
526
        & info ~doc ~docv:"FILE" [ "file-list" ])
1,118✔
527
    in
528
    let json =
529
      let doc = "whether to output a json file, or a binary .odoc-index file" in
530
      Arg.(value & flag & info ~doc [ "json" ])
1,118✔
531
    in
532
    let inputs =
533
      let doc = ".odocl file to index" in
534
      Arg.(value & pos_all convert_fpath [] & info ~doc ~docv:"FILE" [])
1,118✔
535
    in
536
    let roots =
537
      let doc =
538
        "Specifies a directory PATH containing pages or units that should be \
539
         included in the sidebar."
540
      in
541
      Arg.(
542
        value
543
        & opt_all (convert_directory ()) []
1,118✔
544
        & info ~docs ~docv:"NAME:PATH" ~doc [ "root" ])
1,118✔
545
    in
546
    Term.(
547
      const handle_error
1,118✔
548
      $ (const index $ dst $ json $ warnings_options $ roots $ inputs_in_file
1,118✔
549
       $ inputs $ occurrences))
1,118✔
550

551
  let info ~docs =
552
    let doc =
1,118✔
553
      "Generate an index of all identified entries in the .odocl files found \
554
       in the given directories."
555
    in
556
    Term.info "compile-index" ~docs ~doc
557
end
558

559
module Sidebar = struct
560
  open Or_error
561

562
  let output_file ~dst marshall =
563
    match (dst, marshall) with
7✔
564
    | Some file, `JSON when not (Fpath.has_ext "json" (Fpath.v file)) ->
×
565
        Error
×
566
          (`Msg
567
            "When generating a sidebar with --json, the output must have a \
568
             .json file extension")
569
    | Some file, `Marshall
×
570
      when not (Fpath.has_ext "odoc-sidebar" (Fpath.v file)) ->
×
571
        Error
×
572
          (`Msg
573
            "When generating sidebar, the output must have a .odoc-sidebar \
574
             file extension")
575
    | Some file, _ -> Ok (Fs.File.of_string file)
×
576
    | None, `JSON -> Ok (Fs.File.of_string "sidebar.json")
1✔
577
    | None, `Marshall -> Ok (Fs.File.of_string "sidebar.odoc-sidebar")
6✔
578

579
  let generate dst json warnings_options input =
580
    let marshall = if json then `JSON else `Marshall in
1✔
581
    output_file ~dst marshall >>= fun output ->
7✔
582
    Sidebar.generate ~marshall ~output ~warnings_options ~index:input
7✔
583

584
  let cmd =
585
    let dst =
586
      let doc =
587
        "Output file path. Non-existing intermediate directories are created. \
588
         Defaults to sidebar.odoc-sidebar, or sidebar.json if --json is \
589
         passed."
590
      in
591
      Arg.(
592
        value & opt (some string) None & info ~docs ~docv:"PATH" ~doc [ "o" ])
1,118✔
593
    in
594
    let json =
595
      let doc = "whether to output a json file, or a binary .odoc-index file" in
596
      Arg.(value & flag & info ~doc [ "json" ])
1,118✔
597
    in
598
    let inputs =
599
      let doc = ".odoc-index file to generate a value from" in
600
      Arg.(
601
        required & pos 0 (some convert_fpath) None & info ~doc ~docv:"FILE" [])
1,118✔
602
    in
603
    Term.(
604
      const handle_error
1,118✔
605
      $ (const generate $ dst $ json $ warnings_options $ inputs))
1,118✔
606

607
  let info ~docs =
608
    let doc = "Generate a sidebar from an index file." in
1,118✔
609
    Term.info "sidebar-generate" ~docs ~doc
610
end
611

612
module Support_files_command = struct
613
  let support_files without_theme output_dir =
614
    Support_files.write ~without_theme output_dir
21✔
615

616
  let without_theme =
617
    let doc = "Don't copy the default theme to output directory." in
618
    Arg.(value & flag & info ~doc [ "without-theme" ])
1,118✔
619

620
  let cmd = Term.(const support_files $ without_theme $ dst ~create:true ())
1,118✔
621

622
  let info ~docs =
623
    let doc =
1,118✔
624
      "Copy the support files (e.g. default theme, JavaScript files) to the \
625
       output directory."
626
    in
627
    Term.info ~docs ~doc "support-files"
628
end
629

630
module Css = struct
631
  let cmd = Support_files_command.cmd
632

633
  let info ~docs =
634
    let doc =
1,118✔
635
      "DEPRECATED: Use $(i,odoc support-files) to copy the CSS file for the \
636
       default theme."
637
    in
638
    Term.info ~docs ~doc "css"
639
end
640

641
module Odoc_link : sig
642
  val cmd : unit Term.t
643

644
  val info : docs:string -> Term.info
645
end = struct
646
  let get_output_file ~output_file ~input =
647
    match output_file with
287✔
648
    | Some file -> Fs.File.of_string file
34✔
649
    | None -> Fs.File.(set_ext ".odocl" input)
253✔
650

651
  open Or_error
652

653
  (** Find the package/library name the output is part of *)
654
  let find_root_of_input l o =
655
    let l =
564✔
656
      List.map
657
        ~f:(fun (x, p) ->
658
          (x, p, p |> Fs.Directory.to_fpath |> Antichain.absolute_normalization))
84✔
659
        l
660
    in
661
    let o = Antichain.absolute_normalization o in
564✔
662
    match l with
564✔
663
    | [] -> None
485✔
664
    | _ ->
79✔
665
        Odoc_utils.List.find_map
666
          (fun (root, orig_path, norm_path) ->
667
            if Fpath.is_prefix norm_path o then Some (root, orig_path) else None)
21✔
668
          l
669

670
  let current_library_of_input lib_roots input =
671
    find_root_of_input lib_roots input
282✔
672

673
  (** Checks if the package specified with [--current-package] is consistent
674
      with the pages roots and with the output path for pages. *)
675
  let validate_current_package ?detected_package page_roots current_package =
676
    match (current_package, detected_package) with
282✔
677
    | Some curpkgnane, Some (detected_package, _)
2✔
678
      when detected_package <> curpkgnane ->
679
        Error
2✔
680
          (`Msg
681
            "The package name specified with --current-package is not \
682
             consistent with the packages passed as a -P")
683
    | _, (Some _ as r) (* we have equality or only detected package *) -> Ok r
49✔
684
    | None, None -> Ok None
226✔
685
    | Some given, None -> (
5✔
686
        try Ok (Some (given, List.assoc given page_roots))
3✔
687
        with Not_found ->
2✔
688
          Error
689
            (`Msg
690
              "The package name specified with --current-package do not match \
691
               any package passed as a -P"))
692

693
  let find_current_package ~current_package page_roots input =
694
    let detected_package = find_root_of_input page_roots input in
282✔
695
    validate_current_package ?detected_package page_roots current_package
282✔
696

697
  let link directories page_roots lib_roots input_file output_file
698
      current_package warnings_options open_modules custom_layout =
699
    let input = Fs.File.of_string input_file in
287✔
700
    let output = get_output_file ~output_file ~input in
287✔
701
    let check () =
702
      if not custom_layout then
287✔
703
        Antichain.check (page_roots |> List.map ~f:snd) ~opt:"-P" >>= fun () ->
287✔
704
        Antichain.check (lib_roots |> List.map ~f:snd) ~opt:"-L"
282✔
705
      else Ok ()
×
706
    in
707
    check () >>= fun () ->
287✔
708
    let current_lib = current_library_of_input lib_roots input in
282✔
709
    find_current_package ~current_package page_roots input
282✔
710
    >>= fun current_package ->
711
    let current_dir = Fs.File.dirname input in
278✔
712
    let roots =
278✔
713
      Some
714
        {
715
          Resolver.page_roots;
716
          lib_roots;
717
          current_lib;
718
          current_package;
719
          current_dir;
720
        }
721
    in
722

723
    let resolver =
724
      Resolver.create ~important_digests:false ~directories ~open_modules ~roots
725
    in
726
    match Odoc_link.from_odoc ~resolver ~warnings_options input output with
727
    | Error _ as e -> e
×
728
    | Ok _ -> Ok ()
278✔
729

730
  let dst =
731
    let doc =
732
      "Output file path. Non-existing intermediate directories are created. If \
733
       absent outputs a $(i,.odocl) file in the same directory as the input \
734
       file with the same basename."
735
    in
736
    Arg.(
737
      value
738
      & opt (some string) None
1,118✔
739
      & info ~docs ~docv:"PATH.odocl" ~doc [ "o" ])
1,118✔
740

741
  let page_roots =
742
    let doc =
743
      "Specifies a directory DIR containing pages that can be referenced by \
744
       {!/pkgname/pagename}. A pkgname can be specified in the -P command only \
745
       once. All the trees specified by this option and -L must be disjoint."
746
    in
747
    Arg.(
748
      value
749
      & opt_all convert_named_root []
1,118✔
750
      & info ~docs ~docv:"pkgname:DIR" ~doc [ "P" ])
1,118✔
751

752
  let lib_roots =
753
    let doc =
754
      "Specifies a library called libname containing the modules in directory \
755
       DIR. Modules can be referenced both using the flat module namespace \
756
       {!Module} and the absolute reference {!/libname/Module}. All the trees \
757
       specified by this option and -P must be disjoint."
758
    in
759
    Arg.(
760
      value
761
      & opt_all convert_named_root []
1,118✔
762
      & info ~docs ~docv:"libname:DIR" ~doc [ "L" ])
1,118✔
763

764
  let current_package =
765
    let doc =
766
      "Specify the current package name. The matching page root specified with \
767
       -P is used to resolve references using the '//' syntax. A  \
768
       corresponding -P option must be passed."
769
    in
770
    Arg.(
771
      value
772
      & opt (some string) None
1,118✔
773
      & info ~docs ~docv:"pkgname" ~doc [ "current-package" ])
1,118✔
774

775
  let custom_layout =
776
    let doc =
777
      "Signal that a custom layout is being used. This disables the checks \
778
       that the library and package paths are disjoint."
779
    in
780
    Arg.(value & flag (info ~doc [ "custom-layout" ]))
1,118✔
781

782
  let cmd =
783
    let input =
784
      let doc = "Input file" in
785
      Arg.(required & pos 0 (some file) None & info ~doc ~docv:"FILE.odoc" [])
1,118✔
786
    in
787
    Term.(
788
      const handle_error
1,118✔
789
      $ (const link $ odoc_file_directories $ page_roots $ lib_roots $ input
1,118✔
790
       $ dst $ current_package $ warnings_options $ open_modules $ custom_layout
1,118✔
791
        ))
792

793
  let info ~docs =
794
    let man =
1,118✔
795
      [
796
        `S "DEPENDENCIES";
797
        `P
798
          "Any link step depends on the result of all the compile results that \
799
           could potentially be needed to resolve forward references. A \
800
           correct approximation is to start linking only after every compile \
801
           steps are done, passing everything that's possible to $(i,-I). Link \
802
           steps don't have dependencies between them.";
803
      ]
804
    in
805
    let doc =
806
      "Second stage of compilation. Link a $(i,.odoc) into a $(i,.odocl)."
807
    in
808
    Term.info ~docs ~doc ~man "link"
809
end
810

811
module type S = sig
812
  type args
813

814
  val renderer : args Odoc_document.Renderer.t
815

816
  val extra_args : args Cmdliner.Term.t
817
end
818

819
module Make_renderer (R : S) : sig
820
  val process : docs:string -> unit Term.t * Term.info
821

822
  val targets : docs:string -> unit Term.t * Term.info
823

824
  val targets_source : docs:string -> unit Term.t * Term.info
825

826
  val generate : docs:string -> unit Term.t * Term.info
827

828
  val generate_source : docs:string -> unit Term.t * Term.info
829

830
  val generate_asset : docs:string -> unit Term.t * Term.info
831
end = struct
832
  let input_odoc =
833
    let doc = "Input file." in
834
    Arg.(required & pos 0 (some file) None & info ~doc ~docv:"FILE.odoc" [])
3,354✔
835

836
  let input_odocl =
837
    let doc = "Input file." in
838
    Arg.(required & pos 0 (some file) None & info ~doc ~docv:"FILE.odocl" [])
3,354✔
839

840
  module Process = struct
841
    let process extra _hidden directories output_dir syntax input_file
842
        warnings_options =
843
      let resolver =
2✔
844
        Resolver.create ~important_digests:false ~directories ~open_modules:[]
845
          ~roots:None
846
      in
847
      let file = Fs.File.of_string input_file in
848
      Rendering.render_odoc ~renderer:R.renderer ~resolver ~warnings_options
2✔
849
        ~syntax ~output:output_dir extra file
850

851
    let cmd =
852
      let syntax =
853
        let doc = "Available options: ml | re" in
854
        let env = Arg.env_var "ODOC_SYNTAX" in
855
        Arg.(
3,354✔
856
          value
857
          & opt (pconv convert_syntax) Odoc_document.Renderer.OCaml
3,354✔
858
            @@ info ~docv:"SYNTAX" ~doc ~env [ "syntax" ])
3,354✔
859
      in
860
      Term.(
861
        const handle_error
3,354✔
862
        $ (const process $ R.extra_args $ hidden $ odoc_file_directories
3,354✔
863
         $ dst ~create:true () $ syntax $ input_odoc $ warnings_options))
3,354✔
864

865
    let info ~docs =
866
      let doc =
3,354✔
867
        Format.sprintf
868
          "Render %s files from a $(i,.odoc). $(i,link) then $(i,%s-generate) \
869
           should be used instead."
870
          R.renderer.name R.renderer.name
871
      in
872
      Term.info ~docs ~doc R.renderer.name
3,354✔
873
  end
874

875
  let process ~docs = Process.(cmd, info ~docs)
3,354✔
876

877
  module Generate = struct
878
    let generate extra _hidden output_dir syntax extra_suffix input_file
879
        warnings_options sidebar =
880
      let file = Fs.File.of_string input_file in
243✔
881
      Rendering.generate_odoc ~renderer:R.renderer ~warnings_options ~syntax
243✔
882
        ~output:output_dir ~extra_suffix ~sidebar extra file
883

884
    let sidebar =
885
      let doc = "A .odoc-index file, used eg to generate the sidebar." in
886
      Arg.(
887
        value
888
        & opt (some convert_fpath) None
3,354✔
889
        & info [ "sidebar" ] ~doc ~docv:"FILE.odoc-sidebar")
3,354✔
890

891
    let cmd =
892
      let syntax =
893
        let doc = "Available options: ml | re" in
894
        let env = Arg.env_var "ODOC_SYNTAX" in
895
        Arg.(
3,354✔
896
          value
897
          & opt (pconv convert_syntax) Odoc_document.Renderer.OCaml
3,354✔
898
            @@ info ~docv:"SYNTAX" ~doc ~env [ "syntax" ])
3,354✔
899
      in
900
      Term.(
901
        const handle_error
3,354✔
902
        $ (const generate $ R.extra_args $ hidden $ dst ~create:true () $ syntax
3,354✔
903
         $ extra_suffix $ input_odocl $ warnings_options $ sidebar))
3,354✔
904

905
    let info ~docs =
906
      let doc =
3,354✔
907
        Format.sprintf "Generate %s files from a $(i,.odocl)." R.renderer.name
908
      in
909
      Term.info ~docs ~doc (R.renderer.name ^ "-generate")
3,354✔
910
  end
911

912
  let generate ~docs = Generate.(cmd, info ~docs)
3,354✔
913

914
  module Generate_source = struct
915
    let generate extra output_dir syntax extra_suffix input_file
916
        warnings_options source_file sidebar =
917
      Rendering.generate_source_odoc ~renderer:R.renderer ~warnings_options
25✔
918
        ~syntax ~output:output_dir ~extra_suffix ~source_file ~sidebar extra
919
        input_file
920

921
    let input_odocl =
922
      let doc = "Linked implementation file." in
923
      Arg.(
924
        required
925
        & opt (some convert_fpath) None
3,354✔
926
        & info [ "impl" ] ~doc ~docv:"impl-FILE.odocl")
3,354✔
927

928
    let source_file =
929
      let doc = "Source code for the implementation unit." in
930
      Arg.(
931
        required
932
        & pos 0 (some convert_fpath) None
3,354✔
933
        & info ~doc ~docv:"FILE.ml" [])
3,354✔
934

935
    let cmd =
936
      let syntax =
937
        let doc = "Available options: ml | re" in
938
        let env = Arg.env_var "ODOC_SYNTAX" in
939
        Arg.(
3,354✔
940
          value
941
          & opt (pconv convert_syntax) Odoc_document.Renderer.OCaml
3,354✔
942
            @@ info ~docv:"SYNTAX" ~doc ~env [ "syntax" ])
3,354✔
943
      in
944
      let sidebar = Generate.sidebar in
945
      Term.(
946
        const handle_error
3,354✔
947
        $ (const generate $ R.extra_args $ dst ~create:true () $ syntax
3,354✔
948
         $ extra_suffix $ input_odocl $ warnings_options $ source_file $ sidebar
3,354✔
949
          ))
950

951
    let info ~docs =
952
      let doc =
1,118✔
953
        Format.sprintf "Generate %s files from a $(i,impl-*.odocl)."
954
          R.renderer.name
955
      in
956
      Term.info ~docs ~doc (R.renderer.name ^ "-generate-source")
1,118✔
957
  end
958

959
  let generate_source ~docs = Generate_source.(cmd, info ~docs)
1,118✔
960

961
  module Generate_asset = struct
962
    let generate extra output_dir extra_suffix input_file warnings_options
963
        asset_file =
964
      Rendering.generate_asset_odoc ~renderer:R.renderer ~warnings_options
1✔
965
        ~output:output_dir ~extra_suffix ~asset_file extra input_file
966

967
    let input_odocl =
968
      let doc = "Odoc asset unit." in
969
      Arg.(
970
        required
971
        & opt (some convert_fpath) None
3,354✔
972
        & info [ "asset-unit" ] ~doc ~docv:"asset-FILE.odocl")
3,354✔
973

974
    let asset_file =
975
      let doc = "The asset file" in
976
      Arg.(
977
        required
978
        & pos 0 (some convert_fpath) None
3,354✔
979
        & info ~doc ~docv:"FILE.ext" [])
3,354✔
980

981
    let cmd =
982
      Term.(
983
        const handle_error
3,354✔
984
        $ (const generate $ R.extra_args $ dst ~create:true () $ extra_suffix
3,354✔
985
         $ input_odocl $ warnings_options $ asset_file))
3,354✔
986

987
    let info ~docs =
988
      let doc =
1,118✔
989
        Format.sprintf "Generate %s files from a $(i,impl-*.odocl)."
990
          R.renderer.name
991
      in
992
      Term.info ~docs ~doc (R.renderer.name ^ "-generate-asset")
1,118✔
993
  end
994

995
  let generate_asset ~docs = Generate_asset.(cmd, info ~docs)
1,118✔
996

997
  module Targets = struct
998
    let list_targets output_dir directories extra odoc_file =
999
      let odoc_file = Fs.File.of_string odoc_file in
109✔
1000
      let resolver =
109✔
1001
        Resolver.create ~important_digests:false ~directories ~open_modules:[]
1002
          ~roots:None
1003
      in
1004
      let warnings_options =
1005
        {
1006
          Odoc_model.Error.warn_error = false;
1007
          print_warnings = false;
1008
          suppress_warnings = false;
1009
        }
1010
      in
1011
      Rendering.targets_odoc ~resolver ~warnings_options ~syntax:OCaml
1012
        ~renderer:R.renderer ~output:output_dir ~extra odoc_file
1013

1014
    let back_compat =
1015
      let doc =
1016
        "For backwards compatibility when processing $(i,.odoc) rather than \
1017
         $(i,.odocl) files."
1018
      in
1019
      Arg.(
1020
        value
1021
        & opt_all (convert_directory ()) []
3,354✔
1022
        & info ~docs ~docv:"DIR" ~doc [ "I" ])
3,354✔
1023

1024
    let cmd =
1025
      Term.(
1026
        const handle_error
3,354✔
1027
        $ (const list_targets $ dst () $ back_compat $ R.extra_args
3,354✔
1028
         $ input_odocl))
3,354✔
1029

1030
    let info ~docs =
1031
      let doc =
3,354✔
1032
        Format.sprintf
1033
          "Print the files that would be generated by $(i,%s-generate)."
1034
          R.renderer.name
1035
      in
1036
      Term.info (R.renderer.name ^ "-targets") ~docs ~doc
3,354✔
1037
  end
1038

1039
  let targets ~docs = Targets.(cmd, info ~docs)
3,354✔
1040

1041
  module Targets_source = struct
1042
    let list_targets output_dir source_file extra odoc_file =
1043
      let warnings_options =
4✔
1044
        {
1045
          Odoc_model.Error.warn_error = false;
1046
          print_warnings = false;
1047
          suppress_warnings = false;
1048
        }
1049
      in
1050
      Rendering.targets_source_odoc ~warnings_options ~syntax:OCaml
1051
        ~renderer:R.renderer ~output:output_dir ~extra ~source_file odoc_file
1052

1053
    let source_file = Generate_source.source_file
1054
    let input_odocl = Generate_source.input_odocl
1055

1056
    let cmd =
1057
      Term.(
1058
        const handle_error
3,354✔
1059
        $ (const list_targets $ dst () $ source_file $ R.extra_args
3,354✔
1060
         $ input_odocl))
3,354✔
1061

1062
    let info ~docs =
1063
      let doc =
1,118✔
1064
        Format.sprintf
1065
          "Print the files that would be generated by $(i,%s-generate-source)."
1066
          R.renderer.name
1067
      in
1068
      Term.info (R.renderer.name ^ "-targets-source") ~docs ~doc
1,118✔
1069
  end
1070

1071
  let targets_source ~docs = Targets_source.(cmd, info ~docs)
1,118✔
1072
end
1073

1074
module Odoc_latex_url : sig
1075
  val cmd : unit Term.t
1076

1077
  val info : docs:string -> Term.info
1078
end = struct
1079
  let reference =
1080
    let doc = "The reference to be resolved and whose url to be generated." in
1081
    Arg.(required & pos 0 (some string) None & info ~doc ~docv:"REF" [])
1,118✔
1082

1083
  let reference_to_url = Url.reference_to_url_latex
1084

1085
  let cmd =
1086
    Term.(
1087
      const handle_error
1,118✔
1088
      $ (const reference_to_url $ odoc_file_directories $ reference))
1,118✔
1089

1090
  let info ~docs =
1091
    Term.info ~docs ~doc:"Resolve a reference and output its corresponding url."
1,118✔
1092
      "latex-url"
1093
end
1094

1095
module Odoc_html_args = struct
1096
  include Html_page
1097

1098
  let semantic_uris =
1099
    let doc = "Generate pretty (semantic) links." in
1100
    Arg.(value & flag (info ~doc [ "semantic-uris"; "pretty-uris" ]))
1,118✔
1101

1102
  let closed_details =
1103
    let doc =
1104
      "If this flag is passed <details> tags (used for includes) will be \
1105
       closed by default."
1106
    in
1107
    Arg.(value & flag (info ~doc [ "closed-details" ]))
1,118✔
1108

1109
  let indent =
1110
    let doc = "Format the output HTML files with indentation." in
1111
    Arg.(value & flag (info ~doc [ "indent" ]))
1,118✔
1112

1113
  module Uri = struct
1114
    (* Very basic validation and normalization for URI paths. *)
1115

1116
    open Odoc_html.Types
1117

1118
    let is_absolute str =
1119
      List.exists [ "http"; "https"; "file"; "data"; "ftp" ] ~f:(fun scheme ->
19✔
1120
          Astring.String.is_prefix ~affix:(scheme ^ ":") str)
1✔
1121
      || str.[0] = '/'
1✔
1122

1123
    let conv_rel_dir rel =
1124
      let l = Astring.String.cuts ~sep:"/" rel in
5✔
1125
      List.fold_left
5✔
1126
        ~f:(fun acc seg ->
1127
          Some Odoc_document.Url.Path.{ kind = `Page; parent = acc; name = seg })
5✔
1128
        l ~init:None
1129

1130
    let convert_dir : uri Arg.conv =
1131
      let parser str =
1132
        if String.length str = 0 then `Error "invalid URI"
×
1133
        else
1134
          (* The URI is absolute if it starts with a scheme or with '/'. *)
1135
          let last_char = str.[String.length str - 1] in
2✔
1136
          let str =
2✔
1137
            if last_char <> '/' then str
2✔
1138
            else String.sub str ~pos:0 ~len:(String.length str - 1)
×
1139
          in
1140
          `Ok
1141
            (if is_absolute str then (Absolute str : uri)
×
1142
             else
1143
               Relative
2✔
1144
                 (let u = conv_rel_dir str in
1145
                  match u with
2✔
1146
                  | None -> None
×
1147
                  | Some u -> Some { u with kind = `Page }))
2✔
1148
      in
1149
      let printer ppf = function
1150
        | (Absolute uri : uri) -> Format.pp_print_string ppf uri
×
1151
        | Relative _uri -> Format.pp_print_string ppf ""
×
1152
      in
1153
      (parser, printer)
1154

1155
    let convert_file_uri : Odoc_html.Types.file_uri Arg.conv =
1156
      let parser str =
1157
        if String.length str = 0 then `Error "invalid URI"
×
1158
        else
1159
          let conv_rel_file rel =
17✔
1160
            match Astring.String.cut ~rev:true ~sep:"/" rel with
15✔
1161
            | Some (before, after) ->
3✔
1162
                let base = conv_rel_dir before in
1163
                Odoc_document.Url.Path.
3✔
1164
                  { kind = `File; parent = base; name = after }
1165
            | None ->
12✔
1166
                Odoc_document.Url.Path.
1167
                  { kind = `File; parent = None; name = rel }
1168
          in
1169
          `Ok
1170
            (if is_absolute str then (Absolute str : file_uri)
2✔
1171
             else Relative (conv_rel_file str))
15✔
1172
      in
1173
      let printer ppf = function
1174
        | Odoc_html.Types.Absolute uri -> Format.pp_print_string ppf uri
×
1175
        | Odoc_html.Types.Relative _uri -> Format.pp_print_string ppf ""
×
1176
      in
1177
      (parser, printer)
1178
  end
1179

1180
  let home_breadcrumb =
1181
    let doc =
1182
      "Wether to add a 'Home' breadcrumb to go up the root of the given \
1183
       sidebar."
1184
    in
1185
    Arg.(value & flag & info ~docv:"escape" ~doc [ "home-breadcrumb" ])
1,118✔
1186

1187
  let theme_uri =
1188
    let doc =
1189
      "Where to look for theme files (e.g. `URI/odoc.css'). Relative URIs are \
1190
       resolved using `--output-dir' as a target."
1191
    in
1192
    let default : Odoc_html.Types.uri = Odoc_html.Types.Relative None in
1193
    Arg.(
1194
      value
1195
      & opt Uri.convert_dir default
1,118✔
1196
      & info ~docv:"URI" ~doc [ "theme-uri" ])
1,118✔
1197

1198
  let support_uri =
1199
    let doc =
1200
      "Where to look for support files (e.g. `URI/highlite.pack.js'). Relative \
1201
       URIs are resolved using `--output-dir' as a target."
1202
    in
1203
    let default : Odoc_html.Types.uri = Odoc_html.Types.Relative None in
1204
    Arg.(
1205
      value
1206
      & opt Uri.convert_dir default
1,118✔
1207
      & info ~docv:"URI" ~doc [ "support-uri" ])
1,118✔
1208

1209
  let search_uri =
1210
    let doc =
1211
      "Where to look for search scripts. Relative URIs are resolved using \
1212
       `--output-dir' as a target."
1213
    in
1214
    Arg.(
1215
      value
1216
      & opt_all Uri.convert_file_uri []
1,118✔
1217
      & info ~docv:"URI" ~doc [ "search-uri" ])
1,118✔
1218

1219
  let flat =
1220
    let doc =
1221
      "Output HTML files in 'flat' mode, where the hierarchy of modules / \
1222
       module types / classes and class types are reflected in the filenames \
1223
       rather than in the directory structure."
1224
    in
1225
    Arg.(value & flag & info ~docs ~doc [ "flat" ])
1,118✔
1226

1227
  let as_json =
1228
    let doc =
1229
      "EXPERIMENTAL: Output HTML files in 'embeddable json' mode, where HTML \
1230
       fragments (preamble, content) together with metadata (uses_katex, \
1231
       breadcrumbs, table of contents) are emitted in JSON format. The \
1232
       structure of the output should be considered unstable and no guarantees \
1233
       are made about backward compatibility."
1234
    in
1235
    Arg.(value & flag & info ~doc [ "as-json" ])
1,118✔
1236

1237
  let remap =
1238
    let convert_remap =
1239
      let parse inp =
1240
        match Astring.String.cut ~sep:":" inp with
6✔
1241
        | Some (orig, mapped) -> Result.Ok (orig, mapped)
6✔
1242
        | _ -> Error (`Msg "Map must be of the form '<orig>:https://...'")
×
1243
      and print fmt (orig, mapped) = Format.fprintf fmt "%s:%s" orig mapped in
×
1244
      Arg.conv (parse, print)
1,118✔
1245
    in
1246
    let doc = "Remap an identifier to an external URL." in
1247
    Arg.(value & opt_all convert_remap [] & info [ "R" ] ~doc)
1,118✔
1248

1249
  let remap_file =
1250
    let doc = "File containing remap rules." in
1251
    Arg.(value & opt (some file) None & info ~docv:"FILE" ~doc [ "remap-file" ])
1,118✔
1252

1253
  let extra_args =
1254
    let config semantic_uris closed_details indent theme_uri support_uri
1255
        search_uris flat as_json remap remap_file home_breadcrumb =
1256
      let open_details = not closed_details in
242✔
1257
      let remap =
1258
        match remap_file with
1259
        | None -> remap
241✔
1260
        | Some f ->
1✔
1261
            let ic = open_in f in
1262
            let rec loop acc =
1✔
1263
              match input_line ic with
4✔
1264
              | exception _ ->
1✔
1265
                  close_in ic;
1266
                  acc
1✔
1267
              | line -> (
3✔
1268
                  match Astring.String.cut ~sep:":" line with
1269
                  | Some (orig, mapped) -> loop ((orig, mapped) :: acc)
2✔
1270
                  | None -> loop acc)
1✔
1271
            in
1272
            loop []
1✔
1273
      in
1274
      let html_config =
1275
        Odoc_html.Config.v ~theme_uri ~support_uri ~search_uris ~semantic_uris
1276
          ~indent ~flat ~open_details ~as_json ~remap ~home_breadcrumb ()
1277
      in
1278
      { Html_page.html_config }
242✔
1279
    in
1280
    Term.(
1281
      const config $ semantic_uris $ closed_details $ indent $ theme_uri
1,118✔
1282
      $ support_uri $ search_uri $ flat $ as_json $ remap $ remap_file
1,118✔
1283
      $ home_breadcrumb)
1,118✔
1284
end
1285

1286
module Odoc_html = Make_renderer (Odoc_html_args)
1287

1288
module Odoc_html_url : sig
1289
  val cmd : unit Term.t
1290

1291
  val info : docs:string -> Term.info
1292
end = struct
1293
  let root_url =
1294
    let doc =
1295
      "A string to prepend to the generated relative url. A separating / is \
1296
       added if needed."
1297
    in
1298
    Arg.(value & opt (some string) None & info [ "r"; "root-url" ] ~doc)
1,118✔
1299

1300
  let reference =
1301
    let doc = "The reference to be resolved and whose url to be generated." in
1302
    Arg.(required & pos 0 (some string) None & info ~doc ~docv:"REF" [])
1,118✔
1303

1304
  let reference_to_url = Url.reference_to_url_html
1305

1306
  let cmd =
1307
    Term.(
1308
      const handle_error
1,118✔
1309
      $ (const reference_to_url $ Odoc_html_args.extra_args $ root_url
1,118✔
1310
       $ odoc_file_directories $ reference))
1,118✔
1311

1312
  let info ~docs =
1313
    Term.info ~docs ~doc:"Resolve a reference and output its corresponding url."
1,118✔
1314
      "html-url"
1315
end
1316

1317
module Html_fragment : sig
1318
  val cmd : unit Term.t
1319

1320
  val info : docs:string -> Term.info
1321
end = struct
1322
  let html_fragment directories xref_base_uri output_file input_file
1323
      warnings_options =
1324
    let resolver =
×
1325
      Resolver.create ~important_digests:false ~directories ~open_modules:[]
1326
        ~roots:None
1327
    in
1328
    let input_file = Fs.File.of_string input_file in
1329
    let output_file = Fs.File.of_string output_file in
×
1330
    let xref_base_uri =
×
1331
      if xref_base_uri = "" then xref_base_uri
×
1332
      else
1333
        let last_char = xref_base_uri.[String.length xref_base_uri - 1] in
×
1334
        if last_char <> '/' then xref_base_uri ^ "/" else xref_base_uri
×
1335
    in
1336
    Html_fragment.from_mld ~resolver ~xref_base_uri ~output:output_file
1337
      ~warnings_options input_file
1338

1339
  let cmd =
1340
    let output =
1341
      let doc = "Output HTML fragment file." in
1342
      Arg.(
1343
        value & opt string "/dev/stdout"
1,118✔
1344
        & info ~docs ~docv:"file.html" ~doc [ "o"; "output-file" ])
1,118✔
1345
    in
1346
    let input =
1347
      let doc = "Input documentation page file." in
1348
      Arg.(required & pos 0 (some file) None & info ~doc ~docv:"file.mld" [])
1,118✔
1349
    in
1350
    let xref_base_uri =
1351
      let doc =
1352
        "Base URI used to resolve cross-references. Set this to the root of \
1353
         the global docset during local development. By default `.' is used."
1354
      in
1355
      Arg.(value & opt string "" & info ~docv:"URI" ~doc [ "xref-base-uri" ])
1,118✔
1356
    in
1357
    Term.(
1358
      const handle_error
1,118✔
1359
      $ (const html_fragment $ odoc_file_directories $ xref_base_uri $ output
1,118✔
1360
       $ input $ warnings_options))
1,118✔
1361

1362
  let info ~docs =
1363
    Term.info ~docs ~doc:"Generates an html fragment file from an mld one."
1,118✔
1364
      "html-fragment"
1365
end
1366

1367
module Odoc_manpage = Make_renderer (struct
1368
  type args = unit
1369

1370
  let renderer = Man_page.renderer
1371

1372
  let extra_args = Term.const ()
1,118✔
1373
end)
1374

1375
module Odoc_latex = Make_renderer (struct
1376
  type args = Latex.args
1377

1378
  let renderer = Latex.renderer
1379

1380
  let with_children =
1381
    let doc = "Include children at the end of the page." in
1382
    Arg.(value & opt bool true & info ~docv:"BOOL" ~doc [ "with-children" ])
1,118✔
1383

1384
  let extra_args =
1385
    let f with_children = { Latex.with_children } in
75✔
1386
    Term.(const f $ with_children)
1,118✔
1387
end)
1388

1389
module Depends = struct
1390
  module Compile = struct
1391
    let list_dependencies input_files =
1392
      let deps =
5✔
1393
        Depends.for_compile_step (List.map ~f:Fs.File.of_string input_files)
5✔
1394
      in
1395
      List.iter
5✔
1396
        ~f:(fun t ->
1397
          Printf.printf "%s %s\n" (Depends.Compile.name t)
19✔
1398
            (Digest.to_hex @@ Depends.Compile.digest t))
19✔
1399
        deps;
1400
      flush stdout
5✔
1401

1402
    let cmd =
1403
      let input =
1404
        let doc = "Input files" in
1405
        Arg.(non_empty & pos_all file [] & info ~doc ~docv:"file.cm{i,t,ti}" [])
1,118✔
1406
      in
1407
      Term.(const list_dependencies $ input)
1,118✔
1408

1409
    let info ~docs =
1410
      Term.info "compile-deps" ~docs
1,118✔
1411
        ~doc:
1412
          "List units (with their digest) which needs to be compiled in order \
1413
           to compile this one. The unit itself and its digest is also \
1414
           reported in the output.\n\
1415
           Dependencies between compile steps are the same as when compiling \
1416
           the ocaml modules."
1417
  end
1418

1419
  module Link = struct
1420
    let rec fmt_page pp page =
1421
      match page.Odoc_model.Paths.Identifier.iv with
2✔
1422
      | `Page (parent_opt, name) ->
2✔
1423
          Format.fprintf pp "%a%a" fmt_parent_opt parent_opt
1424
            Odoc_model.Names.PageName.fmt name
1425
      | `LeafPage (parent_opt, name) ->
×
1426
          Format.fprintf pp "%a%a" fmt_parent_opt parent_opt
1427
            Odoc_model.Names.PageName.fmt name
1428

1429
    and fmt_parent_opt pp parent_opt =
1430
      match parent_opt with
2✔
1431
      | None -> ()
2✔
1432
      | Some p -> Format.fprintf pp "%a/" fmt_page p
×
1433

1434
    let list_dependencies input_file =
1435
      let open Or_error in
1✔
1436
      Depends.for_rendering_step (Fs.Directory.of_string input_file)
1✔
1437
      >>= fun depends ->
1438
      List.iter depends ~f:(fun (root : Odoc_model.Root.t) ->
1✔
1439
          match root.id.iv with
2✔
1440
          | `Root (Some p, _) ->
2✔
1441
              Format.printf "%a %s %s\n" fmt_page p
1442
                (Odoc_model.Root.Odoc_file.name root.file)
2✔
1443
                (Digest.to_hex root.digest)
2✔
1444
          | _ ->
×
1445
              Format.printf "none %s %s\n"
1446
                (Odoc_model.Root.Odoc_file.name root.file)
×
1447
                (Digest.to_hex root.digest));
×
1448
      Ok ()
1✔
1449

1450
    let cmd =
1451
      let input =
1452
        let doc = "Input directory" in
1453
        Arg.(required & pos 0 (some file) None & info ~doc ~docv:"PKG_DIR" [])
1,118✔
1454
      in
1455
      Term.(const handle_error $ (const list_dependencies $ input))
1,118✔
1456

1457
    let info ~docs =
1458
      Term.info "link-deps" ~docs
1,118✔
1459
        ~doc:
1460
          "Lists a subset of the packages and modules which need to be in \
1461
           odoc's load path to link the $(i, odoc) files in the given \
1462
           directory. Additional packages may be required to resolve all \
1463
           references."
1464
  end
1465

1466
  module Odoc_html = struct
1467
    let includes =
1468
      let doc = "For backwards compatibility. Ignored." in
1469
      Arg.(
1470
        value
1471
        & opt_all (convert_directory ()) []
1,118✔
1472
        & info ~docs ~docv:"DIR" ~doc [ "I" ])
1,118✔
1473

1474
    let cmd =
1475
      let input =
1476
        let doc = "Input directory" in
1477
        Arg.(required & pos 0 (some file) None & info ~doc ~docv:"PKG_DIR" [])
1,118✔
1478
      in
1479
      let cmd _ = Link.list_dependencies in
×
1480
      Term.(const handle_error $ (const cmd $ includes $ input))
1,118✔
1481

1482
    let info ~docs =
1483
      Term.info "html-deps" ~docs ~doc:"DEPRECATED: alias for link-deps"
1,118✔
1484
  end
1485
end
1486

1487
module Targets = struct
1488
  module Compile = struct
1489
    let list_targets dst input =
1490
      let input = Fs.File.of_string input in
×
1491
      let output = Compile.output_file ~dst ~input in
×
1492
      Printf.printf "%s\n" (Fs.File.to_string output);
×
1493
      flush stdout
×
1494

1495
    let cmd = Term.(const list_targets $ Compile.dst $ Compile.input)
1,118✔
1496

1497
    let info ~docs =
1498
      Term.info "compile-targets" ~docs
1,118✔
1499
        ~doc:
1500
          "Print the name of the file produced by $(i,compile). If $(i,-o) is \
1501
           passed, the same path is printed but error checking is performed."
1502
  end
1503

1504
  module Support_files = struct
1505
    let list_targets without_theme output_directory =
1506
      Support_files.print_filenames ~without_theme output_directory
×
1507

1508
    let cmd =
1509
      Term.(const list_targets $ Support_files_command.without_theme $ dst ())
1,118✔
1510

1511
    let info ~docs =
1512
      Term.info "support-files-targets" ~docs
1,118✔
1513
        ~doc:
1514
          "Lists the names of the files that $(i,odoc support-files) outputs."
1515
  end
1516
end
1517

1518
module Occurrences = struct
1519
  open Or_error
1520

1521
  let dst_of_string s =
1522
    let f = Fs.File.of_string s in
9✔
1523
    if not (Fs.File.has_ext ".odoc-occurrences" f) then
9✔
1524
      Error (`Msg "Output file must have '.odoc-occurrences' extension.")
×
1525
    else Ok f
9✔
1526

1527
  module Count = struct
1528
    let count directories dst warnings_options include_hidden =
1529
      dst_of_string dst >>= fun dst ->
8✔
1530
      Occurrences.count ~dst ~warnings_options directories include_hidden
8✔
1531

1532
    let cmd =
1533
      let dst =
1534
        let doc = "Output file path." in
1535
        Arg.(
1536
          required
1537
          & opt (some string) None
1,118✔
1538
          & info ~docs ~docv:"PATH" ~doc [ "o" ])
1,118✔
1539
      in
1540
      let include_hidden =
1541
        let doc = "Include hidden identifiers in the table" in
1542
        Arg.(value & flag & info ~docs ~doc [ "include-hidden" ])
1,118✔
1543
      in
1544
      let input =
1545
        let doc =
1546
          "Directories to recursively traverse, agregating occurrences from \
1547
           $(i,impl-*.odocl) files. Can be present several times."
1548
        in
1549
        Arg.(
1550
          value
1551
          & pos_all (convert_directory ()) []
1,118✔
1552
          & info ~docs ~docv:"DIR" ~doc [])
1,118✔
1553
      in
1554
      Term.(
1555
        const handle_error
1,118✔
1556
        $ (const count $ input $ dst $ warnings_options $ include_hidden))
1,118✔
1557

1558
    let info ~docs =
1559
      let doc =
1,118✔
1560
        "Generate a hashtable mapping identifiers to number of occurrences, as \
1561
         computed from the implementations of .odocl files found in the given \
1562
         directories."
1563
      in
1564
      Term.info "count-occurrences" ~docs ~doc
1565
  end
1566
  module Aggregate = struct
1567
    let index dst files file_list warnings_options =
1568
      match (files, file_list) with
1✔
1569
      | [], [] ->
×
1570
          Error
1571
            (`Msg
1572
              "At least one of --file-list or a path to a file must be passed \
1573
               to odoc aggregate-occurrences")
1574
      | _ ->
1✔
1575
          dst_of_string dst >>= fun dst ->
1✔
1576
          Occurrences.aggregate ~dst ~warnings_options files file_list
1✔
1577

1578
    let cmd =
1579
      let dst =
1580
        let doc = "Output file path." in
1581
        Arg.(
1582
          required
1583
          & opt (some string) None
1,118✔
1584
          & info ~docs ~docv:"PATH" ~doc [ "o" ])
1,118✔
1585
      in
1586
      let inputs_in_file =
1587
        let doc =
1588
          "Input text file containing a line-separated list of paths to files \
1589
           created with count-occurrences."
1590
        in
1591
        Arg.(
1592
          value & opt_all convert_fpath []
1,118✔
1593
          & info ~doc ~docv:"FILE" [ "file-list" ])
1,118✔
1594
      in
1595
      let inputs =
1596
        let doc = "file created with count-occurrences" in
1597
        Arg.(value & pos_all convert_fpath [] & info ~doc ~docv:"FILE" [])
1,118✔
1598
      in
1599
      Term.(
1600
        const handle_error
1,118✔
1601
        $ (const index $ dst $ inputs $ inputs_in_file $ warnings_options))
1,118✔
1602

1603
    let info ~docs =
1604
      let doc = "Aggregate hashtables created with odoc count-occurrences." in
1,118✔
1605
      Term.info "aggregate-occurrences" ~docs ~doc
1606
  end
1607
end
1608

1609
module Odoc_error = struct
1610
  let errors input =
1611
    let open Odoc_odoc in
2✔
1612
    let open Or_error in
1613
    let input = Fs.File.of_string input in
1614
    Odoc_file.load input >>= fun unit ->
2✔
1615
    Odoc_model.Error.print_errors unit.warnings;
2✔
1616
    Ok ()
2✔
1617

1618
  let input =
1619
    let doc = "Input $(i,.odoc) or $(i,.odocl) file" in
1620
    Arg.(required & pos 0 (some file) None & info ~doc ~docv:"FILE" [])
1,118✔
1621

1622
  let cmd = Term.(const handle_error $ (const errors $ input))
1,118✔
1623

1624
  let info ~docs =
1625
    Term.info "errors" ~docs
1,118✔
1626
      ~doc:"Print errors that occurred while compiling or linking."
1627
end
1628

1629
module Classify = struct
1630
  let libdirs =
1631
    let doc = "The directories containing the libraries" in
1632
    Arg.(value & pos_all string [] & info ~doc ~docv:"DIR" [])
1,118✔
1633

1634
  let cmd = Term.(const handle_error $ (const Classify.classify $ libdirs))
1,118✔
1635

1636
  let info ~docs =
1637
    Term.info "classify" ~docs
1,118✔
1638
      ~doc:
1639
        "Classify the modules into libraries based on heuristics. Libraries \
1640
         are specified by the --library option."
1641
end
1642

1643
let section_pipeline = "COMMANDS: Compilation pipeline"
1644
let section_generators = "COMMANDS: Alternative generators"
1645
let section_support = "COMMANDS: Scripting"
1646
let section_legacy = "COMMANDS: Legacy pipeline"
1647
let section_deprecated = "COMMANDS: Deprecated"
1648

1649
(** Sections in the order they should appear. *)
1650
let main_page_sections =
1651
  [
1652
    section_pipeline;
1653
    section_generators;
1654
    section_support;
1655
    section_legacy;
1656
    section_deprecated;
1657
  ]
1658

1659
let () =
1660
  Printexc.record_backtrace true;
1661
  let subcommands =
1,118✔
1662
    [
1663
      Occurrences.Count.(cmd, info ~docs:section_pipeline);
1664
      Occurrences.Aggregate.(cmd, info ~docs:section_pipeline);
1665
      Compile.(cmd, info ~docs:section_pipeline);
1666
      Compile_asset.(cmd, info ~docs:section_pipeline);
1667
      Odoc_link.(cmd, info ~docs:section_pipeline);
1668
      Odoc_html.generate ~docs:section_pipeline;
1669
      Odoc_html.generate_source ~docs:section_pipeline;
1670
      Odoc_html.generate_asset ~docs:section_pipeline;
1671
      Support_files_command.(cmd, info ~docs:section_pipeline);
1672
      Compile_impl.(cmd, info ~docs:section_pipeline);
1673
      Indexing.(cmd, info ~docs:section_pipeline);
1674
      Sidebar.(cmd, info ~docs:section_pipeline);
1675
      Odoc_manpage.generate ~docs:section_generators;
1676
      Odoc_latex.generate ~docs:section_generators;
1677
      Odoc_html_url.(cmd, info ~docs:section_support);
1678
      Odoc_latex_url.(cmd, info ~docs:section_support);
1679
      Targets.Support_files.(cmd, info ~docs:section_support);
1680
      Odoc_error.(cmd, info ~docs:section_support);
1681
      Odoc_html.targets ~docs:section_support;
1682
      Odoc_html.targets_source ~docs:section_support;
1683
      Odoc_manpage.targets ~docs:section_support;
1684
      Odoc_latex.targets ~docs:section_support;
1685
      Depends.Compile.(cmd, info ~docs:section_support);
1686
      Targets.Compile.(cmd, info ~docs:section_support);
1687
      Html_fragment.(cmd, info ~docs:section_legacy);
1688
      Odoc_html.process ~docs:section_legacy;
1689
      Odoc_manpage.process ~docs:section_legacy;
1690
      Odoc_latex.process ~docs:section_legacy;
1691
      Depends.Link.(cmd, info ~docs:section_legacy);
1692
      Css.(cmd, info ~docs:section_deprecated);
1693
      Depends.Odoc_html.(cmd, info ~docs:section_deprecated);
1694
      Classify.(cmd, info ~docs:section_pipeline);
1695
    ]
1696
  in
1697
  let default =
1698
    let print_default () =
1699
      let available_subcommands =
×
1700
        List.map subcommands ~f:(fun (_, info) -> Term.name info)
×
1701
      in
1702
      Printf.printf
×
1703
        "Available subcommands: %s\nSee --help for more information.\n%!"
1704
        (String.concat ~sep:", " available_subcommands)
×
1705
    in
1706
    let man =
1707
      (* Show sections in a defined order. *)
1708
      List.map ~f:(fun s -> `S s) main_page_sections
5,590✔
1709
    in
1710
    ( Term.(const print_default $ const ()),
1,118✔
1711
      Term.info ~man ~version:"%%VERSION%%" "odoc" )
1,118✔
1712
  in
1713
  match Term.eval_choice ~err:Format.err_formatter default subcommands with
1714
  | `Error _ ->
6✔
1715
      Format.pp_print_flush Format.err_formatter ();
1716
      exit 2
×
1717
  | _ -> ()
1,086✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc