Testing of the @doc-json output
at main 502 lines 17 kB view raw
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)