@commentray/render 0.0.4 → 0.0.6

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.
Files changed (49) hide show
  1. package/dist/block-stretch-layout.d.ts.map +1 -1
  2. package/dist/block-stretch-layout.js +11 -23
  3. package/dist/block-stretch-layout.js.map +1 -1
  4. package/dist/browse-page-slug.d.ts +6 -0
  5. package/dist/browse-page-slug.d.ts.map +1 -0
  6. package/dist/browse-page-slug.js +9 -0
  7. package/dist/browse-page-slug.js.map +1 -0
  8. package/dist/build-commentray-nav-search.d.ts +22 -6
  9. package/dist/build-commentray-nav-search.d.ts.map +1 -1
  10. package/dist/build-commentray-nav-search.js +55 -36
  11. package/dist/build-commentray-nav-search.js.map +1 -1
  12. package/dist/build-stamp.d.ts +6 -0
  13. package/dist/build-stamp.d.ts.map +1 -0
  14. package/dist/build-stamp.js +23 -0
  15. package/dist/build-stamp.js.map +1 -0
  16. package/dist/code-browser-block-rays.d.ts +48 -0
  17. package/dist/code-browser-block-rays.d.ts.map +1 -0
  18. package/dist/code-browser-block-rays.js +95 -0
  19. package/dist/code-browser-block-rays.js.map +1 -0
  20. package/dist/code-browser-client.bundle.js +12 -7
  21. package/dist/code-browser-client.js +787 -100
  22. package/dist/code-browser-client.js.map +1 -1
  23. package/dist/code-browser-pair-nav.d.ts +23 -0
  24. package/dist/code-browser-pair-nav.d.ts.map +1 -0
  25. package/dist/code-browser-pair-nav.js +59 -0
  26. package/dist/code-browser-pair-nav.js.map +1 -0
  27. package/dist/code-browser-search.d.ts +50 -0
  28. package/dist/code-browser-search.d.ts.map +1 -1
  29. package/dist/code-browser-search.js +117 -0
  30. package/dist/code-browser-search.js.map +1 -1
  31. package/dist/code-browser.d.ts +51 -2
  32. package/dist/code-browser.d.ts.map +1 -1
  33. package/dist/code-browser.js +863 -196
  34. package/dist/code-browser.js.map +1 -1
  35. package/dist/highlighted-code-lines.d.ts +19 -0
  36. package/dist/highlighted-code-lines.d.ts.map +1 -0
  37. package/dist/highlighted-code-lines.js +61 -0
  38. package/dist/highlighted-code-lines.js.map +1 -0
  39. package/dist/index.d.ts +2 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/markdown-pipeline.d.ts.map +1 -1
  44. package/dist/markdown-pipeline.js +3 -2
  45. package/dist/markdown-pipeline.js.map +1 -1
  46. package/dist/mermaid-runtime-html.d.ts.map +1 -1
  47. package/dist/mermaid-runtime-html.js +3 -1
  48. package/dist/mermaid-runtime-html.js.map +1 -1
  49. package/package.json +2 -2
@@ -1,11 +1,23 @@
1
1
  import { FuzzySearcher, PrefixSearcher, Query, SearcherFactory, SubstringSearcher, } from "@m31coding/fuzzy-search";
2
+ import { activeBlockIdForViewport, clampViewportYToGutterLocal, codeLineDomIndex0, gutterRayBezierPaths, sortBlockLinksBySource, } from "./code-browser-block-rays.js";
2
3
  import { mirroredScrollTop, pickCommentrayLineForSourceScroll, pickSourceLine0ForCommentrayScroll, } from "./code-browser-scroll-sync.js";
3
4
  import { decodeBase64Utf8 } from "./code-browser-encoding.js";
4
5
  import { readEmbeddedRawB64Strings } from "./code-browser-embedded-payload.js";
5
- import { findOrderedTokenSpans, lineAtIndex, offsetToLineIndex } from "./code-browser-search.js";
6
+ import { escapeHtmlHighlightingSearchTokens, filterPairsByDocumentedTreeQuery, findOrderedTokenSpans, lineAtIndex, offsetToLineIndex, pathRowsFromDocumentedPairs, tokenizeQuery, uniqueSourceFilePreviewRows, } from "./code-browser-search.js";
7
+ import { findDocumentedPair, isSameDocumentedPair, normPosixPath, resolveStaticBrowseHref, } from "./code-browser-pair-nav.js";
6
8
  import { readWebStorageItem, writeWebStorageItem } from "./code-browser-web-storage.js";
7
- function tokenizeQuery(q) {
8
- return q.trim().split(/\s+/).filter(Boolean);
9
+ function runMermaidOnFreshDocNodes(docBody) {
10
+ if (typeof globalThis.location !== "undefined" && globalThis.location.protocol === "file:")
11
+ return;
12
+ const nodes = docBody.querySelectorAll(".mermaid");
13
+ if (nodes.length === 0)
14
+ return;
15
+ const m = globalThis
16
+ .commentrayMermaid;
17
+ if (!m)
18
+ return;
19
+ const list = Array.from(nodes);
20
+ void m.run({ nodes: list }).catch(() => { });
9
21
  }
10
22
  function clamp(n, lo, hi) {
11
23
  return Math.max(lo, Math.min(hi, n));
@@ -27,7 +39,9 @@ function snippet(s, maxLen) {
27
39
  function mergeHits(rows, max) {
28
40
  const byKey = new Map();
29
41
  for (const r of rows) {
30
- const key = r.kind === "path" ? `path:${String(r.line)}:${r.text.slice(0, 64)}` : `${r.kind}:${r.line}`;
42
+ const key = r.kind === "path"
43
+ ? `path:${r.spPath ?? ""}|${r.crPath ?? ""}|${r.text.slice(0, 120)}`
44
+ : `${r.kind}:${r.line}:${r.crPath ?? ""}`;
31
45
  const prev = byKey.get(key);
32
46
  if (!prev || r.score > prev.score) {
33
47
  byKey.set(key, r);
@@ -73,43 +87,144 @@ function buildFuzzyHits(searcher, query, topN) {
73
87
  text: row.text,
74
88
  score: 100 + m.quality,
75
89
  source: "fuzzy",
90
+ crPath: row.crPath,
91
+ spPath: row.spPath,
92
+ });
93
+ }
94
+ return out;
95
+ }
96
+ /** Ordered token matches per path row (keeps `spPath` / `crPath` for navigation). */
97
+ function buildOrderedPathHitsFromRows(pathRows, tokens) {
98
+ if (tokens.length === 0)
99
+ return [];
100
+ const out = [];
101
+ for (const row of pathRows) {
102
+ if (row.kind !== "path")
103
+ continue;
104
+ const spans = findOrderedTokenSpans(row.text, tokens);
105
+ if (spans.length === 0)
106
+ continue;
107
+ out.push({
108
+ kind: "path",
109
+ line: row.line,
110
+ text: row.text,
111
+ score: 1000,
112
+ source: "ordered",
113
+ spPath: row.spPath,
114
+ crPath: row.crPath,
76
115
  });
77
116
  }
78
117
  return out;
79
118
  }
80
119
  function computeMergedSearchHits(input) {
81
- const { scope, filePathLabel, commentrayPathLabel, rawCode, rawMd, searcher, queryRaw, tokens } = input;
82
- const pathBlob = [filePathLabel, commentrayPathLabel]
83
- .filter((s) => s.trim().length > 0)
84
- .join("\n");
120
+ const { scope, filePathLabel, commentrayPathLabel, rawCode, rawMd, searcher, queryRaw, tokens, pathBlobWide, pathRowsForOrdering, } = input;
121
+ const pathBlob = (pathBlobWide && pathBlobWide.trim().length > 0
122
+ ? pathBlobWide.trim()
123
+ : [filePathLabel, commentrayPathLabel].filter((s) => s.trim().length > 0).join("\n")) || "";
85
124
  const orderedCode = scope === "commentray-and-paths" ? [] : buildOrderedHits(rawCode, "code", tokens);
86
- const orderedPath = scope === "commentray-and-paths" && pathBlob ? buildOrderedHits(pathBlob, "path", tokens) : [];
125
+ const orderedPath = scope === "commentray-and-paths" && pathRowsForOrdering && pathRowsForOrdering.length > 0
126
+ ? buildOrderedPathHitsFromRows(pathRowsForOrdering, tokens)
127
+ : scope === "commentray-and-paths" && pathBlob
128
+ ? buildOrderedHits(pathBlob, "path", tokens)
129
+ : [];
87
130
  const orderedMd = buildOrderedHits(rawMd, "md", tokens);
88
131
  const fuzzyHits = buildFuzzyHits(searcher, queryRaw, 60);
89
132
  return mergeHits([...orderedCode, ...orderedPath, ...orderedMd, ...fuzzyHits], 80);
90
133
  }
91
- function searchResultsInnerHtml(scope, combined) {
134
+ function searchScopeResultsHintIntro(scope) {
135
+ return scope === "commentray-and-paths"
136
+ ? "Paths + indexed commentray (this page + browse pages when built). Ordered tokens + fuzzy lines."
137
+ : "Whole source: whitespace tokens in order (may span lines). Per-line fuzzy ranking for typos.";
138
+ }
139
+ function searchHitMetaLabel(h, ctx) {
140
+ if (h.kind === "code")
141
+ return `Code L${h.line + 1}`;
142
+ if (h.kind === "path")
143
+ return `Path`;
144
+ const foreign = h.crPath && h.crPath !== ctx.currentCommentrayPath ? ` · ${h.crPath}` : "";
145
+ return `Commentray L${h.line + 1}${foreign}`;
146
+ }
147
+ function searchHitButtonHtml(h, tokens, ctx) {
148
+ const label = searchHitMetaLabel(h, ctx);
149
+ const tag = h.source === "ordered" ? "ordered" : "fuzzy";
150
+ const snippetHtml = escapeHtmlHighlightingSearchTokens(snippet(h.text, 320), tokens);
151
+ const crAttr = escapeHtmlText(h.kind === "md" ? (h.crPath ?? ctx.currentCommentrayPath) : (h.crPath ?? ""));
152
+ const spAttr = escapeHtmlText(h.kind === "md" ? (h.spPath ?? ctx.currentSourcePath) : (h.spPath ?? ""));
153
+ return (`<button type="button" class="hit" data-kind="${h.kind}" data-line="${String(h.line)}" data-cr-path="${crAttr}" data-sp-path="${spAttr}">` +
154
+ `<span class="meta">${escapeHtmlText(label)} <span class="src-tag">(${tag})</span></span>` +
155
+ `<div class="snippet">${snippetHtml}</div></button>`);
156
+ }
157
+ function searchResultsInnerHtml(scope, combined, tokens, ctx) {
92
158
  if (combined.length === 0) {
93
159
  return '<div class="hint">No matches. Try fewer tokens or looser spelling (fuzzy matches per line).</div>';
94
160
  }
95
- const hintIntro = scope === "commentray-and-paths"
96
- ? "Paths + commentray only (no code-body indexing): ordered tokens and per-line fuzzy ranking."
97
- : "Whole source: whitespace tokens in order (may span lines). Per-line fuzzy ranking for typos.";
161
+ const hintIntro = searchScopeResultsHintIntro(scope);
98
162
  const buf = [];
99
163
  buf.push(`<div class="hint">${hintIntro} ${combined.length} hit(s).</div>`);
100
164
  for (const h of combined) {
101
- const label = h.kind === "code"
102
- ? `Code L${h.line + 1}`
103
- : h.kind === "path"
104
- ? `Path L${h.line + 1}`
105
- : `Commentray L${h.line + 1}`;
106
- const tag = h.source === "ordered" ? "ordered" : "fuzzy";
107
- buf.push(`<button type="button" class="hit" data-kind="${h.kind}" data-line="${String(h.line)}">` +
108
- `<span class="meta">${label} <span class="src-tag">(${tag})</span></span>` +
109
- `<div class="snippet">${escapeHtmlText(snippet(h.text, 200))}</div></button>`);
165
+ buf.push(searchHitButtonHtml(h, tokens, ctx));
166
+ }
167
+ return buf.join("");
168
+ }
169
+ function emptyBrowsePreviewHint(scope, rowCount, totalUnique, usedIndexFallback) {
170
+ if (scope === "full") {
171
+ return "Documented source for this page. Type to search.";
172
+ }
173
+ if (usedIndexFallback) {
174
+ return "Documented source on this page. Type to search the index when it is available.";
175
+ }
176
+ if (totalUnique > rowCount) {
177
+ return `Indexed source files (${String(rowCount)} of ${String(totalUnique)} shown). Type to search.`;
178
+ }
179
+ return `Indexed source files (${String(totalUnique)}). Type to search.`;
180
+ }
181
+ function emptySearchBrowsePreviewInnerHtml(hint, rows, ctx) {
182
+ const tokens = [];
183
+ const buf = [`<div class="hint">${escapeHtmlText(hint)}</div>`];
184
+ const hits = rows.map((r, i) => ({
185
+ kind: "path",
186
+ line: i,
187
+ text: r.sourcePath,
188
+ score: 1000,
189
+ source: "ordered",
190
+ spPath: r.sourcePath,
191
+ crPath: r.commentrayPath,
192
+ }));
193
+ for (const h of hits) {
194
+ buf.push(searchHitButtonHtml(h, tokens, ctx));
110
195
  }
111
196
  return buf.join("");
112
197
  }
198
+ function scrollDocToMarkdownLine0(docScrollEl, line0, mdLineCount) {
199
+ const el = docScrollEl.querySelector(`#commentray-md-line-${String(line0)}`);
200
+ if (el instanceof HTMLElement) {
201
+ const top = el.getBoundingClientRect().top -
202
+ docScrollEl.getBoundingClientRect().top +
203
+ docScrollEl.scrollTop;
204
+ docScrollEl.scrollTo({ top: Math.max(0, top - 8), behavior: "smooth" });
205
+ return;
206
+ }
207
+ if (mdLineCount <= 1)
208
+ return;
209
+ const ratio = line0 / Math.max(1, mdLineCount - 1);
210
+ const maxScroll = docScrollEl.scrollHeight - docScrollEl.clientHeight;
211
+ docScrollEl.scrollTo({ top: ratio * Math.max(0, maxScroll), behavior: "smooth" });
212
+ }
213
+ function navigateToDocumentedPair(pair, mdLine0) {
214
+ if (pair.staticBrowseUrl?.trim()) {
215
+ const href = resolveStaticBrowseHref(pair.staticBrowseUrl.trim(), globalThis.location.pathname, globalThis.location.origin);
216
+ const u = new URL(href);
217
+ if (mdLine0 !== null && mdLine0 >= 0)
218
+ u.hash = `commentray-md-line-${String(mdLine0)}`;
219
+ globalThis.location.assign(u.toString());
220
+ return;
221
+ }
222
+ const gh = (pair.commentrayOnGithub ?? "").trim();
223
+ if (gh.length > 0) {
224
+ const url = mdLine0 !== null && mdLine0 >= 0 ? `${gh}#L${String(mdLine0 + 1)}` : gh;
225
+ globalThis.location.assign(url);
226
+ }
227
+ }
113
228
  function readSearchScopeFromShell(shell) {
114
229
  const scopeAttr = shell.getAttribute("data-search-scope") || "";
115
230
  return {
@@ -140,11 +255,101 @@ function buildIndexedSearchRows(scope, rawCode, rawMd, filePathLabel, commentray
140
255
  }
141
256
  function indexSearchLineRows(rows) {
142
257
  const searcher = SearcherFactory.createDefaultSearcher();
143
- searcher.indexEntities(rows, (e) => `${e.kind}:${e.kind === "path" ? e.text : e.line}`, (e) => [e.text]);
258
+ searcher.indexEntities(rows, (e) => {
259
+ if (e.kind === "md" && e.crPath)
260
+ return `md:${e.crPath}:${e.line}`;
261
+ if (e.kind === "path")
262
+ return `path:${e.spPath ?? ""}|${e.crPath ?? ""}|${e.line}|${e.text.slice(0, 120)}`;
263
+ return `${e.kind}:${e.line}`;
264
+ }, (e) => [e.text]);
144
265
  return searcher;
145
266
  }
267
+ function findSearchHitButton(leaf, searchResults) {
268
+ let t = leaf;
269
+ while (t) {
270
+ if (t.classList?.contains("hit"))
271
+ return t;
272
+ if (t === searchResults)
273
+ return null;
274
+ t = t.parentElement;
275
+ }
276
+ return null;
277
+ }
278
+ function scrollCodeHitToView(line) {
279
+ const el = document.getElementById(`code-line-${String(line)}`);
280
+ if (el)
281
+ el.scrollIntoView({ block: "nearest", behavior: "smooth" });
282
+ }
283
+ function handlePathSearchHit(button, deps) {
284
+ const hitCr = (button.getAttribute("data-cr-path") ?? "").trim();
285
+ const hitSp = (button.getAttribute("data-sp-path") ?? "").trim();
286
+ const pair = findDocumentedPair(deps.mutable.documentedPairs, hitCr, hitSp);
287
+ if (pair && isSameDocumentedPair(pair, deps.filePathLabel, deps.mutable.commentrayPathLabel)) {
288
+ deps.docScrollEl.scrollTo({ top: 0, behavior: "smooth" });
289
+ return;
290
+ }
291
+ if (pair)
292
+ navigateToDocumentedPair(pair, null);
293
+ }
294
+ function handleMdSearchHit(line, crHit, deps) {
295
+ const curCr = deps.mutable.commentrayPathLabel.trim();
296
+ const cr = crHit.trim();
297
+ if (cr.length > 0 && normPosixPath(cr) !== normPosixPath(curCr)) {
298
+ const pair = findDocumentedPair(deps.mutable.documentedPairs, cr, "");
299
+ if (pair) {
300
+ navigateToDocumentedPair(pair, line);
301
+ return;
302
+ }
303
+ return;
304
+ }
305
+ scrollDocToMarkdownLine0(deps.docScrollEl, line, deps.mutable.mdLines.length);
306
+ }
307
+ function handleSearchHitButtonClick(button, deps) {
308
+ const kind = button.getAttribute("data-kind");
309
+ const line = parseInt(button.getAttribute("data-line") || "0", 10);
310
+ const crHit = button.getAttribute("data-cr-path")?.trim() ?? "";
311
+ if (kind === "code") {
312
+ scrollCodeHitToView(line);
313
+ return;
314
+ }
315
+ if (kind === "path") {
316
+ handlePathSearchHit(button, deps);
317
+ return;
318
+ }
319
+ handleMdSearchHit(line, crHit, deps);
320
+ }
321
+ /** Empty query + ArrowDown: browse preview HTML, or null when there is nothing to show. */
322
+ function emptyBrowsePreviewInnerHtml(scope, filePathLabel, mutable) {
323
+ const hitCtx = {
324
+ currentCommentrayPath: mutable.commentrayPathLabel,
325
+ currentSourcePath: filePathLabel,
326
+ };
327
+ if (scope === "full") {
328
+ const sp = filePathLabel.trim();
329
+ if (sp.length === 0)
330
+ return null;
331
+ const rows = [
332
+ { sourcePath: sp, commentrayPath: mutable.commentrayPathLabel.trim() },
333
+ ];
334
+ const hint = emptyBrowsePreviewHint("full", rows.length, rows.length, false);
335
+ return emptySearchBrowsePreviewInnerHtml(hint, rows, hitCtx);
336
+ }
337
+ const { rows, totalUnique } = uniqueSourceFilePreviewRows(mutable.documentedPairs);
338
+ if (rows.length > 0) {
339
+ const hint = emptyBrowsePreviewHint("commentray-and-paths", rows.length, totalUnique, false);
340
+ return emptySearchBrowsePreviewInnerHtml(hint, rows, hitCtx);
341
+ }
342
+ const sp = filePathLabel.trim();
343
+ if (sp.length === 0)
344
+ return null;
345
+ const fb = [
346
+ { sourcePath: sp, commentrayPath: mutable.commentrayPathLabel.trim() },
347
+ ];
348
+ const hint = emptyBrowsePreviewHint("commentray-and-paths", fb.length, fb.length, true);
349
+ return emptySearchBrowsePreviewInnerHtml(hint, fb, hitCtx);
350
+ }
146
351
  function wireSearchUi(ctx) {
147
- const { scope, filePathLabel, commentrayPathLabel, rawCode, rawMd, mdLines, searcher, searchInput, searchClear, searchResults, docPane, } = ctx;
352
+ const { scope, filePathLabel, mutable, rawCode, searchInput, searchClear, searchResults, docScrollEl, } = ctx;
148
353
  let debounceTimer;
149
354
  function clearSearch() {
150
355
  clearTimeout(debounceTimer);
@@ -153,6 +358,13 @@ function wireSearchUi(ctx) {
153
358
  searchResults.innerHTML = "";
154
359
  searchResults.hidden = true;
155
360
  }
361
+ function renderEmptyBrowsePreview() {
362
+ const html = emptyBrowsePreviewInnerHtml(scope, filePathLabel, mutable);
363
+ if (html === null)
364
+ return;
365
+ searchResults.hidden = false;
366
+ searchResults.innerHTML = html;
367
+ }
156
368
  function runSearch() {
157
369
  const tokens = tokenizeQuery(searchInput.value);
158
370
  if (tokens.length === 0) {
@@ -163,46 +375,40 @@ function wireSearchUi(ctx) {
163
375
  const combined = computeMergedSearchHits({
164
376
  scope,
165
377
  filePathLabel,
166
- commentrayPathLabel,
378
+ commentrayPathLabel: mutable.commentrayPathLabel,
167
379
  rawCode,
168
- rawMd,
169
- searcher,
380
+ rawMd: mutable.rawMd,
381
+ searcher: mutable.searcher,
170
382
  queryRaw: searchInput.value,
171
383
  tokens,
384
+ pathBlobWide: mutable.pathBlobWide,
385
+ pathRowsForOrdering: mutable.pathRowsForOrdering.length > 0 ? mutable.pathRowsForOrdering : undefined,
172
386
  });
173
387
  searchResults.hidden = false;
174
- searchResults.innerHTML = searchResultsInnerHtml(scope, combined);
388
+ searchResults.innerHTML = searchResultsInnerHtml(scope, combined, tokens, {
389
+ currentCommentrayPath: mutable.commentrayPathLabel,
390
+ currentSourcePath: filePathLabel,
391
+ });
175
392
  }
393
+ const hitClickDeps = { mutable, docScrollEl, filePathLabel };
176
394
  searchResults.addEventListener("click", (ev) => {
177
- let t = ev.target;
178
- while (t && t !== searchResults && (!t.classList || !t.classList.contains("hit"))) {
179
- t = t.parentElement;
180
- }
181
- if (!t || !t.classList || !t.classList.contains("hit"))
395
+ const hit = findSearchHitButton(ev.target, searchResults);
396
+ if (!hit)
182
397
  return;
183
- const kind = t.getAttribute("data-kind");
184
- const line = parseInt(t.getAttribute("data-line") || "0", 10);
185
- if (kind === "code") {
186
- const el = document.getElementById(`code-line-${String(line)}`);
187
- if (el)
188
- el.scrollIntoView({ block: "nearest", behavior: "smooth" });
189
- }
190
- else if (kind === "path") {
191
- docPane.scrollTo({ top: 0, behavior: "smooth" });
192
- }
193
- else {
194
- const total = mdLines.length;
195
- if (total <= 0)
196
- return;
197
- const ratio = line / Math.max(1, total - 1);
198
- const maxScroll = docPane.scrollHeight - docPane.clientHeight;
199
- docPane.scrollTo({ top: ratio * Math.max(0, maxScroll), behavior: "smooth" });
200
- }
398
+ handleSearchHitButtonClick(hit, hitClickDeps);
201
399
  });
202
400
  searchInput.addEventListener("input", () => {
203
401
  clearTimeout(debounceTimer);
204
402
  debounceTimer = setTimeout(runSearch, 200);
205
403
  });
404
+ searchInput.addEventListener("keydown", (e) => {
405
+ if (e.key !== "ArrowDown")
406
+ return;
407
+ if (tokenizeQuery(searchInput.value).length > 0)
408
+ return;
409
+ renderEmptyBrowsePreview();
410
+ e.preventDefault();
411
+ });
206
412
  searchClear.addEventListener("click", clearSearch);
207
413
  document.addEventListener("keydown", (e) => {
208
414
  if (e.key !== "Escape")
@@ -312,8 +518,9 @@ function wireBidirectionalScroll(codePane, docPane, syncFromCode, syncFromDoc) {
312
518
  }, { passive: true });
313
519
  }
314
520
  /** Index-backed scroll sync when `data-scroll-block-links-b64` is present; else see proportional fallback. */
315
- function wireBlockAwareScrollSync(codePane, docPane, links) {
521
+ function wireBlockAwareScrollSync(codePane, docPane, getLinks) {
316
522
  wireBidirectionalScroll(codePane, docPane, () => {
523
+ const links = getLinks();
317
524
  const line1 = probeCodeLine1FromViewport(codePane);
318
525
  const mdLine0 = pickCommentrayLineForSourceScroll(links, line1);
319
526
  if (mdLine0 === null) {
@@ -332,6 +539,7 @@ function wireBlockAwareScrollSync(codePane, docPane, links) {
332
539
  }
333
540
  }
334
541
  }, () => {
542
+ const links = getLinks();
335
543
  const mdLine0 = probeCommentrayLine0FromDoc(docPane);
336
544
  const src0 = pickSourceLine0ForCommentrayScroll(links, mdLine0);
337
545
  if (src0 === null) {
@@ -359,14 +567,151 @@ function wireProportionalScrollSync(codePane, docPane) {
359
567
  codePane.scrollTop = mirroredScrollTop(docPane.scrollTop, docPane.scrollHeight, docPane.clientHeight, codePane.scrollHeight, codePane.clientHeight);
360
568
  });
361
569
  }
570
+ function centerYInViewport(el) {
571
+ const r = el.getBoundingClientRect();
572
+ return (r.top + r.bottom) / 2;
573
+ }
574
+ /**
575
+ * Vertical anchor for gutter rays on the source side. Must match the **numbered row** the reader
576
+ * sees: the full `.code-line` row (line number + highlighted code) shares one grid row with
577
+ * aligned line-heights. Measuring only `pre code` can shift Y (hljs spans, sub-pixel layout) so
578
+ * rays sit above the line labels; the row’s geometric center tracks `lines:a-b` anchors reliably.
579
+ */
580
+ function codeLineHighlightCenterYViewport(lineEl) {
581
+ return centerYInViewport(lineEl);
582
+ }
583
+ function commentaryBandEndYViewport(docScrollEl, next, docTop) {
584
+ if (next) {
585
+ const nextEl = document.getElementById(`commentray-block-${next.id}`);
586
+ return nextEl ? nextEl.getBoundingClientRect().top - 3 : centerYInViewport(docTop);
587
+ }
588
+ const dr = docScrollEl.getBoundingClientRect();
589
+ let bottom = dr.bottom - 4;
590
+ const lastKid = docScrollEl.children[docScrollEl.children.length - 1];
591
+ if (lastKid)
592
+ bottom = Math.min(bottom, lastKid.getBoundingClientRect().bottom - 4);
593
+ return bottom;
594
+ }
595
+ function subscribeBlockRayRedraw(gutter, codePane, docScrollEl, scheduleDraw) {
596
+ const onScrollOrResize = () => scheduleDraw();
597
+ codePane.addEventListener("scroll", onScrollOrResize, { passive: true });
598
+ docScrollEl.addEventListener("scroll", onScrollOrResize, { passive: true });
599
+ globalThis.addEventListener("resize", onScrollOrResize);
600
+ const ro = new ResizeObserver(onScrollOrResize);
601
+ ro.observe(gutter);
602
+ ro.observe(codePane);
603
+ ro.observe(docScrollEl);
604
+ const shell = gutter.parentElement;
605
+ if (shell)
606
+ ro.observe(shell);
607
+ }
608
+ function drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSourceLine1Based) {
609
+ const links = getLinks();
610
+ const sorted = sortBlockLinksBySource(links);
611
+ const gutterRect = gutter.getBoundingClientRect();
612
+ const w = gutterRect.width;
613
+ const h = gutterRect.height;
614
+ if (w <= 0 || h <= 0 || sorted.length === 0) {
615
+ svg.replaceChildren();
616
+ return;
617
+ }
618
+ const activeId = activeBlockIdForViewport(links, probeTopSourceLine1Based());
619
+ svg.setAttribute("viewBox", `0 0 ${String(w)} ${String(h)}`);
620
+ svg.setAttribute("preserveAspectRatio", "none");
621
+ const parts = [];
622
+ for (let i = 0; i < sorted.length; i++) {
623
+ const link = sorted[i];
624
+ if (!link)
625
+ continue;
626
+ const next = sorted[i + 1];
627
+ const i0 = codeLineDomIndex0(link.sourceStart);
628
+ const i1 = codeLineDomIndex0(link.sourceEnd);
629
+ const codeTop = document.getElementById(`code-line-${String(i0)}`);
630
+ const codeBot = document.getElementById(`code-line-${String(i1)}`);
631
+ const docTop = document.getElementById(`commentray-block-${link.id}`);
632
+ if (!codeTop || !codeBot || !docTop)
633
+ continue;
634
+ const docEndYViewport = commentaryBandEndYViewport(docScrollEl, next, docTop);
635
+ const yCodeTop = codeLineHighlightCenterYViewport(codeTop);
636
+ const yCodeBot = codeLineHighlightCenterYViewport(codeBot);
637
+ const yDocTop = docTop.getBoundingClientRect().top + 2;
638
+ const yDocEnd = Math.max(docEndYViewport, yDocTop + 4);
639
+ const c0 = clampViewportYToGutterLocal(yCodeTop, gutterRect.top, h);
640
+ const c1 = clampViewportYToGutterLocal(yDocTop, gutterRect.top, h);
641
+ const c2 = clampViewportYToGutterLocal(yCodeBot, gutterRect.top, h);
642
+ const c3 = clampViewportYToGutterLocal(yDocEnd, gutterRect.top, h);
643
+ const strokeClass = link.id === activeId ? "gutter__rays-path gutter__rays-path--active" : "gutter__rays-path";
644
+ const trailClass = `${strokeClass} gutter__rays-path--trail`;
645
+ const topPaths = gutterRayBezierPaths(0, c0.y, w, c1.y, {
646
+ tension: 0.38,
647
+ clipStart: c0.clipped,
648
+ clipEnd: c1.clipped,
649
+ });
650
+ const botPaths = gutterRayBezierPaths(0, c2.y, w, c3.y, {
651
+ tension: 0.38,
652
+ clipStart: c2.clipped,
653
+ clipEnd: c3.clipped,
654
+ });
655
+ const topExtra = topPaths.dotted ? `<path class="${trailClass}" d="${topPaths.dotted}" />` : "";
656
+ const botExtra = botPaths.dotted ? `<path class="${trailClass}" d="${botPaths.dotted}" />` : "";
657
+ parts.push(`<g class="gutter__rays-block" data-commentray-block="${escapeHtmlText(link.id)}">` +
658
+ `<path class="${strokeClass}" d="${topPaths.solid}" />` +
659
+ topExtra +
660
+ `<path class="${strokeClass}" d="${botPaths.solid}" />` +
661
+ botExtra +
662
+ `</g>`);
663
+ }
664
+ svg.innerHTML = parts.join("");
665
+ }
666
+ /**
667
+ * Splines in the gutter between each block’s source range and its commentary band (dual pane,
668
+ * index-backed blocks). Emphasizes the block aligned with the current source viewport; clamps
669
+ * off-screen endpoints so readers see which way to scroll.
670
+ */
671
+ function wireBlockRayConnectors(args) {
672
+ const { gutter, codePane, docScrollEl, getLinks, probeTopSourceLine1Based } = args;
673
+ const svgNs = "http://www.w3.org/2000/svg";
674
+ const host = document.createElement("div");
675
+ host.className = "gutter__rays";
676
+ host.setAttribute("aria-hidden", "true");
677
+ const svg = document.createElementNS(svgNs, "svg");
678
+ host.appendChild(svg);
679
+ gutter.appendChild(host);
680
+ let raf = 0;
681
+ function scheduleDraw() {
682
+ if (raf !== 0)
683
+ return;
684
+ raf = globalThis.requestAnimationFrame(() => {
685
+ raf = 0;
686
+ drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSourceLine1Based);
687
+ });
688
+ }
689
+ subscribeBlockRayRedraw(gutter, codePane, docScrollEl, scheduleDraw);
690
+ scheduleDraw();
691
+ /** First paint can report gutter height 0 before flex layout settles; redraw after layout. */
692
+ globalThis.requestAnimationFrame(() => {
693
+ scheduleDraw();
694
+ globalThis.requestAnimationFrame(scheduleDraw);
695
+ });
696
+ }
362
697
  function isDocumentedPairNav(x) {
363
698
  if (typeof x !== "object" || x === null)
364
699
  return false;
365
700
  const o = x;
366
- return (typeof o.sourcePath === "string" &&
367
- typeof o.commentrayPath === "string" &&
368
- typeof o.sourceOnGithub === "string" &&
369
- typeof o.commentrayOnGithub === "string");
701
+ if (typeof o.sourcePath !== "string" || typeof o.commentrayPath !== "string")
702
+ return false;
703
+ if (o.staticBrowseUrl !== undefined && typeof o.staticBrowseUrl !== "string")
704
+ return false;
705
+ const sg = o.sourceOnGithub;
706
+ const cg = o.commentrayOnGithub;
707
+ const hasSg = typeof sg === "string";
708
+ const hasCg = typeof cg === "string";
709
+ if (hasSg !== hasCg)
710
+ return false;
711
+ const browseOk = typeof o.staticBrowseUrl === "string" && o.staticBrowseUrl.trim().length > 0;
712
+ if (!browseOk && !hasSg)
713
+ return false;
714
+ return true;
370
715
  }
371
716
  function pairsFromJsonArray(raw) {
372
717
  const pairs = [];
@@ -378,6 +723,50 @@ function pairsFromJsonArray(raw) {
378
723
  }
379
724
  return pairs;
380
725
  }
726
+ function commentrayLineRowFromNavJson(r) {
727
+ if (r.kind !== "commentrayLine")
728
+ return null;
729
+ if (typeof r.line !== "number" || typeof r.text !== "string")
730
+ return null;
731
+ const sp = typeof r.sourcePath === "string" ? r.sourcePath : "";
732
+ const cr = typeof r.commentrayPath === "string" ? r.commentrayPath : "";
733
+ return { kind: "md", line: r.line, text: r.text, spPath: sp, crPath: cr };
734
+ }
735
+ function pathRowFromNavJson(r, pathLine) {
736
+ if (r.kind !== "sourcePath" && r.kind !== "commentrayPath")
737
+ return null;
738
+ const sp = typeof r.sourcePath === "string" ? r.sourcePath : "";
739
+ const cr = typeof r.commentrayPath === "string" ? r.commentrayPath : "";
740
+ const text = r.kind === "sourcePath" ? sp : cr;
741
+ if (!text)
742
+ return null;
743
+ return { kind: "path", line: pathLine, text, spPath: sp, crPath: cr };
744
+ }
745
+ function rowsFromNavSearchJson(doc) {
746
+ if (!doc || typeof doc !== "object")
747
+ return [];
748
+ const rowsRaw = doc.rows;
749
+ if (!Array.isArray(rowsRaw))
750
+ return [];
751
+ const out = [];
752
+ let pathLine = 0;
753
+ for (const raw of rowsRaw) {
754
+ if (!raw || typeof raw !== "object")
755
+ continue;
756
+ const r = raw;
757
+ const mdRow = commentrayLineRowFromNavJson(r);
758
+ if (mdRow) {
759
+ out.push(mdRow);
760
+ continue;
761
+ }
762
+ const pathRow = pathRowFromNavJson(r, pathLine);
763
+ if (pathRow) {
764
+ out.push(pathRow);
765
+ pathLine += 1;
766
+ }
767
+ }
768
+ return out;
769
+ }
381
770
  /** Offline-first: UTF-8 base64 JSON array produced by the static Pages build. */
382
771
  function parseDocumentedPairsFromEmbeddedB64(b64) {
383
772
  const t = b64.trim();
@@ -410,6 +799,43 @@ function insertSourcePathTrie(root, pair) {
410
799
  n = next;
411
800
  }
412
801
  }
802
+ function pathBasenamePosixStyle(p) {
803
+ const t = p.replace(/\\/g, "/").replace(/\/+$/, "");
804
+ const i = t.lastIndexOf("/");
805
+ return i >= 0 ? t.slice(i + 1) : t;
806
+ }
807
+ /** Companion Markdown filename stem (e.g. `main` from `.../README.md/main.md`). */
808
+ function companionDocStem(commentrayPath) {
809
+ const norm = commentrayPath.replace(/\\/g, "/").replace(/\/+$/, "");
810
+ const lastSeg = norm.split("/").filter(Boolean).at(-1) ?? "";
811
+ return lastSeg.replace(/\.md$/i, "");
812
+ }
813
+ function treeFileLinkLabel(pr, disambiguate) {
814
+ const base = pathBasenamePosixStyle(pr.sourcePath);
815
+ if (!disambiguate)
816
+ return base;
817
+ const stem = companionDocStem(pr.commentrayPath);
818
+ return stem !== "" && stem !== base ? `${base} · ${stem}` : base;
819
+ }
820
+ /** Prefer static hub browse page; optional SCM blob URL when the export has no `staticBrowseUrl`. */
821
+ function treeFileLinkHref(pr) {
822
+ const browse = (pr.staticBrowseUrl ?? "").trim();
823
+ if (browse.length > 0) {
824
+ return resolveStaticBrowseHref(browse, globalThis.location.pathname, globalThis.location.origin);
825
+ }
826
+ const gh = (pr.commentrayOnGithub ?? "").trim();
827
+ return gh.length > 0 ? gh : "#";
828
+ }
829
+ function treeFileLinkTitle(pr) {
830
+ const browse = (pr.staticBrowseUrl ?? "").trim();
831
+ if (browse.length > 0) {
832
+ return `${pr.sourcePath} — open this pair in the site viewer`;
833
+ }
834
+ if ((pr.commentrayOnGithub ?? "").trim().length > 0) {
835
+ return `${pr.sourcePath} — open companion commentray on the repository host`;
836
+ }
837
+ return pr.sourcePath;
838
+ }
413
839
  function renderDocumentedTreeHtml(node) {
414
840
  const keys = [...node.children.keys()].sort((a, b) => a.localeCompare(b));
415
841
  if (keys.length === 0)
@@ -424,26 +850,26 @@ function renderDocumentedTreeHtml(node) {
424
850
  lis.push(`<li><div class="tree-dir">${escapeHtmlText(name)}</div>${inner}</li>`);
425
851
  }
426
852
  if (ch.pairs.length > 0) {
853
+ const multi = ch.pairs.length > 1;
427
854
  for (const pr of ch.pairs) {
855
+ const label = escapeHtmlText(treeFileLinkLabel(pr, multi));
856
+ const title = escapeHtmlText(treeFileLinkTitle(pr));
857
+ const href = escapeHtmlText(treeFileLinkHref(pr));
858
+ const useSiteBrowse = (pr.staticBrowseUrl?.trim() ?? "").length > 0;
859
+ const external = useSiteBrowse ? "" : ' target="_blank" rel="noopener noreferrer"';
428
860
  lis.push(`<li><div class="tree-file">` +
429
- `<span class="tree-file-name">${escapeHtmlText(pr.sourcePath)}</span>` +
430
- `<span class="tree-file-links">` +
431
- `<a href="${escapeHtmlText(pr.sourceOnGithub)}" target="_blank" rel="noopener noreferrer">source</a>` +
432
- `<a href="${escapeHtmlText(pr.commentrayOnGithub)}" target="_blank" rel="noopener noreferrer">commentray</a>` +
433
- `</span></div></li>`);
861
+ `<a class="tree-file-link" href="${href}"${external} title="${title}">${label}</a>` +
862
+ `</div></li>`);
434
863
  }
435
864
  }
436
865
  }
437
866
  return `<ul>${lis.join("")}</ul>`;
438
867
  }
439
- function setDocumentedPanelOpen(btn, panel, open) {
440
- panel.hidden = !open;
441
- btn.setAttribute("aria-expanded", open ? "true" : "false");
442
- }
443
- function renderDocumentedPairsIntoHost(treeHost, pairs) {
868
+ function renderDocumentedPairsIntoHost(treeHost, pairs, emptyBecauseFilter) {
444
869
  if (pairs.length === 0) {
445
- treeHost.innerHTML =
446
- '<p class="documented-files-panel__hint">No <code class="documented-files-panel__code">documentedPairs</code> in this export (build with a GitHub repo URL).</p>';
870
+ treeHost.innerHTML = emptyBecauseFilter
871
+ ? '<p class="nav-rail__doc-hub-hint" role="status">No paths match this filter.</p>'
872
+ : '<p class="nav-rail__doc-hub-hint" role="status">No documented pairs in this export.</p>';
447
873
  return;
448
874
  }
449
875
  const root = { children: new Map(), pairs: [] };
@@ -480,33 +906,52 @@ function loadDocumentedPairs(jsonUrl, embeddedB64) {
480
906
  };
481
907
  }
482
908
  function wireDocumentedFilesTree() {
483
- const btn = document.getElementById("documented-files-toggle");
484
- const panel = document.getElementById("documented-files-panel");
909
+ const hub = document.getElementById("documented-files-hub");
485
910
  const treeHost = document.getElementById("documented-files-tree");
911
+ const filterInput = document.getElementById("documented-files-filter");
486
912
  const shell = document.getElementById("shell");
487
- if (!(btn instanceof HTMLButtonElement) || !panel || !treeHost)
913
+ if (!(hub instanceof HTMLDetailsElement) || !(treeHost instanceof HTMLElement)) {
488
914
  return;
489
- const jsonUrl = btn.getAttribute("data-nav-json-url")?.trim() ?? "";
915
+ }
916
+ const treeMount = treeHost;
917
+ const jsonUrl = hub.getAttribute("data-nav-json-url")?.trim() ?? "";
490
918
  const embeddedB64 = shell?.getAttribute("data-documented-pairs-b64")?.trim() ?? "";
491
919
  if (jsonUrl.length === 0 && embeddedB64.length === 0)
492
920
  return;
493
921
  const ensureLoaded = loadDocumentedPairs(jsonUrl, embeddedB64);
494
- btn.addEventListener("click", () => {
495
- const next = panel.hidden !== false;
496
- setDocumentedPanelOpen(btn, panel, next);
497
- if (!next)
922
+ let cachedPairs = null;
923
+ function applyFilterAndRender() {
924
+ if (cachedPairs === null)
498
925
  return;
499
- void (async () => {
500
- try {
501
- const pairs = await ensureLoaded();
502
- renderDocumentedPairsIntoHost(treeHost, pairs);
503
- }
504
- catch {
505
- treeHost.innerHTML =
506
- '<p class="documented-files-panel__hint">Could not load the file list. Check the browser network tab.</p>';
507
- }
508
- })();
926
+ const q = filterInput instanceof HTMLInputElement ? filterInput.value : "";
927
+ const pairs = filterPairsByDocumentedTreeQuery(cachedPairs, q);
928
+ const filterActive = q.trim().length > 0;
929
+ renderDocumentedPairsIntoHost(treeMount, pairs, filterActive && cachedPairs.length > 0 && pairs.length === 0);
930
+ }
931
+ async function hydrateTree() {
932
+ try {
933
+ const pairs = await ensureLoaded();
934
+ cachedPairs = pairs;
935
+ applyFilterAndRender();
936
+ }
937
+ catch {
938
+ cachedPairs = null;
939
+ treeMount.innerHTML =
940
+ '<p class="nav-rail__doc-hub-hint" role="alert">Could not load the file list.</p>';
941
+ }
942
+ }
943
+ hub.addEventListener("toggle", () => {
944
+ if (!hub.open)
945
+ return;
946
+ void hydrateTree();
509
947
  });
948
+ if (filterInput instanceof HTMLInputElement) {
949
+ filterInput.addEventListener("input", () => {
950
+ if (!hub.open || cachedPairs === null)
951
+ return;
952
+ applyFilterAndRender();
953
+ });
954
+ }
510
955
  }
511
956
  function wireSplitter(storageSplit, shell, codePane, gutter, initialPct) {
512
957
  let dragging = false;
@@ -545,7 +990,25 @@ function wireStretchLayoutChrome(codePane) {
545
990
  wireWrapToggle(STORAGE_WRAP_LINES, codePane, wrapCb);
546
991
  }
547
992
  }
548
- function wireDualPaneCodeBrowser(shell, codePane) {
993
+ function parseMultiAnglePayload(script) {
994
+ const t = script?.textContent?.trim() ?? "";
995
+ if (!t)
996
+ return null;
997
+ try {
998
+ const raw = JSON.parse(decodeBase64Utf8(t));
999
+ if (!raw || !Array.isArray(raw.angles) || raw.angles.length < 2)
1000
+ return null;
1001
+ for (const a of raw.angles) {
1002
+ if (typeof a.id !== "string" || typeof a.docInnerHtmlB64 !== "string")
1003
+ return null;
1004
+ }
1005
+ return raw;
1006
+ }
1007
+ catch {
1008
+ return null;
1009
+ }
1010
+ }
1011
+ function readDualPaneDomBundle() {
549
1012
  const docPane = document.getElementById("doc-pane");
550
1013
  const gutter = document.getElementById("gutter");
551
1014
  const wrapCb = document.getElementById("wrap-lines");
@@ -553,40 +1016,264 @@ function wireDualPaneCodeBrowser(shell, codePane) {
553
1016
  const searchClear = document.getElementById("search-clear");
554
1017
  const searchResults = document.getElementById("search-results");
555
1018
  if (!docPane || !gutter || !wrapCb || !searchInput || !searchClear || !searchResults) {
1019
+ return null;
1020
+ }
1021
+ const docBody = document.getElementById("doc-pane-body");
1022
+ const docScrollEl = docBody instanceof HTMLElement ? docBody : docPane;
1023
+ return { docBody, docScrollEl, gutter, wrapCb, searchInput, searchClear, searchResults };
1024
+ }
1025
+ function hubSearcherRowsForDualPane(args) {
1026
+ const { scope, rawCode, filePathLabel, hubNavRows, pathRowsForOrdering, rawMd, commentrayPathLabel, } = args;
1027
+ if (scope !== "commentray-and-paths") {
1028
+ return buildIndexedSearchRows(scope, rawCode, rawMd, filePathLabel, commentrayPathLabel);
1029
+ }
1030
+ if (hubNavRows.length > 0)
1031
+ return hubNavRows;
1032
+ const pathPart = pathRowsForOrdering.length > 0
1033
+ ? pathRowsForOrdering
1034
+ : buildIndexedSearchRows(scope, rawCode, rawMd, filePathLabel, commentrayPathLabel).filter((r) => r.kind === "path");
1035
+ const mdRows = rawMd.split("\n").map((text, line) => ({
1036
+ kind: "md",
1037
+ line,
1038
+ text,
1039
+ spPath: filePathLabel,
1040
+ crPath: commentrayPathLabel,
1041
+ }));
1042
+ return [...pathPart, ...mdRows];
1043
+ }
1044
+ function initialCommentrayScopePathState(shell, scope, filePathLabel, commentrayPathLabel) {
1045
+ if (scope !== "commentray-and-paths") {
1046
+ return { documentedPairs: [], pathRowsForOrdering: [], pathBlobWide: "" };
1047
+ }
1048
+ const documentedPairs = parseDocumentedPairsFromEmbeddedB64(shell.getAttribute("data-documented-pairs-b64")?.trim() ?? "");
1049
+ const pathRowsForOrdering = pathRowsFromDocumentedPairs(documentedPairs);
1050
+ const pathBlobWide = pathRowsForOrdering.length > 0
1051
+ ? pathRowsForOrdering.map((r) => r.text).join("\n")
1052
+ : [filePathLabel, commentrayPathLabel].filter((s) => s.trim().length > 0).join("\n");
1053
+ return { documentedPairs, pathRowsForOrdering, pathBlobWide };
1054
+ }
1055
+ /**
1056
+ * Fetched `commentray-nav-search.json` sometimes omits `staticBrowseUrl` on pairs; the hub embed
1057
+ * carries browse URLs from the same build — merge so search hits open `_site/browse/…`, not GitHub.
1058
+ */
1059
+ function mergeFetchedDocumentedPairsWithEmbeddedBrowse(embedded, fetched) {
1060
+ if (fetched.length === 0)
1061
+ return embedded;
1062
+ if (embedded.length === 0)
1063
+ return fetched;
1064
+ const browseByCr = new Map();
1065
+ for (const p of embedded) {
1066
+ const b = (p.staticBrowseUrl ?? "").trim();
1067
+ if (b.length === 0)
1068
+ continue;
1069
+ browseByCr.set(normPosixPath(p.commentrayPath), b);
1070
+ }
1071
+ return fetched.map((p) => {
1072
+ const have = (p.staticBrowseUrl ?? "").trim();
1073
+ if (have.length > 0)
1074
+ return p;
1075
+ const fromEmb = browseByCr.get(normPosixPath(p.commentrayPath));
1076
+ if (fromEmb !== undefined && fromEmb.length > 0) {
1077
+ return { ...p, staticBrowseUrl: fromEmb };
1078
+ }
1079
+ return p;
1080
+ });
1081
+ }
1082
+ function resolvedNavSearchJsonUrl(shell) {
1083
+ const raw = shell.getAttribute("data-nav-search-json-url")?.trim() ?? "";
1084
+ if (raw.length === 0)
1085
+ return "";
1086
+ try {
1087
+ return new URL(raw, globalThis.location.href).href;
1088
+ }
1089
+ catch {
1090
+ return raw;
1091
+ }
1092
+ }
1093
+ function wireDualPaneNavSearchFetch(shell, embeddedPairs, indexState, mutable, rebuildSearcher, searchInput) {
1094
+ const navSearchUrl = resolvedNavSearchJsonUrl(shell);
1095
+ if (navSearchUrl.length === 0)
1096
+ return;
1097
+ void (async () => {
1098
+ try {
1099
+ const res = await fetch(navSearchUrl, { credentials: "same-origin" });
1100
+ if (!res.ok)
1101
+ return;
1102
+ const doc = (await res.json());
1103
+ const fetched = pairsFromJsonArray(doc.documentedPairs);
1104
+ const mergedPairs = mergeFetchedDocumentedPairsWithEmbeddedBrowse(embeddedPairs, fetched);
1105
+ if (mergedPairs.length > 0) {
1106
+ indexState.documentedPairs = mergedPairs;
1107
+ mutable.documentedPairs = mergedPairs;
1108
+ }
1109
+ const nr = rowsFromNavSearchJson(doc);
1110
+ if (nr.length === 0)
1111
+ return;
1112
+ indexState.hubNavRows = nr;
1113
+ indexState.pathRowsForOrdering = nr.filter((r) => r.kind === "path");
1114
+ mutable.pathRowsForOrdering = indexState.pathRowsForOrdering;
1115
+ mutable.pathBlobWide = indexState.pathRowsForOrdering.map((r) => r.text).join("\n");
1116
+ rebuildSearcher();
1117
+ if (searchInput.value.trim().length > 0) {
1118
+ searchInput.dispatchEvent(new Event("input", { bubbles: true }));
1119
+ }
1120
+ }
1121
+ catch {
1122
+ /* keep embedded index */
1123
+ }
1124
+ })();
1125
+ }
1126
+ function wireDualPaneMultiAngleAndScroll(args) {
1127
+ const { codePane, docScrollEl, docBody, shell, scrollLinksRef, multiPayload, mutable, rebuildSearcher, searchInput, searchResults, } = args;
1128
+ if (multiPayload) {
1129
+ wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinksRef.current);
1130
+ const angleSel = document.getElementById("angle-select");
1131
+ if (angleSel && docBody) {
1132
+ angleSel.addEventListener("change", () => {
1133
+ const a = multiPayload.angles.find((x) => x.id === angleSel.value);
1134
+ if (!a)
1135
+ return;
1136
+ docBody.innerHTML = decodeBase64Utf8(a.docInnerHtmlB64);
1137
+ runMermaidOnFreshDocNodes(docBody);
1138
+ mutable.rawMd = decodeBase64Utf8(a.rawMdB64);
1139
+ mutable.mdLines = mutable.rawMd.split("\n");
1140
+ mutable.commentrayPathLabel = a.commentrayPathForSearch;
1141
+ rebuildSearcher();
1142
+ scrollLinksRef.current = parseScrollBlockLinksFromShell(a.scrollBlockLinksB64);
1143
+ shell.setAttribute("data-scroll-block-links-b64", a.scrollBlockLinksB64);
1144
+ shell.setAttribute("data-search-commentray-path", a.commentrayPathForSearch);
1145
+ const docPathEl = document.getElementById("nav-rail-doc-path");
1146
+ if (docPathEl) {
1147
+ const path = a.commentrayPathForSearch.trim();
1148
+ docPathEl.textContent = path.length > 0 ? path : "—";
1149
+ if (path.length > 0)
1150
+ docPathEl.setAttribute("title", path);
1151
+ else
1152
+ docPathEl.removeAttribute("title");
1153
+ }
1154
+ const gh = document.getElementById("toolbar-commentray-github");
1155
+ if (gh instanceof HTMLAnchorElement) {
1156
+ const browse = a.staticBrowseUrl?.trim() ?? "";
1157
+ if (browse.length > 0) {
1158
+ gh.href = resolveStaticBrowseHref(browse, globalThis.location.pathname, globalThis.location.origin);
1159
+ gh.removeAttribute("target");
1160
+ gh.setAttribute("rel", "noopener");
1161
+ }
1162
+ else {
1163
+ const ghu = a.commentrayOnGithubUrl?.trim();
1164
+ if (ghu) {
1165
+ gh.href = ghu;
1166
+ gh.target = "_blank";
1167
+ gh.setAttribute("rel", "noopener noreferrer");
1168
+ }
1169
+ }
1170
+ }
1171
+ searchInput.value = "";
1172
+ searchResults.innerHTML = "";
1173
+ searchResults.hidden = true;
1174
+ });
1175
+ }
556
1176
  return;
557
1177
  }
1178
+ if (scrollLinksRef.current.length > 0) {
1179
+ wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinksRef.current);
1180
+ return;
1181
+ }
1182
+ wireProportionalScrollSync(codePane, docScrollEl);
1183
+ }
1184
+ function wireDualPaneCommentrayLocationHash(docScrollEl, mdLineCount) {
1185
+ function applyCommentrayLocationHash() {
1186
+ const m = /^commentray-md-line-(\d+)$/.exec(globalThis.location.hash.slice(1));
1187
+ if (!m?.[1])
1188
+ return;
1189
+ const line0 = Number.parseInt(m[1], 10);
1190
+ if (!Number.isFinite(line0))
1191
+ return;
1192
+ scrollDocToMarkdownLine0(docScrollEl, line0, mdLineCount());
1193
+ }
1194
+ globalThis.addEventListener("hashchange", applyCommentrayLocationHash);
1195
+ globalThis.requestAnimationFrame(() => {
1196
+ globalThis.requestAnimationFrame(applyCommentrayLocationHash);
1197
+ });
1198
+ }
1199
+ function wireDualPaneCodeBrowser(shell, codePane) {
1200
+ const dom = readDualPaneDomBundle();
1201
+ if (!dom)
1202
+ return;
1203
+ const { docBody, docScrollEl, gutter, wrapCb, searchInput, searchClear, searchResults } = dom;
558
1204
  const { rawCodeB64, rawMdB64 } = readEmbeddedRawB64Strings(shell, codePane);
559
1205
  const rawCode = decodeBase64Utf8(rawCodeB64);
560
1206
  const rawMd = decodeBase64Utf8(rawMdB64);
561
1207
  const scrollLinks = parseScrollBlockLinksFromShell(shell.getAttribute("data-scroll-block-links-b64") || "");
1208
+ const scrollLinksRef = { current: scrollLinks };
562
1209
  const { scope, filePathLabel, commentrayPathLabel } = readSearchScopeFromShell(shell);
563
- const mdLines = rawMd.split("\n");
564
- const lineRows = buildIndexedSearchRows(scope, rawCode, rawMd, filePathLabel, commentrayPathLabel);
565
- const searcher = indexSearchLineRows(lineRows);
1210
+ const pathInit = initialCommentrayScopePathState(shell, scope, filePathLabel, commentrayPathLabel);
1211
+ const indexState = {
1212
+ hubNavRows: [],
1213
+ documentedPairs: pathInit.documentedPairs,
1214
+ pathRowsForOrdering: pathInit.pathRowsForOrdering,
1215
+ };
1216
+ const mutable = {
1217
+ rawMd,
1218
+ mdLines: rawMd.split("\n"),
1219
+ commentrayPathLabel,
1220
+ searcher: indexSearchLineRows([]),
1221
+ pathBlobWide: pathInit.pathBlobWide,
1222
+ pathRowsForOrdering: indexState.pathRowsForOrdering,
1223
+ documentedPairs: indexState.documentedPairs,
1224
+ };
1225
+ function rebuildSearcher() {
1226
+ mutable.searcher = indexSearchLineRows(hubSearcherRowsForDualPane({
1227
+ scope,
1228
+ rawCode,
1229
+ filePathLabel,
1230
+ hubNavRows: indexState.hubNavRows,
1231
+ pathRowsForOrdering: indexState.pathRowsForOrdering,
1232
+ rawMd: mutable.rawMd,
1233
+ commentrayPathLabel: mutable.commentrayPathLabel,
1234
+ }));
1235
+ }
1236
+ rebuildSearcher();
566
1237
  wireSearchUi({
567
1238
  scope,
568
1239
  filePathLabel,
569
- commentrayPathLabel,
1240
+ mutable,
570
1241
  rawCode,
571
- rawMd,
572
- mdLines,
573
- searcher,
574
1242
  searchInput,
575
1243
  searchClear,
576
1244
  searchResults,
577
- docPane,
1245
+ docScrollEl,
578
1246
  });
1247
+ wireDualPaneNavSearchFetch(shell, pathInit.documentedPairs, indexState, mutable, rebuildSearcher, searchInput);
579
1248
  const pct0 = parseFloat(readWebStorageItem(localStorage, STORAGE_SPLIT_PCT) || "50");
580
1249
  const pct = clamp(Number.isFinite(pct0) ? pct0 : 50, 15, 85);
581
1250
  codePane.style.flex = `0 0 ${pct}%`;
582
1251
  wireWrapToggle(STORAGE_WRAP_LINES, codePane, wrapCb);
583
1252
  wireSplitter(STORAGE_SPLIT_PCT, shell, codePane, gutter, pct);
584
- if (scrollLinks.length > 0) {
585
- wireBlockAwareScrollSync(codePane, docPane, scrollLinks);
586
- }
587
- else {
588
- wireProportionalScrollSync(codePane, docPane);
1253
+ const multiScript = document.getElementById("commentray-multi-angle-b64");
1254
+ const multiPayload = parseMultiAnglePayload(multiScript);
1255
+ wireDualPaneMultiAngleAndScroll({
1256
+ codePane,
1257
+ docScrollEl,
1258
+ docBody,
1259
+ shell,
1260
+ scrollLinksRef,
1261
+ multiPayload,
1262
+ mutable,
1263
+ rebuildSearcher,
1264
+ searchInput,
1265
+ searchResults,
1266
+ });
1267
+ if (scrollLinksRef.current.length > 0) {
1268
+ wireBlockRayConnectors({
1269
+ gutter,
1270
+ codePane,
1271
+ docScrollEl,
1272
+ getLinks: () => scrollLinksRef.current,
1273
+ probeTopSourceLine1Based: () => probeCodeLine1FromViewport(codePane),
1274
+ });
589
1275
  }
1276
+ wireDualPaneCommentrayLocationHash(docScrollEl, () => mutable.mdLines.length);
590
1277
  }
591
1278
  function main() {
592
1279
  wireDocumentedFilesTree();