New odoc rules
This commit is contained in:
parent
86768475b2
commit
7a26c18e20
|
@ -11,6 +11,8 @@ open! No_io
|
|||
module Gen(P : Install_rules.Params) = struct
|
||||
module Alias = Build_system.Alias
|
||||
module SC = Super_context
|
||||
module Odoc = Odoc.Gen(P)
|
||||
|
||||
open P
|
||||
|
||||
let ctx = SC.context sctx
|
||||
|
@ -760,13 +762,7 @@ module Gen(P : Install_rules.Params) = struct
|
|||
SC.add_rule sctx build
|
||||
);
|
||||
|
||||
(* Odoc *)
|
||||
let mld_files =
|
||||
String_set.fold files ~init:[] ~f:(fun fn acc ->
|
||||
if Filename.check_suffix fn ".mld" then fn :: acc else acc)
|
||||
in
|
||||
Odoc.setup_library_rules sctx lib ~dir ~requires ~modules ~dep_graphs
|
||||
~mld_files ~scope
|
||||
Odoc.setup_library_odoc_rules lib ~dir ~requires ~modules ~dep_graphs ~scope
|
||||
;
|
||||
|
||||
let flags =
|
||||
|
@ -983,7 +979,7 @@ module Gen(P : Install_rules.Params) = struct
|
|||
(match components with
|
||||
| ".js" :: rest -> Js_of_ocaml_rules.setup_separate_compilation_rules
|
||||
sctx rest
|
||||
| "_doc" :: rest -> Odoc.gen_rules sctx rest ~dir
|
||||
| "_doc" :: rest -> Odoc.gen_rules rest ~dir
|
||||
| ".ppx" :: rest -> Preprocessing.gen_rules sctx rest
|
||||
| _ ->
|
||||
match Path.Map.find stanzas_per_dir dir with
|
||||
|
@ -1006,7 +1002,13 @@ module Gen(P : Install_rules.Params) = struct
|
|||
let module_names_of_lib = module_names_of_lib
|
||||
let mlds_of_dir = mlds_of_dir
|
||||
end) in
|
||||
Install_rules.init ()
|
||||
Install_rules.init ();
|
||||
Odoc.init ~modules_by_lib:(fun ~dir lib ->
|
||||
let m = modules_by_lib ~dir lib in
|
||||
match m.alias_module with
|
||||
| Some m -> [m]
|
||||
| None -> Module.Name.Map.values m.modules
|
||||
) ~mlds_of_dir
|
||||
end
|
||||
|
||||
module type Gen = sig
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
module Gen (S : sig val sctx : Super_context.t end) = struct
|
||||
|
||||
let setup_library_odoc_rules _ ~dir:_ ~scope:_ ~modules:_ ~requires:_
|
||||
~dep_graphs:_ = ()
|
||||
|
||||
let init ~modules_by_lib:_ ~mlds_of_dir:_ = ()
|
||||
|
||||
let gen_rules ~dir:_ _ = ()
|
||||
end
|
738
src/odoc.ml
738
src/odoc.ml
|
@ -4,113 +4,100 @@ open Build.O
|
|||
|
||||
module SC = Super_context
|
||||
|
||||
module Doc = struct
|
||||
let root sctx = Path.relative (SC.context sctx).Context.build_dir "_doc"
|
||||
let (++) = Path.relative
|
||||
|
||||
type origin =
|
||||
| Public of string
|
||||
| Private of string * Scope_info.Name.t
|
||||
let lib_unique_name lib =
|
||||
let name = Lib.name lib in
|
||||
match Lib.status lib with
|
||||
| Installed -> assert false
|
||||
| Public _ -> name
|
||||
| Private scope_name -> SC.Scope_key.to_string name scope_name
|
||||
|
||||
let dir_internal t origin =
|
||||
let name =
|
||||
match origin with
|
||||
| Public n -> n
|
||||
| Private (n, s) -> sprintf "%s@%s" n (Scope_info.Name.to_string s)
|
||||
in
|
||||
Path.relative (root t) name
|
||||
let pkg_or_lnu lib =
|
||||
match Lib.package lib with
|
||||
| Some p -> Package.Name.to_string p
|
||||
| None -> lib_unique_name lib
|
||||
|
||||
let dir t (lib : Library.t) =
|
||||
dir_internal t
|
||||
(match lib.public with
|
||||
| Some { name; _ } -> Public name
|
||||
| None -> Private (lib.name, lib.scope_name))
|
||||
type target =
|
||||
| Lib of Lib.t
|
||||
| Pkg of Package.Name.t
|
||||
|
||||
let alias = Build_system.Alias.make ".doc-all"
|
||||
module Gen (S : sig val sctx : SC.t end) = struct
|
||||
open S
|
||||
|
||||
let deps t =
|
||||
Build.dyn_paths (Build.arr (
|
||||
List.fold_left ~init:[] ~f:(fun acc (lib : Lib.t) ->
|
||||
if Lib.is_local lib then (
|
||||
let dir =
|
||||
dir_internal t
|
||||
(match Lib.status lib with
|
||||
| Installed -> assert false
|
||||
| Public _ -> Public (Lib.name lib)
|
||||
| Private s -> Private (Lib.name lib, s))
|
||||
in
|
||||
Build_system.Alias.stamp_file (alias ~dir) :: acc
|
||||
) else (
|
||||
acc
|
||||
)
|
||||
)))
|
||||
let context = SC.context sctx
|
||||
|
||||
let alias t lib = alias ~dir:(dir t lib)
|
||||
module Paths = struct
|
||||
let root = context.Context.build_dir ++ "_doc"
|
||||
|
||||
let static_deps t lib = Build_system.Alias.dep (alias t lib)
|
||||
let odocs m =
|
||||
root ++ (
|
||||
match m with
|
||||
| Lib lib -> sprintf "_odoc/lib/%s" (lib_unique_name lib)
|
||||
| Pkg pkg -> sprintf "_odoc/pkg/%s" (Package.Name.to_string pkg)
|
||||
)
|
||||
|
||||
let setup_deps t lib files = SC.add_alias_deps t (alias t lib) files
|
||||
let html_root = root ++ "_html"
|
||||
|
||||
let dir t lib = dir t lib
|
||||
end
|
||||
let html m =
|
||||
html_root ++ (
|
||||
match m with
|
||||
| Pkg pkg -> Package.Name.to_string pkg
|
||||
| Lib lib -> pkg_or_lnu lib
|
||||
)
|
||||
|
||||
let gen_mld_dir (pkg : Package.t) =
|
||||
root ++ "_mlds" ++ (Package.Name.to_string pkg.name)
|
||||
end
|
||||
|
||||
let ( ++ ) = Path.relative
|
||||
module Dep = struct
|
||||
let html_alias m =
|
||||
Build_system.Alias.doc ~dir:(Paths.html m)
|
||||
|
||||
let get_odoc sctx = SC.resolve_program sctx "odoc" ~hint:"opam install odoc"
|
||||
let odoc_ext = ".odoc"
|
||||
let alias = Build_system.Alias.make ".odoc-all"
|
||||
|
||||
module Mld : sig
|
||||
type t
|
||||
val create : name:string -> t
|
||||
let deps =
|
||||
Build.dyn_paths (Build.arr (
|
||||
List.fold_left ~init:[] ~f:(fun acc (lib : Lib.t) ->
|
||||
if Lib.is_local lib then (
|
||||
let dir = Paths.odocs (Lib lib) in
|
||||
Build_system.Alias.stamp_file (alias ~dir) :: acc
|
||||
) else (
|
||||
acc
|
||||
)
|
||||
)))
|
||||
|
||||
val odoc_file : doc_dir:Path.t -> t -> Path.t
|
||||
val odoc_input : doc_dir:Path.t -> t -> Path.t
|
||||
let alias m = alias ~dir:(Paths.odocs m)
|
||||
|
||||
val html_filename : t -> string
|
||||
end = struct
|
||||
type t = string (** source file name without the extension. *)
|
||||
(* let static_deps t lib = Build_system.Alias.dep (alias t lib) *)
|
||||
|
||||
let create ~name = name
|
||||
let setup_deps m files = SC.add_alias_deps sctx (alias m) files
|
||||
end
|
||||
|
||||
let odoc_file ~doc_dir t =
|
||||
Path.relative doc_dir (sprintf "page-%s%s" t odoc_ext)
|
||||
let odoc = SC.resolve_program sctx "odoc" ~hint:"opam install odoc"
|
||||
let odoc_ext = ".odoc"
|
||||
|
||||
let odoc_input ~doc_dir t =
|
||||
Path.relative doc_dir (sprintf "%s-generated.mld" t)
|
||||
module Mld : sig
|
||||
type t
|
||||
|
||||
let html_filename t =
|
||||
sprintf "%s.html" t
|
||||
end
|
||||
val create : Path.t -> t
|
||||
|
||||
module Module_or_mld = struct
|
||||
type t =
|
||||
| Mld of Mld.t
|
||||
| Module of Module.t
|
||||
val odoc_file : doc_dir:Path.t -> t -> Path.t
|
||||
val odoc_input : t -> Path.t
|
||||
|
||||
let odoc_file ~doc_dir = function
|
||||
| Mld m -> Mld.odoc_file ~doc_dir m
|
||||
| Module m -> Module.odoc_file ~doc_dir m
|
||||
end = struct
|
||||
type t = Path.t
|
||||
|
||||
let odoc_input ~obj_dir ~doc_dir = function
|
||||
| Mld m -> Mld.odoc_input ~doc_dir m
|
||||
| Module m -> Module.cmti_file m ~obj_dir
|
||||
let create p = p
|
||||
|
||||
let html_dir ~doc_dir = function
|
||||
| Mld _ -> doc_dir
|
||||
| Module m -> doc_dir ++ String.capitalize m.obj_name
|
||||
let odoc_file ~doc_dir t =
|
||||
let t = Filename.chop_extension (Path.basename t) in
|
||||
Path.relative doc_dir (sprintf "page-%s%s" t odoc_ext)
|
||||
|
||||
let html_file ~doc_dir t =
|
||||
match t with
|
||||
| Mld m -> html_dir ~doc_dir t ++ Mld.html_filename m
|
||||
| Module _ -> html_dir ~doc_dir t ++ "index.html"
|
||||
end
|
||||
let odoc_input t = t
|
||||
end
|
||||
|
||||
let module_or_mld_deps (m : Module_or_mld.t) ~doc_dir
|
||||
~(dep_graphs:Ocamldep.Dep_graphs.t) =
|
||||
match m with
|
||||
| Mld _ ->
|
||||
Build.arr (fun x -> x)
|
||||
| Module m ->
|
||||
let module_deps (m : Module.t) ~doc_dir ~(dep_graphs:Ocamldep.Dep_graphs.t) =
|
||||
Build.dyn_paths
|
||||
((match m.intf with
|
||||
| Some _ ->
|
||||
|
@ -120,194 +107,140 @@ let module_or_mld_deps (m : Module_or_mld.t) ~doc_dir
|
|||
Ocamldep.Dep_graph.deps_of dep_graphs.impl m)
|
||||
>>^ List.map ~f:(Module.odoc_file ~doc_dir))
|
||||
|
||||
let compile sctx (m : Module_or_mld.t) ~odoc ~dir ~obj_dir ~includes ~dep_graphs
|
||||
~doc_dir ~lib_unique_name =
|
||||
let context = SC.context sctx in
|
||||
let odoc_file = Module_or_mld.odoc_file m ~doc_dir in
|
||||
SC.add_rule sctx
|
||||
(module_or_mld_deps m ~doc_dir ~dep_graphs
|
||||
>>>
|
||||
includes
|
||||
>>>
|
||||
Build.run ~context ~dir:doc_dir odoc
|
||||
[ A "compile"
|
||||
; A "-I"; Path dir
|
||||
; Dyn (fun x -> x)
|
||||
; As ["--pkg"; lib_unique_name]
|
||||
; A "-o"; Target odoc_file
|
||||
; Dep (Module_or_mld.odoc_input m ~obj_dir ~doc_dir)
|
||||
]);
|
||||
(m, odoc_file)
|
||||
|
||||
let to_html sctx (m : Module_or_mld.t) odoc_file ~doc_dir ~odoc ~dir ~includes
|
||||
~(lib : Library.t) =
|
||||
let context = SC.context sctx in
|
||||
let html_dir = Module_or_mld.html_dir ~doc_dir m in
|
||||
let html_file = Module_or_mld.html_file ~doc_dir m in
|
||||
let to_remove, jbuilder_keep =
|
||||
match m with
|
||||
| Mld _ -> html_file, []
|
||||
| Module _ ->
|
||||
let jbuilder_keep =
|
||||
Build.create_file (html_dir ++ Config.jbuilder_keep_fname) in
|
||||
html_dir, [jbuilder_keep]
|
||||
in
|
||||
SC.add_rule sctx
|
||||
(Doc.static_deps sctx lib
|
||||
>>>
|
||||
includes
|
||||
>>>
|
||||
Build.progn (
|
||||
Build.remove_tree to_remove
|
||||
:: Build.mkdir html_dir
|
||||
:: Build.run ~context ~dir odoc ~extra_targets:[html_file]
|
||||
[ A "html"
|
||||
; A "-I"; Path doc_dir
|
||||
; Dyn (fun x -> x)
|
||||
; A "-o"; Path (Path.parent doc_dir)
|
||||
; Dep odoc_file
|
||||
]
|
||||
:: jbuilder_keep
|
||||
)
|
||||
);
|
||||
html_file
|
||||
|
||||
let all_mld_files sctx ~(lib : Library.t) ~modules ~dir files =
|
||||
let all_files =
|
||||
if List.mem "index.mld" ~set:files then files else "index.mld" :: files
|
||||
in
|
||||
let lib_name = Library.best_name lib in
|
||||
let doc_dir = Doc.dir sctx lib in
|
||||
List.map all_files ~f:(fun file ->
|
||||
let name = Filename.chop_extension file in
|
||||
let mld = Mld.create ~name in
|
||||
let generated_mld = Mld.odoc_input ~doc_dir mld in
|
||||
let source_mld = dir ++ file in
|
||||
let compile_module (m : Module.t) ~dir ~obj_dir ~includes ~dep_graphs
|
||||
~doc_dir ~pkg_or_lnu =
|
||||
let odoc_file = Module.odoc_file m ~doc_dir in
|
||||
SC.add_rule sctx
|
||||
(Build.if_file_exists source_mld
|
||||
~then_:(Build.contents source_mld)
|
||||
~else_:(Build.arr (fun () ->
|
||||
(if lib.wrapped then
|
||||
sprintf
|
||||
"{1 Library %s}\n\
|
||||
The entry point for this library is module {!module:%s}."
|
||||
lib_name
|
||||
(String.capitalize lib.name)
|
||||
else
|
||||
sprintf
|
||||
"{1 Library %s}\n\
|
||||
This library exposes the following toplevel modules: {!modules:%s}."
|
||||
lib_name
|
||||
((Module.Name.Map.keys modules :> string list)
|
||||
|> String.concat ~sep:" "))))
|
||||
(module_deps m ~doc_dir ~dep_graphs
|
||||
>>>
|
||||
Build.write_file_dyn generated_mld);
|
||||
mld
|
||||
)
|
||||
includes
|
||||
>>>
|
||||
Build.run ~context ~dir:doc_dir odoc
|
||||
[ A "compile"
|
||||
; A "-I"; Path dir
|
||||
; Dyn (fun x -> x)
|
||||
; As ["--pkg"; pkg_or_lnu]
|
||||
; A "-o"; Target odoc_file
|
||||
; Dep (Module.cmti_file m ~obj_dir)
|
||||
]);
|
||||
(m, odoc_file)
|
||||
|
||||
let css_file ~doc_dir = doc_dir ++ "odoc.css"
|
||||
let compile_mld (m : Mld.t) ~includes ~doc_dir ~pkg =
|
||||
let odoc_file = Mld.odoc_file m ~doc_dir in
|
||||
SC.add_rule sctx
|
||||
(includes
|
||||
>>>
|
||||
Build.run ~context ~dir:doc_dir odoc
|
||||
[ A "compile"
|
||||
; Dyn (fun x -> x)
|
||||
; As ["--pkg"; Package.Name.to_string pkg]
|
||||
; A "-o"; Target odoc_file
|
||||
; Dep (Mld.odoc_input m)
|
||||
]);
|
||||
odoc_file
|
||||
|
||||
let toplevel_index ~doc_dir = doc_dir ++ "index.html"
|
||||
type odoc =
|
||||
{ odoc_input: Path.t
|
||||
; html_dir: Path.t
|
||||
; html_file: Path.t
|
||||
; html_alias: Build_system.Alias.t
|
||||
; typ: [`Module | `Mld]
|
||||
}
|
||||
|
||||
let setup_library_rules sctx (lib : Library.t) ~dir ~scope ~modules ~mld_files
|
||||
~requires ~(dep_graphs:Ocamldep.Dep_graph.t Ml_kind.Dict.t) =
|
||||
let doc_dir = Doc.dir sctx lib in
|
||||
let obj_dir, lib_unique_name =
|
||||
let odoc_include_flags libs =
|
||||
let paths =
|
||||
libs |> List.fold_left ~f:(fun paths lib ->
|
||||
if Lib.is_local lib then (
|
||||
Path.Set.add paths (Paths.odocs (Lib lib))
|
||||
) else (
|
||||
paths
|
||||
)
|
||||
) ~init:Path.Set.empty in
|
||||
Arg_spec.S (List.concat_map (Path.Set.to_list paths)
|
||||
~f:(fun dir -> [Arg_spec.A "-I"; Path dir]))
|
||||
|
||||
let to_html (odoc_file : odoc) ~deps =
|
||||
let to_remove, jbuilder_keep =
|
||||
match odoc_file.typ with
|
||||
| `Mld -> odoc_file.html_file, []
|
||||
| `Module ->
|
||||
let jbuilder_keep =
|
||||
Build.create_file (odoc_file.html_dir ++ Config.jbuilder_keep_fname) in
|
||||
odoc_file.html_dir, [jbuilder_keep]
|
||||
in
|
||||
SC.add_rule sctx
|
||||
(deps
|
||||
>>> Build.progn (
|
||||
Build.remove_tree to_remove
|
||||
:: Build.mkdir odoc_file.html_dir
|
||||
:: Build.run ~context ~dir:Paths.html_root
|
||||
odoc ~extra_targets:[odoc_file.html_file]
|
||||
[ A "html"
|
||||
; Dyn odoc_include_flags
|
||||
; A "-o"; Path Paths.html_root
|
||||
; Dep odoc_file.odoc_input
|
||||
]
|
||||
:: jbuilder_keep
|
||||
)
|
||||
);
|
||||
odoc_file.html_file
|
||||
|
||||
let css_file = Paths.html_root ++ "odoc.css"
|
||||
|
||||
let toplevel_index = Paths.html_root ++ "index.html"
|
||||
|
||||
let setup_library_odoc_rules (library : Library.t) ~dir ~scope ~modules
|
||||
~requires ~(dep_graphs:Ocamldep.Dep_graph.t Ml_kind.Dict.t) =
|
||||
let lib =
|
||||
Option.value_exn (Lib.DB.find_even_when_hidden (Scope.libs scope) lib.name)
|
||||
Option.value_exn (Lib.DB.find_even_when_hidden (Scope.libs scope)
|
||||
library.name) in
|
||||
(* Using the proper package name doesn't actually work since odoc assumes
|
||||
that a package contains only 1 library *)
|
||||
let pkg_or_lnu = pkg_or_lnu lib in
|
||||
let doc_dir = Paths.odocs (Lib lib) in
|
||||
let obj_dir = Lib.obj_dir lib in
|
||||
let includes =
|
||||
Build.memoize "includes"
|
||||
(requires
|
||||
>>> Dep.deps
|
||||
>>^ Lib.L.include_flags ~stdlib_dir:context.stdlib_dir)
|
||||
in
|
||||
let name =
|
||||
let name = Lib.name lib in
|
||||
match Lib.status lib with
|
||||
| Installed -> assert false
|
||||
| Public _ -> name
|
||||
| Private scope_name ->
|
||||
sprintf "%s@%s" name (Scope_info.Name.to_string scope_name)
|
||||
let modules_and_odoc_files =
|
||||
List.map (Module.Name.Map.values modules) ~f:(
|
||||
compile_module ~dir ~obj_dir ~includes ~dep_graphs
|
||||
~doc_dir ~pkg_or_lnu)
|
||||
in
|
||||
(Lib.obj_dir lib, name)
|
||||
in
|
||||
let odoc = get_odoc sctx in
|
||||
let includes =
|
||||
let ctx = SC.context sctx in
|
||||
Build.memoize "includes"
|
||||
(requires
|
||||
>>> Doc.deps sctx
|
||||
>>^ Lib.L.include_flags ~stdlib_dir:ctx.stdlib_dir)
|
||||
in
|
||||
let mld_files =
|
||||
all_mld_files sctx ~dir ~lib ~modules mld_files
|
||||
in
|
||||
let mld_and_odoc_files =
|
||||
List.map mld_files ~f:(fun m ->
|
||||
compile sctx ~odoc ~dir ~obj_dir ~includes ~dep_graphs
|
||||
~doc_dir ~lib_unique_name (Mld m))
|
||||
in
|
||||
let modules_and_odoc_files =
|
||||
List.map (Module.Name.Map.values modules) ~f:(fun m ->
|
||||
compile sctx ~odoc ~dir ~obj_dir ~includes ~dep_graphs
|
||||
~doc_dir ~lib_unique_name (Module m))
|
||||
in
|
||||
let inputs_and_odoc_files = modules_and_odoc_files @ mld_and_odoc_files in
|
||||
Doc.setup_deps sctx lib (List.map inputs_and_odoc_files ~f:snd);
|
||||
(*
|
||||
let modules_and_odoc_files =
|
||||
if lib.wrapped then
|
||||
let main_module_name = String.capitalize lib.name in
|
||||
List.filter modules_and_odoc_files
|
||||
~f:(fun (m, _) -> m.Module.name = main_module_name)
|
||||
else
|
||||
modules_and_odoc_files
|
||||
in*)
|
||||
let html_files =
|
||||
List.map inputs_and_odoc_files ~f:(fun (m, odoc_file) ->
|
||||
to_html sctx m odoc_file ~doc_dir ~odoc ~dir ~includes ~lib)
|
||||
in
|
||||
let doc_root = Doc.root sctx in
|
||||
let alias =
|
||||
match lib.public with
|
||||
| None -> Build_system.Alias.private_doc ~dir
|
||||
| Some _ -> Build_system.Alias.doc ~dir in
|
||||
SC.add_alias_deps sctx alias
|
||||
(css_file ~doc_dir:doc_root
|
||||
:: toplevel_index ~doc_dir:doc_root
|
||||
:: html_files)
|
||||
Dep.setup_deps (Lib lib) (List.map modules_and_odoc_files ~f:snd)
|
||||
|
||||
let setup_css_rule sctx =
|
||||
let context = SC.context sctx in
|
||||
let doc_dir = Doc.root sctx in
|
||||
SC.add_rule sctx
|
||||
(Build.run ~context
|
||||
~dir:context.build_dir
|
||||
~extra_targets:[css_file ~doc_dir]
|
||||
(get_odoc sctx)
|
||||
[ A "css"; A "-o"; Path doc_dir ])
|
||||
let setup_css_rule () =
|
||||
SC.add_rule sctx
|
||||
(Build.run ~context
|
||||
~dir:context.build_dir
|
||||
~extra_targets:[css_file]
|
||||
odoc
|
||||
[ A "css"; A "-o"; Path Paths.html_root ])
|
||||
|
||||
let sp = Printf.sprintf
|
||||
let sp = Printf.sprintf
|
||||
|
||||
let setup_toplevel_index_rule sctx =
|
||||
let list_items =
|
||||
Super_context.stanzas_to_consider_for_install sctx
|
||||
|> List.filter_map ~f:(fun (_path, _scope, stanza) ->
|
||||
match stanza with
|
||||
| Stanza.Library
|
||||
{Library.kind = Library.Kind.Normal; public = Some public_info; _} ->
|
||||
let name = public_info.name in
|
||||
let setup_toplevel_index_rule () =
|
||||
let list_items =
|
||||
Super_context.packages sctx
|
||||
|> Package.Name.Map.to_list
|
||||
|> List.filter_map ~f:(fun (name, pkg) ->
|
||||
let name = Package.Name.to_string name in
|
||||
let link = sp {|<a href="%s/index.html">%s</a>|} name name in
|
||||
let version_suffix =
|
||||
match public_info.package.Package.version_from_opam_file with
|
||||
match pkg.Package.version_from_opam_file with
|
||||
| None ->
|
||||
""
|
||||
| Some v ->
|
||||
sp {| <span class="version">%s</span>|} v
|
||||
in
|
||||
Some (sp "<li>%s%s</li>" link version_suffix)
|
||||
|
||||
| _ ->
|
||||
None)
|
||||
in
|
||||
let list_items = String.concat ~sep:"\n " list_items in
|
||||
let html =
|
||||
sp {|<!DOCTYPE html>
|
||||
Some (sp "<li>%s%s</li>" link version_suffix))
|
||||
in
|
||||
let list_items = String.concat ~sep:"\n " list_items in
|
||||
let html = sp
|
||||
{|<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>index</title>
|
||||
|
@ -321,28 +254,281 @@ let setup_toplevel_index_rule sctx =
|
|||
<ol>
|
||||
%s
|
||||
</ol>
|
||||
</body>
|
||||
</html>
|
||||
|} list_items
|
||||
in
|
||||
let doc_dir = Doc.root sctx in
|
||||
SC.add_rule sctx @@ Build.write_file (toplevel_index ~doc_dir) html
|
||||
|
||||
let gen_rules sctx ~dir:_ rest =
|
||||
match rest with
|
||||
| [] ->
|
||||
setup_css_rule sctx;
|
||||
setup_toplevel_index_rule sctx
|
||||
| lib :: _ ->
|
||||
let lib, lib_db =
|
||||
match String.rsplit2 lib ~on:'@' with
|
||||
| None ->
|
||||
(lib, SC.public_libs sctx)
|
||||
| Some (lib, name) ->
|
||||
(lib,
|
||||
Scope.libs
|
||||
(SC.find_scope_by_name sctx (Scope_info.Name.of_string name)))
|
||||
</div>
|
||||
</body>
|
||||
</html>|} list_items
|
||||
in
|
||||
match Lib.DB.find lib_db lib with
|
||||
| Error _ -> ()
|
||||
| Ok lib -> SC.load_dir sctx ~dir:(Lib.src_dir lib)
|
||||
SC.add_rule sctx @@ Build.write_file toplevel_index html
|
||||
|
||||
|
||||
let html_alias pkg =
|
||||
Build_system.Alias.doc ~dir:(
|
||||
Path.append context.build_dir pkg.Package.path
|
||||
)
|
||||
|
||||
let libs_of_pkg ~pkg =
|
||||
match Package.Name.Map.find (SC.libs_by_package sctx) pkg with
|
||||
| None -> Lib.Set.empty
|
||||
| Some (_, libs) -> libs
|
||||
|
||||
let load_all_odoc_rules_pkg ~pkg =
|
||||
let pkg_libs = libs_of_pkg ~pkg in
|
||||
SC.load_dir sctx ~dir:(Paths.odocs (Pkg pkg));
|
||||
Lib.Set.iter pkg_libs ~f:(fun lib ->
|
||||
SC.load_dir sctx ~dir:(Paths.odocs (Lib lib)));
|
||||
pkg_libs
|
||||
|
||||
let create_odoc ~target odoc_input =
|
||||
let html_alias = Dep.html_alias target in
|
||||
let html_base = Paths.html target in
|
||||
match target with
|
||||
| Lib _ ->
|
||||
let html_dir =
|
||||
html_base ++ (
|
||||
Path.basename odoc_input
|
||||
|> Filename.chop_extension
|
||||
|> Stdune.String.capitalize
|
||||
) in
|
||||
{ odoc_input
|
||||
; html_dir
|
||||
; html_file = html_dir ++ "index.html"
|
||||
; typ = `Module
|
||||
; html_alias
|
||||
}
|
||||
| Pkg _ ->
|
||||
{ odoc_input
|
||||
; html_dir = html_base
|
||||
; html_file = html_base ++ sprintf "%s.html" (
|
||||
Path.basename odoc_input
|
||||
|> Filename.chop_extension
|
||||
|> String.drop_prefix ~prefix:"page-"
|
||||
|> Option.value_exn
|
||||
)
|
||||
; typ = `Mld
|
||||
; html_alias
|
||||
}
|
||||
|
||||
let setup_pkg_html_rules =
|
||||
let loaded = Package.Name.Table.create ~default_value:None in
|
||||
let odoc_glob =
|
||||
Re.compile (Re.seq [Re.(rep1 any) ; Re.str ".odoc" ; Re.eos]) in
|
||||
fun ~pkg ~libs ->
|
||||
if Package.Name.Table.get loaded pkg = None then begin
|
||||
Package.Name.Table.set loaded ~key:pkg ~data:(Some ());
|
||||
let odocs =
|
||||
let odocs target =
|
||||
let dir = Paths.odocs target in
|
||||
SC.eval_glob sctx ~dir odoc_glob
|
||||
|> List.map ~f:(fun d -> create_odoc (Path.relative dir d) ~target)
|
||||
in
|
||||
List.concat (
|
||||
odocs (Pkg pkg)
|
||||
:: (List.map libs ~f:(fun lib -> odocs (Lib lib)))
|
||||
) in
|
||||
let html_files =
|
||||
let closure =
|
||||
match Lib.closure libs with
|
||||
| Ok closure -> closure
|
||||
| Error _ -> libs in
|
||||
let deps = Build.return closure >>> Dep.deps in
|
||||
List.map odocs ~f:(to_html ~deps) in
|
||||
List.iter (
|
||||
Dep.html_alias (Pkg pkg)
|
||||
:: List.map ~f:(fun lib -> Dep.html_alias (Lib lib)) libs
|
||||
) ~f:(fun alias ->
|
||||
SC.add_alias_deps sctx alias
|
||||
[ css_file
|
||||
; toplevel_index
|
||||
]
|
||||
);
|
||||
List.combine odocs html_files
|
||||
|> List.iter ~f:(fun (odoc, html) ->
|
||||
SC.add_alias_deps sctx odoc.html_alias [html]
|
||||
);
|
||||
end
|
||||
|
||||
let gen_rules ~dir:_ rest =
|
||||
match rest with
|
||||
| ["_html"] ->
|
||||
setup_css_rule ();
|
||||
setup_toplevel_index_rule ()
|
||||
| "_mlds" :: _pkg :: _
|
||||
| "_odoc" :: "pkg" :: _pkg :: _ ->
|
||||
() (* rules were already setup lazily in gen_rules *)
|
||||
| "_odoc" :: "lib" :: lib :: _ ->
|
||||
let lib, lib_db = SC.Scope_key.of_string sctx lib in
|
||||
begin match Lib.DB.find lib_db lib with
|
||||
| Error _ -> ()
|
||||
| Ok lib -> SC.load_dir sctx ~dir:(Lib.src_dir lib)
|
||||
end
|
||||
| "_html" :: lib_unique_name_or_pkg :: _ ->
|
||||
let setup_html_rules pkg =
|
||||
setup_pkg_html_rules ~pkg ~libs:(
|
||||
Lib.Set.to_list (load_all_odoc_rules_pkg ~pkg)
|
||||
) in
|
||||
(* TODO we can be a better with the error handling in the case where
|
||||
lib_unique_name_or_pkg is neither a valid pkg or lnu *)
|
||||
let lib, lib_db = SC.Scope_key.of_string sctx lib_unique_name_or_pkg in
|
||||
begin match Lib.DB.find lib_db lib with
|
||||
| Error _ -> ()
|
||||
| Ok lib -> Option.iter (Lib.package lib) ~f:setup_html_rules
|
||||
end;
|
||||
Option.iter
|
||||
(Package.Name.Map.find (SC.packages sctx)
|
||||
(Package.Name.of_string lib_unique_name_or_pkg))
|
||||
~f:(fun pkg -> setup_html_rules pkg.name)
|
||||
| _ -> ()
|
||||
|
||||
let setup_package_aliases (pkg : Package.t) =
|
||||
let alias = html_alias pkg in
|
||||
SC.add_alias_deps sctx alias (
|
||||
Dep.html_alias (Pkg pkg.name)
|
||||
:: (libs_of_pkg ~pkg:pkg.name
|
||||
|> Lib.Set.to_list
|
||||
|> List.map ~f:(fun lib -> Dep.html_alias (Lib lib)))
|
||||
|> List.map ~f:Build_system.Alias.stamp_file
|
||||
)
|
||||
|
||||
let pkg_odoc (pkg : Package.t) = Paths.odocs (Pkg pkg.name)
|
||||
|
||||
let entry_modules ~(pkg : Package.t) ~entry_modules_by_lib =
|
||||
libs_of_pkg ~pkg:pkg.name
|
||||
|> Lib.Set.to_list
|
||||
|> List.filter_map ~f:(fun l ->
|
||||
if Lib.is_local l then (
|
||||
Some (l, entry_modules_by_lib l)
|
||||
) else (
|
||||
None
|
||||
))
|
||||
|> Lib.Map.of_list_exn
|
||||
|
||||
let default_index entry_modules =
|
||||
let b = Buffer.create 512 in
|
||||
Lib.Map.iteri entry_modules ~f:(fun lib modules ->
|
||||
Buffer.add_string b (
|
||||
sprintf
|
||||
"{1 Library %s}\n\
|
||||
This library exposes the following toplevel modules: \
|
||||
{!modules:%s}.\n"
|
||||
(Lib.name lib)
|
||||
(modules
|
||||
|> List.map ~f:(fun m -> Module.Name.to_string (Module.name m))
|
||||
|> String.concat ~sep:" ")
|
||||
)
|
||||
);
|
||||
Buffer.contents b
|
||||
|
||||
let check_mlds_no_dupes ~pkg ~mlds =
|
||||
match
|
||||
List.map mlds ~f:(fun mld -> (Path.basename mld, mld))
|
||||
|> String_map.of_list
|
||||
with
|
||||
| Ok m -> m
|
||||
| Error (_, p1, p2) ->
|
||||
die "Package %s has two mld's with the same basename %s, %s"
|
||||
(Package.Name.to_string pkg.Package.name)
|
||||
(Path.to_string_maybe_quoted p1)
|
||||
(Path.to_string_maybe_quoted p2)
|
||||
|
||||
let setup_package_odoc_rules ~pkg ~mlds ~entry_modules_by_lib =
|
||||
let mlds = check_mlds_no_dupes ~pkg ~mlds in
|
||||
let mlds =
|
||||
if String_map.mem mlds "index" then
|
||||
mlds
|
||||
else
|
||||
let entry_modules = entry_modules ~pkg ~entry_modules_by_lib in
|
||||
let gen_mld = Paths.gen_mld_dir pkg ++ "index.mld" in
|
||||
SC.add_rule sctx (
|
||||
Build.write_file gen_mld (default_index entry_modules)
|
||||
);
|
||||
String_map.add mlds "index" gen_mld in
|
||||
let odocs = List.map (String_map.values mlds) ~f:(fun mld ->
|
||||
compile_mld
|
||||
(Mld.create mld)
|
||||
~pkg:pkg.name
|
||||
~doc_dir:(Paths.odocs (Pkg pkg.name))
|
||||
~includes:(Build.arr (fun _ -> Arg_spec.As []))
|
||||
) in
|
||||
Dep.setup_deps (Pkg pkg.name) odocs
|
||||
|
||||
let init ~modules_by_lib ~mlds_of_dir =
|
||||
let docs_by_package =
|
||||
let map = lazy (
|
||||
SC.stanzas sctx
|
||||
|> List.concat_map ~f:(fun (w : SC.Dir_with_jbuild.t) ->
|
||||
List.filter_map w.stanzas ~f:(function
|
||||
| Jbuild.Stanza.Documentation (d : Jbuild.Documentation.t) ->
|
||||
Some (d.package.name, (w.ctx_dir, d))
|
||||
| _ ->
|
||||
None
|
||||
))
|
||||
|> Package.Name.Map.of_list_multi
|
||||
) in
|
||||
fun (p : Package.t) ->
|
||||
Option.value (Package.Name.Map.find (Lazy.force map) p.name) ~default:[]
|
||||
in
|
||||
let modules_by_lib =
|
||||
let module M = Map.Make(
|
||||
struct
|
||||
type t = Path.t * string
|
||||
let compare (d1, l1) (d2, l2) =
|
||||
match Path.compare d1 d2 with
|
||||
| Ordering.Eq -> String.compare l1 l2
|
||||
| o -> o
|
||||
end) in
|
||||
let lib_to_library = lazy (
|
||||
SC.stanzas sctx
|
||||
|> List.concat_map ~f:(fun (w : SC.Dir_with_jbuild.t) ->
|
||||
List.filter_map w.stanzas ~f:(function
|
||||
| Jbuild.Stanza.Library (l : Library.t) ->
|
||||
Some ((w.ctx_dir, Library.best_name l), l)
|
||||
| _ ->
|
||||
None
|
||||
))
|
||||
|> M.of_list_exn
|
||||
) in
|
||||
fun lib ->
|
||||
let dir = Lib.src_dir lib in
|
||||
let library = Option.value_exn (
|
||||
M.find (Lazy.force lib_to_library) (dir, Lib.name lib)
|
||||
) in
|
||||
modules_by_lib ~dir library
|
||||
in
|
||||
SC.packages sctx
|
||||
|> Package.Name.Map.iter ~f:(fun (pkg : Package.t) ->
|
||||
SC.on_load_dir sctx
|
||||
~dir:(pkg_odoc pkg)
|
||||
~f:(fun () ->
|
||||
setup_package_odoc_rules
|
||||
~pkg
|
||||
~mlds:(
|
||||
docs_by_package pkg
|
||||
|> List.concat_map ~f:(fun (dir, doc) -> mlds_of_dir doc ~dir)
|
||||
)
|
||||
~entry_modules_by_lib:modules_by_lib
|
||||
);
|
||||
(* setup @doc to build the correct html for the package *)
|
||||
setup_package_aliases pkg;
|
||||
);
|
||||
Super_context.add_alias_deps
|
||||
sctx
|
||||
(Build_system.Alias.private_doc ~dir:context.build_dir)
|
||||
(SC.stanzas sctx
|
||||
|> List.concat_map ~f:(fun (w : SC.Dir_with_jbuild.t) ->
|
||||
List.filter_map w.stanzas ~f:(function
|
||||
| Jbuild.Stanza.Library (l : Jbuild.Library.t) ->
|
||||
begin match l.public with
|
||||
| Some _ -> None
|
||||
| None ->
|
||||
let scope = SC.find_scope_by_dir sctx w.ctx_dir in
|
||||
Some (Option.value_exn (
|
||||
Lib.DB.find_even_when_hidden (Scope.libs scope) l.name)
|
||||
)
|
||||
end
|
||||
| (_ : Jbuild.Stanza.t) -> None
|
||||
))
|
||||
|> List.map ~f:(fun (lib : Lib.t) ->
|
||||
Build_system.Alias.stamp_file (Dep.alias (Lib lib)))
|
||||
)
|
||||
|
||||
end
|
||||
|
|
28
src/odoc.mli
28
src/odoc.mli
|
@ -2,15 +2,21 @@
|
|||
|
||||
open Jbuild
|
||||
|
||||
val setup_library_rules
|
||||
: Super_context.t
|
||||
-> Library.t
|
||||
-> dir:Path.t
|
||||
-> scope:Scope.t
|
||||
-> modules:Module.t Module.Name.Map.t
|
||||
-> mld_files:string list
|
||||
-> requires:(unit, Lib.t list) Build.t
|
||||
-> dep_graphs:Ocamldep.Dep_graphs.t
|
||||
-> unit
|
||||
module Gen (S : sig val sctx : Super_context.t end) : sig
|
||||
|
||||
val gen_rules : Super_context.t -> dir:Path.t -> string list -> unit
|
||||
val setup_library_odoc_rules
|
||||
: Library.t
|
||||
-> dir:Path.t
|
||||
-> scope:Scope.t
|
||||
-> modules:Module.t Module.Name.Map.t
|
||||
-> requires:(unit, Lib.t list) Build.t
|
||||
-> dep_graphs:Ocamldep.Dep_graphs.t
|
||||
-> unit
|
||||
|
||||
val init
|
||||
: modules_by_lib:(dir:Path.t -> Library.t -> Module.t list)
|
||||
-> mlds_of_dir:(Documentation.t -> dir:Path.t -> Path.t list)
|
||||
-> unit
|
||||
|
||||
val gen_rules : dir:Path.t -> string list -> unit
|
||||
end
|
||||
|
|
|
@ -28,3 +28,6 @@
|
|||
((name runtest)
|
||||
(deps (foo.install))
|
||||
(action (echo "${read:foo.install}"))))
|
||||
|
||||
(documentation
|
||||
((mld_files (doc))))
|
||||
|
|
|
@ -43,3 +43,6 @@
|
|||
"_build/install/default/share/foo/bar.ml"
|
||||
"_build/install/default/share/foo/baz.ml" {"baz.ml"}
|
||||
]
|
||||
doc: [
|
||||
"_build/install/default/doc/foo/odoc-pages/doc.mld" {"odoc-pages/doc.mld"}
|
||||
]
|
||||
|
|
|
@ -1,16 +1,9 @@
|
|||
This test checks that there is no clash when two private libraries have the same name
|
||||
|
||||
$ $JBUILDER build -j1 --display short --root . @doc-private
|
||||
odoc _doc/odoc.css
|
||||
odoc _doc/test@a/page-index.odoc
|
||||
ocamldep a/test.ml.d
|
||||
ocamlc a/.test.objs/test.{cmi,cmo,cmt}
|
||||
odoc _doc/test@a/test.odoc
|
||||
odoc _doc/test@a/index.html
|
||||
odoc _doc/test@b/page-index.odoc
|
||||
odoc _doc/_odoc/lib/test@a/test.odoc
|
||||
ocamldep b/test.ml.d
|
||||
ocamlc b/.test.objs/test.{cmi,cmo,cmt}
|
||||
odoc _doc/test@b/test.odoc
|
||||
odoc _doc/test@b/index.html
|
||||
odoc _doc/test@a/Test/.jbuilder-keep,_doc/test@a/Test/index.html
|
||||
odoc _doc/test@b/Test/.jbuilder-keep,_doc/test@b/Test/index.html
|
||||
odoc _doc/_odoc/lib/test@b/test.odoc
|
||||
|
|
|
@ -1,36 +1,26 @@
|
|||
Duplicate mld's in the same scope
|
||||
$ $JBUILDER build @doc -j1 --display short --root ./same-scope 2>&1 | grep -v Entering
|
||||
odoc _doc/odoc.css
|
||||
odoc _doc/root.lib1/page-index.odoc
|
||||
odoc _doc/root.lib1/page-test.odoc
|
||||
odoc _doc/_html/odoc.css
|
||||
ocamlc lib1/.root_lib1.objs/root_lib1.{cmi,cmo,cmt}
|
||||
odoc _doc/root.lib1/root_lib1.odoc
|
||||
odoc _doc/root.lib1/index.html
|
||||
odoc _doc/root.lib2/page-index.odoc
|
||||
odoc _doc/root.lib2/page-test.odoc
|
||||
odoc _doc/_odoc/lib/root.lib1/root_lib1.odoc
|
||||
ocamlc lib2/.root_lib2.objs/root_lib2.{cmi,cmo,cmt}
|
||||
odoc _doc/root.lib2/root_lib2.odoc
|
||||
odoc _doc/root.lib2/index.html
|
||||
odoc _doc/root.lib1/test.html
|
||||
odoc _doc/root.lib1/Root_lib1/.jbuilder-keep,_doc/root.lib1/Root_lib1/index.html
|
||||
odoc _doc/root.lib2/test.html
|
||||
odoc _doc/root.lib2/Root_lib2/.jbuilder-keep,_doc/root.lib2/Root_lib2/index.html
|
||||
odoc _doc/_odoc/lib/root.lib2/root_lib2.odoc
|
||||
odoc _doc/_html/root/Root_lib1/.jbuilder-keep,_doc/_html/root/Root_lib1/index.html
|
||||
odoc _doc/_odoc/pkg/root/page-index.odoc
|
||||
odoc _doc/_html/root/index.html
|
||||
odoc _doc/_html/root/Root_lib2/.jbuilder-keep,_doc/_html/root/Root_lib2/index.html
|
||||
|
||||
Duplicate mld's in different scope
|
||||
$ rm -rf diff-scope/_build
|
||||
$ $JBUILDER build @doc -j1 --display short --root ./diff-scope 2>&1 | grep -v Entering
|
||||
odoc _doc/odoc.css
|
||||
odoc _doc/scope1/page-foo.odoc
|
||||
odoc _doc/scope1/page-index.odoc
|
||||
odoc _doc/_html/odoc.css
|
||||
ocamlc scope1/.scope1.objs/scope1.{cmi,cmo,cmt}
|
||||
odoc _doc/scope1/scope1.odoc
|
||||
odoc _doc/scope1/foo.html
|
||||
odoc _doc/scope2/page-foo.odoc
|
||||
odoc _doc/scope2/page-index.odoc
|
||||
odoc _doc/_odoc/lib/scope1/scope1.odoc
|
||||
odoc _doc/_html/scope1/Scope1/.jbuilder-keep,_doc/_html/scope1/Scope1/index.html
|
||||
odoc _doc/_odoc/pkg/scope1/page-index.odoc
|
||||
odoc _doc/_html/scope1/index.html
|
||||
ocamlc scope2/.scope2.objs/scope2.{cmi,cmo,cmt}
|
||||
odoc _doc/scope2/scope2.odoc
|
||||
odoc _doc/scope2/foo.html
|
||||
odoc _doc/scope1/index.html
|
||||
odoc _doc/scope1/Scope1/.jbuilder-keep,_doc/scope1/Scope1/index.html
|
||||
odoc _doc/scope2/index.html
|
||||
odoc _doc/scope2/Scope2/.jbuilder-keep,_doc/scope2/Scope2/index.html
|
||||
odoc _doc/_odoc/lib/scope2/scope2.odoc
|
||||
odoc _doc/_html/scope2/Scope2/.jbuilder-keep,_doc/_html/scope2/Scope2/index.html
|
||||
odoc _doc/_odoc/pkg/scope2/page-index.odoc
|
||||
odoc _doc/_html/scope2/index.html
|
||||
|
|
|
@ -20,5 +20,5 @@
|
|||
|
||||
(alias
|
||||
((name runtest)
|
||||
(deps (_doc/index.html))
|
||||
(action (echo "${read:_doc/index.html}"))))
|
||||
(deps (_doc/_html/index.html))
|
||||
(action (echo "${read:_doc/_html/index.html}"))))
|
||||
|
|
|
@ -1,21 +1,15 @@
|
|||
$ $JBUILDER build @doc -j1 --display short --root .
|
||||
ocamldep foo_byte.ml.d
|
||||
ocamlc .foo_byte.objs/foo_byte.{cmi,cmo,cmt}
|
||||
odoc _doc/foo.byte/foo_byte.odoc
|
||||
odoc _doc/foo.byte/page-index.odoc
|
||||
odoc _doc/foo.byte/page-test.odoc
|
||||
odoc _doc/foo.byte/Foo_byte/.jbuilder-keep,_doc/foo.byte/Foo_byte/index.html
|
||||
ocamldep foo.ml.d
|
||||
ocamlc .foo.objs/foo.{cmi,cmo,cmt}
|
||||
odoc _doc/foo/foo.odoc
|
||||
odoc _doc/foo/page-index.odoc
|
||||
odoc _doc/foo/page-test.odoc
|
||||
odoc _doc/foo/Foo/.jbuilder-keep,_doc/foo/Foo/index.html
|
||||
odoc _doc/odoc.css
|
||||
odoc _doc/foo.byte/index.html
|
||||
odoc _doc/foo.byte/test.html
|
||||
odoc _doc/foo/index.html
|
||||
odoc _doc/foo/test.html
|
||||
odoc _doc/_odoc/lib/foo/foo.odoc
|
||||
ocamldep foo_byte.ml.d
|
||||
ocamlc .foo_byte.objs/foo_byte.{cmi,cmo,cmt}
|
||||
odoc _doc/_odoc/lib/foo.byte/foo_byte.odoc
|
||||
odoc _doc/_html/foo/Foo/.jbuilder-keep,_doc/_html/foo/Foo/index.html
|
||||
odoc _doc/_odoc/pkg/foo/page-index.odoc
|
||||
odoc _doc/_html/foo/index.html
|
||||
odoc _doc/_html/odoc.css
|
||||
odoc _doc/_html/foo/Foo_byte/.jbuilder-keep,_doc/_html/foo/Foo_byte/index.html
|
||||
$ $JBUILDER runtest -j1 --display short --root .
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
|
@ -30,7 +24,7 @@
|
|||
<h2>OCaml package documentation</h2>
|
||||
<ol>
|
||||
<li><a href="foo/index.html">foo</a></li>
|
||||
<li><a href="foo.byte/index.html">foo.byte</a></li>
|
||||
</ol>
|
||||
</body>
|
||||
</html>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
Loading…
Reference in New Issue