this repo has no description

feat: add odoc-jon-shell plugin skeleton

Minimal, content-first shell plugin for jon.recoil.org that produces
clean centered HTML pages without sidebars or search — just header,
content, and footer. Registers as "jon-shell" shell for use with
--shell jon-shell.

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

+625
+11
odoc-jon-shell/dune-project
··· 1 + (lang dune 3.18) 2 + (using dune_site 0.1) 3 + (name odoc-jon-shell) 4 + (generate_opam_files true) 5 + 6 + (package 7 + (name odoc-jon-shell) 8 + (synopsis "Minimal content-first shell for jon.recoil.org") 9 + (depends 10 + (ocaml (>= 4.14)) 11 + odoc))
+25
odoc-jon-shell/odoc-jon-shell.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "Minimal content-first shell 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-jon-shell/src/dune
··· 1 + (library 2 + (public_name odoc-jon-shell.impl) 3 + (name odoc_jon_shell) 4 + (libraries odoc.html odoc.extension_api)) 5 + 6 + (plugin 7 + (name odoc-jon-shell) 8 + (libraries odoc-jon-shell.impl) 9 + (site (odoc extensions)))
+258
odoc-jon-shell/src/odoc_jon_shell.ml
··· 1 + (* odoc-jon-shell: A minimal, content-first shell plugin for odoc. 2 + Registers as the "jon-shell" shell, usable with --shell jon-shell. *) 3 + 4 + open Odoc_utils 5 + module Html = Tyxml.Html 6 + module Url = Odoc_document.Url 7 + 8 + (* Register CSS as a support file *) 9 + let () = 10 + Odoc_extension_registry.register_support_file ~prefix:"jon-shell" 11 + { 12 + filename = "extensions/jon-shell.css"; 13 + content = Inline Odoc_jon_shell_css.css; 14 + } 15 + 16 + (* --- Helpers --- *) 17 + 18 + let file_uri ~config ~url (base : Odoc_html.Types.uri) file = 19 + match base with 20 + | Odoc_html.Types.Absolute uri -> uri ^ "/" ^ file 21 + | Relative uri -> 22 + let page = Url.Path.{ kind = `File; parent = uri; name = file } in 23 + Odoc_html.Link.href ~config ~resolve:(Current url) (Url.from_path page) 24 + 25 + (* --- Page assembly --- *) 26 + 27 + let page_creator ~config ~url ~uses_katex ~resources ~header ~preamble content = 28 + let support_uri = Odoc_html.Config.support_uri config in 29 + let file_uri = file_uri ~config ~url in 30 + let shell_css_uri = file_uri support_uri "extensions/jon-shell.css" in 31 + 32 + (* Deduplicate resources *) 33 + let deduplicate_resources resources = 34 + let rec aux seen acc = function 35 + | [] -> List.rev acc 36 + | r :: rest -> 37 + if List.mem r seen then aux seen acc rest 38 + else aux (r :: seen) (r :: acc) rest 39 + in 40 + aux [] [] resources 41 + in 42 + 43 + (* Extension resources: CSS goes in head, JS goes at end of body *) 44 + let extension_css, extension_js = 45 + let open Odoc_extension_registry in 46 + let is_absolute_url url = 47 + String.is_prefix ~affix:"http://" url 48 + || String.is_prefix ~affix:"https://" url 49 + in 50 + let resources = deduplicate_resources resources in 51 + List.partition_map 52 + (function 53 + | Css_url css_url -> 54 + let resolved = 55 + if is_absolute_url css_url then css_url 56 + else file_uri support_uri css_url 57 + in 58 + Left (Html.link ~rel:[ `Stylesheet ] ~href:resolved ()) 59 + | Css_inline code -> 60 + Left (Html.style [ Html.cdata_style code ]) 61 + | Js_url js_url -> 62 + let resolved = 63 + if is_absolute_url js_url then js_url 64 + else file_uri support_uri js_url 65 + in 66 + Right (Html.script ~a:[ Html.a_src resolved; Html.a_defer () ] (Html.txt "")) 67 + | Js_inline code -> 68 + Right (Html.script (Html.cdata_script code))) 69 + resources 70 + in 71 + 72 + (* KaTeX support *) 73 + let katex_elements = 74 + if uses_katex then 75 + let theme_uri = Odoc_html.Config.theme_uri config in 76 + let katex_css_uri = file_uri theme_uri "katex.min.css" in 77 + let katex_js_uri = file_uri support_uri "katex.min.js" in 78 + [ 79 + Html.link ~rel:[ `Stylesheet ] ~href:katex_css_uri (); 80 + Html.script ~a:[ Html.a_src katex_js_uri ] (Html.txt ""); 81 + Html.script 82 + (Html.cdata_script 83 + {| 84 + document.addEventListener("DOMContentLoaded", function () { 85 + var macros = {}; 86 + var elements = Array.from(document.getElementsByClassName("odoc-katex-math")); 87 + for (var i = 0; i < elements.length; i++) { 88 + var el = elements[i]; 89 + var content = el.textContent; 90 + var new_el = document.createElement("span"); 91 + new_el.setAttribute("class", "odoc-katex-math-rendered"); 92 + var display = el.classList.contains("display"); 93 + katex.render(content, new_el, { throwOnError: false, displayMode: display, macros }); 94 + el.replaceWith(new_el); 95 + } 96 + }); 97 + |}); 98 + ] 99 + else [] 100 + in 101 + 102 + let title_string = url.name in 103 + 104 + let head : Html_types.head Html.elt = 105 + let meta_elements = 106 + [ 107 + Html.meta ~a:[ Html.a_charset "utf-8" ] (); 108 + Html.meta 109 + ~a: 110 + [ 111 + Html.a_name "viewport"; 112 + Html.a_content "width=device-width, initial-scale=1"; 113 + ] 114 + (); 115 + Html.link ~rel:[ `Stylesheet ] ~href:shell_css_uri (); 116 + ] 117 + @ extension_css @ katex_elements 118 + in 119 + Html.head (Html.title (Html.txt title_string)) meta_elements 120 + in 121 + 122 + let body = 123 + [ 124 + Html.header 125 + ~a:[ Html.a_class [ "jon-shell-header" ] ] 126 + [ 127 + Html.a ~a:[ Html.a_href "/" ] [ Html.txt "jon.recoil.org" ]; 128 + Html.nav 129 + [ 130 + Html.a ~a:[ Html.a_href "/blog/" ] [ Html.txt "blog" ]; 131 + Html.a ~a:[ Html.a_href "/notebooks/" ] [ Html.txt "notebooks" ]; 132 + Html.a ~a:[ Html.a_href "/projects/" ] [ Html.txt "projects" ]; 133 + Html.a ~a:[ Html.a_href "/reference/" ] [ Html.txt "reference" ]; 134 + ]; 135 + ]; 136 + Html.main 137 + ~a:[ Html.a_class [ "jon-shell-main" ] ] 138 + [ 139 + Html.div 140 + ~a:[ Html.a_class [ "odoc-content" ] ] 141 + ((header :> Html_types.div_content Html.elt list) 142 + @ (preamble :> Html_types.div_content Html.elt list) 143 + @ content); 144 + ]; 145 + Html.footer 146 + ~a:[ Html.a_class [ "jon-shell-footer" ] ] 147 + [ Html.txt "jon ludlam" ]; 148 + ] 149 + @ extension_js 150 + in 151 + 152 + let htmlpp = Html.pp ~indent:(Odoc_html.Config.indent config) () in 153 + let html = 154 + Html.html head (Html.body ~a:[ Html.a_class [ "odoc"; "jon-shell" ] ] body) 155 + in 156 + let content ppf = 157 + htmlpp ppf html; 158 + Format.pp_force_newline ppf () 159 + in 160 + content 161 + 162 + let make ~config ~url ~header ~preamble ~uses_katex ~resources ~assets content 163 + children = 164 + let filename = Odoc_html.Link.Path.as_filename ~config url in 165 + let content = 166 + page_creator ~config ~url ~uses_katex ~resources ~header ~preamble content 167 + in 168 + { Odoc_document.Renderer.filename; content; children; path = url; assets } 169 + 170 + let src_page_creator ~config ~url ~header title content = 171 + let support_uri = Odoc_html.Config.support_uri config in 172 + let file_uri = file_uri ~config ~url in 173 + let shell_css_uri = file_uri support_uri "extensions/jon-shell.css" in 174 + 175 + let title_string = Printf.sprintf "Source: %s" title in 176 + 177 + let head : Html_types.head Html.elt = 178 + let meta_elements = 179 + [ 180 + Html.meta ~a:[ Html.a_charset "utf-8" ] (); 181 + Html.meta 182 + ~a: 183 + [ 184 + Html.a_name "viewport"; 185 + Html.a_content "width=device-width, initial-scale=1"; 186 + ] 187 + (); 188 + Html.link ~rel:[ `Stylesheet ] ~href:shell_css_uri (); 189 + ] 190 + in 191 + Html.head (Html.title (Html.txt title_string)) meta_elements 192 + in 193 + 194 + let body = 195 + [ 196 + Html.header 197 + ~a:[ Html.a_class [ "jon-shell-header" ] ] 198 + [ 199 + Html.a ~a:[ Html.a_href "/" ] [ Html.txt "jon.recoil.org" ]; 200 + Html.nav 201 + [ 202 + Html.a ~a:[ Html.a_href "/blog/" ] [ Html.txt "blog" ]; 203 + Html.a ~a:[ Html.a_href "/notebooks/" ] [ Html.txt "notebooks" ]; 204 + Html.a ~a:[ Html.a_href "/projects/" ] [ Html.txt "projects" ]; 205 + Html.a ~a:[ Html.a_href "/reference/" ] [ Html.txt "reference" ]; 206 + ]; 207 + ]; 208 + Html.main 209 + ~a:[ Html.a_class [ "jon-shell-main" ] ] 210 + [ 211 + Html.div 212 + ~a:[ Html.a_class [ "odoc-content" ] ] 213 + ((header :> Html_types.div_content Html.elt list) 214 + @ (content :> Html_types.div_content Html.elt list)); 215 + ]; 216 + Html.footer 217 + ~a:[ Html.a_class [ "jon-shell-footer" ] ] 218 + [ Html.txt "jon ludlam" ]; 219 + ] 220 + in 221 + 222 + let htmlpp = Html.pp ~indent:false () in 223 + let html = 224 + Html.html head 225 + (Html.body ~a:[ Html.a_class [ "odoc-src"; "jon-shell" ] ] body) 226 + in 227 + let content ppf = 228 + htmlpp ppf html; 229 + Format.pp_force_newline ppf () 230 + in 231 + content 232 + 233 + let make_src ~config ~url ~header title content = 234 + let filename = Odoc_html.Link.Path.as_filename ~config url in 235 + let content = src_page_creator ~config ~url ~header title content in 236 + { 237 + Odoc_document.Renderer.filename; 238 + content; 239 + children = []; 240 + path = url; 241 + assets = []; 242 + } 243 + 244 + (* Register the shell *) 245 + let () = 246 + Odoc_html.Html_shell.register 247 + (module struct 248 + let name = "jon-shell" 249 + 250 + let make ~config (data : Odoc_html.Html_shell.page_data) = 251 + make ~config ~url:data.url ~header:data.header ~preamble:data.preamble 252 + ~uses_katex:data.uses_katex ~resources:data.resources 253 + ~assets:data.assets data.content data.children 254 + 255 + let make_src ~config (data : Odoc_html.Html_shell.src_page_data) = 256 + make_src ~config ~url:data.url ~header:data.header data.title 257 + data.content 258 + end)
+322
odoc-jon-shell/src/odoc_jon_shell_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-bg: var(--code-bg); 21 + --xo-text: var(--text-color); 22 + --xo-gutter-bg: var(--code-bg); 23 + --xo-gutter-text: var(--text-muted); 24 + --xo-gutter-border: var(--border-color); 25 + --xo-stdout-bg: rgba(9, 105, 218, 0.06); 26 + --xo-stdout-text: var(--link-color); 27 + --xo-stderr-bg: rgba(218, 9, 9, 0.06); 28 + --xo-stderr-text: #cf222e; 29 + --xo-meta-bg: var(--code-bg); 30 + --xo-meta-text: var(--text-muted); 31 + --xo-tooltip-bg: var(--bg-color); 32 + --xo-tooltip-text: var(--text-color); 33 + --xo-tooltip-border: var(--border-color); 34 + --xo-btn-bg: var(--code-bg); 35 + --xo-btn-border: var(--border-color); 36 + --xo-btn-text: var(--text-muted); 37 + --xo-btn-hover-bg: var(--text-muted); 38 + --xo-btn-hover-text: var(--bg-color); 39 + } 40 + 41 + @media (prefers-color-scheme: dark) { 42 + :root { 43 + --bg-color: #0d1117; 44 + --text-color: #e6edf3; 45 + --text-muted: #8b949e; 46 + --link-color: #58a6ff; 47 + --link-hover: #79c0ff; 48 + --border-color: #30363d; 49 + --code-bg: #161b22; 50 + --code-border: #30363d; 51 + --header-bg: #161b22; 52 + --highlight-bg: rgba(88, 166, 255, 0.12); 53 + 54 + /* x-ocaml interactive cells - dark overrides */ 55 + --xo-stdout-bg: rgba(88, 166, 255, 0.08); 56 + --xo-stdout-text: #79c0ff; 57 + --xo-stderr-bg: rgba(248, 81, 73, 0.08); 58 + --xo-stderr-text: #f85149; 59 + } 60 + } 61 + 62 + /* Reset */ 63 + * { 64 + box-sizing: border-box; 65 + margin: 0; 66 + padding: 0; 67 + } 68 + 69 + body { 70 + font-family: var(--font-body); 71 + font-size: 18px; 72 + line-height: 1.7; 73 + color: var(--text-color); 74 + background: var(--bg-color); 75 + } 76 + 77 + a { 78 + color: var(--link-color); 79 + text-decoration: none; 80 + } 81 + 82 + a:hover { 83 + color: var(--link-hover); 84 + text-decoration: underline; 85 + } 86 + 87 + /* Header */ 88 + .jon-shell-header { 89 + display: flex; 90 + align-items: center; 91 + justify-content: space-between; 92 + max-width: var(--max-width); 93 + margin: 0 auto; 94 + padding: 16px 20px; 95 + font-size: 14px; 96 + } 97 + 98 + .jon-shell-header > a { 99 + font-weight: 600; 100 + color: var(--text-color); 101 + text-decoration: none; 102 + } 103 + 104 + .jon-shell-header > a:hover { 105 + color: var(--link-color); 106 + } 107 + 108 + .jon-shell-header nav { 109 + display: flex; 110 + gap: 20px; 111 + } 112 + 113 + .jon-shell-header nav a { 114 + color: var(--text-muted); 115 + text-decoration: none; 116 + } 117 + 118 + .jon-shell-header nav a:hover { 119 + color: var(--link-color); 120 + } 121 + 122 + /* Main content */ 123 + .jon-shell-main { 124 + max-width: var(--max-width); 125 + margin: 0 auto; 126 + padding: 24px 20px 60px; 127 + } 128 + 129 + /* Typography */ 130 + .jon-shell-main h1 { 131 + font-size: 2rem; 132 + font-weight: 700; 133 + line-height: 1.2; 134 + margin-bottom: 24px; 135 + } 136 + 137 + .jon-shell-main h2 { 138 + font-size: 1.5rem; 139 + font-weight: 600; 140 + margin-top: 40px; 141 + margin-bottom: 16px; 142 + } 143 + 144 + .jon-shell-main h3 { 145 + font-size: 1.25rem; 146 + font-weight: 600; 147 + margin-top: 32px; 148 + margin-bottom: 12px; 149 + } 150 + 151 + .jon-shell-main h4, 152 + .jon-shell-main h5, 153 + .jon-shell-main h6 { 154 + font-size: 1.1rem; 155 + font-weight: 600; 156 + margin-top: 24px; 157 + margin-bottom: 8px; 158 + } 159 + 160 + .jon-shell-main p { 161 + margin-bottom: 16px; 162 + } 163 + 164 + .jon-shell-main ul, 165 + .jon-shell-main ol { 166 + margin-bottom: 16px; 167 + padding-left: 28px; 168 + } 169 + 170 + .jon-shell-main li { 171 + margin-bottom: 4px; 172 + } 173 + 174 + .jon-shell-main blockquote { 175 + border-left: 3px solid var(--border-color); 176 + margin: 16px 0; 177 + padding: 8px 16px; 178 + color: var(--text-muted); 179 + } 180 + 181 + .jon-shell-main table { 182 + width: 100%; 183 + border-collapse: collapse; 184 + margin-bottom: 16px; 185 + font-size: 0.95em; 186 + } 187 + 188 + .jon-shell-main th, 189 + .jon-shell-main td { 190 + padding: 10px 12px; 191 + border: 1px solid var(--border-color); 192 + text-align: left; 193 + } 194 + 195 + .jon-shell-main th { 196 + background: var(--code-bg); 197 + font-weight: 600; 198 + } 199 + 200 + /* Code */ 201 + .jon-shell-main code { 202 + font-family: var(--font-mono); 203 + font-size: 0.88em; 204 + background: var(--code-bg); 205 + padding: 2px 6px; 206 + border-radius: 4px; 207 + border: 1px solid var(--code-border); 208 + } 209 + 210 + .jon-shell-main pre { 211 + background: var(--code-bg); 212 + border: 1px solid var(--code-border); 213 + border-radius: 6px; 214 + padding: 16px; 215 + overflow-x: auto; 216 + margin-bottom: 16px; 217 + } 218 + 219 + .jon-shell-main pre code { 220 + background: none; 221 + border: none; 222 + padding: 0; 223 + font-size: 0.875rem; 224 + line-height: 1.5; 225 + } 226 + 227 + /* odoc specifics */ 228 + .odoc-spec { 229 + margin: 16px 0; 230 + padding: 12px 16px; 231 + background: var(--code-bg); 232 + border: 1px solid var(--code-border); 233 + border-radius: 6px; 234 + border-left: 3px solid var(--link-color); 235 + } 236 + 237 + .odoc-spec code { 238 + background: none; 239 + border: none; 240 + padding: 0; 241 + } 242 + 243 + .spec { 244 + font-family: var(--font-mono); 245 + font-size: 0.9rem; 246 + } 247 + 248 + .spec-doc { 249 + margin-top: 8px; 250 + padding-top: 8px; 251 + border-top: 1px solid var(--border-color); 252 + font-size: 0.95rem; 253 + } 254 + 255 + .comment-delim { 256 + display: none; 257 + } 258 + 259 + .odoc-include { 260 + margin: 16px 0; 261 + padding: 12px; 262 + border: 1px solid var(--border-color); 263 + border-left: 3px solid var(--text-muted); 264 + border-radius: 6px; 265 + } 266 + 267 + .odoc-include > details > summary { 268 + cursor: pointer; 269 + font-family: var(--font-mono); 270 + font-size: 0.9rem; 271 + } 272 + 273 + .anchor { 274 + color: var(--text-muted); 275 + text-decoration: none; 276 + margin-left: 4px; 277 + opacity: 0; 278 + transition: opacity 0.15s; 279 + } 280 + 281 + h1:hover .anchor, h2:hover .anchor, h3:hover .anchor, 282 + h4:hover .anchor, h5:hover .anchor, h6:hover .anchor, 283 + .spec:hover .anchor { 284 + opacity: 1; 285 + } 286 + 287 + .anchor:hover { 288 + color: var(--link-color); 289 + } 290 + 291 + :target { 292 + background: var(--highlight-bg); 293 + border-radius: 4px; 294 + } 295 + 296 + /* Footer */ 297 + .jon-shell-footer { 298 + max-width: var(--max-width); 299 + margin: 0 auto; 300 + padding: 24px 20px; 301 + border-top: 1px solid var(--border-color); 302 + font-size: 14px; 303 + color: var(--text-muted); 304 + } 305 + 306 + /* Responsive */ 307 + @media (max-width: 600px) { 308 + body { 309 + font-size: 16px; 310 + } 311 + 312 + .jon-shell-header { 313 + flex-direction: column; 314 + gap: 8px; 315 + align-items: flex-start; 316 + } 317 + 318 + .jon-shell-main h1 { 319 + font-size: 1.6rem; 320 + } 321 + } 322 + |}