@commentray/render 0.0.2 → 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 +30 -0
- package/dist/block-stretch-layout.d.ts.map +1 -0
- package/dist/block-stretch-layout.js +104 -0
- package/dist/block-stretch-layout.js.map +1 -0
- package/dist/build-commentray-nav-search.d.ts +62 -0
- package/dist/build-commentray-nav-search.d.ts.map +1 -0
- package/dist/build-commentray-nav-search.js +98 -0
- package/dist/build-commentray-nav-search.js.map +1 -0
- 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 +12 -5
- package/dist/code-browser-client.js +825 -97
- package/dist/code-browser-client.js.map +1 -1
- package/dist/code-browser-embedded-payload.d.ts +10 -0
- package/dist/code-browser-embedded-payload.d.ts.map +1 -0
- package/dist/code-browser-embedded-payload.js +18 -0
- package/dist/code-browser-embedded-payload.js.map +1 -0
- package/dist/code-browser-encoding.d.ts +9 -0
- package/dist/code-browser-encoding.d.ts.map +1 -0
- package/dist/code-browser-encoding.js +24 -0
- package/dist/code-browser-encoding.js.map +1 -0
- package/dist/code-browser-scroll-sync.d.ts +6 -0
- package/dist/code-browser-scroll-sync.d.ts.map +1 -1
- package/dist/code-browser-scroll-sync.js +1 -0
- package/dist/code-browser-scroll-sync.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-web-storage.d.ts +7 -0
- package/dist/code-browser-web-storage.d.ts.map +1 -0
- package/dist/code-browser-web-storage.js +21 -0
- package/dist/code-browser-web-storage.js.map +1 -0
- package/dist/code-browser.d.ts +76 -1
- package/dist/code-browser.d.ts.map +1 -1
- package/dist/code-browser.js +809 -111
- 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 +3 -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 +13 -1
- package/dist/markdown-pipeline.js.map +1 -1
- package/dist/mermaid-runtime-html.d.ts +7 -0
- package/dist/mermaid-runtime-html.d.ts.map +1 -0
- package/dist/mermaid-runtime-html.js +26 -0
- package/dist/mermaid-runtime-html.js.map +1 -0
- package/dist/side-by-side.d.ts +2 -0
- package/dist/side-by-side.d.ts.map +1 -1
- package/dist/side-by-side.js +7 -7
- package/dist/side-by-side.js.map +1 -1
- package/package.json +6 -4
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
import { FuzzySearcher, PrefixSearcher, Query, SearcherFactory, SubstringSearcher, } from "@m31coding/fuzzy-search";
|
|
2
|
-
import {
|
|
2
|
+
import { mirroredScrollTop, pickCommentrayLineForSourceScroll, pickSourceLine0ForCommentrayScroll, } from "./code-browser-scroll-sync.js";
|
|
3
|
+
import { decodeBase64Utf8 } from "./code-browser-encoding.js";
|
|
4
|
+
import { readEmbeddedRawB64Strings } from "./code-browser-embedded-payload.js";
|
|
5
|
+
import { escapeHtmlHighlightingSearchTokens, findOrderedTokenSpans, lineAtIndex, offsetToLineIndex, } from "./code-browser-search.js";
|
|
6
|
+
import { readWebStorageItem, writeWebStorageItem } from "./code-browser-web-storage.js";
|
|
3
7
|
function tokenizeQuery(q) {
|
|
4
8
|
return q.trim().split(/\s+/).filter(Boolean);
|
|
5
9
|
}
|
|
6
10
|
function clamp(n, lo, hi) {
|
|
7
11
|
return Math.max(lo, Math.min(hi, n));
|
|
8
12
|
}
|
|
9
|
-
function decodeB64(b64) {
|
|
10
|
-
try {
|
|
11
|
-
return decodeURIComponent(escape(atob(b64)));
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
try {
|
|
15
|
-
return atob(b64);
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return "";
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
13
|
function escapeHtmlText(s) {
|
|
23
14
|
return s
|
|
24
15
|
.replace(/&/g, "&")
|
|
@@ -36,7 +27,9 @@ function snippet(s, maxLen) {
|
|
|
36
27
|
function mergeHits(rows, max) {
|
|
37
28
|
const byKey = new Map();
|
|
38
29
|
for (const r of rows) {
|
|
39
|
-
const key =
|
|
30
|
+
const key = r.kind === "path"
|
|
31
|
+
? `path:${r.spPath ?? ""}|${r.crPath ?? ""}|${r.text.slice(0, 120)}`
|
|
32
|
+
: `${r.kind}:${r.line}:${r.crPath ?? ""}`;
|
|
40
33
|
const prev = byKey.get(key);
|
|
41
34
|
if (!prev || r.score > prev.score) {
|
|
42
35
|
byKey.set(key, r);
|
|
@@ -82,12 +75,214 @@ function buildFuzzyHits(searcher, query, topN) {
|
|
|
82
75
|
text: row.text,
|
|
83
76
|
score: 100 + m.quality,
|
|
84
77
|
source: "fuzzy",
|
|
78
|
+
crPath: row.crPath,
|
|
79
|
+
spPath: row.spPath,
|
|
85
80
|
});
|
|
86
81
|
}
|
|
87
82
|
return out;
|
|
88
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,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
function computeMergedSearchHits(input) {
|
|
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")) || "";
|
|
112
|
+
const orderedCode = scope === "commentray-and-paths" ? [] : buildOrderedHits(rawCode, "code", 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
|
+
: [];
|
|
118
|
+
const orderedMd = buildOrderedHits(rawMd, "md", tokens);
|
|
119
|
+
const fuzzyHits = buildFuzzyHits(searcher, queryRaw, 60);
|
|
120
|
+
return mergeHits([...orderedCode, ...orderedPath, ...orderedMd, ...fuzzyHits], 80);
|
|
121
|
+
}
|
|
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) {
|
|
146
|
+
if (combined.length === 0) {
|
|
147
|
+
return '<div class="hint">No matches. Try fewer tokens or looser spelling (fuzzy matches per line).</div>';
|
|
148
|
+
}
|
|
149
|
+
const hintIntro = searchScopeResultsHintIntro(scope);
|
|
150
|
+
const buf = [];
|
|
151
|
+
buf.push(`<div class="hint">${hintIntro} ${combined.length} hit(s).</div>`);
|
|
152
|
+
for (const h of combined) {
|
|
153
|
+
buf.push(searchHitButtonHtml(h, tokens, ctx));
|
|
154
|
+
}
|
|
155
|
+
return buf.join("");
|
|
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
|
+
}
|
|
190
|
+
function readSearchScopeFromShell(shell) {
|
|
191
|
+
const scopeAttr = shell.getAttribute("data-search-scope") || "";
|
|
192
|
+
return {
|
|
193
|
+
scope: scopeAttr === "commentray-and-paths" ? "commentray-and-paths" : "full",
|
|
194
|
+
filePathLabel: shell.getAttribute("data-search-file-path") || "",
|
|
195
|
+
commentrayPathLabel: shell.getAttribute("data-search-commentray-path") || "",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function buildIndexedSearchRows(scope, rawCode, rawMd, filePathLabel, commentrayPathLabel) {
|
|
199
|
+
const mdLines = rawMd.split("\n");
|
|
200
|
+
const codeLines = rawCode.split("\n");
|
|
201
|
+
const pathRows = [];
|
|
202
|
+
if (scope === "commentray-and-paths") {
|
|
203
|
+
if (filePathLabel.trim()) {
|
|
204
|
+
pathRows.push({ kind: "path", line: pathRows.length, text: filePathLabel });
|
|
205
|
+
}
|
|
206
|
+
if (commentrayPathLabel.trim()) {
|
|
207
|
+
pathRows.push({ kind: "path", line: pathRows.length, text: commentrayPathLabel });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return [
|
|
211
|
+
...(scope === "full"
|
|
212
|
+
? codeLines.map((text, line) => ({ kind: "code", line, text }))
|
|
213
|
+
: []),
|
|
214
|
+
...pathRows,
|
|
215
|
+
...mdLines.map((text, line) => ({ kind: "md", line, text })),
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
function indexSearchLineRows(rows) {
|
|
219
|
+
const searcher = SearcherFactory.createDefaultSearcher();
|
|
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]);
|
|
227
|
+
return searcher;
|
|
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
|
+
}
|
|
89
276
|
function wireSearchUi(ctx) {
|
|
90
|
-
const {
|
|
277
|
+
const { scope, filePathLabel, mutable, rawCode, searchInput, searchClear, searchResults, docScrollEl, } = ctx;
|
|
278
|
+
let debounceTimer;
|
|
279
|
+
function clearSearch() {
|
|
280
|
+
clearTimeout(debounceTimer);
|
|
281
|
+
debounceTimer = undefined;
|
|
282
|
+
searchInput.value = "";
|
|
283
|
+
searchResults.innerHTML = "";
|
|
284
|
+
searchResults.hidden = true;
|
|
285
|
+
}
|
|
91
286
|
function runSearch() {
|
|
92
287
|
const tokens = tokenizeQuery(searchInput.value);
|
|
93
288
|
if (tokens.length === 0) {
|
|
@@ -95,78 +290,422 @@ function wireSearchUi(ctx) {
|
|
|
95
290
|
searchResults.innerHTML = "";
|
|
96
291
|
return;
|
|
97
292
|
}
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
293
|
+
const combined = computeMergedSearchHits({
|
|
294
|
+
scope,
|
|
295
|
+
filePathLabel,
|
|
296
|
+
commentrayPathLabel: mutable.commentrayPathLabel,
|
|
297
|
+
rawCode,
|
|
298
|
+
rawMd: mutable.rawMd,
|
|
299
|
+
searcher: mutable.searcher,
|
|
300
|
+
queryRaw: searchInput.value,
|
|
301
|
+
tokens,
|
|
302
|
+
pathBlobWide: mutable.pathBlobWide,
|
|
303
|
+
pathRowsForOrdering: mutable.pathRowsForOrdering.length > 0 ? mutable.pathRowsForOrdering : undefined,
|
|
304
|
+
});
|
|
102
305
|
searchResults.hidden = false;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
const buf = [];
|
|
109
|
-
buf.push(`<div class="hint">Whole source: whitespace tokens in order (may span lines). Per-line fuzzy ranking for typos. ${combined.length} hit(s).</div>`);
|
|
110
|
-
for (const h of combined) {
|
|
111
|
-
const label = h.kind === "code" ? `Code L${h.line + 1}` : `Commentray L${h.line + 1}`;
|
|
112
|
-
const tag = h.source === "ordered" ? "ordered" : "fuzzy";
|
|
113
|
-
buf.push(`<button type="button" class="hit" data-kind="${h.kind}" data-line="${String(h.line)}">` +
|
|
114
|
-
`<span class="meta">${label} <span class="src-tag">(${tag})</span></span>` +
|
|
115
|
-
`<div class="snippet">${escapeHtmlText(snippet(h.text, 200))}</div></button>`);
|
|
116
|
-
}
|
|
117
|
-
searchResults.innerHTML = buf.join("");
|
|
306
|
+
searchResults.innerHTML = searchResultsInnerHtml(scope, combined, tokens, {
|
|
307
|
+
currentCommentrayPath: mutable.commentrayPathLabel,
|
|
308
|
+
currentSourcePath: filePathLabel,
|
|
309
|
+
});
|
|
118
310
|
}
|
|
311
|
+
const hitClickDeps = { mutable, docScrollEl };
|
|
119
312
|
searchResults.addEventListener("click", (ev) => {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
t = t.parentElement;
|
|
123
|
-
}
|
|
124
|
-
if (!t || !t.classList || !t.classList.contains("hit"))
|
|
313
|
+
const hit = findSearchHitButton(ev.target, searchResults);
|
|
314
|
+
if (!hit)
|
|
125
315
|
return;
|
|
126
|
-
|
|
127
|
-
const line = parseInt(t.getAttribute("data-line") || "0", 10);
|
|
128
|
-
if (kind === "code") {
|
|
129
|
-
const el = document.getElementById(`code-line-${String(line)}`);
|
|
130
|
-
if (el)
|
|
131
|
-
el.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
const total = mdLines.length;
|
|
135
|
-
if (total <= 0)
|
|
136
|
-
return;
|
|
137
|
-
const ratio = line / Math.max(1, total - 1);
|
|
138
|
-
const maxScroll = docPane.scrollHeight - docPane.clientHeight;
|
|
139
|
-
docPane.scrollTo({ top: ratio * Math.max(0, maxScroll), behavior: "smooth" });
|
|
140
|
-
}
|
|
316
|
+
handleSearchHitButtonClick(hit, hitClickDeps);
|
|
141
317
|
});
|
|
142
|
-
let debounceTimer;
|
|
143
318
|
searchInput.addEventListener("input", () => {
|
|
144
319
|
clearTimeout(debounceTimer);
|
|
145
320
|
debounceTimer = setTimeout(runSearch, 200);
|
|
146
321
|
});
|
|
147
|
-
searchClear.addEventListener("click",
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
322
|
+
searchClear.addEventListener("click", clearSearch);
|
|
323
|
+
document.addEventListener("keydown", (e) => {
|
|
324
|
+
if (e.key !== "Escape")
|
|
325
|
+
return;
|
|
326
|
+
const query = searchInput.value.trim().length > 0;
|
|
327
|
+
const searchFocused = document.activeElement === searchInput;
|
|
328
|
+
const resultsOpen = !searchResults.hidden;
|
|
329
|
+
if (!query && !searchFocused && !resultsOpen)
|
|
330
|
+
return;
|
|
331
|
+
clearSearch();
|
|
332
|
+
if (searchFocused)
|
|
333
|
+
searchInput.blur();
|
|
334
|
+
e.preventDefault();
|
|
152
335
|
});
|
|
153
336
|
}
|
|
154
337
|
function wireWrapToggle(storageWrap, codePane, wrapCb) {
|
|
155
|
-
const wrap = localStorage
|
|
338
|
+
const wrap = readWebStorageItem(localStorage, storageWrap) === "1";
|
|
156
339
|
wrapCb.checked = wrap;
|
|
157
340
|
if (wrap)
|
|
158
341
|
codePane.classList.add("wrap");
|
|
159
342
|
wrapCb.addEventListener("change", () => {
|
|
160
343
|
if (wrapCb.checked) {
|
|
161
344
|
codePane.classList.add("wrap");
|
|
162
|
-
localStorage
|
|
345
|
+
writeWebStorageItem(localStorage, storageWrap, "1");
|
|
163
346
|
}
|
|
164
347
|
else {
|
|
165
348
|
codePane.classList.remove("wrap");
|
|
166
|
-
localStorage
|
|
349
|
+
writeWebStorageItem(localStorage, storageWrap, "0");
|
|
167
350
|
}
|
|
168
351
|
});
|
|
169
352
|
}
|
|
353
|
+
function parseScrollBlockLinksFromShell(b64) {
|
|
354
|
+
const t = b64.trim();
|
|
355
|
+
if (!t)
|
|
356
|
+
return [];
|
|
357
|
+
try {
|
|
358
|
+
const raw = JSON.parse(decodeBase64Utf8(t));
|
|
359
|
+
if (!Array.isArray(raw))
|
|
360
|
+
return [];
|
|
361
|
+
const out = [];
|
|
362
|
+
for (const x of raw) {
|
|
363
|
+
if (typeof x !== "object" || x === null)
|
|
364
|
+
continue;
|
|
365
|
+
const o = x;
|
|
366
|
+
if (typeof o.id === "string" &&
|
|
367
|
+
typeof o.commentrayLine === "number" &&
|
|
368
|
+
typeof o.sourceStart === "number" &&
|
|
369
|
+
typeof o.sourceEnd === "number") {
|
|
370
|
+
out.push({
|
|
371
|
+
id: o.id,
|
|
372
|
+
commentrayLine: o.commentrayLine,
|
|
373
|
+
sourceStart: o.sourceStart,
|
|
374
|
+
sourceEnd: o.sourceEnd,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return [];
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function probeCodeLine1FromViewport(codePane) {
|
|
385
|
+
const y = codePane.getBoundingClientRect().top + 2;
|
|
386
|
+
const rows = codePane.querySelectorAll('[id^="code-line-"]');
|
|
387
|
+
for (const el of rows) {
|
|
388
|
+
const r = el.getBoundingClientRect();
|
|
389
|
+
if (r.bottom > y) {
|
|
390
|
+
const m = /^code-line-(\d+)$/.exec(el.id);
|
|
391
|
+
if (m)
|
|
392
|
+
return Number(m[1]) + 1;
|
|
393
|
+
return 1;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return rows.length > 0 ? rows.length : 1;
|
|
397
|
+
}
|
|
398
|
+
function probeCommentrayLine0FromDoc(docPane) {
|
|
399
|
+
const y = docPane.getBoundingClientRect().top + 2;
|
|
400
|
+
const anchors = docPane.querySelectorAll(".commentray-block-anchor");
|
|
401
|
+
let best = 0;
|
|
402
|
+
for (const a of anchors) {
|
|
403
|
+
const lineAttr = a.getAttribute("data-commentray-line");
|
|
404
|
+
if (lineAttr === null || lineAttr === "")
|
|
405
|
+
continue;
|
|
406
|
+
if (a.getBoundingClientRect().top <= y + 1)
|
|
407
|
+
best = Number(lineAttr);
|
|
408
|
+
else
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
return best;
|
|
412
|
+
}
|
|
413
|
+
function wireBidirectionalScroll(codePane, docPane, syncFromCode, syncFromDoc) {
|
|
414
|
+
let syncing = "none";
|
|
415
|
+
codePane.addEventListener("scroll", () => {
|
|
416
|
+
if (syncing === "doc")
|
|
417
|
+
return;
|
|
418
|
+
syncing = "code";
|
|
419
|
+
syncFromCode();
|
|
420
|
+
syncing = "none";
|
|
421
|
+
}, { passive: true });
|
|
422
|
+
docPane.addEventListener("scroll", () => {
|
|
423
|
+
if (syncing === "code")
|
|
424
|
+
return;
|
|
425
|
+
syncing = "doc";
|
|
426
|
+
syncFromDoc();
|
|
427
|
+
syncing = "none";
|
|
428
|
+
}, { passive: true });
|
|
429
|
+
}
|
|
430
|
+
/** Index-backed scroll sync when `data-scroll-block-links-b64` is present; else see proportional fallback. */
|
|
431
|
+
function wireBlockAwareScrollSync(codePane, docPane, getLinks) {
|
|
432
|
+
wireBidirectionalScroll(codePane, docPane, () => {
|
|
433
|
+
const links = getLinks();
|
|
434
|
+
const line1 = probeCodeLine1FromViewport(codePane);
|
|
435
|
+
const mdLine0 = pickCommentrayLineForSourceScroll(links, line1);
|
|
436
|
+
if (mdLine0 === null) {
|
|
437
|
+
docPane.scrollTop = mirroredScrollTop(codePane.scrollTop, codePane.scrollHeight, codePane.clientHeight, docPane.scrollHeight, docPane.clientHeight);
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
const anchor = docPane.querySelector(`[data-commentray-line="${String(mdLine0)}"]`);
|
|
441
|
+
if (anchor instanceof HTMLElement) {
|
|
442
|
+
const top = anchor.getBoundingClientRect().top -
|
|
443
|
+
docPane.getBoundingClientRect().top +
|
|
444
|
+
docPane.scrollTop;
|
|
445
|
+
docPane.scrollTop = Math.max(0, top - 2);
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
docPane.scrollTop = mirroredScrollTop(codePane.scrollTop, codePane.scrollHeight, codePane.clientHeight, docPane.scrollHeight, docPane.clientHeight);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}, () => {
|
|
452
|
+
const links = getLinks();
|
|
453
|
+
const mdLine0 = probeCommentrayLine0FromDoc(docPane);
|
|
454
|
+
const src0 = pickSourceLine0ForCommentrayScroll(links, mdLine0);
|
|
455
|
+
if (src0 === null) {
|
|
456
|
+
codePane.scrollTop = mirroredScrollTop(docPane.scrollTop, docPane.scrollHeight, docPane.clientHeight, codePane.scrollHeight, codePane.clientHeight);
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
const el = document.getElementById(`code-line-${String(src0)}`);
|
|
460
|
+
if (el) {
|
|
461
|
+
const top = el.getBoundingClientRect().top -
|
|
462
|
+
codePane.getBoundingClientRect().top +
|
|
463
|
+
codePane.scrollTop;
|
|
464
|
+
codePane.scrollTop = Math.max(0, top - 2);
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
codePane.scrollTop = mirroredScrollTop(docPane.scrollTop, docPane.scrollHeight, docPane.clientHeight, codePane.scrollHeight, codePane.clientHeight);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
/** Proportional scroll sync when there is no index-backed block map (GitHub Pages default). */
|
|
473
|
+
function wireProportionalScrollSync(codePane, docPane) {
|
|
474
|
+
wireBidirectionalScroll(codePane, docPane, () => {
|
|
475
|
+
docPane.scrollTop = mirroredScrollTop(codePane.scrollTop, codePane.scrollHeight, codePane.clientHeight, docPane.scrollHeight, docPane.clientHeight);
|
|
476
|
+
}, () => {
|
|
477
|
+
codePane.scrollTop = mirroredScrollTop(docPane.scrollTop, docPane.scrollHeight, docPane.clientHeight, codePane.scrollHeight, codePane.clientHeight);
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
function isDocumentedPairNav(x) {
|
|
481
|
+
if (typeof x !== "object" || x === null)
|
|
482
|
+
return false;
|
|
483
|
+
const o = x;
|
|
484
|
+
return (typeof o.sourcePath === "string" &&
|
|
485
|
+
typeof o.commentrayPath === "string" &&
|
|
486
|
+
typeof o.sourceOnGithub === "string" &&
|
|
487
|
+
typeof o.commentrayOnGithub === "string" &&
|
|
488
|
+
(o.staticBrowseUrl === undefined || typeof o.staticBrowseUrl === "string"));
|
|
489
|
+
}
|
|
490
|
+
function pairsFromJsonArray(raw) {
|
|
491
|
+
const pairs = [];
|
|
492
|
+
if (!Array.isArray(raw))
|
|
493
|
+
return pairs;
|
|
494
|
+
for (const x of raw) {
|
|
495
|
+
if (isDocumentedPairNav(x))
|
|
496
|
+
pairs.push(x);
|
|
497
|
+
}
|
|
498
|
+
return pairs;
|
|
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
|
+
}
|
|
565
|
+
/** Offline-first: UTF-8 base64 JSON array produced by the static Pages build. */
|
|
566
|
+
function parseDocumentedPairsFromEmbeddedB64(b64) {
|
|
567
|
+
const t = b64.trim();
|
|
568
|
+
if (t === "")
|
|
569
|
+
return [];
|
|
570
|
+
try {
|
|
571
|
+
const raw = JSON.parse(decodeBase64Utf8(t));
|
|
572
|
+
return pairsFromJsonArray(raw);
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
return [];
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function insertSourcePathTrie(root, pair) {
|
|
579
|
+
const segments = pair.sourcePath.split("/").filter(Boolean);
|
|
580
|
+
if (segments.length === 0)
|
|
581
|
+
return;
|
|
582
|
+
let n = root;
|
|
583
|
+
for (let i = 0; i < segments.length; i++) {
|
|
584
|
+
const seg = segments[i];
|
|
585
|
+
if (seg === undefined)
|
|
586
|
+
continue;
|
|
587
|
+
if (!n.children.has(seg))
|
|
588
|
+
n.children.set(seg, { children: new Map(), pairs: [] });
|
|
589
|
+
const next = n.children.get(seg);
|
|
590
|
+
if (next === undefined)
|
|
591
|
+
return;
|
|
592
|
+
if (i === segments.length - 1)
|
|
593
|
+
next.pairs.push(pair);
|
|
594
|
+
n = next;
|
|
595
|
+
}
|
|
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
|
+
}
|
|
615
|
+
function renderDocumentedTreeHtml(node) {
|
|
616
|
+
const keys = [...node.children.keys()].sort((a, b) => a.localeCompare(b));
|
|
617
|
+
if (keys.length === 0)
|
|
618
|
+
return "";
|
|
619
|
+
const lis = [];
|
|
620
|
+
for (const name of keys) {
|
|
621
|
+
const ch = node.children.get(name);
|
|
622
|
+
if (ch === undefined)
|
|
623
|
+
continue;
|
|
624
|
+
if (ch.children.size > 0) {
|
|
625
|
+
const inner = renderDocumentedTreeHtml(ch);
|
|
626
|
+
lis.push(`<li><div class="tree-dir">${escapeHtmlText(name)}</div>${inner}</li>`);
|
|
627
|
+
}
|
|
628
|
+
if (ch.pairs.length > 0) {
|
|
629
|
+
const multi = ch.pairs.length > 1;
|
|
630
|
+
for (const pr of ch.pairs) {
|
|
631
|
+
const label = escapeHtmlText(treeFileLinkLabel(pr, multi));
|
|
632
|
+
const title = escapeHtmlText(`${pr.sourcePath} — open companion on GitHub`);
|
|
633
|
+
lis.push(`<li><div class="tree-file">` +
|
|
634
|
+
`<a class="tree-file-link" href="${escapeHtmlText(pr.commentrayOnGithub)}" target="_blank" rel="noopener noreferrer" title="${title}">${label}</a>` +
|
|
635
|
+
`</div></li>`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return `<ul>${lis.join("")}</ul>`;
|
|
640
|
+
}
|
|
641
|
+
function renderDocumentedPairsIntoHost(treeHost, pairs) {
|
|
642
|
+
if (pairs.length === 0) {
|
|
643
|
+
treeHost.innerHTML =
|
|
644
|
+
'<p class="nav-rail__doc-hub-hint" role="status">No documented pairs in this export.</p>';
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const root = { children: new Map(), pairs: [] };
|
|
648
|
+
for (const p of pairs)
|
|
649
|
+
insertSourcePathTrie(root, p);
|
|
650
|
+
treeHost.innerHTML = renderDocumentedTreeHtml(root);
|
|
651
|
+
}
|
|
652
|
+
function loadDocumentedPairs(jsonUrl, embeddedB64) {
|
|
653
|
+
let loaded = null;
|
|
654
|
+
let loadPromise = null;
|
|
655
|
+
return async () => {
|
|
656
|
+
if (loaded !== null)
|
|
657
|
+
return loaded;
|
|
658
|
+
if (loadPromise === null) {
|
|
659
|
+
loadPromise = (async () => {
|
|
660
|
+
if (embeddedB64.length > 0) {
|
|
661
|
+
loaded = parseDocumentedPairsFromEmbeddedB64(embeddedB64);
|
|
662
|
+
if (loaded.length > 0)
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (jsonUrl.length === 0) {
|
|
666
|
+
loaded = [];
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const res = await fetch(jsonUrl, { credentials: "same-origin" });
|
|
670
|
+
if (!res.ok)
|
|
671
|
+
throw new Error(`nav json ${String(res.status)}`);
|
|
672
|
+
const body = (await res.json());
|
|
673
|
+
loaded = pairsFromJsonArray(body.documentedPairs);
|
|
674
|
+
})();
|
|
675
|
+
}
|
|
676
|
+
await loadPromise;
|
|
677
|
+
return loaded ?? [];
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
function wireDocumentedFilesTree() {
|
|
681
|
+
const hub = document.getElementById("documented-files-hub");
|
|
682
|
+
const treeHost = document.getElementById("documented-files-tree");
|
|
683
|
+
const shell = document.getElementById("shell");
|
|
684
|
+
if (!(hub instanceof HTMLDetailsElement) || !(treeHost instanceof HTMLElement)) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const treeMount = treeHost;
|
|
688
|
+
const jsonUrl = hub.getAttribute("data-nav-json-url")?.trim() ?? "";
|
|
689
|
+
const embeddedB64 = shell?.getAttribute("data-documented-pairs-b64")?.trim() ?? "";
|
|
690
|
+
if (jsonUrl.length === 0 && embeddedB64.length === 0)
|
|
691
|
+
return;
|
|
692
|
+
const ensureLoaded = loadDocumentedPairs(jsonUrl, embeddedB64);
|
|
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)
|
|
705
|
+
return;
|
|
706
|
+
void hydrateTree();
|
|
707
|
+
});
|
|
708
|
+
}
|
|
170
709
|
function wireSplitter(storageSplit, shell, codePane, gutter, initialPct) {
|
|
171
710
|
let dragging = false;
|
|
172
711
|
let lastPct = initialPct;
|
|
@@ -185,7 +724,7 @@ function wireSplitter(storageSplit, shell, codePane, gutter, initialPct) {
|
|
|
185
724
|
window.removeEventListener("mouseup", stop);
|
|
186
725
|
document.body.style.cursor = "";
|
|
187
726
|
document.body.style.userSelect = "";
|
|
188
|
-
localStorage
|
|
727
|
+
writeWebStorageItem(localStorage, storageSplit, String(lastPct));
|
|
189
728
|
}
|
|
190
729
|
gutter.addEventListener("mousedown", (e) => {
|
|
191
730
|
e.preventDefault();
|
|
@@ -196,52 +735,241 @@ function wireSplitter(storageSplit, shell, codePane, gutter, initialPct) {
|
|
|
196
735
|
window.addEventListener("mouseup", stop);
|
|
197
736
|
});
|
|
198
737
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
738
|
+
const STORAGE_SPLIT_PCT = "commentray.codeCommentrayStatic.splitPct";
|
|
739
|
+
const STORAGE_WRAP_LINES = "commentray.codeCommentrayStatic.wrap";
|
|
740
|
+
function wireStretchLayoutChrome(codePane) {
|
|
741
|
+
const wrapCb = document.getElementById("wrap-lines");
|
|
742
|
+
if (wrapCb) {
|
|
743
|
+
wireWrapToggle(STORAGE_WRAP_LINES, codePane, wrapCb);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
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() {
|
|
204
765
|
const docPane = document.getElementById("doc-pane");
|
|
205
766
|
const gutter = document.getElementById("gutter");
|
|
206
767
|
const wrapCb = document.getElementById("wrap-lines");
|
|
207
768
|
const searchInput = document.getElementById("search-q");
|
|
208
769
|
const searchClear = document.getElementById("search-clear");
|
|
209
770
|
const searchResults = document.getElementById("search-results");
|
|
210
|
-
if (!
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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);
|
|
871
|
+
return;
|
|
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;
|
|
895
|
+
const { rawCodeB64, rawMdB64 } = readEmbeddedRawB64Strings(shell, codePane);
|
|
896
|
+
const rawCode = decodeBase64Utf8(rawCodeB64);
|
|
897
|
+
const rawMd = decodeBase64Utf8(rawMdB64);
|
|
898
|
+
const scrollLinks = parseScrollBlockLinksFromShell(shell.getAttribute("data-scroll-block-links-b64") || "");
|
|
899
|
+
const { scope, filePathLabel, commentrayPathLabel } = readSearchScopeFromShell(shell);
|
|
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();
|
|
230
927
|
wireSearchUi({
|
|
928
|
+
scope,
|
|
929
|
+
filePathLabel,
|
|
930
|
+
mutable,
|
|
231
931
|
rawCode,
|
|
232
|
-
rawMd,
|
|
233
|
-
mdLines,
|
|
234
|
-
searcher,
|
|
235
932
|
searchInput,
|
|
236
933
|
searchClear,
|
|
237
934
|
searchResults,
|
|
238
|
-
|
|
935
|
+
docScrollEl,
|
|
239
936
|
});
|
|
240
|
-
const
|
|
937
|
+
const navSearchUrl = shell.getAttribute("data-nav-search-json-url")?.trim() ?? "";
|
|
938
|
+
wireDualPaneNavSearchFetch(navSearchUrl, indexState, mutable, rebuildSearcher, searchInput);
|
|
939
|
+
const pct0 = parseFloat(readWebStorageItem(localStorage, STORAGE_SPLIT_PCT) || "50");
|
|
241
940
|
const pct = clamp(Number.isFinite(pct0) ? pct0 : 50, 15, 85);
|
|
242
941
|
codePane.style.flex = `0 0 ${pct}%`;
|
|
243
|
-
wireWrapToggle(
|
|
244
|
-
wireSplitter(
|
|
942
|
+
wireWrapToggle(STORAGE_WRAP_LINES, codePane, wrapCb);
|
|
943
|
+
wireSplitter(STORAGE_SPLIT_PCT, shell, codePane, gutter, pct);
|
|
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);
|
|
959
|
+
}
|
|
960
|
+
function main() {
|
|
961
|
+
wireDocumentedFilesTree();
|
|
962
|
+
const shell = document.getElementById("shell");
|
|
963
|
+
const codePane = document.getElementById("code-pane");
|
|
964
|
+
if (!shell || !codePane) {
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
const layout = shell.getAttribute("data-layout") || "dual";
|
|
968
|
+
if (layout === "stretch") {
|
|
969
|
+
wireStretchLayoutChrome(codePane);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
wireDualPaneCodeBrowser(shell, codePane);
|
|
245
973
|
}
|
|
246
974
|
if (document.readyState === "loading") {
|
|
247
975
|
document.addEventListener("DOMContentLoaded", main);
|