Source file odoc_extension_registry.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
(** Odoc Extension Registry

    This module provides a minimal registry for odoc tag extensions.
    It is kept separate to avoid circular dependencies between
    odoc_document and odoc_extension_api.
*)

module Comment = Odoc_model.Comment
module Location_ = Odoc_model.Location_

(** Resources that can be injected into the page (HTML only) *)
type resource =
  | Js_url of string
  | Css_url of string
  | Js_inline of string
  | Css_inline of string

(** Support files that extensions want to output *)
type support_file = {
  filename : string;  (** Relative path, e.g., "extensions/admonition.css" *)
  content : string;   (** File content *)
}

(** Binary asset generated by an extension (e.g., rendered PNG) *)
type asset = {
  asset_filename : string;  (** Filename for the asset, e.g., "diagram-1.png" *)
  asset_content : bytes;    (** Binary content *)
}

(** Documentation for an extension option *)
type option_doc = {
  opt_name : string;        (** Option name, e.g., "width" *)
  opt_description : string; (** What the option does *)
  opt_default : string option; (** Default value if any *)
}

(** Documentation/metadata for an extension *)
type extension_info = {
  info_kind : [ `Tag | `Code_block ];  (** Type of extension *)
  info_prefix : string;                (** The prefix this extension handles *)
  info_description : string;           (** Short description of what it does *)
  info_options : option_doc list;      (** Supported options *)
  info_example : string option;        (** Example usage *)
}

(** Result of processing a custom tag.
    We use a record with a polymorphic content type that gets
    instantiated with the actual Block.t by odoc_document. *)
type 'block extension_result = {
  content : 'block;
  overrides : (string * string) list;
  resources : resource list;
  assets : asset list;
  (** Binary assets to write alongside the HTML output.
      Use [__ODOC_ASSET__filename__] placeholder in content to reference. *)
}

(** Type of handler functions stored in the registry.
    The handler takes a tag name and content, returns an optional result.
    If None, the tag is handled by the default mechanism. *)
type 'block handler =
  string ->  (* tag name *)
  Comment.nestable_block_element Location_.with_location list ->  (* content *)
  'block extension_result option

(** The registry stores handlers indexed by prefix *)
let handlers : (string, Obj.t) Hashtbl.t = Hashtbl.create 16

(** Registered prefixes for listing *)
let prefixes : (string, unit) Hashtbl.t = Hashtbl.create 16

(** Support files registered by extensions *)
let support_files : (string, support_file) Hashtbl.t = Hashtbl.create 16

let register_handler ~prefix (handler : 'block handler) =
  Hashtbl.replace handlers prefix (Obj.repr handler);
  Hashtbl.replace prefixes prefix ()

let register_support_file ~prefix (file : support_file) =
  let key = prefix ^ ":" ^ file.filename in
  Hashtbl.replace support_files key file

let find_handler (type block) ~prefix : block handler option =
  match Hashtbl.find_opt handlers prefix with
  | None -> None
  | Some h -> Some (Obj.obj h)

let list_prefixes () =
  Hashtbl.fold (fun prefix () acc -> prefix :: acc) prefixes []
  |> List.sort String.compare

let list_support_files () =
  Hashtbl.fold (fun _ file acc -> file :: acc) support_files []

(** Extract the prefix from a tag name (part before the first dot) *)
let prefix_of_tag tag =
  match String.index_opt tag '.' with
  | None -> tag
  | Some i -> String.sub tag 0 i

(** {1 Code Block Handlers}

    Similar to custom tag handlers, but for code blocks like [{@dot[...]}].
    Handlers can transform code blocks based on language and metadata.
*)

(** Metadata for code blocks, extracted from parser AST *)
type code_block_meta = {
  language : string;
  tags : Odoc_parser.Ast.code_block_tag list;
}

(** Type of code block handler functions.
    Takes metadata and code content, returns optional transformed result. *)
type 'block code_block_handler =
  code_block_meta ->  (* language + metadata tags *)
  string ->           (* code content *)
  'block extension_result option

(** Registry for code block handlers, indexed by language prefix *)
let code_block_handlers : (string, Obj.t) Hashtbl.t = Hashtbl.create 16

(** Registered code block prefixes *)
let code_block_prefixes : (string, unit) Hashtbl.t = Hashtbl.create 16

let register_code_block_handler ~prefix (handler : 'block code_block_handler) =
  Hashtbl.replace code_block_handlers prefix (Obj.repr handler);
  Hashtbl.replace code_block_prefixes prefix ()

let find_code_block_handler (type block) ~prefix : block code_block_handler option =
  match Hashtbl.find_opt code_block_handlers prefix with
  | None -> None
  | Some h -> Some (Obj.obj h)

let list_code_block_prefixes () =
  Hashtbl.fold (fun prefix () acc -> prefix :: acc) code_block_prefixes []
  |> List.sort String.compare

(** Extract the prefix from a language tag (part before the first dot) *)
let prefix_of_language = prefix_of_tag

(** {1 Extension Documentation}

    Extensions can register documentation that describes their options
    and usage. This is displayed by [odoc extensions]. *)

(** Registry for extension documentation *)
let extension_infos : (string, extension_info) Hashtbl.t = Hashtbl.create 16

let register_extension_info (info : extension_info) =
  let key = match info.info_kind with
    | `Tag -> "tag:" ^ info.info_prefix
    | `Code_block -> "code:" ^ info.info_prefix
  in
  Hashtbl.replace extension_infos key info

let list_extension_infos () =
  Hashtbl.fold (fun _ info acc -> info :: acc) extension_infos []
  |> List.sort (fun a b -> String.compare a.info_prefix b.info_prefix)