Testing of the @doc-json output
1(* odoc-docsite: A docs.rs-inspired shell plugin for odoc.
2 Registers as the "docsite" shell, usable with --shell docsite. *)
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:"docsite"
11 {
12 filename = "extensions/docsite.css";
13 content = Inline Odoc_docsite_css.css;
14 };
15 Odoc_extension_registry.register_support_file ~prefix:"docsite"
16 {
17 filename = "extensions/docsite.js";
18 content = Inline Odoc_docsite_js.js;
19 }
20
21(* Site title from env var, default "Documentation" *)
22let site_title =
23 match Sys.getenv_opt "ODOC_SITE_TITLE" with
24 | Some t -> t
25 | None -> "Documentation"
26
27(* --- Helpers --- *)
28
29let file_uri ~config ~url (base : Odoc_html.Types.uri) file =
30 match base with
31 | Odoc_html.Types.Absolute uri -> uri ^ "/" ^ file
32 | Relative uri ->
33 let page = Url.Path.{ kind = `File; parent = uri; name = file } in
34 Odoc_html.Link.href ~config ~resolve:(Current url) (Url.from_path page)
35
36(* TyXML helper for generating TOC *)
37let html_of_toc toc =
38 let open Odoc_html.Types in
39 let rec section (s : toc) =
40 let link = Html.a ~a:[ Html.a_href s.href ] s.title in
41 match s.children with [] -> [ link ] | cs -> [ link; sections cs ]
42 and sections the_sections =
43 the_sections
44 |> List.map (fun s -> Html.li (section s))
45 |> Html.ul
46 in
47 match toc with [] -> [] | _ -> [ sections toc ]
48
49let toc_section toc =
50 match toc with
51 | [] -> []
52 | _ ->
53 [
54 Html.div
55 ~a:[ Html.a_class [ "odoc-tocs" ] ]
56 [
57 Html.nav
58 ~a:[ Html.a_class [ "odoc-toc"; "odoc-local-toc" ] ]
59 (Html.div
60 ~a:[ Html.a_class [ "odoc-toc-title" ] ]
61 [ Html.txt "On This Page" ]
62 :: html_of_toc toc);
63 ];
64 ]
65
66(* Breadcrumbs *)
67let html_of_breadcrumbs (breadcrumbs : Odoc_html.Types.breadcrumbs) =
68 let space = Html.txt " " in
69 let sep = [ space; Html.entity "#x00BB"; space ] in
70 let html =
71 List.concat_map_sep ~sep
72 ~f:(fun (breadcrumb : Odoc_html.Types.breadcrumb) ->
73 match breadcrumb.href with
74 | Some href ->
75 [
76 [
77 Html.a
78 ~a:[ Html.a_href href ]
79 (breadcrumb.name
80 :> Html_types.flow5_without_interactive Html.elt list);
81 ];
82 ]
83 | None ->
84 [ (breadcrumb.name :> Html_types.nav_content_fun Html.elt list) ])
85 breadcrumbs.parents
86 |> List.flatten
87 in
88 let current_name :> Html_types.nav_content_fun Html.elt list =
89 breadcrumbs.current.name
90 in
91 let rest =
92 if List.is_empty breadcrumbs.parents then current_name
93 else html @ sep @ current_name
94 in
95 (rest :> [< Html_types.nav_content_fun > `A `PCDATA `Wbr ] Html.elt list)
96
97(* Serialize sidebar data to JSON for inline embedding *)
98let sidebar_json_script sidebar_data =
99 match sidebar_data with
100 | None -> []
101 | Some data ->
102 let json = Odoc_html.Sidebar.to_json data in
103 let json_str = Json.to_string json in
104 [
105 Html.script
106 (Html.cdata_script
107 (Printf.sprintf "window.__DOCSITE_SIDEBAR_DATA__ = %s;" json_str));
108 ]
109
110(* --- Page assembly --- *)
111
112let page_creator ~config ~url ~uses_katex ~resources ~sidebar_data ~toc header
113 breadcrumbs content =
114 let support_uri = Odoc_html.Config.support_uri config in
115 let path = Odoc_html.Link.Path.for_printing url in
116 let file_uri = file_uri ~config ~url in
117 let docsite_css_uri = file_uri support_uri "extensions/docsite.css" in
118 let docsite_js_uri = file_uri support_uri "extensions/docsite.js" in
119
120 (* Compute BASE_URL - relative path from current page to root *)
121 let base_url =
122 let page =
123 Url.Path.{ kind = `File; parent = None; name = "" }
124 in
125 Odoc_html.Link.href ~config ~resolve:(Current url) (Url.from_path page)
126 in
127
128 (* Current URL as relative path from root *)
129 let current_url =
130 let filename = Odoc_html.Link.Path.as_filename ~config url in
131 Fpath.to_string filename
132 in
133
134 (* Deduplicate resources *)
135 let deduplicate_resources resources =
136 let rec aux seen acc = function
137 | [] -> List.rev acc
138 | r :: rest ->
139 if List.mem r seen then aux seen acc rest
140 else aux (r :: seen) (r :: acc) rest
141 in
142 aux [] [] resources
143 in
144
145 (* Extension resources *)
146 let extension_head_elements =
147 let open Odoc_extension_registry in
148 let is_absolute_url url =
149 String.is_prefix ~affix:"http://" url
150 || String.is_prefix ~affix:"https://" url
151 in
152 let resources = deduplicate_resources resources in
153 List.concat_map
154 (function
155 | Js_url js_url ->
156 let resolved =
157 if is_absolute_url js_url then js_url
158 else file_uri support_uri js_url
159 in
160 [ Html.script ~a:[ Html.a_src resolved ] (Html.txt "") ]
161 | Css_url css_url ->
162 let resolved =
163 if is_absolute_url css_url then css_url
164 else file_uri support_uri css_url
165 in
166 [ Html.link ~rel:[ `Stylesheet ] ~href:resolved () ]
167 | Js_inline code ->
168 let id = Printf.sprintf "%x" (Hashtbl.hash code land 0x7FFFFFFF) in
169 [ Html.script ~a:[ Html.a_user_data "spa-inline" id ]
170 (Html.cdata_script code) ]
171 | Css_inline code -> [ Html.style [ Html.cdata_style code ] ])
172 resources
173 in
174
175 (* KaTeX support *)
176 let katex_elements =
177 if uses_katex then
178 let theme_uri = Odoc_html.Config.theme_uri config in
179 let katex_css_uri = file_uri theme_uri "katex.min.css" in
180 let katex_js_uri = file_uri support_uri "katex.min.js" in
181 [
182 Html.link ~rel:[ `Stylesheet ] ~href:katex_css_uri ();
183 Html.script ~a:[ Html.a_src katex_js_uri ] (Html.txt "");
184 Html.script
185 (Html.cdata_script
186 {|
187 document.addEventListener("DOMContentLoaded", function () {
188 var macros = {};
189 var elements = Array.from(document.getElementsByClassName("odoc-katex-math"));
190 for (var i = 0; i < elements.length; i++) {
191 var el = elements[i];
192 var content = el.textContent;
193 var new_el = document.createElement("span");
194 new_el.setAttribute("class", "odoc-katex-math-rendered");
195 var display = el.classList.contains("display");
196 katex.render(content, new_el, { throwOnError: false, displayMode: display, macros });
197 el.replaceWith(new_el);
198 }
199 });
200 |});
201 ]
202 else []
203 in
204
205 let head : Html_types.head Html.elt =
206 let title_string =
207 Printf.sprintf "%s (%s)" url.name (String.concat ~sep:"." path)
208 in
209 let meta_elements =
210 [
211 Html.meta ~a:[ Html.a_charset "utf-8" ] ();
212 Html.link ~rel:[ `Stylesheet ] ~href:docsite_css_uri ();
213 Html.meta
214 ~a:
215 [
216 Html.a_name "viewport";
217 Html.a_content "width=device-width,initial-scale=1.0";
218 ]
219 ();
220 Html.meta
221 ~a:[ Html.a_name "generator"; Html.a_content "odoc %%VERSION%%" ]
222 ();
223 (* Inject BASE_URL and CURRENT_URL for the JS *)
224 Html.script
225 (Html.Unsafe.data
226 (Printf.sprintf "window.BASE_URL = %S; window.CURRENT_URL = %S;"
227 base_url current_url));
228 ]
229 @ katex_elements @ extension_head_elements
230 @ sidebar_json_script sidebar_data
231 in
232 Html.head (Html.title (Html.txt title_string)) meta_elements
233 in
234
235 (* Compute the root-relative href for the brand link *)
236 let brand_href =
237 let index_path =
238 Url.Path.{ kind = `LeafPage; parent = None; name = "index" }
239 in
240 Odoc_html.Link.href ~config ~resolve:(Current url) (Url.from_path index_path)
241 in
242
243 let body =
244 [
245 (* Header bar *)
246 Html.header
247 ~a:[ Html.a_class [ "docsite-header" ] ]
248 [
249 (* Mobile menu toggle *)
250 Html.Unsafe.data
251 {|<button class="menu-toggle" aria-label="Toggle menu">
252 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
253 <line x1="3" y1="12" x2="21" y2="12"></line>
254 <line x1="3" y1="6" x2="21" y2="6"></line>
255 <line x1="3" y1="18" x2="21" y2="18"></line>
256 </svg>
257 </button>|};
258 Html.a
259 ~a:
260 [
261 Html.a_href brand_href;
262 Html.a_class [ "docsite-header-brand" ];
263 ]
264 [ Html.txt site_title ];
265 Html.div
266 ~a:[ Html.a_class [ "docsite-search-container" ] ]
267 [
268 Html.div
269 ~a:[ Html.a_class [ "search-wrapper" ] ]
270 [
271 Html.Unsafe.data
272 {|<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
273 <circle cx="11" cy="11" r="8"></circle>
274 <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
275 </svg>|};
276 Html.input
277 ~a:
278 [
279 Html.a_input_type `Text;
280 Html.a_class [ "search-input" ];
281 Html.a_placeholder "Search documentation...";
282 ]
283 ();
284 Html.span
285 ~a:[ Html.a_class [ "search-shortcut" ] ]
286 [ Html.txt "/" ];
287 Html.div ~a:[ Html.a_class [ "search-results" ] ] [];
288 ];
289 ];
290 ];
291 (* Layout: sidebar + main *)
292 Html.div
293 ~a:[ Html.a_class [ "docsite-layout" ] ]
294 [
295 (* Sidebar *)
296 Html.nav
297 ~a:[ Html.a_class [ "docsite-sidebar" ] ]
298 [
299 Html.div
300 ~a:[ Html.a_class [ "package-selector" ] ]
301 [
302 Html.Unsafe.data
303 {|<select id="package-select">
304 <option value="">All packages</option>
305 </select>|};
306 ];
307 Html.div
308 ~a:
309 [
310 Html.a_class [ "sidebar-nav" ];
311 Html.a_id "sidebar-content";
312 ]
313 [ Html.div ~a:[ Html.a_class [ "sidebar-loading" ] ] [ Html.txt "Loading..." ] ];
314 ];
315 (* Main content area *)
316 Html.main
317 ~a:[ Html.a_class [ "docsite-main" ] ]
318 ([ Html.nav ~a:[ Html.a_class [ "odoc-nav" ] ] (html_of_breadcrumbs breadcrumbs) ]
319 @ [ Html.div ~a:[ Html.a_class [ "odoc-content" ] ]
320 ((header :> Html_types.div_content Html.elt list)
321 @ content) ]
322 @ toc_section toc);
323 ];
324 (* Docsite JS *)
325 Html.script ~a:[ Html.a_src docsite_js_uri; Html.a_defer () ] (Html.txt "");
326 ]
327 in
328
329 let htmlpp = Html.pp ~indent:(Odoc_html.Config.indent config) () in
330 let html =
331 Html.html head (Html.body ~a:[ Html.a_class [ "odoc"; "odoc-docsite" ] ] body)
332 in
333 let content ppf =
334 htmlpp ppf html;
335 Format.pp_force_newline ppf ()
336 in
337 content
338
339let make ~config ~url ~header ~breadcrumbs ~sidebar_data ~toc ~uses_katex
340 ~resources ~assets content children =
341 let filename = Odoc_html.Link.Path.as_filename ~config url in
342 let content =
343 page_creator ~config ~url ~uses_katex ~resources ~sidebar_data ~toc header
344 breadcrumbs content
345 in
346 { Odoc_document.Renderer.filename; content; children; path = url; assets }
347
348let src_page_creator ~breadcrumbs ~config ~url ~header ~sidebar_data title
349 content =
350 let support_uri = Odoc_html.Config.support_uri config in
351 let file_uri = file_uri ~config ~url in
352 let docsite_css_uri = file_uri support_uri "extensions/docsite.css" in
353 let docsite_js_uri = file_uri support_uri "extensions/docsite.js" in
354
355 let base_url =
356 let page =
357 Url.Path.{ kind = `File; parent = None; name = "" }
358 in
359 Odoc_html.Link.href ~config ~resolve:(Current url) (Url.from_path page)
360 in
361 let current_url =
362 let filename = Odoc_html.Link.Path.as_filename ~config url in
363 Fpath.to_string filename
364 in
365
366 let brand_href =
367 let index_path =
368 Url.Path.{ kind = `LeafPage; parent = None; name = "index" }
369 in
370 Odoc_html.Link.href ~config ~resolve:(Current url) (Url.from_path index_path)
371 in
372
373 let head : Html_types.head Html.elt =
374 let title_string = Printf.sprintf "Source: %s" title in
375 let meta_elements =
376 [
377 Html.meta ~a:[ Html.a_charset "utf-8" ] ();
378 Html.link ~rel:[ `Stylesheet ] ~href:docsite_css_uri ();
379 Html.meta
380 ~a:
381 [
382 Html.a_name "viewport";
383 Html.a_content "width=device-width,initial-scale=1.0";
384 ]
385 ();
386 Html.script
387 (Html.Unsafe.data
388 (Printf.sprintf "window.BASE_URL = %S; window.CURRENT_URL = %S;"
389 base_url current_url));
390 ]
391 @ sidebar_json_script sidebar_data
392 in
393 Html.head (Html.title (Html.txt title_string)) meta_elements
394 in
395
396 let body =
397 [
398 Html.header
399 ~a:[ Html.a_class [ "docsite-header" ] ]
400 [
401 Html.a
402 ~a:
403 [
404 Html.a_href brand_href;
405 Html.a_class [ "docsite-header-brand" ];
406 ]
407 [ Html.txt site_title ];
408 Html.div
409 ~a:[ Html.a_class [ "docsite-search-container" ] ]
410 [
411 Html.div
412 ~a:[ Html.a_class [ "search-wrapper" ] ]
413 [
414 Html.input
415 ~a:
416 [
417 Html.a_input_type `Text;
418 Html.a_class [ "search-input" ];
419 Html.a_placeholder "Search documentation...";
420 ]
421 ();
422 Html.div ~a:[ Html.a_class [ "search-results" ] ] [];
423 ];
424 ];
425 ];
426 Html.div
427 ~a:[ Html.a_class [ "docsite-layout" ] ]
428 [
429 Html.nav
430 ~a:[ Html.a_class [ "docsite-sidebar" ] ]
431 [
432 Html.div
433 ~a:[ Html.a_class [ "package-selector" ] ]
434 [
435 Html.Unsafe.data
436 {|<select id="package-select">
437 <option value="">All packages</option>
438 </select>|};
439 ];
440 Html.div
441 ~a:
442 [
443 Html.a_class [ "sidebar-nav" ];
444 Html.a_id "sidebar-content";
445 ]
446 [ Html.div ~a:[ Html.a_class [ "sidebar-loading" ] ] [ Html.txt "Loading..." ] ];
447 ];
448 Html.main
449 ~a:[ Html.a_class [ "docsite-main" ] ]
450 ([ Html.nav ~a:[ Html.a_class [ "odoc-nav" ] ] (html_of_breadcrumbs breadcrumbs) ]
451 @ [ Html.header
452 ~a:[ Html.a_class [ "odoc-preamble" ] ]
453 (header :> Html_types.flow5_without_header_footer Html.elt list) ]
454 @ content);
455 ];
456 Html.script ~a:[ Html.a_src docsite_js_uri; Html.a_defer () ] (Html.txt "");
457 ]
458 in
459
460 let htmlpp = Html.pp ~indent:false () in
461 let html =
462 Html.html head
463 (Html.body ~a:[ Html.a_class [ "odoc-src"; "odoc-docsite" ] ] body)
464 in
465 let content ppf =
466 htmlpp ppf html;
467 Format.pp_force_newline ppf ()
468 in
469 content
470
471let make_src ~config ~url ~breadcrumbs ~header ~sidebar_data title content =
472 let filename = Odoc_html.Link.Path.as_filename ~config url in
473 let content =
474 src_page_creator ~breadcrumbs ~config ~url ~header ~sidebar_data title
475 content
476 in
477 {
478 Odoc_document.Renderer.filename;
479 content;
480 children = [];
481 path = url;
482 assets = [];
483 }
484
485(* Register the shell *)
486let () =
487 Odoc_html.Html_shell.register
488 (module struct
489 let name = "docsite"
490
491 let make ~config (data : Odoc_html.Html_shell.page_data) =
492 make ~config ~url:data.url
493 ~header:(data.header @ data.preamble)
494 ~breadcrumbs:data.breadcrumbs ~sidebar_data:data.sidebar_data
495 ~toc:data.toc ~uses_katex:data.uses_katex ~resources:data.resources
496 ~assets:data.assets data.content data.children
497
498 let make_src ~config (data : Odoc_html.Html_shell.src_page_data) =
499 make_src ~config ~url:data.url ~breadcrumbs:data.breadcrumbs
500 ~header:data.header ~sidebar_data:data.sidebar_data data.title
501 data.content
502 end)