Testing of the @doc-json output
0
fork

Configure Feed

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

Add --config key=value support, scrollycode theming, and SPA inline script execution

Add a config_values field to Html.Config.t and a --config KEY=VALUE
repeatable CLI argument to odoc html-generate, allowing arbitrary
configuration to flow from the command line through to shell plugins.

The docsite shell reads scrollycode.theme from config_values and emits
a <link> for the corresponding theme CSS in both page_creator and
src_page_creator. The dune-workspace passes --config
scrollycode.theme=warm via html_flags.

SPA navigation now collects head script:not([src]) elements from
fetched pages and executes them after newly added external scripts
have loaded. Inline scripts are stamped with a data-spa-inline
attribute (content hash) at HTML generation time; the SPA checks this
attribute to avoid re-executing scripts already present in <head>.

Document the resource type's SPA execution semantics in both
odoc_extension_registry.ml and odoc_extension_api.ml, covering
deduplication behaviour, execution timing, and guidance for extension
authors (prefer MutationObserver over re-execution, avoid
DOMContentLoaded in inline scripts).

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

+43 -4
+23 -3
src/odoc_docsite_js.ml
··· 310 310 document.title = newTitle.textContent; 311 311 } 312 312 313 - // Inject extension CSS/JS from the fetched page that aren't already loaded 313 + // Collect inline scripts first (before we start loading external scripts) 314 314 var fetchedPageBase = ROOT_URL + url; 315 + var inlineScripts = Array.from(doc.querySelectorAll('head script:not([src])')); 316 + 317 + // Inject external CSS/JS; track load events for newly added scripts 318 + var newScriptLoadPromises = []; 315 319 doc.querySelectorAll('head link[rel="stylesheet"], head script[src]').forEach(function(el) { 316 320 var attr = el.tagName === 'LINK' ? 'href' : 'src'; 317 321 var resUrl = el.getAttribute(attr); 318 322 if (!resUrl) return; 319 - // Resolve to absolute for comparison 320 323 var abs = new URL(resUrl, fetchedPageBase).href; 321 324 var selector = el.tagName === 'LINK' 322 325 ? 'link[rel="stylesheet"]' ··· 328 331 }); 329 332 if (!already) { 330 333 var clone = el.cloneNode(true); 331 - // Fix relative URL to be root-relative 332 334 clone.setAttribute(attr, abs); 335 + if (el.tagName === 'SCRIPT') { 336 + var p = new Promise(function(resolve) { 337 + clone.onload = resolve; 338 + clone.onerror = resolve; 339 + }); 340 + newScriptLoadPromises.push(p); 341 + } 333 342 document.head.appendChild(clone); 334 343 } 344 + }); 345 + 346 + // After all newly added external scripts have loaded, execute inline scripts. 347 + // Deduplicate via data-spa-inline attribute set during HTML generation. 348 + Promise.all(newScriptLoadPromises).then(function() { 349 + inlineScripts.forEach(function(el) { 350 + var id = el.getAttribute('data-spa-inline'); 351 + if (id && document.querySelector('head script[data-spa-inline="' + id + '"]')) return; 352 + var s = el.cloneNode(true); 353 + document.head.appendChild(s); 354 + }); 335 355 }); 336 356 337 357 CURRENT_URL = url;
+20 -1
src/odoc_docsite_shell.ml
··· 33 33 let page = Url.Path.{ kind = `File; parent = uri; name = file } in 34 34 Odoc_html.Link.href ~config ~resolve:(Current url) (Url.from_path page) 35 35 36 + let scrollycode_theme_links ~config ~url = 37 + match 38 + List.assoc_opt "scrollycode.theme" 39 + (Odoc_html.Config.config_values config) 40 + with 41 + | None -> [] 42 + | Some theme -> 43 + let support_uri = Odoc_html.Config.support_uri config in 44 + let css_url = 45 + file_uri ~config ~url support_uri 46 + ("extensions/scrollycode-" ^ theme ^ ".css") 47 + in 48 + [ Html.link ~rel:[ `Stylesheet ] ~href:css_url () ] 49 + 36 50 (* TyXML helper for generating TOC *) 37 51 let html_of_toc toc = 38 52 let open Odoc_html.Types in ··· 164 178 else file_uri support_uri css_url 165 179 in 166 180 [ Html.link ~rel:[ `Stylesheet ] ~href:resolved () ] 167 - | Js_inline code -> [ Html.script (Html.cdata_script code) ] 181 + | Js_inline code -> 182 + let id = Printf.sprintf "%x" (Hashtbl.hash code land 0x7FFFFFFF) in 183 + [ Html.script ~a:[ Html.a_user_data "spa-inline" id ] 184 + (Html.cdata_script code) ] 168 185 | Css_inline code -> [ Html.style [ Html.cdata_style code ] ]) 169 186 resources 170 187 in ··· 224 241 base_url current_url)); 225 242 ] 226 243 @ katex_elements @ extension_head_elements 244 + @ scrollycode_theme_links ~config ~url 227 245 @ sidebar_json_script sidebar_data 228 246 in 229 247 Html.head (Html.title (Html.txt title_string)) meta_elements ··· 385 403 (Printf.sprintf "window.BASE_URL = %S; window.CURRENT_URL = %S;" 386 404 base_url current_url)); 387 405 ] 406 + @ scrollycode_theme_links ~config ~url 388 407 @ sidebar_json_script sidebar_data 389 408 in 390 409 Html.head (Html.title (Html.txt title_string)) meta_elements