Odoc plugins for jon.recoil.org
0
fork

Configure Feed

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

Fetch sidebar from sidebar.json and group packages under headings

Stop embedding ~200KB of sidebar JSON inline in every HTML page.
Instead, fetch sidebar.json at runtime — essential for large package
sets like core. Group packages into categorised sections (odoc Core,
odoc Extensions, js_top_worker, Tessera) rendered as static headings,
and flatten the redundant top-level wrapper node.

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

+158 -49
+25 -40
src/odoc_jons_plugins.ml
··· 18 18 content = Inline Odoc_jons_plugins_js.js; 19 19 } 20 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 - ] 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. *) 33 24 34 25 (* --- Helpers --- *) 35 26 ··· 55 46 56 47 (* --- Page assembly --- *) 57 48 58 - let page_creator ~config ~url ~uses_katex ~resources ~sidebar_data ~header 49 + let page_creator ~config ~url ~uses_katex ~resources ~sidebar_data:_ ~header 59 50 ~preamble content = 60 51 let support_uri = Odoc_html.Config.support_uri config in 61 52 let file_uri = file_uri ~config ~url in ··· 172 163 ] 173 164 @ xocaml_meta_tags config 174 165 @ katex_elements @ extension_head_elements 175 - @ sidebar_json_script sidebar_data 176 166 in 177 167 Html.head (Html.title (Html.txt title_string)) meta_elements 178 168 in 179 169 170 + (* Always render the sidebar container — JS will populate it from sidebar.json *) 180 171 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 -> [] 172 + [ 173 + Html.nav 174 + ~a: 175 + [ 176 + Html.a_class [ "jon-shell-sidebar"; "odoc-global-toc" ]; 177 + Html.a_id "sidebar-content"; 178 + ] 179 + []; 180 + ] 193 181 in 194 182 195 183 let body = ··· 253 241 in 254 242 { Odoc_document.Renderer.filename; content; children; path = url; assets } 255 243 256 - let src_page_creator ~config ~url ~header ~sidebar_data title content = 244 + let src_page_creator ~config ~url ~header ~sidebar_data:_ title content = 257 245 let support_uri = Odoc_html.Config.support_uri config in 258 246 let file_uri = file_uri ~config ~url in 259 247 let shell_css_uri = file_uri support_uri "extensions/jon-shell.css" in ··· 289 277 base_url current_url)); 290 278 ] 291 279 @ xocaml_meta_tags config 292 - @ sidebar_json_script sidebar_data 293 280 in 294 281 Html.head (Html.title (Html.txt title_string)) meta_elements 295 282 in 296 283 284 + (* Always render the sidebar container — JS will populate it from sidebar.json *) 297 285 let sidebar_nav = 298 - match sidebar_data with 299 - | Some _ -> 300 - [ 301 - Html.nav 302 - ~a: 303 - [ 304 - Html.a_class [ "jon-shell-sidebar"; "odoc-global-toc" ]; 305 - Html.a_id "sidebar-content"; 306 - ] 307 - []; 308 - ] 309 - | None -> [] 286 + [ 287 + Html.nav 288 + ~a: 289 + [ 290 + Html.a_class [ "jon-shell-sidebar"; "odoc-global-toc" ]; 291 + Html.a_id "sidebar-content"; 292 + ] 293 + []; 294 + ] 310 295 in 311 296 312 297 let body =
+35 -1
src/odoc_jons_plugins_css.ml
··· 185 185 .jon-shell-sidebar a.current_unit { 186 186 color: var(--link-color); 187 187 font-weight: 600; 188 + background: none; 188 189 } 189 190 190 191 /* Collapsible entries — small inline chevron before the link */ ··· 257 258 258 259 /* Hide the top-level wrapper entries (e.g. "OCaml package documentation" > 259 260 "reference") — they waste space and add confusing nesting. Show their 260 - children directly. */ 261 + children directly. Also force their child lists to always be visible 262 + regardless of collapsed state, so package groups at level 3 are shown. */ 261 263 .jon-shell-sidebar > ul > li > .sidebar-toggle, 262 264 .jon-shell-sidebar > ul > li > a, 263 265 .jon-shell-sidebar > ul > li > .sidebar-label, ··· 265 267 .jon-shell-sidebar > ul > li > ul > li > a, 266 268 .jon-shell-sidebar > ul > li > ul > li > .sidebar-label { 267 269 display: none; 270 + } 271 + .jon-shell-sidebar > ul > li > ul, 272 + .jon-shell-sidebar > ul > li > ul > li > ul { 273 + display: block !important; 274 + padding-left: 0; 275 + margin-left: 0; 276 + border-left: none; 277 + } 278 + 279 + /* Group headings in the sidebar — static section headers, not collapsible */ 280 + .jon-shell-sidebar .sidebar-group { 281 + display: block; 282 + margin-top: 1rem; 283 + } 284 + .jon-shell-sidebar .sidebar-group:first-child { 285 + margin-top: 0; 286 + } 287 + .jon-shell-sidebar .sidebar-group-heading { 288 + display: block; 289 + padding: 2px 8px; 290 + font-size: 0.75rem; 291 + font-weight: 600; 292 + letter-spacing: 0.04em; 293 + text-transform: uppercase; 294 + color: var(--text-muted); 295 + opacity: 0.7; 296 + margin-bottom: 2px; 297 + } 298 + .jon-shell-sidebar .sidebar-group > ul { 299 + padding-left: 0; 300 + margin-left: 0; 301 + border-left: none; 268 302 } 269 303 270 304 /* Hero intro - side-by-side text + photo */
+98 -8
src/odoc_jons_plugins_js.ml
··· 10 10 // DOMParser for SPA navigation 11 11 var parser = new DOMParser(); 12 12 13 - // Sidebar rendering from __SIDEBAR_DATA__ JSON 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 + // Apply grouping to the sidebar data tree. 73 + // The structure is: [root] -> [reference] -> [packages...] 74 + // We group the packages level. 75 + function groupSidebarData(data) { 76 + return data.map(function(entry) { 77 + var children = entry.children || []; 78 + return { 79 + node: entry.node, 80 + children: groupPackages(children) 81 + }; 82 + }); 83 + } 84 + 85 + // Sidebar rendering 14 86 function renderEntry(entry) { 15 87 var node = entry.node; 16 - var isActive = node.url === CURRENT_URL; 17 88 var children = entry.children || []; 18 89 var hasChildren = children.length > 0; 90 + 91 + // Group entries render as a heading + always-visible children (not collapsible) 92 + if (node.kind === 'group') { 93 + var html = '<li class="sidebar-group">'; 94 + html += '<span class="sidebar-group-heading">' + node.content + '</span>'; 95 + if (hasChildren) { 96 + html += '<ul>' + children.map(renderEntry).join('') + '</ul>'; 97 + } 98 + return html + '</li>'; 99 + } 100 + 101 + var isActive = node.url === CURRENT_URL; 19 102 var li = '<li'; 20 103 if (hasChildren) li += ' class="collapsed"'; 21 104 li += '>'; ··· 39 122 function initSidebar(data) { 40 123 var container = document.getElementById('sidebar-content'); 41 124 if (!container) return; 42 - var html = '<ul>' + data.map(renderEntry).join('') + '</ul>'; 125 + var grouped = groupSidebarData(data); 126 + // Flatten the top-level wrapper (e.g. "OCaml package documentation") 127 + // since everything lives under it — render its children directly. 128 + var entries = grouped.length === 1 ? grouped[0].children : grouped; 129 + var html = '<ul>' + entries.map(renderEntry).join('') + '</ul>'; 43 130 container.innerHTML = html; 44 131 45 132 // Toggle click handler — only the chevron toggle controls expand/collapse ··· 298 385 // Set initial history state 299 386 history.replaceState({ url: CURRENT_URL }, '', window.location.href); 300 387 301 - // Read sidebar data from inline script tag 302 - var inlineData = window.__SIDEBAR_DATA__; 303 - if (inlineData) { 304 - initSidebar(inlineData); 305 - } 388 + // Load sidebar data from sidebar.json 389 + fetch(ROOT_URL + 'sidebar.json') 390 + .then(function(r) { 391 + if (!r.ok) throw new Error('sidebar.json: ' + r.status); 392 + return r.json(); 393 + }) 394 + .then(function(data) { initSidebar(data); }) 395 + .catch(function(e) { console.warn('Failed to load sidebar:', e); }); 306 396 307 397 initSidebarToggle(); 308 398 })();