this repo has no description
1
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>

+109 -11
+34 -6
src/extension_api/odoc_extension_api.ml
··· 20 20 21 21 (** {1 Extension Types} *) 22 22 23 - (** Resources that can be injected into the page (HTML only) *) 23 + (** Resources that can be injected into the HTML [<head>]. 24 + 25 + See {!Odoc_extension_registry} for full documentation on execution 26 + timing, SPA deduplication semantics, and guidance for extension 27 + authors. 28 + 29 + {b Summary:} 30 + 31 + {ul 32 + {- [Js_url] / [Css_url] — deduplicated by resolved URL. Loaded at 33 + most once across SPA navigations.} 34 + {- [Js_inline] — stamped with [data-spa-inline] at generation time. 35 + Executed {b exactly once}: on the first SPA navigation that 36 + introduces the script. Do not use [DOMContentLoaded] inside 37 + these; use a [MutationObserver] if you need to react to content 38 + changes on subsequent navigations.} 39 + {- [Css_inline] — injected on every navigation (CSS is additive and 40 + idempotent).}} *) 24 41 type resource = Odoc_extension_registry.resource = 25 - | Js_url of string (** External JavaScript: <script src="..."> *) 26 - | Css_url of string (** External CSS: <link rel="stylesheet" href="..."> *) 27 - | Js_inline of string (** Inline JavaScript: <script>...</script> *) 28 - | Css_inline of string (** Inline CSS: <style>...</style> *) 42 + | Js_url of string 43 + (** External JavaScript: emitted as [<script src="…">]. 44 + Deduplicated by absolute URL on SPA navigation. *) 45 + | Css_url of string 46 + (** External CSS: emitted as [<link rel="stylesheet" href="…">]. 47 + Deduplicated by absolute URL on SPA navigation. *) 48 + | Js_inline of string 49 + (** Inline JavaScript: emitted as [<script data-spa-inline="…">…</script>]. 50 + Runs once on first encounter; skipped on subsequent SPA navigations 51 + that carry an identical script. *) 52 + | Css_inline of string 53 + (** Inline CSS: emitted as [<style>…</style>]. 54 + Re-injected on each SPA navigation (idempotent). *) 29 55 30 56 (** Binary asset generated by an extension. 31 57 Assets are written alongside the HTML output. To reference an asset ··· 67 93 E.g., [("html", "<div>...</div>"); ("markdown", "...")] *) 68 94 69 95 resources : resource list; 70 - (** Page-level resources (JS/CSS). Only used by HTML backend. *) 96 + (** Page-level resources (JS/CSS). Only used by HTML backend. 97 + See {!resource} for execution and deduplication semantics, 98 + especially in SPA (single-page app) shells. *) 71 99 72 100 assets : asset list; 73 101 (** Binary assets to write alongside HTML output.
+52 -1
src/extension_registry/odoc_extension_registry.ml
··· 8 8 module Comment = Odoc_model.Comment 9 9 module Location_ = Odoc_model.Location_ 10 10 11 - (** Resources that can be injected into the page (HTML only) *) 11 + (** {2 Page Resources} 12 + 13 + Resources that an extension requests to be injected into the HTML 14 + [<head>]. The shell plugin is responsible for rendering these; the 15 + behaviour described below applies to the {b docsite} shell shipped 16 + with [odoc-docsite]. 17 + 18 + {3 Execution timing} 19 + 20 + On the {b initial full page load} every resource listed by every 21 + extension present on that page is emitted into [<head>] in order. 22 + External scripts ([Js_url]) are loaded via a normal 23 + [<script src="…">] tag and execute when the browser fetches them. 24 + Inline scripts ([Js_inline]) execute synchronously in document order. 25 + 26 + {3 SPA (single-page app) navigation} 27 + 28 + The docsite shell intercepts link clicks and fetches pages via 29 + [fetch()] instead of a full reload. After swapping the page content 30 + it reconciles [<head>] resources: 31 + 32 + {ul 33 + {- {b [Js_url] / [Css_url]:} Compared by resolved absolute URL. 34 + Resources already present in the live document are skipped; 35 + new ones are appended to [<head>] and (for scripts) their 36 + [onload] events are awaited before continuing.} 37 + {- {b [Js_inline]:} Each inline script is stamped at HTML-generation 38 + time with a [data-spa-inline] attribute containing a hash of its 39 + content. On SPA navigation the shell checks whether a [<script>] 40 + with the same [data-spa-inline] value already exists in [<head>]. 41 + If so it is {b not re-executed}. This means inline scripts run 42 + {b exactly once} across SPA navigations — the first time a page 43 + carrying that script is visited.} 44 + {- {b [Css_inline]:} Injected into [<head>] on every navigation 45 + (CSS is additive and idempotent, so duplicates are harmless).}} 46 + 47 + {3 Guidance for extension authors} 48 + 49 + {ul 50 + {- Prefer [Js_url] for library code (e.g. the mermaid runtime). 51 + The deduplication-by-URL ensures it is loaded exactly once.} 52 + {- Use [Js_inline] for one-time initialisation that must run after 53 + the library is loaded (e.g. registering a global observer). 54 + It will execute once; on subsequent SPA navigations the library 55 + and observer are still alive.} 56 + {- If your extension needs to re-process content on every SPA 57 + navigation, set up a [MutationObserver] or listen for the 58 + ["popstate"] event in your [Js_inline] init script rather than 59 + relying on the script being re-executed.} 60 + {- Do {b not} gate initialisation on [DOMContentLoaded] inside a 61 + [Js_inline] script — that event does not re-fire during SPA 62 + navigation.}} *) 12 63 type resource = 13 64 | Js_url of string 14 65 | Css_url of string
+4 -1
src/html/config.ml
··· 16 16 shell : string option; 17 17 home_breadcrumb : string option; 18 18 mode_links : string option; 19 + config_values : (string * string) list; 19 20 } 20 21 21 22 let v ?(search_result = false) ?theme_uri ?support_uri ?(search_uris = []) 22 23 ?(extra_css = []) ~semantic_uris ~indent ~flat ~open_details ~as_json ?shell 23 - ~remap ?home_breadcrumb ?mode_links () = 24 + ~remap ?home_breadcrumb ?mode_links ?(config_values = []) () = 24 25 { 25 26 semantic_uris; 26 27 indent; ··· 36 37 remap; 37 38 home_breadcrumb; 38 39 mode_links; 40 + config_values; 39 41 } 40 42 41 43 let theme_uri config : Types.uri = ··· 67 69 let home_breadcrumb config = config.home_breadcrumb 68 70 69 71 let mode_links config = config.mode_links 72 + let config_values config = config.config_values
+2
src/html/config.mli
··· 17 17 remap:(string * string) list -> 18 18 ?home_breadcrumb:string -> 19 19 ?mode_links:string -> 20 + ?config_values:(string * string) list -> 20 21 unit -> 21 22 t 22 23 (** [search_result] indicates whether this is a summary for a search result. In ··· 49 50 val home_breadcrumb : t -> string option 50 51 51 52 val mode_links : t -> string option 53 + val config_values : t -> (string * string) list
+17 -3
src/odoc/bin/main.ml
··· 1318 1318 $(i,https://example.com/modes#local)." 1319 1319 in 1320 1320 Arg.(value & opt (some string) None & info [ "mode-links" ] ~docv:"URI" ~doc) 1321 + let config_values_raw = 1322 + let doc = 1323 + "A configuration key=value pair passed to the shell plugin. \ 1324 + May be repeated, e.g. $(b,--config scrollycode.theme=warm)." 1325 + in 1326 + Arg.(value & opt_all string [] & info [ "config" ] ~docv:"KEY=VALUE" ~doc) 1321 1327 1322 1328 let extra_args = 1323 1329 let config semantic_uris closed_details indent theme_uri support_uri 1324 1330 search_uris extra_css flat as_json shell remap remap_file 1325 - home_breadcrumb mode_links = 1331 + home_breadcrumb mode_links config_values_raw = 1326 1332 let open_details = not closed_details in 1327 1333 let remap = 1328 1334 match remap_file with ··· 1335 1341 | None -> acc) 1336 1342 [] 1337 1343 in 1344 + let config_values = 1345 + List.filter_map 1346 + ~f:(fun s -> 1347 + match String.cut ~sep:"=" s with 1348 + | None -> None 1349 + | Some (k, v) -> Some (k, v)) 1350 + config_values_raw 1351 + in 1338 1352 let html_config = 1339 1353 Odoc_html.Config.v ~theme_uri ~support_uri ~search_uris ~extra_css 1340 1354 ~semantic_uris ~indent ~flat ~open_details ~as_json ?shell ~remap 1341 - ?home_breadcrumb ?mode_links () 1355 + ?home_breadcrumb ?mode_links ~config_values () 1342 1356 in 1343 1357 { Html_page.html_config } 1344 1358 in 1345 1359 Term.( 1346 1360 const config $ semantic_uris $ closed_details $ indent $ theme_uri 1347 1361 $ support_uri $ search_uri $ extra_css $ flat $ as_json $ shell $ remap 1348 - $ remap_file $ home_breadcrumb $ mode_links) 1362 + $ remap_file $ home_breadcrumb $ mode_links $ config_values_raw) 1349 1363 end 1350 1364 1351 1365 module Odoc_html = Make_renderer (Odoc_html_args)