My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Remove odoc-jons-plugins for subtree re-add

-2009
-11
odoc-jons-plugins/dune-project
··· 1 - (lang dune 3.18) 2 - (using dune_site 0.1) 3 - (name odoc-jons-plugins) 4 - (generate_opam_files true) 5 - 6 - (package 7 - (name odoc-jons-plugins) 8 - (synopsis "odoc shell and extensions for jon.recoil.org") 9 - (depends 10 - (ocaml (>= 4.14)) 11 - odoc))
-25
odoc-jons-plugins/odoc-jons-plugins.opam
··· 1 - # This file is generated by dune, edit dune-project instead 2 - opam-version: "2.0" 3 - synopsis: "odoc shell and extensions for jon.recoil.org" 4 - depends: [ 5 - "dune" {>= "3.18"} 6 - "ocaml" {>= "4.14"} 7 - "odoc" 8 - ] 9 - build: [ 10 - ["dune" "subst"] {dev} 11 - [ 12 - "dune" 13 - "build" 14 - "-p" 15 - name 16 - "-j" 17 - jobs 18 - "--promote-install-files=false" 19 - "@install" 20 - "@runtest" {with-test} 21 - "@doc" {with-doc} 22 - ] 23 - ["dune" "install" "-p" name "--create-install-files" name] 24 - ] 25 - x-maintenance-intent: ["(latest)"]
-9
odoc-jons-plugins/src/dune
··· 1 - (library 2 - (public_name odoc-jons-plugins.impl) 3 - (name odoc_jons_plugins) 4 - (libraries odoc.html odoc.extension_api)) 5 - 6 - (plugin 7 - (name odoc-jons-plugins) 8 - (libraries odoc-jons-plugins.impl) 9 - (site (odoc extensions)))
-784
odoc-jons-plugins/src/odoc_jons_plugins.ml
··· 1 - (* odoc-jons-plugins: Shell and extensions for jon.recoil.org. 2 - Registers the "jon-shell" shell and metadata tag extensions. *) 3 - 4 - open Odoc_utils 5 - module Html = Tyxml.Html 6 - module Url = Odoc_document.Url 7 - 8 - (* Register CSS and JS as support files *) 9 - let () = 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 - (* Sidebar data is now loaded at runtime from sidebar.json instead of being 22 - embedded inline in every page. This avoids duplicating ~200KB of JSON 23 - across every HTML file and is essential for large package sets. *) 24 - 25 - (* --- Helpers --- *) 26 - 27 - let file_uri ~config ~url (base : Odoc_html.Types.uri) file = 28 - match base with 29 - | Odoc_html.Types.Absolute uri -> uri ^ "/" ^ file 30 - | Relative uri -> 31 - let page = Url.Path.{ kind = `File; parent = uri; name = file } in 32 - Odoc_html.Link.href ~config ~resolve:(Current url) (Url.from_path page) 33 - 34 - (* --- Config-driven meta tags --- *) 35 - 36 - let xocaml_meta_tags config = 37 - let prefix = "x-ocaml." in 38 - Odoc_html.Config.config_values config 39 - |> List.filter_map (fun (k, v) -> 40 - match String.cut ~sep:prefix k with 41 - | Some ("", suffix) -> 42 - let meta_name = "x-ocaml-" ^ suffix in 43 - Some 44 - (Html.meta ~a:[ Html.a_name meta_name; Html.a_content v ] ()) 45 - | _ -> None) 46 - 47 - (* --- Page assembly --- *) 48 - 49 - let page_creator ~config ~url ~uses_katex ~resources ~sidebar_data:_ ~header 50 - ~preamble content = 51 - let support_uri = Odoc_html.Config.support_uri config in 52 - let file_uri = file_uri ~config ~url in 53 - let shell_css_uri = file_uri support_uri "extensions/jon-shell.css" in 54 - let shell_js_uri = file_uri support_uri "extensions/jon-shell.js" in 55 - 56 - (* Compute BASE_URL - relative path from current page to root *) 57 - let base_url = 58 - let page = Url.Path.{ kind = `File; parent = None; name = "" } in 59 - Odoc_html.Link.href ~config ~resolve:(Current url) (Url.from_path page) 60 - in 61 - 62 - (* Current URL as relative path from root *) 63 - let current_url = 64 - let filename = Odoc_html.Link.Path.as_filename ~config url in 65 - Fpath.to_string filename 66 - in 67 - 68 - (* Deduplicate resources *) 69 - let deduplicate_resources resources = 70 - let rec aux seen acc = function 71 - | [] -> List.rev acc 72 - | r :: rest -> 73 - if List.mem r seen then aux seen acc rest 74 - else aux (r :: seen) (r :: acc) rest 75 - in 76 - aux [] [] resources 77 - in 78 - 79 - (* Extension resources: all go in head for SPA resource discovery *) 80 - let extension_head_elements = 81 - let open Odoc_extension_registry in 82 - let is_absolute_url url = 83 - String.is_prefix ~affix:"http://" url 84 - || String.is_prefix ~affix:"https://" url 85 - in 86 - let resources = deduplicate_resources resources in 87 - List.concat_map 88 - (function 89 - | Css_url css_url -> 90 - let resolved = 91 - if is_absolute_url css_url then css_url 92 - else file_uri support_uri css_url 93 - in 94 - [ Html.link ~rel:[ `Stylesheet ] ~href:resolved () ] 95 - | Css_inline code -> [ Html.style [ Html.cdata_style code ] ] 96 - | Js_url js_url -> 97 - let resolved = 98 - if is_absolute_url js_url then js_url 99 - else file_uri support_uri js_url 100 - in 101 - [ Html.script ~a:[ Html.a_src resolved ] (Html.txt "") ] 102 - | Js_inline code -> 103 - let id = 104 - Printf.sprintf "%x" (Hashtbl.hash code land 0x7FFFFFFF) 105 - in 106 - [ 107 - Html.script 108 - ~a:[ Html.a_user_data "spa-inline" id ] 109 - (Html.cdata_script code); 110 - ]) 111 - resources 112 - in 113 - 114 - (* KaTeX support *) 115 - let katex_elements = 116 - if uses_katex then 117 - let theme_uri = Odoc_html.Config.theme_uri config in 118 - let katex_css_uri = file_uri theme_uri "katex.min.css" in 119 - let katex_js_uri = file_uri support_uri "katex.min.js" in 120 - [ 121 - Html.link ~rel:[ `Stylesheet ] ~href:katex_css_uri (); 122 - Html.script ~a:[ Html.a_src katex_js_uri ] (Html.txt ""); 123 - Html.script 124 - (Html.cdata_script 125 - {| 126 - document.addEventListener("DOMContentLoaded", function () { 127 - var macros = {}; 128 - var elements = Array.from(document.getElementsByClassName("odoc-katex-math")); 129 - for (var i = 0; i < elements.length; i++) { 130 - var el = elements[i]; 131 - var content = el.textContent; 132 - var new_el = document.createElement("span"); 133 - new_el.setAttribute("class", "odoc-katex-math-rendered"); 134 - var display = el.classList.contains("display"); 135 - katex.render(content, new_el, { throwOnError: false, displayMode: display, macros }); 136 - el.replaceWith(new_el); 137 - } 138 - }); 139 - |}); 140 - ] 141 - else [] 142 - in 143 - 144 - let title_string = url.name in 145 - 146 - let head : Html_types.head Html.elt = 147 - let meta_elements = 148 - [ 149 - Html.meta ~a:[ Html.a_charset "utf-8" ] (); 150 - Html.meta 151 - ~a: 152 - [ 153 - Html.a_name "viewport"; 154 - Html.a_content "width=device-width, initial-scale=1"; 155 - ] 156 - (); 157 - Html.link ~rel:[ `Stylesheet ] ~href:shell_css_uri (); 158 - (* Inject BASE_URL and CURRENT_URL for SPA JS *) 159 - Html.script 160 - (Html.Unsafe.data 161 - (Printf.sprintf "window.BASE_URL = %S; window.CURRENT_URL = %S;" 162 - base_url current_url)); 163 - ] 164 - @ xocaml_meta_tags config 165 - @ katex_elements @ extension_head_elements 166 - in 167 - Html.head (Html.title (Html.txt title_string)) meta_elements 168 - in 169 - 170 - (* Only render the sidebar container for reference pages — 171 - JS will populate it from sidebar.json *) 172 - let sidebar_nav = 173 - if Astring.String.is_prefix ~affix:"reference/" current_url then 174 - [ 175 - Html.nav 176 - ~a: 177 - [ 178 - Html.a_class [ "jon-shell-sidebar"; "odoc-global-toc" ]; 179 - Html.a_id "sidebar-content"; 180 - ] 181 - []; 182 - ] 183 - else [] 184 - in 185 - 186 - let body = 187 - [ 188 - Html.header 189 - ~a:[ Html.a_class [ "jon-shell-header" ] ] 190 - [ 191 - Html.button 192 - ~a: 193 - [ 194 - Html.a_class [ "jon-shell-sidebar-toggle" ]; 195 - Html.a_title "Toggle sidebar"; 196 - ] 197 - [ Html.txt "\xe2\x98\xb0" ]; 198 - Html.a ~a:[ Html.a_href "/" ] [ Html.txt "jon.recoil.org" ]; 199 - Html.nav 200 - [ 201 - Html.a ~a:[ Html.a_href "/blog/" ] [ Html.txt "blog" ]; 202 - Html.a ~a:[ Html.a_href "/notebooks/" ] [ Html.txt "notebooks" ]; 203 - Html.a ~a:[ Html.a_href "/projects/" ] [ Html.txt "projects" ]; 204 - Html.a ~a:[ Html.a_href "/reference/" ] [ Html.txt "reference" ]; 205 - ]; 206 - ]; 207 - Html.main 208 - ~a:[ Html.a_class [ "jon-shell-main" ] ] 209 - (sidebar_nav 210 - @ [ 211 - Html.div 212 - ~a:[ Html.a_class [ "odoc-content" ] ] 213 - ((header :> Html_types.div_content Html.elt list) 214 - @ (preamble :> Html_types.div_content Html.elt list) 215 - @ content); 216 - ]); 217 - Html.footer 218 - ~a:[ Html.a_class [ "jon-shell-footer" ] ] 219 - [ Html.txt "jon ludlam" ]; 220 - ] 221 - @ [ 222 - Html.script 223 - ~a:[ Html.a_src shell_js_uri; Html.a_defer () ] 224 - (Html.txt ""); 225 - ] 226 - in 227 - 228 - let htmlpp = Html.pp ~indent:(Odoc_html.Config.indent config) () in 229 - let html = 230 - Html.html head (Html.body ~a:[ Html.a_class [ "odoc"; "jon-shell" ] ] body) 231 - in 232 - let content ppf = 233 - htmlpp ppf html; 234 - Format.pp_force_newline ppf () 235 - in 236 - content 237 - 238 - let make ~config ~url ~header ~preamble ~uses_katex ~resources ~sidebar_data 239 - ~assets content children = 240 - let filename = Odoc_html.Link.Path.as_filename ~config url in 241 - let content = 242 - page_creator ~config ~url ~uses_katex ~resources ~sidebar_data ~header 243 - ~preamble content 244 - in 245 - { Odoc_document.Renderer.filename; content; children; path = url; assets } 246 - 247 - let src_page_creator ~config ~url ~header ~sidebar_data:_ title content = 248 - let support_uri = Odoc_html.Config.support_uri config in 249 - let file_uri = file_uri ~config ~url in 250 - let shell_css_uri = file_uri support_uri "extensions/jon-shell.css" in 251 - let shell_js_uri = file_uri support_uri "extensions/jon-shell.js" in 252 - 253 - (* Compute BASE_URL and CURRENT_URL for SPA *) 254 - let base_url = 255 - let page = Url.Path.{ kind = `File; parent = None; name = "" } in 256 - Odoc_html.Link.href ~config ~resolve:(Current url) (Url.from_path page) 257 - in 258 - let current_url = 259 - let filename = Odoc_html.Link.Path.as_filename ~config url in 260 - Fpath.to_string filename 261 - in 262 - 263 - let title_string = Printf.sprintf "Source: %s" title in 264 - 265 - let head : Html_types.head Html.elt = 266 - let meta_elements = 267 - [ 268 - Html.meta ~a:[ Html.a_charset "utf-8" ] (); 269 - Html.meta 270 - ~a: 271 - [ 272 - Html.a_name "viewport"; 273 - Html.a_content "width=device-width, initial-scale=1"; 274 - ] 275 - (); 276 - Html.link ~rel:[ `Stylesheet ] ~href:shell_css_uri (); 277 - Html.script 278 - (Html.Unsafe.data 279 - (Printf.sprintf "window.BASE_URL = %S; window.CURRENT_URL = %S;" 280 - base_url current_url)); 281 - ] 282 - @ xocaml_meta_tags config 283 - in 284 - Html.head (Html.title (Html.txt title_string)) meta_elements 285 - in 286 - 287 - (* Only render the sidebar container for reference pages — 288 - JS will populate it from sidebar.json *) 289 - let sidebar_nav = 290 - if Astring.String.is_prefix ~affix:"reference/" current_url then 291 - [ 292 - Html.nav 293 - ~a: 294 - [ 295 - Html.a_class [ "jon-shell-sidebar"; "odoc-global-toc" ]; 296 - Html.a_id "sidebar-content"; 297 - ] 298 - []; 299 - ] 300 - else [] 301 - in 302 - 303 - let body = 304 - [ 305 - Html.header 306 - ~a:[ Html.a_class [ "jon-shell-header" ] ] 307 - [ 308 - Html.button 309 - ~a: 310 - [ 311 - Html.a_class [ "jon-shell-sidebar-toggle" ]; 312 - Html.a_title "Toggle sidebar"; 313 - ] 314 - [ Html.txt "\xe2\x98\xb0" ]; 315 - Html.a ~a:[ Html.a_href "/" ] [ Html.txt "jon.recoil.org" ]; 316 - Html.nav 317 - [ 318 - Html.a ~a:[ Html.a_href "/blog/" ] [ Html.txt "blog" ]; 319 - Html.a ~a:[ Html.a_href "/notebooks/" ] [ Html.txt "notebooks" ]; 320 - Html.a ~a:[ Html.a_href "/projects/" ] [ Html.txt "projects" ]; 321 - Html.a ~a:[ Html.a_href "/reference/" ] [ Html.txt "reference" ]; 322 - ]; 323 - ]; 324 - Html.main 325 - ~a:[ Html.a_class [ "jon-shell-main" ] ] 326 - (sidebar_nav 327 - @ [ 328 - Html.div 329 - ~a:[ Html.a_class [ "odoc-content" ] ] 330 - ((header :> Html_types.div_content Html.elt list) 331 - @ (content :> Html_types.div_content Html.elt list)); 332 - ]); 333 - Html.footer 334 - ~a:[ Html.a_class [ "jon-shell-footer" ] ] 335 - [ Html.txt "jon ludlam" ]; 336 - Html.script 337 - ~a:[ Html.a_src shell_js_uri; Html.a_defer () ] 338 - (Html.txt ""); 339 - ] 340 - in 341 - 342 - let htmlpp = Html.pp ~indent:false () in 343 - let html = 344 - Html.html head 345 - (Html.body ~a:[ Html.a_class [ "odoc-src"; "jon-shell" ] ] body) 346 - in 347 - let content ppf = 348 - htmlpp ppf html; 349 - Format.pp_force_newline ppf () 350 - in 351 - content 352 - 353 - let make_src ~config ~url ~header ~sidebar_data title content = 354 - let filename = Odoc_html.Link.Path.as_filename ~config url in 355 - let content = 356 - src_page_creator ~config ~url ~header ~sidebar_data title content 357 - in 358 - { 359 - Odoc_document.Renderer.filename; 360 - content; 361 - children = []; 362 - path = url; 363 - assets = []; 364 - } 365 - 366 - (* Register the shell *) 367 - let () = 368 - Odoc_html.Html_shell.register 369 - (module struct 370 - let name = "jon-shell" 371 - 372 - let make ~config (data : Odoc_html.Html_shell.page_data) = 373 - make ~config ~url:data.url ~header:data.header ~preamble:data.preamble 374 - ~uses_katex:data.uses_katex ~resources:data.resources 375 - ~sidebar_data:data.sidebar_data ~assets:data.assets data.content 376 - data.children 377 - 378 - let make_src ~config (data : Odoc_html.Html_shell.src_page_data) = 379 - make_src ~config ~url:data.url ~header:data.header 380 - ~sidebar_data:data.sidebar_data data.title data.content 381 - end) 382 - 383 - (* --- Metadata tag extensions --- 384 - 385 - Custom tags like @published, @notanotebook, and @packages are used as 386 - metadata for tooling (feed generation, blog indexing) but should not 387 - appear in the rendered HTML. We register extension handlers that 388 - suppress them by returning empty content. *) 389 - 390 - module Api = Odoc_extension_api 391 - 392 - let hidden_tag_extension prefix = 393 - let module E = struct 394 - let prefix = prefix 395 - let to_document ~tag:_ _content = 396 - Api.simple_output [] 397 - end in 398 - Api.Registry.register (module E) 399 - 400 - let () = 401 - List.iter hidden_tag_extension [ "published"; "notanotebook"; "packages" ] 402 - 403 - (* --- Recent posts extension --- *) 404 - 405 - module Recent_posts = struct 406 - open Odoc_document.Types 407 - 408 - let prefix = "recent-posts" 409 - 410 - (* Check if a string matches YYYY-MM-DD date pattern *) 411 - let is_date s = 412 - let s = Stdlib.String.trim s in 413 - Stdlib.String.length s = 10 414 - && s.[4] = '-' && s.[7] = '-' 415 - && (try 416 - let _ = int_of_string (Stdlib.String.sub s 0 4) in 417 - let _ = int_of_string (Stdlib.String.sub s 5 2) in 418 - let _ = int_of_string (Stdlib.String.sub s 8 2) in 419 - true 420 - with _ -> false) 421 - 422 - (* Format "2025-12-15" -> "Dec 2025" *) 423 - let format_date s = 424 - let s = Stdlib.String.trim s in 425 - let year = Stdlib.String.sub s 0 4 in 426 - let month = Stdlib.String.sub s 5 2 in 427 - let m = match month with 428 - | "01" -> "Jan" | "02" -> "Feb" | "03" -> "Mar" | "04" -> "Apr" 429 - | "05" -> "May" | "06" -> "Jun" | "07" -> "Jul" | "08" -> "Aug" 430 - | "09" -> "Sep" | "10" -> "Oct" | "11" -> "Nov" | "12" -> "Dec" 431 - | _ -> month 432 - in 433 - m ^ " " ^ year 434 - 435 - (* Find the first unordered list in Block.t *) 436 - let find_list (blocks : Block.t) = 437 - List.find_map (fun (b : Block.one) -> 438 - match b.desc with 439 - | List (Unordered, items) -> Some items 440 - | _ -> None 441 - ) blocks 442 - 443 - (* Extract the link inline element from a list of inlines *) 444 - let find_link (inlines : Inline.t) = 445 - List.find_map (fun (i : Inline.one) -> 446 - match i.desc with 447 - | Link _ -> Some i 448 - | _ -> None 449 - ) inlines 450 - 451 - (* Extract date string from trailing text in inlines *) 452 - let find_date (inlines : Inline.t) = 453 - let texts = List.filter_map (fun (i : Inline.one) -> 454 - match i.desc with 455 - | Text s when is_date s -> Some (Stdlib.String.trim s) 456 - | _ -> None 457 - ) inlines in 458 - match texts with 459 - | [] -> None 460 - | _ -> Some (List.hd (List.rev texts)) 461 - 462 - (* Extract link and date from inlines *) 463 - let extract_from_inlines inlines = 464 - match find_link inlines, find_date inlines with 465 - | Some link, Some date -> Some (link, date) 466 - | Some link, None -> Some (link, "") 467 - | _ -> None 468 - 469 - (* Extract link and date from a list item (Block.t) *) 470 - let extract_item (item : Block.t) = 471 - List.find_map (fun (b : Block.one) -> 472 - match b.desc with 473 - | Paragraph inlines | Inline inlines -> extract_from_inlines inlines 474 - | _ -> None 475 - ) item 476 - 477 - (* Extract an optional excerpt Block.t from a list item. 478 - The link phase injects synopsis paragraphs as extra elements in list items. 479 - We look for a second paragraph (after the one containing the link/date). *) 480 - let extract_excerpt (item : Block.t) = 481 - let paragraphs = List.filter (fun (b : Block.one) -> 482 - match b.desc with 483 - | Paragraph _ -> true 484 - | _ -> false 485 - ) item in 486 - match paragraphs with 487 - | _ :: excerpt :: _ -> Some excerpt 488 - | _ -> None 489 - 490 - let raw html = Block.{ attr = []; desc = Raw_markup ("html", html) } 491 - 492 - let inline_block (inlines : Inline.t) = 493 - Block.{ attr = []; desc = Inline inlines } 494 - 495 - let recent_posts_css = {| 496 - /* Recent posts extension - neutralize at-tags wrapper */ 497 - .jon-shell-main ul.at-tags:has(li.recent-posts) { 498 - margin: 0; 499 - padding: 0; 500 - list-style: none; 501 - margin-left: 0; 502 - } 503 - .jon-shell-main .at-tags li.recent-posts { 504 - list-style: none; 505 - padding: 0; 506 - padding-left: 0; 507 - margin: 0; 508 - text-indent: 0; 509 - } 510 - .recent-posts { 511 - margin: 1.5rem 0 2rem; 512 - } 513 - .recent-posts-header { 514 - display: flex; 515 - align-items: baseline; 516 - justify-content: space-between; 517 - margin-bottom: 1.25rem; 518 - } 519 - .recent-posts-title { 520 - font-size: 1.35rem; 521 - font-weight: 500; 522 - color: var(--text-color); 523 - letter-spacing: -0.01em; 524 - } 525 - .recent-posts-link { 526 - font-size: 0.85rem; 527 - color: var(--text-muted); 528 - } 529 - .recent-posts-link:hover { 530 - color: var(--link-color); 531 - } 532 - .recent-posts-featured { 533 - background: var(--surface-color, #fff); 534 - border: 1px solid var(--border-color); 535 - border-radius: 10px; 536 - padding: 1.5rem 1.75rem; 537 - margin-bottom: 1.25rem; 538 - transition: box-shadow 0.2s ease; 539 - } 540 - .recent-posts-featured:hover { 541 - box-shadow: 0 2px 12px rgba(0,0,0,0.06); 542 - } 543 - .recent-posts-featured-label { 544 - font-size: 0.72rem; 545 - font-weight: 600; 546 - letter-spacing: 0.08em; 547 - text-transform: uppercase; 548 - color: var(--accent-color, #b44e2d); 549 - margin-bottom: 0.5rem; 550 - } 551 - .recent-posts-featured-title { 552 - font-size: 1.35rem; 553 - font-weight: 500; 554 - line-height: 1.3; 555 - margin-bottom: 0.5rem; 556 - } 557 - .recent-posts-featured-title a { 558 - color: var(--text-color); 559 - text-decoration: none; 560 - } 561 - .recent-posts-featured-title a:hover { 562 - color: var(--accent-color, #b44e2d); 563 - } 564 - .recent-posts-featured-date { 565 - font-size: 0.8rem; 566 - color: var(--text-muted); 567 - } 568 - .recent-posts-featured-excerpt { 569 - font-size: 0.92rem; 570 - line-height: 1.55; 571 - color: var(--text-muted); 572 - margin: 0.75rem 0 0; 573 - display: -webkit-box; 574 - -webkit-line-clamp: 3; 575 - -webkit-box-orient: vertical; 576 - overflow: hidden; 577 - } 578 - .recent-posts-list { 579 - display: flex; 580 - flex-direction: column; 581 - gap: 1px; 582 - background: var(--border-color); 583 - border-radius: 10px; 584 - overflow: hidden; 585 - border: 1px solid var(--border-color); 586 - } 587 - .recent-posts-item { 588 - display: grid; 589 - grid-template-columns: 1fr auto; 590 - gap: 1rem; 591 - align-items: baseline; 592 - padding: 0.875rem 1.25rem; 593 - background: var(--surface-color, #fff); 594 - transition: background 0.15s ease; 595 - } 596 - .recent-posts-item:hover { 597 - background: var(--bg-hover, #f0f2f5); 598 - } 599 - .recent-posts-item-title { 600 - font-size: 1rem; 601 - font-weight: 400; 602 - line-height: 1.4; 603 - } 604 - .recent-posts-item-title a { 605 - color: var(--text-color); 606 - text-decoration: none; 607 - } 608 - .recent-posts-item-title a:hover { 609 - color: var(--accent-color, #b44e2d); 610 - } 611 - .recent-posts-item-date { 612 - font-size: 0.78rem; 613 - color: var(--text-muted); 614 - white-space: nowrap; 615 - font-variant-numeric: tabular-nums; 616 - } 617 - @media (prefers-color-scheme: dark) { 618 - .recent-posts-featured { 619 - background: var(--surface-color, #22242a); 620 - } 621 - .recent-posts-featured:hover { 622 - box-shadow: 0 2px 12px rgba(0,0,0,0.2); 623 - } 624 - .recent-posts-item { 625 - background: var(--surface-color, #22242a); 626 - } 627 - .recent-posts-item:hover { 628 - background: var(--bg-hover, #2a2d35); 629 - } 630 - } 631 - @media (max-width: 700px) { 632 - .recent-posts-item { 633 - grid-template-columns: 1fr; 634 - gap: 0.25rem; 635 - } 636 - .recent-posts-featured { 637 - padding: 1.25rem; 638 - } 639 - } 640 - |} 641 - 642 - module Identifier = Odoc_model.Paths.Identifier 643 - 644 - (* Build a path hierarchy from a page identifier, for use with Env.lookup_page_by_path. 645 - E.g. LeafPage(Page(Page(Page(None, "jon-site"), "blog"), "2026"), "weeknotes-2026-09") 646 - -> (`TAbsolutePath, ["jon-site"; "blog"; "2026"; "weeknotes-2026-09"]) *) 647 - let rec hierarchy_of_page_id (id : Identifier.Page.t) : string list = 648 - match id.iv with 649 - | `Page (Some parent, name) | `LeafPage (Some parent, name) -> 650 - hierarchy_of_container_id parent @ [Odoc_model.Names.PageName.to_string name] 651 - | `Page (None, name) | `LeafPage (None, name) -> 652 - [Odoc_model.Names.PageName.to_string name] 653 - 654 - and hierarchy_of_container_id (id : Identifier.ContainerPage.t) : string list = 655 - match id.iv with 656 - | `Page (Some parent, name) -> 657 - hierarchy_of_container_id parent @ [Odoc_model.Names.PageName.to_string name] 658 - | `Page (None, name) -> 659 - [Odoc_model.Names.PageName.to_string name] 660 - 661 - (* Extract page identifier from a resolved reference in a list item's content *) 662 - let extract_page_id_from_item 663 - (item : Api.Comment.nestable_block_element Api.Location_.with_location list) = 664 - List.find_map (fun (el : Api.Comment.nestable_block_element Api.Location_.with_location) -> 665 - match el.Api.Location_.value with 666 - | `Paragraph inlines -> 667 - List.find_map (fun (inline : Api.Comment.inline_element Api.Location_.with_location) -> 668 - match inline.Api.Location_.value with 669 - | `Reference (`Resolved r, _) -> ( 670 - match Odoc_model.Paths.Reference.Resolved.identifier r with 671 - | Some id -> ( 672 - match (id : Identifier.t).iv with 673 - | `LeafPage _ as iv -> 674 - Some ({ iv; ihash = id.ihash; ikey = id.ikey } : Identifier.Page.t) 675 - | `Page _ as iv -> 676 - Some ({ iv; ihash = id.ihash; ikey = id.ikey } : Identifier.Page.t) 677 - | _ -> None) 678 - | None -> None) 679 - | _ -> None 680 - ) inlines 681 - | _ -> None 682 - ) item 683 - 684 - (* Link phase: enrich list items with page synopses from the environment. 685 - For each list item that contains a page reference, look up the page 686 - and inject its synopsis as an additional paragraph. *) 687 - let link ~tag:_ env content = 688 - let enrich_item item = 689 - match extract_page_id_from_item item with 690 - | None -> item 691 - | Some page_id -> 692 - let segments = hierarchy_of_page_id page_id in 693 - let hierarchy : Odoc_model.Paths.Reference.Hierarchy.t = 694 - (`TCurrentPackage, segments) 695 - in 696 - (match Api.Env.lookup_page_by_path hierarchy env with 697 - | Error _ -> item 698 - | Ok page -> 699 - (* Find first paragraph, skipping headings and tags *) 700 - let synopsis = 701 - List.find_map (fun (el : Odoc_model.Comment.block_element Api.Location_.with_location) -> 702 - match el.Api.Location_.value with 703 - | `Paragraph p -> Some p 704 - | `Heading _ | `Tag _ -> None 705 - | _ -> None 706 - ) page.content.elements 707 - in 708 - match synopsis with 709 - | None -> item 710 - | Some synopsis_paragraph -> 711 - let dummy_loc = { 712 - Api.Location_.file = ""; start = { line = 0; column = 0 }; 713 - end_ = { line = 0; column = 0 } 714 - } in 715 - item @ [ { Api.Location_.value = `Paragraph synopsis_paragraph; 716 - location = dummy_loc } ]) 717 - in 718 - List.map (fun (el : Api.Comment.nestable_block_element Api.Location_.with_location) -> 719 - match el.Api.Location_.value with 720 - | `List (kind, items) -> 721 - let enriched_items = List.map enrich_item items in 722 - { el with value = `List (kind, enriched_items) } 723 - | _ -> el 724 - ) content 725 - 726 - let to_document ~tag:_ content = 727 - let blocks = Api.blocks_of_nestable_elements content in 728 - match find_list blocks with 729 - | None -> Api.simple_output blocks 730 - | Some items -> 731 - let parsed = List.filter_map extract_item items in 732 - (* Extract excerpts from the first list item (featured) *) 733 - let featured_excerpt = match items with 734 - | first_item :: _ -> extract_excerpt first_item 735 - | [] -> None 736 - in 737 - match parsed with 738 - | [] -> Api.simple_output blocks 739 - | (featured_link, featured_date) :: rest -> 740 - let result = ref [] in 741 - let add b = result := b :: !result in 742 - (* Section open + header *) 743 - add (raw {|<div class="recent-posts"><div class="recent-posts-header"><span class="recent-posts-title">Recent writing</span><a href="/blog/" class="recent-posts-link">All posts &rarr;</a></div>|}); 744 - (* Featured card *) 745 - add (raw {|<div class="recent-posts-featured"><div class="recent-posts-featured-label">Featured</div><div class="recent-posts-featured-title">|}); 746 - add (inline_block [featured_link]); 747 - let date_html = 748 - if featured_date = "" then "" 749 - else Printf.sprintf {|<div class="recent-posts-featured-date">%s</div>|} (format_date featured_date) 750 - in 751 - add (raw (Printf.sprintf {|</div>%s|} date_html)); 752 - (* Excerpt from referenced page *) 753 - (match featured_excerpt with 754 - | Some excerpt_block -> 755 - (match excerpt_block.desc with 756 - | Paragraph inlines -> 757 - add (raw {|<p class="recent-posts-featured-excerpt">|}); 758 - add (inline_block inlines); 759 - add (raw {|</p>|}) 760 - | _ -> ()) 761 - | None -> ()); 762 - add (raw {|</div>|}); 763 - (* Post list *) 764 - if rest <> [] then begin 765 - add (raw {|<div class="recent-posts-list">|}); 766 - List.iter (fun (link, date) -> 767 - add (raw {|<div class="recent-posts-item"><div class="recent-posts-item-title">|}); 768 - add (inline_block [link]); 769 - let date_str = if date = "" then "" else format_date date in 770 - add (raw (Printf.sprintf {|</div><span class="recent-posts-item-date">%s</span></div>|} date_str)) 771 - ) rest; 772 - add (raw {|</div>|}) 773 - end; 774 - (* Section close *) 775 - add (raw {|</div>|}); 776 - { 777 - content = List.rev !result; 778 - overrides = []; 779 - resources = [Css_inline recent_posts_css]; 780 - assets = []; 781 - } 782 - end 783 - 784 - let () = Api.Registry.register_with_link (module Recent_posts)
-788
odoc-jons-plugins/src/odoc_jons_plugins_css.ml
··· 1 - let css = 2 - {| 3 - :root { 4 - --max-width: 700px; 5 - --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 6 - "Helvetica Neue", Arial, sans-serif; 7 - --font-mono: "SF Mono", Consolas, "Liberation Mono", Menlo, monospace; 8 - --bg-color: #ffffff; 9 - --text-color: #1a1a2e; 10 - --text-muted: #6b7280; 11 - --link-color: #b44e2d; 12 - --link-hover: #943f24; 13 - --border-color: #e5e7eb; 14 - --code-bg: #f6f8fa; 15 - --code-border: #e5e7eb; 16 - --header-bg: #ffffff; 17 - --highlight-bg: rgba(180, 78, 45, 0.08); 18 - 19 - /* x-ocaml interactive cells */ 20 - --xo-font-size: 0.875rem; 21 - --xo-bg: var(--code-bg); 22 - --xo-text: var(--text-color); 23 - --xo-gutter-bg: var(--code-bg); 24 - --xo-gutter-text: var(--text-muted); 25 - --xo-gutter-border: var(--border-color); 26 - --xo-stdout-bg: rgba(180, 78, 45, 0.06); 27 - --xo-stdout-text: var(--link-color); 28 - --xo-stderr-bg: rgba(218, 9, 9, 0.06); 29 - --xo-stderr-text: #cf222e; 30 - --xo-meta-bg: var(--code-bg); 31 - --xo-meta-text: var(--text-muted); 32 - --xo-tooltip-bg: var(--bg-color); 33 - --xo-tooltip-text: var(--text-color); 34 - --xo-tooltip-border: var(--border-color); 35 - --xo-btn-bg: var(--code-bg); 36 - --xo-btn-border: var(--border-color); 37 - --xo-btn-text: var(--text-muted); 38 - --xo-btn-hover-bg: var(--text-muted); 39 - --xo-btn-hover-text: var(--bg-color); 40 - } 41 - 42 - @media (prefers-color-scheme: dark) { 43 - :root { 44 - --bg-color: #0d1117; 45 - --text-color: #e6edf3; 46 - --text-muted: #8b949e; 47 - --link-color: #e07850; 48 - --link-hover: #f09070; 49 - --border-color: #30363d; 50 - --code-bg: #161b22; 51 - --code-border: #30363d; 52 - --header-bg: #161b22; 53 - --highlight-bg: rgba(224, 120, 80, 0.12); 54 - 55 - /* x-ocaml interactive cells - dark overrides */ 56 - --xo-stdout-bg: rgba(88, 166, 255, 0.08); 57 - --xo-stdout-text: #79c0ff; 58 - --xo-stderr-bg: rgba(248, 81, 73, 0.08); 59 - --xo-stderr-text: #f85149; 60 - } 61 - } 62 - 63 - /* Reset */ 64 - * { 65 - box-sizing: border-box; 66 - margin: 0; 67 - padding: 0; 68 - } 69 - 70 - body { 71 - font-family: var(--font-body); 72 - font-size: 16px; 73 - line-height: 1.7; 74 - color: var(--text-color); 75 - background: var(--bg-color); 76 - } 77 - 78 - a { 79 - color: var(--link-color); 80 - text-decoration: none; 81 - } 82 - 83 - a:hover { 84 - color: var(--link-hover); 85 - text-decoration: underline; 86 - } 87 - 88 - /* Header */ 89 - .jon-shell-header { 90 - display: flex; 91 - align-items: center; 92 - gap: 12px; 93 - max-width: calc(var(--max-width) + 300px); 94 - margin: 0 auto; 95 - padding: 16px 20px; 96 - font-size: 14px; 97 - border-bottom: 1px solid var(--border-color); 98 - } 99 - 100 - .jon-shell-header > a { 101 - font-weight: 600; 102 - color: var(--text-color); 103 - text-decoration: none; 104 - } 105 - 106 - .jon-shell-header > a:hover { 107 - color: var(--link-color); 108 - } 109 - 110 - .jon-shell-header nav { 111 - display: flex; 112 - gap: 20px; 113 - margin-left: auto; 114 - } 115 - 116 - .jon-shell-header nav a { 117 - color: var(--text-muted); 118 - text-decoration: none; 119 - } 120 - 121 - .jon-shell-header nav a:hover { 122 - color: var(--link-color); 123 - } 124 - 125 - /* Main content */ 126 - .jon-shell-main { 127 - max-width: calc(var(--max-width) + 300px); 128 - margin: 0 auto; 129 - padding: 24px 20px 60px; 130 - display: flex; 131 - gap: 32px; 132 - } 133 - 134 - .jon-shell-main .odoc-content { 135 - flex: 1; 136 - min-width: 0; 137 - } 138 - 139 - /* Sidebar */ 140 - .jon-shell-sidebar { 141 - width: 260px; 142 - flex-shrink: 0; 143 - font-size: 0.85rem; 144 - line-height: 1.5; 145 - position: sticky; 146 - top: 24px; 147 - align-self: flex-start; 148 - max-height: calc(100vh - 48px); 149 - overflow-y: auto; 150 - } 151 - .jon-shell-sidebar:empty { 152 - display: none; 153 - } 154 - 155 - /* Remove indentation from the top-level sidebar list */ 156 - 157 - .jon-shell-sidebar ul ul { 158 - padding-left: 12px; 159 - margin-left: 4px; 160 - border-left: 1px solid var(--border-color); 161 - } 162 - 163 - .jon-shell-sidebar li { 164 - margin: 0; 165 - } 166 - 167 - .jon-shell-sidebar a, 168 - .jon-shell-sidebar .sidebar-label { 169 - display: block; 170 - padding: 2px 8px; 171 - color: var(--text-muted); 172 - text-decoration: none; 173 - border-radius: 4px; 174 - } 175 - 176 - .jon-shell-sidebar a:hover { 177 - color: var(--link-color); 178 - background: var(--highlight-bg); 179 - text-decoration: none; 180 - } 181 - 182 - .jon-shell-sidebar a.current_unit { 183 - color: var(--link-color); 184 - font-weight: 600; 185 - background: none; 186 - } 187 - 188 - /* Collapsible entries — small inline chevron before the link */ 189 - .jon-shell-sidebar .sidebar-toggle { 190 - display: inline-flex; 191 - align-items: center; 192 - justify-content: center; 193 - width: 20px; 194 - height: 20px; 195 - cursor: pointer; 196 - color: var(--text-muted); 197 - opacity: 0.55; 198 - font-size: 1em; 199 - vertical-align: middle; 200 - flex-shrink: 0; 201 - transition: opacity 0.15s, transform 0.15s; 202 - user-select: none; 203 - } 204 - 205 - .jon-shell-sidebar .sidebar-toggle:hover { 206 - opacity: 0.8; 207 - } 208 - 209 - .jon-shell-sidebar .sidebar-toggle::before { 210 - content: "\25B8"; 211 - display: block; 212 - transition: transform 0.15s; 213 - } 214 - 215 - .jon-shell-sidebar li:not(.collapsed) > .sidebar-toggle::before { 216 - transform: rotate(90deg); 217 - } 218 - 219 - .jon-shell-sidebar li.collapsed > ul { 220 - display: none; 221 - } 222 - 223 - /* Items with children: put toggle and link on same line */ 224 - .jon-shell-sidebar li:has(> .sidebar-toggle) { 225 - display: flex; 226 - flex-wrap: wrap; 227 - align-items: baseline; 228 - } 229 - 230 - .jon-shell-sidebar li:has(> .sidebar-toggle) > a, 231 - .jon-shell-sidebar li:has(> .sidebar-toggle) > .sidebar-label { 232 - flex: 1; 233 - min-width: 0; 234 - } 235 - 236 - .jon-shell-sidebar li:has(> .sidebar-toggle) > ul { 237 - flex-basis: 100%; 238 - } 239 - 240 - /* Items without children: indent to align with toggle items */ 241 - .jon-shell-sidebar li:not(:has(> .sidebar-toggle)) > a, 242 - .jon-shell-sidebar li:not(:has(> .sidebar-toggle)) > .sidebar-label { 243 - margin-left: 20px; 244 - } 245 - 246 - /* Ensure collapsed children are fully hidden including overflow */ 247 - .jon-shell-sidebar li.collapsed > ul { 248 - display: none !important; 249 - } 250 - 251 - /* Hide the bare "index" text breadcrumb from odoc sidebar */ 252 - .jon-shell-sidebar > ul > li:last-child:not(:has(a)) { 253 - display: none; 254 - } 255 - 256 - /* Collapse the top-level wrapper padding so content aligns with header */ 257 - 258 - /* Group headings in the sidebar — static section headers, not collapsible */ 259 - .jon-shell-sidebar .sidebar-group { 260 - display: block; 261 - margin-top: 1rem; 262 - } 263 - .jon-shell-sidebar .sidebar-group:first-child { 264 - margin-top: 0; 265 - } 266 - .jon-shell-sidebar .sidebar-group-heading { 267 - display: block; 268 - padding: 2px 8px; 269 - font-size: 0.75rem; 270 - font-weight: 600; 271 - letter-spacing: 0.04em; 272 - text-transform: uppercase; 273 - color: var(--text-muted); 274 - opacity: 0.7; 275 - margin-bottom: 2px; 276 - } 277 - .jon-shell-sidebar .sidebar-group > ul { 278 - padding-left: 0; 279 - margin-left: 0; 280 - border-left: none; 281 - } 282 - 283 - /* Hero intro - side-by-side text + photo */ 284 - .hero-intro { 285 - display: grid; 286 - grid-template-columns: 1fr auto; 287 - gap: 2rem; 288 - align-items: start; 289 - margin-bottom: 0.5rem; 290 - } 291 - .hero-intro .hero-text { 292 - min-width: 0; 293 - } 294 - .hero-intro .hero-text > p { 295 - margin-top: 0; 296 - } 297 - .hero-photo { 298 - width: 120px; 299 - height: 120px; 300 - border-radius: 50%; 301 - object-fit: cover; 302 - box-shadow: 0 0 0 1px var(--border-color); 303 - margin-top: 4px; 304 - } 305 - @media (max-width: 700px) { 306 - .hero-intro { 307 - grid-template-columns: 1fr; 308 - gap: 1rem; 309 - } 310 - .hero-intro .hero-photo { 311 - order: -1; 312 - } 313 - } 314 - 315 - /* Typography */ 316 - .jon-shell-main h1 { 317 - font-size: 2rem; 318 - font-weight: 700; 319 - line-height: 1.2; 320 - margin-bottom: 24px; 321 - } 322 - 323 - .jon-shell-main h2 { 324 - font-size: 1.5rem; 325 - font-weight: 600; 326 - margin-top: 40px; 327 - margin-bottom: 16px; 328 - } 329 - 330 - .jon-shell-main h3 { 331 - font-size: 1.25rem; 332 - font-weight: 600; 333 - margin-top: 32px; 334 - margin-bottom: 12px; 335 - } 336 - 337 - .jon-shell-main h4, 338 - .jon-shell-main h5, 339 - .jon-shell-main h6 { 340 - font-size: 1.1rem; 341 - font-weight: 600; 342 - margin-top: 24px; 343 - margin-bottom: 8px; 344 - } 345 - 346 - .jon-shell-main p { 347 - margin-bottom: 16px; 348 - } 349 - 350 - .jon-shell-main ul, 351 - .jon-shell-main ol { 352 - margin-bottom: 16px; 353 - padding-left: 28px; 354 - } 355 - 356 - /* Sidebar lists reset — must come after .jon-shell-main ul to win cascade */ 357 - .jon-shell-sidebar ul { 358 - list-style: none; 359 - padding: 0; 360 - margin: 0; 361 - } 362 - 363 - .jon-shell-main li { 364 - margin-bottom: 4px; 365 - } 366 - 367 - .jon-shell-main blockquote { 368 - border-left: 3px solid var(--border-color); 369 - margin: 16px 0; 370 - padding: 8px 16px; 371 - color: var(--text-muted); 372 - } 373 - 374 - .jon-shell-main table { 375 - width: 100%; 376 - border-collapse: collapse; 377 - margin-bottom: 16px; 378 - font-size: 0.95em; 379 - } 380 - 381 - .jon-shell-main th, 382 - .jon-shell-main td { 383 - padding: 10px 12px; 384 - border: 1px solid var(--border-color); 385 - text-align: left; 386 - } 387 - 388 - .jon-shell-main th { 389 - background: var(--code-bg); 390 - font-weight: 600; 391 - } 392 - 393 - /* Code */ 394 - .jon-shell-main code { 395 - font-family: var(--font-mono); 396 - font-size: 0.88em; 397 - background: var(--code-bg); 398 - padding: 2px 6px; 399 - border-radius: 4px; 400 - border: 1px solid var(--code-border); 401 - } 402 - 403 - .jon-shell-main pre { 404 - background: var(--code-bg); 405 - border: 1px solid var(--code-border); 406 - border-radius: 6px; 407 - padding: 16px; 408 - overflow-x: auto; 409 - margin-bottom: 16px; 410 - } 411 - 412 - .jon-shell-main pre code { 413 - background: none; 414 - border: none; 415 - padding: 0; 416 - font-size: 0.875rem; 417 - line-height: 1.5; 418 - } 419 - 420 - /* odoc specifics */ 421 - .odoc-spec { 422 - margin: 16px 0; 423 - padding: 12px 16px; 424 - background: var(--code-bg); 425 - border: 1px solid var(--code-border); 426 - border-radius: 6px; 427 - border-left: 3px solid var(--link-color); 428 - } 429 - 430 - .odoc-spec code { 431 - background: none; 432 - border: none; 433 - padding: 0; 434 - } 435 - 436 - .spec { 437 - font-family: var(--font-mono); 438 - font-size: 0.9rem; 439 - } 440 - 441 - .spec-doc { 442 - margin-top: 8px; 443 - padding-top: 8px; 444 - border-top: 1px solid var(--border-color); 445 - font-size: 0.95rem; 446 - } 447 - 448 - .comment-delim { 449 - display: none; 450 - } 451 - 452 - .odoc-include { 453 - margin: 16px 0; 454 - padding: 12px; 455 - border: 1px solid var(--border-color); 456 - border-left: 3px solid var(--text-muted); 457 - border-radius: 6px; 458 - } 459 - 460 - .odoc-include > details > summary { 461 - cursor: pointer; 462 - font-family: var(--font-mono); 463 - font-size: 0.9rem; 464 - } 465 - 466 - /* Source links float right inside spec blocks and headings */ 467 - a.source_link { 468 - float: right; 469 - color: var(--text-muted); 470 - font-family: var(--font-body); 471 - font-size: 0.8rem; 472 - font-weight: normal; 473 - } 474 - 475 - a.source_link:hover { 476 - color: var(--link-color); 477 - } 478 - 479 - /* Source code pages */ 480 - .source_container { 481 - display: flex; 482 - margin-top: 0; 483 - font-family: var(--font-mono); 484 - font-size: 0.85rem; 485 - line-height: 1.4; 486 - background: var(--code-bg); 487 - border: 1px solid var(--code-border); 488 - border-radius: 6px; 489 - overflow-x: auto; 490 - } 491 - 492 - .source_line_column { 493 - padding: 12px 0; 494 - text-align: right; 495 - color: var(--text-muted); 496 - background: var(--code-bg); 497 - border-right: 1px solid var(--code-border); 498 - user-select: none; 499 - } 500 - 501 - .source_line { 502 - padding: 0 12px; 503 - } 504 - 505 - .source_code { 506 - flex-grow: 1; 507 - padding: 12px 16px; 508 - color: var(--text-color); 509 - overflow-x: auto; 510 - } 511 - 512 - .source_code pre { 513 - margin: 0; 514 - background: none; 515 - border: none; 516 - padding: 0; 517 - } 518 - 519 - .source_code code { 520 - background: none; 521 - border: none; 522 - padding: 0; 523 - font-size: inherit; 524 - } 525 - 526 - .odoc-src pre a { 527 - color: inherit; 528 - } 529 - 530 - /* Source directory listings */ 531 - .odoc-directory::before { 532 - content: "\01F4C1"; 533 - margin-right: 0.3em; 534 - } 535 - 536 - /* Source code syntax highlighting */ 537 - :root { 538 - --src-keyword: #cf222e; 539 - --src-uident: #0550ae; 540 - --src-lident: var(--text-color); 541 - --src-literal: #0a3069; 542 - --src-comment: var(--text-muted); 543 - --src-docstring: #116329; 544 - --src-separator: #953800; 545 - --src-parens: #953800; 546 - --src-operator: #8250df; 547 - --src-underscore: var(--text-muted); 548 - } 549 - 550 - @media (prefers-color-scheme: dark) { 551 - :root { 552 - --src-keyword: #ff7b72; 553 - --src-uident: #79c0ff; 554 - --src-lident: var(--text-color); 555 - --src-literal: #a5d6ff; 556 - --src-comment: var(--text-muted); 557 - --src-docstring: #7ee787; 558 - --src-separator: #d29922; 559 - --src-parens: #d29922; 560 - --src-operator: #d2a8ff; 561 - --src-underscore: var(--text-muted); 562 - } 563 - } 564 - 565 - /* Keywords */ 566 - .AND, .ANDOP, .AS, .ASSERT, 567 - .BAR, .BEGIN, 568 - .CLASS, .CONSTRAINT, 569 - .DO, .DONE, .DOWNTO, 570 - .ELSE, .END, .EXCEPTION, .EXTERNAL, 571 - .FOR, .FUN, .FUNCTION, .FUNCTOR, 572 - .IF, .IN, .INCLUDE, .INHERIT, .INITIALIZER, 573 - .LAZY, .LESSMINUS, .LET, .LETOP, 574 - .MATCH, .METHOD, .MINUSGREATER, .MODULE, .MUTABLE, 575 - .NEW, .NONREC, 576 - .OBJECT, .OF, .OPEN, 577 - .PERCENT, .PRIVATE, 578 - .REC, 579 - .SEMISEMI, .SIG, .STRUCT, 580 - .THEN, .TO, .TRY, .TYPE, 581 - .VAL, .VIRTUAL, 582 - .WHEN, .WITH, .WHILE { 583 - color: var(--src-keyword); 584 - } 585 - 586 - /* Separators */ 587 - .COMMA, .COLON, .COLONGREATER, .SEMI { 588 - color: var(--src-separator); 589 - } 590 - 591 - /* Parens */ 592 - .BARRBRACKET, 593 - .LBRACE, .LBRACELESS, 594 - .LBRACKET, .LBRACKETAT, .LBRACKETATAT, .LBRACKETATATAT, 595 - .LBRACKETBAR, .LBRACKETGREATER, .LBRACKETLESS, 596 - .LBRACKETPERCENT, .LBRACKETPERCENTPERCENT, 597 - .LPAREN, .RBRACE, .RBRACKET, .RPAREN { 598 - color: var(--src-parens); 599 - } 600 - 601 - /* Operators */ 602 - .BANG, .PREFIXOP, 603 - .INFIXOP0, .INFIXOP1, .INFIXOP2, .INFIXOP3, .INFIXOP4, 604 - .BARBAR, .PLUS, .STAR, .AMPERAMPER, .AMPERAND, .COLONEQUAL, 605 - .GREATER, .LESS, .MINUS, .MINUSDOT, .MINUSGREATER, 606 - .OR, .PLUSDOT, .PLUSEQ, .EQUAL { 607 - color: var(--src-operator); 608 - } 609 - 610 - /* Upper case idents */ 611 - .UIDENT, .COLONCOLON, .TRUE, .FALSE { 612 - color: var(--src-uident); 613 - } 614 - 615 - /* Lower case idents */ 616 - .LIDENT, .QUESTION, .QUOTE, .TILDE { 617 - color: var(--src-lident); 618 - } 619 - 620 - /* Literals */ 621 - .STRING, .CHAR, .INT, .FLOAT, .QUOTED_STRING_EXPR, .QUOTED_STRING_ITEM { 622 - color: var(--src-literal); 623 - } 624 - 625 - .UNDERSCORE { 626 - color: var(--src-underscore); 627 - } 628 - 629 - .DOCSTRING { 630 - color: var(--src-docstring); 631 - } 632 - 633 - .COMMENT { 634 - color: var(--src-comment); 635 - font-style: italic; 636 - } 637 - 638 - .anchor { 639 - color: var(--text-muted); 640 - text-decoration: none; 641 - margin-left: 4px; 642 - opacity: 0; 643 - transition: opacity 0.15s; 644 - } 645 - 646 - h1:hover .anchor, h2:hover .anchor, h3:hover .anchor, 647 - h4:hover .anchor, h5:hover .anchor, h6:hover .anchor, 648 - .spec:hover .anchor { 649 - opacity: 1; 650 - } 651 - 652 - .anchor:hover { 653 - color: var(--link-color); 654 - } 655 - 656 - :target { 657 - background: var(--highlight-bg); 658 - border-radius: 4px; 659 - } 660 - 661 - /* Footer */ 662 - .jon-shell-footer { 663 - max-width: calc(var(--max-width) + 300px); 664 - margin: 0 auto; 665 - padding: 24px 20px; 666 - border-top: 1px solid var(--border-color); 667 - font-size: 14px; 668 - color: var(--text-muted); 669 - } 670 - 671 - /* Responsive */ 672 - @media (max-width: 800px) { 673 - .jon-shell-sidebar { 674 - display: none; 675 - } 676 - 677 - .jon-shell-main { 678 - max-width: var(--max-width); 679 - } 680 - 681 - .jon-shell-header { 682 - max-width: var(--max-width); 683 - } 684 - 685 - .jon-shell-footer { 686 - max-width: var(--max-width); 687 - } 688 - } 689 - 690 - @media (max-width: 600px) { 691 - body { 692 - font-size: 16px; 693 - } 694 - 695 - .jon-shell-header { 696 - flex-direction: column; 697 - gap: 8px; 698 - align-items: flex-start; 699 - } 700 - 701 - .jon-shell-main h1 { 702 - font-size: 1.6rem; 703 - } 704 - } 705 - /* Sidebar toggle button */ 706 - .jon-shell-sidebar-toggle { 707 - background: none; 708 - border: 1px solid var(--border-color); 709 - border-radius: 4px; 710 - color: var(--text-muted); 711 - font-size: 14px; 712 - cursor: pointer; 713 - padding: 2px 6px; 714 - line-height: 1; 715 - transition: color 0.15s, border-color 0.15s; 716 - } 717 - .jon-shell-sidebar-toggle:hover { 718 - color: var(--link-color); 719 - border-color: var(--link-color); 720 - } 721 - 722 - /* Hidden sidebar state */ 723 - body.sidebar-hidden .jon-shell-sidebar { display: none; } 724 - 725 - /* ================================================================ 726 - Scrollycode theming 727 - Maps jon-shell variables to --sc-* custom properties. 728 - The extension's structural CSS uses these properties for styling. 729 - ================================================================ */ 730 - 731 - /* Theme custom properties — map shell vars to scrollycode contract */ 732 - .sc-container { 733 - --sc-font-display: var(--font-body); 734 - --sc-font-body: var(--font-body); 735 - --sc-font-code: var(--font-mono); 736 - --sc-bg: var(--bg-color); 737 - --sc-text: var(--text-color); 738 - --sc-text-dim: var(--text-muted); 739 - --sc-accent: var(--link-color); 740 - --sc-accent-soft: var(--highlight-bg); 741 - --sc-code-bg: #1a1a2e; 742 - --sc-code-text: #d4d0c8; 743 - --sc-code-gutter: #3a3a52; 744 - --sc-border: var(--border-color); 745 - --sc-focus-bg: var(--highlight-bg); 746 - --sc-panel-radius: 12px; 747 - --sc-mobile-step-bg: rgba(255,255,255,0.5); 748 - 749 - /* Syntax highlighting */ 750 - --sc-hl-keyword: #f0a6a0; 751 - --sc-hl-type: #8ec8e8; 752 - --sc-hl-string: #b8d89a; 753 - --sc-hl-comment: #6a6a82; 754 - --sc-hl-number: #ddb97a; 755 - --sc-hl-module: #e8c87a; 756 - --sc-hl-operator: #c8a8d8; 757 - --sc-hl-punct: #7a7a92; 758 - } 759 - 760 - /* Hero: centered for jon-shell */ 761 - .sc-container .sc-hero { 762 - border-bottom: 1px solid var(--sc-border); 763 - text-align: center; 764 - } 765 - .sc-container .sc-hero p { 766 - margin: 0 auto; 767 - } 768 - 769 - /* Dark mode overrides */ 770 - @media (prefers-color-scheme: dark) { 771 - .sc-container { 772 - --sc-code-bg: #0f0f18; 773 - --sc-code-text: #c8c5d8; 774 - --sc-code-gutter: #2a2a3e; 775 - --sc-mobile-step-bg: rgba(255,255,255,0.04); 776 - 777 - /* Dark syntax colors */ 778 - --sc-hl-keyword: #ff7eb3; 779 - --sc-hl-type: #7dd3fc; 780 - --sc-hl-string: #4ade80; 781 - --sc-hl-comment: #4a4a62; 782 - --sc-hl-number: #fbbf24; 783 - --sc-hl-module: #c4b5fd; 784 - --sc-hl-operator: #67e8f9; 785 - --sc-hl-punct: #4a4a62; 786 - } 787 - } 788 - |}
-392
odoc-jons-plugins/src/odoc_jons_plugins_js.ml
··· 1 - let js = 2 - {| 3 - // Global state 4 - var BASE_URL = window.BASE_URL || './'; 5 - var CURRENT_URL = window.CURRENT_URL || 'index.html'; 6 - 7 - // Compute the root URL for absolute fetching (handles SPA navigation) 8 - var ROOT_URL = new URL(BASE_URL, window.location.href).href; 9 - 10 - // DOMParser for SPA navigation 11 - var parser = new DOMParser(); 12 - 13 - // Package groups for sidebar organisation 14 - var PACKAGE_GROUPS = [ 15 - { name: 'odoc Core', 16 - packages: ['odoc', 'odoc-parser', 'odoc-driver', 'odoc-bench', 'sherlodoc'] }, 17 - { name: 'odoc Extensions', 18 - match: function(pkg) { 19 - return /^odoc-/.test(pkg) && ['odoc-parser', 'odoc-driver', 'odoc-bench'].indexOf(pkg) < 0; 20 - } }, 21 - { name: 'js_top_worker', 22 - match: function(pkg) { return /^js_top_worker/.test(pkg); } }, 23 - { name: 'Tessera', 24 - match: function(pkg) { return /^tessera-/.test(pkg); } } 25 - ]; 26 - 27 - // Extract package name from a sidebar entry's URL (e.g. "reference/odoc/index.html" -> "odoc") 28 - function pkgName(entry) { 29 - var url = entry.node && entry.node.url; 30 - if (!url) return ''; 31 - var parts = url.split('/'); 32 - return parts.length >= 2 ? parts[parts.length - 2] : ''; 33 - } 34 - 35 - // Group package entries under collapsible group headers. 36 - // Operates on the children of the "reference" node (depth 2 in the tree). 37 - function groupPackages(entries) { 38 - var groups = {}; 39 - var ungrouped = []; 40 - 41 - entries.forEach(function(entry) { 42 - var pkg = pkgName(entry); 43 - var matched = false; 44 - for (var i = 0; i < PACKAGE_GROUPS.length; i++) { 45 - var g = PACKAGE_GROUPS[i]; 46 - var inGroup = g.packages 47 - ? g.packages.indexOf(pkg) >= 0 48 - : g.match && g.match(pkg); 49 - if (inGroup) { 50 - if (!groups[g.name]) groups[g.name] = []; 51 - groups[g.name].push(entry); 52 - matched = true; 53 - break; 54 - } 55 - } 56 - if (!matched) ungrouped.push(entry); 57 - }); 58 - 59 - var result = []; 60 - PACKAGE_GROUPS.forEach(function(g) { 61 - var members = groups[g.name]; 62 - if (members && members.length > 0) { 63 - result.push({ 64 - node: { content: g.name, kind: 'group', url: null }, 65 - children: members 66 - }); 67 - } 68 - }); 69 - return result.concat(ungrouped); 70 - } 71 - 72 - // Sidebar rendering 73 - function renderEntry(entry) { 74 - var node = entry.node; 75 - var children = entry.children || []; 76 - var hasChildren = children.length > 0; 77 - 78 - // Group entries render as a heading + always-visible children (not collapsible) 79 - if (node.kind === 'group') { 80 - var html = '<li class="sidebar-group">'; 81 - html += '<span class="sidebar-group-heading">' + node.content + '</span>'; 82 - if (hasChildren) { 83 - html += '<ul>' + children.map(renderEntry).join('') + '</ul>'; 84 - } 85 - return html + '</li>'; 86 - } 87 - 88 - var isActive = node.url === CURRENT_URL; 89 - var li = '<li'; 90 - if (hasChildren) li += ' class="collapsed"'; 91 - li += '>'; 92 - if (hasChildren) { 93 - li += '<span class="sidebar-toggle"></span>'; 94 - } 95 - if (node.url) { 96 - li += '<a href="' + BASE_URL + node.url + '"' + 97 - ' data-nav="' + node.url + '"' + 98 - (isActive ? ' class="current_unit"' : '') + '>' + 99 - node.content + '</a>'; 100 - } else { 101 - li += '<span class="sidebar-label">' + node.content + '</span>'; 102 - } 103 - if (hasChildren) { 104 - li += '<ul>' + children.map(renderEntry).join('') + '</ul>'; 105 - } 106 - return li + '</li>'; 107 - } 108 - 109 - function initSidebar(data) { 110 - var container = document.getElementById('sidebar-content'); 111 - if (!container) return; 112 - // Flatten single-child wrapper nodes (e.g. "OCaml package documentation" > 113 - // "reference") — they add nesting without value. 114 - var entries = data; 115 - while (entries.length === 1 && entries[0].children && entries[0].children.length > 0) { 116 - entries = entries[0].children; 117 - } 118 - // Group the packages into sections 119 - entries = groupPackages(entries); 120 - var html = '<ul>' + entries.map(renderEntry).join('') + '</ul>'; 121 - container.innerHTML = html; 122 - 123 - // Toggle click handler — only the chevron toggle controls expand/collapse 124 - container.addEventListener('click', function(e) { 125 - var toggle = e.target.closest('.sidebar-toggle'); 126 - if (!toggle) return; 127 - e.preventDefault(); 128 - e.stopPropagation(); 129 - var li = toggle.parentElement; 130 - li.classList.toggle('collapsed'); 131 - }); 132 - 133 - updateSidebarActive(); 134 - } 135 - 136 - function updateSidebarActive() { 137 - var container = document.getElementById('sidebar-content'); 138 - if (!container) return; 139 - 140 - // Remove old active 141 - container.querySelectorAll('.current_unit').forEach(function(el) { 142 - el.classList.remove('current_unit'); 143 - }); 144 - 145 - // Find exact match 146 - var activeLink = container.querySelector('[data-nav="' + CURRENT_URL + '"]'); 147 - 148 - // If no exact match, try ancestor URLs 149 - if (!activeLink) { 150 - var parts = CURRENT_URL.split('#')[0].split('/'); 151 - while (parts.length > 1 && !activeLink) { 152 - parts.pop(); 153 - var tryUrl = parts.join('/') + '/index.html'; 154 - activeLink = container.querySelector('[data-nav="' + tryUrl + '"]'); 155 - } 156 - } 157 - 158 - if (activeLink) { 159 - activeLink.classList.add('current_unit'); 160 - // Expand all ancestor <li> nodes so the active link is visible 161 - var parent = activeLink.parentElement; 162 - while (parent && parent !== container) { 163 - if (parent.tagName === 'LI') { 164 - parent.classList.remove('collapsed'); 165 - } 166 - parent = parent.parentElement; 167 - } 168 - // Scroll into view 169 - setTimeout(function() { 170 - activeLink.scrollIntoView({ block: 'center', behavior: 'instant' }); 171 - }, 0); 172 - } 173 - } 174 - 175 - // SPA Navigation 176 - async function navigateTo(url, pushState) { 177 - if (pushState === undefined) pushState = true; 178 - try { 179 - var response = await fetch(ROOT_URL + url); 180 - if (!response.ok) throw new Error('Failed to load page'); 181 - var html = await response.text(); 182 - var doc = parser.parseFromString(html, 'text/html'); 183 - 184 - // Swap content — use DOM node adoption instead of innerHTML so that 185 - // custom elements (e.g. <x-ocaml>) get properly connected and their 186 - // connectedCallback fires. 187 - var newContent = doc.querySelector('.odoc-content'); 188 - var oldContent = document.querySelector('.odoc-content'); 189 - if (newContent && oldContent) { 190 - oldContent.textContent = ''; 191 - while (newContent.firstChild) { 192 - oldContent.appendChild(document.adoptNode(newContent.firstChild)); 193 - } 194 - } 195 - 196 - // Update title 197 - var newTitle = doc.querySelector('title'); 198 - if (newTitle) { 199 - document.title = newTitle.textContent; 200 - } 201 - 202 - // Update body class (regular page vs source page) 203 - document.body.className = doc.body.className; 204 - 205 - // Load new CSS/JS resources from fetched page 206 - var fetchedPageBase = ROOT_URL + url; 207 - var inlineScripts = Array.from(doc.querySelectorAll('head script:not([src])')); 208 - 209 - var newScriptLoadPromises = []; 210 - doc.querySelectorAll('head link[rel="stylesheet"], head script[src]').forEach(function(el) { 211 - var attr = el.tagName === 'LINK' ? 'href' : 'src'; 212 - var resUrl = el.getAttribute(attr); 213 - if (!resUrl) return; 214 - var abs = new URL(resUrl, fetchedPageBase).href; 215 - var selector = el.tagName === 'LINK' 216 - ? 'link[rel="stylesheet"]' 217 - : 'script[src]'; 218 - var already = Array.from(document.querySelectorAll('head ' + selector)).some(function(existing) { 219 - var existingUrl = existing.getAttribute(attr); 220 - if (!existingUrl) return false; 221 - return new URL(existingUrl, window.location.href).href === abs; 222 - }); 223 - if (!already) { 224 - var node; 225 - if (el.tagName === 'SCRIPT') { 226 - // Must create a fresh script element — cloneNode creates a 227 - // "parser-inserted" script that browsers refuse to execute. 228 - node = document.createElement('script'); 229 - node.src = abs; 230 - var p = new Promise(function(resolve) { 231 - node.onload = resolve; 232 - node.onerror = resolve; 233 - }); 234 - newScriptLoadPromises.push(p); 235 - } else { 236 - node = el.cloneNode(true); 237 - node.setAttribute(attr, abs); 238 - } 239 - document.head.appendChild(node); 240 - } 241 - }); 242 - 243 - // After external scripts load, execute inline scripts (deduplicated) 244 - Promise.all(newScriptLoadPromises).then(function() { 245 - inlineScripts.forEach(function(el) { 246 - var id = el.getAttribute('data-spa-inline'); 247 - if (id && document.querySelector('head script[data-spa-inline="' + id + '"]')) return; 248 - var s = el.cloneNode(true); 249 - document.head.appendChild(s); 250 - }); 251 - }); 252 - 253 - // Update state 254 - CURRENT_URL = url; 255 - if (pushState) { 256 - history.pushState({ url: url }, '', ROOT_URL + url); 257 - } 258 - 259 - updateSidebarActive(); 260 - 261 - // Scroll handling 262 - var hash = url.indexOf('#') >= 0 ? '#' + url.split('#')[1] : ''; 263 - if (hash) { 264 - setTimeout(function() { 265 - var target = document.querySelector(hash); 266 - if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' }); 267 - }, 100); 268 - } else { 269 - window.scrollTo(0, 0); 270 - } 271 - 272 - } catch (e) { 273 - console.error('Navigation failed:', e); 274 - window.location.href = ROOT_URL + url; 275 - } 276 - } 277 - 278 - // Popstate for back/forward 279 - window.addEventListener('popstate', function(e) { 280 - if (e.state && e.state.url) { 281 - navigateTo(e.state.url, false); 282 - } 283 - }); 284 - 285 - // Click interception for SPA navigation 286 - document.addEventListener('click', function(e) { 287 - var link = e.target.closest('a[href]'); 288 - if (!link) return; 289 - 290 - var href = link.getAttribute('href'); 291 - if (!href) return; 292 - 293 - // Skip external, hash-only, mailto, javascript links 294 - if (href.indexOf('http') === 0 || href.indexOf('//') === 0 || href.indexOf('#') === 0 || 295 - href.indexOf('mailto:') === 0 || href.indexOf('javascript:') === 0) { 296 - return; 297 - } 298 - 299 - // If the link has data-nav, use it directly (sidebar links) 300 - var navPath = link.dataset.nav; 301 - if (navPath) { 302 - // Only SPA-navigate within reference/ 303 - if (navPath.indexOf('reference/') !== 0) return; 304 - e.preventDefault(); 305 - navigateTo(navPath); 306 - return; 307 - } 308 - 309 - // Resolve relative href against current URL 310 - var targetUrl = href; 311 - if (href.indexOf('/') === 0) { 312 - targetUrl = href.slice(1); 313 - } else if (href.indexOf('./') === 0) { 314 - var currentDir = CURRENT_URL.substring(0, CURRENT_URL.lastIndexOf('/') + 1); 315 - targetUrl = currentDir + href.slice(2); 316 - } else if (href.indexOf('../') === 0) { 317 - var currentParts = CURRENT_URL.split('/'); 318 - currentParts.pop(); 319 - var hrefParts = href.split('/'); 320 - for (var i = 0; i < hrefParts.length; i++) { 321 - var part = hrefParts[i]; 322 - if (part === '..') { 323 - currentParts.pop(); 324 - } else if (part !== '.') { 325 - currentParts.push(part); 326 - } 327 - } 328 - targetUrl = currentParts.join('/'); 329 - } else { 330 - var curDir = CURRENT_URL.substring(0, CURRENT_URL.lastIndexOf('/') + 1); 331 - targetUrl = curDir + href; 332 - } 333 - 334 - // Skip if navigating to same page with just a hash change 335 - if (targetUrl.indexOf('#') >= 0) { 336 - var pathAndHash = targetUrl.split('#'); 337 - if (pathAndHash[0] === CURRENT_URL || pathAndHash[0] === '') { 338 - return; 339 - } 340 - } 341 - 342 - // Skip links outside /reference/ (e.g. /blog/ in header) 343 - if (targetUrl.indexOf('reference/') !== 0) { 344 - return; 345 - } 346 - 347 - e.preventDefault(); 348 - navigateTo(targetUrl); 349 - }); 350 - 351 - // Sidebar toggle 352 - function initSidebarToggle() { 353 - var btn = document.querySelector('.jon-shell-sidebar-toggle'); 354 - if (!btn) return; 355 - var sidebar = document.querySelector('.jon-shell-sidebar'); 356 - if (!sidebar) { btn.style.display = 'none'; return; } 357 - 358 - // Restore preference 359 - if (localStorage.getItem('jon-shell-sidebar') === 'hidden') { 360 - document.body.classList.add('sidebar-hidden'); 361 - } 362 - 363 - btn.addEventListener('click', function() { 364 - document.body.classList.toggle('sidebar-hidden'); 365 - var hidden = document.body.classList.contains('sidebar-hidden'); 366 - localStorage.setItem('jon-shell-sidebar', hidden ? 'hidden' : 'visible'); 367 - }); 368 - } 369 - 370 - // Initialize 371 - (function() { 372 - // Mark header for SPA detection 373 - var header = document.querySelector('.jon-shell-header'); 374 - if (header) header.dataset.spaInit = 'true'; 375 - 376 - // Set initial history state 377 - history.replaceState({ url: CURRENT_URL }, '', window.location.href); 378 - 379 - // Load sidebar data only for reference pages 380 - if (CURRENT_URL.indexOf('reference/') === 0) { 381 - fetch(ROOT_URL + 'sidebar.json') 382 - .then(function(r) { 383 - if (!r.ok) throw new Error('sidebar.json: ' + r.status); 384 - return r.json(); 385 - }) 386 - .then(function(data) { initSidebar(data); }) 387 - .catch(function(e) { console.warn('Failed to load sidebar:', e); }); 388 - } 389 - 390 - initSidebarToggle(); 391 - })(); 392 - |}