···11+Odoc currently supports a number of "tags" like `@raise` `@param` `@since` and so on. I would like to add support for "custom tags" where a user-defined tag can be created with new behaviour at the linking step and the HTML generation step.
22+33+For example, we might have a new tag for referencing IETF RFCs - e.g. `@rfc 9110 Section 5.5`. Or we might have a new tag for an example block:
44+55+```
66+@example This is an example of foo bar.
77+It's a multi-line thing that ends up in an
88+outlined box in the HTML
99+```
1010+1111+Or we might have something where we can resolve references:
1212+1313+```
1414+@handles This handles the {!Foo} exception.
1515+```
1616+1717+Now the way to handle these is to write an odoc extension somehow
1818+where we'd write code that uses the odoc APIs to handle the extensions. These pieces of code would be called during the link
1919+and HTML generation phases of odoc.
2020+2121+We need to come up with some mechanisms to make this happen.
2222+2323+Firstly, how do we tell odoc about this? Do we use Dynlink to
2424+load in the new handlers, or do we recompile a new odoc binary, linking in the new handlers?
2525+2626+Secondly, how do we tell `dune` that this needs to be done?
2727+2828+Thirdly, how do we tell the ocaml docs CI that this needs to be done? This would presumably require some new fields in the opam
2929+file.
3030+3131+## Design Decisions
3232+3333+### Q1: Dynamic plugins using dune's sites mechanism
3434+3535+Extensions are OCaml libraries that are dynamically loaded at runtime using
3636+dune's plugins/sites mechanism (`dune-site`). This approach:
3737+3838+- **No custom odoc binary**: Extensions are loaded by the standard odoc at runtime
3939+- **Independent installation**: Extensions are installed as separate opam packages
4040+- **Automatic discovery**: odoc discovers installed extensions via the sites mechanism
4141+- **Ecosystem-friendly**: Follows established dune patterns for extensibility
4242+4343+#### How it works
4444+4545+1. **odoc declares a plugin site** in its `dune-project`:
4646+4747+```lisp
4848+(lang dune 3.21)
4949+(using dune_site 0.1)
5050+(name odoc)
5151+5252+(package
5353+ (name odoc)
5454+ (sites (lib extensions))) ; Extensions are installed here
5555+```
5656+5757+2. **odoc uses `generate_sites_module`** to discover and load plugins:
5858+5959+```lisp
6060+; In odoc's dune file
6161+(executable
6262+ (name odoc_main)
6363+ (libraries odoc_core dune-site dune-site.plugins)
6464+ (modules odoc_main sites))
6565+6666+(generate_sites_module
6767+ (module sites)
6868+ (plugins (odoc extensions)))
6969+```
7070+7171+3. **Extension packages declare themselves as plugins**:
7272+7373+```lisp
7474+; In odoc-rfc-extension/dune-project
7575+(lang dune 3.21)
7676+(using dune_site 0.1)
7777+(name odoc-rfc-extension)
7878+7979+(package (name odoc-rfc-extension))
8080+```
8181+8282+```lisp
8383+; In odoc-rfc-extension/dune
8484+(library
8585+ (public_name odoc-rfc-extension.impl)
8686+ (name odoc_rfc_impl)
8787+ (libraries odoc.extension_api))
8888+8989+(plugin
9090+ (name odoc-rfc-extension)
9191+ (libraries odoc-rfc-extension.impl)
9292+ (site (odoc extensions)))
9393+```
9494+9595+4. **odoc loads all installed extensions** at startup:
9696+9797+```ocaml
9898+(* In odoc_main.ml *)
9999+let () = Sites.Plugins.Extensions.load_all ()
100100+(* Extensions register themselves during load *)
101101+```
102102+103103+#### ABI compatibility
104104+105105+The primary concern with dynamic loading is ABI compatibility - plugins must be
106106+compiled with the same OCaml version and compatible compiler flags. This is
107107+mitigated by:
108108+109109+- **opam's OCaml version constraints**: Extension packages depend on specific
110110+ OCaml versions, so opam ensures compatibility
111111+- **Rebuild on OCaml upgrade**: When the OCaml compiler is upgraded, all
112112+ packages (including extensions) are rebuilt
113113+- **Clear error messages**: If loading fails due to ABI mismatch, odoc reports
114114+ a clear error directing users to rebuild the extension
115115+116116+### Q2: Extension registration pattern
117117+118118+Extensions register themselves with odoc's extension registry when loaded.
119119+This is the standard dune plugin pattern:
120120+121121+```ocaml
122122+(* In odoc.extension_api *)
123123+module Registry : sig
124124+ val register : (module Odoc_tag_extension) -> unit
125125+ val find : string -> (module Odoc_tag_extension) option
126126+ val all : unit -> (module Odoc_tag_extension) list
127127+end
128128+129129+(* In odoc_rfc_impl.ml - executed when plugin loads *)
130130+let () =
131131+ Odoc.Extension_api.Registry.register (module Rfc_extension)
132132+```
133133+134134+### Q3: Declaration in dune-project and opam
135135+136136+Users declare which extensions they need in their `dune-project`. This serves
137137+two purposes:
138138+139139+1. **Build-time dependency**: Ensures the extension is available when building docs
140140+2. **CI solver hint**: Allows ocaml.org doc CI to know which extensions to install
141141+142142+```lisp
143143+(package
144144+ (name mypkg)
145145+ (depends
146146+ (odoc (>= 3.0))
147147+ (odoc-rfc-extension (>= 1.0))
148148+ (odoc-graphviz-extension (>= 1.0))))
149149+```
150150+151151+Since extensions are regular opam packages with `(plugin ...)` stanzas, they
152152+appear as normal dependencies. The CI solver simply installs all dependencies,
153153+which includes the extensions.
154154+155155+For explicit documentation about which extensions a package uses, an optional
156156+`x-odoc-extensions` field can be added:
157157+158158+```
159159+x-odoc-extensions: ["odoc-rfc-extension" "odoc-graphviz-extension"]
160160+```
161161+162162+This is informational only - the actual dependency resolution uses the
163163+standard `depends` field.
164164+165165+### Q4: Fallback for missing extensions
166166+167167+When odoc encounters a custom tag but the extension is not installed:
168168+169169+1. **Warning**: "Unknown tag @rfc - is odoc-rfc-extension installed?"
170170+2. **Graceful degradation**: The tag content is rendered as a blockquote with
171171+ a note about the missing extension
172172+3. **No build failure**: Documentation generation continues
173173+174174+This allows documentation to be built even if extensions are missing, which is
175175+important for:
176176+- Quick local builds without all extensions
177177+- CI environments that don't have all extensions configured
178178+- Viewing older docs where extensions may have changed
179179+180180+## Extension Interface
181181+182182+Extensions are OCaml modules implementing the `Odoc_tag_extension` signature.
183183+Each extension claims a prefix and handles all tags starting with that prefix:
184184+185185+- `@rfc` → rfc extension
186186+- `@rfc.section` → rfc extension
187187+- `@callout` → callout extension
188188+- `@callout.box` → callout extension
189189+190190+### Extension Output
191191+192192+Extensions return content that can be rendered by any backend, with optional
193193+backend-specific overrides for cases where different output is needed:
194194+195195+```ocaml
196196+(** Resources that can be injected into the page (HTML only) *)
197197+type resource =
198198+ | Js_url of string (** External JavaScript: <script src="..."> *)
199199+ | Css_url of string (** External CSS: <link rel="stylesheet" href="..."> *)
200200+ | Js_inline of string (** Inline JavaScript: <script>...</script> *)
201201+ | Css_inline of string (** Inline CSS: <style>...</style> *)
202202+203203+(** Output from the document phase *)
204204+type extension_output = {
205205+ content : Odoc_document.Types.Block.t;
206206+ (** Universal content - used by all backends unless overridden *)
207207+208208+ overrides : (string * string) list;
209209+ (** Backend-specific raw content overrides.
210210+ E.g., [("html", "<div>...</div>"); ("markdown", "```dot\n...\n```")]
211211+ If present for a backend, used instead of [content]. *)
212212+213213+ resources : resource list;
214214+ (** Page-level resources (JS/CSS). Only used by HTML backend. *)
215215+}
216216+```
217217+218218+**Rendering logic:**
219219+1. Backend checks `overrides` for its name (e.g., "html", "markdown", "latex")
220220+2. If found, use that raw string directly
221221+3. Otherwise, render `content` using the standard Document → output pipeline
222222+4. HTML backend also collects and deduplicates `resources` for page HEAD/BODY
223223+224224+### Module Signature
225225+226226+```ocaml
227227+module type Odoc_tag_extension = sig
228228+ (** The tag prefix this extension handles.
229229+ E.g., "callout" handles @callout, @callout.box, @callout.bubble *)
230230+ val prefix : string
231231+232232+ (** Link phase: process/validate content, resolve custom references.
233233+ Called during odoc link with the linking environment. *)
234234+ val link :
235235+ tag:string ->
236236+ Odoc_xref2.Env.t ->
237237+ Odoc_model.Comment.nestable_block_element list ->
238238+ Odoc_model.Comment.nestable_block_element list
239239+240240+ (** Document phase: convert tag to document elements for rendering.
241241+ Called during document generation. Returns content plus any
242242+ page-level resources needed (JS/CSS). *)
243243+ val to_document :
244244+ tag:string ->
245245+ Odoc_model.Comment.nestable_block_element list ->
246246+ extension_output
247247+end
248248+249249+(** Raised when an extension receives a tag variant it doesn't support.
250250+ E.g., callout extension receiving @callout.unknown *)
251251+exception Unsupported_tag of string
252252+```
253253+254254+### Example Extensions
255255+256256+#### Graphviz (with backend overrides)
257257+258258+This extension needs different output for HTML vs Markdown:
259259+260260+```ocaml
261261+(* odoc_graphviz_extension.ml *)
262262+263263+let prefix = "dot"
264264+265265+let link ~tag _env content = content
266266+267267+let to_document ~tag content =
268268+ let dot_source = extract_text content in
269269+ {
270270+ (* Fallback: just show the source as a code block *)
271271+ content = Block.[Source [...]];
272272+273273+ (* Backend-specific rendering *)
274274+ overrides = [
275275+ ("html", Printf.sprintf {|<div class="graphviz">%s</div>|}
276276+ (escape_html dot_source));
277277+ ("markdown", Printf.sprintf "```dot\n%s\n```" dot_source);
278278+ ];
279279+280280+ (* HTML needs the renderer script *)
281281+ resources = [
282282+ Js_url "https://cdn.jsdelivr.net/npm/@viz-js/viz/lib/viz-standalone.js";
283283+ Js_inline {|
284284+ document.querySelectorAll('.graphviz').forEach(async el => {
285285+ const viz = await Viz.instance();
286286+ el.innerHTML = viz.renderSVGElement(el.textContent).outerHTML;
287287+ });
288288+ |};
289289+ ];
290290+ }
291291+```
292292+293293+#### Callout (universal content)
294294+295295+Simple extensions can use Document types that work everywhere:
296296+297297+```ocaml
298298+(* odoc_callout_extension.ml *)
299299+300300+let prefix = "callout"
301301+302302+let link ~tag _env content = content
303303+304304+let to_document ~tag content =
305305+ let block_content = render_content content in
306306+ let content = match tag with
307307+ | "callout" | "callout.box" ->
308308+ (* Returns Block.t with a styled div - works for all backends *)
309309+ make_callout_block ~style:`Box block_content
310310+ | "callout.bubble" ->
311311+ make_callout_block ~style:`Bubble block_content
312312+ | _ ->
313313+ raise (Unsupported_tag tag)
314314+ in
315315+ (* No overrides needed - Document types render well everywhere *)
316316+ { content; overrides = []; resources = [] }
317317+```
318318+319319+### Error Handling
320320+321321+When odoc encounters a custom tag:
322322+323323+1. Look up extension by prefix (first component before `.`)
324324+2. If no extension registered: warning "Unknown tag @foo"
325325+3. If extension raises `Unsupported_tag`: error "Tag @foo.bar not supported by 'foo' extension"
326326+4. Extension errors during link/render are reported with source location
327327+328328+## Dune Integration
329329+330330+### Extension loading in dune's doc rules
331331+332332+When dune runs `odoc link` or `odoc html-generate`, the extensions are loaded
333333+automatically because:
334334+335335+1. odoc is built with `dune-site.plugins` support
336336+2. The `Sites.Plugins.Extensions.load_all ()` call happens at odoc startup
337337+3. Any extensions installed in the `odoc/extensions` site are discovered
338338+339339+No special dune rules are needed - if the extension package is installed,
340340+odoc will find and use it.
341341+342342+### Development workflow
343343+344344+During development (before extensions are installed), extensions can be
345345+loaded by setting environment variables that dune-site respects:
346346+347347+```bash
348348+# Point to local extension build
349349+export DUNE_DIR_LOCATIONS="odoc:lib:extensions:_build/default/my-extension"
350350+dune build @doc
351351+```
352352+353353+Alternatively, dune could be enhanced to understand that packages with
354354+`(plugin (site (odoc extensions)))` should have their build directories
355355+added to the site path when building docs.
356356+357357+### Complete example: RFC extension package
358358+359359+Here's the full structure of an RFC extension package:
360360+361361+```
362362+odoc-rfc-extension/
363363+├── dune-project
364364+├── odoc-rfc-extension.opam
365365+├── src/
366366+│ ├── dune
367367+│ └── rfc_extension.ml
368368+└── test/
369369+ └── ...
370370+```
371371+372372+**dune-project**:
373373+```lisp
374374+(lang dune 3.21)
375375+(using dune_site 0.1)
376376+(name odoc-rfc-extension)
377377+(generate_opam_files true)
378378+379379+(package
380380+ (name odoc-rfc-extension)
381381+ (synopsis "RFC reference extension for odoc")
382382+ (depends
383383+ (ocaml (>= 4.14))
384384+ (odoc (>= 3.0))))
385385+```
386386+387387+**src/dune**:
388388+```lisp
389389+(library
390390+ (public_name odoc-rfc-extension.impl)
391391+ (name rfc_extension)
392392+ (libraries odoc.extension_api))
393393+394394+(plugin
395395+ (name odoc-rfc-extension)
396396+ (libraries odoc-rfc-extension.impl)
397397+ (site (odoc extensions)))
398398+```
399399+400400+**src/rfc_extension.ml**:
401401+```ocaml
402402+open Odoc_extension_api
403403+404404+let prefix = "rfc"
405405+406406+let link ~tag _env content = content
407407+408408+let to_document ~tag content =
409409+ (* Parse "@rfc 9110" or "@rfc 9110 Section 5.5" *)
410410+ let rfc_num, section = parse_rfc_reference content in
411411+ let url = Printf.sprintf "https://www.rfc-editor.org/rfc/rfc%d" rfc_num in
412412+ let url = match section with
413413+ | None -> url
414414+ | Some s -> url ^ "#" ^ s
415415+ in
416416+ let link_text = match section with
417417+ | None -> Printf.sprintf "RFC %d" rfc_num
418418+ | Some s -> Printf.sprintf "RFC %d %s" rfc_num s
419419+ in
420420+ {
421421+ content = Block.[
422422+ Paragraph [Inline.Link { url; text = [Inline.Text link_text] }]
423423+ ];
424424+ overrides = [];
425425+ resources = [];
426426+ }
427427+428428+(* Register on load *)
429429+let () = Registry.register (module struct
430430+ let prefix = prefix
431431+ let link = link
432432+ let to_document = to_document
433433+end)
434434+```
435435+436436+## Trade-offs: Dynamic vs Static Linking
437437+438438+### Dynamic plugins (recommended)
439439+440440+**Advantages:**
441441+- No need to rebuild odoc for each project
442442+- Extensions are independent packages with their own release cycles
443443+- Natural fit with opam package management
444444+- Standard dune pattern used by other tools
445445+- Extensions can be added/removed without touching the main project
446446+447447+**Disadvantages:**
448448+- ABI compatibility requirements (same OCaml version)
449449+- Slightly more complex deployment (multiple packages)
450450+- Runtime discovery adds small startup overhead
451451+- Cross-compilation may be more complex
452452+453453+### Static linking (alternative)
454454+455455+**Advantages:**
456456+- Single binary with all extensions baked in
457457+- No runtime ABI concerns
458458+- Simpler deployment for specialized use cases
459459+- Works in environments where dynlink is unavailable
460460+461461+**Disadvantages:**
462462+- Requires rebuilding a custom odoc binary
463463+- Extensions tightly coupled to specific odoc version
464464+- More complex build setup
465465+- Doesn't fit well with standard opam workflows
466466+467467+### Recommendation
468468+469469+The dynamic plugin approach using dune-site is recommended as the primary
470470+mechanism because:
471471+472472+1. It follows established dune patterns
473473+2. It integrates naturally with opam
474474+3. It allows extensions to evolve independently
475475+4. The ABI concerns are well-handled by opam's dependency resolver
476476+5. It's the approach used by other OCaml tools with plugin systems
477477+478478+Static linking could be supported as an advanced option for specific use
479479+cases (embedded systems, specialized deployments), but shouldn't be the
480480+default.
481481+482482+## Implementation Plan
483483+484484+### Phase 1: Core extension infrastructure
485485+486486+1. Add `odoc.extension_api` library with:
487487+ - `Odoc_tag_extension` module type
488488+ - `Registry` module for extension registration
489489+ - `extension_output` type and helpers
490490+491491+2. Modify odoc to:
492492+ - Add `(sites (lib extensions))` to dune-project
493493+ - Add `dune-site` and `dune-site.plugins` dependencies
494494+ - Generate sites module and call `load_all ()` at startup
495495+ - Hook extension registry into link and html-generate phases
496496+497497+### Phase 2: Extension discovery and error handling
498498+499499+1. Implement graceful fallback for unknown tags
500500+2. Add helpful error messages for ABI mismatches
501501+3. Add `odoc extensions` subcommand to list installed extensions
502502+503503+### Phase 3: Example extensions
504504+505505+1. Create `odoc-rfc-extension` as a reference implementation
506506+2. Create `odoc-callout-extension` showing universal content
507507+3. Create `odoc-graphviz-extension` showing backend overrides
508508+509509+### Phase 4: Documentation and ecosystem
510510+511511+1. Document extension authoring guide
512512+2. Work with ocaml.org CI to support extensions
513513+3. Consider creating an `odoc-extensions` opam repository or tag
514514+
···21212222let default_lang_tag = "ocaml"
23232424+(** Resource collection for extension handlers.
2525+ Resources are collected during document generation and retrieved when
2626+ building the final page. *)
2727+module Resources = struct
2828+ let collected : Odoc_extension_registry.resource list ref = ref []
2929+3030+ let add resources =
3131+ collected := !collected @ resources
3232+3333+ let take () =
3434+ let result = !collected in
3535+ collected := [];
3636+ result
3737+3838+ let clear () =
3939+ collected := []
4040+end
4141+4242+(** Asset collection for extension handlers.
4343+ Assets (binary files like PNGs) are collected during document generation
4444+ and written alongside the HTML output. *)
4545+module Assets = struct
4646+ let collected : Odoc_extension_registry.asset list ref = ref []
4747+4848+ let add assets =
4949+ collected := !collected @ assets
5050+5151+ let take () =
5252+ let result = !collected in
5353+ collected := [];
5454+ result
5555+5656+ let clear () =
5757+ collected := []
5858+end
5959+2460let source_of_code s =
2561 if s = "" then [] else [ Source.Elt [ inline @@ Inline.Text s ] ]
2662···216252 fun content ->
217253 match content with
218254 | `Paragraph p -> [ paragraph p ]
219219- | `Code_block (lang_tag, code, outputs) ->
220220- let lang_tag =
221221- match lang_tag with None -> default_lang_tag | Some t -> t
255255+ | `Code_block c ->
256256+ let lang_tag, other_tags =
257257+ match c.meta with
258258+ | Some { language = { Odoc_parser.Loc.value; _ }; tags } -> (value, tags)
259259+ | None -> (default_lang_tag, [])
222260 in
223223- let rest =
224224- match outputs with
225225- | Some xs -> nestable_block_element_list xs
226226- | None -> []
261261+ let prefix = Odoc_extension_registry.prefix_of_language lang_tag in
262262+ (* Check for a registered code block handler *)
263263+ let handler_result =
264264+ match Odoc_extension_registry.find_code_block_handler ~prefix with
265265+ | Some handler ->
266266+ let meta = { Odoc_extension_registry.language = lang_tag; tags = other_tags } in
267267+ handler meta (Odoc_model.Location_.value c.content)
268268+ | None -> None
227269 in
228228- [
229229- block
230230- @@ Source (lang_tag, source_of_code (Odoc_model.Location_.value code));
231231- ]
232232- @ rest
270270+ (match handler_result with
271271+ | Some result ->
272272+ (* Handler produced a result, collect resources/assets and use content *)
273273+ Resources.add result.resources;
274274+ Assets.add result.assets;
275275+ result.content
276276+ | None ->
277277+ (* No handler or handler declined, use default rendering *)
278278+ let rest =
279279+ match c.output with
280280+ | Some xs -> nestable_block_element_list xs
281281+ | None -> []
282282+ in
283283+ let value : 'a Odoc_parser.Loc.with_location -> 'a = fun x -> x.value in
284284+ let classes =
285285+ List.filter_map
286286+ (function `Binding (_, _) -> None | `Tag t -> Some (value t))
287287+ other_tags
288288+ in
289289+ let data =
290290+ List.filter_map
291291+ (function
292292+ | `Binding (k, v) -> Some (value k, value v) | `Tag _ -> None)
293293+ other_tags
294294+ in
295295+ [
296296+ block
297297+ @@ Source
298298+ ( lang_tag,
299299+ classes,
300300+ data,
301301+ source_of_code (Odoc_model.Location_.value c.content),
302302+ rest );
303303+ ]
304304+ @ rest)
233305 | `Math_block s -> [ block @@ Math s ]
234306 | `Verbatim s -> [ block @@ Verbatim s ]
235307 | `Modules ms -> [ module_references ms ]
···374446 let content = content_to_inline ~prefix:[ sp ] content in
375447 item ~tag:"alert"
376448 [ block (Block.Inline ([ inline @@ Text tag ] @ content)) ]
449449+ | `Custom (name, content) ->
450450+ (* Check if there's a registered extension for this tag *)
451451+ let prefix = Odoc_extension_registry.prefix_of_tag name in
452452+ (match Odoc_extension_registry.find_handler ~prefix with
453453+ | Some handler ->
454454+ (match handler name content with
455455+ | Some result ->
456456+ (* Extension handled the tag - collect resources/assets and use output *)
457457+ Resources.add result.Odoc_extension_registry.resources;
458458+ Assets.add result.Odoc_extension_registry.assets;
459459+ { Description.attr = [ name ];
460460+ key = [ inline ~attr:[ "at-tag" ] (Text name) ];
461461+ definition = result.Odoc_extension_registry.content }
462462+ | None ->
463463+ (* Extension declined to handle this tag variant *)
464464+ item ~tag:name (nestable_block_element_list content))
465465+ | None ->
466466+ (* No extension registered - use default handling *)
467467+ item ~tag:name (nestable_block_element_list content))
377468378469let attached_block_element : Comment.attached_block_element -> Block.t =
379470 function
···8787let make_expansion_page ~source_anchor url comments items =
8888 let comment = List.concat comments in
8989 let preamble, items = prepare_preamble comment items in
9090- { Page.preamble; items; url; source_anchor }
9090+ let resources = Comment.Resources.take () in
9191+ let assets = Comment.Assets.take () in
9292+ { Page.preamble; items; url; source_anchor; resources; assets }
91939294include Generator_signatures
9395···18131815 let url = Url.Path.from_identifier t.name in
18141816 let preamble, items = Sectioning.docs t.content.elements in
18151817 let source_anchor = None in
18161816- Document.Page { Page.preamble; items; url; source_anchor }
18181818+ let resources = Comment.Resources.take () in
18191819+ let assets = Comment.Assets.take () in
18201820+ Document.Page { Page.preamble; items; url; source_anchor; resources; assets }
1817182118181822 let implementation (v : Odoc_model.Lang.Implementation.t) syntax_info
18191823 source_code =
+3-1
src/document/renderer.ml
···99 path : Url.Path.t;
1010 content : Format.formatter -> unit;
1111 children : page list;
1212+ assets : Odoc_extension_registry.asset list;
1313+ (** Binary assets to write alongside this page *)
1214}
13151416let traverse ~f t =
1517 let rec aux node =
1616- f node.filename node.content;
1818+ f node.filename node.content node.assets;
1719 List.iter aux node.children
1820 in
1921 List.iter aux t
+22-1
src/document/types.ml
···8989 | Paragraph of Inline.t
9090 | List of list_type * t list
9191 | Description of Description.t
9292- | Source of lang_tag * Source.t
9292+ | Source of lang_tag * string list * (string * string) list * Source.t * t
9393 | Math of Math.t
9494 | Verbatim of string
9595 | Raw_markup of Raw_markup.t
···185185 source_anchor : Url.t option;
186186 (** Url to the corresponding source code. Might be a whole source file
187187 or a sub part. *)
188188+ resources : Odoc_extension_registry.resource list;
189189+ (** Resources (JS/CSS) to inject into the page, collected from extensions. *)
190190+ assets : Odoc_extension_registry.asset list;
191191+ (** Binary assets to write alongside this page's HTML output. *)
188192 }
189193end =
190194 Page
···202206 type t = { url : Url.Path.t; contents : code }
203207end =
204208 Source_page
209209+210210+(** Resources that extensions can inject into pages (HTML only) *)
211211+module Resource = struct
212212+ type t =
213213+ | Js_url of string (** External JavaScript: <script src="..."> *)
214214+ | Css_url of string (** External CSS: <link rel="stylesheet" href="..."> *)
215215+ | Js_inline of string (** Inline JavaScript: <script>...</script> *)
216216+ | Css_inline of string (** Inline CSS: <style>...</style> *)
217217+218218+ let equal a b =
219219+ match (a, b) with
220220+ | Js_url a, Js_url b -> String.equal a b
221221+ | Css_url a, Css_url b -> String.equal a b
222222+ | Js_inline a, Js_inline b -> String.equal a b
223223+ | Css_inline a, Css_inline b -> String.equal a b
224224+ | _ -> false
225225+end
205226206227module Document = struct
207228 type t = Page of Page.t | Source_page of Source_page.t
···130130 ();
131131 ]
132132133133-let page_creator ~config ~url ~uses_katex ~global_toc header breadcrumbs
134134- local_toc content =
133133+let page_creator ~config ~url ~uses_katex ~resources ~global_toc header
134134+ breadcrumbs local_toc content =
135135 let theme_uri = Config.theme_uri config in
136136 let support_uri = Config.support_uri config in
137137 let search_uris = Config.search_uris config in
···183183 (Html.txt "");
184184 ]
185185 in
186186+ (* Convert extension resources to HTML elements *)
187187+ let extension_resources =
188188+ let open Odoc_extension_registry in
189189+ List.concat_map
190190+ (function
191191+ | Js_url url ->
192192+ [ Html.script ~a:[ Html.a_src url ] (Html.txt "") ]
193193+ | Css_url url ->
194194+ [ Html.link ~rel:[ `Stylesheet ] ~href:url () ]
195195+ | Js_inline code ->
196196+ [ Html.script (Html.cdata_script code) ]
197197+ | Css_inline code ->
198198+ [ Html.style [ Html.cdata_style code ] ])
199199+ resources
200200+ in
186201 let meta_elements =
187202 let highlightjs_meta =
188203 let highlight_js_uri = file_uri support_uri "highlight.pack.js" in
···219234 else []
220235 in
221236 default_meta_elements ~config ~url @ highlightjs_meta @ katex_meta
237237+ @ extension_resources
222238 in
223239 let meta_elements = meta_elements @ search_scripts in
224240 Html.head (Html.title (Html.txt title_string)) meta_elements
···247263 in
248264 content
249265250250-let make ~config ~url ~header ~breadcrumbs ~sidebar ~toc ~uses_katex content
251251- children =
266266+let make ~config ~url ~header ~breadcrumbs ~sidebar ~toc ~uses_katex ~resources
267267+ ~assets content children =
252268 let filename = Link.Path.as_filename ~config url in
253269 let content =
254254- page_creator ~config ~url ~uses_katex ~global_toc:sidebar header breadcrumbs
255255- toc content
270270+ page_creator ~config ~url ~uses_katex ~resources ~global_toc:sidebar header
271271+ breadcrumbs toc content
256272 in
257257- { Odoc_document.Renderer.filename; content; children; path = url }
273273+ { Odoc_document.Renderer.filename; content; children; path = url; assets }
258274259275let path_of_module_of_source ppf url =
260276 match url.Url.Path.parent with
···295311 let content =
296312 src_page_creator ~breadcrumbs ~config ~url ~header ~sidebar title content
297313 in
298298- { Odoc_document.Renderer.filename; content; children = []; path = url }
314314+ { Odoc_document.Renderer.filename; content; children = []; path = url; assets = [] }
+2
src/html/html_page.mli
···2828 sidebar:Html_types.div_content Html.elt list option ->
2929 toc:Types.toc list ->
3030 uses_katex:bool ->
3131+ resources:Odoc_extension_registry.resource list ->
3232+ assets:Odoc_extension_registry.asset list ->
3133 Html_types.div_content Html.elt list ->
3234 Odoc_document.Renderer.page list ->
3335 Odoc_document.Renderer.page
+2-2
src/latex/generator.ml
···350350 ]
351351 | Raw_markup r -> raw_markup r
352352 | Verbatim s -> [ Verbatim s ]
353353- | Source (_, c) -> non_empty_block_code ~config c
353353+ | Source (_, _, _, c, _) -> non_empty_block_code ~config c
354354 | Math s ->
355355 [
356356 Break Paragraph;
···522522 if config.with_children then link_children ppf children else ()
523523 in
524524 let content ppf = Fmt.pf ppf "@[<v>%a@,%t@]@." pp content children_input in
525525- { Odoc_document.Renderer.filename; content; children; path = url }
525525+ { Odoc_document.Renderer.filename; content; children; path = url; assets = [] }
526526end
527527528528module Page = struct
+2-2
src/manpage/generator.ml
···394394 indent 2 (str "@" ++ key ++ str ":" ++ sp ++ def)
395395 in
396396 list ~sep:break (List.map f descrs) ++ continue rest
397397- | Source (_, content) ->
397397+ | Source (_, _, _, content, _) ->
398398 env "EX" "EE" "" (source_code content) ++ continue rest
399399 | Math s -> math s ++ continue rest
400400 | Verbatim content -> env "EX" "EE" "" (str "%s" content) ++ continue rest
···562562 and children = List.concat_map subpage (Subpages.compute p) in
563563 let content ppf = Format.fprintf ppf "%a@." Roff.pp (page p) in
564564 let filename = Link.as_filename p.url in
565565- { Renderer.filename; content; children; path = p.url }
565565+ { Renderer.filename; content; children; path = p.url; assets = [] }
566566567567let render = function
568568 | Document.Page page -> [ render_page page ]
+2-2
src/markdown2/generator.ml
···4444 (fun (b : Types.Block.one) ->
4545 match b.desc with
4646 | Paragraph inline | Inline inline -> inline_text_only inline
4747- | Source (_, s) -> source inline_text_only s
4747+ | Source (_, _, _, s, _) -> source inline_text_only s
4848 | List (_, items) -> List.concat_map block_text_only items
4949 | Verbatim s -> [ s ]
5050 | _ -> [])
···143143 Renderer.Block.Code_block { info_string = None; code = [ s ] }
144144 in
145145 [ code_snippet ]
146146- | Source (lang, s) ->
146146+ | Source (lang, _, _, s, _) ->
147147 let code = s |> source inline_text_only |> List.map (fun s -> s) in
148148 let code_snippet =
149149 Renderer.Block.Code_block { info_string = Some lang; code }
+2-2
src/markdown2/markdown_page.ml
···33let make ~config ~url doc children =
44 let filename = Link.Path.as_filename ~config url in
55 let content ppf = Format.fprintf ppf "%s" (Renderer.to_string doc) in
66- { Odoc_document.Renderer.filename; content; children; path = url }
66+ { Odoc_document.Renderer.filename; content; children; path = url; assets = [] }
7788let make_src ~config ~url _title block_list =
99 let filename = Link.Path.as_filename ~config url in
···1212 let doc = root_block in
1313 Format.fprintf ppf "%s" (Renderer.to_string doc)
1414 in
1515- { Odoc_document.Renderer.filename; content; children = []; path = url }
1515+ { Odoc_document.Renderer.filename; content; children = []; path = url; assets = [] }
+14-6
src/model/comment.ml
···60606161type media_element = [ `Media of media_href * media * string ]
62626363-type nestable_block_element =
6363+type code_block = {
6464+ meta : Odoc_parser.Ast.code_block_meta option;
6565+ delimiter : string option;
6666+ content : string with_location;
6767+ (** This is the raw content, that is the exact string inside the
6868+ delimiters. In order to get the "processed" content, see
6969+ {!Odoc_parser.codeblock_content} *)
7070+ output : nestable_block_element with_location list option;
7171+}
7272+7373+and nestable_block_element =
6474 [ `Paragraph of paragraph
6565- | `Code_block of
6666- string option
6767- * string with_location
6868- * nestable_block_element with_location list option
7575+ | `Code_block of code_block
6976 | `Math_block of string
7077 | `Verbatim of string
7178 | `Modules of module_reference list
···8996 | `Since of string
9097 | `Before of string * nestable_block_element with_location list
9198 | `Version of string
9292- | `Alert of string * string option ]
9999+ | `Alert of string * string option
100100+ | `Custom of string * nestable_block_element with_location list ]
9310194102type heading_level =
95103 [ `Title
+6-9
src/model/semantics.ml
···216216 match element with
217217 | { value = `Paragraph content; location } ->
218218 Location.at location (`Paragraph (inline_elements content))
219219- | { value = `Code_block { meta; delimiter = _; content; output }; location }
220220- ->
221221- let lang_tag =
222222- match meta with
223223- | Some { language = { Location.value; _ }; _ } -> Some value
224224- | None -> None
225225- in
226226- let outputs =
219219+ | { value = `Code_block { meta; delimiter; content; output }; location } ->
220220+ let output =
227221 match output with
228222 | None -> None
229223 | Some l -> Some (List.map nestable_block_element l)
···234228 let warnings = List.map Error.t_of_parser_t warnings in
235229 List.iter (Error.raise_warning ~non_fatal:true) warnings;
236230 let content = Location.at content.location trimmed_content in
237237- Location.at location (`Code_block (lang_tag, content, outputs))
231231+ let code_block = { Comment.meta; delimiter; content; output } in
232232+ Location.at location (`Code_block code_block)
238233 | { value = `Math_block s; location } -> Location.at location (`Math_block s)
239234 | { value = `Verbatim v; location } ->
240235 let v, warnings = Odoc_parser.codeblock_content location v in
···309304 let ok t = Ok (Location.at location (`Tag t)) in
310305 match tag with
311306 | (`Author _ | `Since _ | `Version _) as tag -> ok tag
307307+ | `Custom (name, content) ->
308308+ ok (`Custom (name, nestable_block_elements content))
312309 | `Deprecated content -> ok (`Deprecated (nestable_block_elements content))
313310 | `Param (name, content) ->
314311 ok (`Param (name, nestable_block_elements content))
+123-126
src/model_desc/comment_desc.ml
···5566let ignore_loc x = x.Location_.value
7788-type general_inline_element =
99- [ `Space
1010- | `Word of string
1111- | `Code_span of string
1212- | `Math_span of string
1313- | `Raw_markup of raw_markup_target * string
1414- | `Styled of style * general_inline_element with_location list
1515- | `Reference of Paths.Reference.t * general_link_content
1616- | `Link of string * general_link_content ]
1717-1818-and general_link_content = general_inline_element with_location list
1919-2020-type general_block_element =
2121- [ `Paragraph of general_link_content
2222- | `Code_block of
2323- string option
2424- * string with_location
2525- * general_block_element with_location list option
2626- | `Math_block of string
2727- | `Verbatim of string
2828- | `Modules of Comment.module_reference list
2929- | `List of
3030- [ `Unordered | `Ordered ] * general_block_element with_location list list
3131- | `Table of general_block_element abstract_table
3232- | `Heading of
3333- Comment.heading_attrs * Identifier.Label.t * general_link_content
3434- | `Tag of general_tag
3535- | `Media of
3636- [ `Reference of Paths.Reference.t | `Link of string ] * media * string ]
3737-3838-and general_tag =
3939- [ `Author of string
4040- | `Deprecated of general_docs
4141- | `Param of string * general_docs
4242- | `Raise of
4343- [ `Code_span of string
4444- | `Reference of Paths.Reference.t * general_link_content ]
4545- * general_docs
4646- | `Return of general_docs
4747- | `See of [ `Url | `File | `Document ] * string * general_docs
4848- | `Since of string
4949- | `Before of string * general_docs
5050- | `Version of string
5151- | `Alert of string * string option ]
5252-5353-and general_docs = general_block_element with_location list
5454-558let media =
569 Variant
5710 (function
···6013 | `Video -> C0 "`Video"
6114 | `Image -> C0 "`Image")
62156363-let rec inline_element : general_inline_element t =
6464- let style =
6565- Variant
6666- (function
6767- | `Bold -> C0 "`Bold"
6868- | `Italic -> C0 "`Italic"
6969- | `Emphasis -> C0 "`Emphasis"
7070- | `Superscript -> C0 "`Superscript"
7171- | `Subscript -> C0 "`Subscript")
7272- in
1616+let rec leaf_inline_element_fn : Odoc_model.Comment.leaf_inline_element -> case
1717+ = function
1818+ | `Space -> C0 "`Space"
1919+ | `Word x -> C ("`Word", x, string)
2020+ | `Code_span x -> C ("`Code_span", x, string)
2121+ | `Math_span x -> C ("`Math_span", x, string)
2222+ | `Raw_markup (x1, x2) -> C ("`Raw_markup", (x1, x2), Pair (string, string))
2323+2424+and style =
7325 Variant
7426 (function
7575- | `Space -> C0 "`Space"
7676- | `Word x -> C ("`Word", x, string)
7777- | `Code_span x -> C ("`Code_span", x, string)
7878- | `Math_span x -> C ("`Math_span", x, string)
7979- | `Raw_markup (x1, x2) -> C ("`Raw_markup", (x1, x2), Pair (string, string))
8080- | `Styled (x1, x2) -> C ("`Styled", (x1, x2), Pair (style, link_content))
8181- | `Reference (x1, x2) ->
8282- C ("`Reference", (x1, x2), Pair (reference, link_content))
8383- | `Link (x1, x2) -> C ("`Link", (x1, x2), Pair (string, link_content)))
2727+ | `Bold -> C0 "`Bold"
2828+ | `Italic -> C0 "`Italic"
2929+ | `Emphasis -> C0 "`Emphasis"
3030+ | `Superscript -> C0 "`Superscript"
3131+ | `Subscript -> C0 "`Subscript")
3232+3333+and non_link_inline_element_fn : non_link_inline_element -> case = function
3434+ | #leaf_inline_element as x -> leaf_inline_element_fn x
3535+ | `Styled (x1, x2) -> C ("`Styled", (x1, x2), Pair (style, link_content))
84368585-and link_content : general_link_content t =
8686- List (Indirect (ignore_loc, inline_element))
3737+and reference_element_fn : reference_element -> case = function
3838+ | `Reference (x1, x2) ->
3939+ C ("`Reference", (x1, x2), Pair (reference, link_content))
4040+4141+and inline_element_fn : inline_element -> case = function
4242+ | #leaf_inline_element as x -> leaf_inline_element_fn x
4343+ | #reference_element as x -> reference_element_fn x
4444+ | `Styled (x1, x2) -> C ("`Styled", (x1, x2), Pair (style, paragraph))
4545+ | `Link (x1, x2) -> C ("`Link", (x1, x2), Pair (string, link_content))
4646+4747+and link_content =
4848+ List (Indirect (ignore_loc, Variant non_link_inline_element_fn))
4949+5050+and inline_element = Variant inline_element_fn
5151+5252+and paragraph = List (Indirect (ignore_loc, inline_element))
87538854let module_reference =
8955 let simplify m =
9090- ( (m.module_reference :> Paths.Reference.t),
9191- (m.module_synopsis :> general_link_content option) )
5656+ ((m.module_reference :> Paths.Reference.t), m.module_synopsis)
9257 in
9393- Indirect (simplify, Pair (reference, Option link_content))
5858+ Indirect (simplify, Pair (reference, Option paragraph))
94599560let heading =
9661 let heading_level =
···11075 F ("heading_label_explicit", (fun h -> h.heading_label_explicit), bool);
11176 ]
11277 in
113113- Triple (heading_attrs, identifier, link_content)
7878+ Triple (heading_attrs, identifier, paragraph)
1147911580let media_href =
11681 Variant
11782 (function
118118- | `Reference r -> C ("`Reference", r, reference)
8383+ | `Reference r ->
8484+ C ("`Reference", (r : Reference.Asset.t :> Reference.t), reference)
11985 | `Link l -> C ("`Link", l, string))
12086121121-let rec block_element : general_block_element t =
8787+let code_block_tag : Odoc_parser.Ast.code_block_tag t =
8888+ Variant
8989+ (function
9090+ | `Tag x -> C ("`Tag", ignore_loc x, string)
9191+ | `Binding (x1, x2) ->
9292+ C ("`Binding", (ignore_loc x1, ignore_loc x2), Pair (string, string)))
9393+9494+let code_block_meta : Odoc_parser.Ast.code_block_meta t =
9595+ Record
9696+ [
9797+ F ("language", (fun h -> ignore_loc h.language), string);
9898+ F ("warnings_tag", (fun h -> h.tags), List code_block_tag);
9999+ ]
100100+101101+let rec code_block : code_block t =
102102+ Record
103103+ [
104104+ F ("meta", (fun h -> h.meta), Option code_block_meta);
105105+ F ("delimiter", (fun h -> h.delimiter), Option string);
106106+ F ("content", (fun h -> ignore_loc h.content), string);
107107+ F ("output", (fun h -> h.output), Option nestable_elements);
108108+ ]
109109+110110+and nestable_block_element_fn : nestable_block_element -> case =
122111 let list_kind =
123112 Variant
124113 (function `Unordered -> C0 "`Unordered" | `Ordered -> C0 "`Ordered")
125114 in
115115+ function
116116+ | `Paragraph x -> C ("`Paragraph", x, paragraph)
117117+ | `Code_block c -> C ("`Code_block", c, code_block)
118118+ | `Math_block x -> C ("`Math_block", x, string)
119119+ | `Verbatim x -> C ("`Verbatim", x, string)
120120+ | `Modules x -> C ("`Modules", x, List module_reference)
121121+ | `Table { data; align } ->
122122+ let cell_type_desc =
123123+ Variant (function `Header -> C0 "`Header" | `Data -> C0 "`Data")
124124+ in
125125+ let data_desc = List (List (Pair (nestable_elements, cell_type_desc))) in
126126+ let align_desc =
127127+ Option
128128+ (Variant
129129+ (function
130130+ | `Left -> C0 "`Left"
131131+ | `Center -> C0 "`Center"
132132+ | `Right -> C0 "`Right"))
133133+ in
134134+ let align_desc = List align_desc in
135135+ let table_desc = Pair (data_desc, Option align_desc) in
136136+ C ("`Table", (data, align), table_desc)
137137+ | `List (x1, x2) ->
138138+ C ("`List", (x1, x2), Pair (list_kind, List nestable_elements))
139139+ | `Media (x1, m, x2) ->
140140+ C ("`Media", (x1, m, x2), Triple (media_href, media, string))
141141+142142+and nestable_block_element : nestable_block_element t =
143143+ Variant nestable_block_element_fn
144144+145145+and nestable_elements : nestable_block_element with_location list t =
146146+ List
147147+ (Indirect
148148+ ( (fun x ->
149149+ let x :> nestable_block_element Location_.with_location = x in
150150+ ignore_loc x),
151151+ nestable_block_element ))
152152+153153+and block_element : block_element t =
126154 Variant
127155 (function
128128- | `Paragraph x -> C ("`Paragraph", x, link_content)
129129- | `Code_block (x1, x2, _) ->
130130- C ("`Code_block", (x1, ignore_loc x2), Pair (Option string, string))
131131- | `Math_block x -> C ("`Math_block", x, string)
132132- | `Verbatim x -> C ("`Verbatim", x, string)
133133- | `Modules x -> C ("`Modules", x, List module_reference)
134134- | `List (x1, x2) ->
135135- C
136136- ( "`List",
137137- (x1, (x2 :> general_docs list)),
138138- Pair (list_kind, List general_content) )
139139- | `Table { data; align } ->
140140- let cell_type_desc =
141141- Variant (function `Header -> C0 "`Header" | `Data -> C0 "`Data")
142142- in
143143- let data_desc = List (List (Pair (general_content, cell_type_desc))) in
144144- let align_desc =
145145- Option
146146- (Variant
147147- (function
148148- | `Left -> C0 "`Left"
149149- | `Center -> C0 "`Center"
150150- | `Right -> C0 "`Right"))
151151- in
152152- let align_desc = List align_desc in
153153- let table_desc = Pair (data_desc, Option align_desc) in
154154- C ("`Table", (data, align), table_desc)
155155- | `Heading h -> C ("`Heading", h, heading)
156156- | `Tag x -> C ("`Tag", x, tag)
157157- | `Media (x1, m, x2) ->
158158- C ("`Media", (x1, m, x2), Triple (media_href, media, string)))
156156+ | #nestable_block_element as x -> nestable_block_element_fn x
157157+ | `Heading (x1, x2, x3) ->
158158+ C ("`Heading", (x1, (x2 :> Identifier.t), x3), heading)
159159+ | `Tag x -> C ("`Tag", x, tag))
159160160160-and tag : general_tag t =
161161+and tag : tag t =
161162 let url_kind =
162163 Variant
163164 (function
···168169 Variant
169170 (function
170171 | `Author x -> C ("`Author", x, string)
171171- | `Deprecated x -> C ("`Deprecated", x, general_content)
172172- | `Param (x1, x2) -> C ("`Param", (x1, x2), Pair (string, general_content))
173173- | `Raise (x1, x2) ->
172172+ | `Deprecated x -> C ("`Deprecated", x, nestable_elements)
173173+ | `Param (x1, x2) -> C ("`Param", (x1, x2), Pair (string, nestable_elements))
174174+ | `Raise (`Code_span x1, x2) ->
175175+ C ("`Raise", (x1, x2), Pair (string, nestable_elements))
176176+ | `Raise (`Reference (x1, x2), x3) ->
174177 C
175178 ( "`Raise",
176176- ((x1 :> general_inline_element), x2),
177177- Pair (inline_element, general_content) )
178178- | `Return x -> C ("`Return", x, general_content)
179179+ (x1, x2, x3),
180180+ Triple (reference, link_content, nestable_elements) )
181181+ | `Return x -> C ("`Return", x, nestable_elements)
179182 | `See (x1, x2, x3) ->
180180- C ("`See", (x1, x2, x3), Triple (url_kind, string, general_content))
183183+ C ("`See", (x1, x2, x3), Triple (url_kind, string, nestable_elements))
181184 | `Since x -> C ("`Since", x, string)
182182- | `Before (x1, x2) -> C ("`Before", (x1, x2), Pair (string, general_content))
185185+ | `Before (x1, x2) ->
186186+ C ("`Before", (x1, x2), Pair (string, nestable_elements))
183187 | `Version x -> C ("`Version", x, string)
184184- | `Alert (x1, x2) -> C ("`Alert", (x1, x2), Pair (string, Option string)))
188188+ | `Alert (x1, x2) -> C ("`Alert", (x1, x2), Pair (string, Option string))
189189+ | `Custom (x1, x2) -> C ("`" ^ x1, x2, nestable_elements))
185190186186-and general_content : general_docs t =
187187- List (Indirect (ignore_loc, block_element))
191191+let elements : elements t = List (Indirect (ignore_loc, block_element))
188192189189-let elements : elements t =
190190- Indirect ((fun x -> (x :> general_docs)), general_content)
191193let docs =
192194 Record
193195 [
···198200let docs_or_stop : docs_or_stop t =
199201 Variant (function `Docs x -> C ("`Docs", x, docs) | `Stop -> C0 "`Stop")
200202201201-let inline_element : inline_element Location_.with_location list Type_desc.t =
202202- List
203203- (Indirect
204204- ( (fun x ->
205205- let x :> general_inline_element Location_.with_location = x in
206206- ignore_loc x),
207207- inline_element ))
203203+let inline_elements : inline_element with_location list t =
204204+ List (Indirect (ignore_loc, inline_element))
+1-1
src/model_desc/comment_desc.mli
···11open Odoc_model
22open Odoc_model.Comment
3344-val inline_element : inline_element Location_.with_location list Type_desc.t
44+val inline_elements : inline_element Location_.with_location list Type_desc.t
5566val elements : elements Type_desc.t
77
+1-1
src/model_desc/lang_desc.ml
···722722 (fun t ->
723723 (t.short_title
724724 :> Comment.inline_element Location_.with_location list option)),
725725- Option Comment_desc.inline_element );
725725+ Option Comment_desc.inline_elements );
726726 F
727727 ( "toc_status",
728728 (fun t ->
···99open Odoc_odoc
1010open Cmdliner
11111212+(* Load all installed extensions at startup *)
1313+let () = Sites.Plugins.Extensions.load_all ()
1414+1215let convert_syntax : Odoc_document.Renderer.syntax Arg.conv =
1316 let syntax_parser str =
1417 match str with
···17611764let section_legacy = "COMMANDS: Legacy pipeline"
17621765let section_deprecated = "COMMANDS: Deprecated"
1763176617671767+module Extensions = struct
17681768+ let print_option_doc opt =
17691769+ let default = match opt.Odoc_extension_api.opt_default with
17701770+ | Some d -> Printf.sprintf " (default: %s)" d
17711771+ | None -> ""
17721772+ in
17731773+ Printf.printf " %s: %s%s\n%!" opt.opt_name opt.opt_description default
17741774+17751775+ let print_extension_info info =
17761776+ let open Odoc_extension_api in
17771777+ let kind_str = match info.info_kind with
17781778+ | `Tag -> Printf.sprintf "@%s" info.info_prefix
17791779+ | `Code_block -> Printf.sprintf "{@%s[...]}" info.info_prefix
17801780+ in
17811781+ Printf.printf "\n %s\n%!" kind_str;
17821782+ Printf.printf " %s\n%!" info.info_description;
17831783+ if info.info_options <> [] then begin
17841784+ Printf.printf " Options:\n%!";
17851785+ List.iter ~f:print_option_doc info.info_options
17861786+ end;
17871787+ match info.info_example with
17881788+ | Some ex -> Printf.printf " Example:\n %s\n%!" ex
17891789+ | None -> ()
17901790+17911791+ let run () =
17921792+ let tag_prefixes = Odoc_extension_api.Registry.list_prefixes () in
17931793+ let code_block_prefixes = Odoc_extension_api.Registry.list_code_block_prefixes () in
17941794+ let infos = Odoc_extension_api.Registry.list_extension_infos () in
17951795+ match tag_prefixes, code_block_prefixes with
17961796+ | [], [] ->
17971797+ Printf.printf "No extensions installed.\n%!";
17981798+ Printf.printf "Extensions can be installed as opam packages that register with odoc.\n%!"
17991799+ | _ ->
18001800+ Printf.printf "Installed extensions:\n%!";
18011801+ if infos <> [] then
18021802+ (* Show detailed info for extensions that registered documentation *)
18031803+ List.iter ~f:print_extension_info infos
18041804+ else begin
18051805+ (* Fallback to simple list if no documentation registered *)
18061806+ if tag_prefixes <> [] then begin
18071807+ Printf.printf " Tag handlers:\n%!";
18081808+ List.iter ~f:(fun prefix -> Printf.printf " @%s\n%!" prefix) tag_prefixes
18091809+ end;
18101810+ if code_block_prefixes <> [] then begin
18111811+ Printf.printf " Code block handlers:\n%!";
18121812+ List.iter ~f:(fun prefix -> Printf.printf " {@%s[...]}\n%!" prefix) code_block_prefixes
18131813+ end
18141814+ end
18151815+18161816+ let cmd = Term.(const run $ const ())
18171817+ let info ~docs = Cmd.info "extensions" ~docs ~doc:"List installed odoc extensions"
18181818+end
18191819+17641820(** Sections in the order they should appear. *)
17651821let main_page_sections =
17661822 [
···18131869 Depends.Odoc_html.(cmd, info ~docs:section_deprecated);
18141870 Classify.(cmd, info ~docs:section_pipeline);
18151871 Extract_code.(cmd, info ~docs:section_pipeline);
18721872+ Extensions.(cmd, info ~docs:section_support);
18161873 ]
18171874 in
18181875 let main =
···5252let render_document renderer ~sidebar ~output:root_dir ~extra_suffix ~extra doc
5353 =
5454 let pages = renderer.Renderer.render extra sidebar doc in
5555- Renderer.traverse pages ~f:(fun filename content ->
5555+ Renderer.traverse pages ~f:(fun filename content assets ->
5656 let filename = prepare ~extra_suffix ~output_dir:root_dir filename in
5757+ (* Write assets to the same directory as the HTML file *)
5858+ let asset_dir = Fpath.parent filename in
5959+ List.iter (fun (asset : Odoc_extension_registry.asset) ->
6060+ let asset_path = Fpath.(asset_dir / asset.asset_filename) in
6161+ Io_utils.with_open_out_bin (Fs.File.to_string asset_path) @@ fun oc ->
6262+ output_bytes oc asset.asset_content
6363+ ) assets;
6464+ (* Write the HTML content *)
5765 Io_utils.with_formatter_out (Fs.File.to_string filename) @@ fun fmt ->
5866 Format.fprintf fmt "%t@?" content)
5967···131139 in
132140 doc >>= fun doc ->
133141 let pages = renderer.Renderer.render extra None doc in
134134- Renderer.traverse pages ~f:(fun filename _content ->
142142+ Renderer.traverse pages ~f:(fun filename _content _assets ->
135143 let filename = Fpath.normalize @@ Fs.File.append root_dir filename in
136144 Format.printf "%a\n" Fpath.pp filename);
137145 Ok ()
···146154 List.iter
147155 (fun doc ->
148156 let pages = renderer.Renderer.render extra None doc in
149149- Renderer.traverse pages ~f:(fun filename _content ->
157157+ Renderer.traverse pages ~f:(fun filename _content _assets ->
150158 let filename =
151159 Fpath.normalize @@ Fs.File.append root_dir filename
152160 in
+8-1
src/odoc/support_files.ml
···1414 let name = Fs.File.create ~directory:output_directory ~name in
1515 f name content
1616 in
1717+ (* Built-in support files *)
1718 let files = Odoc_html_support_files.file_list in
1819 List.iter
1920 (fun f ->
2021 match Odoc_html_support_files.read f with
2122 | Some content when should_include ~without_theme f -> file f content
2223 | _ -> ())
2323- files
2424+ files;
2525+ (* Extension support files *)
2626+ let extension_files = Odoc_extension_registry.list_support_files () in
2727+ List.iter
2828+ (fun (ext_file : Odoc_extension_registry.support_file) ->
2929+ file ext_file.filename ext_file.content)
3030+ extension_files
24312532let write =
2633 iter_files (fun name content ->
+1-1
src/parser/ast.ml
···9494 | `Toc_status of nestable_block_element with_location list
9595 | `Order_category of nestable_block_element with_location list
9696 | `Short_title of nestable_block_element with_location list ]
9797-9897(** Internal tags are used to exercise fine control over the output of odoc.
9998 They are never rendered in the output *)
10099···103102 | `Deprecated of nestable_block_element with_location list
104103 | `Param of string * nestable_block_element with_location list
105104 | `Raise of string * nestable_block_element with_location list
105105+ | `Custom of string * nestable_block_element with_location list
106106 | `Return of nestable_block_element with_location list
107107 | `See of
108108 [ `Url | `File | `Document ]
···11+<!DOCTYPE html>
22+<html xmlns="http://www.w3.org/1999/xhtml"><head><title>test_code_blocks (test.test_code_blocks)</title><meta charset="utf-8"/><link rel="stylesheet" href="../odoc.css"/><meta name="generator" content="odoc %%VERSION%%"/><meta name="viewport" content="width=device-width,initial-scale=1.0"/><script src="../highlight.pack.js"></script><script>hljs.initHighlightingOnLoad();</script></head><body class="odoc"><nav class="odoc-nav"><a href="index.html">Up</a> – <a href="../index.html">Index</a> » <a href="index.html">test</a> » test_code_blocks</nav><header class="odoc-preamble"><h1 id="test-code-block-handlers"><a href="#test-code-block-handlers" class="anchor"></a>Test Code Block Handlers</h1><p>This page tests code block metadata preservation and handler infrastructure.</p></header><div class="odoc-tocs"><nav class="odoc-toc odoc-local-toc"><ul><li><a href="#basic-code-block">Basic Code Block</a></li><li><a href="#code-block-with-metadata">Code Block with Metadata</a></li><li><a href="#code-block-with-bare-tags">Code Block with Bare Tags</a></li><li><a href="#code-block-with-mixed-tags">Code Block with Mixed Tags</a></li><li><a href="#custom-language-handler">Custom Language Handler</a></li></ul></nav></div><div class="odoc-content"><h2 id="basic-code-block"><a href="#basic-code-block" class="anchor"></a>Basic Code Block</h2><p>A simple code block with just a language tag:</p><div><pre class="language-ocaml"><code>let x = 1</code></pre></div><h2 id="code-block-with-metadata"><a href="#code-block-with-metadata" class="anchor"></a>Code Block with Metadata</h2><p>A code block with key=value bindings:</p><div><pre class="language-ocaml"><code>let y = 2</code></pre></div><h2 id="code-block-with-bare-tags"><a href="#code-block-with-bare-tags" class="anchor"></a>Code Block with Bare Tags</h2><p>A code block with bare tags (boolean flags):</p><div><pre class="language-ocaml"><code>let z = 3</code></pre></div><h2 id="code-block-with-mixed-tags"><a href="#code-block-with-mixed-tags" class="anchor"></a>Code Block with Mixed Tags</h2><p>A code block with both bindings and bare tags:</p><div><pre class="language-python"><code>def hello():
33+ print("Hello")</code></pre></div><h2 id="custom-language-handler"><a href="#custom-language-handler" class="anchor"></a>Custom Language Handler</h2><p>This would be handled by a custom handler if registered:</p><div><pre class="language-dot"><code>digraph G {
44+ a -> b -> c;
55+ b -> d;
66+}</code></pre></div><div><pre class="language-mermaid"><code>sequenceDiagram
77+ Alice->>Bob: Hello
88+ Bob-->>Alice: Hi</code></pre></div></div></body></html>
+72
test/integration/code_block_handlers.t/run.t
···11+Test code block metadata preservation and handler infrastructure.
22+33+First, compile the test page:
44+55+ $ odoc compile --package test test_code_blocks.mld
66+77+Link:
88+99+ $ odoc link -I . page-test_code_blocks.odoc
1010+1111+Generate HTML:
1212+1313+ $ odoc html-generate -o html page-test_code_blocks.odocl
1414+1515+Check the HTML output exists:
1616+1717+ $ test -f html/test/test_code_blocks.html && echo "HTML generated"
1818+ HTML generated
1919+2020+Verify code blocks are rendered with language classes.
2121+The language-* class should be present for each code block:
2222+2323+ $ grep -o 'class="[^"]*language-[^"]*"' html/test/test_code_blocks.html | sort | uniq
2424+ class="language-dot"
2525+ class="language-mermaid"
2626+ class="language-msc"
2727+ class="language-ocaml"
2828+ class="language-python"
2929+3030+Verify the code content is preserved in the output:
3131+3232+ $ grep -q "let x = 1" html/test/test_code_blocks.html && echo "ocaml code preserved"
3333+ ocaml code preserved
3434+3535+ $ grep -q "let y = 2" html/test/test_code_blocks.html && echo "ocaml with metadata preserved"
3636+ ocaml with metadata preserved
3737+3838+ $ grep -q "digraph G" html/test/test_code_blocks.html && echo "dot code preserved"
3939+ dot code preserved
4040+4141+ $ grep -q "sequenceDiagram" html/test/test_code_blocks.html && echo "mermaid code preserved"
4242+ mermaid code preserved
4343+4444+ $ grep -q "msc {" html/test/test_code_blocks.html && echo "msc code preserved"
4545+ msc code preserved
4646+4747+Verify bare tags don't break rendering (skip, noeval):
4848+4949+ $ grep -q "let z = 3" html/test/test_code_blocks.html && echo "code with bare tags preserved"
5050+ code with bare tags preserved
5151+5252+Verify bindings don't break rendering (version=5.0):
5353+5454+ $ grep -q "def hello" html/test/test_code_blocks.html && echo "python code preserved"
5555+ python code preserved
5656+5757+Verify format option is accepted (format=png, format=svg):
5858+5959+ $ grep -q "digraph Dependencies" html/test/test_code_blocks.html && echo "dot with format=png preserved"
6060+ dot with format=png preserved
6161+6262+ $ grep -q "digraph Circular" html/test/test_code_blocks.html && echo "dot with format=svg preserved"
6363+ dot with format=svg preserved
6464+6565+ $ grep -q "pie title Pets" html/test/test_code_blocks.html && echo "mermaid with format=png preserved"
6666+ mermaid with format=png preserved
6767+6868+Test the odoc extensions command works:
6969+7070+ $ odoc extensions | head -2
7171+ No extensions installed.
7272+ Extensions can be installed as opam packages that register with odoc.
···11+(** Test code block handler registration and invocation.
22+ This module registers a test handler and provides functions to verify it works. *)
33+44+module Block = Odoc_document.Types.Block
55+66+(** A simple handler that transforms dot code blocks into a placeholder *)
77+module Dot_handler : Odoc_extension_api.Code_Block_Extension = struct
88+ let prefix = "dot"
99+1010+ let to_document meta content =
1111+ (* Extract metadata *)
1212+ let width = Odoc_extension_api.get_binding "width" meta.tags in
1313+ let height = Odoc_extension_api.get_binding "height" meta.tags in
1414+ let format = Odoc_extension_api.get_binding "format" meta.tags in
1515+1616+ (* Create a placeholder block showing we processed it *)
1717+ let info = Printf.sprintf "[DOT HANDLER: lang=%s, width=%s, height=%s, format=%s, content_len=%d]"
1818+ meta.language
1919+ (Option.value ~default:"none" width)
2020+ (Option.value ~default:"none" height)
2121+ (Option.value ~default:"png" format)
2222+ (String.length content)
2323+ in
2424+ let inline = Odoc_document.Types.Inline.[{
2525+ attr = ["dot-placeholder"];
2626+ desc = Text info
2727+ }] in
2828+ let block = Block.[{
2929+ attr = ["dot-output"];
3030+ desc = Paragraph inline
3131+ }] in
3232+ Some (Odoc_extension_api.simple_output block)
3333+end
3434+3535+(** A handler for mermaid diagrams *)
3636+module Mermaid_handler : Odoc_extension_api.Code_Block_Extension = struct
3737+ let prefix = "mermaid"
3838+3939+ let to_document meta content =
4040+ let theme = Odoc_extension_api.get_binding "theme" meta.tags in
4141+ let info = Printf.sprintf "[MERMAID HANDLER: theme=%s, content_len=%d]"
4242+ (Option.value ~default:"default" theme)
4343+ (String.length content)
4444+ in
4545+ let inline = Odoc_document.Types.Inline.[{
4646+ attr = ["mermaid-placeholder"];
4747+ desc = Text info
4848+ }] in
4949+ let block = Block.[{
5050+ attr = ["mermaid-output"];
5151+ desc = Paragraph inline
5252+ }] in
5353+ Some (Odoc_extension_api.simple_output block)
5454+end
5555+5656+(** A handler that declines to process (returns None) *)
5757+module Declining_handler : Odoc_extension_api.Code_Block_Extension = struct
5858+ let prefix = "decline"
5959+6060+ let to_document _meta _content = None
6161+end
6262+6363+let () =
6464+ (* Register handlers *)
6565+ Odoc_extension_api.Registry.register_code_block (module Dot_handler);
6666+ Odoc_extension_api.Registry.register_code_block (module Mermaid_handler);
6767+ Odoc_extension_api.Registry.register_code_block (module Declining_handler);
6868+6969+ (* Print registered prefixes *)
7070+ print_endline "Registered code block handlers:";
7171+ List.iter (fun p -> Printf.printf " - %s\n" p)
7272+ (Odoc_extension_api.Registry.list_code_block_prefixes ())
-66
test/integration/compile.t/run.t
···4455 $ ocamlc -bin-annot -c ast.mli
66 $ odoc compile --package foo ast.cmti
77- File "ast.mli", line 1, characters 4-17:
88- Warning: Unknown tag '@TxtAttribute'.
99- File "ast.mli", line 4, characters 4-21:
1010- Warning: Unknown tag '@ValueDeclaration'.
1111- File "ast.mli", line 6, characters 4-21:
1212- Warning: Unknown tag '@ValueDeclaration'.
1313- File "ast.mli", line 8, characters 4-21:
1414- Warning: Unknown tag '@ValueDeclaration'.
1515- File "ast.mli", line 11, characters 4-20:
1616- Warning: Unknown tag '@TypeDeclaration'.
1717- File "ast.mli", line 13, characters 4-20:
1818- Warning: Unknown tag '@TypeDeclaration'.
1919- File "ast.mli", line 14, characters 16-39:
2020- Warning: Unknown tag '@ConstructorDeclaration'.
2121- File "ast.mli", line 15, characters 16-39:
2222- Warning: Unknown tag '@ConstructorDeclaration'.
2323- File "ast.mli", line 22, characters 4-20:
2424- Warning: Unknown tag '@TypeDeclaration'.
2525- File "ast.mli", line 24, characters 15-32:
2626- Warning: Unknown tag '@LabelDeclaration'.
2727- File "ast.mli", line 25, characters 16-33:
2828- Warning: Unknown tag '@LabelDeclaration'.
2929- File "ast.mli", line 29, characters 4-20:
3030- Warning: Unknown tag '@TypeDeclaration'.
3131- File "ast.mli", line 31, characters 4-18:
3232- Warning: Unknown tag '@TypeExtension'.
3333- File "ast.mli", line 32, characters 17-27:
3434- Warning: Unknown tag '@Extension'.
3535- File "ast.mli", line 33, characters 17-27:
3636- Warning: Unknown tag '@Extension'.
3737- File "ast.mli", line 35, characters 4-26:
3838- Warning: Unknown tag '@ModuleTypeDeclaration'.
3939- File "ast.mli", line 37, characters 6-19:
4040- Warning: Unknown tag '@TxtAttribute'.
4141- File "ast.mli", line 40, characters 6-22:
4242- Warning: Unknown tag '@TypeDeclaration'.
4343- File "ast.mli", line 43, characters 4-22:
4444- Warning: Unknown tag '@ModuleDeclaration'.
4545- File "ast.mli", line 45, characters 6-19:
4646- Warning: Unknown tag '@TxtAttribute'.
4747- File "ast.mli", line 47, characters 6-24:
4848- Warning: Unknown tag '@ModuleDeclaration'.
4949- File "ast.mli", line 49, characters 8-21:
5050- Warning: Unknown tag '@TxtAttribute'.
5151- File "ast.mli", line 52, characters 8-24:
5252- Warning: Unknown tag '@TypeDeclaration'.
5353- File "ast.mli", line 57, characters 4-14:
5454- Warning: Unknown tag '@Exception'.
5555- File "ast.mli", line 60, characters 4-11:
5656- Warning: Unknown tag '@Hidden'.
5757- File "ast.mli", line 63, characters 4-23:
5858- Warning: Unknown tag '@IncludeDescription'.
5959- File "ast.mli", line 68, characters 6-22:
6060- Warning: Unknown tag '@TypeDeclaration'.
6161- File "ast.mli", line 65, characters 6-19:
6262- Warning: Unknown tag '@TxtAttribute'.
6363- File "ast.mli", line 68, characters 6-22:
6464- Warning: Unknown tag '@TypeDeclaration'.
6565- File "ast.mli", line 71, characters 4-10:
6666- Warning: Unknown tag '@Class'.
6767- File "ast.mli", line 74, characters 6-13:
6868- Warning: Unknown tag '@Method'.
697708Test different parsing errors.
719···10442 Warning: Identifier in reference should not be empty.
10543 File "parser_errors.mli", line 40, characters 4-8:
10644 Warning: '@see' should be followed by <url>, 'file', or "document title".
107107- File "parser_errors.mli", line 43, characters 4-15:
108108- Warning: Unknown tag '@UnknownTag'.
10945 File "parser_errors.mli", line 46, characters 4-5:
11046 Warning: Unpaired '}' (end of markup).
11147 Suggestion: try '\}'.
···15591 Error: Identifier in reference should not be empty.
15692 File "parser_errors.mli", line 40, characters 4-8:
15793 Error: '@see' should be followed by <url>, 'file', or "document title".
158158- File "parser_errors.mli", line 43, characters 4-15:
159159- Error: Unknown tag '@UnknownTag'.
16094 File "parser_errors.mli", line 46, characters 4-5:
16195 Error: Unpaired '}' (end of markup).
16296 Suggestion: try '\}'.
+55
test/integration/extension_plugins.t/run.t
···11+Test the extension plugin system.
22+33+This tests:
44+1. Custom tags compile without error (graceful fallback)
55+2. The 'odoc extensions' command works
66+3. Custom tags are rendered in HTML output with default handling
77+4. Support files mechanism works
88+99+First, compile the test module with custom tags:
1010+1111+ $ ocamlc -bin-annot -c test_extension.ml
1212+1313+Compile with odoc - custom tags should work without errors:
1414+1515+ $ odoc compile --package test test_extension.cmt
1616+1717+Link the compiled unit:
1818+1919+ $ odoc link -I . test_extension.odoc
2020+2121+Generate HTML output:
2222+2323+ $ odoc html-generate -o html test_extension.odocl
2424+2525+Test the 'odoc extensions' command.
2626+The output depends on what extensions are installed:
2727+2828+ $ odoc extensions | head -1
2929+ No extensions installed.
3030+3131+Check that tag content is preserved in the output.
3232+3333+The custom.note tag should be rendered (either by extension or default):
3434+3535+ $ grep -q "This is a custom note tag" html/test/Test_extension/index.html && echo "custom.note content found"
3636+ custom.note content found
3737+3838+The mytag tags should be rendered:
3939+4040+ $ grep -q "Some custom content here" html/test/Test_extension/index.html && echo "mytag content found"
4141+ mytag content found
4242+4343+The admonition.warning content should be present:
4444+4545+ $ grep -q "This operation may fail" html/test/Test_extension/index.html && echo "admonition content found"
4646+ admonition content found
4747+4848+Test the support-files command works:
4949+5050+ $ odoc support-files -o support
5151+ $ test -d support && echo "support directory created"
5252+ support directory created
5353+5454+ $ test -f support/odoc.css && echo "odoc.css present"
5555+ odoc.css present
···11+(** Test module with custom tags.
22+33+ This module uses custom tags to test the extension system.
44+55+ @custom.note This is a custom note tag.
66+*)
77+88+(** A function with custom documentation tags.
99+1010+ @rfc 9110
1111+ @admonition.warning This operation may fail.
1212+*)
1313+let example_function () = ()
1414+1515+(** Another function.
1616+1717+ @mytag Some custom content here.
1818+ @mytag.variant A variant of mytag.
1919+*)
2020+let another_function x = x + 1
···952952 let prefix =
953953 test "@authorfoo";
954954 [%expect
955955- {|
956956- {"value":[{"`Paragraph":[{"`Word":"@authorfoo"}]}],"warnings":["File \"f.ml\", line 1, characters 0-10:\nUnknown tag '@authorfoo'.","File \"f.ml.mld\":\nPages (.mld files) should start with a heading."]} |}]
955955+ {| {"value":[{"`Tag":{"`authorfoo":[]}}],"warnings":["File \"f.ml.mld\":\nPages (.mld files) should start with a heading."]} |}]
957956958957 let not_allowed =
959958 test ~tags_allowed:false "@author Foo bar";