this repo has no description
1
fork

Configure Feed

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

Add custom inline extensions to odoc

Support [{&name payload}] inline syntax in .mld pages and docstrings.
Plugins register handlers via [Registry.register_inline] in the odoc
extension API; each handler receives the raw payload string and
returns HTML that is spliced into the rendered output.

Implementation rides on the existing [Raw_markup] AST variant with a
synthetic target prefix [odoc-ext:<name>]. The lexer emits
[Raw_markup (Some ("odoc-ext:" ^ name), payload)] when it sees
[{&name payload}]; the HTML generator's [raw_markup] function detects
the prefix and dispatches to the inline-handler registry, falling
back to emitting the payload raw if no handler is registered.

This keeps the patch small (no new AST variant, no backend pattern
match audits) at the cost of a lightly-punned Raw_markup target. Can
be promoted to a proper variant later.

Two smoke-test plugins ship in odoc-jons-plugins:

{&kbd Ctrl-K} -> <kbd>Ctrl-K</kbd>
{&margin an aside} -> <span class="margin-note">an aside</span>

Both render end-to-end after [opam reinstall odoc odoc-jons-plugins].

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+108 -8
+39
src/extension_api/odoc_extension_api.ml
··· 165 165 [`Binding (key, value)] for key=value pairs. *) 166 166 } 167 167 168 + (** {1 Inline Extensions} 169 + 170 + Extensions can handle inline-level custom elements written as 171 + [{&name payload}] in odoc comments. The extension receives the raw 172 + payload string and returns the HTML that should be spliced into 173 + the output verbatim. 174 + 175 + Example: 176 + {[ 177 + module Margin = struct 178 + let prefix = "margin" 179 + let to_html payload = 180 + Printf.sprintf 181 + {|<span class="margin-note">%s</span>|} 182 + payload 183 + end 184 + 185 + let () = Registry.register_inline (module Margin) 186 + ]} 187 + 188 + In a comment: [Some text with {&margin an aside about X} inline.] 189 + 190 + Inline extensions are HTML-only. On other backends the element 191 + renders to the empty string. Payload is a raw string — the 192 + extension decides how to parse it. *) 193 + 194 + module type Inline_Extension = sig 195 + val prefix : string 196 + (** The inline tag prefix, e.g. ["margin"] handles [{&margin ...}]. *) 197 + 198 + val to_html : string -> string 199 + (** [to_html payload] returns the raw HTML to splice into the output. 200 + The returned string is emitted without further processing; the 201 + extension is responsible for any escaping. *) 202 + end 203 + 168 204 (** The signature that code block extensions must implement *) 169 205 module type Code_Block_Extension = sig 170 206 val prefix : string ··· 238 274 E.link ~tag (Obj.obj env) content 239 275 in 240 276 Odoc_extension_registry.register_link_handler ~prefix:E.prefix link_handler 277 + 278 + let register_inline (module E : Inline_Extension) = 279 + Odoc_extension_registry.register_inline_handler ~prefix:E.prefix E.to_html 241 280 242 281 let register_code_block (module E : Code_Block_Extension) = 243 282 let handler meta content =
+34
src/extension_registry/odoc_extension_registry.ml
··· 235 235 236 236 let find_link_handler ~prefix = 237 237 Hashtbl.find_opt link_handlers prefix 238 + 239 + (** {1 Inline Extension Handlers} 240 + 241 + Extensions can register handlers for inline-level custom elements 242 + written as [{&name payload}] in odoc comments. The handler receives 243 + the raw payload string and returns a chunk of HTML that will be 244 + spliced into the rendered output verbatim. 245 + 246 + These extensions are HTML-only. On other backends the element 247 + renders to the empty string. *) 248 + 249 + type inline_handler = string -> string 250 + (** [payload -> raw html]. *) 251 + 252 + let inline_handlers : (string, inline_handler) Hashtbl.t = Hashtbl.create 16 253 + 254 + let inline_prefixes : (string, unit) Hashtbl.t = Hashtbl.create 16 255 + 256 + let register_inline_handler ~prefix (handler : inline_handler) = 257 + Hashtbl.replace inline_handlers prefix handler; 258 + Hashtbl.replace inline_prefixes prefix () 259 + 260 + let find_inline_handler ~prefix = 261 + Hashtbl.find_opt inline_handlers prefix 262 + 263 + let list_inline_prefixes () = 264 + Hashtbl.fold (fun prefix () acc -> prefix :: acc) inline_prefixes [] 265 + |> List.sort String.compare 266 + 267 + (** Synthetic target prefix used in [Comment.Raw_markup] to carry 268 + inline-extension payloads through the AST without adding a new 269 + variant. Lexer emits [Raw_markup (Some (inline_extension_target_prefix 270 + ^ name), payload)]; renderers detect the prefix and dispatch. *) 271 + let inline_extension_target_prefix = "odoc-ext:"
+22 -8
src/html/generator.ml
··· 83 83 84 84 and raw_markup (t : Raw_markup.t) = 85 85 let target, content = t in 86 - match Astring.String.Ascii.lowercase target with 87 - | "html" -> 88 - (* This is OK because we output *textual* HTML. 89 - In theory, we should try to parse the HTML with lambdasoup and rebuild 90 - the HTML tree from there. 91 - *) 92 - [ Html.Unsafe.data content ] 93 - | _ -> [] 86 + let lowercase_target = Astring.String.Ascii.lowercase target in 87 + let ext_prefix = Odoc_extension_registry.inline_extension_target_prefix in 88 + let ext_prefix_len = Stdlib.String.length ext_prefix in 89 + if Stdlib.String.length lowercase_target >= ext_prefix_len 90 + && Stdlib.String.sub lowercase_target 0 ext_prefix_len = ext_prefix 91 + then 92 + let name = 93 + Stdlib.String.sub lowercase_target ext_prefix_len 94 + (Stdlib.String.length lowercase_target - ext_prefix_len) 95 + in 96 + match Odoc_extension_registry.find_inline_handler ~prefix:name with 97 + | Some handler -> [ Html.Unsafe.data (handler content) ] 98 + | None -> [ Html.Unsafe.data content ] 99 + else 100 + match lowercase_target with 101 + | "html" -> 102 + (* This is OK because we output *textual* HTML. 103 + In theory, we should try to parse the HTML with lambdasoup and rebuild 104 + the HTML tree from there. 105 + *) 106 + [ Html.Unsafe.data content ] 107 + | _ -> [] 94 108 95 109 and source k ?a ?mode_links (t : Source.t) = 96 110 let rec token (x : Source.token) =
+13
src/parser/lexer.mll
··· 423 423 ~in_what:(Token.describe token)); 424 424 emit input token } 425 425 426 + | "{&" (language_tag_char+ as ext_name) horizontal_space+ 427 + ([^ '}']* as ext_payload) ('}' | eof as ext_end) 428 + { 429 + let token = `Raw_markup (Some ("odoc-ext:" ^ ext_name), ext_payload) in 430 + if ext_end <> "}" then 431 + warning 432 + input 433 + ~start_offset:(Lexing.lexeme_end lexbuf) 434 + (Parse_error.not_allowed 435 + ~what:(Token.describe `End) 436 + ~in_what:(Token.describe token)); 437 + emit input token } 438 + 426 439 | "{ul" 427 440 { emit input (`Begin_list `Unordered) } 428 441