Source file rfc_extension.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
(** RFC extension for odoc.

    Provides tags for linking to IETF RFCs:
    - [@rfc 9110] - Link to RFC 9110
    - [@rfc 9110 Section 5.5] - Link to RFC 9110 Section 5.5
    - [@rfc 9110 section-5.5] - Link to RFC 9110 with anchor

    The extension generates links to https://www.rfc-editor.org/rfc/rfcNNNN
*)

open Odoc_extension_api

let prefix = "rfc"

(** CSS styles for RFC references *)
let rfc_css = {|
/* RFC extension styles */
.rfc-reference {
  font-family: monospace;
  background: #f5f5f5;
  padding: 0.1em 0.3em;
  border-radius: 3px;
  border: 1px solid #ddd;
}
.rfc-reference a {
  text-decoration: none;
  color: #0366d6;
}
.rfc-reference a:hover {
  text-decoration: underline;
}
|}

(** Parse RFC reference from tag content.
    Supports formats:
    - "9110" -> RFC 9110
    - "9110 Section 5.5" -> RFC 9110 Section 5.5
    - "9110 section-5.5" -> RFC 9110 with anchor #section-5.5
    - "RFC 9110" -> RFC 9110 (optional RFC prefix)
*)
let parse_rfc_reference content =
  let text = String.trim (text_of_nestable_block_elements content) in
  (* Remove optional "RFC " prefix *)
  let text =
    if String.length text > 4 &&
       (String.sub text 0 4 = "RFC " || String.sub text 0 4 = "rfc ") then
      String.sub text 4 (String.length text - 4)
    else
      text
  in
  (* Split on first space to get RFC number and optional section *)
  match String.index_opt text ' ' with
  | None ->
      (* Just RFC number *)
      (text, None)
  | Some i ->
      let rfc_num = String.sub text 0 i in
      let rest = String.trim (String.sub text (i + 1) (String.length text - i - 1)) in
      (* Check if it's "Section X.Y" or an anchor like "section-5.5" *)
      if String.length rest > 8 &&
         (String.sub rest 0 8 = "Section " || String.sub rest 0 8 = "section ") then
        (rfc_num, Some (`Section (String.sub rest 8 (String.length rest - 8))))
      else if String.length rest > 0 && rest.[0] = '#' then
        (rfc_num, Some (`Anchor (String.sub rest 1 (String.length rest - 1))))
      else if String.contains rest '-' then
        (* Treat as anchor if it contains a hyphen (e.g., "section-5.5") *)
        (rfc_num, Some (`Anchor rest))
      else
        (rfc_num, Some (`Section rest))

(** Generate URL for RFC reference *)
let rfc_url rfc_num section =
  let base = Printf.sprintf "https://www.rfc-editor.org/rfc/rfc%s" rfc_num in
  match section with
  | None -> base
  | Some (`Anchor anchor) -> base ^ "#" ^ anchor
  | Some (`Section sec) ->
      (* Convert "5.5" to "section-5.5" anchor *)
      let anchor = "section-" ^ (String.map (fun c -> if c = ' ' then '-' else c) sec) in
      base ^ "#" ^ anchor

(** Generate display text for RFC reference *)
let rfc_display_text rfc_num section =
  match section with
  | None -> Printf.sprintf "RFC %s" rfc_num
  | Some (`Anchor _) -> Printf.sprintf "RFC %s" rfc_num
  | Some (`Section sec) -> Printf.sprintf "RFC %s Section %s" rfc_num sec

(** Document phase - generate RFC link *)
let to_document ~tag:_ content =
  let rfc_num, section = parse_rfc_reference content in
  let url = rfc_url rfc_num section in
  let display = rfc_display_text rfc_num section in

  (* Create inline link wrapped in styled span *)
  let link_inline = Inline.[{
    attr = [];
    desc = Link {
      target = External url;
      content = [{ attr = []; desc = Text display }];
      tooltip = Some (Printf.sprintf "IETF %s" display);
    }
  }] in

  (* Wrap in a span with rfc-reference class *)
  let content = Block.[{
    attr = [ "rfc-reference" ];
    desc = Inline link_inline
  }] in

  {
    content;
    overrides = [];
    resources = [
      Css_url "extensions/rfc.css";
    ];
    assets = [];
  }

(* Register extension and support files *)
let () =
  Registry.register (module struct
    let prefix = prefix
    let to_document = to_document
  end);
  Registry.register_support_file ~prefix {
    filename = "extensions/rfc.css";
    content = rfc_css;
  }