@commentray/render 0.0.4 → 0.0.5

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.
@@ -2,7 +2,7 @@ import { FuzzySearcher, PrefixSearcher, Query, SearcherFactory, SubstringSearche
2
2
  import { mirroredScrollTop, pickCommentrayLineForSourceScroll, pickSourceLine0ForCommentrayScroll, } from "./code-browser-scroll-sync.js";
3
3
  import { decodeBase64Utf8 } from "./code-browser-encoding.js";
4
4
  import { readEmbeddedRawB64Strings } from "./code-browser-embedded-payload.js";
5
- import { findOrderedTokenSpans, lineAtIndex, offsetToLineIndex } from "./code-browser-search.js";
5
+ import { escapeHtmlHighlightingSearchTokens, findOrderedTokenSpans, lineAtIndex, offsetToLineIndex, } from "./code-browser-search.js";
6
6
  import { readWebStorageItem, writeWebStorageItem } from "./code-browser-web-storage.js";
7
7
  function tokenizeQuery(q) {
8
8
  return q.trim().split(/\s+/).filter(Boolean);
@@ -27,7 +27,9 @@ function snippet(s, maxLen) {
27
27
  function mergeHits(rows, max) {
28
28
  const byKey = new Map();
29
29
  for (const r of rows) {
30
- const key = r.kind === "path" ? `path:${String(r.line)}:${r.text.slice(0, 64)}` : `${r.kind}:${r.line}`;
30
+ const key = r.kind === "path"
31
+ ? `path:${r.spPath ?? ""}|${r.crPath ?? ""}|${r.text.slice(0, 120)}`
32
+ : `${r.kind}:${r.line}:${r.crPath ?? ""}`;
31
33
  const prev = byKey.get(key);
32
34
  if (!prev || r.score > prev.score) {
33
35
  byKey.set(key, r);
@@ -73,43 +75,118 @@ function buildFuzzyHits(searcher, query, topN) {
73
75
  text: row.text,
74
76
  score: 100 + m.quality,
75
77
  source: "fuzzy",
78
+ crPath: row.crPath,
79
+ spPath: row.spPath,
80
+ });
81
+ }
82
+ return out;
83
+ }
84
+ /** Ordered token matches per path row (keeps `spPath` / `crPath` for navigation). */
85
+ function buildOrderedPathHitsFromRows(pathRows, tokens) {
86
+ if (tokens.length === 0)
87
+ return [];
88
+ const out = [];
89
+ for (const row of pathRows) {
90
+ if (row.kind !== "path")
91
+ continue;
92
+ const spans = findOrderedTokenSpans(row.text, tokens);
93
+ if (spans.length === 0)
94
+ continue;
95
+ out.push({
96
+ kind: "path",
97
+ line: row.line,
98
+ text: row.text,
99
+ score: 1000,
100
+ source: "ordered",
101
+ spPath: row.spPath,
102
+ crPath: row.crPath,
76
103
  });
77
104
  }
78
105
  return out;
79
106
  }
80
107
  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");
108
+ const { scope, filePathLabel, commentrayPathLabel, rawCode, rawMd, searcher, queryRaw, tokens, pathBlobWide, pathRowsForOrdering, } = input;
109
+ const pathBlob = (pathBlobWide && pathBlobWide.trim().length > 0
110
+ ? pathBlobWide.trim()
111
+ : [filePathLabel, commentrayPathLabel].filter((s) => s.trim().length > 0).join("\n")) || "";
85
112
  const orderedCode = scope === "commentray-and-paths" ? [] : buildOrderedHits(rawCode, "code", tokens);
86
- const orderedPath = scope === "commentray-and-paths" && pathBlob ? buildOrderedHits(pathBlob, "path", tokens) : [];
113
+ const orderedPath = scope === "commentray-and-paths" && pathRowsForOrdering && pathRowsForOrdering.length > 0
114
+ ? buildOrderedPathHitsFromRows(pathRowsForOrdering, tokens)
115
+ : scope === "commentray-and-paths" && pathBlob
116
+ ? buildOrderedHits(pathBlob, "path", tokens)
117
+ : [];
87
118
  const orderedMd = buildOrderedHits(rawMd, "md", tokens);
88
119
  const fuzzyHits = buildFuzzyHits(searcher, queryRaw, 60);
89
120
  return mergeHits([...orderedCode, ...orderedPath, ...orderedMd, ...fuzzyHits], 80);
90
121
  }
91
- function searchResultsInnerHtml(scope, combined) {
122
+ function searchScopeResultsHintIntro(scope) {
123
+ return scope === "commentray-and-paths"
124
+ ? "Paths + indexed commentray (this page + browse pages when built). Ordered tokens + fuzzy lines."
125
+ : "Whole source: whitespace tokens in order (may span lines). Per-line fuzzy ranking for typos.";
126
+ }
127
+ function searchHitMetaLabel(h, ctx) {
128
+ if (h.kind === "code")
129
+ return `Code L${h.line + 1}`;
130
+ if (h.kind === "path")
131
+ return `Path`;
132
+ const foreign = h.crPath && h.crPath !== ctx.currentCommentrayPath ? ` · ${h.crPath}` : "";
133
+ return `Commentray L${h.line + 1}${foreign}`;
134
+ }
135
+ function searchHitButtonHtml(h, tokens, ctx) {
136
+ const label = searchHitMetaLabel(h, ctx);
137
+ const tag = h.source === "ordered" ? "ordered" : "fuzzy";
138
+ const snippetHtml = escapeHtmlHighlightingSearchTokens(snippet(h.text, 320), tokens);
139
+ const crAttr = escapeHtmlText(h.kind === "md" ? (h.crPath ?? ctx.currentCommentrayPath) : (h.crPath ?? ""));
140
+ const spAttr = escapeHtmlText(h.kind === "md" ? (h.spPath ?? ctx.currentSourcePath) : (h.spPath ?? ""));
141
+ return (`<button type="button" class="hit" data-kind="${h.kind}" data-line="${String(h.line)}" data-cr-path="${crAttr}" data-sp-path="${spAttr}">` +
142
+ `<span class="meta">${escapeHtmlText(label)} <span class="src-tag">(${tag})</span></span>` +
143
+ `<div class="snippet">${snippetHtml}</div></button>`);
144
+ }
145
+ function searchResultsInnerHtml(scope, combined, tokens, ctx) {
92
146
  if (combined.length === 0) {
93
147
  return '<div class="hint">No matches. Try fewer tokens or looser spelling (fuzzy matches per line).</div>';
94
148
  }
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.";
149
+ const hintIntro = searchScopeResultsHintIntro(scope);
98
150
  const buf = [];
99
151
  buf.push(`<div class="hint">${hintIntro} ${combined.length} hit(s).</div>`);
100
152
  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>`);
153
+ buf.push(searchHitButtonHtml(h, tokens, ctx));
110
154
  }
111
155
  return buf.join("");
112
156
  }
157
+ function scrollDocToMarkdownLine0(docScrollEl, line0, mdLineCount) {
158
+ const el = docScrollEl.querySelector(`#commentray-md-line-${String(line0)}`);
159
+ if (el instanceof HTMLElement) {
160
+ const top = el.getBoundingClientRect().top -
161
+ docScrollEl.getBoundingClientRect().top +
162
+ docScrollEl.scrollTop;
163
+ docScrollEl.scrollTo({ top: Math.max(0, top - 8), behavior: "smooth" });
164
+ return;
165
+ }
166
+ if (mdLineCount <= 1)
167
+ return;
168
+ const ratio = line0 / Math.max(1, mdLineCount - 1);
169
+ const maxScroll = docScrollEl.scrollHeight - docScrollEl.clientHeight;
170
+ docScrollEl.scrollTo({ top: ratio * Math.max(0, maxScroll), behavior: "smooth" });
171
+ }
172
+ function openForeignPairBrowseOrGithub(pairs, commentrayPath, mdLine0) {
173
+ const p = pairs.find((x) => x.commentrayPath === commentrayPath);
174
+ if (!p)
175
+ return;
176
+ if (p.staticBrowseUrl?.trim()) {
177
+ const u = new URL(p.staticBrowseUrl.trim(), globalThis.location.href);
178
+ if (mdLine0 !== null && mdLine0 >= 0)
179
+ u.hash = `commentray-md-line-${String(mdLine0)}`;
180
+ globalThis.open(u.toString(), "_blank", "noopener,noreferrer");
181
+ return;
182
+ }
183
+ if (p.commentrayOnGithub) {
184
+ const url = mdLine0 !== null && mdLine0 >= 0
185
+ ? `${p.commentrayOnGithub}#L${String(mdLine0 + 1)}`
186
+ : p.commentrayOnGithub;
187
+ globalThis.open(url, "_blank", "noopener,noreferrer");
188
+ }
189
+ }
113
190
  function readSearchScopeFromShell(shell) {
114
191
  const scopeAttr = shell.getAttribute("data-search-scope") || "";
115
192
  return {
@@ -140,11 +217,64 @@ function buildIndexedSearchRows(scope, rawCode, rawMd, filePathLabel, commentray
140
217
  }
141
218
  function indexSearchLineRows(rows) {
142
219
  const searcher = SearcherFactory.createDefaultSearcher();
143
- searcher.indexEntities(rows, (e) => `${e.kind}:${e.kind === "path" ? e.text : e.line}`, (e) => [e.text]);
220
+ searcher.indexEntities(rows, (e) => {
221
+ if (e.kind === "md" && e.crPath)
222
+ return `md:${e.crPath}:${e.line}`;
223
+ if (e.kind === "path")
224
+ return `path:${e.spPath ?? ""}|${e.crPath ?? ""}|${e.line}|${e.text.slice(0, 120)}`;
225
+ return `${e.kind}:${e.line}`;
226
+ }, (e) => [e.text]);
144
227
  return searcher;
145
228
  }
229
+ function findSearchHitButton(leaf, searchResults) {
230
+ let t = leaf;
231
+ while (t) {
232
+ if (t.classList?.contains("hit"))
233
+ return t;
234
+ if (t === searchResults)
235
+ return null;
236
+ t = t.parentElement;
237
+ }
238
+ return null;
239
+ }
240
+ function scrollCodeHitToView(line) {
241
+ const el = document.getElementById(`code-line-${String(line)}`);
242
+ if (el)
243
+ el.scrollIntoView({ block: "nearest", behavior: "smooth" });
244
+ }
245
+ function handlePathSearchHit(button, deps) {
246
+ const cr = (button.getAttribute("data-cr-path")?.trim() ?? "").trim();
247
+ const curCr = deps.mutable.commentrayPathLabel.trim();
248
+ const isCurrentPair = cr.length === 0 || cr === curCr;
249
+ if (isCurrentPair) {
250
+ deps.docScrollEl.scrollTo({ top: 0, behavior: "smooth" });
251
+ return;
252
+ }
253
+ openForeignPairBrowseOrGithub(deps.mutable.documentedPairs, cr, null);
254
+ }
255
+ function handleMdSearchHit(line, crHit, deps) {
256
+ if (crHit.length > 0 && crHit !== deps.mutable.commentrayPathLabel) {
257
+ openForeignPairBrowseOrGithub(deps.mutable.documentedPairs, crHit, line);
258
+ return;
259
+ }
260
+ scrollDocToMarkdownLine0(deps.docScrollEl, line, deps.mutable.mdLines.length);
261
+ }
262
+ function handleSearchHitButtonClick(button, deps) {
263
+ const kind = button.getAttribute("data-kind");
264
+ const line = parseInt(button.getAttribute("data-line") || "0", 10);
265
+ const crHit = button.getAttribute("data-cr-path")?.trim() ?? "";
266
+ if (kind === "code") {
267
+ scrollCodeHitToView(line);
268
+ return;
269
+ }
270
+ if (kind === "path") {
271
+ handlePathSearchHit(button, deps);
272
+ return;
273
+ }
274
+ handleMdSearchHit(line, crHit, deps);
275
+ }
146
276
  function wireSearchUi(ctx) {
147
- const { scope, filePathLabel, commentrayPathLabel, rawCode, rawMd, mdLines, searcher, searchInput, searchClear, searchResults, docPane, } = ctx;
277
+ const { scope, filePathLabel, mutable, rawCode, searchInput, searchClear, searchResults, docScrollEl, } = ctx;
148
278
  let debounceTimer;
149
279
  function clearSearch() {
150
280
  clearTimeout(debounceTimer);
@@ -163,41 +293,27 @@ function wireSearchUi(ctx) {
163
293
  const combined = computeMergedSearchHits({
164
294
  scope,
165
295
  filePathLabel,
166
- commentrayPathLabel,
296
+ commentrayPathLabel: mutable.commentrayPathLabel,
167
297
  rawCode,
168
- rawMd,
169
- searcher,
298
+ rawMd: mutable.rawMd,
299
+ searcher: mutable.searcher,
170
300
  queryRaw: searchInput.value,
171
301
  tokens,
302
+ pathBlobWide: mutable.pathBlobWide,
303
+ pathRowsForOrdering: mutable.pathRowsForOrdering.length > 0 ? mutable.pathRowsForOrdering : undefined,
172
304
  });
173
305
  searchResults.hidden = false;
174
- searchResults.innerHTML = searchResultsInnerHtml(scope, combined);
306
+ searchResults.innerHTML = searchResultsInnerHtml(scope, combined, tokens, {
307
+ currentCommentrayPath: mutable.commentrayPathLabel,
308
+ currentSourcePath: filePathLabel,
309
+ });
175
310
  }
311
+ const hitClickDeps = { mutable, docScrollEl };
176
312
  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"))
313
+ const hit = findSearchHitButton(ev.target, searchResults);
314
+ if (!hit)
182
315
  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
- }
316
+ handleSearchHitButtonClick(hit, hitClickDeps);
201
317
  });
202
318
  searchInput.addEventListener("input", () => {
203
319
  clearTimeout(debounceTimer);
@@ -312,8 +428,9 @@ function wireBidirectionalScroll(codePane, docPane, syncFromCode, syncFromDoc) {
312
428
  }, { passive: true });
313
429
  }
314
430
  /** Index-backed scroll sync when `data-scroll-block-links-b64` is present; else see proportional fallback. */
315
- function wireBlockAwareScrollSync(codePane, docPane, links) {
431
+ function wireBlockAwareScrollSync(codePane, docPane, getLinks) {
316
432
  wireBidirectionalScroll(codePane, docPane, () => {
433
+ const links = getLinks();
317
434
  const line1 = probeCodeLine1FromViewport(codePane);
318
435
  const mdLine0 = pickCommentrayLineForSourceScroll(links, line1);
319
436
  if (mdLine0 === null) {
@@ -332,6 +449,7 @@ function wireBlockAwareScrollSync(codePane, docPane, links) {
332
449
  }
333
450
  }
334
451
  }, () => {
452
+ const links = getLinks();
335
453
  const mdLine0 = probeCommentrayLine0FromDoc(docPane);
336
454
  const src0 = pickSourceLine0ForCommentrayScroll(links, mdLine0);
337
455
  if (src0 === null) {
@@ -366,7 +484,8 @@ function isDocumentedPairNav(x) {
366
484
  return (typeof o.sourcePath === "string" &&
367
485
  typeof o.commentrayPath === "string" &&
368
486
  typeof o.sourceOnGithub === "string" &&
369
- typeof o.commentrayOnGithub === "string");
487
+ typeof o.commentrayOnGithub === "string" &&
488
+ (o.staticBrowseUrl === undefined || typeof o.staticBrowseUrl === "string"));
370
489
  }
371
490
  function pairsFromJsonArray(raw) {
372
491
  const pairs = [];
@@ -378,6 +497,71 @@ function pairsFromJsonArray(raw) {
378
497
  }
379
498
  return pairs;
380
499
  }
500
+ function pathRowsFromDocumentedPairs(pairs) {
501
+ const seen = new Set();
502
+ const out = [];
503
+ let line = 0;
504
+ for (const p of pairs) {
505
+ for (const seg of [p.sourcePath, p.commentrayPath]) {
506
+ const t = seg.trim();
507
+ if (!t || seen.has(t))
508
+ continue;
509
+ seen.add(t);
510
+ out.push({
511
+ kind: "path",
512
+ line: line++,
513
+ text: t,
514
+ spPath: p.sourcePath,
515
+ crPath: p.commentrayPath,
516
+ });
517
+ }
518
+ }
519
+ return out;
520
+ }
521
+ function commentrayLineRowFromNavJson(r) {
522
+ if (r.kind !== "commentrayLine")
523
+ return null;
524
+ if (typeof r.line !== "number" || typeof r.text !== "string")
525
+ return null;
526
+ const sp = typeof r.sourcePath === "string" ? r.sourcePath : "";
527
+ const cr = typeof r.commentrayPath === "string" ? r.commentrayPath : "";
528
+ return { kind: "md", line: r.line, text: r.text, spPath: sp, crPath: cr };
529
+ }
530
+ function pathRowFromNavJson(r, pathLine) {
531
+ if (r.kind !== "sourcePath" && r.kind !== "commentrayPath")
532
+ return null;
533
+ const sp = typeof r.sourcePath === "string" ? r.sourcePath : "";
534
+ const cr = typeof r.commentrayPath === "string" ? r.commentrayPath : "";
535
+ const text = r.kind === "sourcePath" ? sp : cr;
536
+ if (!text)
537
+ return null;
538
+ return { kind: "path", line: pathLine, text, spPath: sp, crPath: cr };
539
+ }
540
+ function rowsFromNavSearchJson(doc) {
541
+ if (!doc || typeof doc !== "object")
542
+ return [];
543
+ const rowsRaw = doc.rows;
544
+ if (!Array.isArray(rowsRaw))
545
+ return [];
546
+ const out = [];
547
+ let pathLine = 0;
548
+ for (const raw of rowsRaw) {
549
+ if (!raw || typeof raw !== "object")
550
+ continue;
551
+ const r = raw;
552
+ const mdRow = commentrayLineRowFromNavJson(r);
553
+ if (mdRow) {
554
+ out.push(mdRow);
555
+ continue;
556
+ }
557
+ const pathRow = pathRowFromNavJson(r, pathLine);
558
+ if (pathRow) {
559
+ out.push(pathRow);
560
+ pathLine += 1;
561
+ }
562
+ }
563
+ return out;
564
+ }
381
565
  /** Offline-first: UTF-8 base64 JSON array produced by the static Pages build. */
382
566
  function parseDocumentedPairsFromEmbeddedB64(b64) {
383
567
  const t = b64.trim();
@@ -410,6 +594,24 @@ function insertSourcePathTrie(root, pair) {
410
594
  n = next;
411
595
  }
412
596
  }
597
+ function pathBasenamePosixStyle(p) {
598
+ const t = p.replace(/\\/g, "/").replace(/\/+$/, "");
599
+ const i = t.lastIndexOf("/");
600
+ return i >= 0 ? t.slice(i + 1) : t;
601
+ }
602
+ /** Companion Markdown filename stem (e.g. `main` from `.../README.md/main.md`). */
603
+ function companionDocStem(commentrayPath) {
604
+ const norm = commentrayPath.replace(/\\/g, "/").replace(/\/+$/, "");
605
+ const lastSeg = norm.split("/").filter(Boolean).at(-1) ?? "";
606
+ return lastSeg.replace(/\.md$/i, "");
607
+ }
608
+ function treeFileLinkLabel(pr, disambiguate) {
609
+ const base = pathBasenamePosixStyle(pr.sourcePath);
610
+ if (!disambiguate)
611
+ return base;
612
+ const stem = companionDocStem(pr.commentrayPath);
613
+ return stem !== "" && stem !== base ? `${base} · ${stem}` : base;
614
+ }
413
615
  function renderDocumentedTreeHtml(node) {
414
616
  const keys = [...node.children.keys()].sort((a, b) => a.localeCompare(b));
415
617
  if (keys.length === 0)
@@ -424,26 +626,22 @@ function renderDocumentedTreeHtml(node) {
424
626
  lis.push(`<li><div class="tree-dir">${escapeHtmlText(name)}</div>${inner}</li>`);
425
627
  }
426
628
  if (ch.pairs.length > 0) {
629
+ const multi = ch.pairs.length > 1;
427
630
  for (const pr of ch.pairs) {
631
+ const label = escapeHtmlText(treeFileLinkLabel(pr, multi));
632
+ const title = escapeHtmlText(`${pr.sourcePath} — open companion on GitHub`);
428
633
  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>`);
634
+ `<a class="tree-file-link" href="${escapeHtmlText(pr.commentrayOnGithub)}" target="_blank" rel="noopener noreferrer" title="${title}">${label}</a>` +
635
+ `</div></li>`);
434
636
  }
435
637
  }
436
638
  }
437
639
  return `<ul>${lis.join("")}</ul>`;
438
640
  }
439
- function setDocumentedPanelOpen(btn, panel, open) {
440
- panel.hidden = !open;
441
- btn.setAttribute("aria-expanded", open ? "true" : "false");
442
- }
443
641
  function renderDocumentedPairsIntoHost(treeHost, pairs) {
444
642
  if (pairs.length === 0) {
445
643
  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>';
644
+ '<p class="nav-rail__doc-hub-hint" role="status">No documented pairs in this export.</p>';
447
645
  return;
448
646
  }
449
647
  const root = { children: new Map(), pairs: [] };
@@ -480,32 +678,32 @@ function loadDocumentedPairs(jsonUrl, embeddedB64) {
480
678
  };
481
679
  }
482
680
  function wireDocumentedFilesTree() {
483
- const btn = document.getElementById("documented-files-toggle");
484
- const panel = document.getElementById("documented-files-panel");
681
+ const hub = document.getElementById("documented-files-hub");
485
682
  const treeHost = document.getElementById("documented-files-tree");
486
683
  const shell = document.getElementById("shell");
487
- if (!(btn instanceof HTMLButtonElement) || !panel || !treeHost)
684
+ if (!(hub instanceof HTMLDetailsElement) || !(treeHost instanceof HTMLElement)) {
488
685
  return;
489
- const jsonUrl = btn.getAttribute("data-nav-json-url")?.trim() ?? "";
686
+ }
687
+ const treeMount = treeHost;
688
+ const jsonUrl = hub.getAttribute("data-nav-json-url")?.trim() ?? "";
490
689
  const embeddedB64 = shell?.getAttribute("data-documented-pairs-b64")?.trim() ?? "";
491
690
  if (jsonUrl.length === 0 && embeddedB64.length === 0)
492
691
  return;
493
692
  const ensureLoaded = loadDocumentedPairs(jsonUrl, embeddedB64);
494
- btn.addEventListener("click", () => {
495
- const next = panel.hidden !== false;
496
- setDocumentedPanelOpen(btn, panel, next);
497
- if (!next)
693
+ async function hydrateTree() {
694
+ try {
695
+ const pairs = await ensureLoaded();
696
+ renderDocumentedPairsIntoHost(treeMount, pairs);
697
+ }
698
+ catch {
699
+ treeMount.innerHTML =
700
+ '<p class="nav-rail__doc-hub-hint" role="alert">Could not load the file list.</p>';
701
+ }
702
+ }
703
+ hub.addEventListener("toggle", () => {
704
+ if (!hub.open)
498
705
  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
- })();
706
+ void hydrateTree();
509
707
  });
510
708
  }
511
709
  function wireSplitter(storageSplit, shell, codePane, gutter, initialPct) {
@@ -545,7 +743,25 @@ function wireStretchLayoutChrome(codePane) {
545
743
  wireWrapToggle(STORAGE_WRAP_LINES, codePane, wrapCb);
546
744
  }
547
745
  }
548
- function wireDualPaneCodeBrowser(shell, codePane) {
746
+ function parseMultiAnglePayload(script) {
747
+ const t = script?.textContent?.trim() ?? "";
748
+ if (!t)
749
+ return null;
750
+ try {
751
+ const raw = JSON.parse(decodeBase64Utf8(t));
752
+ if (!raw || !Array.isArray(raw.angles) || raw.angles.length < 2)
753
+ return null;
754
+ for (const a of raw.angles) {
755
+ if (typeof a.id !== "string" || typeof a.docInnerHtmlB64 !== "string")
756
+ return null;
757
+ }
758
+ return raw;
759
+ }
760
+ catch {
761
+ return null;
762
+ }
763
+ }
764
+ function readDualPaneDomBundle() {
549
765
  const docPane = document.getElementById("doc-pane");
550
766
  const gutter = document.getElementById("gutter");
551
767
  const wrapCb = document.getElementById("wrap-lines");
@@ -553,40 +769,193 @@ function wireDualPaneCodeBrowser(shell, codePane) {
553
769
  const searchClear = document.getElementById("search-clear");
554
770
  const searchResults = document.getElementById("search-results");
555
771
  if (!docPane || !gutter || !wrapCb || !searchInput || !searchClear || !searchResults) {
772
+ return null;
773
+ }
774
+ const docBody = document.getElementById("doc-pane-body");
775
+ const docScrollEl = docBody instanceof HTMLElement ? docBody : docPane;
776
+ return { docBody, docScrollEl, gutter, wrapCb, searchInput, searchClear, searchResults };
777
+ }
778
+ function hubSearcherRowsForDualPane(args) {
779
+ const { scope, rawCode, filePathLabel, hubNavRows, pathRowsForOrdering, rawMd, commentrayPathLabel, } = args;
780
+ if (scope !== "commentray-and-paths") {
781
+ return buildIndexedSearchRows(scope, rawCode, rawMd, filePathLabel, commentrayPathLabel);
782
+ }
783
+ if (hubNavRows.length > 0)
784
+ return hubNavRows;
785
+ const pathPart = pathRowsForOrdering.length > 0
786
+ ? pathRowsForOrdering
787
+ : buildIndexedSearchRows(scope, rawCode, rawMd, filePathLabel, commentrayPathLabel).filter((r) => r.kind === "path");
788
+ const mdRows = rawMd.split("\n").map((text, line) => ({
789
+ kind: "md",
790
+ line,
791
+ text,
792
+ spPath: filePathLabel,
793
+ crPath: commentrayPathLabel,
794
+ }));
795
+ return [...pathPart, ...mdRows];
796
+ }
797
+ function initialCommentrayScopePathState(shell, scope, filePathLabel, commentrayPathLabel) {
798
+ if (scope !== "commentray-and-paths") {
799
+ return { documentedPairs: [], pathRowsForOrdering: [], pathBlobWide: "" };
800
+ }
801
+ const documentedPairs = parseDocumentedPairsFromEmbeddedB64(shell.getAttribute("data-documented-pairs-b64")?.trim() ?? "");
802
+ const pathRowsForOrdering = pathRowsFromDocumentedPairs(documentedPairs);
803
+ const pathBlobWide = pathRowsForOrdering.length > 0
804
+ ? pathRowsForOrdering.map((r) => r.text).join("\n")
805
+ : [filePathLabel, commentrayPathLabel].filter((s) => s.trim().length > 0).join("\n");
806
+ return { documentedPairs, pathRowsForOrdering, pathBlobWide };
807
+ }
808
+ function wireDualPaneNavSearchFetch(navSearchUrl, indexState, mutable, rebuildSearcher, searchInput) {
809
+ if (navSearchUrl.length === 0)
810
+ return;
811
+ void (async () => {
812
+ try {
813
+ const res = await fetch(navSearchUrl, { credentials: "same-origin" });
814
+ if (!res.ok)
815
+ return;
816
+ const doc = (await res.json());
817
+ const fetched = pairsFromJsonArray(doc.documentedPairs);
818
+ if (fetched.length > 0) {
819
+ indexState.documentedPairs = fetched;
820
+ mutable.documentedPairs = fetched;
821
+ }
822
+ const nr = rowsFromNavSearchJson(doc);
823
+ if (nr.length === 0)
824
+ return;
825
+ indexState.hubNavRows = nr;
826
+ indexState.pathRowsForOrdering = nr.filter((r) => r.kind === "path");
827
+ mutable.pathRowsForOrdering = indexState.pathRowsForOrdering;
828
+ mutable.pathBlobWide = indexState.pathRowsForOrdering.map((r) => r.text).join("\n");
829
+ rebuildSearcher();
830
+ if (searchInput.value.trim().length > 0) {
831
+ searchInput.dispatchEvent(new Event("input", { bubbles: true }));
832
+ }
833
+ }
834
+ catch {
835
+ /* keep embedded index */
836
+ }
837
+ })();
838
+ }
839
+ function wireDualPaneMultiAngleAndScroll(args) {
840
+ const { codePane, docScrollEl, docBody, shell, scrollLinks, multiPayload, mutable, rebuildSearcher, searchInput, searchResults, } = args;
841
+ const activeLinks = { current: scrollLinks };
842
+ if (multiPayload) {
843
+ wireBlockAwareScrollSync(codePane, docScrollEl, () => activeLinks.current);
844
+ const angleSel = document.getElementById("angle-select");
845
+ if (angleSel && docBody) {
846
+ angleSel.addEventListener("change", () => {
847
+ const a = multiPayload.angles.find((x) => x.id === angleSel.value);
848
+ if (!a)
849
+ return;
850
+ docBody.innerHTML = decodeBase64Utf8(a.docInnerHtmlB64);
851
+ mutable.rawMd = decodeBase64Utf8(a.rawMdB64);
852
+ mutable.mdLines = mutable.rawMd.split("\n");
853
+ mutable.commentrayPathLabel = a.commentrayPathForSearch;
854
+ rebuildSearcher();
855
+ activeLinks.current = parseScrollBlockLinksFromShell(a.scrollBlockLinksB64);
856
+ shell.setAttribute("data-scroll-block-links-b64", a.scrollBlockLinksB64);
857
+ shell.setAttribute("data-search-commentray-path", a.commentrayPathForSearch);
858
+ const gh = document.getElementById("toolbar-commentray-github");
859
+ if (gh instanceof HTMLAnchorElement && a.commentrayOnGithubUrl?.trim()) {
860
+ gh.href = a.commentrayOnGithubUrl.trim();
861
+ }
862
+ searchInput.value = "";
863
+ searchResults.innerHTML = "";
864
+ searchResults.hidden = true;
865
+ });
866
+ }
867
+ return;
868
+ }
869
+ if (scrollLinks.length > 0) {
870
+ wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinks);
556
871
  return;
557
872
  }
873
+ wireProportionalScrollSync(codePane, docScrollEl);
874
+ }
875
+ function wireDualPaneCommentrayLocationHash(docScrollEl, mdLineCount) {
876
+ function applyCommentrayLocationHash() {
877
+ const m = /^commentray-md-line-(\d+)$/.exec(globalThis.location.hash.slice(1));
878
+ if (!m?.[1])
879
+ return;
880
+ const line0 = Number.parseInt(m[1], 10);
881
+ if (!Number.isFinite(line0))
882
+ return;
883
+ scrollDocToMarkdownLine0(docScrollEl, line0, mdLineCount());
884
+ }
885
+ globalThis.addEventListener("hashchange", applyCommentrayLocationHash);
886
+ globalThis.requestAnimationFrame(() => {
887
+ globalThis.requestAnimationFrame(applyCommentrayLocationHash);
888
+ });
889
+ }
890
+ function wireDualPaneCodeBrowser(shell, codePane) {
891
+ const dom = readDualPaneDomBundle();
892
+ if (!dom)
893
+ return;
894
+ const { docBody, docScrollEl, gutter, wrapCb, searchInput, searchClear, searchResults } = dom;
558
895
  const { rawCodeB64, rawMdB64 } = readEmbeddedRawB64Strings(shell, codePane);
559
896
  const rawCode = decodeBase64Utf8(rawCodeB64);
560
897
  const rawMd = decodeBase64Utf8(rawMdB64);
561
898
  const scrollLinks = parseScrollBlockLinksFromShell(shell.getAttribute("data-scroll-block-links-b64") || "");
562
899
  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);
900
+ const pathInit = initialCommentrayScopePathState(shell, scope, filePathLabel, commentrayPathLabel);
901
+ const indexState = {
902
+ hubNavRows: [],
903
+ documentedPairs: pathInit.documentedPairs,
904
+ pathRowsForOrdering: pathInit.pathRowsForOrdering,
905
+ };
906
+ const mutable = {
907
+ rawMd,
908
+ mdLines: rawMd.split("\n"),
909
+ commentrayPathLabel,
910
+ searcher: indexSearchLineRows([]),
911
+ pathBlobWide: pathInit.pathBlobWide,
912
+ pathRowsForOrdering: indexState.pathRowsForOrdering,
913
+ documentedPairs: indexState.documentedPairs,
914
+ };
915
+ function rebuildSearcher() {
916
+ mutable.searcher = indexSearchLineRows(hubSearcherRowsForDualPane({
917
+ scope,
918
+ rawCode,
919
+ filePathLabel,
920
+ hubNavRows: indexState.hubNavRows,
921
+ pathRowsForOrdering: indexState.pathRowsForOrdering,
922
+ rawMd: mutable.rawMd,
923
+ commentrayPathLabel: mutable.commentrayPathLabel,
924
+ }));
925
+ }
926
+ rebuildSearcher();
566
927
  wireSearchUi({
567
928
  scope,
568
929
  filePathLabel,
569
- commentrayPathLabel,
930
+ mutable,
570
931
  rawCode,
571
- rawMd,
572
- mdLines,
573
- searcher,
574
932
  searchInput,
575
933
  searchClear,
576
934
  searchResults,
577
- docPane,
935
+ docScrollEl,
578
936
  });
937
+ const navSearchUrl = shell.getAttribute("data-nav-search-json-url")?.trim() ?? "";
938
+ wireDualPaneNavSearchFetch(navSearchUrl, indexState, mutable, rebuildSearcher, searchInput);
579
939
  const pct0 = parseFloat(readWebStorageItem(localStorage, STORAGE_SPLIT_PCT) || "50");
580
940
  const pct = clamp(Number.isFinite(pct0) ? pct0 : 50, 15, 85);
581
941
  codePane.style.flex = `0 0 ${pct}%`;
582
942
  wireWrapToggle(STORAGE_WRAP_LINES, codePane, wrapCb);
583
943
  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);
589
- }
944
+ const multiScript = document.getElementById("commentray-multi-angle-b64");
945
+ const multiPayload = parseMultiAnglePayload(multiScript);
946
+ wireDualPaneMultiAngleAndScroll({
947
+ codePane,
948
+ docScrollEl,
949
+ docBody,
950
+ shell,
951
+ scrollLinks,
952
+ multiPayload,
953
+ mutable,
954
+ rebuildSearcher,
955
+ searchInput,
956
+ searchResults,
957
+ });
958
+ wireDualPaneCommentrayLocationHash(docScrollEl, () => mutable.mdLines.length);
590
959
  }
591
960
  function main() {
592
961
  wireDocumentedFilesTree();