diff --git a/DESIGN_SHOW.md b/DESIGN_SHOW.md index 6a74905..a9af5bd 100644 --- a/DESIGN_SHOW.md +++ b/DESIGN_SHOW.md @@ -28,11 +28,12 @@ loads units by name on demand. This is the *most permissive* env builder resolving an arbitrary reference from the command line — see `url.ml`, which already uses it. -So our resolution code is, almost verbatim, `Url.resolve`: +So our resolution code is, almost verbatim, `Url.resolve` (but with package +roots, see below — `Url.resolve` passes `~roots:None`): ```ocaml let resolver = - Resolver.create ~important_digests:false ~directories ~open_modules:[] ~roots:None + Resolver.create ~important_digests:false ~directories ~open_modules:[] ~roots in let reference = Odoc_model.Semantics.parse_reference s (* string -> Reference.t *) @@ -73,6 +74,26 @@ Only *top-level unit* files need to be reachable by name; sub-modules signature in memory, not from files. Cross-package module-name clashes are handled by odoc already (it warns and picks the first match). +### Package-qualified references — `-L` and `-P` + +The flat `directories` set (the `-I` equivalent) is enough for ordinary dotted +references — a `` `Name `` query always goes through the by-name +`Accessible_paths`, regardless of roots. But **package-qualified path +references** (`{!/pkg/Module.value}`, `{!/pkg/page}`) resolve through *named +roots*, which `~roots:None` doesn't provide. So `show` also reconstructs the +`-L`/`-P` roots the driver links with (`landing_pages.ml`, `odoc_unit.ml`): + +- `-P :/doc/` (page roots) — every top-level dir under `doc/`; +- `-L :/doc//` (lib roots) — their immediate + subdirectories. + +These are passed via `Resolver.create`'s `~roots`, with `current_lib` / +`current_package` left `None` (there is no "current" unit, so relative and +current-package path references — `./x`, `//x` — don't apply; absolute `/pkg/…` +ones do). The named-root dirs are a subset of `directories`, so this adds the +path-reference capability without changing ordinary name resolution. Building +the roots is cheap: `Named_roots` scans each lazily, only on access. + We resolve against `.odoc` (compiled) files, not `.odocl` — `Accessible_paths` only loads the `.odoc` extension, and compiled units already carry their docstrings. (References *inside* a docstring are unresolved in `.odoc`; that @@ -123,11 +144,22 @@ documentation found".) ## Rendering to Markdown — page vs. leaf -What we render depends on what the reference points at, discriminated by -`Odoc_document.Url.from_identifier ~stop_before:false id`: an identifier whose -URL has an **empty anchor** is its own page (a module, module type, class, or -the unit itself); anything with an anchor is an item living *inside* a page (a -value, type, exception, …). +What we render depends on what the reference points at, discriminated by the +loaded file's content and `Odoc_document.Url.from_identifier ~stop_before:false +id`: + +- a **doc (mld) page** (`Page_content`, URL kind `` `Page ``/`` `LeafPage ``) — + render the whole page via `Renderer.document_of_page`; +- a **module-like** item with an **empty anchor** (module, module type, class, + or the unit itself) — render its whole page (below); +- anything with a non-empty **anchor** is an item living *inside* a page (a + value, type, exception, …) — render just its doc comment. + +The owning file is found uniformly for all of these: `index_key` maps the +identifier to the `.odocl` basename (a unit's ``, a page's +`page-`), candidates are loaded, and the one whose root-identifier key is +a suffix of the target's is kept (`owns`) — disambiguating both same-named units +*and* the many identically-named `page-index.odocl` files across packages. **Module-like (empty anchor): render the whole page.** Just printing the top-comment throws away the body, so instead we render the entire module the diff --git a/bin/main.ml b/bin/main.ml index 10d2945..8dc82cc 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -313,7 +313,8 @@ let show_cmd = "An odoc reference to an item, e.g. \ $(b,Odoc_model.Paths.Identifier) or $(b,Stdlib.List.map). The \ same syntax accepted inside $(b,{!...}) in doc comments, including \ - kind tags such as $(b,val:) or $(b,type:).") + kind tags such as $(b,val:) or $(b,type:) and package-qualified \ + paths such as $(b,/stdlib/Stdlib.List.map) or $(b,/astring/index).") in let run switch reference = match switch with @@ -337,9 +338,12 @@ let show_cmd = `P "Resolves $(i,REFERENCE) against every package documented in the \ switch, using the same reference-resolution machinery as \ - $(b,odoc link), then prints the resolved item's doc comment rendered \ - as Markdown. Unlike a single-package tool, the whole switch is always \ - searched."; + $(b,odoc link), then prints it as Markdown: a module, module type, \ + class or page is rendered in full; a leaf item (value, type, \ + exception, …) as its doc comment. Unlike a single-package tool, the \ + whole switch is always searched, and package-qualified path \ + references ($(b,/pkg/...)) resolve via per-package $(b,-L)/$(b,-P) \ + roots reconstructed from the switch layout."; ] in Cmd.v diff --git a/lib/show.ml b/lib/show.ml index 954976f..69ef7c8 100644 --- a/lib/show.ml +++ b/lib/show.ml @@ -14,6 +14,7 @@ module Id = Odoc_model.Paths.Identifier module Lang = Odoc_model.Lang +module Url = Odoc_document.Url (* {1 Stage 1: scan the switch} *) @@ -32,13 +33,37 @@ let all_files root = in loop [] root -(* The include directories (every dir containing a [.odoc] file) and an index - from capitalised unit name to its [.odocl] files. odoc names a compilation - unit's file after the lowercased module name, e.g. [Astring] -> - [astring.odocl], and [Accessible_paths] looks units up by capitalised - basename, so we key the index the same way. *) +(* Immediate subdirectories of [dir], as (basename, path) pairs. *) +let subdirs dir = + match Bos.OS.Dir.contents ~rel:false dir with + | Error _ -> [] + | Ok entries -> + List.filter_map + (fun p -> + match Bos.OS.Dir.exists p with + | Ok true -> Some (Fpath.basename p, p) + | _ -> None) + entries + +type scan = { + doc : Fpath.t; (** the switch's [doc/] root *) + directories : Fpath.t list; (** every dir holding a [.odoc] (the [-I] set) *) + odocl : (string, Fpath.t) Hashtbl.t; (** capitalised unit name -> [.odocl] *) + page_roots : (string * Fpath.t) list; (** package name -> [doc/] ([-P]) *) + lib_roots : (string * Fpath.t) list; + (** library name -> [doc//] ([-L]) *) +} + +(* The driver links each package with [-P :doc/] for every package and + [-L :doc//] for every library (see [landing_pages.ml] and + [odoc_unit.ml] in the driver). To resolve the package-qualified path + references those produce ([{!/pkg/...}]), we reconstruct the same named roots + for the whole switch: packages are the top-level dirs under [doc/], libraries + their immediate subdirectories. (Name lookups for ordinary dotted references + still go through the flat [-I] set; the roots only add path references.) *) let scan sw = - let files = all_files (Switch.doc_dir sw) in + let doc = Switch.doc_dir sw in + let files = all_files doc in let dirs = Hashtbl.create 256 in let odocl = Hashtbl.create 256 in List.iter @@ -54,20 +79,34 @@ let scan sw = Hashtbl.add odocl name p | _ -> ()) files; - let dirs = Hashtbl.fold (fun _ d acc -> d :: acc) dirs [] in - (dirs, odocl) + let directories = Hashtbl.fold (fun _ d acc -> d :: acc) dirs [] in + let page_roots = subdirs doc in + let lib_roots = List.concat_map (fun (_pkg, dir) -> subdirs dir) page_roots in + { doc; directories; odocl; page_roots; lib_roots } (* {1 Stage 2: resolve the reference} *) -let resolve_to_id ~directories ref_str = - let directories = - List.map - (fun d -> Odoc_odoc.Fs.Directory.of_string (Fpath.to_string d)) - directories +let resolve_to_id { doc; directories; page_roots; lib_roots; _ } ref_str = + let dir = Odoc_odoc.Fs.Directory.of_string in + let directories = List.map (fun d -> dir (Fpath.to_string d)) directories in + let named = List.map (fun (n, d) -> (n, dir (Fpath.to_string d))) in + let roots = + Some + { + Odoc_odoc.Resolver.page_roots = named page_roots; + lib_roots = named lib_roots; + current_lib = None; + current_package = None; + (* No "current" unit: [show] resolves a reference from nowhere, so + relative/current-package path references don't apply. current_dir is + required but only feeds the include set and the (here unused) + hierarchy, so the doc root is a harmless value. *) + current_dir = dir (Fpath.to_string doc); + } in let resolver = Odoc_odoc.Resolver.create ~important_digests:false ~directories - ~open_modules:[] ~roots:None + ~open_modules:[] ~roots in let warnings_options = { @@ -215,52 +254,74 @@ let docs_of_unit target (cu : Lang.Compilation_unit.t) = | Module sg -> if matches (cu.id :> Id.t) then Some sg.doc else search_sig matches sg -(* A unit owns [target] when its root identifier is a suffix of [target]'s key. +(* The root identifier of a loaded file: a unit's own module id, or a page's id. + (Implementations and assets carry no doc comment we render.) *) +let content_root : Odoc_odoc.Odoc_file.content -> Id.t option = function + | Unit_content cu -> Some (cu.id :> Id.t) + | Page_content p -> Some (p.name :> Id.t) + | Impl_content _ | Asset_content _ -> None + +(* A file owns [target] when its root identifier is a suffix of [target]'s key. Identifier keys are dotted from the leaf up to the root container page (the package), e.g. [t_result.r_Stdlib.p_stdlib.p_ocamlfind], so this both - confirms the right unit and disambiguates a module name that occurs in more - than one package — we must load the very unit the reference resolved into, - not just any file of the same name. *) -let owns target (cu : Lang.Compilation_unit.t) = - let tk = target.Id.ikey and uk = (cu.id :> Id.t).Id.ikey in - let lt = String.length tk and lu = String.length uk in - lt >= lu && String.sub tk (lt - lu) lu = uk - -let load_cu path = - match Odoc_odoc.Odoc_file.load path with - | Ok { content = Unit_content cu; _ } -> Some cu - | Ok _ | Error (`Msg _) -> None - -let load_unit odocl ref_str id = + confirms the right file and disambiguates a name that occurs in more than one + package — we must load the very file the reference resolved into, not just + any file of the same name. *) +let owns target content = + match content_root content with + | None -> false + | Some root -> + let tk = target.Id.ikey and uk = root.Id.ikey in + let lt = String.length tk and lu = String.length uk in + lt >= lu && String.sub tk (lt - lu) lu = uk + +(* The odocl-index key (capitalised file basename) of the file owning [id]: a + unit lives in [.odocl]; a doc page in [page-.odocl]. *) +let index_key ~is_page id = match Id.fullname id with - | [] -> Error (`Msg "could not determine the root module of the reference") - | root :: _ -> ( - match Hashtbl.find_all odocl (String.capitalize_ascii root) with + | [] -> None + | root :: _ -> + if is_page then + match List.rev (Id.fullname id) with + | leaf :: _ -> Some (String.capitalize_ascii ("page-" ^ leaf)) + | [] -> None + else Some (String.capitalize_ascii root) + +let load_content path = + match Odoc_odoc.Odoc_file.load path with + | Ok { content; _ } -> Some content + | Error (`Msg _) -> None + +let load_owning odocl ref_str ~is_page id = + match index_key ~is_page id with + | None -> Error (`Msg "could not determine the root of the reference") + | Some key -> ( + match Hashtbl.find_all odocl key with | [] -> Error (`Msg (Printf.sprintf - "reference %S resolves into %s, but no linked documentation \ - for it was found in the switch" - ref_str root)) + "reference %S resolved, but no linked documentation for it \ + was found in the switch" + ref_str)) | candidates -> ( - (* Prefer the unit that actually owns the identifier; fall back to - the first loadable candidate if the keying scheme ever changes. *) + (* Prefer the file that actually owns the identifier; fall back to the + first loadable candidate if the keying scheme ever changes. *) let rec pick fallback = function | [] -> fallback | path :: rest -> ( - match load_cu path with - | Some cu when owns id cu -> Some cu - | Some cu -> pick (match fallback with None -> Some cu | f -> f) rest + match load_content path with + | Some c when owns id c -> Some c + | Some c -> pick (Option.value fallback ~default:c |> Option.some) rest | None -> pick fallback rest) in match pick None candidates with - | Some cu -> Ok cu + | Some c -> Ok c | None -> Error (`Msg - (Printf.sprintf - "could not load any linked documentation for %s" root)))) + (Printf.sprintf "could not load linked documentation for %s" + ref_str)))) (* {1 Rendering} *) @@ -282,19 +343,14 @@ let render_page_content (p : Odoc_document.Renderer.page) = Format.pp_print_flush fmt (); Buffer.contents b -(* Render the whole compilation unit to its tree of Markdown pages (the unit's - own page plus a child page per nested module/class), then return the page - whose path is [target] — i.e. the full content of the referenced module, - not just its top-comment. *) -let render_module_page cu target = - let doc = - Odoc_document.Renderer.document_of_compilation_unit - ~syntax:Odoc_document.Renderer.OCaml cu - in +(* Render a document to its tree of Markdown pages, then return the content of + the page whose path is [target]. A compilation unit becomes its own page plus + a child page per nested module/class, so this picks out the referenced + module's full page (not just its top-comment); a doc page becomes a single + page. *) +let render_document doc target = let pages = Odoc_markdown.Generator.render ~config doc in - let same a b = - Odoc_document.Url.Path.to_list a = Odoc_document.Url.Path.to_list b - in + let same a b = Url.Path.to_list a = Url.Path.to_list b in let rec find pages = List.find_map (fun (p : Odoc_document.Renderer.page) -> @@ -303,6 +359,18 @@ let render_module_page cu target = in Option.map render_page_content (find pages) +let render_module_page cu target = + render_document + (Odoc_document.Renderer.document_of_compilation_unit + ~syntax:Odoc_document.Renderer.OCaml cu) + target + +let render_doc_page page target = + render_document + (Odoc_document.Renderer.document_of_page ~syntax:Odoc_document.Renderer.OCaml + page) + target + (* {1 Entry point} *) (* The resolver prints ambiguity notices straight to stderr (it cannot help it: @@ -329,17 +397,32 @@ let docstring_of ref_str id cu = | Some docs -> Ok (render_docs docs) let show sw ref_str = - let directories, odocl = scan sw in - Result.bind (with_suppressed_stderr (fun () -> resolve_to_id ~directories ref_str)) + let scan = scan sw in + Result.bind (with_suppressed_stderr (fun () -> resolve_to_id scan ref_str)) @@ fun id -> - Result.bind (load_unit odocl ref_str id) @@ fun cu -> - (* An identifier whose URL has no anchor is its own page (a module, module - type, class or the unit itself): render that whole page. Anything with an - anchor is an item living inside a page (a value, type, exception, …): show - just its doc comment. *) - let url = Odoc_document.Url.from_identifier ~stop_before:false id in - if url.Odoc_document.Url.Anchor.anchor = "" then - match render_module_page cu url.Odoc_document.Url.Anchor.page with - | Some s -> Ok s - | None -> docstring_of ref_str id cu - else docstring_of ref_str id cu + let url = Url.from_identifier ~stop_before:false id in + let target = url.Url.Anchor.page in + let is_page = + match url.Url.Anchor.kind with `Page | `LeafPage -> true | _ -> false + in + Result.bind (load_owning scan.odocl ref_str ~is_page id) @@ fun content -> + match content with + | Page_content page -> ( + (* A documentation (mld) page: render the whole page. *) + match render_doc_page page target with + | Some s -> Ok s + | None -> Error (`Msg (Printf.sprintf "could not render page %s" ref_str))) + | Unit_content cu -> ( + (* An identifier whose URL has no anchor is its own page (a module, module + type, class or the unit itself): render that whole page. Anything with + an anchor is an item living inside a page (a value, type, exception, …): + show just its doc comment. *) + match url.Url.Anchor.anchor with + | "" -> ( + match render_module_page cu target with + | Some s -> Ok s + | None -> docstring_of ref_str id cu) + | _ -> docstring_of ref_str id cu) + | Impl_content _ | Asset_content _ -> + Error + (`Msg (Printf.sprintf "%s does not refer to documentation" ref_str))