@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.
- 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/build-commentray-nav-search.d.ts +11 -1
- package/dist/build-commentray-nav-search.d.ts.map +1 -1
- package/dist/build-commentray-nav-search.js +45 -26
- 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-client.bundle.js +11 -6
- package/dist/code-browser-client.js +463 -94
- package/dist/code-browser-client.js.map +1 -1
- package/dist/code-browser-search.d.ts +5 -0
- package/dist/code-browser-search.d.ts.map +1 -1
- package/dist/code-browser-search.js +28 -0
- package/dist/code-browser-search.js.map +1 -1
- package/dist/code-browser.d.ts +30 -2
- package/dist/code-browser.d.ts.map +1 -1
- package/dist/code-browser.js +599 -171
- 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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/markdown-pipeline.js +1 -1
- package/dist/markdown-pipeline.js.map +1 -1
- package/package.json +2 -2
|
@@ -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"
|
|
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 =
|
|
83
|
-
|
|
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" &&
|
|
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
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
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,
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
`<
|
|
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>`);
|
|
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="
|
|
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
|
|
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 (!(
|
|
684
|
+
if (!(hub instanceof HTMLDetailsElement) || !(treeHost instanceof HTMLElement)) {
|
|
488
685
|
return;
|
|
489
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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 (
|
|
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
|
|
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
|
|
564
|
-
const
|
|
565
|
-
|
|
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
|
-
|
|
930
|
+
mutable,
|
|
570
931
|
rawCode,
|
|
571
|
-
rawMd,
|
|
572
|
-
mdLines,
|
|
573
|
-
searcher,
|
|
574
932
|
searchInput,
|
|
575
933
|
searchClear,
|
|
576
934
|
searchResults,
|
|
577
|
-
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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();
|