@commentray/render 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,7 @@ import { DUAL_PANE_BLOCK_REVEAL_LEAD_CSS_PX, READING_LEAD_ALIGN_TOLERANCE_CSS_PX
14
14
  import { wireWideModeIntroTour } from "./code-browser-wide-intro-controller.js";
15
15
  import { readWebStorageItem, writeWebStorageItem } from "./code-browser-web-storage.js";
16
16
  import { dispatchCommentrayMermaidDone, wireBlockStretchBufferSync, } from "./block-stretch-buffer-sync.js";
17
+ import { COMMENTRAY_MERMAID_MODULE_READY_EVENT } from "./commentray-mermaid-events.js";
17
18
  /**
18
19
  * Hub pages emit `./browse/…` relative to the site root. From `/…/browse/current.html` the browser
19
20
  * would otherwise resolve `./browse/…` against that folder and nest a second `browse/` segment.
@@ -30,30 +31,82 @@ function rewriteHubRelativeBrowseAnchorsIn(root) {
30
31
  el.href = resolveStaticBrowseHref(raw, path, origin);
31
32
  }
32
33
  }
34
+ /**
35
+ * The Mermaid bundle is loaded via a trailing `type="module"` script (async `import()`). The main
36
+ * client runs as a classic inline script **before** that module executes, so `cy.visit` + an
37
+ * immediate pane flip can call {@link runMermaidOnFreshDocNodes} while `commentrayMermaid` is still
38
+ * undefined. We enqueue roots and flush on {@link COMMENTRAY_MERMAID_MODULE_READY_EVENT}.
39
+ */
40
+ const pendingMermaidDocRoots = new Set();
41
+ const queuedMermaidDocRoots = new Set();
42
+ let mermaidQueueInFlight = null;
43
+ function collectFreshMermaidPreNodes(roots) {
44
+ const uniq = new Set();
45
+ for (const root of roots) {
46
+ for (const pre of Array.from(root.querySelectorAll("pre.mermaid"))) {
47
+ const wrap = pre.closest(".commentray-mermaid");
48
+ if (wrap !== null && wrap.querySelector("svg") !== null)
49
+ continue;
50
+ uniq.add(pre);
51
+ }
52
+ }
53
+ return Array.from(uniq);
54
+ }
55
+ function pumpQueuedMermaidRuns() {
56
+ if (mermaidQueueInFlight)
57
+ return mermaidQueueInFlight;
58
+ mermaidQueueInFlight = (async () => {
59
+ while (queuedMermaidDocRoots.size > 0) {
60
+ const m = globalThis
61
+ .commentrayMermaid;
62
+ if (!m) {
63
+ for (const root of queuedMermaidDocRoots)
64
+ pendingMermaidDocRoots.add(root);
65
+ queuedMermaidDocRoots.clear();
66
+ return;
67
+ }
68
+ const roots = Array.from(queuedMermaidDocRoots);
69
+ queuedMermaidDocRoots.clear();
70
+ const nodes = collectFreshMermaidPreNodes(roots);
71
+ if (nodes.length === 0)
72
+ continue;
73
+ try {
74
+ await m.run({ nodes });
75
+ dispatchCommentrayMermaidDone();
76
+ }
77
+ catch (err) {
78
+ console.error("Commentray: mermaid.run failed", err);
79
+ }
80
+ }
81
+ })().finally(() => {
82
+ mermaidQueueInFlight = null;
83
+ if (queuedMermaidDocRoots.size > 0)
84
+ void pumpQueuedMermaidRuns();
85
+ });
86
+ return mermaidQueueInFlight;
87
+ }
88
+ function queueMermaidRunForDocRoot(docRoot) {
89
+ queuedMermaidDocRoots.add(docRoot);
90
+ return pumpQueuedMermaidRuns();
91
+ }
92
+ function flushPendingMermaidDocRoots() {
93
+ const roots = Array.from(pendingMermaidDocRoots);
94
+ pendingMermaidDocRoots.clear();
95
+ for (const root of roots) {
96
+ void queueMermaidRunForDocRoot(root);
97
+ }
98
+ }
99
+ globalThis.addEventListener(COMMENTRAY_MERMAID_MODULE_READY_EVENT, () => {
100
+ flushPendingMermaidDocRoots();
101
+ });
33
102
  function runMermaidOnFreshDocNodes(docBody) {
34
103
  if (typeof globalThis.location !== "undefined" && globalThis.location.protocol === "file:")
35
104
  return Promise.resolve();
36
- /** Only fenced diagram sources; Mermaid leaves other `.mermaid` nodes in the tree after render. */
37
- const allPres = Array.from(docBody.querySelectorAll("pre.mermaid"));
38
- /** Do not re-run on wrappers that already have SVG (avoids corrupting output after dual-mobile pane flip). */
39
- const nodes = allPres.filter((pre) => {
40
- const wrap = pre.closest(".commentray-mermaid");
41
- return wrap === null || wrap.querySelector("svg") === null;
42
- });
43
- if (nodes.length === 0)
44
- return Promise.resolve();
45
- const m = globalThis
46
- .commentrayMermaid;
47
- if (!m)
105
+ if (!globalThis.commentrayMermaid) {
106
+ pendingMermaidDocRoots.add(docBody);
48
107
  return Promise.resolve();
49
- return m
50
- .run({ nodes })
51
- .then(() => {
52
- dispatchCommentrayMermaidDone();
53
- })
54
- .catch((err) => {
55
- console.error("Commentray: mermaid.run failed", err);
56
- });
108
+ }
109
+ return queueMermaidRunForDocRoot(docBody);
57
110
  }
58
111
  function clamp(n, lo, hi) {
59
112
  return Math.max(lo, Math.min(hi, n));
@@ -1038,6 +1091,14 @@ function emptyBrowsePreviewInnerHtml(scope, filePathLabel, mutable) {
1038
1091
  const hint = emptyBrowsePreviewHint("commentray-and-paths", fb.length, fb.length, true);
1039
1092
  return emptySearchBrowsePreviewInnerHtml(hint, fb, hitCtx);
1040
1093
  }
1094
+ /**
1095
+ * List rows use `focus({ preventScroll: true })` so the page does not jump; follow with
1096
+ * `scrollIntoView` so overflow panels (`#search-results`, `#documented-files-tree`) still scroll.
1097
+ */
1098
+ function focusListRowAndReveal(el) {
1099
+ el.focus({ preventScroll: true });
1100
+ el.scrollIntoView({ block: "nearest", inline: "nearest" });
1101
+ }
1041
1102
  function wireSearchResultsHitListKeyboard(searchResults, searchInput) {
1042
1103
  searchResults.addEventListener("keydown", (e) => {
1043
1104
  if (e.isComposing || searchResults.hidden)
@@ -1052,17 +1113,17 @@ function wireSearchResultsHitListKeyboard(searchResults, searchInput) {
1052
1113
  if (idx < 0)
1053
1114
  return;
1054
1115
  if (e.key === "ArrowDown" && idx < hits.length - 1) {
1055
- hits[idx + 1].focus({ preventScroll: true });
1116
+ focusListRowAndReveal(hits[idx + 1]);
1056
1117
  e.preventDefault();
1057
1118
  return;
1058
1119
  }
1059
1120
  if (e.key === "ArrowUp") {
1060
1121
  if (idx > 0) {
1061
- hits[idx - 1].focus({ preventScroll: true });
1122
+ focusListRowAndReveal(hits[idx - 1]);
1062
1123
  e.preventDefault();
1063
1124
  return;
1064
1125
  }
1065
- searchInput.focus({ preventScroll: true });
1126
+ focusListRowAndReveal(searchInput);
1066
1127
  e.preventDefault();
1067
1128
  }
1068
1129
  });
@@ -1076,7 +1137,7 @@ function wireSearchInputKeyboard(searchInput, searchResults, actions) {
1076
1137
  if (!searchResults.hidden) {
1077
1138
  const hits = listSearchHitButtons(searchResults);
1078
1139
  if (hits.length > 0 && document.activeElement === searchInput) {
1079
- hits[0].focus({ preventScroll: true });
1140
+ focusListRowAndReveal(hits[0]);
1080
1141
  e.preventDefault();
1081
1142
  return;
1082
1143
  }
@@ -2262,7 +2323,34 @@ function focusDocumentedFilesFilterInput() {
2262
2323
  const el = document.getElementById("documented-files-filter");
2263
2324
  if (!(el instanceof HTMLInputElement))
2264
2325
  return;
2265
- el.focus({ preventScroll: true });
2326
+ focusListRowAndReveal(el);
2327
+ }
2328
+ function focusDocumentedFilesHubSummaryIfPresent(hub) {
2329
+ const sum = hub.querySelector("summary");
2330
+ if (sum instanceof HTMLElement)
2331
+ sum.focus({ preventScroll: true });
2332
+ }
2333
+ /** Escape to close; pointer outside `#documented-files-hub` to close (capture). */
2334
+ function wireDocumentedFilesHubDismissalHandlers(hub) {
2335
+ function onEscape(ev) {
2336
+ if (!hub.open || ev.key !== "Escape")
2337
+ return;
2338
+ ev.preventDefault();
2339
+ hub.open = false;
2340
+ focusDocumentedFilesHubSummaryIfPresent(hub);
2341
+ }
2342
+ document.addEventListener("keydown", onEscape, true);
2343
+ document.addEventListener("pointerdown", (ev) => {
2344
+ if (!hub.open)
2345
+ return;
2346
+ const t = ev.target;
2347
+ if (!(t instanceof Node))
2348
+ return;
2349
+ if (hub.contains(t))
2350
+ return;
2351
+ hub.open = false;
2352
+ focusDocumentedFilesHubSummaryIfPresent(hub);
2353
+ }, true);
2266
2354
  }
2267
2355
  function wireDocumentedFilesTree() {
2268
2356
  const hub = document.getElementById("documented-files-hub");
@@ -2313,16 +2401,7 @@ function wireDocumentedFilesTree() {
2313
2401
  return;
2314
2402
  void hydrateTree();
2315
2403
  });
2316
- function onDocumentedFilesHubEscape(ev) {
2317
- if (!detailsHub.open || ev.key !== "Escape")
2318
- return;
2319
- ev.preventDefault();
2320
- detailsHub.open = false;
2321
- const sum = detailsHub.querySelector("summary");
2322
- if (sum instanceof HTMLElement)
2323
- sum.focus({ preventScroll: true });
2324
- }
2325
- document.addEventListener("keydown", onDocumentedFilesHubEscape, true);
2404
+ wireDocumentedFilesHubDismissalHandlers(detailsHub);
2326
2405
  treeMount.addEventListener("keydown", (e) => {
2327
2406
  if (!detailsHub.open || e.isComposing)
2328
2407
  return;
@@ -2337,19 +2416,19 @@ function wireDocumentedFilesTree() {
2337
2416
  return;
2338
2417
  if (e.key === "ArrowDown") {
2339
2418
  if (idx < links.length - 1) {
2340
- links[idx + 1].focus({ preventScroll: true });
2419
+ focusListRowAndReveal(links[idx + 1]);
2341
2420
  e.preventDefault();
2342
2421
  }
2343
2422
  return;
2344
2423
  }
2345
2424
  if (e.key === "ArrowUp") {
2346
2425
  if (idx > 0) {
2347
- links[idx - 1].focus({ preventScroll: true });
2426
+ focusListRowAndReveal(links[idx - 1]);
2348
2427
  e.preventDefault();
2349
2428
  return;
2350
2429
  }
2351
2430
  if (filterInput instanceof HTMLInputElement) {
2352
- filterInput.focus({ preventScroll: true });
2431
+ focusListRowAndReveal(filterInput);
2353
2432
  e.preventDefault();
2354
2433
  }
2355
2434
  }
@@ -2366,7 +2445,7 @@ function wireDocumentedFilesTree() {
2366
2445
  const links = listDocumentedTreeFileLinks(treeMount);
2367
2446
  if (links.length === 0)
2368
2447
  return;
2369
- links[0].focus({ preventScroll: true });
2448
+ focusListRowAndReveal(links[0]);
2370
2449
  e.preventDefault();
2371
2450
  });
2372
2451
  }