this repo has no description
at main 391 lines 14 kB view raw
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 13module Comment = Odoc_model.Comment 14module Location_ = Odoc_model.Location_ 15module Block = Odoc_document.Types.Block 16module Inline = Odoc_document.Types.Inline 17module Description = Odoc_document.Types.Description 18module Url = Odoc_document.Url 19module Target = Odoc_document.Types.Target 20 21(** {1 Extension Types} *) 22 23(** Resources that can be injected into the HTML [<head>]. 24 25 See {!Odoc_extension_registry} for full documentation on execution 26 timing, SPA deduplication semantics, and guidance for extension 27 authors. 28 29 {b Summary:} 30 31 {ul 32 {- [Js_url] / [Css_url] — deduplicated by resolved URL. Loaded at 33 most once across SPA navigations.} 34 {- [Js_inline] — stamped with [data-spa-inline] at generation time. 35 Executed {b exactly once}: on the first SPA navigation that 36 introduces the script. Do not use [DOMContentLoaded] inside 37 these; use a [MutationObserver] if you need to react to content 38 changes on subsequent navigations.} 39 {- [Css_inline] — injected on every navigation (CSS is additive and 40 idempotent).}} *) 41type resource = Odoc_extension_registry.resource = 42 | Js_url of string 43 (** External JavaScript: emitted as [<script src="…">]. 44 Deduplicated by absolute URL on SPA navigation. *) 45 | Css_url of string 46 (** External CSS: emitted as [<link rel="stylesheet" href="…">]. 47 Deduplicated by absolute URL on SPA navigation. *) 48 | Js_inline of string 49 (** Inline JavaScript: emitted as [<script data-spa-inline="…">…</script>]. 50 Runs once on first encounter; skipped on subsequent SPA navigations 51 that carry an identical script. *) 52 | Css_inline of string 53 (** Inline CSS: emitted as [<style>…</style>]. 54 Re-injected on each SPA navigation (idempotent). *) 55 56(** Binary asset generated by an extension. 57 Assets are written alongside the HTML output. To reference an asset 58 in your content, use the placeholder [__ODOC_ASSET__filename__] which 59 will be replaced with the correct relative path during HTML generation. *) 60type asset = Odoc_extension_registry.asset = { 61 asset_filename : string; (** Filename for the asset, e.g., "diagram-1.png" *) 62 asset_content : bytes; (** Binary content *) 63} 64 65(** {1 Extension Documentation} 66 67 Extensions can register documentation describing their options and usage. 68 This information is displayed by [odoc extensions --help]. *) 69 70(** Documentation for a single option *) 71type option_doc = Odoc_extension_registry.option_doc = { 72 opt_name : string; (** Option name, e.g., "width" *) 73 opt_description : string; (** What the option does *) 74 opt_default : string option; (** Default value if any *) 75} 76 77(** Documentation/metadata for an extension *) 78type extension_info = Odoc_extension_registry.extension_info = { 79 info_kind : [ `Tag | `Code_block ]; (** Type of extension *) 80 info_prefix : string; (** The prefix this extension handles *) 81 info_description : string; (** Short description *) 82 info_options : option_doc list; (** Supported options *) 83 info_example : string option; (** Example usage *) 84} 85 86(** Output from the document phase *) 87type extension_output = { 88 content : Block.t; 89 (** Universal content - used by all backends unless overridden *) 90 91 overrides : (string * string) list; 92 (** Backend-specific raw content overrides. 93 E.g., [("html", "<div>...</div>"); ("markdown", "...")] *) 94 95 resources : resource list; 96 (** Page-level resources (JS/CSS). Only used by HTML backend. 97 See {!resource} for execution and deduplication semantics, 98 especially in SPA (single-page app) shells. *) 99 100 assets : asset list; 101 (** Binary assets to write alongside HTML output. 102 Reference in content using [__ODOC_ASSET__filename__] placeholder. *) 103} 104 105(** Raised when an extension receives a tag variant it doesn't support *) 106exception Unsupported_tag of string 107 108(** {1 Link Environment} 109 110 Extensions that need to look up other pages or modules during linking 111 can use the cross-reference environment. *) 112 113module Env = Odoc_xref2.Env 114 115(** {1 Extension Interface} *) 116 117(** The signature that all tag extensions must implement *) 118module type Extension = sig 119 val prefix : string 120 (** The tag prefix this extension handles. 121 E.g., "note" handles [@note], "admonition" handles [@admonition.note] *) 122 123 val to_document : 124 tag:string -> 125 Comment.nestable_block_element Location_.with_location list -> 126 extension_output 127 (** Document phase: convert tag to renderable content. 128 Called during document generation. Returns content plus any 129 page-level resources needed (JS/CSS). *) 130end 131 132(** Extensions that also need link-time access to the cross-reference 133 environment implement this extended signature. *) 134module type Extension_with_link = sig 135 include Extension 136 137 val link : 138 tag:string -> 139 Env.t -> 140 Comment.nestable_block_element Location_.with_location list -> 141 Comment.nestable_block_element Location_.with_location list 142 (** Link phase: transform tag content with access to the cross-reference 143 environment. Called during linking, after references have been resolved. 144 Use [Env.lookup_page_by_name] etc. to look up other pages. *) 145end 146 147(** {1 Code Block Extensions} 148 149 Extensions can also handle code blocks like [{@dot[...]}] or 150 [{@mermaid[...]}]. These extensions receive the language tag, 151 metadata (key=value pairs), and the code content. 152*) 153 154(** Metadata for code blocks *) 155type code_block_meta = Odoc_extension_registry.code_block_meta = { 156 language : string; 157 (** The language tag, e.g., "dot" or "mermaid" *) 158 159 tags : Odoc_parser.Ast.code_block_tag list; 160 (** Additional metadata tags like [width=500] or [format=svg]. 161 Each tag is either [`Tag name] for bare tags or 162 [`Binding (key, value)] for key=value pairs. *) 163} 164 165(** The signature that code block extensions must implement *) 166module type Code_Block_Extension = sig 167 val prefix : string 168 (** The language prefix this extension handles. 169 E.g., "dot" handles [{@dot[...]}], "mermaid" handles [{@mermaid[...]}] *) 170 171 val to_document : 172 code_block_meta -> 173 string -> 174 extension_output option 175 (** Transform a code block. Takes metadata and code content. 176 Returns [Some output] to replace the code block, or [None] to 177 fall back to default rendering. 178 179 Example metadata for [{\@dot width=500 format=svg[digraph \{...\}]}]: 180 - [meta.language = "dot"] 181 - [meta.tags = [`Binding ("width", "500"); `Binding ("format", "svg")]] 182 - content = "digraph \{...\}" *) 183end 184 185(** {1 Support Files} 186 187 Extensions can register support files (CSS, JS, images, etc.) that 188 will be output by [odoc support-files]. 189*) 190 191type support_file_content = Odoc_extension_registry.support_file_content = 192 | Inline of string 193 | Copy_from of string 194 195type support_file = Odoc_extension_registry.support_file = { 196 filename : string; (** Relative path, e.g., "extensions/admonition.css" *) 197 content : support_file_content; 198} 199 200(** {1 Extension Registry} 201 202 Extensions register themselves here when loaded. 203 odoc queries the registry when processing custom tags. 204*) 205 206module Registry = struct 207 let register (module E : Extension) = 208 let handler tag content = 209 try 210 let result = E.to_document ~tag content in 211 Some { 212 Odoc_extension_registry.content = result.content; 213 overrides = result.overrides; 214 resources = result.resources; 215 assets = result.assets; 216 } 217 with Unsupported_tag _ -> None 218 in 219 Odoc_extension_registry.register_handler ~prefix:E.prefix handler 220 221 let register_with_link (module E : Extension_with_link) = 222 let handler tag content = 223 try 224 let result = E.to_document ~tag content in 225 Some { 226 Odoc_extension_registry.content = result.content; 227 overrides = result.overrides; 228 resources = result.resources; 229 assets = result.assets; 230 } 231 with Unsupported_tag _ -> None 232 in 233 Odoc_extension_registry.register_handler ~prefix:E.prefix handler; 234 let link_handler tag env content = 235 E.link ~tag (Obj.obj env) content 236 in 237 Odoc_extension_registry.register_link_handler ~prefix:E.prefix link_handler 238 239 let register_code_block (module E : Code_Block_Extension) = 240 let handler meta content = 241 match E.to_document meta content with 242 | Some result -> 243 Some { 244 Odoc_extension_registry.content = result.content; 245 overrides = result.overrides; 246 resources = result.resources; 247 assets = result.assets; 248 } 249 | None -> None 250 in 251 Odoc_extension_registry.register_code_block_handler ~prefix:E.prefix handler 252 253 (** Register a support file for this extension. 254 The file will be output when [odoc support-files] is run. *) 255 let register_support_file ~prefix file = 256 Odoc_extension_registry.register_support_file ~prefix file 257 258 let find prefix = 259 Odoc_extension_registry.find_handler ~prefix 260 261 let find_code_block prefix = 262 Odoc_extension_registry.find_code_block_handler ~prefix 263 264 let list_prefixes () = 265 Odoc_extension_registry.list_prefixes () 266 267 let list_code_block_prefixes () = 268 Odoc_extension_registry.list_code_block_prefixes () 269 270 let list_support_files () = 271 Odoc_extension_registry.list_support_files () 272 273 (** Register documentation for an extension. 274 This will be displayed by [odoc extensions]. *) 275 let register_extension_info info = 276 Odoc_extension_registry.register_extension_info info 277 278 (** List all registered extension documentation *) 279 let list_extension_infos () = 280 Odoc_extension_registry.list_extension_infos () 281end 282 283(** {1 Helper Functions} *) 284 285(** Convert Comment AST to Block elements, preserving references and formatting. 286 This is the proper way to convert tag content to renderable blocks. *) 287let blocks_of_nestable_elements = Odoc_document.Comment.nestable_block_element_list 288 289(** Extract plain text from nestable block elements (for simple parsing) *) 290let rec text_of_inline (inline : Comment.inline_element Location_.with_location) = 291 match inline.Location_.value with 292 | `Space -> " " 293 | `Word w -> w 294 | `Code_span c -> c 295 | `Math_span m -> m 296 | `Raw_markup (_, r) -> r 297 | `Styled (_, inlines) -> text_of_inlines inlines 298 | `Reference (_, content) -> text_of_link_content content 299 | `Link (_, content) -> text_of_link_content content 300 301and text_of_inlines inlines = 302 String.concat "" (List.map text_of_inline inlines) 303 304and text_of_link_content (content : Comment.link_content) = 305 String.concat "" (List.map text_of_non_link content) 306 307and text_of_non_link (el : Comment.non_link_inline_element Location_.with_location) = 308 match el.Location_.value with 309 | `Space -> " " 310 | `Word w -> w 311 | `Code_span c -> c 312 | `Math_span m -> m 313 | `Raw_markup (_, r) -> r 314 | `Styled (_, content) -> text_of_link_content content 315 316let text_of_paragraph (p : Comment.paragraph) = 317 text_of_inlines p 318 319let rec text_of_nestable_block_elements elements = 320 let buf = Buffer.create 256 in 321 List.iter (fun (el : Comment.nestable_block_element Location_.with_location) -> 322 match el.Location_.value with 323 | `Paragraph p -> Buffer.add_string buf (text_of_paragraph p) 324 | `Code_block c -> Buffer.add_string buf c.content.Location_.value 325 | `Math_block m -> Buffer.add_string buf m 326 | `Verbatim v -> Buffer.add_string buf v 327 | `Modules _ -> () 328 | `Table _ -> () 329 | `List (_, items) -> 330 List.iter (fun item -> 331 Buffer.add_string buf (text_of_nestable_block_elements item) 332 ) items 333 | `Media _ -> () 334 ) elements; 335 Buffer.contents buf 336 337(** Create a simple paragraph block *) 338let paragraph text = 339 let inline = Inline.[ { attr = []; desc = Text text } ] in 340 Block.[ { attr = []; desc = Paragraph inline } ] 341 342(** Create an inline link *) 343let link ~url ~text = 344 Inline.[{ 345 attr = []; 346 desc = Link { 347 target = External url; 348 content = [{ attr = []; desc = Text text }]; 349 tooltip = None 350 } 351 }] 352 353(** Create an empty extension output with just content *) 354let simple_output content = 355 { content; overrides = []; resources = []; assets = [] } 356 357(** {1 Code Block Metadata Helpers} *) 358 359(** Get the value of a binding from code block tags. 360 E.g., for [{@dot width=500[...]}], [get_binding "width" meta.tags] 361 returns [Some "500"]. *) 362let get_binding key tags = 363 List.find_map (function 364 | `Binding (k, v) -> 365 if k.Odoc_parser.Loc.value = key then Some v.Odoc_parser.Loc.value 366 else None 367 | `Tag _ -> None 368 ) tags 369 370(** Check if a bare tag is present in code block tags. 371 E.g., for [{@ocaml line-numbers[...]}], [has_tag "line-numbers" meta.tags] 372 returns [true]. *) 373let has_tag name tags = 374 List.exists (function 375 | `Tag t -> t.Odoc_parser.Loc.value = name 376 | `Binding _ -> false 377 ) tags 378 379(** Get all bindings as a list of (key, value) pairs *) 380let get_all_bindings tags = 381 List.filter_map (function 382 | `Binding (k, v) -> Some (k.Odoc_parser.Loc.value, v.Odoc_parser.Loc.value) 383 | `Tag _ -> None 384 ) tags 385 386(** Get all bare tags as a list of names *) 387let get_all_tags tags = 388 List.filter_map (function 389 | `Tag t -> Some t.Odoc_parser.Loc.value 390 | `Binding _ -> None 391 ) tags