diff --git a/DESIGN_SHOW.md b/DESIGN_SHOW.md index 92cedb9..6a74905 100644 --- a/DESIGN_SHOW.md +++ b/DESIGN_SHOW.md @@ -79,7 +79,7 @@ docstrings. (References *inside* a docstring are unresolved in `.odoc`; that only matters if we want to turn doc-comment cross-references into links — see Open questions.) -## From resolved identifier to docstring +## From resolved identifier to the owning unit This is the one piece `odoc link`/`url.ml` doesn't already hand us — and the chosen approach is to **load the target unit's linked `.odocl` and walk its @@ -121,11 +121,34 @@ heading such as `{2 …}` is *not* a top-comment — it is a floating comment it so a module whose signature opens with only a heading correctly reports "no documentation found".) -## Rendering to Markdown +## Rendering to Markdown — page vs. leaf -odoc's rendering pipeline is Lang → `Odoc_document` IR → backend. The doc -comment's `elements` go into the IR via `Odoc_document.Comment.standalone`, and -the Markdown backend (`src/markdown2`, library `odoc.markdown`) serialises it. +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, …). + +**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 +way `odoc markdown-generate` would: + +```ocaml +let doc = Odoc_document.Renderer.document_of_compilation_unit ~syntax:OCaml cu in +let pages = Odoc_markdown.Generator.render ~config doc in +(* pages is a tree: the unit's own page plus a child page per nested + module/class. Pick the one whose Url.Path matches the target identifier + and render its [content] formatter to a string. *) +``` + +This yields a heading, the module's doc, and each member's signature line with +its synopsis — e.g. the shape of odoc's own +`test/generators/markdown/Toplevel_comments.md`. Rendering the whole owning unit +to reach a nested module's child page is a little wasteful for a big unit like +`Stdlib`, but simple and correct; optimise later if it bites. + +**Leaf (non-empty anchor): render just the doc comment.** The comment's +`elements` go into the IR via `Odoc_document.Comment.standalone`; `Renderer.to_string` takes a single block, so the item list is wrapped in `Renderer.Block.Blocks`: @@ -177,10 +200,11 @@ the switch's odoc. - **Choosing among multiple matches.** A bare module name ambiguous across packages currently resolves to whichever odoc lists first. A `--package` filter (as `search` has) would let the user disambiguate deliberately. -- **Whole-item rendering.** `show` prints only the doc *comment*. Rendering the - item's signature (the `type`/`val` declaration) above it is a different - `Odoc_document` entry point (`Generator.Make`) and a larger job; out of scope - for the first cut. +- **Leaf signatures.** Module-like references render the full page, but a *leaf* + reference (a `val`/`type`/…) still prints only its doc *comment*, not the + `val foo : …` / `type t = …` signature line above it. Showing that line would + mean locating the item's rendered entry within its parent page (or rendering + the single item) — a reasonable follow-up. - **Cross-reference link targets.** With `Base ""` the in-comment links are not meaningfully clickable from a terminal; a future mode could rewrite them to `switchdocs show` invocations or to the switch's HTML. diff --git a/lib/show.ml b/lib/show.ml index 1beca49..954976f 100644 --- a/lib/show.ml +++ b/lib/show.ml @@ -264,8 +264,10 @@ let load_unit odocl ref_str id = (* {1 Rendering} *) -let render_markdown (docs : Odoc_model.Comment.docs) = - let config = Odoc_markdown.Config.make ~root_url:None ~allow_html:false () in +let config = Odoc_markdown.Config.make ~root_url:None ~allow_html:false () + +(* A single leaf item's doc comment, on its own. *) +let render_docs (docs : Odoc_model.Comment.docs) = let blocks = Odoc_document.Comment.standalone docs.elements |> Odoc_markdown.Generator.items ~config @@ -273,6 +275,34 @@ let render_markdown (docs : Odoc_model.Comment.docs) = in Odoc_markdown.Renderer.to_string (Odoc_markdown.Renderer.Block.Blocks blocks) +let render_page_content (p : Odoc_document.Renderer.page) = + let b = Buffer.create 4096 in + let fmt = Format.formatter_of_buffer b in + p.content fmt; + 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 + 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 rec find pages = + List.find_map + (fun (p : Odoc_document.Renderer.page) -> + if same p.path target then Some p else find p.children) + pages + in + Option.map render_page_content (find pages) + (* {1 Entry point} *) (* The resolver prints ambiguity notices straight to stderr (it cannot help it: @@ -291,12 +321,25 @@ let with_suppressed_stderr f = Unix.dup2 saved Unix.stderr; Unix.close saved) +(* The leaf-item fallback: just the doc comment. *) +let docstring_of ref_str id cu = + match docs_of_unit id cu with + | None | Some { elements = []; _ } -> + Error (`Msg (Printf.sprintf "no documentation found for %s" ref_str)) + | 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)) @@ fun id -> Result.bind (load_unit odocl ref_str id) @@ fun cu -> - match docs_of_unit id cu with - | None | Some { elements = []; _ } -> - Error (`Msg (Printf.sprintf "no documentation found for %s" ref_str)) - | Some docs -> Ok (render_markdown docs) + (* 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