@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.
- package/dist/block-stretch-layout.d.ts.map +1 -1
- package/dist/block-stretch-layout.js +11 -23
- package/dist/block-stretch-layout.js.map +1 -1
- package/dist/browse-page-slug.d.ts +6 -0
- package/dist/browse-page-slug.d.ts.map +1 -0
- package/dist/browse-page-slug.js +9 -0
- package/dist/browse-page-slug.js.map +1 -0
- package/dist/build-commentray-nav-search.d.ts +22 -6
- package/dist/build-commentray-nav-search.d.ts.map +1 -1
- package/dist/build-commentray-nav-search.js +55 -36
- package/dist/build-commentray-nav-search.js.map +1 -1
- package/dist/build-stamp.d.ts +6 -0
- package/dist/build-stamp.d.ts.map +1 -0
- package/dist/build-stamp.js +23 -0
- package/dist/build-stamp.js.map +1 -0
- package/dist/code-browser-block-rays.d.ts +48 -0
- package/dist/code-browser-block-rays.d.ts.map +1 -0
- package/dist/code-browser-block-rays.js +95 -0
- package/dist/code-browser-block-rays.js.map +1 -0
- package/dist/code-browser-client.bundle.js +12 -7
- package/dist/code-browser-client.js +787 -100
- package/dist/code-browser-client.js.map +1 -1
- package/dist/code-browser-pair-nav.d.ts +23 -0
- package/dist/code-browser-pair-nav.d.ts.map +1 -0
- package/dist/code-browser-pair-nav.js +59 -0
- package/dist/code-browser-pair-nav.js.map +1 -0
- package/dist/code-browser-search.d.ts +50 -0
- package/dist/code-browser-search.d.ts.map +1 -1
- package/dist/code-browser-search.js +117 -0
- package/dist/code-browser-search.js.map +1 -1
- package/dist/code-browser.d.ts +51 -2
- package/dist/code-browser.d.ts.map +1 -1
- package/dist/code-browser.js +863 -196
- package/dist/code-browser.js.map +1 -1
- package/dist/highlighted-code-lines.d.ts +19 -0
- package/dist/highlighted-code-lines.d.ts.map +1 -0
- package/dist/highlighted-code-lines.js +61 -0
- package/dist/highlighted-code-lines.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/markdown-pipeline.d.ts.map +1 -1
- package/dist/markdown-pipeline.js +3 -2
- package/dist/markdown-pipeline.js.map +1 -1
- package/dist/mermaid-runtime-html.d.ts.map +1 -1
- package/dist/mermaid-runtime-html.js +3 -1
- package/dist/mermaid-runtime-html.js.map +1 -1
- 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
|
|
8
|
-
|
|
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"
|
|
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 =
|
|
83
|
-
|
|
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" &&
|
|
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
|
|
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
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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) =>
|
|
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,
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
`<
|
|
430
|
-
|
|
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
|
|
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="
|
|
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
|
|
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 (!(
|
|
913
|
+
if (!(hub instanceof HTMLDetailsElement) || !(treeHost instanceof HTMLElement)) {
|
|
488
914
|
return;
|
|
489
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (!next)
|
|
922
|
+
let cachedPairs = null;
|
|
923
|
+
function applyFilterAndRender() {
|
|
924
|
+
if (cachedPairs === null)
|
|
498
925
|
return;
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
|
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
|
|
564
|
-
const
|
|
565
|
-
|
|
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
|
-
|
|
1240
|
+
mutable,
|
|
570
1241
|
rawCode,
|
|
571
|
-
rawMd,
|
|
572
|
-
mdLines,
|
|
573
|
-
searcher,
|
|
574
1242
|
searchInput,
|
|
575
1243
|
searchClear,
|
|
576
1244
|
searchResults,
|
|
577
|
-
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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();
|