forked from
anil.recoil.org/monopam-myspace
My aggregated monorepo of OCaml code, automaintained
1(* odoc-jons-plugins: Shell and extensions for jon.recoil.org.
2 Registers the "jon-shell" shell and metadata tag extensions. *)
3
4open Odoc_utils
5module Html = Tyxml.Html
6module Url = Odoc_document.Url
7
8(* Register CSS and JS as support files *)
9let () =
10 Odoc_extension_registry.register_support_file ~prefix:"jon-shell"
11 {
12 filename = "extensions/jon-shell.css";
13 content = Inline Odoc_jons_plugins_css.css;
14 };
15 Odoc_extension_registry.register_support_file ~prefix:"jon-shell"
16 {
17 filename = "extensions/jon-shell.js";
18 content = Inline Odoc_jons_plugins_js.js;
19 }
20
21(* Serialize sidebar data to JSON for inline embedding *)
22let sidebar_json_script sidebar_data =
23 match sidebar_data with
24 | None -> []
25 | Some data ->
26 let json = Odoc_html.Sidebar.to_json data in
27 let json_str = Json.to_string json in
28 [
29 Html.script
30 (Html.cdata_script
31 (Printf.sprintf "window.__SIDEBAR_DATA__ = %s;" json_str));
32 ]
33
34(* --- Helpers --- *)
35
36let file_uri ~config ~url (base : Odoc_html.Types.uri) file =
37 match base with
38 | Odoc_html.Types.Absolute uri -> uri ^ "/" ^ file
39 | Relative uri ->
40 let page = Url.Path.{ kind = `File; parent = uri; name = file } in
41 Odoc_html.Link.href ~config ~resolve:(Current url) (Url.from_path page)
42
43(* --- Config-driven meta tags --- *)
44
45let xocaml_meta_tags config =
46 let prefix = "x-ocaml." in
47 Odoc_html.Config.config_values config
48 |> List.filter_map (fun (k, v) ->
49 match String.cut ~sep:prefix k with
50 | Some ("", suffix) ->
51 let meta_name = "x-ocaml-" ^ suffix in
52 Some
53 (Html.meta ~a:[ Html.a_name meta_name; Html.a_content v ] ())
54 | _ -> None)
55
56(* --- Page assembly --- *)
57
58let page_creator ~config ~url ~uses_katex ~resources ~sidebar_data ~header
59 ~preamble content =
60 let support_uri = Odoc_html.Config.support_uri config in
61 let file_uri = file_uri ~config ~url in
62 let shell_css_uri = file_uri support_uri "extensions/jon-shell.css" in
63 let shell_js_uri = file_uri support_uri "extensions/jon-shell.js" in
64
65 (* Compute BASE_URL - relative path from current page to root *)
66 let base_url =
67 let page = Url.Path.{ kind = `File; parent = None; name = "" } in
68 Odoc_html.Link.href ~config ~resolve:(Current url) (Url.from_path page)
69 in
70
71 (* Current URL as relative path from root *)
72 let current_url =
73 let filename = Odoc_html.Link.Path.as_filename ~config url in
74 Fpath.to_string filename
75 in
76
77 (* Deduplicate resources *)
78 let deduplicate_resources resources =
79 let rec aux seen acc = function
80 | [] -> List.rev acc
81 | r :: rest ->
82 if List.mem r seen then aux seen acc rest
83 else aux (r :: seen) (r :: acc) rest
84 in
85 aux [] [] resources
86 in
87
88 (* Extension resources: all go in head for SPA resource discovery *)
89 let extension_head_elements =
90 let open Odoc_extension_registry in
91 let is_absolute_url url =
92 String.is_prefix ~affix:"http://" url
93 || String.is_prefix ~affix:"https://" url
94 in
95 let resources = deduplicate_resources resources in
96 List.concat_map
97 (function
98 | Css_url css_url ->
99 let resolved =
100 if is_absolute_url css_url then css_url
101 else file_uri support_uri css_url
102 in
103 [ Html.link ~rel:[ `Stylesheet ] ~href:resolved () ]
104 | Css_inline code -> [ Html.style [ Html.cdata_style code ] ]
105 | Js_url js_url ->
106 let resolved =
107 if is_absolute_url js_url then js_url
108 else file_uri support_uri js_url
109 in
110 [ Html.script ~a:[ Html.a_src resolved ] (Html.txt "") ]
111 | Js_inline code ->
112 let id =
113 Printf.sprintf "%x" (Hashtbl.hash code land 0x7FFFFFFF)
114 in
115 [
116 Html.script
117 ~a:[ Html.a_user_data "spa-inline" id ]
118 (Html.cdata_script code);
119 ])
120 resources
121 in
122
123 (* KaTeX support *)
124 let katex_elements =
125 if uses_katex then
126 let theme_uri = Odoc_html.Config.theme_uri config in
127 let katex_css_uri = file_uri theme_uri "katex.min.css" in
128 let katex_js_uri = file_uri support_uri "katex.min.js" in
129 [
130 Html.link ~rel:[ `Stylesheet ] ~href:katex_css_uri ();
131 Html.script ~a:[ Html.a_src katex_js_uri ] (Html.txt "");
132 Html.script
133 (Html.cdata_script
134 {|
135 document.addEventListener("DOMContentLoaded", function () {
136 var macros = {};
137 var elements = Array.from(document.getElementsByClassName("odoc-katex-math"));
138 for (var i = 0; i < elements.length; i++) {
139 var el = elements[i];
140 var content = el.textContent;
141 var new_el = document.createElement("span");
142 new_el.setAttribute("class", "odoc-katex-math-rendered");
143 var display = el.classList.contains("display");
144 katex.render(content, new_el, { throwOnError: false, displayMode: display, macros });
145 el.replaceWith(new_el);
146 }
147 });
148 |});
149 ]
150 else []
151 in
152
153 let title_string = url.name in
154
155 let head : Html_types.head Html.elt =
156 let meta_elements =
157 [
158 Html.meta ~a:[ Html.a_charset "utf-8" ] ();
159 Html.meta
160 ~a:
161 [
162 Html.a_name "viewport";
163 Html.a_content "width=device-width, initial-scale=1";
164 ]
165 ();
166 Html.link ~rel:[ `Stylesheet ] ~href:shell_css_uri ();
167 (* Inject BASE_URL and CURRENT_URL for SPA JS *)
168 Html.script
169 (Html.Unsafe.data
170 (Printf.sprintf "window.BASE_URL = %S; window.CURRENT_URL = %S;"
171 base_url current_url));
172 ]
173 @ xocaml_meta_tags config
174 @ katex_elements @ extension_head_elements
175 @ sidebar_json_script sidebar_data
176 in
177 Html.head (Html.title (Html.txt title_string)) meta_elements
178 in
179
180 let sidebar_nav =
181 match sidebar_data with
182 | Some _ ->
183 [
184 Html.nav
185 ~a:
186 [
187 Html.a_class [ "jon-shell-sidebar"; "odoc-global-toc" ];
188 Html.a_id "sidebar-content";
189 ]
190 [];
191 ]
192 | None -> []
193 in
194
195 let body =
196 [
197 Html.header
198 ~a:[ Html.a_class [ "jon-shell-header" ] ]
199 [
200 Html.a ~a:[ Html.a_href "/" ] [ Html.txt "jon.recoil.org" ];
201 Html.nav
202 [
203 Html.a ~a:[ Html.a_href "/blog/" ] [ Html.txt "blog" ];
204 Html.a ~a:[ Html.a_href "/notebooks/" ] [ Html.txt "notebooks" ];
205 Html.a ~a:[ Html.a_href "/projects/" ] [ Html.txt "projects" ];
206 Html.a ~a:[ Html.a_href "/reference/" ] [ Html.txt "reference" ];
207 ];
208 ];
209 Html.main
210 ~a:[ Html.a_class [ "jon-shell-main" ] ]
211 (sidebar_nav
212 @ [
213 Html.div
214 ~a:[ Html.a_class [ "odoc-content" ] ]
215 ((header :> Html_types.div_content Html.elt list)
216 @ (preamble :> Html_types.div_content Html.elt list)
217 @ content);
218 ]);
219 Html.footer
220 ~a:[ Html.a_class [ "jon-shell-footer" ] ]
221 [ Html.txt "jon ludlam" ];
222 ]
223 @ [
224 Html.script
225 ~a:[ Html.a_src shell_js_uri; Html.a_defer () ]
226 (Html.txt "");
227 ]
228 in
229
230 let htmlpp = Html.pp ~indent:(Odoc_html.Config.indent config) () in
231 let html =
232 Html.html head (Html.body ~a:[ Html.a_class [ "odoc"; "jon-shell" ] ] body)
233 in
234 let content ppf =
235 htmlpp ppf html;
236 Format.pp_force_newline ppf ()
237 in
238 content
239
240let make ~config ~url ~header ~preamble ~uses_katex ~resources ~sidebar_data
241 ~assets content children =
242 let filename = Odoc_html.Link.Path.as_filename ~config url in
243 let content =
244 page_creator ~config ~url ~uses_katex ~resources ~sidebar_data ~header
245 ~preamble content
246 in
247 { Odoc_document.Renderer.filename; content; children; path = url; assets }
248
249let src_page_creator ~config ~url ~header ~sidebar_data title content =
250 let support_uri = Odoc_html.Config.support_uri config in
251 let file_uri = file_uri ~config ~url in
252 let shell_css_uri = file_uri support_uri "extensions/jon-shell.css" in
253 let shell_js_uri = file_uri support_uri "extensions/jon-shell.js" in
254
255 (* Compute BASE_URL and CURRENT_URL for SPA *)
256 let base_url =
257 let page = Url.Path.{ kind = `File; parent = None; name = "" } in
258 Odoc_html.Link.href ~config ~resolve:(Current url) (Url.from_path page)
259 in
260 let current_url =
261 let filename = Odoc_html.Link.Path.as_filename ~config url in
262 Fpath.to_string filename
263 in
264
265 let title_string = Printf.sprintf "Source: %s" title in
266
267 let head : Html_types.head Html.elt =
268 let meta_elements =
269 [
270 Html.meta ~a:[ Html.a_charset "utf-8" ] ();
271 Html.meta
272 ~a:
273 [
274 Html.a_name "viewport";
275 Html.a_content "width=device-width, initial-scale=1";
276 ]
277 ();
278 Html.link ~rel:[ `Stylesheet ] ~href:shell_css_uri ();
279 Html.script
280 (Html.Unsafe.data
281 (Printf.sprintf "window.BASE_URL = %S; window.CURRENT_URL = %S;"
282 base_url current_url));
283 ]
284 @ xocaml_meta_tags config
285 @ sidebar_json_script sidebar_data
286 in
287 Html.head (Html.title (Html.txt title_string)) meta_elements
288 in
289
290 let sidebar_nav =
291 match sidebar_data with
292 | Some _ ->
293 [
294 Html.nav
295 ~a:
296 [
297 Html.a_class [ "jon-shell-sidebar"; "odoc-global-toc" ];
298 Html.a_id "sidebar-content";
299 ]
300 [];
301 ]
302 | None -> []
303 in
304
305 let body =
306 [
307 Html.header
308 ~a:[ Html.a_class [ "jon-shell-header" ] ]
309 [
310 Html.a ~a:[ Html.a_href "/" ] [ Html.txt "jon.recoil.org" ];
311 Html.nav
312 [
313 Html.a ~a:[ Html.a_href "/blog/" ] [ Html.txt "blog" ];
314 Html.a ~a:[ Html.a_href "/notebooks/" ] [ Html.txt "notebooks" ];
315 Html.a ~a:[ Html.a_href "/projects/" ] [ Html.txt "projects" ];
316 Html.a ~a:[ Html.a_href "/reference/" ] [ Html.txt "reference" ];
317 ];
318 ];
319 Html.main
320 ~a:[ Html.a_class [ "jon-shell-main" ] ]
321 (sidebar_nav
322 @ [
323 Html.div
324 ~a:[ Html.a_class [ "odoc-content" ] ]
325 ((header :> Html_types.div_content Html.elt list)
326 @ (content :> Html_types.div_content Html.elt list));
327 ]);
328 Html.footer
329 ~a:[ Html.a_class [ "jon-shell-footer" ] ]
330 [ Html.txt "jon ludlam" ];
331 Html.script
332 ~a:[ Html.a_src shell_js_uri; Html.a_defer () ]
333 (Html.txt "");
334 ]
335 in
336
337 let htmlpp = Html.pp ~indent:false () in
338 let html =
339 Html.html head
340 (Html.body ~a:[ Html.a_class [ "odoc-src"; "jon-shell" ] ] body)
341 in
342 let content ppf =
343 htmlpp ppf html;
344 Format.pp_force_newline ppf ()
345 in
346 content
347
348let make_src ~config ~url ~header ~sidebar_data title content =
349 let filename = Odoc_html.Link.Path.as_filename ~config url in
350 let content =
351 src_page_creator ~config ~url ~header ~sidebar_data title content
352 in
353 {
354 Odoc_document.Renderer.filename;
355 content;
356 children = [];
357 path = url;
358 assets = [];
359 }
360
361(* Register the shell *)
362let () =
363 Odoc_html.Html_shell.register
364 (module struct
365 let name = "jon-shell"
366
367 let make ~config (data : Odoc_html.Html_shell.page_data) =
368 make ~config ~url:data.url ~header:data.header ~preamble:data.preamble
369 ~uses_katex:data.uses_katex ~resources:data.resources
370 ~sidebar_data:data.sidebar_data ~assets:data.assets data.content
371 data.children
372
373 let make_src ~config (data : Odoc_html.Html_shell.src_page_data) =
374 make_src ~config ~url:data.url ~header:data.header
375 ~sidebar_data:data.sidebar_data data.title data.content
376 end)
377
378(* --- Metadata tag extensions ---
379
380 Custom tags like @published, @notanotebook, and @packages are used as
381 metadata for tooling (feed generation, blog indexing) but should not
382 appear in the rendered HTML. We register extension handlers that
383 suppress them by returning empty content. *)
384
385module Api = Odoc_extension_api
386
387let hidden_tag_extension prefix =
388 let module E = struct
389 let prefix = prefix
390
391 let to_document ~tag:_ _content =
392 Api.simple_output []
393 end in
394 Api.Registry.register (module E)
395
396let () =
397 List.iter hidden_tag_extension [ "published"; "notanotebook"; "packages" ]