My aggregated monorepo of OCaml code, automaintained
at main 397 lines 13 kB view raw
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" ]