this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge commit '4bc4adc655856b59141d848e480399c5c46253ed'

+2876 -416
+514
custom_tags.md
··· 1 + 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. 2 + 3 + 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: 4 + 5 + ``` 6 + @example This is an example of foo bar. 7 + It's a multi-line thing that ends up in an 8 + outlined box in the HTML 9 + ``` 10 + 11 + Or we might have something where we can resolve references: 12 + 13 + ``` 14 + @handles This handles the {!Foo} exception. 15 + ``` 16 + 17 + Now the way to handle these is to write an odoc extension somehow 18 + where we'd write code that uses the odoc APIs to handle the extensions. These pieces of code would be called during the link 19 + and HTML generation phases of odoc. 20 + 21 + We need to come up with some mechanisms to make this happen. 22 + 23 + Firstly, how do we tell odoc about this? Do we use Dynlink to 24 + load in the new handlers, or do we recompile a new odoc binary, linking in the new handlers? 25 + 26 + Secondly, how do we tell `dune` that this needs to be done? 27 + 28 + 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 29 + file. 30 + 31 + ## Design Decisions 32 + 33 + ### Q1: Dynamic plugins using dune's sites mechanism 34 + 35 + Extensions are OCaml libraries that are dynamically loaded at runtime using 36 + dune's plugins/sites mechanism (`dune-site`). This approach: 37 + 38 + - **No custom odoc binary**: Extensions are loaded by the standard odoc at runtime 39 + - **Independent installation**: Extensions are installed as separate opam packages 40 + - **Automatic discovery**: odoc discovers installed extensions via the sites mechanism 41 + - **Ecosystem-friendly**: Follows established dune patterns for extensibility 42 + 43 + #### How it works 44 + 45 + 1. **odoc declares a plugin site** in its `dune-project`: 46 + 47 + ```lisp 48 + (lang dune 3.21) 49 + (using dune_site 0.1) 50 + (name odoc) 51 + 52 + (package 53 + (name odoc) 54 + (sites (lib extensions))) ; Extensions are installed here 55 + ``` 56 + 57 + 2. **odoc uses `generate_sites_module`** to discover and load plugins: 58 + 59 + ```lisp 60 + ; In odoc's dune file 61 + (executable 62 + (name odoc_main) 63 + (libraries odoc_core dune-site dune-site.plugins) 64 + (modules odoc_main sites)) 65 + 66 + (generate_sites_module 67 + (module sites) 68 + (plugins (odoc extensions))) 69 + ``` 70 + 71 + 3. **Extension packages declare themselves as plugins**: 72 + 73 + ```lisp 74 + ; In odoc-rfc-extension/dune-project 75 + (lang dune 3.21) 76 + (using dune_site 0.1) 77 + (name odoc-rfc-extension) 78 + 79 + (package (name odoc-rfc-extension)) 80 + ``` 81 + 82 + ```lisp 83 + ; In odoc-rfc-extension/dune 84 + (library 85 + (public_name odoc-rfc-extension.impl) 86 + (name odoc_rfc_impl) 87 + (libraries odoc.extension_api)) 88 + 89 + (plugin 90 + (name odoc-rfc-extension) 91 + (libraries odoc-rfc-extension.impl) 92 + (site (odoc extensions))) 93 + ``` 94 + 95 + 4. **odoc loads all installed extensions** at startup: 96 + 97 + ```ocaml 98 + (* In odoc_main.ml *) 99 + let () = Sites.Plugins.Extensions.load_all () 100 + (* Extensions register themselves during load *) 101 + ``` 102 + 103 + #### ABI compatibility 104 + 105 + The primary concern with dynamic loading is ABI compatibility - plugins must be 106 + compiled with the same OCaml version and compatible compiler flags. This is 107 + mitigated by: 108 + 109 + - **opam's OCaml version constraints**: Extension packages depend on specific 110 + OCaml versions, so opam ensures compatibility 111 + - **Rebuild on OCaml upgrade**: When the OCaml compiler is upgraded, all 112 + packages (including extensions) are rebuilt 113 + - **Clear error messages**: If loading fails due to ABI mismatch, odoc reports 114 + a clear error directing users to rebuild the extension 115 + 116 + ### Q2: Extension registration pattern 117 + 118 + Extensions register themselves with odoc's extension registry when loaded. 119 + This is the standard dune plugin pattern: 120 + 121 + ```ocaml 122 + (* In odoc.extension_api *) 123 + module Registry : sig 124 + val register : (module Odoc_tag_extension) -> unit 125 + val find : string -> (module Odoc_tag_extension) option 126 + val all : unit -> (module Odoc_tag_extension) list 127 + end 128 + 129 + (* In odoc_rfc_impl.ml - executed when plugin loads *) 130 + let () = 131 + Odoc.Extension_api.Registry.register (module Rfc_extension) 132 + ``` 133 + 134 + ### Q3: Declaration in dune-project and opam 135 + 136 + Users declare which extensions they need in their `dune-project`. This serves 137 + two purposes: 138 + 139 + 1. **Build-time dependency**: Ensures the extension is available when building docs 140 + 2. **CI solver hint**: Allows ocaml.org doc CI to know which extensions to install 141 + 142 + ```lisp 143 + (package 144 + (name mypkg) 145 + (depends 146 + (odoc (>= 3.0)) 147 + (odoc-rfc-extension (>= 1.0)) 148 + (odoc-graphviz-extension (>= 1.0)))) 149 + ``` 150 + 151 + Since extensions are regular opam packages with `(plugin ...)` stanzas, they 152 + appear as normal dependencies. The CI solver simply installs all dependencies, 153 + which includes the extensions. 154 + 155 + For explicit documentation about which extensions a package uses, an optional 156 + `x-odoc-extensions` field can be added: 157 + 158 + ``` 159 + x-odoc-extensions: ["odoc-rfc-extension" "odoc-graphviz-extension"] 160 + ``` 161 + 162 + This is informational only - the actual dependency resolution uses the 163 + standard `depends` field. 164 + 165 + ### Q4: Fallback for missing extensions 166 + 167 + When odoc encounters a custom tag but the extension is not installed: 168 + 169 + 1. **Warning**: "Unknown tag @rfc - is odoc-rfc-extension installed?" 170 + 2. **Graceful degradation**: The tag content is rendered as a blockquote with 171 + a note about the missing extension 172 + 3. **No build failure**: Documentation generation continues 173 + 174 + This allows documentation to be built even if extensions are missing, which is 175 + important for: 176 + - Quick local builds without all extensions 177 + - CI environments that don't have all extensions configured 178 + - Viewing older docs where extensions may have changed 179 + 180 + ## Extension Interface 181 + 182 + Extensions are OCaml modules implementing the `Odoc_tag_extension` signature. 183 + Each extension claims a prefix and handles all tags starting with that prefix: 184 + 185 + - `@rfc` → rfc extension 186 + - `@rfc.section` → rfc extension 187 + - `@callout` → callout extension 188 + - `@callout.box` → callout extension 189 + 190 + ### Extension Output 191 + 192 + Extensions return content that can be rendered by any backend, with optional 193 + backend-specific overrides for cases where different output is needed: 194 + 195 + ```ocaml 196 + (** Resources that can be injected into the page (HTML only) *) 197 + type resource = 198 + | Js_url of string (** External JavaScript: <script src="..."> *) 199 + | Css_url of string (** External CSS: <link rel="stylesheet" href="..."> *) 200 + | Js_inline of string (** Inline JavaScript: <script>...</script> *) 201 + | Css_inline of string (** Inline CSS: <style>...</style> *) 202 + 203 + (** Output from the document phase *) 204 + type extension_output = { 205 + content : Odoc_document.Types.Block.t; 206 + (** Universal content - used by all backends unless overridden *) 207 + 208 + overrides : (string * string) list; 209 + (** Backend-specific raw content overrides. 210 + E.g., [("html", "<div>...</div>"); ("markdown", "```dot\n...\n```")] 211 + If present for a backend, used instead of [content]. *) 212 + 213 + resources : resource list; 214 + (** Page-level resources (JS/CSS). Only used by HTML backend. *) 215 + } 216 + ``` 217 + 218 + **Rendering logic:** 219 + 1. Backend checks `overrides` for its name (e.g., "html", "markdown", "latex") 220 + 2. If found, use that raw string directly 221 + 3. Otherwise, render `content` using the standard Document → output pipeline 222 + 4. HTML backend also collects and deduplicates `resources` for page HEAD/BODY 223 + 224 + ### Module Signature 225 + 226 + ```ocaml 227 + module type Odoc_tag_extension = sig 228 + (** The tag prefix this extension handles. 229 + E.g., "callout" handles @callout, @callout.box, @callout.bubble *) 230 + val prefix : string 231 + 232 + (** Link phase: process/validate content, resolve custom references. 233 + Called during odoc link with the linking environment. *) 234 + val link : 235 + tag:string -> 236 + Odoc_xref2.Env.t -> 237 + Odoc_model.Comment.nestable_block_element list -> 238 + Odoc_model.Comment.nestable_block_element list 239 + 240 + (** Document phase: convert tag to document elements for rendering. 241 + Called during document generation. Returns content plus any 242 + page-level resources needed (JS/CSS). *) 243 + val to_document : 244 + tag:string -> 245 + Odoc_model.Comment.nestable_block_element list -> 246 + extension_output 247 + end 248 + 249 + (** Raised when an extension receives a tag variant it doesn't support. 250 + E.g., callout extension receiving @callout.unknown *) 251 + exception Unsupported_tag of string 252 + ``` 253 + 254 + ### Example Extensions 255 + 256 + #### Graphviz (with backend overrides) 257 + 258 + This extension needs different output for HTML vs Markdown: 259 + 260 + ```ocaml 261 + (* odoc_graphviz_extension.ml *) 262 + 263 + let prefix = "dot" 264 + 265 + let link ~tag _env content = content 266 + 267 + let to_document ~tag content = 268 + let dot_source = extract_text content in 269 + { 270 + (* Fallback: just show the source as a code block *) 271 + content = Block.[Source [...]]; 272 + 273 + (* Backend-specific rendering *) 274 + overrides = [ 275 + ("html", Printf.sprintf {|<div class="graphviz">%s</div>|} 276 + (escape_html dot_source)); 277 + ("markdown", Printf.sprintf "```dot\n%s\n```" dot_source); 278 + ]; 279 + 280 + (* HTML needs the renderer script *) 281 + resources = [ 282 + Js_url "https://cdn.jsdelivr.net/npm/@viz-js/viz/lib/viz-standalone.js"; 283 + Js_inline {| 284 + document.querySelectorAll('.graphviz').forEach(async el => { 285 + const viz = await Viz.instance(); 286 + el.innerHTML = viz.renderSVGElement(el.textContent).outerHTML; 287 + }); 288 + |}; 289 + ]; 290 + } 291 + ``` 292 + 293 + #### Callout (universal content) 294 + 295 + Simple extensions can use Document types that work everywhere: 296 + 297 + ```ocaml 298 + (* odoc_callout_extension.ml *) 299 + 300 + let prefix = "callout" 301 + 302 + let link ~tag _env content = content 303 + 304 + let to_document ~tag content = 305 + let block_content = render_content content in 306 + let content = match tag with 307 + | "callout" | "callout.box" -> 308 + (* Returns Block.t with a styled div - works for all backends *) 309 + make_callout_block ~style:`Box block_content 310 + | "callout.bubble" -> 311 + make_callout_block ~style:`Bubble block_content 312 + | _ -> 313 + raise (Unsupported_tag tag) 314 + in 315 + (* No overrides needed - Document types render well everywhere *) 316 + { content; overrides = []; resources = [] } 317 + ``` 318 + 319 + ### Error Handling 320 + 321 + When odoc encounters a custom tag: 322 + 323 + 1. Look up extension by prefix (first component before `.`) 324 + 2. If no extension registered: warning "Unknown tag @foo" 325 + 3. If extension raises `Unsupported_tag`: error "Tag @foo.bar not supported by 'foo' extension" 326 + 4. Extension errors during link/render are reported with source location 327 + 328 + ## Dune Integration 329 + 330 + ### Extension loading in dune's doc rules 331 + 332 + When dune runs `odoc link` or `odoc html-generate`, the extensions are loaded 333 + automatically because: 334 + 335 + 1. odoc is built with `dune-site.plugins` support 336 + 2. The `Sites.Plugins.Extensions.load_all ()` call happens at odoc startup 337 + 3. Any extensions installed in the `odoc/extensions` site are discovered 338 + 339 + No special dune rules are needed - if the extension package is installed, 340 + odoc will find and use it. 341 + 342 + ### Development workflow 343 + 344 + During development (before extensions are installed), extensions can be 345 + loaded by setting environment variables that dune-site respects: 346 + 347 + ```bash 348 + # Point to local extension build 349 + export DUNE_DIR_LOCATIONS="odoc:lib:extensions:_build/default/my-extension" 350 + dune build @doc 351 + ``` 352 + 353 + Alternatively, dune could be enhanced to understand that packages with 354 + `(plugin (site (odoc extensions)))` should have their build directories 355 + added to the site path when building docs. 356 + 357 + ### Complete example: RFC extension package 358 + 359 + Here's the full structure of an RFC extension package: 360 + 361 + ``` 362 + odoc-rfc-extension/ 363 + ├── dune-project 364 + ├── odoc-rfc-extension.opam 365 + ├── src/ 366 + │ ├── dune 367 + │ └── rfc_extension.ml 368 + └── test/ 369 + └── ... 370 + ``` 371 + 372 + **dune-project**: 373 + ```lisp 374 + (lang dune 3.21) 375 + (using dune_site 0.1) 376 + (name odoc-rfc-extension) 377 + (generate_opam_files true) 378 + 379 + (package 380 + (name odoc-rfc-extension) 381 + (synopsis "RFC reference extension for odoc") 382 + (depends 383 + (ocaml (>= 4.14)) 384 + (odoc (>= 3.0)))) 385 + ``` 386 + 387 + **src/dune**: 388 + ```lisp 389 + (library 390 + (public_name odoc-rfc-extension.impl) 391 + (name rfc_extension) 392 + (libraries odoc.extension_api)) 393 + 394 + (plugin 395 + (name odoc-rfc-extension) 396 + (libraries odoc-rfc-extension.impl) 397 + (site (odoc extensions))) 398 + ``` 399 + 400 + **src/rfc_extension.ml**: 401 + ```ocaml 402 + open Odoc_extension_api 403 + 404 + let prefix = "rfc" 405 + 406 + let link ~tag _env content = content 407 + 408 + let to_document ~tag content = 409 + (* Parse "@rfc 9110" or "@rfc 9110 Section 5.5" *) 410 + let rfc_num, section = parse_rfc_reference content in 411 + let url = Printf.sprintf "https://www.rfc-editor.org/rfc/rfc%d" rfc_num in 412 + let url = match section with 413 + | None -> url 414 + | Some s -> url ^ "#" ^ s 415 + in 416 + let link_text = match section with 417 + | None -> Printf.sprintf "RFC %d" rfc_num 418 + | Some s -> Printf.sprintf "RFC %d %s" rfc_num s 419 + in 420 + { 421 + content = Block.[ 422 + Paragraph [Inline.Link { url; text = [Inline.Text link_text] }] 423 + ]; 424 + overrides = []; 425 + resources = []; 426 + } 427 + 428 + (* Register on load *) 429 + let () = Registry.register (module struct 430 + let prefix = prefix 431 + let link = link 432 + let to_document = to_document 433 + end) 434 + ``` 435 + 436 + ## Trade-offs: Dynamic vs Static Linking 437 + 438 + ### Dynamic plugins (recommended) 439 + 440 + **Advantages:** 441 + - No need to rebuild odoc for each project 442 + - Extensions are independent packages with their own release cycles 443 + - Natural fit with opam package management 444 + - Standard dune pattern used by other tools 445 + - Extensions can be added/removed without touching the main project 446 + 447 + **Disadvantages:** 448 + - ABI compatibility requirements (same OCaml version) 449 + - Slightly more complex deployment (multiple packages) 450 + - Runtime discovery adds small startup overhead 451 + - Cross-compilation may be more complex 452 + 453 + ### Static linking (alternative) 454 + 455 + **Advantages:** 456 + - Single binary with all extensions baked in 457 + - No runtime ABI concerns 458 + - Simpler deployment for specialized use cases 459 + - Works in environments where dynlink is unavailable 460 + 461 + **Disadvantages:** 462 + - Requires rebuilding a custom odoc binary 463 + - Extensions tightly coupled to specific odoc version 464 + - More complex build setup 465 + - Doesn't fit well with standard opam workflows 466 + 467 + ### Recommendation 468 + 469 + The dynamic plugin approach using dune-site is recommended as the primary 470 + mechanism because: 471 + 472 + 1. It follows established dune patterns 473 + 2. It integrates naturally with opam 474 + 3. It allows extensions to evolve independently 475 + 4. The ABI concerns are well-handled by opam's dependency resolver 476 + 5. It's the approach used by other OCaml tools with plugin systems 477 + 478 + Static linking could be supported as an advanced option for specific use 479 + cases (embedded systems, specialized deployments), but shouldn't be the 480 + default. 481 + 482 + ## Implementation Plan 483 + 484 + ### Phase 1: Core extension infrastructure 485 + 486 + 1. Add `odoc.extension_api` library with: 487 + - `Odoc_tag_extension` module type 488 + - `Registry` module for extension registration 489 + - `extension_output` type and helpers 490 + 491 + 2. Modify odoc to: 492 + - Add `(sites (lib extensions))` to dune-project 493 + - Add `dune-site` and `dune-site.plugins` dependencies 494 + - Generate sites module and call `load_all ()` at startup 495 + - Hook extension registry into link and html-generate phases 496 + 497 + ### Phase 2: Extension discovery and error handling 498 + 499 + 1. Implement graceful fallback for unknown tags 500 + 2. Add helpful error messages for ABI mismatches 501 + 3. Add `odoc extensions` subcommand to list installed extensions 502 + 503 + ### Phase 3: Example extensions 504 + 505 + 1. Create `odoc-rfc-extension` as a reference implementation 506 + 2. Create `odoc-callout-extension` showing universal content 507 + 3. Create `odoc-graphviz-extension` showing backend overrides 508 + 509 + ### Phase 4: Documentation and ecosystem 510 + 511 + 1. Document extension authoring guide 512 + 2. Work with ocaml.org CI to support extensions 513 + 3. Consider creating an `odoc-extensions` opam repository or tag 514 +
+40
dune-project
··· 1 1 (lang dune 3.18) 2 2 3 + (using dune_site 0.1) 4 + 3 5 (name odoc) 4 6 5 7 (documentation "https://ocaml.org/p/odoc") ··· 29 31 "Paul-Elliot Angl\195\168s d'Auriac <paul-elliot@tarides.com>") 30 32 31 33 (cram enable) 34 + 35 + (package 36 + (name odoc) 37 + (sites (lib extensions))) 38 + 39 + (package (name odoc-parser)) 40 + (package (name odoc-md)) 41 + (package (name odoc-driver)) 42 + (package (name odoc-bench)) 43 + (package (name sherlodoc)) 44 + 45 + ; Example extension packages 46 + (package 47 + (name odoc-dot-extension) 48 + (synopsis "Graphviz/DOT diagram support for odoc documentation") 49 + (description "Renders {@dot[...]} code blocks as SVG diagrams using Graphviz. 50 + Supports width, height, and layout engine options.") 51 + (depends 52 + (ocaml (>= 4.14)) 53 + odoc)) 54 + 55 + (package 56 + (name odoc-mermaid-extension) 57 + (synopsis "Mermaid diagram support for odoc documentation") 58 + (description "Renders {@mermaid[...]} code blocks as interactive diagrams. 59 + Supports flowcharts, sequence diagrams, class diagrams, and more.") 60 + (depends 61 + (ocaml (>= 4.14)) 62 + odoc)) 63 + 64 + (package 65 + (name odoc-msc-extension) 66 + (synopsis "Message Sequence Chart support for odoc documentation") 67 + (description "Renders {@msc[...]} code blocks as message sequence charts. 68 + Uses the MscGen syntax for defining sequence diagrams.") 69 + (depends 70 + (ocaml (>= 4.14)) 71 + odoc)) 32 72 33 73 (using mdx 0.3) 34 74
+292
examples/extensions/dot/src/dot_extension.ml
··· 1 + (** Graphviz/DOT diagram extension for odoc. 2 + 3 + Renders [{@dot[...]}] code blocks as diagrams. By default uses client-side 4 + JavaScript (Viz.js), but can render server-side to PNG/SVG with format option. 5 + 6 + Example: 7 + {[ 8 + {@dot layout=neato[ 9 + digraph G { 10 + a -> b -> c; 11 + b -> d; 12 + } 13 + ]} 14 + ]} 15 + *) 16 + 17 + module Api = Odoc_extension_api 18 + module Block = Odoc_document.Types.Block 19 + module Inline = Odoc_document.Types.Inline 20 + 21 + (** The Viz.js library URL for client-side rendering *) 22 + let viz_js_url = "https://unpkg.com/viz.js@2.1.2/viz.js" 23 + let viz_full_js_url = "https://unpkg.com/viz.js@2.1.2/full.render.js" 24 + 25 + (** Generate a unique ID for each diagram *) 26 + let diagram_counter = ref 0 27 + 28 + let fresh_id () = 29 + incr diagram_counter; 30 + Printf.sprintf "dot-diagram-%d" !diagram_counter 31 + 32 + (** Extract option values *) 33 + let get_layout tags = 34 + Api.get_binding "layout" tags 35 + |> Option.value ~default:"dot" 36 + 37 + let get_format tags = 38 + Api.get_binding "format" tags 39 + 40 + let get_filename tags = 41 + Api.get_binding "filename" tags 42 + 43 + let get_dimensions tags = 44 + let width = Api.get_binding "width" tags in 45 + let height = Api.get_binding "height" tags in 46 + (width, height) 47 + 48 + (** Check if content looks like a complete DOT graph *) 49 + let has_graph_wrapper content = 50 + let trimmed = String.trim content in 51 + String.length trimmed > 0 && 52 + (let starts_with prefix s = 53 + String.length s >= String.length prefix && 54 + String.sub s 0 (String.length prefix) = prefix 55 + in 56 + starts_with "digraph" trimmed || 57 + starts_with "graph" trimmed || 58 + starts_with "strict" trimmed) 59 + 60 + (** Wrap content in a digraph if needed *) 61 + let ensure_graph_wrapper content = 62 + if has_graph_wrapper content then content 63 + else Printf.sprintf "digraph G {\n%s\n}" content 64 + 65 + (** Build inline style string from dimensions *) 66 + let make_style width height = 67 + let parts = [] in 68 + let parts = match width with 69 + | Some w -> Printf.sprintf "width: %s" w :: parts 70 + | None -> parts 71 + in 72 + let parts = match height with 73 + | Some h -> Printf.sprintf "height: %s" h :: parts 74 + | None -> parts 75 + in 76 + match parts with 77 + | [] -> "" 78 + | ps -> String.concat "; " (List.rev ps) 79 + 80 + (** Run the dot command to render to a specific format *) 81 + let run_dot ~layout ~format content = 82 + (* Create temp file for input *) 83 + let tmp_in = Filename.temp_file "odoc_dot_" ".dot" in 84 + let tmp_out = Filename.temp_file "odoc_dot_" ("." ^ format) in 85 + Fun.protect ~finally:(fun () -> 86 + (try Sys.remove tmp_in with _ -> ()); 87 + (try Sys.remove tmp_out with _ -> ()) 88 + ) (fun () -> 89 + (* Write DOT content *) 90 + let oc = open_out tmp_in in 91 + output_string oc content; 92 + close_out oc; 93 + (* Run dot command *) 94 + let cmd = Printf.sprintf "dot -K%s -T%s -o %s %s 2>&1" 95 + layout format (Filename.quote tmp_out) (Filename.quote tmp_in) in 96 + let ic = Unix.open_process_in cmd in 97 + let error_output = Buffer.create 256 in 98 + (try 99 + while true do 100 + Buffer.add_string error_output (input_line ic); 101 + Buffer.add_char error_output '\n' 102 + done 103 + with End_of_file -> ()); 104 + let status = Unix.close_process_in ic in 105 + match status with 106 + | Unix.WEXITED 0 -> 107 + (* Read the output file *) 108 + let ic = open_in_bin tmp_out in 109 + let len = in_channel_length ic in 110 + let data = Bytes.create len in 111 + really_input ic data 0 len; 112 + close_in ic; 113 + Ok data 114 + | _ -> 115 + Error (Buffer.contents error_output) 116 + ) 117 + 118 + (** JavaScript code to render a single diagram (for client-side rendering) *) 119 + let render_script id layout content = 120 + Printf.sprintf {| 121 + (function() { 122 + function renderDot() { 123 + var container = document.getElementById('%s'); 124 + if (!container) return; 125 + 126 + if (typeof Viz === 'undefined') { 127 + container.innerHTML = '<pre style="color: red;">Viz.js not loaded</pre>'; 128 + return; 129 + } 130 + 131 + var viz = new Viz(); 132 + viz.renderSVGElement(%S, { engine: %S }) 133 + .then(function(svg) { 134 + container.innerHTML = ''; 135 + container.appendChild(svg); 136 + }) 137 + .catch(function(error) { 138 + container.innerHTML = '<pre style="color: red;">' + error + '</pre>'; 139 + }); 140 + } 141 + 142 + if (document.readyState === 'loading') { 143 + document.addEventListener('DOMContentLoaded', renderDot); 144 + } else { 145 + renderDot(); 146 + } 147 + })(); 148 + |} id content layout 149 + 150 + module Dot_handler : Api.Code_Block_Extension = struct 151 + let prefix = "dot" 152 + 153 + let to_document meta content = 154 + let id = fresh_id () in 155 + let layout = get_layout meta.Api.tags in 156 + let format = get_format meta.Api.tags in 157 + let filename_opt = get_filename meta.Api.tags in 158 + let (width, height) = get_dimensions meta.Api.tags in 159 + let style = make_style width height in 160 + let style_attr = if style = "" then "" else Printf.sprintf " style=\"%s\"" style in 161 + 162 + (* Auto-wrap in digraph if needed *) 163 + let dot_content = ensure_graph_wrapper content in 164 + 165 + match format with 166 + | Some "png" | Some "svg" -> 167 + (* Server-side rendering *) 168 + let fmt = match format with Some f -> f | None -> "png" in 169 + let base_filename = match filename_opt with 170 + | Some f -> f 171 + | None -> Printf.sprintf "dot-%s.%s" id fmt 172 + in 173 + (match run_dot ~layout ~format:fmt dot_content with 174 + | Ok data -> 175 + let html = Printf.sprintf 176 + {|<div id="%s" class="odoc-dot-diagram"%s><img src="%s" alt="DOT diagram" /></div>|} 177 + id style_attr base_filename 178 + in 179 + let block = Block.[{ 180 + attr = ["odoc-dot"]; 181 + desc = Raw_markup ("html", html) 182 + }] in 183 + Some { 184 + Api.content = block; 185 + overrides = []; 186 + resources = []; 187 + assets = [{ Api.asset_filename = base_filename; asset_content = data }]; 188 + } 189 + | Error err -> 190 + (* Show error message *) 191 + let html = Printf.sprintf 192 + "<div id=\"%s\" class=\"odoc-dot-diagram odoc-dot-error\"><pre style=\"color: red;\">Error rendering DOT diagram (is graphviz installed?):\n%s</pre><pre>%s</pre></div>" 193 + id err content 194 + in 195 + let block = Block.[{ 196 + attr = ["odoc-dot"; "odoc-dot-error"]; 197 + desc = Raw_markup ("html", html) 198 + }] in 199 + Some { 200 + Api.content = block; 201 + overrides = []; 202 + resources = []; 203 + assets = []; 204 + }) 205 + 206 + | Some unknown_format -> 207 + (* Unknown format - show error *) 208 + let html = Printf.sprintf 209 + {|<div class="odoc-dot-error"><pre style="color: red;">Unknown format: %s (supported: png, svg)</pre></div>|} 210 + unknown_format 211 + in 212 + let block = Block.[{ 213 + attr = ["odoc-dot-error"]; 214 + desc = Raw_markup ("html", html) 215 + }] in 216 + Some { 217 + Api.content = block; 218 + overrides = []; 219 + resources = []; 220 + assets = []; 221 + } 222 + 223 + | None -> 224 + (* Default: client-side JavaScript rendering *) 225 + let html = Printf.sprintf 226 + {|<div id="%s" class="odoc-dot-diagram"%s><pre>%s</pre></div>|} 227 + id style_attr content 228 + in 229 + let script = render_script id layout dot_content in 230 + let block = Block.[{ 231 + attr = ["odoc-dot"]; 232 + desc = Raw_markup ("html", html) 233 + }] in 234 + Some { 235 + Api.content = block; 236 + overrides = []; 237 + resources = [ 238 + Api.Js_url viz_js_url; 239 + Api.Js_url viz_full_js_url; 240 + Api.Js_inline script; 241 + ]; 242 + assets = []; 243 + } 244 + end 245 + 246 + (** CSS for dot diagrams *) 247 + let dot_css = {| 248 + .odoc-dot-diagram { 249 + margin: 1em 0; 250 + overflow: auto; 251 + } 252 + 253 + .odoc-dot-diagram svg, 254 + .odoc-dot-diagram img { 255 + max-width: 100%; 256 + height: auto; 257 + } 258 + 259 + .odoc-dot-diagram pre { 260 + background: #f5f5f5; 261 + padding: 1em; 262 + border-radius: 4px; 263 + overflow-x: auto; 264 + } 265 + 266 + .odoc-dot-error pre { 267 + color: #c00; 268 + } 269 + |} 270 + 271 + (** Extension documentation *) 272 + let extension_info : Api.extension_info = { 273 + info_kind = `Code_block; 274 + info_prefix = "dot"; 275 + info_description = "Render Graphviz/DOT diagrams. Uses client-side Viz.js by default, or server-side graphviz with format=png|svg."; 276 + info_options = [ 277 + { opt_name = "format"; opt_description = "Output format: png, svg (server-side), or omit for client-side JS"; opt_default = None }; 278 + { opt_name = "layout"; opt_description = "Graphviz layout engine"; opt_default = Some "dot" }; 279 + { opt_name = "width"; opt_description = "CSS width (e.g., 500px, 100%)"; opt_default = None }; 280 + { opt_name = "height"; opt_description = "CSS height"; opt_default = None }; 281 + { opt_name = "filename"; opt_description = "Output filename for server-side rendering"; opt_default = Some "auto-generated" }; 282 + ]; 283 + info_example = Some "{@dot format=png layout=neato[a -> b -> c]}"; 284 + } 285 + 286 + let () = 287 + Api.Registry.register_code_block (module Dot_handler); 288 + Api.Registry.register_extension_info extension_info; 289 + Api.Registry.register_support_file ~prefix:"dot" { 290 + filename = "extensions/dot.css"; 291 + content = dot_css; 292 + }
+10
examples/extensions/dot/src/dune
··· 1 + (library 2 + (name dot_extension) 3 + (public_name odoc-dot-extension.impl) 4 + (libraries odoc_extension_api odoc_parser unix)) 5 + 6 + (plugin 7 + (name odoc-dot-extension) 8 + (package odoc-dot-extension) 9 + (libraries odoc-dot-extension.impl) 10 + (site (odoc extensions)))
+10
examples/extensions/mermaid/src/dune
··· 1 + (library 2 + (name mermaid_extension) 3 + (public_name odoc-mermaid-extension.impl) 4 + (libraries odoc_extension_api odoc_parser unix)) 5 + 6 + (plugin 7 + (name odoc-mermaid-extension) 8 + (package odoc-mermaid-extension) 9 + (libraries odoc-mermaid-extension.impl) 10 + (site (odoc extensions)))
+266
examples/extensions/mermaid/src/mermaid_extension.ml
··· 1 + (** Mermaid diagram extension for odoc. 2 + 3 + Renders [{@mermaid[...]}] code blocks as diagrams. By default uses client-side 4 + JavaScript, but can render server-side to PNG/SVG with format option 5 + (requires mermaid-cli/mmdc). 6 + 7 + Example: 8 + {[ 9 + {@mermaid theme=forest[ 10 + graph LR 11 + A[Start] --> B{Decision} 12 + B -->|Yes| C[OK] 13 + B -->|No| D[Cancel] 14 + ]} 15 + ]} 16 + *) 17 + 18 + module Api = Odoc_extension_api 19 + module Block = Odoc_document.Types.Block 20 + module Inline = Odoc_document.Types.Inline 21 + 22 + (** Mermaid.js CDN URL *) 23 + let mermaid_js_url = "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js" 24 + 25 + (** Generate a unique ID for each diagram *) 26 + let diagram_counter = ref 0 27 + 28 + let fresh_id () = 29 + incr diagram_counter; 30 + Printf.sprintf "mermaid-diagram-%d" !diagram_counter 31 + 32 + (** Extract option values *) 33 + let get_theme tags = 34 + Api.get_binding "theme" tags 35 + |> Option.value ~default:"default" 36 + 37 + let get_format tags = 38 + Api.get_binding "format" tags 39 + 40 + let get_filename tags = 41 + Api.get_binding "filename" tags 42 + 43 + let get_dimensions tags = 44 + let width = Api.get_binding "width" tags in 45 + let height = Api.get_binding "height" tags in 46 + (width, height) 47 + 48 + (** Build inline style string from dimensions *) 49 + let make_style width height = 50 + let parts = [] in 51 + let parts = match width with 52 + | Some w -> Printf.sprintf "width: %s" w :: parts 53 + | None -> parts 54 + in 55 + let parts = match height with 56 + | Some h -> Printf.sprintf "height: %s" h :: parts 57 + | None -> parts 58 + in 59 + match parts with 60 + | [] -> "" 61 + | ps -> String.concat "; " (List.rev ps) 62 + 63 + (** HTML-escape content for safe embedding *) 64 + let html_escape s = 65 + let buf = Buffer.create (String.length s) in 66 + String.iter (fun c -> 67 + match c with 68 + | '<' -> Buffer.add_string buf "&lt;" 69 + | '>' -> Buffer.add_string buf "&gt;" 70 + | '&' -> Buffer.add_string buf "&amp;" 71 + | '"' -> Buffer.add_string buf "&quot;" 72 + | c -> Buffer.add_char buf c 73 + ) s; 74 + Buffer.contents buf 75 + 76 + (** Puppeteer config for environments that need --no-sandbox *) 77 + let puppeteer_config = {|{"args": ["--no-sandbox", "--disable-setuid-sandbox"]}|} 78 + 79 + (** Run mmdc (mermaid-cli) to render to a specific format *) 80 + let run_mmdc ~theme ~format content = 81 + let tmp_in = Filename.temp_file "odoc_mermaid_" ".mmd" in 82 + let tmp_out = Filename.temp_file "odoc_mermaid_" ("." ^ format) in 83 + let tmp_config = Filename.temp_file "odoc_mermaid_" ".json" in 84 + Fun.protect ~finally:(fun () -> 85 + (try Sys.remove tmp_in with _ -> ()); 86 + (try Sys.remove tmp_out with _ -> ()); 87 + (try Sys.remove tmp_config with _ -> ()) 88 + ) (fun () -> 89 + (* Write Mermaid content *) 90 + let oc = open_out tmp_in in 91 + output_string oc content; 92 + close_out oc; 93 + (* Write puppeteer config for --no-sandbox *) 94 + let oc = open_out tmp_config in 95 + output_string oc puppeteer_config; 96 + close_out oc; 97 + (* Run mmdc command with puppeteer config *) 98 + let cmd = Printf.sprintf "mmdc -i %s -o %s -t %s -b transparent -p %s 2>&1" 99 + (Filename.quote tmp_in) (Filename.quote tmp_out) theme (Filename.quote tmp_config) in 100 + let ic = Unix.open_process_in cmd in 101 + let error_output = Buffer.create 256 in 102 + (try 103 + while true do 104 + Buffer.add_string error_output (input_line ic); 105 + Buffer.add_char error_output '\n' 106 + done 107 + with End_of_file -> ()); 108 + let status = Unix.close_process_in ic in 109 + match status with 110 + | Unix.WEXITED 0 -> 111 + (* Read the output file *) 112 + let ic = open_in_bin tmp_out in 113 + let len = in_channel_length ic in 114 + let data = Bytes.create len in 115 + really_input ic data 0 len; 116 + close_in ic; 117 + Ok data 118 + | _ -> 119 + Error (Buffer.contents error_output) 120 + ) 121 + 122 + module Mermaid_handler : Api.Code_Block_Extension = struct 123 + let prefix = "mermaid" 124 + 125 + let to_document meta content = 126 + let id = fresh_id () in 127 + let theme = get_theme meta.Api.tags in 128 + let format = get_format meta.Api.tags in 129 + let filename_opt = get_filename meta.Api.tags in 130 + let (width, height) = get_dimensions meta.Api.tags in 131 + let style = make_style width height in 132 + let style_attr = if style = "" then "" else Printf.sprintf " style=\"%s\"" style in 133 + 134 + match format with 135 + | Some "png" | Some "svg" -> 136 + (* Server-side rendering with mmdc *) 137 + let fmt = match format with Some f -> f | None -> "png" in 138 + let base_filename = match filename_opt with 139 + | Some f -> f 140 + | None -> Printf.sprintf "mermaid-%s.%s" id fmt 141 + in 142 + (match run_mmdc ~theme ~format:fmt content with 143 + | Ok data -> 144 + let html = Printf.sprintf 145 + {|<div id="%s" class="odoc-mermaid-diagram"%s><img src="%s" alt="Mermaid diagram" /></div>|} 146 + id style_attr base_filename 147 + in 148 + let block = Block.[{ 149 + attr = ["odoc-mermaid"]; 150 + desc = Raw_markup ("html", html) 151 + }] in 152 + Some { 153 + Api.content = block; 154 + overrides = []; 155 + resources = []; 156 + assets = [{ Api.asset_filename = base_filename; asset_content = data }]; 157 + } 158 + | Error err -> 159 + (* Show error message *) 160 + let html = Printf.sprintf 161 + "<div id=\"%s\" class=\"odoc-mermaid-diagram odoc-mermaid-error\"><pre style=\"color: red;\">Error rendering Mermaid diagram (is mmdc installed?):\n%s</pre><pre>%s</pre></div>" 162 + id err (html_escape content) 163 + in 164 + let block = Block.[{ 165 + attr = ["odoc-mermaid"; "odoc-mermaid-error"]; 166 + desc = Raw_markup ("html", html) 167 + }] in 168 + Some { 169 + Api.content = block; 170 + overrides = []; 171 + resources = []; 172 + assets = []; 173 + }) 174 + 175 + | Some unknown_format -> 176 + let html = Printf.sprintf 177 + {|<div class="odoc-mermaid-error"><pre style="color: red;">Unknown format: %s (supported: png, svg)</pre></div>|} 178 + unknown_format 179 + in 180 + let block = Block.[{ 181 + attr = ["odoc-mermaid-error"]; 182 + desc = Raw_markup ("html", html) 183 + }] in 184 + Some { 185 + Api.content = block; 186 + overrides = []; 187 + resources = []; 188 + assets = []; 189 + } 190 + 191 + | None -> 192 + (* Default: client-side JavaScript rendering *) 193 + let html = Printf.sprintf 194 + {|<div id="%s" class="odoc-mermaid-diagram"%s><pre class="mermaid">%s</pre></div>|} 195 + id style_attr (html_escape content) 196 + in 197 + let init_script = Printf.sprintf {| 198 + if (typeof window.mermaidInitialized === 'undefined') { 199 + window.mermaidInitialized = true; 200 + mermaid.initialize({ 201 + startOnLoad: true, 202 + theme: '%s', 203 + securityLevel: 'loose' 204 + }); 205 + } 206 + |} theme 207 + in 208 + let block = Block.[{ 209 + attr = ["odoc-mermaid"]; 210 + desc = Raw_markup ("html", html) 211 + }] in 212 + Some { 213 + Api.content = block; 214 + overrides = []; 215 + resources = [ 216 + Api.Js_url mermaid_js_url; 217 + Api.Js_inline init_script; 218 + ]; 219 + assets = []; 220 + } 221 + end 222 + 223 + (** CSS for mermaid diagrams *) 224 + let mermaid_css = {| 225 + .odoc-mermaid-diagram { 226 + margin: 1em 0; 227 + overflow: auto; 228 + } 229 + 230 + .odoc-mermaid-diagram svg, 231 + .odoc-mermaid-diagram img { 232 + max-width: 100%; 233 + height: auto; 234 + } 235 + 236 + .odoc-mermaid-diagram pre.mermaid { 237 + background: transparent; 238 + } 239 + 240 + .odoc-mermaid-error pre { 241 + color: #c00; 242 + } 243 + |} 244 + 245 + (** Extension documentation *) 246 + let extension_info : Api.extension_info = { 247 + info_kind = `Code_block; 248 + info_prefix = "mermaid"; 249 + info_description = "Render Mermaid diagrams (flowcharts, sequence diagrams, etc.). Uses client-side JS by default, or server-side mmdc with format=png|svg."; 250 + info_options = [ 251 + { opt_name = "format"; opt_description = "Output format: png, svg (requires mmdc), or omit for client-side JS"; opt_default = None }; 252 + { opt_name = "theme"; opt_description = "Mermaid theme (default, forest, dark, neutral)"; opt_default = Some "default" }; 253 + { opt_name = "width"; opt_description = "CSS width (e.g., 500px, 100%)"; opt_default = None }; 254 + { opt_name = "height"; opt_description = "CSS height"; opt_default = None }; 255 + { opt_name = "filename"; opt_description = "Output filename for server-side rendering"; opt_default = Some "auto-generated" }; 256 + ]; 257 + info_example = Some "{@mermaid format=png theme=forest[graph LR; A-->B]}"; 258 + } 259 + 260 + let () = 261 + Api.Registry.register_code_block (module Mermaid_handler); 262 + Api.Registry.register_extension_info extension_info; 263 + Api.Registry.register_support_file ~prefix:"mermaid" { 264 + filename = "extensions/mermaid.css"; 265 + content = mermaid_css; 266 + }
+10
examples/extensions/msc/src/dune
··· 1 + (library 2 + (name msc_extension) 3 + (public_name odoc-msc-extension.impl) 4 + (libraries odoc_extension_api odoc_parser unix)) 5 + 6 + (plugin 7 + (name odoc-msc-extension) 8 + (package odoc-msc-extension) 9 + (libraries odoc-msc-extension.impl) 10 + (site (odoc extensions)))
+272
examples/extensions/msc/src/msc_extension.ml
··· 1 + (** Message Sequence Chart extension for odoc. 2 + 3 + Renders [{@msc[...]}] code blocks as sequence diagrams. By default uses 4 + client-side JavaScript (mscgen-inpage), but can render server-side to 5 + PNG/SVG with format option (requires mscgen). 6 + 7 + Example: 8 + {[ 9 + {@msc format=png width=600px[ 10 + msc { 11 + a, b, c; 12 + a -> b [label="request"]; 13 + b -> c [label="forward"]; 14 + c -> b [label="response"]; 15 + b -> a [label="reply"]; 16 + } 17 + ]} 18 + ]} 19 + *) 20 + 21 + module Api = Odoc_extension_api 22 + module Block = Odoc_document.Types.Block 23 + module Inline = Odoc_document.Types.Inline 24 + 25 + (** MscGen.js CDN URL - the inpage version auto-renders on DOMContentLoaded *) 26 + let mscgen_js_url = "https://unpkg.com/mscgenjs-inpage@4/dist/mscgen-inpage.js" 27 + 28 + (** Script to load mscgenjs with defer-like behavior *) 29 + let loader_script = Printf.sprintf {| 30 + (function() { 31 + function loadMscgen() { 32 + var script = document.createElement('script'); 33 + script.src = %S; 34 + script.async = false; 35 + document.head.appendChild(script); 36 + } 37 + if (document.readyState === 'loading') { 38 + document.addEventListener('DOMContentLoaded', loadMscgen); 39 + } else { 40 + loadMscgen(); 41 + } 42 + })(); 43 + |} mscgen_js_url 44 + 45 + 46 + (** Generate a unique ID for each diagram *) 47 + let diagram_counter = ref 0 48 + 49 + let fresh_id () = 50 + incr diagram_counter; 51 + Printf.sprintf "msc-diagram-%d" !diagram_counter 52 + 53 + (** Extract option values *) 54 + let get_style tags = 55 + Api.get_binding "named-style" tags 56 + |> Option.value ~default:"basic" 57 + 58 + let get_format tags = 59 + Api.get_binding "format" tags 60 + 61 + let get_filename tags = 62 + Api.get_binding "filename" tags 63 + 64 + (** Extract CSS dimensions *) 65 + let get_dimensions tags = 66 + let width = Api.get_binding "width" tags in 67 + let height = Api.get_binding "height" tags in 68 + (width, height) 69 + 70 + (** Build inline style string from dimensions *) 71 + let make_style width height = 72 + let parts = [] in 73 + let parts = match width with 74 + | Some w -> Printf.sprintf "width: %s" w :: parts 75 + | None -> parts 76 + in 77 + let parts = match height with 78 + | Some h -> Printf.sprintf "height: %s" h :: parts 79 + | None -> parts 80 + in 81 + match parts with 82 + | [] -> "" 83 + | ps -> String.concat "; " (List.rev ps) 84 + 85 + (** HTML-escape content for safe embedding *) 86 + let html_escape s = 87 + let buf = Buffer.create (String.length s) in 88 + String.iter (fun c -> 89 + match c with 90 + | '<' -> Buffer.add_string buf "&lt;" 91 + | '>' -> Buffer.add_string buf "&gt;" 92 + | '&' -> Buffer.add_string buf "&amp;" 93 + | '"' -> Buffer.add_string buf "&quot;" 94 + | c -> Buffer.add_char buf c 95 + ) s; 96 + Buffer.contents buf 97 + 98 + (** Run mscgen to render to a specific format *) 99 + let run_mscgen ~format content = 100 + let tmp_in = Filename.temp_file "odoc_msc_" ".msc" in 101 + let tmp_out = Filename.temp_file "odoc_msc_" ("." ^ format) in 102 + Fun.protect ~finally:(fun () -> 103 + (try Sys.remove tmp_in with _ -> ()); 104 + (try Sys.remove tmp_out with _ -> ()) 105 + ) (fun () -> 106 + (* Write MSC content *) 107 + let oc = open_out tmp_in in 108 + output_string oc content; 109 + close_out oc; 110 + (* Run mscgen command *) 111 + let cmd = Printf.sprintf "mscgen -T %s -i %s -o %s 2>&1" 112 + format (Filename.quote tmp_in) (Filename.quote tmp_out) in 113 + let ic = Unix.open_process_in cmd in 114 + let error_output = Buffer.create 256 in 115 + (try 116 + while true do 117 + Buffer.add_string error_output (input_line ic); 118 + Buffer.add_char error_output '\n' 119 + done 120 + with End_of_file -> ()); 121 + let status = Unix.close_process_in ic in 122 + match status with 123 + | Unix.WEXITED 0 -> 124 + (* Read the output file *) 125 + let ic = open_in_bin tmp_out in 126 + let len = in_channel_length ic in 127 + let data = Bytes.create len in 128 + really_input ic data 0 len; 129 + close_in ic; 130 + Ok data 131 + | _ -> 132 + Error (Buffer.contents error_output) 133 + ) 134 + 135 + module Msc_handler : Api.Code_Block_Extension = struct 136 + let prefix = "msc" 137 + 138 + let to_document meta content = 139 + let id = fresh_id () in 140 + let style_name = get_style meta.Api.tags in 141 + let format = get_format meta.Api.tags in 142 + let filename_opt = get_filename meta.Api.tags in 143 + let (width, height) = get_dimensions meta.Api.tags in 144 + let style = make_style width height in 145 + let style_attr = if style = "" then "" else Printf.sprintf " style=\"%s\"" style in 146 + 147 + match format with 148 + | Some "png" | Some "svg" -> 149 + (* Server-side rendering with mscgen *) 150 + let fmt = match format with Some f -> f | None -> "png" in 151 + let base_filename = match filename_opt with 152 + | Some f -> f 153 + | None -> Printf.sprintf "msc-%s.%s" id fmt 154 + in 155 + (match run_mscgen ~format:fmt content with 156 + | Ok data -> 157 + let html = Printf.sprintf 158 + {|<div id="%s" class="odoc-msc-diagram"%s><img src="%s" alt="MSC diagram" /></div>|} 159 + id style_attr base_filename 160 + in 161 + let block = Block.[{ 162 + attr = ["odoc-msc"]; 163 + desc = Raw_markup ("html", html) 164 + }] in 165 + Some { 166 + Api.content = block; 167 + overrides = []; 168 + resources = []; 169 + assets = [{ Api.asset_filename = base_filename; asset_content = data }]; 170 + } 171 + | Error err -> 172 + (* Show error message *) 173 + let html = Printf.sprintf 174 + "<div id=\"%s\" class=\"odoc-msc-diagram odoc-msc-error\"><pre style=\"color: red;\">Error rendering MSC diagram (is mscgen installed?):\n%s</pre><pre>%s</pre></div>" 175 + id err (html_escape content) 176 + in 177 + let block = Block.[{ 178 + attr = ["odoc-msc"; "odoc-msc-error"]; 179 + desc = Raw_markup ("html", html) 180 + }] in 181 + Some { 182 + Api.content = block; 183 + overrides = []; 184 + resources = []; 185 + assets = []; 186 + }) 187 + 188 + | Some unknown_format -> 189 + let html = Printf.sprintf 190 + {|<div class="odoc-msc-error"><pre style="color: red;">Unknown format: %s (supported: png, svg)</pre></div>|} 191 + unknown_format 192 + in 193 + let block = Block.[{ 194 + attr = ["odoc-msc-error"]; 195 + desc = Raw_markup ("html", html) 196 + }] in 197 + Some { 198 + Api.content = block; 199 + overrides = []; 200 + resources = []; 201 + assets = []; 202 + } 203 + 204 + | None -> 205 + (* Default: client-side JavaScript rendering *) 206 + let data_style = if style_name = "basic" then "" else Printf.sprintf " data-named-style=\"%s\"" style_name in 207 + let html = Printf.sprintf 208 + {|<div id="%s" class="odoc-msc-diagram"%s><script type="text/x-mscgen"%s>%s</script><noscript><pre>%s</pre></noscript></div>|} 209 + id style_attr data_style content (html_escape content) 210 + in 211 + let block = Block.[{ 212 + attr = ["odoc-msc"]; 213 + desc = Raw_markup ("html", html) 214 + }] in 215 + Some { 216 + Api.content = block; 217 + overrides = []; 218 + resources = [ 219 + Api.Js_inline loader_script; 220 + ]; 221 + assets = []; 222 + } 223 + end 224 + 225 + (** CSS for MSC diagrams *) 226 + let msc_css = {| 227 + .odoc-msc-diagram { 228 + margin: 1em 0; 229 + overflow: auto; 230 + } 231 + 232 + .odoc-msc-diagram svg, 233 + .odoc-msc-diagram img { 234 + max-width: 100%; 235 + height: auto; 236 + } 237 + 238 + /* Fallback for noscript */ 239 + .odoc-msc-diagram noscript pre { 240 + background: #f8f8f8; 241 + padding: 1em; 242 + border-radius: 4px; 243 + overflow-x: auto; 244 + } 245 + 246 + .odoc-msc-error pre { 247 + color: #c00; 248 + } 249 + |} 250 + 251 + (** Extension documentation *) 252 + let extension_info : Api.extension_info = { 253 + info_kind = `Code_block; 254 + info_prefix = "msc"; 255 + info_description = "Render Message Sequence Charts. Uses client-side mscgen-inpage.js by default, or server-side mscgen with format=png|svg."; 256 + info_options = [ 257 + { opt_name = "format"; opt_description = "Output format: png, svg (requires mscgen), or omit for client-side JS"; opt_default = None }; 258 + { opt_name = "named-style"; opt_description = "MscGen style (basic, lazy, classic, etc.)"; opt_default = Some "basic" }; 259 + { opt_name = "width"; opt_description = "CSS width (e.g., 500px, 100%)"; opt_default = None }; 260 + { opt_name = "height"; opt_description = "CSS height"; opt_default = None }; 261 + { opt_name = "filename"; opt_description = "Output filename for server-side rendering"; opt_default = Some "auto-generated" }; 262 + ]; 263 + info_example = Some "{@msc format=png[msc { a,b; a->b; }]}"; 264 + } 265 + 266 + let () = 267 + Api.Registry.register_code_block (module Msc_handler); 268 + Api.Registry.register_extension_info extension_info; 269 + Api.Registry.register_support_file ~prefix:"msc" { 270 + filename = "extensions/msc.css"; 271 + content = msc_css; 272 + }
-51
odoc-bench.opam
··· 1 - opam-version: "2.0" 2 - 3 - version: "dev" 4 - homepage: "https://github.com/ocaml/odoc" 5 - doc: "https://ocaml.github.io/odoc/" 6 - bug-reports: "https://github.com/ocaml/odoc/issues" 7 - license: "ISC" 8 - 9 - maintainer: [ 10 - "Daniel Bünzli <daniel.buenzli@erratique.ch>" 11 - "Jon Ludlam <jon@recoil.org>" 12 - "Jules Aguillon <juloo.dsi@gmail.com>" 13 - "Paul-Elliot Anglès d'Auriac <paul-elliot@tarides.com>" 14 - ] 15 - authors: [ 16 - "Anton Bachin <antonbachin@yahoo.com>" 17 - "Daniel Bünzli <daniel.buenzli@erratique.ch>" 18 - "David Sheets <sheets@alum.mit.edu>" 19 - "Jon Ludlam <jon@recoil.org>" 20 - "Jules Aguillon <juloo.dsi@gmail.com>" 21 - "Leo White <leo@lpw25.net>" 22 - "Lubega Simon <lubegasimon73@gmail.com>" 23 - "Paul-Elliot Anglès d'Auriac <paul-elliot@tarides.com>" 24 - "Thomas Refis <trefis@janestreet.com>" 25 - ] 26 - dev-repo: "git+https://github.com/ocaml/odoc.git" 27 - 28 - synopsis: "Meta package defining dependencies for the benchmark rule. Not released" 29 - 30 - depends: [ 31 - "astring" 32 - "cmdliner" {>= "1.3.0"} 33 - "cppo" {build & >= "1.1.0"} 34 - "dune" {>= "3.18.0"} 35 - "fpath" 36 - "ocaml" {>= "4.02.0"} 37 - "result" 38 - "tyxml" {>= "4.3.0"} 39 - "fmt" 40 - "ocamlfind" 41 - "bos" 42 - "yojson" {>= "1.6.0"} 43 - "mdx" {>= "2.3.0"} 44 - "core" {= "v0.16.2"} 45 - "core_kernel" {= "v0.16.0"} 46 - "crunch" 47 - "odig" 48 - "base" 49 - "alcotest" 50 - "menhirLib" 51 - ]
+29
odoc-dot-extension.opam
··· 1 + opam-version: "2.0" 2 + version: "dev" 3 + synopsis: "Graphviz/DOT diagram support for odoc documentation" 4 + description: """ 5 + Renders {@dot[...]} code blocks as SVG diagrams using Graphviz. 6 + Supports width, height, and layout engine options.""" 7 + maintainer: ["Jon Ludlam <jon@recoil.org>"] 8 + authors: ["Jon Ludlam <jon@recoil.org>"] 9 + license: "ISC" 10 + homepage: "https://github.com/ocaml/odoc" 11 + bug-reports: "https://github.com/ocaml/odoc/issues" 12 + dev-repo: "git+https://github.com/ocaml/odoc.git" 13 + depends: [ 14 + "dune" {>= "3.18"} 15 + "ocaml" {>= "4.14"} 16 + "odoc" {= version} 17 + ] 18 + build: [ 19 + [ 20 + "dune" 21 + "build" 22 + "-p" 23 + name 24 + "-j" 25 + jobs 26 + "@install" 27 + ] 28 + ] 29 + x-maintenance-intent: ["(latest)"]
+29
odoc-mermaid-extension.opam
··· 1 + opam-version: "2.0" 2 + version: "dev" 3 + synopsis: "Mermaid diagram support for odoc documentation" 4 + description: """ 5 + Renders {@mermaid[...]} code blocks as interactive diagrams. 6 + Supports flowcharts, sequence diagrams, class diagrams, and more.""" 7 + maintainer: ["Jon Ludlam <jon@recoil.org>"] 8 + authors: ["Jon Ludlam <jon@recoil.org>"] 9 + license: "ISC" 10 + homepage: "https://github.com/ocaml/odoc" 11 + bug-reports: "https://github.com/ocaml/odoc/issues" 12 + dev-repo: "git+https://github.com/ocaml/odoc.git" 13 + depends: [ 14 + "dune" {>= "3.18"} 15 + "ocaml" {>= "4.14"} 16 + "odoc" {= version} 17 + ] 18 + build: [ 19 + [ 20 + "dune" 21 + "build" 22 + "-p" 23 + name 24 + "-j" 25 + jobs 26 + "@install" 27 + ] 28 + ] 29 + x-maintenance-intent: ["(latest)"]
+29
odoc-msc-extension.opam
··· 1 + opam-version: "2.0" 2 + version: "dev" 3 + synopsis: "Message Sequence Chart support for odoc documentation" 4 + description: """ 5 + Renders {@msc[...]} code blocks as message sequence charts. 6 + Uses the MscGen syntax for defining sequence diagrams.""" 7 + maintainer: ["Jon Ludlam <jon@recoil.org>"] 8 + authors: ["Jon Ludlam <jon@recoil.org>"] 9 + license: "ISC" 10 + homepage: "https://github.com/ocaml/odoc" 11 + bug-reports: "https://github.com/ocaml/odoc/issues" 12 + dev-repo: "git+https://github.com/ocaml/odoc.git" 13 + depends: [ 14 + "dune" {>= "3.18"} 15 + "ocaml" {>= "4.14"} 16 + "odoc" {= version} 17 + ] 18 + build: [ 19 + [ 20 + "dune" 21 + "build" 22 + "-p" 23 + name 24 + "-j" 25 + jobs 26 + "@install" 27 + ] 28 + ] 29 + x-maintenance-intent: ["(latest)"]
+5
odoc.opam
··· 44 44 "cmdliner" {>= "1.3.0"} 45 45 "cppo" {build & >= "1.1.0"} 46 46 "dune" {>= "3.18.0"} 47 + "dune-site" {>= "3.18.0"} 47 48 "fpath" {>= "0.7.3"} 48 49 "ocaml" {>= "4.08.0" & < "5.5"} 49 50 "tyxml" {>= "4.4.0"} ··· 57 58 "bos" {with-test} 58 59 "bisect_ppx" {with-test & > "2.5.0"} 59 60 ] 61 + 62 + x-dune-sites: [["lib" "extensions"]] 60 63 61 64 conflicts: [ "ocaml-option-bytecode-only" ] 62 65 ··· 75 78 name 76 79 "-j" 77 80 jobs 81 + "--promote-install-files=false" 78 82 "@install" 79 83 "@runtest" {with-test} 80 84 "@doc" {with-doc} 81 85 ] 86 + ["dune" "install" "-p" name "--create-install-files" name] 82 87 ] 83 88 x-maintenance-intent: ["(latest)"]
+1 -1
src/document/codefmt.ml
··· 245 245 let documentedSrc f = [ DocumentedSrc.Code (render f) ] 246 246 247 247 let codeblock ?attr f = 248 - [ block ?attr @@ Block.Source (Comment.default_lang_tag, render f) ] 248 + [ block ?attr @@ Block.Source (Comment.default_lang_tag, [], [], render f, []) ] 249 249 250 250 let keyword keyword ppf = pf ppf "@{<keyword>%s@}" keyword 251 251
+103 -12
src/document/comment.ml
··· 21 21 22 22 let default_lang_tag = "ocaml" 23 23 24 + (** Resource collection for extension handlers. 25 + Resources are collected during document generation and retrieved when 26 + building the final page. *) 27 + module Resources = struct 28 + let collected : Odoc_extension_registry.resource list ref = ref [] 29 + 30 + let add resources = 31 + collected := !collected @ resources 32 + 33 + let take () = 34 + let result = !collected in 35 + collected := []; 36 + result 37 + 38 + let clear () = 39 + collected := [] 40 + end 41 + 42 + (** Asset collection for extension handlers. 43 + Assets (binary files like PNGs) are collected during document generation 44 + and written alongside the HTML output. *) 45 + module Assets = struct 46 + let collected : Odoc_extension_registry.asset list ref = ref [] 47 + 48 + let add assets = 49 + collected := !collected @ assets 50 + 51 + let take () = 52 + let result = !collected in 53 + collected := []; 54 + result 55 + 56 + let clear () = 57 + collected := [] 58 + end 59 + 24 60 let source_of_code s = 25 61 if s = "" then [] else [ Source.Elt [ inline @@ Inline.Text s ] ] 26 62 ··· 216 252 fun content -> 217 253 match content with 218 254 | `Paragraph p -> [ paragraph p ] 219 - | `Code_block (lang_tag, code, outputs) -> 220 - let lang_tag = 221 - match lang_tag with None -> default_lang_tag | Some t -> t 255 + | `Code_block c -> 256 + let lang_tag, other_tags = 257 + match c.meta with 258 + | Some { language = { Odoc_parser.Loc.value; _ }; tags } -> (value, tags) 259 + | None -> (default_lang_tag, []) 222 260 in 223 - let rest = 224 - match outputs with 225 - | Some xs -> nestable_block_element_list xs 226 - | None -> [] 261 + let prefix = Odoc_extension_registry.prefix_of_language lang_tag in 262 + (* Check for a registered code block handler *) 263 + let handler_result = 264 + match Odoc_extension_registry.find_code_block_handler ~prefix with 265 + | Some handler -> 266 + let meta = { Odoc_extension_registry.language = lang_tag; tags = other_tags } in 267 + handler meta (Odoc_model.Location_.value c.content) 268 + | None -> None 227 269 in 228 - [ 229 - block 230 - @@ Source (lang_tag, source_of_code (Odoc_model.Location_.value code)); 231 - ] 232 - @ rest 270 + (match handler_result with 271 + | Some result -> 272 + (* Handler produced a result, collect resources/assets and use content *) 273 + Resources.add result.resources; 274 + Assets.add result.assets; 275 + result.content 276 + | None -> 277 + (* No handler or handler declined, use default rendering *) 278 + let rest = 279 + match c.output with 280 + | Some xs -> nestable_block_element_list xs 281 + | None -> [] 282 + in 283 + let value : 'a Odoc_parser.Loc.with_location -> 'a = fun x -> x.value in 284 + let classes = 285 + List.filter_map 286 + (function `Binding (_, _) -> None | `Tag t -> Some (value t)) 287 + other_tags 288 + in 289 + let data = 290 + List.filter_map 291 + (function 292 + | `Binding (k, v) -> Some (value k, value v) | `Tag _ -> None) 293 + other_tags 294 + in 295 + [ 296 + block 297 + @@ Source 298 + ( lang_tag, 299 + classes, 300 + data, 301 + source_of_code (Odoc_model.Location_.value c.content), 302 + rest ); 303 + ] 304 + @ rest) 233 305 | `Math_block s -> [ block @@ Math s ] 234 306 | `Verbatim s -> [ block @@ Verbatim s ] 235 307 | `Modules ms -> [ module_references ms ] ··· 374 446 let content = content_to_inline ~prefix:[ sp ] content in 375 447 item ~tag:"alert" 376 448 [ block (Block.Inline ([ inline @@ Text tag ] @ content)) ] 449 + | `Custom (name, content) -> 450 + (* Check if there's a registered extension for this tag *) 451 + let prefix = Odoc_extension_registry.prefix_of_tag name in 452 + (match Odoc_extension_registry.find_handler ~prefix with 453 + | Some handler -> 454 + (match handler name content with 455 + | Some result -> 456 + (* Extension handled the tag - collect resources/assets and use output *) 457 + Resources.add result.Odoc_extension_registry.resources; 458 + Assets.add result.Odoc_extension_registry.assets; 459 + { Description.attr = [ name ]; 460 + key = [ inline ~attr:[ "at-tag" ] (Text name) ]; 461 + definition = result.Odoc_extension_registry.content } 462 + | None -> 463 + (* Extension declined to handle this tag variant *) 464 + item ~tag:name (nestable_block_element_list content)) 465 + | None -> 466 + (* No extension registered - use default handling *) 467 + item ~tag:name (nestable_block_element_list content)) 377 468 378 469 let attached_block_element : Comment.attached_block_element -> Block.t = 379 470 function
+1
src/document/dune
··· 16 16 (backend landmarks --auto)) 17 17 (libraries 18 18 odoc_model 19 + odoc_extension_registry 19 20 fpath 20 21 astring 21 22 syntax_highlighter
+6 -2
src/document/generator.ml
··· 87 87 let make_expansion_page ~source_anchor url comments items = 88 88 let comment = List.concat comments in 89 89 let preamble, items = prepare_preamble comment items in 90 - { Page.preamble; items; url; source_anchor } 90 + let resources = Comment.Resources.take () in 91 + let assets = Comment.Assets.take () in 92 + { Page.preamble; items; url; source_anchor; resources; assets } 91 93 92 94 include Generator_signatures 93 95 ··· 1813 1815 let url = Url.Path.from_identifier t.name in 1814 1816 let preamble, items = Sectioning.docs t.content.elements in 1815 1817 let source_anchor = None in 1816 - Document.Page { Page.preamble; items; url; source_anchor } 1818 + let resources = Comment.Resources.take () in 1819 + let assets = Comment.Assets.take () in 1820 + Document.Page { Page.preamble; items; url; source_anchor; resources; assets } 1817 1821 1818 1822 let implementation (v : Odoc_model.Lang.Implementation.t) syntax_info 1819 1823 source_code =
+3 -1
src/document/renderer.ml
··· 9 9 path : Url.Path.t; 10 10 content : Format.formatter -> unit; 11 11 children : page list; 12 + assets : Odoc_extension_registry.asset list; 13 + (** Binary assets to write alongside this page *) 12 14 } 13 15 14 16 let traverse ~f t = 15 17 let rec aux node = 16 - f node.filename node.content; 18 + f node.filename node.content node.assets; 17 19 List.iter aux node.children 18 20 in 19 21 List.iter aux t
+22 -1
src/document/types.ml
··· 89 89 | Paragraph of Inline.t 90 90 | List of list_type * t list 91 91 | Description of Description.t 92 - | Source of lang_tag * Source.t 92 + | Source of lang_tag * string list * (string * string) list * Source.t * t 93 93 | Math of Math.t 94 94 | Verbatim of string 95 95 | Raw_markup of Raw_markup.t ··· 185 185 source_anchor : Url.t option; 186 186 (** Url to the corresponding source code. Might be a whole source file 187 187 or a sub part. *) 188 + resources : Odoc_extension_registry.resource list; 189 + (** Resources (JS/CSS) to inject into the page, collected from extensions. *) 190 + assets : Odoc_extension_registry.asset list; 191 + (** Binary assets to write alongside this page's HTML output. *) 188 192 } 189 193 end = 190 194 Page ··· 202 206 type t = { url : Url.Path.t; contents : code } 203 207 end = 204 208 Source_page 209 + 210 + (** Resources that extensions can inject into pages (HTML only) *) 211 + module Resource = struct 212 + type t = 213 + | Js_url of string (** External JavaScript: <script src="..."> *) 214 + | Css_url of string (** External CSS: <link rel="stylesheet" href="..."> *) 215 + | Js_inline of string (** Inline JavaScript: <script>...</script> *) 216 + | Css_inline of string (** Inline CSS: <style>...</style> *) 217 + 218 + let equal a b = 219 + match (a, b) with 220 + | Js_url a, Js_url b -> String.equal a b 221 + | Css_url a, Css_url b -> String.equal a b 222 + | Js_inline a, Js_inline b -> String.equal a b 223 + | Css_inline a, Css_inline b -> String.equal a b 224 + | _ -> false 225 + end 205 226 206 227 module Document = struct 207 228 type t = Page of Page.t | Source_page of Source_page.t
+4
src/extension_api/dune
··· 1 + (library 2 + (name odoc_extension_api) 3 + (public_name odoc.extension_api) 4 + (libraries odoc_model odoc_document odoc_extension_registry))
+315
src/extension_api/odoc_extension_api.ml
··· 1 + (** Odoc Extension API 2 + 3 + This module provides the interface for odoc tag extensions. 4 + Extensions are dynamically loaded plugins that handle custom tags 5 + like [@note], [@rfc], [@example], etc. 6 + *) 7 + 8 + (** {1 Re-exported Types} 9 + 10 + These are the odoc types that extensions need to work with. 11 + *) 12 + 13 + module Comment = Odoc_model.Comment 14 + module Location_ = Odoc_model.Location_ 15 + module Block = Odoc_document.Types.Block 16 + module Inline = Odoc_document.Types.Inline 17 + module Description = Odoc_document.Types.Description 18 + module Url = Odoc_document.Url 19 + module Target = Odoc_document.Types.Target 20 + 21 + (** {1 Extension Types} *) 22 + 23 + (** Resources that can be injected into the page (HTML only) *) 24 + type resource = Odoc_extension_registry.resource = 25 + | Js_url of string (** External JavaScript: <script src="..."> *) 26 + | Css_url of string (** External CSS: <link rel="stylesheet" href="..."> *) 27 + | Js_inline of string (** Inline JavaScript: <script>...</script> *) 28 + | Css_inline of string (** Inline CSS: <style>...</style> *) 29 + 30 + (** Binary asset generated by an extension. 31 + Assets are written alongside the HTML output. To reference an asset 32 + in your content, use the placeholder [__ODOC_ASSET__filename__] which 33 + will be replaced with the correct relative path during HTML generation. *) 34 + type asset = Odoc_extension_registry.asset = { 35 + asset_filename : string; (** Filename for the asset, e.g., "diagram-1.png" *) 36 + asset_content : bytes; (** Binary content *) 37 + } 38 + 39 + (** {1 Extension Documentation} 40 + 41 + Extensions can register documentation describing their options and usage. 42 + This information is displayed by [odoc extensions --help]. *) 43 + 44 + (** Documentation for a single option *) 45 + type option_doc = Odoc_extension_registry.option_doc = { 46 + opt_name : string; (** Option name, e.g., "width" *) 47 + opt_description : string; (** What the option does *) 48 + opt_default : string option; (** Default value if any *) 49 + } 50 + 51 + (** Documentation/metadata for an extension *) 52 + type extension_info = Odoc_extension_registry.extension_info = { 53 + info_kind : [ `Tag | `Code_block ]; (** Type of extension *) 54 + info_prefix : string; (** The prefix this extension handles *) 55 + info_description : string; (** Short description *) 56 + info_options : option_doc list; (** Supported options *) 57 + info_example : string option; (** Example usage *) 58 + } 59 + 60 + (** Output from the document phase *) 61 + type extension_output = { 62 + content : Block.t; 63 + (** Universal content - used by all backends unless overridden *) 64 + 65 + overrides : (string * string) list; 66 + (** Backend-specific raw content overrides. 67 + E.g., [("html", "<div>...</div>"); ("markdown", "...")] *) 68 + 69 + resources : resource list; 70 + (** Page-level resources (JS/CSS). Only used by HTML backend. *) 71 + 72 + assets : asset list; 73 + (** Binary assets to write alongside HTML output. 74 + Reference in content using [__ODOC_ASSET__filename__] placeholder. *) 75 + } 76 + 77 + (** Raised when an extension receives a tag variant it doesn't support *) 78 + exception Unsupported_tag of string 79 + 80 + (** {1 Extension Interface} *) 81 + 82 + (** The signature that all tag extensions must implement *) 83 + module type Extension = sig 84 + val prefix : string 85 + (** The tag prefix this extension handles. 86 + E.g., "note" handles [@note], "admonition" handles [@admonition.note] *) 87 + 88 + val to_document : 89 + tag:string -> 90 + Comment.nestable_block_element Location_.with_location list -> 91 + extension_output 92 + (** Document phase: convert tag to renderable content. 93 + Called during document generation. Returns content plus any 94 + page-level resources needed (JS/CSS). *) 95 + end 96 + 97 + (** {1 Code Block Extensions} 98 + 99 + Extensions can also handle code blocks like [{@dot[...]}] or 100 + [{@mermaid[...]}]. These extensions receive the language tag, 101 + metadata (key=value pairs), and the code content. 102 + *) 103 + 104 + (** Metadata for code blocks *) 105 + type code_block_meta = Odoc_extension_registry.code_block_meta = { 106 + language : string; 107 + (** The language tag, e.g., "dot" or "mermaid" *) 108 + 109 + tags : Odoc_parser.Ast.code_block_tag list; 110 + (** Additional metadata tags like [width=500] or [format=svg]. 111 + Each tag is either [`Tag name] for bare tags or 112 + [`Binding (key, value)] for key=value pairs. *) 113 + } 114 + 115 + (** The signature that code block extensions must implement *) 116 + module type Code_Block_Extension = sig 117 + val prefix : string 118 + (** The language prefix this extension handles. 119 + E.g., "dot" handles [{@dot[...]}], "mermaid" handles [{@mermaid[...]}] *) 120 + 121 + val to_document : 122 + code_block_meta -> 123 + string -> 124 + extension_output option 125 + (** Transform a code block. Takes metadata and code content. 126 + Returns [Some output] to replace the code block, or [None] to 127 + fall back to default rendering. 128 + 129 + Example metadata for [{@dot width=500 format=svg[digraph {...}]}]: 130 + - [meta.language = "dot"] 131 + - [meta.tags = [`Binding ("width", "500"); `Binding ("format", "svg")]] 132 + - content = "digraph {...}" *) 133 + end 134 + 135 + (** {1 Support Files} 136 + 137 + Extensions can register support files (CSS, JS, images, etc.) that 138 + will be output by [odoc support-files]. 139 + *) 140 + 141 + type support_file = Odoc_extension_registry.support_file = { 142 + filename : string; (** Relative path, e.g., "extensions/admonition.css" *) 143 + content : string; (** File content *) 144 + } 145 + 146 + (** {1 Extension Registry} 147 + 148 + Extensions register themselves here when loaded. 149 + odoc queries the registry when processing custom tags. 150 + *) 151 + 152 + module Registry = struct 153 + let register (module E : Extension) = 154 + let handler tag content = 155 + try 156 + let result = E.to_document ~tag content in 157 + Some { 158 + Odoc_extension_registry.content = result.content; 159 + overrides = result.overrides; 160 + resources = result.resources; 161 + assets = result.assets; 162 + } 163 + with Unsupported_tag _ -> None 164 + in 165 + Odoc_extension_registry.register_handler ~prefix:E.prefix handler 166 + 167 + let register_code_block (module E : Code_Block_Extension) = 168 + let handler meta content = 169 + match E.to_document meta content with 170 + | Some result -> 171 + Some { 172 + Odoc_extension_registry.content = result.content; 173 + overrides = result.overrides; 174 + resources = result.resources; 175 + assets = result.assets; 176 + } 177 + | None -> None 178 + in 179 + Odoc_extension_registry.register_code_block_handler ~prefix:E.prefix handler 180 + 181 + (** Register a support file for this extension. 182 + The file will be output when [odoc support-files] is run. *) 183 + let register_support_file ~prefix file = 184 + Odoc_extension_registry.register_support_file ~prefix file 185 + 186 + let find prefix = 187 + Odoc_extension_registry.find_handler ~prefix 188 + 189 + let find_code_block prefix = 190 + Odoc_extension_registry.find_code_block_handler ~prefix 191 + 192 + let list_prefixes () = 193 + Odoc_extension_registry.list_prefixes () 194 + 195 + let list_code_block_prefixes () = 196 + Odoc_extension_registry.list_code_block_prefixes () 197 + 198 + let list_support_files () = 199 + Odoc_extension_registry.list_support_files () 200 + 201 + (** Register documentation for an extension. 202 + This will be displayed by [odoc extensions]. *) 203 + let register_extension_info info = 204 + Odoc_extension_registry.register_extension_info info 205 + 206 + (** List all registered extension documentation *) 207 + let list_extension_infos () = 208 + Odoc_extension_registry.list_extension_infos () 209 + end 210 + 211 + (** {1 Helper Functions} *) 212 + 213 + (** Extract plain text from nestable block elements (for simple parsing) *) 214 + let rec text_of_inline (inline : Comment.inline_element Location_.with_location) = 215 + match inline.Location_.value with 216 + | `Space -> " " 217 + | `Word w -> w 218 + | `Code_span c -> c 219 + | `Math_span m -> m 220 + | `Raw_markup (_, r) -> r 221 + | `Styled (_, inlines) -> text_of_inlines inlines 222 + | `Reference (_, content) -> text_of_link_content content 223 + | `Link (_, content) -> text_of_link_content content 224 + 225 + and text_of_inlines inlines = 226 + String.concat "" (List.map text_of_inline inlines) 227 + 228 + and text_of_link_content (content : Comment.link_content) = 229 + String.concat "" (List.map text_of_non_link content) 230 + 231 + and text_of_non_link (el : Comment.non_link_inline_element Location_.with_location) = 232 + match el.Location_.value with 233 + | `Space -> " " 234 + | `Word w -> w 235 + | `Code_span c -> c 236 + | `Math_span m -> m 237 + | `Raw_markup (_, r) -> r 238 + | `Styled (_, content) -> text_of_link_content content 239 + 240 + let text_of_paragraph (p : Comment.paragraph) = 241 + text_of_inlines p 242 + 243 + let rec text_of_nestable_block_elements elements = 244 + let buf = Buffer.create 256 in 245 + List.iter (fun (el : Comment.nestable_block_element Location_.with_location) -> 246 + match el.Location_.value with 247 + | `Paragraph p -> Buffer.add_string buf (text_of_paragraph p) 248 + | `Code_block c -> Buffer.add_string buf c.content.Location_.value 249 + | `Math_block m -> Buffer.add_string buf m 250 + | `Verbatim v -> Buffer.add_string buf v 251 + | `Modules _ -> () 252 + | `Table _ -> () 253 + | `List (_, items) -> 254 + List.iter (fun item -> 255 + Buffer.add_string buf (text_of_nestable_block_elements item) 256 + ) items 257 + | `Media _ -> () 258 + ) elements; 259 + Buffer.contents buf 260 + 261 + (** Create a simple paragraph block *) 262 + let paragraph text = 263 + let inline = Inline.[ { attr = []; desc = Text text } ] in 264 + Block.[ { attr = []; desc = Paragraph inline } ] 265 + 266 + (** Create an inline link *) 267 + let link ~url ~text = 268 + Inline.[{ 269 + attr = []; 270 + desc = Link { 271 + target = External url; 272 + content = [{ attr = []; desc = Text text }]; 273 + tooltip = None 274 + } 275 + }] 276 + 277 + (** Create an empty extension output with just content *) 278 + let simple_output content = 279 + { content; overrides = []; resources = []; assets = [] } 280 + 281 + (** {1 Code Block Metadata Helpers} *) 282 + 283 + (** Get the value of a binding from code block tags. 284 + E.g., for [{@dot width=500[...]}], [get_binding "width" meta.tags] 285 + returns [Some "500"]. *) 286 + let get_binding key tags = 287 + List.find_map (function 288 + | `Binding (k, v) -> 289 + if k.Odoc_parser.Loc.value = key then Some v.Odoc_parser.Loc.value 290 + else None 291 + | `Tag _ -> None 292 + ) tags 293 + 294 + (** Check if a bare tag is present in code block tags. 295 + E.g., for [{@ocaml line-numbers[...]}], [has_tag "line-numbers" meta.tags] 296 + returns [true]. *) 297 + let has_tag name tags = 298 + List.exists (function 299 + | `Tag t -> t.Odoc_parser.Loc.value = name 300 + | `Binding _ -> false 301 + ) tags 302 + 303 + (** Get all bindings as a list of (key, value) pairs *) 304 + let get_all_bindings tags = 305 + List.filter_map (function 306 + | `Binding (k, v) -> Some (k.Odoc_parser.Loc.value, v.Odoc_parser.Loc.value) 307 + | `Tag _ -> None 308 + ) tags 309 + 310 + (** Get all bare tags as a list of names *) 311 + let get_all_tags tags = 312 + List.filter_map (function 313 + | `Tag t -> Some t.Odoc_parser.Loc.value 314 + | `Binding _ -> None 315 + ) tags
+4
src/extension_registry/dune
··· 1 + (library 2 + (name odoc_extension_registry) 3 + (public_name odoc.extension_registry) 4 + (libraries odoc_model))
+159
src/extension_registry/odoc_extension_registry.ml
··· 1 + (** Odoc Extension Registry 2 + 3 + This module provides a minimal registry for odoc tag extensions. 4 + It is kept separate to avoid circular dependencies between 5 + odoc_document and odoc_extension_api. 6 + *) 7 + 8 + module Comment = Odoc_model.Comment 9 + module Location_ = Odoc_model.Location_ 10 + 11 + (** Resources that can be injected into the page (HTML only) *) 12 + type resource = 13 + | Js_url of string 14 + | Css_url of string 15 + | Js_inline of string 16 + | Css_inline of string 17 + 18 + (** Support files that extensions want to output *) 19 + type support_file = { 20 + filename : string; (** Relative path, e.g., "extensions/admonition.css" *) 21 + content : string; (** File content *) 22 + } 23 + 24 + (** Binary asset generated by an extension (e.g., rendered PNG) *) 25 + type asset = { 26 + asset_filename : string; (** Filename for the asset, e.g., "diagram-1.png" *) 27 + asset_content : bytes; (** Binary content *) 28 + } 29 + 30 + (** Documentation for an extension option *) 31 + type option_doc = { 32 + opt_name : string; (** Option name, e.g., "width" *) 33 + opt_description : string; (** What the option does *) 34 + opt_default : string option; (** Default value if any *) 35 + } 36 + 37 + (** Documentation/metadata for an extension *) 38 + type extension_info = { 39 + info_kind : [ `Tag | `Code_block ]; (** Type of extension *) 40 + info_prefix : string; (** The prefix this extension handles *) 41 + info_description : string; (** Short description of what it does *) 42 + info_options : option_doc list; (** Supported options *) 43 + info_example : string option; (** Example usage *) 44 + } 45 + 46 + (** Result of processing a custom tag. 47 + We use a record with a polymorphic content type that gets 48 + instantiated with the actual Block.t by odoc_document. *) 49 + type 'block extension_result = { 50 + content : 'block; 51 + overrides : (string * string) list; 52 + resources : resource list; 53 + assets : asset list; 54 + (** Binary assets to write alongside the HTML output. 55 + Use [__ODOC_ASSET__filename__] placeholder in content to reference. *) 56 + } 57 + 58 + (** Type of handler functions stored in the registry. 59 + The handler takes a tag name and content, returns an optional result. 60 + If None, the tag is handled by the default mechanism. *) 61 + type 'block handler = 62 + string -> (* tag name *) 63 + Comment.nestable_block_element Location_.with_location list -> (* content *) 64 + 'block extension_result option 65 + 66 + (** The registry stores handlers indexed by prefix *) 67 + let handlers : (string, Obj.t) Hashtbl.t = Hashtbl.create 16 68 + 69 + (** Registered prefixes for listing *) 70 + let prefixes : (string, unit) Hashtbl.t = Hashtbl.create 16 71 + 72 + (** Support files registered by extensions *) 73 + let support_files : (string, support_file) Hashtbl.t = Hashtbl.create 16 74 + 75 + let register_handler ~prefix (handler : 'block handler) = 76 + Hashtbl.replace handlers prefix (Obj.repr handler); 77 + Hashtbl.replace prefixes prefix () 78 + 79 + let register_support_file ~prefix (file : support_file) = 80 + let key = prefix ^ ":" ^ file.filename in 81 + Hashtbl.replace support_files key file 82 + 83 + let find_handler (type block) ~prefix : block handler option = 84 + match Hashtbl.find_opt handlers prefix with 85 + | None -> None 86 + | Some h -> Some (Obj.obj h) 87 + 88 + let list_prefixes () = 89 + Hashtbl.fold (fun prefix () acc -> prefix :: acc) prefixes [] 90 + |> List.sort String.compare 91 + 92 + let list_support_files () = 93 + Hashtbl.fold (fun _ file acc -> file :: acc) support_files [] 94 + 95 + (** Extract the prefix from a tag name (part before the first dot) *) 96 + let prefix_of_tag tag = 97 + match String.index_opt tag '.' with 98 + | None -> tag 99 + | Some i -> String.sub tag 0 i 100 + 101 + (** {1 Code Block Handlers} 102 + 103 + Similar to custom tag handlers, but for code blocks like [{@dot[...]}]. 104 + Handlers can transform code blocks based on language and metadata. 105 + *) 106 + 107 + (** Metadata for code blocks, extracted from parser AST *) 108 + type code_block_meta = { 109 + language : string; 110 + tags : Odoc_parser.Ast.code_block_tag list; 111 + } 112 + 113 + (** Type of code block handler functions. 114 + Takes metadata and code content, returns optional transformed result. *) 115 + type 'block code_block_handler = 116 + code_block_meta -> (* language + metadata tags *) 117 + string -> (* code content *) 118 + 'block extension_result option 119 + 120 + (** Registry for code block handlers, indexed by language prefix *) 121 + let code_block_handlers : (string, Obj.t) Hashtbl.t = Hashtbl.create 16 122 + 123 + (** Registered code block prefixes *) 124 + let code_block_prefixes : (string, unit) Hashtbl.t = Hashtbl.create 16 125 + 126 + let register_code_block_handler ~prefix (handler : 'block code_block_handler) = 127 + Hashtbl.replace code_block_handlers prefix (Obj.repr handler); 128 + Hashtbl.replace code_block_prefixes prefix () 129 + 130 + let find_code_block_handler (type block) ~prefix : block code_block_handler option = 131 + match Hashtbl.find_opt code_block_handlers prefix with 132 + | None -> None 133 + | Some h -> Some (Obj.obj h) 134 + 135 + let list_code_block_prefixes () = 136 + Hashtbl.fold (fun prefix () acc -> prefix :: acc) code_block_prefixes [] 137 + |> List.sort String.compare 138 + 139 + (** Extract the prefix from a language tag (part before the first dot) *) 140 + let prefix_of_language = prefix_of_tag 141 + 142 + (** {1 Extension Documentation} 143 + 144 + Extensions can register documentation that describes their options 145 + and usage. This is displayed by [odoc extensions]. *) 146 + 147 + (** Registry for extension documentation *) 148 + let extension_infos : (string, extension_info) Hashtbl.t = Hashtbl.create 16 149 + 150 + let register_extension_info (info : extension_info) = 151 + let key = match info.info_kind with 152 + | `Tag -> "tag:" ^ info.info_prefix 153 + | `Code_block -> "code:" ^ info.info_prefix 154 + in 155 + Hashtbl.replace extension_infos key info 156 + 157 + let list_extension_infos () = 158 + Hashtbl.fold (fun _ info acc -> info :: acc) extension_infos [] 159 + |> List.sort (fun a b -> String.compare a.info_prefix b.info_prefix)
+7 -4
src/html/generator.ml
··· 256 256 mk_block Html.ul (List.map item l) 257 257 | Raw_markup r -> raw_markup r 258 258 | Verbatim s -> mk_block Html.pre [ Html.txt s ] 259 - | Source (lang_tag, c) -> 259 + | Source (lang_tag, _classes, _data, c, output) -> 260 260 let extra_class = [ "language-" ^ lang_tag ] in 261 - mk_block ~extra_class Html.pre (source (inline ~config ~resolve) c) 261 + mk_block Html.div 262 + ((mk_block ~extra_class Html.pre 263 + (source (inline ~config ~resolve) c)) 264 + @ block ~config ~resolve output) 262 265 | Math s -> mk_block Html.div [ block_math s ] 263 266 | Audio (target, alt) -> 264 267 let audio src alt = ··· 649 652 List.map (include_ ~config ~sidebar) subpages 650 653 651 654 and page ~config ~sidebar p : Odoc_document.Renderer.page = 652 - let { Page.preamble = _; items = i; url; source_anchor } = 655 + let { Page.preamble = _; items = i; url; source_anchor; resources; assets } = 653 656 Doctree.Labels.disambiguate_page ~enter_subpages:false p 654 657 in 655 658 let subpages = subpages ~config ~sidebar @@ Doctree.Subpages.compute p in ··· 681 684 subpages 682 685 else 683 686 Html_page.make ~sidebar ~config ~header:(header @ preamble) ~toc 684 - ~breadcrumbs ~url ~uses_katex content subpages 687 + ~breadcrumbs ~url ~uses_katex ~resources ~assets content subpages 685 688 686 689 and source_page ~config ~sidebar sp = 687 690 let { Source_page.url; contents } = sp in
+2 -2
src/html/html_fragment_json.ml
··· 64 64 ("content", `String (json_of_html config content)); 65 65 ])) 66 66 in 67 - { Odoc_document.Renderer.filename; content; children; path = url } 67 + { Odoc_document.Renderer.filename; content; children; path = url; assets = [] } 68 68 69 69 let make_src ~config ~url ~breadcrumbs ~sidebar ~header content = 70 70 let filename = Link.Path.as_filename ~config url in ··· 87 87 (List.map (Format.asprintf "%a" htmlpp) content)) ); 88 88 ])) 89 89 in 90 - { Odoc_document.Renderer.filename; content; children = []; path = url } 90 + { Odoc_document.Renderer.filename; content; children = []; path = url; assets = [] }
+24 -8
src/html/html_page.ml
··· 130 130 (); 131 131 ] 132 132 133 - let page_creator ~config ~url ~uses_katex ~global_toc header breadcrumbs 134 - local_toc content = 133 + let page_creator ~config ~url ~uses_katex ~resources ~global_toc header 134 + breadcrumbs local_toc content = 135 135 let theme_uri = Config.theme_uri config in 136 136 let support_uri = Config.support_uri config in 137 137 let search_uris = Config.search_uris config in ··· 183 183 (Html.txt ""); 184 184 ] 185 185 in 186 + (* Convert extension resources to HTML elements *) 187 + let extension_resources = 188 + let open Odoc_extension_registry in 189 + List.concat_map 190 + (function 191 + | Js_url url -> 192 + [ Html.script ~a:[ Html.a_src url ] (Html.txt "") ] 193 + | Css_url url -> 194 + [ Html.link ~rel:[ `Stylesheet ] ~href:url () ] 195 + | Js_inline code -> 196 + [ Html.script (Html.cdata_script code) ] 197 + | Css_inline code -> 198 + [ Html.style [ Html.cdata_style code ] ]) 199 + resources 200 + in 186 201 let meta_elements = 187 202 let highlightjs_meta = 188 203 let highlight_js_uri = file_uri support_uri "highlight.pack.js" in ··· 219 234 else [] 220 235 in 221 236 default_meta_elements ~config ~url @ highlightjs_meta @ katex_meta 237 + @ extension_resources 222 238 in 223 239 let meta_elements = meta_elements @ search_scripts in 224 240 Html.head (Html.title (Html.txt title_string)) meta_elements ··· 247 263 in 248 264 content 249 265 250 - let make ~config ~url ~header ~breadcrumbs ~sidebar ~toc ~uses_katex content 251 - children = 266 + let make ~config ~url ~header ~breadcrumbs ~sidebar ~toc ~uses_katex ~resources 267 + ~assets content children = 252 268 let filename = Link.Path.as_filename ~config url in 253 269 let content = 254 - page_creator ~config ~url ~uses_katex ~global_toc:sidebar header breadcrumbs 255 - toc content 270 + page_creator ~config ~url ~uses_katex ~resources ~global_toc:sidebar header 271 + breadcrumbs toc content 256 272 in 257 - { Odoc_document.Renderer.filename; content; children; path = url } 273 + { Odoc_document.Renderer.filename; content; children; path = url; assets } 258 274 259 275 let path_of_module_of_source ppf url = 260 276 match url.Url.Path.parent with ··· 295 311 let content = 296 312 src_page_creator ~breadcrumbs ~config ~url ~header ~sidebar title content 297 313 in 298 - { Odoc_document.Renderer.filename; content; children = []; path = url } 314 + { Odoc_document.Renderer.filename; content; children = []; path = url; assets = [] }
+2
src/html/html_page.mli
··· 28 28 sidebar:Html_types.div_content Html.elt list option -> 29 29 toc:Types.toc list -> 30 30 uses_katex:bool -> 31 + resources:Odoc_extension_registry.resource list -> 32 + assets:Odoc_extension_registry.asset list -> 31 33 Html_types.div_content Html.elt list -> 32 34 Odoc_document.Renderer.page list -> 33 35 Odoc_document.Renderer.page
+2 -2
src/latex/generator.ml
··· 350 350 ] 351 351 | Raw_markup r -> raw_markup r 352 352 | Verbatim s -> [ Verbatim s ] 353 - | Source (_, c) -> non_empty_block_code ~config c 353 + | Source (_, _, _, c, _) -> non_empty_block_code ~config c 354 354 | Math s -> 355 355 [ 356 356 Break Paragraph; ··· 522 522 if config.with_children then link_children ppf children else () 523 523 in 524 524 let content ppf = Fmt.pf ppf "@[<v>%a@,%t@]@." pp content children_input in 525 - { Odoc_document.Renderer.filename; content; children; path = url } 525 + { Odoc_document.Renderer.filename; content; children; path = url; assets = [] } 526 526 end 527 527 528 528 module Page = struct
+2 -2
src/manpage/generator.ml
··· 394 394 indent 2 (str "@" ++ key ++ str ":" ++ sp ++ def) 395 395 in 396 396 list ~sep:break (List.map f descrs) ++ continue rest 397 - | Source (_, content) -> 397 + | Source (_, _, _, content, _) -> 398 398 env "EX" "EE" "" (source_code content) ++ continue rest 399 399 | Math s -> math s ++ continue rest 400 400 | Verbatim content -> env "EX" "EE" "" (str "%s" content) ++ continue rest ··· 562 562 and children = List.concat_map subpage (Subpages.compute p) in 563 563 let content ppf = Format.fprintf ppf "%a@." Roff.pp (page p) in 564 564 let filename = Link.as_filename p.url in 565 - { Renderer.filename; content; children; path = p.url } 565 + { Renderer.filename; content; children; path = p.url; assets = [] } 566 566 567 567 let render = function 568 568 | Document.Page page -> [ render_page page ]
+2 -2
src/markdown2/generator.ml
··· 44 44 (fun (b : Types.Block.one) -> 45 45 match b.desc with 46 46 | Paragraph inline | Inline inline -> inline_text_only inline 47 - | Source (_, s) -> source inline_text_only s 47 + | Source (_, _, _, s, _) -> source inline_text_only s 48 48 | List (_, items) -> List.concat_map block_text_only items 49 49 | Verbatim s -> [ s ] 50 50 | _ -> []) ··· 143 143 Renderer.Block.Code_block { info_string = None; code = [ s ] } 144 144 in 145 145 [ code_snippet ] 146 - | Source (lang, s) -> 146 + | Source (lang, _, _, s, _) -> 147 147 let code = s |> source inline_text_only |> List.map (fun s -> s) in 148 148 let code_snippet = 149 149 Renderer.Block.Code_block { info_string = Some lang; code }
+2 -2
src/markdown2/markdown_page.ml
··· 3 3 let make ~config ~url doc children = 4 4 let filename = Link.Path.as_filename ~config url in 5 5 let content ppf = Format.fprintf ppf "%s" (Renderer.to_string doc) in 6 - { Odoc_document.Renderer.filename; content; children; path = url } 6 + { Odoc_document.Renderer.filename; content; children; path = url; assets = [] } 7 7 8 8 let make_src ~config ~url _title block_list = 9 9 let filename = Link.Path.as_filename ~config url in ··· 12 12 let doc = root_block in 13 13 Format.fprintf ppf "%s" (Renderer.to_string doc) 14 14 in 15 - { Odoc_document.Renderer.filename; content; children = []; path = url } 15 + { Odoc_document.Renderer.filename; content; children = []; path = url; assets = [] }
+14 -6
src/model/comment.ml
··· 60 60 61 61 type media_element = [ `Media of media_href * media * string ] 62 62 63 - type nestable_block_element = 63 + type code_block = { 64 + meta : Odoc_parser.Ast.code_block_meta option; 65 + delimiter : string option; 66 + content : string with_location; 67 + (** This is the raw content, that is the exact string inside the 68 + delimiters. In order to get the "processed" content, see 69 + {!Odoc_parser.codeblock_content} *) 70 + output : nestable_block_element with_location list option; 71 + } 72 + 73 + and nestable_block_element = 64 74 [ `Paragraph of paragraph 65 - | `Code_block of 66 - string option 67 - * string with_location 68 - * nestable_block_element with_location list option 75 + | `Code_block of code_block 69 76 | `Math_block of string 70 77 | `Verbatim of string 71 78 | `Modules of module_reference list ··· 89 96 | `Since of string 90 97 | `Before of string * nestable_block_element with_location list 91 98 | `Version of string 92 - | `Alert of string * string option ] 99 + | `Alert of string * string option 100 + | `Custom of string * nestable_block_element with_location list ] 93 101 94 102 type heading_level = 95 103 [ `Title
+6 -9
src/model/semantics.ml
··· 216 216 match element with 217 217 | { value = `Paragraph content; location } -> 218 218 Location.at location (`Paragraph (inline_elements content)) 219 - | { value = `Code_block { meta; delimiter = _; content; output }; location } 220 - -> 221 - let lang_tag = 222 - match meta with 223 - | Some { language = { Location.value; _ }; _ } -> Some value 224 - | None -> None 225 - in 226 - let outputs = 219 + | { value = `Code_block { meta; delimiter; content; output }; location } -> 220 + let output = 227 221 match output with 228 222 | None -> None 229 223 | Some l -> Some (List.map nestable_block_element l) ··· 234 228 let warnings = List.map Error.t_of_parser_t warnings in 235 229 List.iter (Error.raise_warning ~non_fatal:true) warnings; 236 230 let content = Location.at content.location trimmed_content in 237 - Location.at location (`Code_block (lang_tag, content, outputs)) 231 + let code_block = { Comment.meta; delimiter; content; output } in 232 + Location.at location (`Code_block code_block) 238 233 | { value = `Math_block s; location } -> Location.at location (`Math_block s) 239 234 | { value = `Verbatim v; location } -> 240 235 let v, warnings = Odoc_parser.codeblock_content location v in ··· 309 304 let ok t = Ok (Location.at location (`Tag t)) in 310 305 match tag with 311 306 | (`Author _ | `Since _ | `Version _) as tag -> ok tag 307 + | `Custom (name, content) -> 308 + ok (`Custom (name, nestable_block_elements content)) 312 309 | `Deprecated content -> ok (`Deprecated (nestable_block_elements content)) 313 310 | `Param (name, content) -> 314 311 ok (`Param (name, nestable_block_elements content))
+123 -126
src/model_desc/comment_desc.ml
··· 5 5 6 6 let ignore_loc x = x.Location_.value 7 7 8 - type general_inline_element = 9 - [ `Space 10 - | `Word of string 11 - | `Code_span of string 12 - | `Math_span of string 13 - | `Raw_markup of raw_markup_target * string 14 - | `Styled of style * general_inline_element with_location list 15 - | `Reference of Paths.Reference.t * general_link_content 16 - | `Link of string * general_link_content ] 17 - 18 - and general_link_content = general_inline_element with_location list 19 - 20 - type general_block_element = 21 - [ `Paragraph of general_link_content 22 - | `Code_block of 23 - string option 24 - * string with_location 25 - * general_block_element with_location list option 26 - | `Math_block of string 27 - | `Verbatim of string 28 - | `Modules of Comment.module_reference list 29 - | `List of 30 - [ `Unordered | `Ordered ] * general_block_element with_location list list 31 - | `Table of general_block_element abstract_table 32 - | `Heading of 33 - Comment.heading_attrs * Identifier.Label.t * general_link_content 34 - | `Tag of general_tag 35 - | `Media of 36 - [ `Reference of Paths.Reference.t | `Link of string ] * media * string ] 37 - 38 - and general_tag = 39 - [ `Author of string 40 - | `Deprecated of general_docs 41 - | `Param of string * general_docs 42 - | `Raise of 43 - [ `Code_span of string 44 - | `Reference of Paths.Reference.t * general_link_content ] 45 - * general_docs 46 - | `Return of general_docs 47 - | `See of [ `Url | `File | `Document ] * string * general_docs 48 - | `Since of string 49 - | `Before of string * general_docs 50 - | `Version of string 51 - | `Alert of string * string option ] 52 - 53 - and general_docs = general_block_element with_location list 54 - 55 8 let media = 56 9 Variant 57 10 (function ··· 60 13 | `Video -> C0 "`Video" 61 14 | `Image -> C0 "`Image") 62 15 63 - let rec inline_element : general_inline_element t = 64 - let style = 65 - Variant 66 - (function 67 - | `Bold -> C0 "`Bold" 68 - | `Italic -> C0 "`Italic" 69 - | `Emphasis -> C0 "`Emphasis" 70 - | `Superscript -> C0 "`Superscript" 71 - | `Subscript -> C0 "`Subscript") 72 - in 16 + let rec leaf_inline_element_fn : Odoc_model.Comment.leaf_inline_element -> case 17 + = function 18 + | `Space -> C0 "`Space" 19 + | `Word x -> C ("`Word", x, string) 20 + | `Code_span x -> C ("`Code_span", x, string) 21 + | `Math_span x -> C ("`Math_span", x, string) 22 + | `Raw_markup (x1, x2) -> C ("`Raw_markup", (x1, x2), Pair (string, string)) 23 + 24 + and style = 73 25 Variant 74 26 (function 75 - | `Space -> C0 "`Space" 76 - | `Word x -> C ("`Word", x, string) 77 - | `Code_span x -> C ("`Code_span", x, string) 78 - | `Math_span x -> C ("`Math_span", x, string) 79 - | `Raw_markup (x1, x2) -> C ("`Raw_markup", (x1, x2), Pair (string, string)) 80 - | `Styled (x1, x2) -> C ("`Styled", (x1, x2), Pair (style, link_content)) 81 - | `Reference (x1, x2) -> 82 - C ("`Reference", (x1, x2), Pair (reference, link_content)) 83 - | `Link (x1, x2) -> C ("`Link", (x1, x2), Pair (string, link_content))) 27 + | `Bold -> C0 "`Bold" 28 + | `Italic -> C0 "`Italic" 29 + | `Emphasis -> C0 "`Emphasis" 30 + | `Superscript -> C0 "`Superscript" 31 + | `Subscript -> C0 "`Subscript") 32 + 33 + and non_link_inline_element_fn : non_link_inline_element -> case = function 34 + | #leaf_inline_element as x -> leaf_inline_element_fn x 35 + | `Styled (x1, x2) -> C ("`Styled", (x1, x2), Pair (style, link_content)) 84 36 85 - and link_content : general_link_content t = 86 - List (Indirect (ignore_loc, inline_element)) 37 + and reference_element_fn : reference_element -> case = function 38 + | `Reference (x1, x2) -> 39 + C ("`Reference", (x1, x2), Pair (reference, link_content)) 40 + 41 + and inline_element_fn : inline_element -> case = function 42 + | #leaf_inline_element as x -> leaf_inline_element_fn x 43 + | #reference_element as x -> reference_element_fn x 44 + | `Styled (x1, x2) -> C ("`Styled", (x1, x2), Pair (style, paragraph)) 45 + | `Link (x1, x2) -> C ("`Link", (x1, x2), Pair (string, link_content)) 46 + 47 + and link_content = 48 + List (Indirect (ignore_loc, Variant non_link_inline_element_fn)) 49 + 50 + and inline_element = Variant inline_element_fn 51 + 52 + and paragraph = List (Indirect (ignore_loc, inline_element)) 87 53 88 54 let module_reference = 89 55 let simplify m = 90 - ( (m.module_reference :> Paths.Reference.t), 91 - (m.module_synopsis :> general_link_content option) ) 56 + ((m.module_reference :> Paths.Reference.t), m.module_synopsis) 92 57 in 93 - Indirect (simplify, Pair (reference, Option link_content)) 58 + Indirect (simplify, Pair (reference, Option paragraph)) 94 59 95 60 let heading = 96 61 let heading_level = ··· 110 75 F ("heading_label_explicit", (fun h -> h.heading_label_explicit), bool); 111 76 ] 112 77 in 113 - Triple (heading_attrs, identifier, link_content) 78 + Triple (heading_attrs, identifier, paragraph) 114 79 115 80 let media_href = 116 81 Variant 117 82 (function 118 - | `Reference r -> C ("`Reference", r, reference) 83 + | `Reference r -> 84 + C ("`Reference", (r : Reference.Asset.t :> Reference.t), reference) 119 85 | `Link l -> C ("`Link", l, string)) 120 86 121 - let rec block_element : general_block_element t = 87 + let code_block_tag : Odoc_parser.Ast.code_block_tag t = 88 + Variant 89 + (function 90 + | `Tag x -> C ("`Tag", ignore_loc x, string) 91 + | `Binding (x1, x2) -> 92 + C ("`Binding", (ignore_loc x1, ignore_loc x2), Pair (string, string))) 93 + 94 + let code_block_meta : Odoc_parser.Ast.code_block_meta t = 95 + Record 96 + [ 97 + F ("language", (fun h -> ignore_loc h.language), string); 98 + F ("warnings_tag", (fun h -> h.tags), List code_block_tag); 99 + ] 100 + 101 + let rec code_block : code_block t = 102 + Record 103 + [ 104 + F ("meta", (fun h -> h.meta), Option code_block_meta); 105 + F ("delimiter", (fun h -> h.delimiter), Option string); 106 + F ("content", (fun h -> ignore_loc h.content), string); 107 + F ("output", (fun h -> h.output), Option nestable_elements); 108 + ] 109 + 110 + and nestable_block_element_fn : nestable_block_element -> case = 122 111 let list_kind = 123 112 Variant 124 113 (function `Unordered -> C0 "`Unordered" | `Ordered -> C0 "`Ordered") 125 114 in 115 + function 116 + | `Paragraph x -> C ("`Paragraph", x, paragraph) 117 + | `Code_block c -> C ("`Code_block", c, code_block) 118 + | `Math_block x -> C ("`Math_block", x, string) 119 + | `Verbatim x -> C ("`Verbatim", x, string) 120 + | `Modules x -> C ("`Modules", x, List module_reference) 121 + | `Table { data; align } -> 122 + let cell_type_desc = 123 + Variant (function `Header -> C0 "`Header" | `Data -> C0 "`Data") 124 + in 125 + let data_desc = List (List (Pair (nestable_elements, cell_type_desc))) in 126 + let align_desc = 127 + Option 128 + (Variant 129 + (function 130 + | `Left -> C0 "`Left" 131 + | `Center -> C0 "`Center" 132 + | `Right -> C0 "`Right")) 133 + in 134 + let align_desc = List align_desc in 135 + let table_desc = Pair (data_desc, Option align_desc) in 136 + C ("`Table", (data, align), table_desc) 137 + | `List (x1, x2) -> 138 + C ("`List", (x1, x2), Pair (list_kind, List nestable_elements)) 139 + | `Media (x1, m, x2) -> 140 + C ("`Media", (x1, m, x2), Triple (media_href, media, string)) 141 + 142 + and nestable_block_element : nestable_block_element t = 143 + Variant nestable_block_element_fn 144 + 145 + and nestable_elements : nestable_block_element with_location list t = 146 + List 147 + (Indirect 148 + ( (fun x -> 149 + let x :> nestable_block_element Location_.with_location = x in 150 + ignore_loc x), 151 + nestable_block_element )) 152 + 153 + and block_element : block_element t = 126 154 Variant 127 155 (function 128 - | `Paragraph x -> C ("`Paragraph", x, link_content) 129 - | `Code_block (x1, x2, _) -> 130 - C ("`Code_block", (x1, ignore_loc x2), Pair (Option string, string)) 131 - | `Math_block x -> C ("`Math_block", x, string) 132 - | `Verbatim x -> C ("`Verbatim", x, string) 133 - | `Modules x -> C ("`Modules", x, List module_reference) 134 - | `List (x1, x2) -> 135 - C 136 - ( "`List", 137 - (x1, (x2 :> general_docs list)), 138 - Pair (list_kind, List general_content) ) 139 - | `Table { data; align } -> 140 - let cell_type_desc = 141 - Variant (function `Header -> C0 "`Header" | `Data -> C0 "`Data") 142 - in 143 - let data_desc = List (List (Pair (general_content, cell_type_desc))) in 144 - let align_desc = 145 - Option 146 - (Variant 147 - (function 148 - | `Left -> C0 "`Left" 149 - | `Center -> C0 "`Center" 150 - | `Right -> C0 "`Right")) 151 - in 152 - let align_desc = List align_desc in 153 - let table_desc = Pair (data_desc, Option align_desc) in 154 - C ("`Table", (data, align), table_desc) 155 - | `Heading h -> C ("`Heading", h, heading) 156 - | `Tag x -> C ("`Tag", x, tag) 157 - | `Media (x1, m, x2) -> 158 - C ("`Media", (x1, m, x2), Triple (media_href, media, string))) 156 + | #nestable_block_element as x -> nestable_block_element_fn x 157 + | `Heading (x1, x2, x3) -> 158 + C ("`Heading", (x1, (x2 :> Identifier.t), x3), heading) 159 + | `Tag x -> C ("`Tag", x, tag)) 159 160 160 - and tag : general_tag t = 161 + and tag : tag t = 161 162 let url_kind = 162 163 Variant 163 164 (function ··· 168 169 Variant 169 170 (function 170 171 | `Author x -> C ("`Author", x, string) 171 - | `Deprecated x -> C ("`Deprecated", x, general_content) 172 - | `Param (x1, x2) -> C ("`Param", (x1, x2), Pair (string, general_content)) 173 - | `Raise (x1, x2) -> 172 + | `Deprecated x -> C ("`Deprecated", x, nestable_elements) 173 + | `Param (x1, x2) -> C ("`Param", (x1, x2), Pair (string, nestable_elements)) 174 + | `Raise (`Code_span x1, x2) -> 175 + C ("`Raise", (x1, x2), Pair (string, nestable_elements)) 176 + | `Raise (`Reference (x1, x2), x3) -> 174 177 C 175 178 ( "`Raise", 176 - ((x1 :> general_inline_element), x2), 177 - Pair (inline_element, general_content) ) 178 - | `Return x -> C ("`Return", x, general_content) 179 + (x1, x2, x3), 180 + Triple (reference, link_content, nestable_elements) ) 181 + | `Return x -> C ("`Return", x, nestable_elements) 179 182 | `See (x1, x2, x3) -> 180 - C ("`See", (x1, x2, x3), Triple (url_kind, string, general_content)) 183 + C ("`See", (x1, x2, x3), Triple (url_kind, string, nestable_elements)) 181 184 | `Since x -> C ("`Since", x, string) 182 - | `Before (x1, x2) -> C ("`Before", (x1, x2), Pair (string, general_content)) 185 + | `Before (x1, x2) -> 186 + C ("`Before", (x1, x2), Pair (string, nestable_elements)) 183 187 | `Version x -> C ("`Version", x, string) 184 - | `Alert (x1, x2) -> C ("`Alert", (x1, x2), Pair (string, Option string))) 188 + | `Alert (x1, x2) -> C ("`Alert", (x1, x2), Pair (string, Option string)) 189 + | `Custom (x1, x2) -> C ("`" ^ x1, x2, nestable_elements)) 185 190 186 - and general_content : general_docs t = 187 - List (Indirect (ignore_loc, block_element)) 191 + let elements : elements t = List (Indirect (ignore_loc, block_element)) 188 192 189 - let elements : elements t = 190 - Indirect ((fun x -> (x :> general_docs)), general_content) 191 193 let docs = 192 194 Record 193 195 [ ··· 198 200 let docs_or_stop : docs_or_stop t = 199 201 Variant (function `Docs x -> C ("`Docs", x, docs) | `Stop -> C0 "`Stop") 200 202 201 - let inline_element : inline_element Location_.with_location list Type_desc.t = 202 - List 203 - (Indirect 204 - ( (fun x -> 205 - let x :> general_inline_element Location_.with_location = x in 206 - ignore_loc x), 207 - inline_element )) 203 + let inline_elements : inline_element with_location list t = 204 + List (Indirect (ignore_loc, inline_element))
+1 -1
src/model_desc/comment_desc.mli
··· 1 1 open Odoc_model 2 2 open Odoc_model.Comment 3 3 4 - val inline_element : inline_element Location_.with_location list Type_desc.t 4 + val inline_elements : inline_element Location_.with_location list Type_desc.t 5 5 6 6 val elements : elements Type_desc.t 7 7
+1 -1
src/model_desc/lang_desc.ml
··· 722 722 (fun t -> 723 723 (t.short_title 724 724 :> Comment.inline_element Location_.with_location list option)), 725 - Option Comment_desc.inline_element ); 725 + Option Comment_desc.inline_elements ); 726 726 F 727 727 ( "toc_status", 728 728 (fun t ->
+7 -1
src/odoc/bin/dune
··· 2 2 (name main) 3 3 (package odoc) 4 4 (public_name odoc) 5 - (libraries cmdliner odoc_model odoc_odoc) 5 + (libraries cmdliner odoc_model odoc_odoc odoc_extension_api 6 + dune-site dune-site.plugins) 7 + (modules main sites) 6 8 (instrumentation 7 9 (backend landmarks --auto)) 8 10 (instrumentation 9 11 (backend bisect_ppx))) 12 + 13 + (generate_sites_module 14 + (module sites) 15 + (plugins (odoc extensions)))
+57
src/odoc/bin/main.ml
··· 9 9 open Odoc_odoc 10 10 open Cmdliner 11 11 12 + (* Load all installed extensions at startup *) 13 + let () = Sites.Plugins.Extensions.load_all () 14 + 12 15 let convert_syntax : Odoc_document.Renderer.syntax Arg.conv = 13 16 let syntax_parser str = 14 17 match str with ··· 1761 1764 let section_legacy = "COMMANDS: Legacy pipeline" 1762 1765 let section_deprecated = "COMMANDS: Deprecated" 1763 1766 1767 + module Extensions = struct 1768 + let print_option_doc opt = 1769 + let default = match opt.Odoc_extension_api.opt_default with 1770 + | Some d -> Printf.sprintf " (default: %s)" d 1771 + | None -> "" 1772 + in 1773 + Printf.printf " %s: %s%s\n%!" opt.opt_name opt.opt_description default 1774 + 1775 + let print_extension_info info = 1776 + let open Odoc_extension_api in 1777 + let kind_str = match info.info_kind with 1778 + | `Tag -> Printf.sprintf "@%s" info.info_prefix 1779 + | `Code_block -> Printf.sprintf "{@%s[...]}" info.info_prefix 1780 + in 1781 + Printf.printf "\n %s\n%!" kind_str; 1782 + Printf.printf " %s\n%!" info.info_description; 1783 + if info.info_options <> [] then begin 1784 + Printf.printf " Options:\n%!"; 1785 + List.iter ~f:print_option_doc info.info_options 1786 + end; 1787 + match info.info_example with 1788 + | Some ex -> Printf.printf " Example:\n %s\n%!" ex 1789 + | None -> () 1790 + 1791 + let run () = 1792 + let tag_prefixes = Odoc_extension_api.Registry.list_prefixes () in 1793 + let code_block_prefixes = Odoc_extension_api.Registry.list_code_block_prefixes () in 1794 + let infos = Odoc_extension_api.Registry.list_extension_infos () in 1795 + match tag_prefixes, code_block_prefixes with 1796 + | [], [] -> 1797 + Printf.printf "No extensions installed.\n%!"; 1798 + Printf.printf "Extensions can be installed as opam packages that register with odoc.\n%!" 1799 + | _ -> 1800 + Printf.printf "Installed extensions:\n%!"; 1801 + if infos <> [] then 1802 + (* Show detailed info for extensions that registered documentation *) 1803 + List.iter ~f:print_extension_info infos 1804 + else begin 1805 + (* Fallback to simple list if no documentation registered *) 1806 + if tag_prefixes <> [] then begin 1807 + Printf.printf " Tag handlers:\n%!"; 1808 + List.iter ~f:(fun prefix -> Printf.printf " @%s\n%!" prefix) tag_prefixes 1809 + end; 1810 + if code_block_prefixes <> [] then begin 1811 + Printf.printf " Code block handlers:\n%!"; 1812 + List.iter ~f:(fun prefix -> Printf.printf " {@%s[...]}\n%!" prefix) code_block_prefixes 1813 + end 1814 + end 1815 + 1816 + let cmd = Term.(const run $ const ()) 1817 + let info ~docs = Cmd.info "extensions" ~docs ~doc:"List installed odoc extensions" 1818 + end 1819 + 1764 1820 (** Sections in the order they should appear. *) 1765 1821 let main_page_sections = 1766 1822 [ ··· 1813 1869 Depends.Odoc_html.(cmd, info ~docs:section_deprecated); 1814 1870 Classify.(cmd, info ~docs:section_pipeline); 1815 1871 Extract_code.(cmd, info ~docs:section_pipeline); 1872 + Extensions.(cmd, info ~docs:section_support); 1816 1873 ] 1817 1874 in 1818 1875 let main =
+1
src/odoc/dune
··· 14 14 odoc_json_index 15 15 odoc_xref2 16 16 odoc_occurrences 17 + odoc_extension_registry 17 18 tyxml 18 19 unix) 19 20 (instrumentation
+2 -1
src/odoc/extract_code.cppo.ml
··· 56 56 | `Raise (_, l) 57 57 | `Return l 58 58 | `See (_, _, l) 59 - | `Before (_, l) ) -> 59 + | `Before (_, l) 60 + | `Custom (_, l) ) -> 60 61 List.iter (nestable_block_element line_directives oc names) l 61 62 | `Tag 62 63 ( `Author _ | `Since _ | `Version _ | `Canonical _ | `Inline | `Open
+11 -3
src/odoc/rendering.ml
··· 52 52 let render_document renderer ~sidebar ~output:root_dir ~extra_suffix ~extra doc 53 53 = 54 54 let pages = renderer.Renderer.render extra sidebar doc in 55 - Renderer.traverse pages ~f:(fun filename content -> 55 + Renderer.traverse pages ~f:(fun filename content assets -> 56 56 let filename = prepare ~extra_suffix ~output_dir:root_dir filename in 57 + (* Write assets to the same directory as the HTML file *) 58 + let asset_dir = Fpath.parent filename in 59 + List.iter (fun (asset : Odoc_extension_registry.asset) -> 60 + let asset_path = Fpath.(asset_dir / asset.asset_filename) in 61 + Io_utils.with_open_out_bin (Fs.File.to_string asset_path) @@ fun oc -> 62 + output_bytes oc asset.asset_content 63 + ) assets; 64 + (* Write the HTML content *) 57 65 Io_utils.with_formatter_out (Fs.File.to_string filename) @@ fun fmt -> 58 66 Format.fprintf fmt "%t@?" content) 59 67 ··· 131 139 in 132 140 doc >>= fun doc -> 133 141 let pages = renderer.Renderer.render extra None doc in 134 - Renderer.traverse pages ~f:(fun filename _content -> 142 + Renderer.traverse pages ~f:(fun filename _content _assets -> 135 143 let filename = Fpath.normalize @@ Fs.File.append root_dir filename in 136 144 Format.printf "%a\n" Fpath.pp filename); 137 145 Ok () ··· 146 154 List.iter 147 155 (fun doc -> 148 156 let pages = renderer.Renderer.render extra None doc in 149 - Renderer.traverse pages ~f:(fun filename _content -> 157 + Renderer.traverse pages ~f:(fun filename _content _assets -> 150 158 let filename = 151 159 Fpath.normalize @@ Fs.File.append root_dir filename 152 160 in
+8 -1
src/odoc/support_files.ml
··· 14 14 let name = Fs.File.create ~directory:output_directory ~name in 15 15 f name content 16 16 in 17 + (* Built-in support files *) 17 18 let files = Odoc_html_support_files.file_list in 18 19 List.iter 19 20 (fun f -> 20 21 match Odoc_html_support_files.read f with 21 22 | Some content when should_include ~without_theme f -> file f content 22 23 | _ -> ()) 23 - files 24 + files; 25 + (* Extension support files *) 26 + let extension_files = Odoc_extension_registry.list_support_files () in 27 + List.iter 28 + (fun (ext_file : Odoc_extension_registry.support_file) -> 29 + file ext_file.filename ext_file.content) 30 + extension_files 24 31 25 32 let write = 26 33 iter_files (fun name content ->
+1 -1
src/parser/ast.ml
··· 94 94 | `Toc_status of nestable_block_element with_location list 95 95 | `Order_category of nestable_block_element with_location list 96 96 | `Short_title of nestable_block_element with_location list ] 97 - 98 97 (** Internal tags are used to exercise fine control over the output of odoc. 99 98 They are never rendered in the output *) 100 99 ··· 103 102 | `Deprecated of nestable_block_element with_location list 104 103 | `Param of string * nestable_block_element with_location list 105 104 | `Raise of string * nestable_block_element with_location list 105 + | `Custom of string * nestable_block_element with_location list 106 106 | `Return of nestable_block_element with_location list 107 107 | `See of 108 108 [ `Url | `File | `Document ]
+2 -3
src/parser/lexer.mll
··· 549 549 { warning input Parse_error.truncated_see; 550 550 emit input (`Word "@see") } 551 551 552 - | '@' ['a'-'z' 'A'-'Z']+ as tag 553 - { warning input (Parse_error.unknown_tag tag); 554 - emit input (`Word tag) } 552 + | '@' (['a'-'z' 'A'-'Z'] ['a'-'z' 'A'-'Z' '0'-'9' '_' '.']* as tag) 553 + { emit input (`Tag (`Custom tag)) } 555 554 556 555 | '@' 557 556 { warning input Parse_error.stray_at;
+3 -1
src/parser/syntax.ml
··· 621 621 | `Toc_status -> [ `Word "@toc_status" ] 622 622 | `Order_category -> [ `Word "@order_category" ] 623 623 | `Short_title -> [ `Word "@short_title" ] 624 + | `Custom tag -> [ `Word ("@" ^ tag) ] 624 625 625 626 (* {3 Block element lists} *) 626 627 ··· 821 822 let tag = Loc.at location (`Tag tag) in 822 823 consume_block_elements `After_text (tag :: acc) 823 824 | ( `Deprecated | `Return | `Children_order | `Short_title 824 - | `Toc_status | `Order_category ) as tag -> 825 + | `Toc_status | `Order_category | `Custom _ ) as tag -> 825 826 let content, _stream_head, where_in_line = 826 827 block_element_list (In_implicitly_ended `Tag) 827 828 ~parent_markup:token input ··· 834 835 | `Children_order -> `Children_order content 835 836 | `Short_title -> `Short_title content 836 837 | `Order_category -> `Order_category content 838 + | `Custom s -> `Custom (s, content) 837 839 in 838 840 let location = 839 841 location :: List.map Loc.location content |> Loc.span
+64 -100
src/parser/test/test.ml
··· 239 239 | `Open -> Atom "@open" 240 240 | `Closed -> Atom "@closed" 241 241 | `Hidden -> Atom "@hidden" 242 + | `Custom (s, es) -> 243 + List 244 + ([ Atom "@custom"; Atom s ] 245 + @ List.map (at.at (nestable_block_element at)) es) 242 246 243 247 let block_element at : Ast.block_element Loc.with_location -> sexp = function 244 248 | { value = #Ast.nestable_block_element; _ } as e -> ··· 4839 4843 let prefix = 4840 4844 test "@deprecatedfoo"; 4841 4845 [%expect 4842 - {| 4843 - ((output 4844 - (((f.ml (1 0) (1 14)) 4845 - (paragraph (((f.ml (1 0) (1 14)) (word @deprecatedfoo))))))) 4846 - (warnings 4847 - ( "File \"f.ml\", line 1, characters 0-14:\ 4848 - \nUnknown tag '@deprecatedfoo'."))) |}] 4846 + {| ((output (((f.ml (1 0) (1 14)) (@custom deprecatedfoo)))) (warnings ())) |}] 4849 4847 4850 4848 let after_code_block = 4851 4849 test "{[foo]} @deprecated"; ··· 4991 4989 let prefix = 4992 4990 test "@paramfoo"; 4993 4991 [%expect 4994 - {| 4995 - ((output 4996 - (((f.ml (1 0) (1 9)) (paragraph (((f.ml (1 0) (1 9)) (word @paramfoo))))))) 4997 - (warnings 4998 - ( "File \"f.ml\", line 1, characters 0-9:\ 4999 - \nUnknown tag '@paramfoo'."))) |}] 4992 + {| ((output (((f.ml (1 0) (1 9)) (@custom paramfoo)))) (warnings ())) |}] 5000 4993 5001 4994 let after_code_block = 5002 4995 test "{[foo]} @param foo"; ··· 5043 5036 let prefix = 5044 5037 test "@raisefoo"; 5045 5038 [%expect 5046 - {| 5047 - ((output 5048 - (((f.ml (1 0) (1 9)) (paragraph (((f.ml (1 0) (1 9)) (word @raisefoo))))))) 5049 - (warnings 5050 - ( "File \"f.ml\", line 1, characters 0-9:\ 5051 - \nUnknown tag '@raisefoo'."))) |}] 5039 + {| ((output (((f.ml (1 0) (1 9)) (@custom raisefoo)))) (warnings ())) |}] 5052 5040 end in 5053 5041 () 5054 5042 ··· 5074 5062 let prefix = 5075 5063 test "@returnfoo"; 5076 5064 [%expect 5077 - {| 5078 - ((output 5079 - (((f.ml (1 0) (1 10)) 5080 - (paragraph (((f.ml (1 0) (1 10)) (word @returnfoo))))))) 5081 - (warnings 5082 - ( "File \"f.ml\", line 1, characters 0-10:\ 5083 - \nUnknown tag '@returnfoo'."))) |}] 5065 + {| ((output (((f.ml (1 0) (1 10)) (@custom returnfoo)))) (warnings ())) |}] 5084 5066 end in 5085 5067 () 5086 5068 ··· 5168 5150 let prefix = 5169 5151 test "@seefoo"; 5170 5152 [%expect 5171 - {| 5172 - ((output 5173 - (((f.ml (1 0) (1 7)) (paragraph (((f.ml (1 0) (1 7)) (word @seefoo))))))) 5174 - (warnings 5175 - ( "File \"f.ml\", line 1, characters 0-7:\ 5176 - \nUnknown tag '@seefoo'."))) |}] 5153 + {| ((output (((f.ml (1 0) (1 7)) (@custom seefoo)))) (warnings ())) |}] 5177 5154 5178 5155 let after_code_block = 5179 5156 test "{[foo]} @see <foo>"; ··· 5238 5215 let prefix = 5239 5216 test "@sincefoo"; 5240 5217 [%expect 5241 - {| 5242 - ((output 5243 - (((f.ml (1 0) (1 9)) (paragraph (((f.ml (1 0) (1 9)) (word @sincefoo))))))) 5244 - (warnings 5245 - ( "File \"f.ml\", line 1, characters 0-9:\ 5246 - \nUnknown tag '@sincefoo'."))) |}] 5218 + {| ((output (((f.ml (1 0) (1 9)) (@custom sincefoo)))) (warnings ())) |}] 5247 5219 5248 5220 let with_whitespace = 5249 5221 test "@since foo bar"; ··· 5303 5275 let prefix = 5304 5276 test "@beforefoo"; 5305 5277 [%expect 5306 - {| 5307 - ((output 5308 - (((f.ml (1 0) (1 10)) 5309 - (paragraph (((f.ml (1 0) (1 10)) (word @beforefoo))))))) 5310 - (warnings 5311 - ( "File \"f.ml\", line 1, characters 0-10:\ 5312 - \nUnknown tag '@beforefoo'."))) |}] 5278 + {| ((output (((f.ml (1 0) (1 10)) (@custom beforefoo)))) (warnings ())) |}] 5313 5279 end in 5314 5280 () 5315 5281 ··· 5332 5298 let prefix = 5333 5299 test "@versionfoo"; 5334 5300 [%expect 5335 - {| 5336 - ((output 5337 - (((f.ml (1 0) (1 11)) 5338 - (paragraph (((f.ml (1 0) (1 11)) (word @versionfoo))))))) 5339 - (warnings 5340 - ( "File \"f.ml\", line 1, characters 0-11:\ 5341 - \nUnknown tag '@versionfoo'."))) |}] 5301 + {| ((output (((f.ml (1 0) (1 11)) (@custom versionfoo)))) (warnings ())) |}] 5342 5302 5343 5303 let with_whitespace = 5344 5304 test "@version foo bar"; ··· 5403 5363 let prefix = 5404 5364 test "@canonicalfoo"; 5405 5365 [%expect 5406 - {| 5407 - ((output 5408 - (((f.ml (1 0) (1 13)) 5409 - (paragraph (((f.ml (1 0) (1 13)) (word @canonicalfoo))))))) 5410 - (warnings 5411 - ( "File \"f.ml\", line 1, characters 0-13:\ 5412 - \nUnknown tag '@canonicalfoo'."))) |}] 5366 + {| ((output (((f.ml (1 0) (1 13)) (@custom canonicalfoo)))) (warnings ())) |}] 5413 5367 5414 5368 (* TODO This should probably be an error of some kind, as Foo Bar is not a 5415 5369 valid module path. *) ··· 5432 5386 let prefix = 5433 5387 test "@inlinefoo"; 5434 5388 [%expect 5435 - {| 5436 - ((output 5437 - (((f.ml (1 0) (1 10)) 5438 - (paragraph (((f.ml (1 0) (1 10)) (word @inlinefoo))))))) 5439 - (warnings 5440 - ( "File \"f.ml\", line 1, characters 0-10:\ 5441 - \nUnknown tag '@inlinefoo'."))) |}] 5389 + {| ((output (((f.ml (1 0) (1 10)) (@custom inlinefoo)))) (warnings ())) |}] 5442 5390 5443 5391 let extra_whitespace = 5444 5392 test "@inline"; ··· 5495 5443 let prefix = 5496 5444 test "@openfoo"; 5497 5445 [%expect 5498 - {| 5499 - ((output 5500 - (((f.ml (1 0) (1 8)) (paragraph (((f.ml (1 0) (1 8)) (word @openfoo))))))) 5501 - (warnings 5502 - ( "File \"f.ml\", line 1, characters 0-8:\ 5503 - \nUnknown tag '@openfoo'."))) |}] 5446 + {| ((output (((f.ml (1 0) (1 8)) (@custom openfoo)))) (warnings ())) |}] 5504 5447 5505 5448 let extra_whitespace = 5506 5449 test "@open"; ··· 5557 5500 let prefix = 5558 5501 test "@closedfoo"; 5559 5502 [%expect 5560 - {| 5561 - ((output 5562 - (((f.ml (1 0) (1 10)) 5563 - (paragraph (((f.ml (1 0) (1 10)) (word @closedfoo))))))) 5564 - (warnings 5565 - ( "File \"f.ml\", line 1, characters 0-10:\ 5566 - \nUnknown tag '@closedfoo'."))) |}] 5503 + {| ((output (((f.ml (1 0) (1 10)) (@custom closedfoo)))) (warnings ())) |}] 5567 5504 5568 5505 let extra_whitespace = 5569 5506 test "@closed"; ··· 5620 5557 let prefix = 5621 5558 test "@hiddenfoo"; 5622 5559 [%expect 5623 - {| 5624 - ((output 5625 - (((f.ml (1 0) (1 10)) 5626 - (paragraph (((f.ml (1 0) (1 10)) (word @hiddenfoo))))))) 5627 - (warnings 5628 - ( "File \"f.ml\", line 1, characters 0-10:\ 5629 - \nUnknown tag '@hiddenfoo'."))) |}] 5560 + {| ((output (((f.ml (1 0) (1 10)) (@custom hiddenfoo)))) (warnings ())) |}] 5630 5561 5631 5562 let extra_whitespace = 5632 5563 test "@hidden"; ··· 6168 6099 let error_on_first_line = 6169 6100 test "@foo"; 6170 6101 [%expect 6171 - {| 6172 - ((output 6173 - (((f.ml (1 0) (1 4)) (paragraph (((f.ml (1 0) (1 4)) (word @foo))))))) 6174 - (warnings ( "File \"f.ml\", line 1, characters 0-4:\ 6175 - \nUnknown tag '@foo'."))) |}] 6102 + {| ((output (((f.ml (1 0) (1 4)) (@custom foo)))) (warnings ())) |}] 6176 6103 6177 6104 let error_on_second_line = 6178 6105 test " \n @foo"; 6179 6106 [%expect 6180 - {| 6181 - ((output 6182 - (((f.ml (2 2) (2 6)) (paragraph (((f.ml (2 2) (2 6)) (word @foo))))))) 6183 - (warnings ( "File \"f.ml\", line 2, characters 2-6:\ 6184 - \nUnknown tag '@foo'."))) |}] 6107 + {| ((output (((f.ml (2 2) (2 6)) (@custom foo)))) (warnings ())) |}] 6185 6108 end in 6186 6109 () 6187 6110 ··· 6236 6159 let custom_tag = 6237 6160 test "@custom"; 6238 6161 [%expect 6162 + {| ((output (((f.ml (1 0) (1 7)) (@custom custom)))) (warnings ())) |}] 6163 + 6164 + let custom_tag_with_dot = 6165 + test "@foo.bar"; 6166 + [%expect 6167 + {| ((output (((f.ml (1 0) (1 8)) (@custom foo.bar)))) (warnings ())) |}] 6168 + 6169 + let custom_tag_with_multiple_dots = 6170 + test "@callout.box.large"; 6171 + [%expect 6172 + {| ((output (((f.ml (1 0) (1 18)) (@custom callout.box.large)))) (warnings ())) |}] 6173 + 6174 + let custom_tag_with_underscore = 6175 + test "@foo_bar"; 6176 + [%expect 6177 + {| ((output (((f.ml (1 0) (1 8)) (@custom foo_bar)))) (warnings ())) |}] 6178 + 6179 + let custom_tag_with_number = 6180 + test "@rfc2616"; 6181 + [%expect 6182 + {| ((output (((f.ml (1 0) (1 8)) (@custom rfc2616)))) (warnings ())) |}] 6183 + 6184 + let custom_tag_with_content = 6185 + test "@foo.bar This is the content"; 6186 + [%expect 6239 6187 {| 6240 6188 ((output 6241 - (((f.ml (1 0) (1 7)) (paragraph (((f.ml (1 0) (1 7)) (word @custom))))))) 6242 - (warnings 6243 - ( "File \"f.ml\", line 1, characters 0-7:\ 6244 - \nUnknown tag '@custom'."))) |}] 6189 + (((f.ml (1 0) (1 28)) 6190 + (@custom foo.bar 6191 + ((f.ml (1 9) (1 28)) 6192 + (paragraph 6193 + (((f.ml (1 9) (1 13)) (word This)) ((f.ml (1 13) (1 14)) space) 6194 + ((f.ml (1 14) (1 16)) (word is)) ((f.ml (1 16) (1 17)) space) 6195 + ((f.ml (1 17) (1 20)) (word the)) ((f.ml (1 20) (1 21)) space) 6196 + ((f.ml (1 21) (1 28)) (word content))))))))) 6197 + (warnings ())) 6198 + |}] 6199 + 6200 + let custom_tag_trailing_dot = 6201 + test "@foo. bar"; 6202 + [%expect 6203 + {| 6204 + ((output 6205 + (((f.ml (1 0) (1 9)) 6206 + (@custom foo. 6207 + ((f.ml (1 6) (1 9)) (paragraph (((f.ml (1 6) (1 9)) (word bar))))))))) 6208 + (warnings ())) |}] 6245 6209 6246 6210 let custom_reference_kind = 6247 6211 test "{!custom:foo}";
+4 -1
src/parser/token.ml
··· 25 25 | `Inline 26 26 | `Open 27 27 | `Closed 28 - | `Hidden ] ] 28 + | `Hidden 29 + | `Custom of string ] ] 29 30 30 31 type media = [ `Audio | `Video | `Image ] 31 32 type media_href = [ `Reference of string | `Link of string ] ··· 134 135 let label = match label with None -> "" | Some label -> ":" ^ label in 135 136 Printf.sprintf "'{%i%s'" level label 136 137 | `Tag (`Author _) -> "'@author'" 138 + | `Tag (`Custom s) -> "'@" ^ s ^ "'" 137 139 | `Tag `Deprecated -> "'@deprecated'" 138 140 | `Tag (`Param _) -> "'@param'" 139 141 | `Tag (`Raise _) -> "'@raise'" ··· 250 252 | `Tag `Toc_status -> "'@toc_status" 251 253 | `Tag `Order_category -> "'@order_category" 252 254 | `Tag `Short_title -> "'@short_title" 255 + | `Tag (`Custom s) -> "'@" ^ s ^ "'" 253 256 | `Comment -> "top-level text" 254 257 255 258 let describe_element = function
+1 -1
src/search/text.ml
··· 55 55 |> List.concat |> String.concat " " 56 56 | `Heading (_, _, h) -> inlines h 57 57 | `Modules _ -> "" 58 - | `Code_block (_, s, _todo) -> s |> get_value 58 + | `Code_block c -> c.content |> get_value 59 59 | `Verbatim v -> v 60 60 | `Math_block m -> m 61 61 | `Media (_, _, is) -> is
+4
src/xref2/link.ml
··· 343 343 344 344 and comment_tag env warnings_tag parent ~loc:_ (x : Comment.tag) = 345 345 match x with 346 + | `Custom (name, content) -> 347 + `Custom 348 + ( name, 349 + comment_nestable_block_element_list env warnings_tag parent content ) 346 350 | `Deprecated content -> 347 351 `Deprecated 348 352 (comment_nestable_block_element_list env warnings_tag parent content)
+8
test/integration/code_block_handlers.t/html/test/test_code_blocks.html
··· 1 + <!DOCTYPE html> 2 + <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> &#x00BB; <a href="index.html">test</a> &#x00BB; 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(): 3 + print(&quot;Hello&quot;)</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 { 4 + a -&gt; b -&gt; c; 5 + b -&gt; d; 6 + }</code></pre></div><div><pre class="language-mermaid"><code>sequenceDiagram 7 + Alice-&gt;&gt;Bob: Hello 8 + Bob--&gt;&gt;Alice: Hi</code></pre></div></div></body></html>
+72
test/integration/code_block_handlers.t/run.t
··· 1 + Test code block metadata preservation and handler infrastructure. 2 + 3 + First, compile the test page: 4 + 5 + $ odoc compile --package test test_code_blocks.mld 6 + 7 + Link: 8 + 9 + $ odoc link -I . page-test_code_blocks.odoc 10 + 11 + Generate HTML: 12 + 13 + $ odoc html-generate -o html page-test_code_blocks.odocl 14 + 15 + Check the HTML output exists: 16 + 17 + $ test -f html/test/test_code_blocks.html && echo "HTML generated" 18 + HTML generated 19 + 20 + Verify code blocks are rendered with language classes. 21 + The language-* class should be present for each code block: 22 + 23 + $ grep -o 'class="[^"]*language-[^"]*"' html/test/test_code_blocks.html | sort | uniq 24 + class="language-dot" 25 + class="language-mermaid" 26 + class="language-msc" 27 + class="language-ocaml" 28 + class="language-python" 29 + 30 + Verify the code content is preserved in the output: 31 + 32 + $ grep -q "let x = 1" html/test/test_code_blocks.html && echo "ocaml code preserved" 33 + ocaml code preserved 34 + 35 + $ grep -q "let y = 2" html/test/test_code_blocks.html && echo "ocaml with metadata preserved" 36 + ocaml with metadata preserved 37 + 38 + $ grep -q "digraph G" html/test/test_code_blocks.html && echo "dot code preserved" 39 + dot code preserved 40 + 41 + $ grep -q "sequenceDiagram" html/test/test_code_blocks.html && echo "mermaid code preserved" 42 + mermaid code preserved 43 + 44 + $ grep -q "msc {" html/test/test_code_blocks.html && echo "msc code preserved" 45 + msc code preserved 46 + 47 + Verify bare tags don't break rendering (skip, noeval): 48 + 49 + $ grep -q "let z = 3" html/test/test_code_blocks.html && echo "code with bare tags preserved" 50 + code with bare tags preserved 51 + 52 + Verify bindings don't break rendering (version=5.0): 53 + 54 + $ grep -q "def hello" html/test/test_code_blocks.html && echo "python code preserved" 55 + python code preserved 56 + 57 + Verify format option is accepted (format=png, format=svg): 58 + 59 + $ grep -q "digraph Dependencies" html/test/test_code_blocks.html && echo "dot with format=png preserved" 60 + dot with format=png preserved 61 + 62 + $ grep -q "digraph Circular" html/test/test_code_blocks.html && echo "dot with format=svg preserved" 63 + dot with format=svg preserved 64 + 65 + $ grep -q "pie title Pets" html/test/test_code_blocks.html && echo "mermaid with format=png preserved" 66 + mermaid with format=png preserved 67 + 68 + Test the odoc extensions command works: 69 + 70 + $ odoc extensions | head -2 71 + No extensions installed. 72 + Extensions can be installed as opam packages that register with odoc.
+169
test/integration/code_block_handlers.t/test_code_blocks.mld
··· 1 + {0 Test Code Block Handlers} 2 + 3 + This page tests code block metadata preservation and handler infrastructure. 4 + 5 + {1 Basic Code Block} 6 + 7 + A simple code block with just a language tag: 8 + 9 + {@ocaml[ 10 + let x = 1 11 + ]} 12 + 13 + {1 Code Block with Metadata} 14 + 15 + A code block with key=value bindings: 16 + 17 + {@ocaml env=test version=5.0[ 18 + let y = 2 19 + ]} 20 + 21 + {1 Code Block with Bare Tags} 22 + 23 + A code block with bare tags (boolean flags): 24 + 25 + {@ocaml skip noeval[ 26 + let z = 3 27 + ]} 28 + 29 + {1 Code Block with Mixed Tags} 30 + 31 + A code block with both bindings and bare tags: 32 + 33 + {@python version=3.11 async highlight[ 34 + def hello(): 35 + print("Hello") 36 + ]} 37 + 38 + {1 Graphviz DOT Diagrams} 39 + 40 + Client-side rendering (default) with Viz.js: 41 + 42 + {@dot[ 43 + digraph G { 44 + a -> b -> c; 45 + b -> d; 46 + } 47 + ]} 48 + 49 + With layout and dimensions: 50 + 51 + {@dot layout=neato width=400px[ 52 + graph G { 53 + a -- b -- c -- a; 54 + } 55 + ]} 56 + 57 + Server-side PNG rendering (requires graphviz installed): 58 + 59 + {@dot format=png filename=my-graph.png[ 60 + digraph Dependencies { 61 + rankdir=LR; 62 + A [label="Module A"]; 63 + B [label="Module B"]; 64 + C [label="Module C"]; 65 + A -> B -> C; 66 + } 67 + ]} 68 + 69 + Server-side SVG rendering: 70 + 71 + {@dot format=svg layout=circo[ 72 + digraph Circular { 73 + a -> b -> c -> d -> e -> a; 74 + } 75 + ]} 76 + 77 + {1 Mermaid Diagrams} 78 + 79 + Client-side rendering (default) with Mermaid.js: 80 + 81 + {@mermaid[ 82 + graph LR 83 + A[Start] --> B{Decision} 84 + B -->|Yes| C[OK] 85 + B -->|No| D[Cancel] 86 + ]} 87 + 88 + With theme option: 89 + 90 + {@mermaid theme=forest[ 91 + sequenceDiagram 92 + Alice->>Bob: Hello 93 + Bob-->>Alice: Hi 94 + ]} 95 + 96 + Server-side PNG rendering (requires mmdc/mermaid-cli installed): 97 + 98 + {@mermaid format=png theme=neutral[ 99 + pie title Pets 100 + "Dogs" : 386 101 + "Cats" : 85 102 + "Birds" : 15 103 + ]} 104 + 105 + Server-side SVG with custom filename: 106 + 107 + {@mermaid format=svg filename=class-diagram.svg[ 108 + classDiagram 109 + Animal <|-- Duck 110 + Animal <|-- Fish 111 + Animal : +int age 112 + Animal : +String gender 113 + Animal: +isMammal() 114 + Duck : +String beakColor 115 + Duck : +swim() 116 + Fish : +int sizeInFeet 117 + Fish : +canEat() 118 + ]} 119 + 120 + {1 Message Sequence Charts} 121 + 122 + Client-side rendering (default) with mscgen-inpage.js: 123 + 124 + {@msc[ 125 + msc { 126 + a, b, c; 127 + a -> b [label="request"]; 128 + b -> c [label="forward"]; 129 + c -> b [label="response"]; 130 + b -> a [label="reply"]; 131 + } 132 + ]} 133 + 134 + With named style and dimensions: 135 + 136 + {@msc named-style=lazy width=500px[ 137 + msc { 138 + client, server, database; 139 + client -> server [label="HTTP GET"]; 140 + server -> database [label="SELECT"]; 141 + database -> server [label="rows"]; 142 + server -> client [label="JSON"]; 143 + } 144 + ]} 145 + 146 + Server-side PNG rendering (requires mscgen installed): 147 + 148 + {@msc format=png[ 149 + msc { 150 + Alice, Bob, Carol; 151 + Alice -> Bob [label="Hello"]; 152 + Bob -> Carol [label="Hi there"]; 153 + Carol -> Alice [label="Greetings"]; 154 + } 155 + ]} 156 + 157 + Server-side SVG with custom filename: 158 + 159 + {@msc format=svg filename=protocol.svg[ 160 + msc { 161 + sender, receiver; 162 + sender -> receiver [label="SYN"]; 163 + receiver -> sender [label="SYN-ACK"]; 164 + sender -> receiver [label="ACK"]; 165 + --- [label="connection established"]; 166 + sender -> receiver [label="DATA"]; 167 + receiver -> sender [label="ACK"]; 168 + } 169 + ]}
+72
test/integration/code_block_handlers.t/test_handler.ml
··· 1 + (** Test code block handler registration and invocation. 2 + This module registers a test handler and provides functions to verify it works. *) 3 + 4 + module Block = Odoc_document.Types.Block 5 + 6 + (** A simple handler that transforms dot code blocks into a placeholder *) 7 + module Dot_handler : Odoc_extension_api.Code_Block_Extension = struct 8 + let prefix = "dot" 9 + 10 + let to_document meta content = 11 + (* Extract metadata *) 12 + let width = Odoc_extension_api.get_binding "width" meta.tags in 13 + let height = Odoc_extension_api.get_binding "height" meta.tags in 14 + let format = Odoc_extension_api.get_binding "format" meta.tags in 15 + 16 + (* Create a placeholder block showing we processed it *) 17 + let info = Printf.sprintf "[DOT HANDLER: lang=%s, width=%s, height=%s, format=%s, content_len=%d]" 18 + meta.language 19 + (Option.value ~default:"none" width) 20 + (Option.value ~default:"none" height) 21 + (Option.value ~default:"png" format) 22 + (String.length content) 23 + in 24 + let inline = Odoc_document.Types.Inline.[{ 25 + attr = ["dot-placeholder"]; 26 + desc = Text info 27 + }] in 28 + let block = Block.[{ 29 + attr = ["dot-output"]; 30 + desc = Paragraph inline 31 + }] in 32 + Some (Odoc_extension_api.simple_output block) 33 + end 34 + 35 + (** A handler for mermaid diagrams *) 36 + module Mermaid_handler : Odoc_extension_api.Code_Block_Extension = struct 37 + let prefix = "mermaid" 38 + 39 + let to_document meta content = 40 + let theme = Odoc_extension_api.get_binding "theme" meta.tags in 41 + let info = Printf.sprintf "[MERMAID HANDLER: theme=%s, content_len=%d]" 42 + (Option.value ~default:"default" theme) 43 + (String.length content) 44 + in 45 + let inline = Odoc_document.Types.Inline.[{ 46 + attr = ["mermaid-placeholder"]; 47 + desc = Text info 48 + }] in 49 + let block = Block.[{ 50 + attr = ["mermaid-output"]; 51 + desc = Paragraph inline 52 + }] in 53 + Some (Odoc_extension_api.simple_output block) 54 + end 55 + 56 + (** A handler that declines to process (returns None) *) 57 + module Declining_handler : Odoc_extension_api.Code_Block_Extension = struct 58 + let prefix = "decline" 59 + 60 + let to_document _meta _content = None 61 + end 62 + 63 + let () = 64 + (* Register handlers *) 65 + Odoc_extension_api.Registry.register_code_block (module Dot_handler); 66 + Odoc_extension_api.Registry.register_code_block (module Mermaid_handler); 67 + Odoc_extension_api.Registry.register_code_block (module Declining_handler); 68 + 69 + (* Print registered prefixes *) 70 + print_endline "Registered code block handlers:"; 71 + List.iter (fun p -> Printf.printf " - %s\n" p) 72 + (Odoc_extension_api.Registry.list_code_block_prefixes ())
-66
test/integration/compile.t/run.t
··· 4 4 5 5 $ ocamlc -bin-annot -c ast.mli 6 6 $ odoc compile --package foo ast.cmti 7 - File "ast.mli", line 1, characters 4-17: 8 - Warning: Unknown tag '@TxtAttribute'. 9 - File "ast.mli", line 4, characters 4-21: 10 - Warning: Unknown tag '@ValueDeclaration'. 11 - File "ast.mli", line 6, characters 4-21: 12 - Warning: Unknown tag '@ValueDeclaration'. 13 - File "ast.mli", line 8, characters 4-21: 14 - Warning: Unknown tag '@ValueDeclaration'. 15 - File "ast.mli", line 11, characters 4-20: 16 - Warning: Unknown tag '@TypeDeclaration'. 17 - File "ast.mli", line 13, characters 4-20: 18 - Warning: Unknown tag '@TypeDeclaration'. 19 - File "ast.mli", line 14, characters 16-39: 20 - Warning: Unknown tag '@ConstructorDeclaration'. 21 - File "ast.mli", line 15, characters 16-39: 22 - Warning: Unknown tag '@ConstructorDeclaration'. 23 - File "ast.mli", line 22, characters 4-20: 24 - Warning: Unknown tag '@TypeDeclaration'. 25 - File "ast.mli", line 24, characters 15-32: 26 - Warning: Unknown tag '@LabelDeclaration'. 27 - File "ast.mli", line 25, characters 16-33: 28 - Warning: Unknown tag '@LabelDeclaration'. 29 - File "ast.mli", line 29, characters 4-20: 30 - Warning: Unknown tag '@TypeDeclaration'. 31 - File "ast.mli", line 31, characters 4-18: 32 - Warning: Unknown tag '@TypeExtension'. 33 - File "ast.mli", line 32, characters 17-27: 34 - Warning: Unknown tag '@Extension'. 35 - File "ast.mli", line 33, characters 17-27: 36 - Warning: Unknown tag '@Extension'. 37 - File "ast.mli", line 35, characters 4-26: 38 - Warning: Unknown tag '@ModuleTypeDeclaration'. 39 - File "ast.mli", line 37, characters 6-19: 40 - Warning: Unknown tag '@TxtAttribute'. 41 - File "ast.mli", line 40, characters 6-22: 42 - Warning: Unknown tag '@TypeDeclaration'. 43 - File "ast.mli", line 43, characters 4-22: 44 - Warning: Unknown tag '@ModuleDeclaration'. 45 - File "ast.mli", line 45, characters 6-19: 46 - Warning: Unknown tag '@TxtAttribute'. 47 - File "ast.mli", line 47, characters 6-24: 48 - Warning: Unknown tag '@ModuleDeclaration'. 49 - File "ast.mli", line 49, characters 8-21: 50 - Warning: Unknown tag '@TxtAttribute'. 51 - File "ast.mli", line 52, characters 8-24: 52 - Warning: Unknown tag '@TypeDeclaration'. 53 - File "ast.mli", line 57, characters 4-14: 54 - Warning: Unknown tag '@Exception'. 55 - File "ast.mli", line 60, characters 4-11: 56 - Warning: Unknown tag '@Hidden'. 57 - File "ast.mli", line 63, characters 4-23: 58 - Warning: Unknown tag '@IncludeDescription'. 59 - File "ast.mli", line 68, characters 6-22: 60 - Warning: Unknown tag '@TypeDeclaration'. 61 - File "ast.mli", line 65, characters 6-19: 62 - Warning: Unknown tag '@TxtAttribute'. 63 - File "ast.mli", line 68, characters 6-22: 64 - Warning: Unknown tag '@TypeDeclaration'. 65 - File "ast.mli", line 71, characters 4-10: 66 - Warning: Unknown tag '@Class'. 67 - File "ast.mli", line 74, characters 6-13: 68 - Warning: Unknown tag '@Method'. 69 7 70 8 Test different parsing errors. 71 9 ··· 104 42 Warning: Identifier in reference should not be empty. 105 43 File "parser_errors.mli", line 40, characters 4-8: 106 44 Warning: '@see' should be followed by <url>, 'file', or "document title". 107 - File "parser_errors.mli", line 43, characters 4-15: 108 - Warning: Unknown tag '@UnknownTag'. 109 45 File "parser_errors.mli", line 46, characters 4-5: 110 46 Warning: Unpaired '}' (end of markup). 111 47 Suggestion: try '\}'. ··· 155 91 Error: Identifier in reference should not be empty. 156 92 File "parser_errors.mli", line 40, characters 4-8: 157 93 Error: '@see' should be followed by <url>, 'file', or "document title". 158 - File "parser_errors.mli", line 43, characters 4-15: 159 - Error: Unknown tag '@UnknownTag'. 160 94 File "parser_errors.mli", line 46, characters 4-5: 161 95 Error: Unpaired '}' (end of markup). 162 96 Suggestion: try '\}'.
+55
test/integration/extension_plugins.t/run.t
··· 1 + Test the extension plugin system. 2 + 3 + This tests: 4 + 1. Custom tags compile without error (graceful fallback) 5 + 2. The 'odoc extensions' command works 6 + 3. Custom tags are rendered in HTML output with default handling 7 + 4. Support files mechanism works 8 + 9 + First, compile the test module with custom tags: 10 + 11 + $ ocamlc -bin-annot -c test_extension.ml 12 + 13 + Compile with odoc - custom tags should work without errors: 14 + 15 + $ odoc compile --package test test_extension.cmt 16 + 17 + Link the compiled unit: 18 + 19 + $ odoc link -I . test_extension.odoc 20 + 21 + Generate HTML output: 22 + 23 + $ odoc html-generate -o html test_extension.odocl 24 + 25 + Test the 'odoc extensions' command. 26 + The output depends on what extensions are installed: 27 + 28 + $ odoc extensions | head -1 29 + No extensions installed. 30 + 31 + Check that tag content is preserved in the output. 32 + 33 + The custom.note tag should be rendered (either by extension or default): 34 + 35 + $ grep -q "This is a custom note tag" html/test/Test_extension/index.html && echo "custom.note content found" 36 + custom.note content found 37 + 38 + The mytag tags should be rendered: 39 + 40 + $ grep -q "Some custom content here" html/test/Test_extension/index.html && echo "mytag content found" 41 + mytag content found 42 + 43 + The admonition.warning content should be present: 44 + 45 + $ grep -q "This operation may fail" html/test/Test_extension/index.html && echo "admonition content found" 46 + admonition content found 47 + 48 + Test the support-files command works: 49 + 50 + $ odoc support-files -o support 51 + $ test -d support && echo "support directory created" 52 + support directory created 53 + 54 + $ test -f support/odoc.css && echo "odoc.css present" 55 + odoc.css present
+20
test/integration/extension_plugins.t/test_extension.ml
··· 1 + (** Test module with custom tags. 2 + 3 + This module uses custom tags to test the extension system. 4 + 5 + @custom.note This is a custom note tag. 6 + *) 7 + 8 + (** A function with custom documentation tags. 9 + 10 + @rfc 9110 11 + @admonition.warning This operation may fail. 12 + *) 13 + let example_function () = () 14 + 15 + (** Another function. 16 + 17 + @mytag Some custom content here. 18 + @mytag.variant A variant of mytag. 19 + *) 20 + let another_function x = x + 1
+2 -2
test/integration/html_support_files.t/run.t
··· 1 1 $ odoc support-files -o with-theme 2 - $ find with-theme | sort 2 + $ find with-theme | grep -v extensions | sort 3 3 with-theme 4 4 with-theme/fonts 5 5 with-theme/fonts/KaTeX_AMS-Regular.woff2 ··· 40 40 with-theme/odoc_search.js 41 41 42 42 $ odoc support-files --without-theme -o without-theme 43 - $ find without-theme | sort 43 + $ find without-theme | grep -v extensions | sort 44 44 without-theme 45 45 without-theme/fonts 46 46 without-theme/fonts/KaTeX_AMS-Regular.woff2
+1 -2
test/model/semantics/test.ml
··· 952 952 let prefix = 953 953 test "@authorfoo"; 954 954 [%expect 955 - {| 956 - {"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."]} |}] 955 + {| {"value":[{"`Tag":{"`authorfoo":[]}}],"warnings":["File \"f.ml.mld\":\nPages (.mld files) should start with a heading."]} |}] 957 956 958 957 let not_allowed = 959 958 test ~tags_allowed:false "@author Foo bar";