An RFC extension for odoc
1(** RFC extension for odoc.
2
3 Provides tags for linking to IETF RFCs:
4 - [@rfc 9110] - Link to RFC 9110
5 - [@rfc 9110 Section 5.5] - Link to RFC 9110 Section 5.5
6 - [@rfc 9110 section-5.5] - Link to RFC 9110 with anchor
7
8 The extension generates links to https://www.rfc-editor.org/rfc/rfcNNNN
9*)
10
11open Odoc_extension_api
12
13let prefix = "rfc"
14
15(** CSS styles for RFC references *)
16let rfc_css = {|
17/* RFC extension styles */
18.rfc-reference {
19 font-family: monospace;
20 background: #f5f5f5;
21 padding: 0.1em 0.3em;
22 border-radius: 3px;
23 border: 1px solid #ddd;
24}
25.rfc-reference a {
26 text-decoration: none;
27 color: #0366d6;
28}
29.rfc-reference a:hover {
30 text-decoration: underline;
31}
32|}
33
34(** Parse RFC reference from tag content.
35 Supports formats:
36 - "9110" -> RFC 9110
37 - "9110 Section 5.5" -> RFC 9110 Section 5.5
38 - "9110 section-5.5" -> RFC 9110 with anchor #section-5.5
39 - "RFC 9110" -> RFC 9110 (optional RFC prefix)
40*)
41let parse_rfc_reference content =
42 let text = String.trim (text_of_nestable_block_elements content) in
43 (* Remove optional "RFC " prefix *)
44 let text =
45 if String.length text > 4 &&
46 (String.sub text 0 4 = "RFC " || String.sub text 0 4 = "rfc ") then
47 String.sub text 4 (String.length text - 4)
48 else
49 text
50 in
51 (* Split on first space to get RFC number and optional section *)
52 match String.index_opt text ' ' with
53 | None ->
54 (* Just RFC number *)
55 (text, None)
56 | Some i ->
57 let rfc_num = String.sub text 0 i in
58 let rest = String.trim (String.sub text (i + 1) (String.length text - i - 1)) in
59 (* Check if it's "Section X.Y" or an anchor like "section-5.5" *)
60 if String.length rest > 8 &&
61 (String.sub rest 0 8 = "Section " || String.sub rest 0 8 = "section ") then
62 (rfc_num, Some (`Section (String.sub rest 8 (String.length rest - 8))))
63 else if String.length rest > 0 && rest.[0] = '#' then
64 (rfc_num, Some (`Anchor (String.sub rest 1 (String.length rest - 1))))
65 else if String.contains rest '-' then
66 (* Treat as anchor if it contains a hyphen (e.g., "section-5.5") *)
67 (rfc_num, Some (`Anchor rest))
68 else
69 (rfc_num, Some (`Section rest))
70
71(** Generate URL for RFC reference *)
72let rfc_url rfc_num section =
73 let base = Printf.sprintf "https://www.rfc-editor.org/rfc/rfc%s" rfc_num in
74 match section with
75 | None -> base
76 | Some (`Anchor anchor) -> base ^ "#" ^ anchor
77 | Some (`Section sec) ->
78 (* Convert "5.5" to "section-5.5" anchor *)
79 let anchor = "section-" ^ (String.map (fun c -> if c = ' ' then '-' else c) sec) in
80 base ^ "#" ^ anchor
81
82(** Generate display text for RFC reference *)
83let rfc_display_text rfc_num section =
84 match section with
85 | None -> Printf.sprintf "RFC %s" rfc_num
86 | Some (`Anchor _) -> Printf.sprintf "RFC %s" rfc_num
87 | Some (`Section sec) -> Printf.sprintf "RFC %s Section %s" rfc_num sec
88
89(** Document phase - generate RFC link *)
90let to_document ~tag:_ content =
91 let rfc_num, section = parse_rfc_reference content in
92 let url = rfc_url rfc_num section in
93 let display = rfc_display_text rfc_num section in
94
95 (* Create inline link wrapped in styled span *)
96 let link_inline = Inline.[{
97 attr = [];
98 desc = Link {
99 target = External url;
100 content = [{ attr = []; desc = Text display }];
101 tooltip = Some (Printf.sprintf "IETF %s" display);
102 }
103 }] in
104
105 (* Wrap in a span with rfc-reference class *)
106 let content = Block.[{
107 attr = [ "rfc-reference" ];
108 desc = Inline link_inline
109 }] in
110
111 {
112 content;
113 overrides = [];
114 resources = [
115 Css_url "extensions/rfc.css";
116 ];
117 assets = [];
118 }
119
120(* Register extension and support files *)
121let () =
122 Registry.register (module struct
123 let prefix = prefix
124 let to_document = to_document
125 end);
126 Registry.register_support_file ~prefix {
127 filename = "extensions/rfc.css";
128 content = Inline rfc_css;
129 }