My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

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

Fix docsite sidebar rendering, CSS theming, and support file deployment

- Fix sidebar kindBadge: map leaf-page→pg, module-type→MT, etc. (was
showing "L" for leaf-page entries)
- Stop escaping sidebar content HTML (entries contain <code> tags)
- Fix inline sidebar JSON: use Html.cdata_script instead of Html.txt
to prevent HTML-escaping inside <script> tags
- Add --xo-* CSS custom properties for x-ocaml cells in light/dark themes
- Fix support file registration: use 'opam var share' instead of
'opam var x-ocaml:share' (works without x-ocaml being an opam package)
- Suppress empty <li> from config-only extension tags (@x-ocaml.*)
- Add worker_url field to jtw opam findlib_index.json
- Refactor x-ocaml.js worker URL discovery to support both direct
worker_url and day10-style version/content_hash paths

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

+110 -44
+10 -2
js_top_worker/bin/jtw.ml
··· 233 233 in 234 234 (* TODO: dep_paths should also contribute META paths once we have full universe info *) 235 235 let _ = dep_paths in 236 - let findlib_json = `Assoc [("meta_files", `List metas_json)] in 236 + let compiler_field = 237 + if no_worker then [] 238 + else [("compiler", `Assoc [("worker_url", `String "worker.js")])] 239 + in 240 + let findlib_json = `Assoc (("meta_files", `List metas_json) :: compiler_field) in 237 241 Out_channel.with_open_bin 238 242 Fpath.(output_dir / "findlib_index.json" |> to_string) 239 243 (fun oc -> Printf.fprintf oc "%s\n" (Yojson.Safe.to_string findlib_json)); ··· 511 515 pkg_path ^ "/" ^ local_meta_path) 512 516 pkg_results 513 517 in 514 - let root_index = `Assoc [("meta_files", `List (List.map (fun s -> `String s) all_metas))] in 518 + let compiler_field = 519 + if no_worker then [] 520 + else [("compiler", `Assoc [("worker_url", `String "worker.js")])] 521 + in 522 + let root_index = `Assoc (("meta_files", `List (List.map (fun s -> `String s) all_metas)) :: compiler_field) in 515 523 Out_channel.with_open_bin Fpath.(output_dir / "findlib_index.json" |> to_string) 516 524 (fun oc -> Printf.fprintf oc "%s\n" (Yojson.Safe.to_string root_index)); 517 525
+28
odoc-docsite/src/odoc_docsite_css.ml
··· 17 17 --header-height: 56px; 18 18 --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04); 19 19 --shadow-md: 0 3px 6px rgba(0, 0, 0, 0.08); 20 + 21 + /* x-ocaml interactive cells */ 22 + --xo-bg: var(--code-bg); 23 + --xo-text: var(--text-color); 24 + --xo-gutter-bg: var(--code-bg); 25 + --xo-gutter-text: var(--text-muted); 26 + --xo-gutter-border: var(--border-color); 27 + --xo-stdout-bg: rgba(9, 105, 218, 0.06); 28 + --xo-stdout-text: var(--link-color); 29 + --xo-stderr-bg: rgba(218, 9, 9, 0.06); 30 + --xo-stderr-text: #cf222e; 31 + --xo-meta-bg: var(--code-bg); 32 + --xo-meta-text: var(--text-muted); 33 + --xo-tooltip-bg: var(--content-bg); 34 + --xo-tooltip-text: var(--text-color); 35 + --xo-tooltip-border: var(--border-color); 36 + --xo-btn-bg: var(--code-bg); 37 + --xo-btn-border: var(--border-color); 38 + --xo-btn-text: var(--text-muted); 39 + --xo-btn-hover-bg: var(--text-muted); 40 + --xo-btn-hover-text: var(--content-bg); 20 41 } 21 42 22 43 @media (prefers-color-scheme: dark) { ··· 36 57 --include-bg: rgba(255, 255, 255, 0.02); 37 58 --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2); 38 59 --shadow-md: 0 3px 6px rgba(0, 0, 0, 0.3); 60 + 61 + /* x-ocaml interactive cells - dark overrides */ 62 + --xo-stdout-bg: rgba(88, 166, 255, 0.08); 63 + --xo-stdout-text: #79c0ff; 64 + --xo-stderr-bg: rgba(248, 81, 73, 0.08); 65 + --xo-stderr-text: #f85149; 66 + --xo-meta-text: var(--text-muted); 39 67 } 40 68 } 41 69
+15 -6
odoc-docsite/src/odoc_docsite_js.ml
··· 28 28 // Sidebar rendering 29 29 function kindBadge(kind) { 30 30 if (!kind) return ''; 31 - const labels = { package: 'P', module: 'M', page: 'pg' }; 31 + const labels = { 32 + 'package': 'P', 'module': 'M', 'module-type': 'MT', 33 + 'page': 'pg', 'leaf-page': 'pg', 'class': 'C', 'class-type': 'CT', 34 + 'library': 'L' 35 + }; 32 36 const label = labels[kind] || kind[0].toUpperCase(); 33 - return '<span class="sidebar-kind sidebar-kind-' + kind + '" title="' + kind + '">' + label + '</span>'; 37 + var cssKind = kind.replace(/[^a-z]/g, '-'); 38 + return '<span class="sidebar-kind sidebar-kind-' + cssKind + '" title="' + kind + '">' + label + '</span>'; 34 39 } 35 40 36 41 function renderSidebarEntry(entry) { ··· 42 47 var isCollapsed = sidebarState[stateKey] !== true; 43 48 var badge = kindBadge(node.kind); 44 49 50 + // Content may contain inline HTML (e.g. <code>odoc</code>), so we 51 + // pass it through as-is rather than escaping it. 52 + var contentHtml = node.content; 53 + 45 54 // Render packages/modules with children as collapsible groups 46 55 if (hasChildren) { 47 56 var childrenHtml = entry.children.map(function(c) { return renderSidebarEntry(c); }).join(''); 48 57 var linkHtml = node.url 49 - ? '<a class="sidebar-link' + (isActive ? ' active' : '') + '" href="' + BASE_URL + node.url + '" data-nav="' + node.url + '">' + badge + escapeHtml(node.content) + '</a>' 50 - : '<span>' + badge + escapeHtml(node.content) + '</span>'; 58 + ? '<a class="sidebar-link' + (isActive ? ' active' : '') + '" href="' + BASE_URL + node.url + '" data-nav="' + node.url + '">' + badge + contentHtml + '</a>' 59 + : '<span>' + badge + contentHtml + '</span>'; 51 60 return '<div class="sidebar-group' + (isCollapsed ? ' collapsed' : '') + '" data-id="' + escapeHtml(stateKey) + '">' + 52 61 '<div class="sidebar-group-header">' + 53 62 '<svg class="sidebar-toggle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' + ··· 61 70 // Leaf nodes: add spacer for alignment with expandable items 62 71 var spacer = (node.kind === 'package' || node.kind === 'module') 63 72 ? '<span class="sidebar-toggle-spacer"></span>' : ''; 64 - return '<a class="sidebar-link sidebar-leaf' + (isActive ? ' active' : '') + '" href="' + BASE_URL + node.url + '" data-nav="' + node.url + '">' + spacer + badge + escapeHtml(node.content) + '</a>'; 73 + return '<a class="sidebar-link sidebar-leaf' + (isActive ? ' active' : '') + '" href="' + BASE_URL + node.url + '" data-nav="' + node.url + '">' + spacer + badge + contentHtml + '</a>'; 65 74 } else { 66 - return '<span class="sidebar-link">' + badge + escapeHtml(node.content) + '</span>'; 75 + return '<span class="sidebar-link">' + badge + contentHtml + '</span>'; 67 76 } 68 77 } 69 78
+1 -1
odoc-docsite/src/odoc_docsite_shell.ml
··· 103 103 let json_str = Json.to_string json in 104 104 [ 105 105 Html.script 106 - (Html.txt 106 + (Html.cdata_script 107 107 (Printf.sprintf "window.__DOCSITE_SIDEBAR_DATA__ = %s;" json_str)); 108 108 ] 109 109
+14 -13
odoc-interactive-extension/src/interactive_extension.ml
··· 173 173 Api.Registry.register_code_block (module X_ocaml_code); 174 174 Api.Registry.register_extension_info config_info; 175 175 Api.Registry.register_extension_info code_info; 176 - (* Find x-ocaml.js from the x-ocaml opam package's share directory *) 177 - let share_dir = 176 + (* Find x-ocaml.js from the opam share directory. 177 + We use 'opam var share' (the switch-level share dir) rather than 178 + 'opam var x-ocaml:share' which requires x-ocaml to be a registered 179 + opam package. dune install puts the file at <share>/x-ocaml/x-ocaml.js. *) 180 + let x_ocaml_js = 178 181 try 179 - let ic = Unix.open_process_in "opam var x-ocaml:share 2>/dev/null" in 182 + let ic = Unix.open_process_in "opam var share 2>/dev/null" in 180 183 let line = input_line ic in 181 184 let _ = Unix.close_process_in ic in 182 - Some (String.trim line) 185 + Some (String.trim line ^ "/x-ocaml/x-ocaml.js") 183 186 with _ -> None 184 187 in 185 - (match share_dir with 186 - | Some dir -> 187 - let path = dir ^ "/x-ocaml.js" in 188 - if Sys.file_exists path then 189 - Api.Registry.register_support_file ~prefix:"x-ocaml" { 190 - filename = "_x-ocaml/x-ocaml.js"; 191 - content = Copy_from path; 192 - } 193 - | None -> ()) 188 + (match x_ocaml_js with 189 + | Some path when Sys.file_exists path -> 190 + Api.Registry.register_support_file ~prefix:"x-ocaml" { 191 + filename = "_x-ocaml/x-ocaml.js"; 192 + content = Copy_from path; 193 + } 194 + | _ -> ())
+8 -2
odoc/src/document/comment.ml
··· 459 459 (* Extension handled the tag - collect resources/assets and use output *) 460 460 Resources.add result.Odoc_extension_registry.resources; 461 461 Assets.add result.Odoc_extension_registry.assets; 462 - (* Use empty key - extension output in definition is self-describing *) 463 462 { Description.attr = [ name ]; 464 463 key = []; 465 464 definition = result.Odoc_extension_registry.content } ··· 473 472 let attached_block_element : Comment.attached_block_element -> Block.t = 474 473 function 475 474 | #Comment.nestable_block_element as e -> nestable_block_element e 476 - | `Tag t -> [ block ~attr:[ "at-tags" ] @@ Description [ tag t ] ] 475 + | `Tag t -> 476 + let t = tag t in 477 + if t.Description.key = [] && t.Description.definition = [] then 478 + (* Extension tag with no visible output (e.g. config-only tags 479 + that only inject resources). Emit nothing. *) 480 + [] 481 + else 482 + [ block ~attr:[ "at-tags" ] @@ Description [ t ] ] 477 483 478 484 (* TODO collaesce tags *) 479 485
+34 -20
x-ocaml/src/x_ocaml.ml
··· 60 60 if status = 200 then Some (Jv.to_string (Jv.get xhr "responseText")) 61 61 else None 62 62 63 - (** Extract compiler version and content_hash from a findlib_index.json. 63 + (** Parse the compiler object from findlib_index.json. 64 64 Uses browser JSON.parse — no OCaml JSON library needed. *) 65 - let compiler_from_findlib_index = 65 + let findlib_index_json = 66 66 match findlib_index_url with 67 67 | None -> None 68 68 | Some url -> ··· 70 70 | None -> None 71 71 | Some text -> 72 72 (try 73 - let json = Jv.call (Jv.get Jv.global "JSON") "parse" [| Jv.of_string text |] in 74 - let compiler = Jv.get json "compiler" in 75 - if Jv.is_none compiler || Jv.is_undefined compiler then None 76 - else 73 + Some (Jv.call (Jv.get Jv.global "JSON") "parse" [| Jv.of_string text |]) 74 + with _ -> None) 75 + 76 + let findlib_index_base_dir = 77 + match findlib_index_url with 78 + | None -> "" 79 + | Some fi_url -> 80 + match String.rindex_opt fi_url '/' with 81 + | Some i -> String.sub fi_url 0 (i + 1) 82 + | None -> fi_url 83 + 84 + (** Derive the worker URL from findlib_index.json's compiler field. 85 + Supports two modes: 86 + - Direct: compiler.worker_url (relative to findlib_index.json) 87 + - Day10-style: compiler.version + compiler.content_hash constructs 88 + ../../../compiler/{version}/{content_hash}/worker.js *) 89 + let worker_url_from_findlib_index = 90 + match findlib_index_json with 91 + | None -> None 92 + | Some json -> 93 + let compiler = Jv.get json "compiler" in 94 + if Jv.is_none compiler || Jv.is_undefined compiler then None 95 + else 96 + let worker_url = Jv.get compiler "worker_url" in 97 + if not (Jv.is_none worker_url || Jv.is_undefined worker_url) then 98 + Some (findlib_index_base_dir ^ Jv.to_string worker_url) 99 + else 100 + try 77 101 let version = Jv.to_string (Jv.get compiler "version") in 78 102 let content_hash = Jv.to_string (Jv.get compiler "content_hash") in 79 - Some (version, content_hash) 80 - with _ -> None) 103 + Some (findlib_index_base_dir ^ "../../../compiler/" ^ version ^ "/" ^ content_hash ^ "/worker.js") 104 + with _ -> None 81 105 82 106 let worker_url = 83 107 (* Priority: 1) explicit x-ocaml-worker meta tag, ··· 86 110 match read_meta "x-ocaml-worker" with 87 111 | Some url -> url 88 112 | None -> 89 - match compiler_from_findlib_index with 90 - | Some (version, content_hash) -> 91 - (* Construct worker URL relative to the findlib_index's directory. 92 - findlib_index is at e.g. .../p/yojson/3.0.0/findlib_index.json 93 - We need .../compiler/<ver>/<hash>/worker.js 94 - Relative from the findlib_index dir: ../../../compiler/... *) 95 - let fi_url = Option.get findlib_index_url in 96 - let base_dir = match String.rindex_opt fi_url '/' with 97 - | Some i -> String.sub fi_url 0 (i + 1) 98 - | None -> fi_url 99 - in 100 - base_dir ^ "../../../compiler/" ^ version ^ "/" ^ content_hash ^ "/worker.js" 113 + match worker_url_from_findlib_index with 114 + | Some url -> url 101 115 | None -> 102 116 match current_attribute "src-worker" with 103 117 | None ->