Odoc plugins for jon.recoil.org
0
fork

Configure Feed

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

Rename odoc-jon-shell to odoc-jons-plugins and add atom.xml

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+1376
+11
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.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
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)))
+397
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 + (* Serialize sidebar data to JSON for inline embedding *) 22 + let 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 + 36 + let 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 + 45 + let 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 + 58 + let 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 + 240 + let 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 + 249 + let 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 + 348 + let 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 *) 362 + let () = 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 + 385 + module Api = Odoc_extension_api 386 + 387 + let 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 + 396 + let () = 397 + List.iter hidden_tag_extension [ "published"; "notanotebook"; "packages" ]
+658
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: #0969da; 12 + --link-hover: #0550ae; 13 + --border-color: #e5e7eb; 14 + --code-bg: #f6f8fa; 15 + --code-border: #e5e7eb; 16 + --header-bg: #ffffff; 17 + --highlight-bg: rgba(9, 105, 218, 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(9, 105, 218, 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: #58a6ff; 48 + --link-hover: #79c0ff; 49 + --border-color: #30363d; 50 + --code-bg: #161b22; 51 + --code-border: #30363d; 52 + --header-bg: #161b22; 53 + --highlight-bg: rgba(88, 166, 255, 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 + justify-content: space-between; 93 + max-width: calc(var(--max-width) + 300px); 94 + margin: 0 auto; 95 + padding: 16px 20px; 96 + font-size: 14px; 97 + } 98 + 99 + .jon-shell-header > a { 100 + font-weight: 600; 101 + color: var(--text-color); 102 + text-decoration: none; 103 + } 104 + 105 + .jon-shell-header > a:hover { 106 + color: var(--link-color); 107 + } 108 + 109 + .jon-shell-header nav { 110 + display: flex; 111 + gap: 20px; 112 + } 113 + 114 + .jon-shell-header nav a { 115 + color: var(--text-muted); 116 + text-decoration: none; 117 + } 118 + 119 + .jon-shell-header nav a:hover { 120 + color: var(--link-color); 121 + } 122 + 123 + /* Main content */ 124 + .jon-shell-main { 125 + max-width: calc(var(--max-width) + 300px); 126 + margin: 0 auto; 127 + padding: 24px 20px 60px; 128 + display: flex; 129 + gap: 32px; 130 + } 131 + 132 + .jon-shell-main .odoc-content { 133 + flex: 1; 134 + min-width: 0; 135 + } 136 + 137 + /* Sidebar */ 138 + .jon-shell-sidebar { 139 + width: 260px; 140 + flex-shrink: 0; 141 + font-size: 0.85rem; 142 + line-height: 1.5; 143 + position: sticky; 144 + top: 24px; 145 + align-self: flex-start; 146 + max-height: calc(100vh - 48px); 147 + overflow-y: auto; 148 + } 149 + 150 + /* Collapse the top two wrapper levels so content aligns with header */ 151 + .jon-shell-sidebar > ul > li > ul, 152 + .jon-shell-sidebar > ul > li > ul > li > ul { 153 + padding-left: 0; 154 + margin-left: 0; 155 + border-left: none; 156 + } 157 + 158 + .jon-shell-sidebar ul ul { 159 + padding-left: 12px; 160 + margin-left: 4px; 161 + border-left: 1px solid var(--border-color); 162 + } 163 + 164 + .jon-shell-sidebar li { 165 + margin: 0; 166 + } 167 + 168 + .jon-shell-sidebar a, 169 + .jon-shell-sidebar .sidebar-label { 170 + display: block; 171 + padding: 2px 8px; 172 + color: var(--text-muted); 173 + text-decoration: none; 174 + border-radius: 4px; 175 + } 176 + 177 + .jon-shell-sidebar a:hover { 178 + color: var(--link-color); 179 + background: var(--highlight-bg); 180 + text-decoration: none; 181 + } 182 + 183 + .jon-shell-sidebar a.current_unit { 184 + color: var(--link-color); 185 + font-weight: 600; 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 + /* Hide the top-level wrapper entries (e.g. "OCaml package documentation" > 257 + "reference") — they waste space and add confusing nesting. Show their 258 + children directly. */ 259 + .jon-shell-sidebar > ul > li > .sidebar-toggle, 260 + .jon-shell-sidebar > ul > li > a, 261 + .jon-shell-sidebar > ul > li > .sidebar-label, 262 + .jon-shell-sidebar > ul > li > ul > li > .sidebar-toggle, 263 + .jon-shell-sidebar > ul > li > ul > li > a, 264 + .jon-shell-sidebar > ul > li > ul > li > .sidebar-label { 265 + display: none; 266 + } 267 + 268 + /* Typography */ 269 + .jon-shell-main h1 { 270 + font-size: 2rem; 271 + font-weight: 700; 272 + line-height: 1.2; 273 + margin-bottom: 24px; 274 + } 275 + 276 + .jon-shell-main h2 { 277 + font-size: 1.5rem; 278 + font-weight: 600; 279 + margin-top: 40px; 280 + margin-bottom: 16px; 281 + } 282 + 283 + .jon-shell-main h3 { 284 + font-size: 1.25rem; 285 + font-weight: 600; 286 + margin-top: 32px; 287 + margin-bottom: 12px; 288 + } 289 + 290 + .jon-shell-main h4, 291 + .jon-shell-main h5, 292 + .jon-shell-main h6 { 293 + font-size: 1.1rem; 294 + font-weight: 600; 295 + margin-top: 24px; 296 + margin-bottom: 8px; 297 + } 298 + 299 + .jon-shell-main p { 300 + margin-bottom: 16px; 301 + } 302 + 303 + .jon-shell-main ul, 304 + .jon-shell-main ol { 305 + margin-bottom: 16px; 306 + padding-left: 28px; 307 + } 308 + 309 + /* Sidebar lists reset — must come after .jon-shell-main ul to win cascade */ 310 + .jon-shell-sidebar ul { 311 + list-style: none; 312 + padding: 0; 313 + margin: 0; 314 + } 315 + 316 + .jon-shell-main li { 317 + margin-bottom: 4px; 318 + } 319 + 320 + .jon-shell-main blockquote { 321 + border-left: 3px solid var(--border-color); 322 + margin: 16px 0; 323 + padding: 8px 16px; 324 + color: var(--text-muted); 325 + } 326 + 327 + .jon-shell-main table { 328 + width: 100%; 329 + border-collapse: collapse; 330 + margin-bottom: 16px; 331 + font-size: 0.95em; 332 + } 333 + 334 + .jon-shell-main th, 335 + .jon-shell-main td { 336 + padding: 10px 12px; 337 + border: 1px solid var(--border-color); 338 + text-align: left; 339 + } 340 + 341 + .jon-shell-main th { 342 + background: var(--code-bg); 343 + font-weight: 600; 344 + } 345 + 346 + /* Code */ 347 + .jon-shell-main code { 348 + font-family: var(--font-mono); 349 + font-size: 0.88em; 350 + background: var(--code-bg); 351 + padding: 2px 6px; 352 + border-radius: 4px; 353 + border: 1px solid var(--code-border); 354 + } 355 + 356 + .jon-shell-main pre { 357 + background: var(--code-bg); 358 + border: 1px solid var(--code-border); 359 + border-radius: 6px; 360 + padding: 16px; 361 + overflow-x: auto; 362 + margin-bottom: 16px; 363 + } 364 + 365 + .jon-shell-main pre code { 366 + background: none; 367 + border: none; 368 + padding: 0; 369 + font-size: 0.875rem; 370 + line-height: 1.5; 371 + } 372 + 373 + /* odoc specifics */ 374 + .odoc-spec { 375 + margin: 16px 0; 376 + padding: 12px 16px; 377 + background: var(--code-bg); 378 + border: 1px solid var(--code-border); 379 + border-radius: 6px; 380 + border-left: 3px solid var(--link-color); 381 + } 382 + 383 + .odoc-spec code { 384 + background: none; 385 + border: none; 386 + padding: 0; 387 + } 388 + 389 + .spec { 390 + font-family: var(--font-mono); 391 + font-size: 0.9rem; 392 + } 393 + 394 + .spec-doc { 395 + margin-top: 8px; 396 + padding-top: 8px; 397 + border-top: 1px solid var(--border-color); 398 + font-size: 0.95rem; 399 + } 400 + 401 + .comment-delim { 402 + display: none; 403 + } 404 + 405 + .odoc-include { 406 + margin: 16px 0; 407 + padding: 12px; 408 + border: 1px solid var(--border-color); 409 + border-left: 3px solid var(--text-muted); 410 + border-radius: 6px; 411 + } 412 + 413 + .odoc-include > details > summary { 414 + cursor: pointer; 415 + font-family: var(--font-mono); 416 + font-size: 0.9rem; 417 + } 418 + 419 + /* Source links float right inside spec blocks and headings */ 420 + a.source_link { 421 + float: right; 422 + color: var(--text-muted); 423 + font-family: var(--font-body); 424 + font-size: 0.8rem; 425 + font-weight: normal; 426 + } 427 + 428 + a.source_link:hover { 429 + color: var(--link-color); 430 + } 431 + 432 + /* Source code pages */ 433 + .source_container { 434 + display: flex; 435 + margin-top: 0; 436 + font-family: var(--font-mono); 437 + font-size: 0.85rem; 438 + line-height: 1.4; 439 + background: var(--code-bg); 440 + border: 1px solid var(--code-border); 441 + border-radius: 6px; 442 + overflow-x: auto; 443 + } 444 + 445 + .source_line_column { 446 + padding: 12px 0; 447 + text-align: right; 448 + color: var(--text-muted); 449 + background: var(--code-bg); 450 + border-right: 1px solid var(--code-border); 451 + user-select: none; 452 + } 453 + 454 + .source_line { 455 + padding: 0 12px; 456 + } 457 + 458 + .source_code { 459 + flex-grow: 1; 460 + padding: 12px 16px; 461 + color: var(--text-color); 462 + overflow-x: auto; 463 + } 464 + 465 + .source_code pre { 466 + margin: 0; 467 + background: none; 468 + border: none; 469 + padding: 0; 470 + } 471 + 472 + .source_code code { 473 + background: none; 474 + border: none; 475 + padding: 0; 476 + font-size: inherit; 477 + } 478 + 479 + .odoc-src pre a { 480 + color: inherit; 481 + } 482 + 483 + /* Source directory listings */ 484 + .odoc-directory::before { 485 + content: "\01F4C1"; 486 + margin-right: 0.3em; 487 + } 488 + 489 + /* Source code syntax highlighting */ 490 + :root { 491 + --src-keyword: #cf222e; 492 + --src-uident: #0550ae; 493 + --src-lident: var(--text-color); 494 + --src-literal: #0a3069; 495 + --src-comment: var(--text-muted); 496 + --src-docstring: #116329; 497 + --src-separator: #953800; 498 + --src-parens: #953800; 499 + --src-operator: #8250df; 500 + --src-underscore: var(--text-muted); 501 + } 502 + 503 + @media (prefers-color-scheme: dark) { 504 + :root { 505 + --src-keyword: #ff7b72; 506 + --src-uident: #79c0ff; 507 + --src-lident: var(--text-color); 508 + --src-literal: #a5d6ff; 509 + --src-comment: var(--text-muted); 510 + --src-docstring: #7ee787; 511 + --src-separator: #d29922; 512 + --src-parens: #d29922; 513 + --src-operator: #d2a8ff; 514 + --src-underscore: var(--text-muted); 515 + } 516 + } 517 + 518 + /* Keywords */ 519 + .AND, .ANDOP, .AS, .ASSERT, 520 + .BAR, .BEGIN, 521 + .CLASS, .CONSTRAINT, 522 + .DO, .DONE, .DOWNTO, 523 + .ELSE, .END, .EXCEPTION, .EXTERNAL, 524 + .FOR, .FUN, .FUNCTION, .FUNCTOR, 525 + .IF, .IN, .INCLUDE, .INHERIT, .INITIALIZER, 526 + .LAZY, .LESSMINUS, .LET, .LETOP, 527 + .MATCH, .METHOD, .MINUSGREATER, .MODULE, .MUTABLE, 528 + .NEW, .NONREC, 529 + .OBJECT, .OF, .OPEN, 530 + .PERCENT, .PRIVATE, 531 + .REC, 532 + .SEMISEMI, .SIG, .STRUCT, 533 + .THEN, .TO, .TRY, .TYPE, 534 + .VAL, .VIRTUAL, 535 + .WHEN, .WITH, .WHILE { 536 + color: var(--src-keyword); 537 + } 538 + 539 + /* Separators */ 540 + .COMMA, .COLON, .COLONGREATER, .SEMI { 541 + color: var(--src-separator); 542 + } 543 + 544 + /* Parens */ 545 + .BARRBRACKET, 546 + .LBRACE, .LBRACELESS, 547 + .LBRACKET, .LBRACKETAT, .LBRACKETATAT, .LBRACKETATATAT, 548 + .LBRACKETBAR, .LBRACKETGREATER, .LBRACKETLESS, 549 + .LBRACKETPERCENT, .LBRACKETPERCENTPERCENT, 550 + .LPAREN, .RBRACE, .RBRACKET, .RPAREN { 551 + color: var(--src-parens); 552 + } 553 + 554 + /* Operators */ 555 + .BANG, .PREFIXOP, 556 + .INFIXOP0, .INFIXOP1, .INFIXOP2, .INFIXOP3, .INFIXOP4, 557 + .BARBAR, .PLUS, .STAR, .AMPERAMPER, .AMPERAND, .COLONEQUAL, 558 + .GREATER, .LESS, .MINUS, .MINUSDOT, .MINUSGREATER, 559 + .OR, .PLUSDOT, .PLUSEQ, .EQUAL { 560 + color: var(--src-operator); 561 + } 562 + 563 + /* Upper case idents */ 564 + .UIDENT, .COLONCOLON, .TRUE, .FALSE { 565 + color: var(--src-uident); 566 + } 567 + 568 + /* Lower case idents */ 569 + .LIDENT, .QUESTION, .QUOTE, .TILDE { 570 + color: var(--src-lident); 571 + } 572 + 573 + /* Literals */ 574 + .STRING, .CHAR, .INT, .FLOAT, .QUOTED_STRING_EXPR, .QUOTED_STRING_ITEM { 575 + color: var(--src-literal); 576 + } 577 + 578 + .UNDERSCORE { 579 + color: var(--src-underscore); 580 + } 581 + 582 + .DOCSTRING { 583 + color: var(--src-docstring); 584 + } 585 + 586 + .COMMENT { 587 + color: var(--src-comment); 588 + font-style: italic; 589 + } 590 + 591 + .anchor { 592 + color: var(--text-muted); 593 + text-decoration: none; 594 + margin-left: 4px; 595 + opacity: 0; 596 + transition: opacity 0.15s; 597 + } 598 + 599 + h1:hover .anchor, h2:hover .anchor, h3:hover .anchor, 600 + h4:hover .anchor, h5:hover .anchor, h6:hover .anchor, 601 + .spec:hover .anchor { 602 + opacity: 1; 603 + } 604 + 605 + .anchor:hover { 606 + color: var(--link-color); 607 + } 608 + 609 + :target { 610 + background: var(--highlight-bg); 611 + border-radius: 4px; 612 + } 613 + 614 + /* Footer */ 615 + .jon-shell-footer { 616 + max-width: calc(var(--max-width) + 300px); 617 + margin: 0 auto; 618 + padding: 24px 20px; 619 + border-top: 1px solid var(--border-color); 620 + font-size: 14px; 621 + color: var(--text-muted); 622 + } 623 + 624 + /* Responsive */ 625 + @media (max-width: 800px) { 626 + .jon-shell-sidebar { 627 + display: none; 628 + } 629 + 630 + .jon-shell-main { 631 + max-width: var(--max-width); 632 + } 633 + 634 + .jon-shell-header { 635 + max-width: var(--max-width); 636 + } 637 + 638 + .jon-shell-footer { 639 + max-width: var(--max-width); 640 + } 641 + } 642 + 643 + @media (max-width: 600px) { 644 + body { 645 + font-size: 16px; 646 + } 647 + 648 + .jon-shell-header { 649 + flex-direction: column; 650 + gap: 8px; 651 + align-items: flex-start; 652 + } 653 + 654 + .jon-shell-main h1 { 655 + font-size: 1.6rem; 656 + } 657 + } 658 + |}
+276
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 + // Sidebar rendering from __SIDEBAR_DATA__ JSON 14 + function renderEntry(entry) { 15 + var node = entry.node; 16 + var isActive = node.url === CURRENT_URL; 17 + var children = entry.children || []; 18 + var hasChildren = children.length > 0; 19 + var li = '<li'; 20 + if (hasChildren) li += ' class="collapsed"'; 21 + li += '>'; 22 + if (hasChildren) { 23 + li += '<span class="sidebar-toggle"></span>'; 24 + } 25 + if (node.url) { 26 + li += '<a href="' + BASE_URL + node.url + '"' + 27 + ' data-nav="' + node.url + '"' + 28 + (isActive ? ' class="current_unit"' : '') + '>' + 29 + node.content + '</a>'; 30 + } else { 31 + li += '<span class="sidebar-label">' + node.content + '</span>'; 32 + } 33 + if (hasChildren) { 34 + li += '<ul>' + children.map(renderEntry).join('') + '</ul>'; 35 + } 36 + return li + '</li>'; 37 + } 38 + 39 + function initSidebar(data) { 40 + var container = document.getElementById('sidebar-content'); 41 + if (!container) return; 42 + var html = '<ul>' + data.map(renderEntry).join('') + '</ul>'; 43 + container.innerHTML = html; 44 + 45 + // Toggle click handler — only the chevron toggle controls expand/collapse 46 + container.addEventListener('click', function(e) { 47 + var toggle = e.target.closest('.sidebar-toggle'); 48 + if (!toggle) return; 49 + e.preventDefault(); 50 + e.stopPropagation(); 51 + var li = toggle.parentElement; 52 + li.classList.toggle('collapsed'); 53 + }); 54 + 55 + updateSidebarActive(); 56 + } 57 + 58 + function updateSidebarActive() { 59 + var container = document.getElementById('sidebar-content'); 60 + if (!container) return; 61 + 62 + // Remove old active 63 + container.querySelectorAll('.current_unit').forEach(function(el) { 64 + el.classList.remove('current_unit'); 65 + }); 66 + 67 + // Find exact match 68 + var activeLink = container.querySelector('[data-nav="' + CURRENT_URL + '"]'); 69 + 70 + // If no exact match, try ancestor URLs 71 + if (!activeLink) { 72 + var parts = CURRENT_URL.split('#')[0].split('/'); 73 + while (parts.length > 1 && !activeLink) { 74 + parts.pop(); 75 + var tryUrl = parts.join('/') + '/index.html'; 76 + activeLink = container.querySelector('[data-nav="' + tryUrl + '"]'); 77 + } 78 + } 79 + 80 + if (activeLink) { 81 + activeLink.classList.add('current_unit'); 82 + // Expand all ancestor <li> nodes so the active link is visible 83 + var parent = activeLink.parentElement; 84 + while (parent && parent !== container) { 85 + if (parent.tagName === 'LI') { 86 + parent.classList.remove('collapsed'); 87 + } 88 + parent = parent.parentElement; 89 + } 90 + // Scroll into view 91 + setTimeout(function() { 92 + activeLink.scrollIntoView({ block: 'center', behavior: 'instant' }); 93 + }, 0); 94 + } 95 + } 96 + 97 + // SPA Navigation 98 + async function navigateTo(url, pushState) { 99 + if (pushState === undefined) pushState = true; 100 + try { 101 + var response = await fetch(ROOT_URL + url); 102 + if (!response.ok) throw new Error('Failed to load page'); 103 + var html = await response.text(); 104 + var doc = parser.parseFromString(html, 'text/html'); 105 + 106 + // Swap content 107 + var newContent = doc.querySelector('.odoc-content'); 108 + if (newContent) { 109 + document.querySelector('.odoc-content').innerHTML = newContent.innerHTML; 110 + } 111 + 112 + // Update title 113 + var newTitle = doc.querySelector('title'); 114 + if (newTitle) { 115 + document.title = newTitle.textContent; 116 + } 117 + 118 + // Update body class (regular page vs source page) 119 + document.body.className = doc.body.className; 120 + 121 + // Load new CSS/JS resources from fetched page 122 + var fetchedPageBase = ROOT_URL + url; 123 + var inlineScripts = Array.from(doc.querySelectorAll('head script:not([src])')); 124 + 125 + var newScriptLoadPromises = []; 126 + doc.querySelectorAll('head link[rel="stylesheet"], head script[src]').forEach(function(el) { 127 + var attr = el.tagName === 'LINK' ? 'href' : 'src'; 128 + var resUrl = el.getAttribute(attr); 129 + if (!resUrl) return; 130 + var abs = new URL(resUrl, fetchedPageBase).href; 131 + var selector = el.tagName === 'LINK' 132 + ? 'link[rel="stylesheet"]' 133 + : 'script[src]'; 134 + var already = Array.from(document.querySelectorAll('head ' + selector)).some(function(existing) { 135 + var existingUrl = existing.getAttribute(attr); 136 + if (!existingUrl) return false; 137 + return new URL(existingUrl, window.location.href).href === abs; 138 + }); 139 + if (!already) { 140 + var clone = el.cloneNode(true); 141 + clone.setAttribute(attr, abs); 142 + if (el.tagName === 'SCRIPT') { 143 + var p = new Promise(function(resolve) { 144 + clone.onload = resolve; 145 + clone.onerror = resolve; 146 + }); 147 + newScriptLoadPromises.push(p); 148 + } 149 + document.head.appendChild(clone); 150 + } 151 + }); 152 + 153 + // After external scripts load, execute inline scripts (deduplicated) 154 + Promise.all(newScriptLoadPromises).then(function() { 155 + inlineScripts.forEach(function(el) { 156 + var id = el.getAttribute('data-spa-inline'); 157 + if (id && document.querySelector('head script[data-spa-inline="' + id + '"]')) return; 158 + var s = el.cloneNode(true); 159 + document.head.appendChild(s); 160 + }); 161 + }); 162 + 163 + // Update state 164 + CURRENT_URL = url; 165 + if (pushState) { 166 + history.pushState({ url: url }, '', ROOT_URL + url); 167 + } 168 + 169 + updateSidebarActive(); 170 + 171 + // Scroll handling 172 + var hash = url.indexOf('#') >= 0 ? '#' + url.split('#')[1] : ''; 173 + if (hash) { 174 + setTimeout(function() { 175 + var target = document.querySelector(hash); 176 + if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' }); 177 + }, 100); 178 + } else { 179 + window.scrollTo(0, 0); 180 + } 181 + 182 + } catch (e) { 183 + console.error('Navigation failed:', e); 184 + window.location.href = ROOT_URL + url; 185 + } 186 + } 187 + 188 + // Popstate for back/forward 189 + window.addEventListener('popstate', function(e) { 190 + if (e.state && e.state.url) { 191 + navigateTo(e.state.url, false); 192 + } 193 + }); 194 + 195 + // Click interception for SPA navigation 196 + document.addEventListener('click', function(e) { 197 + var link = e.target.closest('a[href]'); 198 + if (!link) return; 199 + 200 + var href = link.getAttribute('href'); 201 + if (!href) return; 202 + 203 + // Skip external, hash-only, mailto, javascript links 204 + if (href.indexOf('http') === 0 || href.indexOf('//') === 0 || href.indexOf('#') === 0 || 205 + href.indexOf('mailto:') === 0 || href.indexOf('javascript:') === 0) { 206 + return; 207 + } 208 + 209 + // If the link has data-nav, use it directly (sidebar links) 210 + var navPath = link.dataset.nav; 211 + if (navPath) { 212 + // Only SPA-navigate within reference/ 213 + if (navPath.indexOf('reference/') !== 0) return; 214 + e.preventDefault(); 215 + navigateTo(navPath); 216 + return; 217 + } 218 + 219 + // Resolve relative href against current URL 220 + var targetUrl = href; 221 + if (href.indexOf('/') === 0) { 222 + targetUrl = href.slice(1); 223 + } else if (href.indexOf('./') === 0) { 224 + var currentDir = CURRENT_URL.substring(0, CURRENT_URL.lastIndexOf('/') + 1); 225 + targetUrl = currentDir + href.slice(2); 226 + } else if (href.indexOf('../') === 0) { 227 + var currentParts = CURRENT_URL.split('/'); 228 + currentParts.pop(); 229 + var hrefParts = href.split('/'); 230 + for (var i = 0; i < hrefParts.length; i++) { 231 + var part = hrefParts[i]; 232 + if (part === '..') { 233 + currentParts.pop(); 234 + } else if (part !== '.') { 235 + currentParts.push(part); 236 + } 237 + } 238 + targetUrl = currentParts.join('/'); 239 + } else { 240 + var curDir = CURRENT_URL.substring(0, CURRENT_URL.lastIndexOf('/') + 1); 241 + targetUrl = curDir + href; 242 + } 243 + 244 + // Skip if navigating to same page with just a hash change 245 + if (targetUrl.indexOf('#') >= 0) { 246 + var pathAndHash = targetUrl.split('#'); 247 + if (pathAndHash[0] === CURRENT_URL || pathAndHash[0] === '') { 248 + return; 249 + } 250 + } 251 + 252 + // Skip links outside /reference/ (e.g. /blog/ in header) 253 + if (targetUrl.indexOf('reference/') !== 0) { 254 + return; 255 + } 256 + 257 + e.preventDefault(); 258 + navigateTo(targetUrl); 259 + }); 260 + 261 + // Initialize 262 + (function() { 263 + // Mark header for SPA detection 264 + var header = document.querySelector('.jon-shell-header'); 265 + if (header) header.dataset.spaInit = 'true'; 266 + 267 + // Set initial history state 268 + history.replaceState({ url: CURRENT_URL }, '', window.location.href); 269 + 270 + // Read sidebar data from inline script tag 271 + var inlineData = window.__SIDEBAR_DATA__; 272 + if (inlineData) { 273 + initSidebar(inlineData); 274 + } 275 + })(); 276 + |}