this repo has no description
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