@commentray/render 0.0.5 → 0.0.8
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/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 +11 -5
- package/dist/build-commentray-nav-search.d.ts.map +1 -1
- package/dist/build-commentray-nav-search.js +11 -11
- package/dist/build-commentray-nav-search.js.map +1 -1
- 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 -12
- package/dist/code-browser-client.js +1106 -145
- package/dist/code-browser-client.js.map +1 -1
- package/dist/code-browser-color-theme.d.ts +15 -0
- package/dist/code-browser-color-theme.d.ts.map +1 -0
- package/dist/code-browser-color-theme.js +73 -0
- package/dist/code-browser-color-theme.js.map +1 -0
- package/dist/code-browser-pair-nav.d.ts +31 -0
- package/dist/code-browser-pair-nav.d.ts.map +1 -0
- package/dist/code-browser-pair-nav.js +77 -0
- package/dist/code-browser-pair-nav.js.map +1 -0
- package/dist/code-browser-scroll-sync.js +1 -1
- package/dist/code-browser-scroll-sync.js.map +1 -1
- package/dist/code-browser-search.d.ts +45 -0
- package/dist/code-browser-search.d.ts.map +1 -1
- package/dist/code-browser-search.js +89 -0
- package/dist/code-browser-search.js.map +1 -1
- package/dist/code-browser.d.ts +24 -3
- package/dist/code-browser.d.ts.map +1 -1
- package/dist/code-browser.js +1202 -288
- package/dist/code-browser.js.map +1 -1
- package/dist/hljs-stylesheet-themes.d.ts +13 -0
- package/dist/hljs-stylesheet-themes.d.ts.map +1 -0
- package/dist/hljs-stylesheet-themes.js +19 -0
- package/dist/hljs-stylesheet-themes.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/inline-favicon.d.ts +2 -0
- package/dist/inline-favicon.d.ts.map +1 -0
- package/dist/inline-favicon.js +25 -0
- package/dist/inline-favicon.js.map +1 -0
- package/dist/markdown-pipeline.d.ts.map +1 -1
- package/dist/markdown-pipeline.js +38 -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 +13 -3
- package/dist/mermaid-runtime-html.js.map +1 -1
- package/dist/package-version.d.ts.map +1 -1
- package/dist/package-version.js +4 -4
- package/dist/package-version.js.map +1 -1
- package/dist/side-by-side-layout.css +58 -0
- package/dist/side-by-side.d.ts.map +1 -1
- package/dist/side-by-side.js +10 -12
- package/dist/side-by-side.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,15 +1,224 @@
|
|
|
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 { escapeHtmlHighlightingSearchTokens, 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, isHubRelativeStaticBrowseHref, isSameDocumentedPair, normPosixPath, resolveStaticBrowseHref, staticBrowseHrefForShellDataAttribute, } from "./code-browser-pair-nav.js";
|
|
8
|
+
import { COMMENTRAY_COLOR_THEME_STORAGE_KEY, applyCommentrayColorTheme, nextCommentrayColorThemeMode, parseCommentrayColorThemeMode, } from "./code-browser-color-theme.js";
|
|
6
9
|
import { readWebStorageItem, writeWebStorageItem } from "./code-browser-web-storage.js";
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Hub pages emit `./browse/…` relative to the site root. From `/…/browse/current.html` the browser
|
|
12
|
+
* would otherwise resolve that to `…/browse/browse/…`.
|
|
13
|
+
*/
|
|
14
|
+
function rewriteHubRelativeBrowseAnchorsIn(root) {
|
|
15
|
+
const path = globalThis.location.pathname;
|
|
16
|
+
const origin = globalThis.location.origin;
|
|
17
|
+
for (const el of Array.from(root.querySelectorAll("a[href]"))) {
|
|
18
|
+
if (!(el instanceof HTMLAnchorElement))
|
|
19
|
+
continue;
|
|
20
|
+
const raw = el.getAttribute("href")?.trim() ?? "";
|
|
21
|
+
if (!isHubRelativeStaticBrowseHref(raw))
|
|
22
|
+
continue;
|
|
23
|
+
el.href = resolveStaticBrowseHref(raw, path, origin);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function runMermaidOnFreshDocNodes(docBody) {
|
|
27
|
+
if (typeof globalThis.location !== "undefined" && globalThis.location.protocol === "file:")
|
|
28
|
+
return;
|
|
29
|
+
/** Only fenced diagram sources; Mermaid leaves other `.mermaid` nodes in the tree after render. */
|
|
30
|
+
const allPres = Array.from(docBody.querySelectorAll("pre.mermaid"));
|
|
31
|
+
/** Do not re-run on wrappers that already have SVG (avoids corrupting output after dual-mobile pane flip). */
|
|
32
|
+
const nodes = allPres.filter((pre) => {
|
|
33
|
+
const wrap = pre.closest(".commentray-mermaid");
|
|
34
|
+
return wrap === null || wrap.querySelector("svg") === null;
|
|
35
|
+
});
|
|
36
|
+
if (nodes.length === 0)
|
|
37
|
+
return;
|
|
38
|
+
const m = globalThis
|
|
39
|
+
.commentrayMermaid;
|
|
40
|
+
if (!m)
|
|
41
|
+
return;
|
|
42
|
+
void m.run({ nodes }).catch((err) => {
|
|
43
|
+
console.error("Commentray: mermaid.run failed", err);
|
|
44
|
+
});
|
|
9
45
|
}
|
|
10
46
|
function clamp(n, lo, hi) {
|
|
11
47
|
return Math.max(lo, Math.min(hi, n));
|
|
12
48
|
}
|
|
49
|
+
/** Y offset in `scrollEl`’s scroll coordinate space to place `child`’s top at the scrollport (CSS px). */
|
|
50
|
+
function scrollTopToAlignChildTop(scrollEl, child, leadCssPx) {
|
|
51
|
+
const cr = child.getBoundingClientRect();
|
|
52
|
+
const sr = scrollEl.getBoundingClientRect();
|
|
53
|
+
return scrollEl.scrollTop + (cr.top - sr.top) - scrollEl.clientTop - leadCssPx;
|
|
54
|
+
}
|
|
55
|
+
/** Avoid feedback loops when sub-pixel math matches the current position (common with browser zoom). */
|
|
56
|
+
function applyScrollTopClamped(scrollEl, nextTop) {
|
|
57
|
+
const maxY = Math.max(0, scrollEl.scrollHeight - scrollEl.clientHeight);
|
|
58
|
+
const clamped = clamp(nextTop, 0, maxY);
|
|
59
|
+
if (Math.abs(scrollEl.scrollTop - clamped) < 0.25)
|
|
60
|
+
return;
|
|
61
|
+
scrollEl.scrollTop = clamped;
|
|
62
|
+
}
|
|
63
|
+
function paneUsesInternalYScroll(el) {
|
|
64
|
+
const max = el.scrollHeight - el.clientHeight;
|
|
65
|
+
if (max <= 1)
|
|
66
|
+
return false;
|
|
67
|
+
const oy = getComputedStyle(el).overflowY;
|
|
68
|
+
return oy === "auto" || oy === "scroll" || oy === "overlay";
|
|
69
|
+
}
|
|
70
|
+
function rootScrollingElement() {
|
|
71
|
+
const s = document.scrollingElement;
|
|
72
|
+
if (s instanceof HTMLElement)
|
|
73
|
+
return s;
|
|
74
|
+
return document.documentElement;
|
|
75
|
+
}
|
|
76
|
+
function applyWindowScrollRatio(ratio) {
|
|
77
|
+
const root = rootScrollingElement();
|
|
78
|
+
const maxY = Math.max(0, root.scrollHeight - root.clientHeight);
|
|
79
|
+
const next = clamp(ratio * maxY, 0, maxY);
|
|
80
|
+
if (Math.abs(root.scrollTop - next) < 0.25)
|
|
81
|
+
return;
|
|
82
|
+
root.scrollTop = next;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Reveal `child` near the top of the reading surface: the pane’s own scrollport when it scrolls
|
|
86
|
+
* internally (desktop dual-pane), otherwise the document root (narrow flow layout).
|
|
87
|
+
*/
|
|
88
|
+
function applyRevealChildInPane(scrollport, child, leadCssPx) {
|
|
89
|
+
if (paneUsesInternalYScroll(scrollport)) {
|
|
90
|
+
applyScrollTopClamped(scrollport, Math.round(scrollTopToAlignChildTop(scrollport, child, leadCssPx)));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const root = rootScrollingElement();
|
|
94
|
+
const cr = child.getBoundingClientRect();
|
|
95
|
+
const targetTop = globalThis.scrollY + cr.top - leadCssPx;
|
|
96
|
+
const maxY = Math.max(0, root.scrollHeight - root.clientHeight);
|
|
97
|
+
const clamped = clamp(targetTop, 0, maxY);
|
|
98
|
+
if (Math.abs(root.scrollTop - clamped) < 0.25)
|
|
99
|
+
return;
|
|
100
|
+
root.scrollTop = clamped;
|
|
101
|
+
}
|
|
102
|
+
function windowScrollRatio() {
|
|
103
|
+
const root = rootScrollingElement();
|
|
104
|
+
const maxY = Math.max(0, root.scrollHeight - root.clientHeight);
|
|
105
|
+
return maxY > 0 ? clamp(root.scrollTop / maxY, 0, 1) : 0;
|
|
106
|
+
}
|
|
107
|
+
function applyDocToCodeFlipPlanImpl(codePane, _docPane, plan) {
|
|
108
|
+
if (plan.k === "block") {
|
|
109
|
+
const el = codePane.querySelector(`#code-line-${String(plan.src0)}`);
|
|
110
|
+
if (el) {
|
|
111
|
+
applyRevealChildInPane(codePane, el, 2);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
applyWindowScrollRatio(plan.winRatio);
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (plan.k === "mirrorW") {
|
|
119
|
+
if (paneUsesInternalYScroll(codePane)) {
|
|
120
|
+
const maxC = Math.max(0, codePane.scrollHeight - codePane.clientHeight);
|
|
121
|
+
applyScrollTopClamped(codePane, plan.ratio * maxC);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
applyWindowScrollRatio(plan.ratio);
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const nextTop = mirroredScrollTop(plan.docTop, plan.docSH, plan.docCH, codePane.scrollHeight, codePane.clientHeight);
|
|
129
|
+
if (paneUsesInternalYScroll(codePane)) {
|
|
130
|
+
applyScrollTopClamped(codePane, nextTop);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const denom = Math.max(1, codePane.scrollHeight - codePane.clientHeight);
|
|
134
|
+
applyWindowScrollRatio(clamp(nextTop / denom, 0, 1));
|
|
135
|
+
}
|
|
136
|
+
function applyCodeToDocFlipPlanImpl(_codePane, docPane, plan) {
|
|
137
|
+
if (plan.k === "block") {
|
|
138
|
+
const anchor = docPane.querySelector(`[data-commentray-line="${String(plan.mdLine0)}"]`);
|
|
139
|
+
if (anchor instanceof HTMLElement) {
|
|
140
|
+
applyRevealChildInPane(docPane, anchor, 2);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
applyWindowScrollRatio(plan.winRatio);
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (plan.k === "mirrorW") {
|
|
148
|
+
if (paneUsesInternalYScroll(docPane)) {
|
|
149
|
+
const maxD = Math.max(0, docPane.scrollHeight - docPane.clientHeight);
|
|
150
|
+
applyScrollTopClamped(docPane, plan.ratio * maxD);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
applyWindowScrollRatio(plan.ratio);
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const nextTop = mirroredScrollTop(plan.codeTop, plan.codeSH, plan.codeCH, docPane.scrollHeight, docPane.clientHeight);
|
|
158
|
+
if (paneUsesInternalYScroll(docPane)) {
|
|
159
|
+
applyScrollTopClamped(docPane, nextTop);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const denom = Math.max(1, docPane.scrollHeight - docPane.clientHeight);
|
|
163
|
+
applyWindowScrollRatio(clamp(nextTop / denom, 0, 1));
|
|
164
|
+
}
|
|
165
|
+
function buildDocToCodeFlipPlanBlockAware(docPane, getLinks) {
|
|
166
|
+
const winRatio = windowScrollRatio();
|
|
167
|
+
const links = getLinks();
|
|
168
|
+
const mdLine0 = probeCommentrayLine0FromDoc(docPane);
|
|
169
|
+
const src0 = pickSourceLine0ForCommentrayScroll(links, mdLine0);
|
|
170
|
+
if (src0 !== null)
|
|
171
|
+
return { k: "block", src0, winRatio };
|
|
172
|
+
if (paneUsesInternalYScroll(docPane)) {
|
|
173
|
+
return {
|
|
174
|
+
k: "mirrorI",
|
|
175
|
+
docTop: docPane.scrollTop,
|
|
176
|
+
docSH: docPane.scrollHeight,
|
|
177
|
+
docCH: docPane.clientHeight,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return { k: "mirrorW", ratio: winRatio };
|
|
181
|
+
}
|
|
182
|
+
function buildCodeToDocFlipPlanBlockAware(codePane, _docPane, getLinks) {
|
|
183
|
+
const winRatio = windowScrollRatio();
|
|
184
|
+
const links = getLinks();
|
|
185
|
+
const line1 = probeCodeLine1FromViewport(codePane);
|
|
186
|
+
const mdLine0 = pickCommentrayLineForSourceScroll(links, line1);
|
|
187
|
+
if (mdLine0 === null) {
|
|
188
|
+
if (paneUsesInternalYScroll(codePane)) {
|
|
189
|
+
return {
|
|
190
|
+
k: "mirrorI",
|
|
191
|
+
codeTop: codePane.scrollTop,
|
|
192
|
+
codeSH: codePane.scrollHeight,
|
|
193
|
+
codeCH: codePane.clientHeight,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return { k: "mirrorW", ratio: winRatio };
|
|
197
|
+
}
|
|
198
|
+
return { k: "block", mdLine0, winRatio };
|
|
199
|
+
}
|
|
200
|
+
function buildDocToCodeFlipPlanProportional(docPane) {
|
|
201
|
+
if (paneUsesInternalYScroll(docPane)) {
|
|
202
|
+
return {
|
|
203
|
+
k: "mirrorI",
|
|
204
|
+
docTop: docPane.scrollTop,
|
|
205
|
+
docSH: docPane.scrollHeight,
|
|
206
|
+
docCH: docPane.clientHeight,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return { k: "mirrorW", ratio: windowScrollRatio() };
|
|
210
|
+
}
|
|
211
|
+
function buildCodeToDocFlipPlanProportional(codePane) {
|
|
212
|
+
if (paneUsesInternalYScroll(codePane)) {
|
|
213
|
+
return {
|
|
214
|
+
k: "mirrorI",
|
|
215
|
+
codeTop: codePane.scrollTop,
|
|
216
|
+
codeSH: codePane.scrollHeight,
|
|
217
|
+
codeCH: codePane.clientHeight,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return { k: "mirrorW", ratio: windowScrollRatio() };
|
|
221
|
+
}
|
|
13
222
|
function escapeHtmlText(s) {
|
|
14
223
|
return s
|
|
15
224
|
.replace(/&/g, "&")
|
|
@@ -154,37 +363,62 @@ function searchResultsInnerHtml(scope, combined, tokens, ctx) {
|
|
|
154
363
|
}
|
|
155
364
|
return buf.join("");
|
|
156
365
|
}
|
|
366
|
+
function emptyBrowsePreviewHint(scope, rowCount, totalUnique, usedIndexFallback) {
|
|
367
|
+
if (scope === "full") {
|
|
368
|
+
return "Documented source for this page. Type to search.";
|
|
369
|
+
}
|
|
370
|
+
if (usedIndexFallback) {
|
|
371
|
+
return "Documented source on this page. Type to search the index when it is available.";
|
|
372
|
+
}
|
|
373
|
+
if (totalUnique > rowCount) {
|
|
374
|
+
return `Indexed source files (${String(rowCount)} of ${String(totalUnique)} shown). Type to search.`;
|
|
375
|
+
}
|
|
376
|
+
return `Indexed source files (${String(totalUnique)}). Type to search.`;
|
|
377
|
+
}
|
|
378
|
+
function emptySearchBrowsePreviewInnerHtml(hint, rows, ctx) {
|
|
379
|
+
const tokens = [];
|
|
380
|
+
const buf = [`<div class="hint">${escapeHtmlText(hint)}</div>`];
|
|
381
|
+
const hits = rows.map((r, i) => ({
|
|
382
|
+
kind: "path",
|
|
383
|
+
line: i,
|
|
384
|
+
text: r.sourcePath,
|
|
385
|
+
score: 1000,
|
|
386
|
+
source: "ordered",
|
|
387
|
+
spPath: r.sourcePath,
|
|
388
|
+
crPath: r.commentrayPath,
|
|
389
|
+
}));
|
|
390
|
+
for (const h of hits) {
|
|
391
|
+
buf.push(searchHitButtonHtml(h, tokens, ctx));
|
|
392
|
+
}
|
|
393
|
+
return buf.join("");
|
|
394
|
+
}
|
|
157
395
|
function scrollDocToMarkdownLine0(docScrollEl, line0, mdLineCount) {
|
|
158
396
|
const el = docScrollEl.querySelector(`#commentray-md-line-${String(line0)}`);
|
|
159
397
|
if (el instanceof HTMLElement) {
|
|
160
|
-
const top =
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
docScrollEl.scrollTo({ top: Math.max(0, top - 8), behavior: "smooth" });
|
|
398
|
+
const top = Math.round(scrollTopToAlignChildTop(docScrollEl, el, 8));
|
|
399
|
+
const maxY = Math.round(Math.max(0, docScrollEl.scrollHeight - docScrollEl.clientHeight));
|
|
400
|
+
docScrollEl.scrollTo({ top: clamp(top, 0, maxY), behavior: "smooth" });
|
|
164
401
|
return;
|
|
165
402
|
}
|
|
166
403
|
if (mdLineCount <= 1)
|
|
167
404
|
return;
|
|
168
405
|
const ratio = line0 / Math.max(1, mdLineCount - 1);
|
|
169
|
-
const maxScroll = docScrollEl.scrollHeight - docScrollEl.clientHeight;
|
|
170
|
-
docScrollEl.scrollTo({ top: ratio *
|
|
406
|
+
const maxScroll = Math.max(0, docScrollEl.scrollHeight - docScrollEl.clientHeight);
|
|
407
|
+
docScrollEl.scrollTo({ top: ratio * maxScroll, behavior: "smooth" });
|
|
171
408
|
}
|
|
172
|
-
function
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (p.staticBrowseUrl?.trim()) {
|
|
177
|
-
const u = new URL(p.staticBrowseUrl.trim(), globalThis.location.href);
|
|
409
|
+
function navigateToDocumentedPair(pair, mdLine0) {
|
|
410
|
+
if (pair.staticBrowseUrl?.trim()) {
|
|
411
|
+
const href = resolveStaticBrowseHref(pair.staticBrowseUrl.trim(), globalThis.location.pathname, globalThis.location.origin);
|
|
412
|
+
const u = new URL(href);
|
|
178
413
|
if (mdLine0 !== null && mdLine0 >= 0)
|
|
179
414
|
u.hash = `commentray-md-line-${String(mdLine0)}`;
|
|
180
|
-
globalThis.
|
|
415
|
+
globalThis.location.assign(u.toString());
|
|
181
416
|
return;
|
|
182
417
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
globalThis.open(url, "_blank", "noopener,noreferrer");
|
|
418
|
+
const gh = (pair.commentrayOnGithub ?? "").trim();
|
|
419
|
+
if (gh.length > 0) {
|
|
420
|
+
const url = mdLine0 !== null && mdLine0 >= 0 ? `${gh}#L${String(mdLine0 + 1)}` : gh;
|
|
421
|
+
globalThis.location.assign(url);
|
|
188
422
|
}
|
|
189
423
|
}
|
|
190
424
|
function readSearchScopeFromShell(shell) {
|
|
@@ -243,18 +477,25 @@ function scrollCodeHitToView(line) {
|
|
|
243
477
|
el.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
244
478
|
}
|
|
245
479
|
function handlePathSearchHit(button, deps) {
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
if (
|
|
480
|
+
const hitCr = (button.getAttribute("data-cr-path") ?? "").trim();
|
|
481
|
+
const hitSp = (button.getAttribute("data-sp-path") ?? "").trim();
|
|
482
|
+
const pair = findDocumentedPair(deps.mutable.documentedPairs, hitCr, hitSp);
|
|
483
|
+
if (pair && isSameDocumentedPair(pair, deps.filePathLabel, deps.mutable.commentrayPathLabel)) {
|
|
250
484
|
deps.docScrollEl.scrollTo({ top: 0, behavior: "smooth" });
|
|
251
485
|
return;
|
|
252
486
|
}
|
|
253
|
-
|
|
487
|
+
if (pair)
|
|
488
|
+
navigateToDocumentedPair(pair, null);
|
|
254
489
|
}
|
|
255
490
|
function handleMdSearchHit(line, crHit, deps) {
|
|
256
|
-
|
|
257
|
-
|
|
491
|
+
const curCr = deps.mutable.commentrayPathLabel.trim();
|
|
492
|
+
const cr = crHit.trim();
|
|
493
|
+
if (cr.length > 0 && normPosixPath(cr) !== normPosixPath(curCr)) {
|
|
494
|
+
const pair = findDocumentedPair(deps.mutable.documentedPairs, cr, "");
|
|
495
|
+
if (pair) {
|
|
496
|
+
navigateToDocumentedPair(pair, line);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
258
499
|
return;
|
|
259
500
|
}
|
|
260
501
|
scrollDocToMarkdownLine0(deps.docScrollEl, line, deps.mutable.mdLines.length);
|
|
@@ -273,6 +514,36 @@ function handleSearchHitButtonClick(button, deps) {
|
|
|
273
514
|
}
|
|
274
515
|
handleMdSearchHit(line, crHit, deps);
|
|
275
516
|
}
|
|
517
|
+
/** Empty query + ArrowDown: browse preview HTML, or null when there is nothing to show. */
|
|
518
|
+
function emptyBrowsePreviewInnerHtml(scope, filePathLabel, mutable) {
|
|
519
|
+
const hitCtx = {
|
|
520
|
+
currentCommentrayPath: mutable.commentrayPathLabel,
|
|
521
|
+
currentSourcePath: filePathLabel,
|
|
522
|
+
};
|
|
523
|
+
if (scope === "full") {
|
|
524
|
+
const sp = filePathLabel.trim();
|
|
525
|
+
if (sp.length === 0)
|
|
526
|
+
return null;
|
|
527
|
+
const rows = [
|
|
528
|
+
{ sourcePath: sp, commentrayPath: mutable.commentrayPathLabel.trim() },
|
|
529
|
+
];
|
|
530
|
+
const hint = emptyBrowsePreviewHint("full", rows.length, rows.length, false);
|
|
531
|
+
return emptySearchBrowsePreviewInnerHtml(hint, rows, hitCtx);
|
|
532
|
+
}
|
|
533
|
+
const { rows, totalUnique } = uniqueSourceFilePreviewRows(mutable.documentedPairs);
|
|
534
|
+
if (rows.length > 0) {
|
|
535
|
+
const hint = emptyBrowsePreviewHint("commentray-and-paths", rows.length, totalUnique, false);
|
|
536
|
+
return emptySearchBrowsePreviewInnerHtml(hint, rows, hitCtx);
|
|
537
|
+
}
|
|
538
|
+
const sp = filePathLabel.trim();
|
|
539
|
+
if (sp.length === 0)
|
|
540
|
+
return null;
|
|
541
|
+
const fb = [
|
|
542
|
+
{ sourcePath: sp, commentrayPath: mutable.commentrayPathLabel.trim() },
|
|
543
|
+
];
|
|
544
|
+
const hint = emptyBrowsePreviewHint("commentray-and-paths", fb.length, fb.length, true);
|
|
545
|
+
return emptySearchBrowsePreviewInnerHtml(hint, fb, hitCtx);
|
|
546
|
+
}
|
|
276
547
|
function wireSearchUi(ctx) {
|
|
277
548
|
const { scope, filePathLabel, mutable, rawCode, searchInput, searchClear, searchResults, docScrollEl, } = ctx;
|
|
278
549
|
let debounceTimer;
|
|
@@ -283,6 +554,13 @@ function wireSearchUi(ctx) {
|
|
|
283
554
|
searchResults.innerHTML = "";
|
|
284
555
|
searchResults.hidden = true;
|
|
285
556
|
}
|
|
557
|
+
function renderEmptyBrowsePreview() {
|
|
558
|
+
const html = emptyBrowsePreviewInnerHtml(scope, filePathLabel, mutable);
|
|
559
|
+
if (html === null)
|
|
560
|
+
return;
|
|
561
|
+
searchResults.hidden = false;
|
|
562
|
+
searchResults.innerHTML = html;
|
|
563
|
+
}
|
|
286
564
|
function runSearch() {
|
|
287
565
|
const tokens = tokenizeQuery(searchInput.value);
|
|
288
566
|
if (tokens.length === 0) {
|
|
@@ -308,7 +586,7 @@ function wireSearchUi(ctx) {
|
|
|
308
586
|
currentSourcePath: filePathLabel,
|
|
309
587
|
});
|
|
310
588
|
}
|
|
311
|
-
const hitClickDeps = { mutable, docScrollEl };
|
|
589
|
+
const hitClickDeps = { mutable, docScrollEl, filePathLabel };
|
|
312
590
|
searchResults.addEventListener("click", (ev) => {
|
|
313
591
|
const hit = findSearchHitButton(ev.target, searchResults);
|
|
314
592
|
if (!hit)
|
|
@@ -319,6 +597,14 @@ function wireSearchUi(ctx) {
|
|
|
319
597
|
clearTimeout(debounceTimer);
|
|
320
598
|
debounceTimer = setTimeout(runSearch, 200);
|
|
321
599
|
});
|
|
600
|
+
searchInput.addEventListener("keydown", (e) => {
|
|
601
|
+
if (e.key !== "ArrowDown")
|
|
602
|
+
return;
|
|
603
|
+
if (tokenizeQuery(searchInput.value).length > 0)
|
|
604
|
+
return;
|
|
605
|
+
renderEmptyBrowsePreview();
|
|
606
|
+
e.preventDefault();
|
|
607
|
+
});
|
|
322
608
|
searchClear.addEventListener("click", clearSearch);
|
|
323
609
|
document.addEventListener("keydown", (e) => {
|
|
324
610
|
if (e.key !== "Escape")
|
|
@@ -334,20 +620,51 @@ function wireSearchUi(ctx) {
|
|
|
334
620
|
e.preventDefault();
|
|
335
621
|
});
|
|
336
622
|
}
|
|
337
|
-
|
|
623
|
+
/**
|
|
624
|
+
* After toggling `pre-wrap`, line rows reflow without necessarily resizing the pane’s border box,
|
|
625
|
+
* so gutter block rays and scroll sync must be nudged explicitly.
|
|
626
|
+
*
|
|
627
|
+
* Pass optional `docWrapRoots` (e.g. `#doc-pane` and `#doc-pane-body` in dual layout): the toggle
|
|
628
|
+
* syncs `wrap` on those nodes so commentary fenced blocks and prose honor the control. `#doc-pane-body`
|
|
629
|
+
* is targeted in CSS with an id so rules win over `pre code.hljs` from the highlight.js theme.
|
|
630
|
+
*/
|
|
631
|
+
function wireWrapToggle(storageWrap, codePane, wrapCb, onAfterLayout, ...docWrapRoots) {
|
|
632
|
+
const docTargets = docWrapRoots.filter((el) => el instanceof HTMLElement);
|
|
338
633
|
const wrap = readWebStorageItem(localStorage, storageWrap) === "1";
|
|
339
634
|
wrapCb.checked = wrap;
|
|
340
|
-
if (wrap)
|
|
635
|
+
if (wrap) {
|
|
341
636
|
codePane.classList.add("wrap");
|
|
637
|
+
for (const el of docTargets)
|
|
638
|
+
el.classList.add("wrap");
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
codePane.classList.remove("wrap");
|
|
642
|
+
for (const el of docTargets)
|
|
643
|
+
el.classList.remove("wrap");
|
|
644
|
+
}
|
|
342
645
|
wrapCb.addEventListener("change", () => {
|
|
343
646
|
if (wrapCb.checked) {
|
|
344
647
|
codePane.classList.add("wrap");
|
|
648
|
+
for (const el of docTargets)
|
|
649
|
+
el.classList.add("wrap");
|
|
345
650
|
writeWebStorageItem(localStorage, storageWrap, "1");
|
|
346
651
|
}
|
|
347
652
|
else {
|
|
348
653
|
codePane.classList.remove("wrap");
|
|
654
|
+
for (const el of docTargets)
|
|
655
|
+
el.classList.remove("wrap");
|
|
349
656
|
writeWebStorageItem(localStorage, storageWrap, "0");
|
|
350
657
|
}
|
|
658
|
+
if (!onAfterLayout)
|
|
659
|
+
return;
|
|
660
|
+
queueMicrotask(() => {
|
|
661
|
+
requestAnimationFrame(() => {
|
|
662
|
+
onAfterLayout();
|
|
663
|
+
requestAnimationFrame(() => {
|
|
664
|
+
onAfterLayout();
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
});
|
|
351
668
|
});
|
|
352
669
|
}
|
|
353
670
|
function parseScrollBlockLinksFromShell(b64) {
|
|
@@ -381,111 +698,367 @@ function parseScrollBlockLinksFromShell(b64) {
|
|
|
381
698
|
return [];
|
|
382
699
|
}
|
|
383
700
|
}
|
|
701
|
+
function rootScrollNearDocumentEnd(edgePx = 56) {
|
|
702
|
+
const root = rootScrollingElement();
|
|
703
|
+
const maxY = Math.max(0, root.scrollHeight - root.clientHeight);
|
|
704
|
+
return maxY > 0 && root.scrollTop >= maxY - edgePx;
|
|
705
|
+
}
|
|
384
706
|
function probeCodeLine1FromViewport(codePane) {
|
|
385
|
-
const y = codePane.getBoundingClientRect().top + 2;
|
|
386
707
|
const rows = codePane.querySelectorAll('[id^="code-line-"]');
|
|
708
|
+
if (rows.length === 0)
|
|
709
|
+
return 1;
|
|
710
|
+
if (!paneUsesInternalYScroll(codePane)) {
|
|
711
|
+
if (rootScrollNearDocumentEnd()) {
|
|
712
|
+
const last = rows[rows.length - 1];
|
|
713
|
+
const m = /^code-line-(\d+)$/.exec(last.id);
|
|
714
|
+
if (m)
|
|
715
|
+
return Number(m[1]) + 1;
|
|
716
|
+
return rows.length;
|
|
717
|
+
}
|
|
718
|
+
const sr = codePane.getBoundingClientRect();
|
|
719
|
+
const vh = globalThis.innerHeight;
|
|
720
|
+
const clipT = Math.max(0, sr.top);
|
|
721
|
+
const clipB = Math.min(vh, sr.bottom);
|
|
722
|
+
const y = clipT + Math.max(2, Math.min(40, (clipB - clipT) * 0.15));
|
|
723
|
+
for (const el of rows) {
|
|
724
|
+
const r = el.getBoundingClientRect();
|
|
725
|
+
if (r.bottom > y - 1e-3) {
|
|
726
|
+
const m = /^code-line-(\d+)$/.exec(el.id);
|
|
727
|
+
if (m)
|
|
728
|
+
return Number(m[1]) + 1;
|
|
729
|
+
return 1;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return rows.length;
|
|
733
|
+
}
|
|
734
|
+
const sr = codePane.getBoundingClientRect();
|
|
735
|
+
const y = sr.top + codePane.clientTop + 2;
|
|
387
736
|
for (const el of rows) {
|
|
388
737
|
const r = el.getBoundingClientRect();
|
|
389
|
-
if (r.bottom > y) {
|
|
738
|
+
if (r.bottom > y - 1e-3) {
|
|
390
739
|
const m = /^code-line-(\d+)$/.exec(el.id);
|
|
391
740
|
if (m)
|
|
392
741
|
return Number(m[1]) + 1;
|
|
393
742
|
return 1;
|
|
394
743
|
}
|
|
395
744
|
}
|
|
396
|
-
return rows.length
|
|
745
|
+
return rows.length;
|
|
397
746
|
}
|
|
398
747
|
function probeCommentrayLine0FromDoc(docPane) {
|
|
399
|
-
const y = docPane.getBoundingClientRect().top + 2;
|
|
400
748
|
const anchors = docPane.querySelectorAll(".commentray-block-anchor");
|
|
749
|
+
if (anchors.length === 0)
|
|
750
|
+
return 0;
|
|
751
|
+
if (!paneUsesInternalYScroll(docPane)) {
|
|
752
|
+
if (rootScrollNearDocumentEnd()) {
|
|
753
|
+
const last = anchors[anchors.length - 1];
|
|
754
|
+
const lineAttr = last.getAttribute("data-commentray-line");
|
|
755
|
+
return lineAttr !== null && lineAttr !== "" ? Number(lineAttr) : 0;
|
|
756
|
+
}
|
|
757
|
+
const dr = docPane.getBoundingClientRect();
|
|
758
|
+
const vh = globalThis.innerHeight;
|
|
759
|
+
const clipT = Math.max(0, dr.top);
|
|
760
|
+
const clipB = Math.min(vh, dr.bottom);
|
|
761
|
+
const y = clipT + Math.max(2, Math.min(40, (clipB - clipT) * 0.15));
|
|
762
|
+
let best = 0;
|
|
763
|
+
for (const a of anchors) {
|
|
764
|
+
const lineAttr = a.getAttribute("data-commentray-line");
|
|
765
|
+
if (lineAttr === null || lineAttr === "")
|
|
766
|
+
continue;
|
|
767
|
+
if (a.getBoundingClientRect().top <= y + 1 + 1e-3)
|
|
768
|
+
best = Number(lineAttr);
|
|
769
|
+
else
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
return best;
|
|
773
|
+
}
|
|
774
|
+
const dr = docPane.getBoundingClientRect();
|
|
775
|
+
const y = dr.top + docPane.clientTop + 2;
|
|
401
776
|
let best = 0;
|
|
402
777
|
for (const a of anchors) {
|
|
403
778
|
const lineAttr = a.getAttribute("data-commentray-line");
|
|
404
779
|
if (lineAttr === null || lineAttr === "")
|
|
405
780
|
continue;
|
|
406
|
-
if (a.getBoundingClientRect().top <= y + 1)
|
|
781
|
+
if (a.getBoundingClientRect().top <= y + 1 + 1e-3)
|
|
407
782
|
best = Number(lineAttr);
|
|
408
783
|
else
|
|
409
784
|
break;
|
|
410
785
|
}
|
|
411
786
|
return best;
|
|
412
787
|
}
|
|
788
|
+
/**
|
|
789
|
+
* Programmatic `scrollTop` on the partner pane can emit a `scroll` event after the
|
|
790
|
+
* synchronous `syncing` guard is cleared; that late event would mirror back and
|
|
791
|
+
* jerk the pane the user is scrolling. We arm a short-lived skip on the partner
|
|
792
|
+
* before each sync-driven update, and release one skip after two rAFs if no event
|
|
793
|
+
* consumed it (e.g. `applyScrollTopClamped` no-oped).
|
|
794
|
+
*/
|
|
795
|
+
function armIgnoreNextPaneScrollReaction(armed) {
|
|
796
|
+
armed.n++;
|
|
797
|
+
queueMicrotask(() => {
|
|
798
|
+
requestAnimationFrame(() => {
|
|
799
|
+
requestAnimationFrame(() => {
|
|
800
|
+
armed.n = Math.max(0, armed.n - 1);
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
}
|
|
413
805
|
function wireBidirectionalScroll(codePane, docPane, syncFromCode, syncFromDoc) {
|
|
414
806
|
let syncing = "none";
|
|
807
|
+
const ignoreCodeScrollFromPartnerSync = { n: 0 };
|
|
808
|
+
const ignoreDocScrollFromPartnerSync = { n: 0 };
|
|
415
809
|
codePane.addEventListener("scroll", () => {
|
|
810
|
+
if (ignoreCodeScrollFromPartnerSync.n > 0) {
|
|
811
|
+
ignoreCodeScrollFromPartnerSync.n--;
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
416
814
|
if (syncing === "doc")
|
|
417
815
|
return;
|
|
418
816
|
syncing = "code";
|
|
817
|
+
armIgnoreNextPaneScrollReaction(ignoreDocScrollFromPartnerSync);
|
|
419
818
|
syncFromCode();
|
|
420
819
|
syncing = "none";
|
|
421
820
|
}, { passive: true });
|
|
422
821
|
docPane.addEventListener("scroll", () => {
|
|
822
|
+
if (ignoreDocScrollFromPartnerSync.n > 0) {
|
|
823
|
+
ignoreDocScrollFromPartnerSync.n--;
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
423
826
|
if (syncing === "code")
|
|
424
827
|
return;
|
|
425
828
|
syncing = "doc";
|
|
829
|
+
armIgnoreNextPaneScrollReaction(ignoreCodeScrollFromPartnerSync);
|
|
426
830
|
syncFromDoc();
|
|
427
831
|
syncing = "none";
|
|
428
832
|
}, { passive: true });
|
|
429
833
|
}
|
|
430
834
|
/** Index-backed scroll sync when `data-scroll-block-links-b64` is present; else see proportional fallback. */
|
|
431
835
|
function wireBlockAwareScrollSync(codePane, docPane, getLinks) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
470
|
-
});
|
|
836
|
+
let pendingDocToCode = null;
|
|
837
|
+
let pendingCodeToDoc = null;
|
|
838
|
+
const syncFromCodeToDoc = () => {
|
|
839
|
+
applyCodeToDocFlipPlanImpl(codePane, docPane, buildCodeToDocFlipPlanBlockAware(codePane, docPane, getLinks));
|
|
840
|
+
};
|
|
841
|
+
const syncFromDocToCode = () => {
|
|
842
|
+
applyDocToCodeFlipPlanImpl(codePane, docPane, buildDocToCodeFlipPlanBlockAware(docPane, getLinks));
|
|
843
|
+
};
|
|
844
|
+
const prepareMobileFlipToCode = () => {
|
|
845
|
+
pendingDocToCode = buildDocToCodeFlipPlanBlockAware(docPane, getLinks);
|
|
846
|
+
};
|
|
847
|
+
const finishMobileFlipToCode = () => {
|
|
848
|
+
if (!pendingDocToCode)
|
|
849
|
+
return;
|
|
850
|
+
const p = pendingDocToCode;
|
|
851
|
+
pendingDocToCode = null;
|
|
852
|
+
applyDocToCodeFlipPlanImpl(codePane, docPane, p);
|
|
853
|
+
};
|
|
854
|
+
const prepareMobileFlipToDoc = () => {
|
|
855
|
+
pendingCodeToDoc = buildCodeToDocFlipPlanBlockAware(codePane, docPane, getLinks);
|
|
856
|
+
};
|
|
857
|
+
const finishMobileFlipToDoc = () => {
|
|
858
|
+
if (!pendingCodeToDoc)
|
|
859
|
+
return;
|
|
860
|
+
const p = pendingCodeToDoc;
|
|
861
|
+
pendingCodeToDoc = null;
|
|
862
|
+
applyCodeToDocFlipPlanImpl(codePane, docPane, p);
|
|
863
|
+
};
|
|
864
|
+
wireBidirectionalScroll(codePane, docPane, syncFromCodeToDoc, syncFromDocToCode);
|
|
865
|
+
return {
|
|
866
|
+
syncFromCodeToDoc,
|
|
867
|
+
syncFromDocToCode,
|
|
868
|
+
prepareMobileFlipToCode,
|
|
869
|
+
finishMobileFlipToCode,
|
|
870
|
+
prepareMobileFlipToDoc,
|
|
871
|
+
finishMobileFlipToDoc,
|
|
872
|
+
};
|
|
471
873
|
}
|
|
472
874
|
/** Proportional scroll sync when there is no index-backed block map (GitHub Pages default). */
|
|
473
875
|
function wireProportionalScrollSync(codePane, docPane) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
codePane
|
|
876
|
+
let pendingDocToCode = null;
|
|
877
|
+
let pendingCodeToDoc = null;
|
|
878
|
+
const syncFromCodeToDoc = () => {
|
|
879
|
+
applyCodeToDocFlipPlanImpl(codePane, docPane, buildCodeToDocFlipPlanProportional(codePane));
|
|
880
|
+
};
|
|
881
|
+
const syncFromDocToCode = () => {
|
|
882
|
+
applyDocToCodeFlipPlanImpl(codePane, docPane, buildDocToCodeFlipPlanProportional(docPane));
|
|
883
|
+
};
|
|
884
|
+
const prepareMobileFlipToCode = () => {
|
|
885
|
+
pendingDocToCode = buildDocToCodeFlipPlanProportional(docPane);
|
|
886
|
+
};
|
|
887
|
+
const finishMobileFlipToCode = () => {
|
|
888
|
+
if (!pendingDocToCode)
|
|
889
|
+
return;
|
|
890
|
+
const p = pendingDocToCode;
|
|
891
|
+
pendingDocToCode = null;
|
|
892
|
+
applyDocToCodeFlipPlanImpl(codePane, docPane, p);
|
|
893
|
+
};
|
|
894
|
+
const prepareMobileFlipToDoc = () => {
|
|
895
|
+
pendingCodeToDoc = buildCodeToDocFlipPlanProportional(codePane);
|
|
896
|
+
};
|
|
897
|
+
const finishMobileFlipToDoc = () => {
|
|
898
|
+
if (!pendingCodeToDoc)
|
|
899
|
+
return;
|
|
900
|
+
const p = pendingCodeToDoc;
|
|
901
|
+
pendingCodeToDoc = null;
|
|
902
|
+
applyCodeToDocFlipPlanImpl(codePane, docPane, p);
|
|
903
|
+
};
|
|
904
|
+
wireBidirectionalScroll(codePane, docPane, syncFromCodeToDoc, syncFromDocToCode);
|
|
905
|
+
return {
|
|
906
|
+
syncFromCodeToDoc,
|
|
907
|
+
syncFromDocToCode,
|
|
908
|
+
prepareMobileFlipToCode,
|
|
909
|
+
finishMobileFlipToCode,
|
|
910
|
+
prepareMobileFlipToDoc,
|
|
911
|
+
finishMobileFlipToDoc,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
function centerYInViewport(el) {
|
|
915
|
+
const r = el.getBoundingClientRect();
|
|
916
|
+
return (r.top + r.bottom) / 2;
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Vertical anchor for gutter rays on the source side. Must match the **numbered row** the reader
|
|
920
|
+
* sees: the full `.code-line` row (line number + highlighted code) shares one grid row with
|
|
921
|
+
* aligned line-heights. Measuring only `pre code` can shift Y (hljs spans, sub-pixel layout) so
|
|
922
|
+
* rays sit above the line labels; the row’s geometric center tracks `lines:a-b` anchors reliably.
|
|
923
|
+
*/
|
|
924
|
+
function codeLineHighlightCenterYViewport(lineEl) {
|
|
925
|
+
return centerYInViewport(lineEl);
|
|
926
|
+
}
|
|
927
|
+
function commentaryBandEndYViewport(docScrollEl, next, docTop) {
|
|
928
|
+
if (next) {
|
|
929
|
+
const nextEl = document.getElementById(`commentray-block-${next.id}`);
|
|
930
|
+
return nextEl ? nextEl.getBoundingClientRect().top - 3 : centerYInViewport(docTop);
|
|
931
|
+
}
|
|
932
|
+
const dr = docScrollEl.getBoundingClientRect();
|
|
933
|
+
let bottom = dr.bottom - 4;
|
|
934
|
+
const lastKid = docScrollEl.children[docScrollEl.children.length - 1];
|
|
935
|
+
if (lastKid)
|
|
936
|
+
bottom = Math.min(bottom, lastKid.getBoundingClientRect().bottom - 4);
|
|
937
|
+
return bottom;
|
|
938
|
+
}
|
|
939
|
+
function subscribeBlockRayRedraw(gutter, codePane, docScrollEl, scheduleDraw) {
|
|
940
|
+
const onScrollOrResize = () => scheduleDraw();
|
|
941
|
+
codePane.addEventListener("scroll", onScrollOrResize, { passive: true });
|
|
942
|
+
docScrollEl.addEventListener("scroll", onScrollOrResize, { passive: true });
|
|
943
|
+
globalThis.addEventListener("resize", onScrollOrResize);
|
|
944
|
+
const ro = new ResizeObserver(onScrollOrResize);
|
|
945
|
+
ro.observe(gutter);
|
|
946
|
+
ro.observe(codePane);
|
|
947
|
+
ro.observe(docScrollEl);
|
|
948
|
+
const shell = gutter.parentElement;
|
|
949
|
+
if (shell)
|
|
950
|
+
ro.observe(shell);
|
|
951
|
+
}
|
|
952
|
+
function drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSourceLine1Based) {
|
|
953
|
+
const links = getLinks();
|
|
954
|
+
const sorted = sortBlockLinksBySource(links);
|
|
955
|
+
const gutterRect = gutter.getBoundingClientRect();
|
|
956
|
+
const w = gutterRect.width;
|
|
957
|
+
const h = gutterRect.height;
|
|
958
|
+
if (w <= 0 || h <= 0 || sorted.length === 0) {
|
|
959
|
+
svg.replaceChildren();
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
const activeId = activeBlockIdForViewport(links, probeTopSourceLine1Based());
|
|
963
|
+
svg.setAttribute("viewBox", `0 0 ${String(w)} ${String(h)}`);
|
|
964
|
+
svg.setAttribute("preserveAspectRatio", "none");
|
|
965
|
+
const parts = [];
|
|
966
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
967
|
+
const link = sorted[i];
|
|
968
|
+
if (!link)
|
|
969
|
+
continue;
|
|
970
|
+
const next = sorted[i + 1];
|
|
971
|
+
const i0 = codeLineDomIndex0(link.sourceStart);
|
|
972
|
+
const i1 = codeLineDomIndex0(link.sourceEnd);
|
|
973
|
+
const codeTop = document.getElementById(`code-line-${String(i0)}`);
|
|
974
|
+
const codeBot = document.getElementById(`code-line-${String(i1)}`);
|
|
975
|
+
const docTop = document.getElementById(`commentray-block-${link.id}`);
|
|
976
|
+
if (!codeTop || !codeBot || !docTop)
|
|
977
|
+
continue;
|
|
978
|
+
const docEndYViewport = commentaryBandEndYViewport(docScrollEl, next, docTop);
|
|
979
|
+
const yCodeTop = codeLineHighlightCenterYViewport(codeTop);
|
|
980
|
+
const yCodeBot = codeLineHighlightCenterYViewport(codeBot);
|
|
981
|
+
const yDocTop = docTop.getBoundingClientRect().top + 2;
|
|
982
|
+
const yDocEnd = Math.max(docEndYViewport, yDocTop + 4);
|
|
983
|
+
const c0 = clampViewportYToGutterLocal(yCodeTop, gutterRect.top, h);
|
|
984
|
+
const c1 = clampViewportYToGutterLocal(yDocTop, gutterRect.top, h);
|
|
985
|
+
const c2 = clampViewportYToGutterLocal(yCodeBot, gutterRect.top, h);
|
|
986
|
+
const c3 = clampViewportYToGutterLocal(yDocEnd, gutterRect.top, h);
|
|
987
|
+
const strokeClass = link.id === activeId ? "gutter__rays-path gutter__rays-path--active" : "gutter__rays-path";
|
|
988
|
+
const trailClass = `${strokeClass} gutter__rays-path--trail`;
|
|
989
|
+
const topPaths = gutterRayBezierPaths(0, c0.y, w, c1.y, {
|
|
990
|
+
tension: 0.38,
|
|
991
|
+
clipStart: c0.clipped,
|
|
992
|
+
clipEnd: c1.clipped,
|
|
993
|
+
});
|
|
994
|
+
const botPaths = gutterRayBezierPaths(0, c2.y, w, c3.y, {
|
|
995
|
+
tension: 0.38,
|
|
996
|
+
clipStart: c2.clipped,
|
|
997
|
+
clipEnd: c3.clipped,
|
|
998
|
+
});
|
|
999
|
+
const topExtra = topPaths.dotted ? `<path class="${trailClass}" d="${topPaths.dotted}" />` : "";
|
|
1000
|
+
const botExtra = botPaths.dotted ? `<path class="${trailClass}" d="${botPaths.dotted}" />` : "";
|
|
1001
|
+
parts.push(`<g class="gutter__rays-block" data-commentray-block="${escapeHtmlText(link.id)}">` +
|
|
1002
|
+
`<path class="${strokeClass}" d="${topPaths.solid}" />` +
|
|
1003
|
+
topExtra +
|
|
1004
|
+
`<path class="${strokeClass}" d="${botPaths.solid}" />` +
|
|
1005
|
+
botExtra +
|
|
1006
|
+
`</g>`);
|
|
1007
|
+
}
|
|
1008
|
+
svg.innerHTML = parts.join("");
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Splines in the gutter between each block’s source range and its commentary band (dual pane,
|
|
1012
|
+
* index-backed blocks). Emphasizes the block aligned with the current source viewport; clamps
|
|
1013
|
+
* off-screen endpoints so readers see which way to scroll.
|
|
1014
|
+
*
|
|
1015
|
+
* @returns Request a redraw after DOM changes that do not resize the panes (e.g. multi-angle body swap).
|
|
1016
|
+
*/
|
|
1017
|
+
function wireBlockRayConnectors(args) {
|
|
1018
|
+
const { gutter, codePane, docScrollEl, getLinks, probeTopSourceLine1Based } = args;
|
|
1019
|
+
const svgNs = "http://www.w3.org/2000/svg";
|
|
1020
|
+
const host = document.createElement("div");
|
|
1021
|
+
host.className = "gutter__rays";
|
|
1022
|
+
host.setAttribute("aria-hidden", "true");
|
|
1023
|
+
const svg = document.createElementNS(svgNs, "svg");
|
|
1024
|
+
host.appendChild(svg);
|
|
1025
|
+
gutter.appendChild(host);
|
|
1026
|
+
let raf = 0;
|
|
1027
|
+
function scheduleDraw() {
|
|
1028
|
+
if (raf !== 0)
|
|
1029
|
+
return;
|
|
1030
|
+
raf = globalThis.requestAnimationFrame(() => {
|
|
1031
|
+
raf = 0;
|
|
1032
|
+
drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSourceLine1Based);
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
subscribeBlockRayRedraw(gutter, codePane, docScrollEl, scheduleDraw);
|
|
1036
|
+
scheduleDraw();
|
|
1037
|
+
/** First paint can report gutter height 0 before flex layout settles; redraw after layout. */
|
|
1038
|
+
globalThis.requestAnimationFrame(() => {
|
|
1039
|
+
scheduleDraw();
|
|
1040
|
+
globalThis.requestAnimationFrame(scheduleDraw);
|
|
478
1041
|
});
|
|
1042
|
+
return scheduleDraw;
|
|
479
1043
|
}
|
|
480
1044
|
function isDocumentedPairNav(x) {
|
|
481
1045
|
if (typeof x !== "object" || x === null)
|
|
482
1046
|
return false;
|
|
483
1047
|
const o = x;
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
1048
|
+
if (typeof o.sourcePath !== "string" || typeof o.commentrayPath !== "string")
|
|
1049
|
+
return false;
|
|
1050
|
+
if (o.staticBrowseUrl !== undefined && typeof o.staticBrowseUrl !== "string")
|
|
1051
|
+
return false;
|
|
1052
|
+
const sg = o.sourceOnGithub;
|
|
1053
|
+
const cg = o.commentrayOnGithub;
|
|
1054
|
+
const hasSg = typeof sg === "string";
|
|
1055
|
+
const hasCg = typeof cg === "string";
|
|
1056
|
+
if (hasSg !== hasCg)
|
|
1057
|
+
return false;
|
|
1058
|
+
const browseOk = typeof o.staticBrowseUrl === "string" && o.staticBrowseUrl.trim().length > 0;
|
|
1059
|
+
if (!browseOk && !hasSg)
|
|
1060
|
+
return false;
|
|
1061
|
+
return true;
|
|
489
1062
|
}
|
|
490
1063
|
function pairsFromJsonArray(raw) {
|
|
491
1064
|
const pairs = [];
|
|
@@ -497,27 +1070,6 @@ function pairsFromJsonArray(raw) {
|
|
|
497
1070
|
}
|
|
498
1071
|
return pairs;
|
|
499
1072
|
}
|
|
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
1073
|
function commentrayLineRowFromNavJson(r) {
|
|
522
1074
|
if (r.kind !== "commentrayLine")
|
|
523
1075
|
return null;
|
|
@@ -612,6 +1164,25 @@ function treeFileLinkLabel(pr, disambiguate) {
|
|
|
612
1164
|
const stem = companionDocStem(pr.commentrayPath);
|
|
613
1165
|
return stem !== "" && stem !== base ? `${base} · ${stem}` : base;
|
|
614
1166
|
}
|
|
1167
|
+
/** Prefer static hub browse page; optional SCM blob URL when the export has no `staticBrowseUrl`. */
|
|
1168
|
+
function treeFileLinkHref(pr) {
|
|
1169
|
+
const browse = (pr.staticBrowseUrl ?? "").trim();
|
|
1170
|
+
if (browse.length > 0) {
|
|
1171
|
+
return resolveStaticBrowseHref(browse, globalThis.location.pathname, globalThis.location.origin);
|
|
1172
|
+
}
|
|
1173
|
+
const gh = (pr.commentrayOnGithub ?? "").trim();
|
|
1174
|
+
return gh.length > 0 ? gh : "#";
|
|
1175
|
+
}
|
|
1176
|
+
function treeFileLinkTitle(pr) {
|
|
1177
|
+
const browse = (pr.staticBrowseUrl ?? "").trim();
|
|
1178
|
+
if (browse.length > 0) {
|
|
1179
|
+
return `${pr.sourcePath} — open this pair in the site viewer`;
|
|
1180
|
+
}
|
|
1181
|
+
if ((pr.commentrayOnGithub ?? "").trim().length > 0) {
|
|
1182
|
+
return `${pr.sourcePath} — open companion commentray on the repository host`;
|
|
1183
|
+
}
|
|
1184
|
+
return pr.sourcePath;
|
|
1185
|
+
}
|
|
615
1186
|
function renderDocumentedTreeHtml(node) {
|
|
616
1187
|
const keys = [...node.children.keys()].sort((a, b) => a.localeCompare(b));
|
|
617
1188
|
if (keys.length === 0)
|
|
@@ -629,19 +1200,23 @@ function renderDocumentedTreeHtml(node) {
|
|
|
629
1200
|
const multi = ch.pairs.length > 1;
|
|
630
1201
|
for (const pr of ch.pairs) {
|
|
631
1202
|
const label = escapeHtmlText(treeFileLinkLabel(pr, multi));
|
|
632
|
-
const title = escapeHtmlText(
|
|
1203
|
+
const title = escapeHtmlText(treeFileLinkTitle(pr));
|
|
1204
|
+
const href = escapeHtmlText(treeFileLinkHref(pr));
|
|
1205
|
+
const useSiteBrowse = (pr.staticBrowseUrl?.trim() ?? "").length > 0;
|
|
1206
|
+
const external = useSiteBrowse ? "" : ' target="_blank" rel="noopener noreferrer"';
|
|
633
1207
|
lis.push(`<li><div class="tree-file">` +
|
|
634
|
-
`<a class="tree-file-link" href="${
|
|
1208
|
+
`<a class="tree-file-link" href="${href}"${external} title="${title}">${label}</a>` +
|
|
635
1209
|
`</div></li>`);
|
|
636
1210
|
}
|
|
637
1211
|
}
|
|
638
1212
|
}
|
|
639
1213
|
return `<ul>${lis.join("")}</ul>`;
|
|
640
1214
|
}
|
|
641
|
-
function renderDocumentedPairsIntoHost(treeHost, pairs) {
|
|
1215
|
+
function renderDocumentedPairsIntoHost(treeHost, pairs, emptyBecauseFilter) {
|
|
642
1216
|
if (pairs.length === 0) {
|
|
643
|
-
treeHost.innerHTML =
|
|
644
|
-
'<p class="nav-rail__doc-hub-hint" role="status">No
|
|
1217
|
+
treeHost.innerHTML = emptyBecauseFilter
|
|
1218
|
+
? '<p class="nav-rail__doc-hub-hint" role="status">No paths match this filter.</p>'
|
|
1219
|
+
: '<p class="nav-rail__doc-hub-hint" role="status">No documented pairs in this export.</p>';
|
|
645
1220
|
return;
|
|
646
1221
|
}
|
|
647
1222
|
const root = { children: new Map(), pairs: [] };
|
|
@@ -677,9 +1252,56 @@ function loadDocumentedPairs(jsonUrl, embeddedB64) {
|
|
|
677
1252
|
return loaded ?? [];
|
|
678
1253
|
};
|
|
679
1254
|
}
|
|
1255
|
+
/**
|
|
1256
|
+
* On narrow viewports the toolbar strip uses horizontal overflow; absolutely positioned
|
|
1257
|
+
* `.nav-rail__doc-hub-inner` is clipped. Pin the panel with `position: fixed` while open.
|
|
1258
|
+
*/
|
|
1259
|
+
function wireDocumentedFilesTreeMobileFlyout(hub) {
|
|
1260
|
+
const innerCandidate = hub.querySelector(".nav-rail__doc-hub-inner");
|
|
1261
|
+
if (!(innerCandidate instanceof HTMLElement)) {
|
|
1262
|
+
return () => { };
|
|
1263
|
+
}
|
|
1264
|
+
const flyoutInner = innerCandidate;
|
|
1265
|
+
const mq = globalThis.matchMedia("(max-width: 767px)");
|
|
1266
|
+
function summaryEl() {
|
|
1267
|
+
const s = hub.querySelector("summary");
|
|
1268
|
+
return s instanceof HTMLElement ? s : null;
|
|
1269
|
+
}
|
|
1270
|
+
function placeFlyout() {
|
|
1271
|
+
if (!hub.open || !mq.matches) {
|
|
1272
|
+
flyoutInner.style.removeProperty("position");
|
|
1273
|
+
flyoutInner.style.removeProperty("top");
|
|
1274
|
+
flyoutInner.style.removeProperty("left");
|
|
1275
|
+
flyoutInner.style.removeProperty("right");
|
|
1276
|
+
flyoutInner.style.removeProperty("width");
|
|
1277
|
+
flyoutInner.style.removeProperty("max-width");
|
|
1278
|
+
flyoutInner.style.removeProperty("max-height");
|
|
1279
|
+
flyoutInner.style.removeProperty("z-index");
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
const sum = summaryEl();
|
|
1283
|
+
if (!sum)
|
|
1284
|
+
return;
|
|
1285
|
+
const r = sum.getBoundingClientRect();
|
|
1286
|
+
const pad = 8;
|
|
1287
|
+
flyoutInner.style.position = "fixed";
|
|
1288
|
+
flyoutInner.style.top = `${String(Math.round(r.bottom + 4))}px`;
|
|
1289
|
+
flyoutInner.style.left = `${String(Math.round(pad))}px`;
|
|
1290
|
+
flyoutInner.style.right = `${String(Math.round(pad))}px`;
|
|
1291
|
+
flyoutInner.style.width = "auto";
|
|
1292
|
+
flyoutInner.style.maxWidth = "none";
|
|
1293
|
+
flyoutInner.style.maxHeight = "min(52vh, 400px)";
|
|
1294
|
+
flyoutInner.style.zIndex = "200";
|
|
1295
|
+
}
|
|
1296
|
+
mq.addEventListener("change", placeFlyout);
|
|
1297
|
+
globalThis.addEventListener("resize", placeFlyout);
|
|
1298
|
+
globalThis.addEventListener("scroll", placeFlyout, true);
|
|
1299
|
+
return placeFlyout;
|
|
1300
|
+
}
|
|
680
1301
|
function wireDocumentedFilesTree() {
|
|
681
1302
|
const hub = document.getElementById("documented-files-hub");
|
|
682
1303
|
const treeHost = document.getElementById("documented-files-tree");
|
|
1304
|
+
const filterInput = document.getElementById("documented-files-filter");
|
|
683
1305
|
const shell = document.getElementById("shell");
|
|
684
1306
|
if (!(hub instanceof HTMLDetailsElement) || !(treeHost instanceof HTMLElement)) {
|
|
685
1307
|
return;
|
|
@@ -689,22 +1311,45 @@ function wireDocumentedFilesTree() {
|
|
|
689
1311
|
const embeddedB64 = shell?.getAttribute("data-documented-pairs-b64")?.trim() ?? "";
|
|
690
1312
|
if (jsonUrl.length === 0 && embeddedB64.length === 0)
|
|
691
1313
|
return;
|
|
1314
|
+
const placeDocHubFlyout = wireDocumentedFilesTreeMobileFlyout(hub);
|
|
692
1315
|
const ensureLoaded = loadDocumentedPairs(jsonUrl, embeddedB64);
|
|
1316
|
+
let cachedPairs = null;
|
|
1317
|
+
function applyFilterAndRender() {
|
|
1318
|
+
if (cachedPairs === null)
|
|
1319
|
+
return;
|
|
1320
|
+
const q = filterInput instanceof HTMLInputElement ? filterInput.value : "";
|
|
1321
|
+
const pairs = filterPairsByDocumentedTreeQuery(cachedPairs, q);
|
|
1322
|
+
const filterActive = q.trim().length > 0;
|
|
1323
|
+
renderDocumentedPairsIntoHost(treeMount, pairs, filterActive && cachedPairs.length > 0 && pairs.length === 0);
|
|
1324
|
+
}
|
|
693
1325
|
async function hydrateTree() {
|
|
694
1326
|
try {
|
|
695
1327
|
const pairs = await ensureLoaded();
|
|
696
|
-
|
|
1328
|
+
cachedPairs = pairs;
|
|
1329
|
+
applyFilterAndRender();
|
|
697
1330
|
}
|
|
698
1331
|
catch {
|
|
1332
|
+
cachedPairs = null;
|
|
699
1333
|
treeMount.innerHTML =
|
|
700
1334
|
'<p class="nav-rail__doc-hub-hint" role="alert">Could not load the file list.</p>';
|
|
701
1335
|
}
|
|
702
1336
|
}
|
|
703
1337
|
hub.addEventListener("toggle", () => {
|
|
1338
|
+
placeDocHubFlyout();
|
|
1339
|
+
if (hub.open) {
|
|
1340
|
+
globalThis.requestAnimationFrame(placeDocHubFlyout);
|
|
1341
|
+
}
|
|
704
1342
|
if (!hub.open)
|
|
705
1343
|
return;
|
|
706
1344
|
void hydrateTree();
|
|
707
1345
|
});
|
|
1346
|
+
if (filterInput instanceof HTMLInputElement) {
|
|
1347
|
+
filterInput.addEventListener("input", () => {
|
|
1348
|
+
if (!hub.open || cachedPairs === null)
|
|
1349
|
+
return;
|
|
1350
|
+
applyFilterAndRender();
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
708
1353
|
}
|
|
709
1354
|
function wireSplitter(storageSplit, shell, codePane, gutter, initialPct) {
|
|
710
1355
|
let dragging = false;
|
|
@@ -717,6 +1362,7 @@ function wireSplitter(storageSplit, shell, codePane, gutter, initialPct) {
|
|
|
717
1362
|
const p = clamp((x / rect.width) * 100, 15, 85);
|
|
718
1363
|
lastPct = p;
|
|
719
1364
|
codePane.style.flex = `0 0 ${p}%`;
|
|
1365
|
+
shell.style.setProperty("--split-pct", `${String(p)}%`);
|
|
720
1366
|
}
|
|
721
1367
|
function stop() {
|
|
722
1368
|
dragging = false;
|
|
@@ -737,10 +1383,118 @@ function wireSplitter(storageSplit, shell, codePane, gutter, initialPct) {
|
|
|
737
1383
|
}
|
|
738
1384
|
const STORAGE_SPLIT_PCT = "commentray.codeCommentrayStatic.splitPct";
|
|
739
1385
|
const STORAGE_WRAP_LINES = "commentray.codeCommentrayStatic.wrap";
|
|
1386
|
+
const STORAGE_DUAL_MOBILE_PANE = "commentray.codeCommentrayStatic.dualMobilePane";
|
|
1387
|
+
/** Matches `code-browser.ts` `@media (max-width: 767px)` (dual column from 768px up). */
|
|
1388
|
+
const DUAL_MOBILE_SINGLE_PANE_MQ = "(max-width: 767px)";
|
|
1389
|
+
function normalizedDualMobilePane(v) {
|
|
1390
|
+
return v === "code" ? "code" : "doc";
|
|
1391
|
+
}
|
|
1392
|
+
/** When the commentary pane is visible, (re)run Mermaid so diagrams are not laid out under display:none. */
|
|
1393
|
+
function scheduleMermaidWhenDualDocPaneVisible(shell, mq) {
|
|
1394
|
+
const kick = () => {
|
|
1395
|
+
if (shell.getAttribute("data-layout") !== "dual")
|
|
1396
|
+
return;
|
|
1397
|
+
if (!mq.matches)
|
|
1398
|
+
return;
|
|
1399
|
+
if (normalizedDualMobilePane(shell.getAttribute("data-dual-mobile-pane")) !== "doc")
|
|
1400
|
+
return;
|
|
1401
|
+
const docBody = document.getElementById("doc-pane-body");
|
|
1402
|
+
if (!(docBody instanceof HTMLElement))
|
|
1403
|
+
return;
|
|
1404
|
+
runMermaidOnFreshDocNodes(docBody);
|
|
1405
|
+
};
|
|
1406
|
+
queueMicrotask(() => {
|
|
1407
|
+
kick();
|
|
1408
|
+
requestAnimationFrame(() => {
|
|
1409
|
+
kick();
|
|
1410
|
+
requestAnimationFrame(kick);
|
|
1411
|
+
});
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
function wireDualMobilePaneFlipScrollAffordance(primaryFlip, scrollFlip, mq) {
|
|
1415
|
+
const hideScroll = () => {
|
|
1416
|
+
scrollFlip.hidden = true;
|
|
1417
|
+
scrollFlip.classList.remove("is-visible");
|
|
1418
|
+
};
|
|
1419
|
+
const showScroll = () => {
|
|
1420
|
+
scrollFlip.hidden = false;
|
|
1421
|
+
scrollFlip.classList.add("is-visible");
|
|
1422
|
+
};
|
|
1423
|
+
/** Prefer geometry over IntersectionObserver: a sliver “intersecting” the viewport is still unusable. */
|
|
1424
|
+
const tick = () => {
|
|
1425
|
+
if (!mq.matches) {
|
|
1426
|
+
hideScroll();
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
const r = primaryFlip.getBoundingClientRect();
|
|
1430
|
+
const vh = globalThis.innerHeight;
|
|
1431
|
+
const margin = 10;
|
|
1432
|
+
const offScreen = r.bottom < margin || r.top > vh - margin;
|
|
1433
|
+
if (offScreen)
|
|
1434
|
+
showScroll();
|
|
1435
|
+
else
|
|
1436
|
+
hideScroll();
|
|
1437
|
+
};
|
|
1438
|
+
globalThis.addEventListener("scroll", tick, { passive: true });
|
|
1439
|
+
globalThis.addEventListener("resize", tick, { passive: true });
|
|
1440
|
+
mq.addEventListener("change", tick);
|
|
1441
|
+
globalThis.requestAnimationFrame(tick);
|
|
1442
|
+
}
|
|
1443
|
+
function wireDualMobilePaneFlip(shell, flipBtn, scrollRunners, flipScrollBtn) {
|
|
1444
|
+
const mq = globalThis.matchMedia(DUAL_MOBILE_SINGLE_PANE_MQ);
|
|
1445
|
+
function readStoredPane() {
|
|
1446
|
+
return normalizedDualMobilePane(readWebStorageItem(localStorage, STORAGE_DUAL_MOBILE_PANE));
|
|
1447
|
+
}
|
|
1448
|
+
function applyForViewport() {
|
|
1449
|
+
if (mq.matches) {
|
|
1450
|
+
shell.setAttribute("data-dual-mobile-pane", readStoredPane());
|
|
1451
|
+
}
|
|
1452
|
+
else {
|
|
1453
|
+
shell.removeAttribute("data-dual-mobile-pane");
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
const runFlip = () => {
|
|
1457
|
+
if (!mq.matches)
|
|
1458
|
+
return;
|
|
1459
|
+
const cur = normalizedDualMobilePane(shell.getAttribute("data-dual-mobile-pane"));
|
|
1460
|
+
const next = cur === "code" ? "doc" : "code";
|
|
1461
|
+
if (next === "code") {
|
|
1462
|
+
scrollRunners.prepareMobileFlipToCode();
|
|
1463
|
+
}
|
|
1464
|
+
else {
|
|
1465
|
+
scrollRunners.prepareMobileFlipToDoc();
|
|
1466
|
+
}
|
|
1467
|
+
shell.setAttribute("data-dual-mobile-pane", next);
|
|
1468
|
+
writeWebStorageItem(localStorage, STORAGE_DUAL_MOBILE_PANE, next);
|
|
1469
|
+
globalThis.requestAnimationFrame(() => {
|
|
1470
|
+
globalThis.requestAnimationFrame(() => {
|
|
1471
|
+
if (next === "code") {
|
|
1472
|
+
scrollRunners.finishMobileFlipToCode();
|
|
1473
|
+
}
|
|
1474
|
+
else {
|
|
1475
|
+
scrollRunners.finishMobileFlipToDoc();
|
|
1476
|
+
}
|
|
1477
|
+
});
|
|
1478
|
+
});
|
|
1479
|
+
// Only here (not on every viewport apply): avoids redundant Mermaid passes on load/resize for the default commentary-first shell.
|
|
1480
|
+
if (next === "doc") {
|
|
1481
|
+
scheduleMermaidWhenDualDocPaneVisible(shell, mq);
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
flipBtn.addEventListener("click", runFlip);
|
|
1485
|
+
if (flipScrollBtn) {
|
|
1486
|
+
flipScrollBtn.addEventListener("click", runFlip);
|
|
1487
|
+
wireDualMobilePaneFlipScrollAffordance(flipBtn, flipScrollBtn, mq);
|
|
1488
|
+
}
|
|
1489
|
+
mq.addEventListener("change", applyForViewport);
|
|
1490
|
+
applyForViewport();
|
|
1491
|
+
}
|
|
740
1492
|
function wireStretchLayoutChrome(codePane) {
|
|
741
1493
|
const wrapCb = document.getElementById("wrap-lines");
|
|
742
1494
|
if (wrapCb) {
|
|
743
|
-
wireWrapToggle(STORAGE_WRAP_LINES, codePane, wrapCb)
|
|
1495
|
+
wireWrapToggle(STORAGE_WRAP_LINES, codePane, wrapCb, () => {
|
|
1496
|
+
globalThis.dispatchEvent(new Event("resize"));
|
|
1497
|
+
});
|
|
744
1498
|
}
|
|
745
1499
|
}
|
|
746
1500
|
function parseMultiAnglePayload(script) {
|
|
@@ -805,7 +1559,46 @@ function initialCommentrayScopePathState(shell, scope, filePathLabel, commentray
|
|
|
805
1559
|
: [filePathLabel, commentrayPathLabel].filter((s) => s.trim().length > 0).join("\n");
|
|
806
1560
|
return { documentedPairs, pathRowsForOrdering, pathBlobWide };
|
|
807
1561
|
}
|
|
808
|
-
|
|
1562
|
+
/**
|
|
1563
|
+
* Fetched `commentray-nav-search.json` sometimes omits `staticBrowseUrl` on pairs; the hub embed
|
|
1564
|
+
* carries browse URLs from the same build — merge so search hits open `_site/browse/…`, not GitHub.
|
|
1565
|
+
*/
|
|
1566
|
+
function mergeFetchedDocumentedPairsWithEmbeddedBrowse(embedded, fetched) {
|
|
1567
|
+
if (fetched.length === 0)
|
|
1568
|
+
return embedded;
|
|
1569
|
+
if (embedded.length === 0)
|
|
1570
|
+
return fetched;
|
|
1571
|
+
const browseByCr = new Map();
|
|
1572
|
+
for (const p of embedded) {
|
|
1573
|
+
const b = (p.staticBrowseUrl ?? "").trim();
|
|
1574
|
+
if (b.length === 0)
|
|
1575
|
+
continue;
|
|
1576
|
+
browseByCr.set(normPosixPath(p.commentrayPath), b);
|
|
1577
|
+
}
|
|
1578
|
+
return fetched.map((p) => {
|
|
1579
|
+
const have = (p.staticBrowseUrl ?? "").trim();
|
|
1580
|
+
if (have.length > 0)
|
|
1581
|
+
return p;
|
|
1582
|
+
const fromEmb = browseByCr.get(normPosixPath(p.commentrayPath));
|
|
1583
|
+
if (fromEmb !== undefined && fromEmb.length > 0) {
|
|
1584
|
+
return { ...p, staticBrowseUrl: fromEmb };
|
|
1585
|
+
}
|
|
1586
|
+
return p;
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
function resolvedNavSearchJsonUrl(shell) {
|
|
1590
|
+
const raw = shell.getAttribute("data-nav-search-json-url")?.trim() ?? "";
|
|
1591
|
+
if (raw.length === 0)
|
|
1592
|
+
return "";
|
|
1593
|
+
try {
|
|
1594
|
+
return new URL(raw, globalThis.location.href).href;
|
|
1595
|
+
}
|
|
1596
|
+
catch {
|
|
1597
|
+
return raw;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
function wireDualPaneNavSearchFetch(shell, embeddedPairs, indexState, mutable, rebuildSearcher, searchInput) {
|
|
1601
|
+
const navSearchUrl = resolvedNavSearchJsonUrl(shell);
|
|
809
1602
|
if (navSearchUrl.length === 0)
|
|
810
1603
|
return;
|
|
811
1604
|
void (async () => {
|
|
@@ -815,9 +1608,10 @@ function wireDualPaneNavSearchFetch(navSearchUrl, indexState, mutable, rebuildSe
|
|
|
815
1608
|
return;
|
|
816
1609
|
const doc = (await res.json());
|
|
817
1610
|
const fetched = pairsFromJsonArray(doc.documentedPairs);
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1611
|
+
const mergedPairs = mergeFetchedDocumentedPairsWithEmbeddedBrowse(embeddedPairs, fetched);
|
|
1612
|
+
if (mergedPairs.length > 0) {
|
|
1613
|
+
indexState.documentedPairs = mergedPairs;
|
|
1614
|
+
mutable.documentedPairs = mergedPairs;
|
|
821
1615
|
}
|
|
822
1616
|
const nr = rowsFromNavSearchJson(doc);
|
|
823
1617
|
if (nr.length === 0)
|
|
@@ -837,10 +1631,9 @@ function wireDualPaneNavSearchFetch(navSearchUrl, indexState, mutable, rebuildSe
|
|
|
837
1631
|
})();
|
|
838
1632
|
}
|
|
839
1633
|
function wireDualPaneMultiAngleAndScroll(args) {
|
|
840
|
-
const { codePane, docScrollEl, docBody, shell,
|
|
841
|
-
const activeLinks = { current: scrollLinks };
|
|
1634
|
+
const { codePane, docScrollEl, docBody, shell, scrollLinksRef, multiPayload, mutable, rebuildSearcher, searchInput, searchResults, requestBlockRayRedraw, } = args;
|
|
842
1635
|
if (multiPayload) {
|
|
843
|
-
wireBlockAwareScrollSync(codePane, docScrollEl, () =>
|
|
1636
|
+
const runners = wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinksRef.current);
|
|
844
1637
|
const angleSel = document.getElementById("angle-select");
|
|
845
1638
|
if (angleSel && docBody) {
|
|
846
1639
|
angleSel.addEventListener("change", () => {
|
|
@@ -848,29 +1641,56 @@ function wireDualPaneMultiAngleAndScroll(args) {
|
|
|
848
1641
|
if (!a)
|
|
849
1642
|
return;
|
|
850
1643
|
docBody.innerHTML = decodeBase64Utf8(a.docInnerHtmlB64);
|
|
1644
|
+
runMermaidOnFreshDocNodes(docBody);
|
|
1645
|
+
rewriteHubRelativeBrowseAnchorsIn(docBody);
|
|
851
1646
|
mutable.rawMd = decodeBase64Utf8(a.rawMdB64);
|
|
852
1647
|
mutable.mdLines = mutable.rawMd.split("\n");
|
|
853
1648
|
mutable.commentrayPathLabel = a.commentrayPathForSearch;
|
|
854
1649
|
rebuildSearcher();
|
|
855
|
-
|
|
1650
|
+
scrollLinksRef.current = parseScrollBlockLinksFromShell(a.scrollBlockLinksB64);
|
|
856
1651
|
shell.setAttribute("data-scroll-block-links-b64", a.scrollBlockLinksB64);
|
|
857
1652
|
shell.setAttribute("data-search-commentray-path", a.commentrayPathForSearch);
|
|
858
|
-
const
|
|
859
|
-
if (
|
|
860
|
-
|
|
1653
|
+
const docPathEl = document.getElementById("nav-rail-doc-path");
|
|
1654
|
+
if (docPathEl) {
|
|
1655
|
+
const path = a.commentrayPathForSearch.trim();
|
|
1656
|
+
docPathEl.textContent = path.length > 0 ? path : "—";
|
|
1657
|
+
if (path.length > 0)
|
|
1658
|
+
docPathEl.setAttribute("title", path);
|
|
1659
|
+
else
|
|
1660
|
+
docPathEl.removeAttribute("title");
|
|
1661
|
+
}
|
|
1662
|
+
const browse = a.staticBrowseUrl?.trim() ?? "";
|
|
1663
|
+
if (browse.length > 0) {
|
|
1664
|
+
const resolved = staticBrowseHrefForShellDataAttribute(browse, globalThis.location.pathname, globalThis.location.origin);
|
|
1665
|
+
shell.setAttribute("data-commentray-pair-browse-href", resolved);
|
|
1666
|
+
}
|
|
1667
|
+
else {
|
|
1668
|
+
const ghu = a.commentrayOnGithubUrl?.trim();
|
|
1669
|
+
if (ghu) {
|
|
1670
|
+
shell.setAttribute("data-commentray-pair-browse-href", ghu);
|
|
1671
|
+
}
|
|
1672
|
+
else {
|
|
1673
|
+
shell.removeAttribute("data-commentray-pair-browse-href");
|
|
1674
|
+
}
|
|
861
1675
|
}
|
|
862
1676
|
searchInput.value = "";
|
|
863
1677
|
searchResults.innerHTML = "";
|
|
864
1678
|
searchResults.hidden = true;
|
|
1679
|
+
requestBlockRayRedraw?.();
|
|
1680
|
+
globalThis.requestAnimationFrame(() => {
|
|
1681
|
+
requestBlockRayRedraw?.();
|
|
1682
|
+
globalThis.requestAnimationFrame(() => {
|
|
1683
|
+
requestBlockRayRedraw?.();
|
|
1684
|
+
});
|
|
1685
|
+
});
|
|
865
1686
|
});
|
|
866
1687
|
}
|
|
867
|
-
return;
|
|
1688
|
+
return runners;
|
|
868
1689
|
}
|
|
869
|
-
if (
|
|
870
|
-
wireBlockAwareScrollSync(codePane, docScrollEl, () =>
|
|
871
|
-
return;
|
|
1690
|
+
if (scrollLinksRef.current.length > 0) {
|
|
1691
|
+
return wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinksRef.current);
|
|
872
1692
|
}
|
|
873
|
-
wireProportionalScrollSync(codePane, docScrollEl);
|
|
1693
|
+
return wireProportionalScrollSync(codePane, docScrollEl);
|
|
874
1694
|
}
|
|
875
1695
|
function wireDualPaneCommentrayLocationHash(docScrollEl, mdLineCount) {
|
|
876
1696
|
function applyCommentrayLocationHash() {
|
|
@@ -887,15 +1707,12 @@ function wireDualPaneCommentrayLocationHash(docScrollEl, mdLineCount) {
|
|
|
887
1707
|
globalThis.requestAnimationFrame(applyCommentrayLocationHash);
|
|
888
1708
|
});
|
|
889
1709
|
}
|
|
890
|
-
function
|
|
891
|
-
const dom = readDualPaneDomBundle();
|
|
892
|
-
if (!dom)
|
|
893
|
-
return;
|
|
894
|
-
const { docBody, docScrollEl, gutter, wrapCb, searchInput, searchClear, searchResults } = dom;
|
|
1710
|
+
function buildDualPaneSearcherBundle(shell, codePane) {
|
|
895
1711
|
const { rawCodeB64, rawMdB64 } = readEmbeddedRawB64Strings(shell, codePane);
|
|
896
1712
|
const rawCode = decodeBase64Utf8(rawCodeB64);
|
|
897
1713
|
const rawMd = decodeBase64Utf8(rawMdB64);
|
|
898
1714
|
const scrollLinks = parseScrollBlockLinksFromShell(shell.getAttribute("data-scroll-block-links-b64") || "");
|
|
1715
|
+
const scrollLinksRef = { current: scrollLinks };
|
|
899
1716
|
const { scope, filePathLabel, commentrayPathLabel } = readSearchScopeFromShell(shell);
|
|
900
1717
|
const pathInit = initialCommentrayScopePathState(shell, scope, filePathLabel, commentrayPathLabel);
|
|
901
1718
|
const indexState = {
|
|
@@ -924,40 +1741,184 @@ function wireDualPaneCodeBrowser(shell, codePane) {
|
|
|
924
1741
|
}));
|
|
925
1742
|
}
|
|
926
1743
|
rebuildSearcher();
|
|
927
|
-
|
|
1744
|
+
return {
|
|
1745
|
+
rawCode,
|
|
1746
|
+
rawMd,
|
|
1747
|
+
scrollLinksRef,
|
|
928
1748
|
scope,
|
|
929
1749
|
filePathLabel,
|
|
1750
|
+
commentrayPathLabel,
|
|
1751
|
+
pathInit,
|
|
1752
|
+
indexState,
|
|
930
1753
|
mutable,
|
|
931
|
-
|
|
1754
|
+
rebuildSearcher,
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
function wireDualPaneCodeBrowser(shell, codePane) {
|
|
1758
|
+
const dom = readDualPaneDomBundle();
|
|
1759
|
+
if (!dom)
|
|
1760
|
+
return;
|
|
1761
|
+
const { docBody, docScrollEl, gutter, wrapCb, searchInput, searchClear, searchResults } = dom;
|
|
1762
|
+
const bundle = buildDualPaneSearcherBundle(shell, codePane);
|
|
1763
|
+
rewriteHubRelativeBrowseAnchorsIn(document);
|
|
1764
|
+
wireSearchUi({
|
|
1765
|
+
scope: bundle.scope,
|
|
1766
|
+
filePathLabel: bundle.filePathLabel,
|
|
1767
|
+
mutable: bundle.mutable,
|
|
1768
|
+
rawCode: bundle.rawCode,
|
|
932
1769
|
searchInput,
|
|
933
1770
|
searchClear,
|
|
934
1771
|
searchResults,
|
|
935
1772
|
docScrollEl,
|
|
936
1773
|
});
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
const
|
|
940
|
-
const pct = clamp(Number.isFinite(pct0) ? pct0 : 50, 15, 85);
|
|
1774
|
+
wireDualPaneNavSearchFetch(shell, bundle.pathInit.documentedPairs, bundle.indexState, bundle.mutable, bundle.rebuildSearcher, searchInput);
|
|
1775
|
+
const pct0 = parseFloat(readWebStorageItem(localStorage, STORAGE_SPLIT_PCT) || "46");
|
|
1776
|
+
const pct = clamp(Number.isFinite(pct0) ? pct0 : 46, 15, 85);
|
|
941
1777
|
codePane.style.flex = `0 0 ${pct}%`;
|
|
942
|
-
|
|
1778
|
+
shell.style.setProperty("--split-pct", `${String(pct)}%`);
|
|
1779
|
+
const docPaneEl = document.getElementById("doc-pane");
|
|
1780
|
+
const docPaneForWrap = docPaneEl instanceof HTMLElement ? docPaneEl : null;
|
|
1781
|
+
const blockRayRedraw = {};
|
|
1782
|
+
wireWrapToggle(STORAGE_WRAP_LINES, codePane, wrapCb, () => {
|
|
1783
|
+
blockRayRedraw.request?.();
|
|
1784
|
+
codePane.dispatchEvent(new Event("scroll"));
|
|
1785
|
+
docScrollEl.dispatchEvent(new Event("scroll"));
|
|
1786
|
+
}, docPaneForWrap, docBody);
|
|
943
1787
|
wireSplitter(STORAGE_SPLIT_PCT, shell, codePane, gutter, pct);
|
|
944
1788
|
const multiScript = document.getElementById("commentray-multi-angle-b64");
|
|
945
1789
|
const multiPayload = parseMultiAnglePayload(multiScript);
|
|
946
|
-
|
|
1790
|
+
const shouldWireBlockRays = multiPayload !== null || bundle.scrollLinksRef.current.length > 0;
|
|
1791
|
+
const requestBlockRayRedraw = shouldWireBlockRays
|
|
1792
|
+
? wireBlockRayConnectors({
|
|
1793
|
+
gutter,
|
|
1794
|
+
codePane,
|
|
1795
|
+
docScrollEl,
|
|
1796
|
+
getLinks: () => bundle.scrollLinksRef.current,
|
|
1797
|
+
probeTopSourceLine1Based: () => probeCodeLine1FromViewport(codePane),
|
|
1798
|
+
})
|
|
1799
|
+
: undefined;
|
|
1800
|
+
blockRayRedraw.request = requestBlockRayRedraw;
|
|
1801
|
+
const scrollRunners = wireDualPaneMultiAngleAndScroll({
|
|
947
1802
|
codePane,
|
|
948
1803
|
docScrollEl,
|
|
949
1804
|
docBody,
|
|
950
1805
|
shell,
|
|
951
|
-
|
|
1806
|
+
scrollLinksRef: bundle.scrollLinksRef,
|
|
952
1807
|
multiPayload,
|
|
953
|
-
mutable,
|
|
954
|
-
rebuildSearcher,
|
|
1808
|
+
mutable: bundle.mutable,
|
|
1809
|
+
rebuildSearcher: bundle.rebuildSearcher,
|
|
955
1810
|
searchInput,
|
|
956
1811
|
searchResults,
|
|
1812
|
+
requestBlockRayRedraw,
|
|
957
1813
|
});
|
|
958
|
-
|
|
1814
|
+
const flipBtn = document.getElementById("mobile-pane-flip");
|
|
1815
|
+
const flipScrollBtn = document.getElementById("mobile-pane-flip-scroll");
|
|
1816
|
+
if (flipBtn instanceof HTMLButtonElement) {
|
|
1817
|
+
wireDualMobilePaneFlip(shell, flipBtn, scrollRunners, flipScrollBtn instanceof HTMLButtonElement ? flipScrollBtn : null);
|
|
1818
|
+
}
|
|
1819
|
+
wireDualPaneCommentrayLocationHash(docScrollEl, () => bundle.mutable.mdLines.length);
|
|
1820
|
+
}
|
|
1821
|
+
function commentrayThemeModeLabel(mode) {
|
|
1822
|
+
if (mode === "light")
|
|
1823
|
+
return "Light";
|
|
1824
|
+
if (mode === "dark")
|
|
1825
|
+
return "Dark";
|
|
1826
|
+
return "System";
|
|
1827
|
+
}
|
|
1828
|
+
function setCommentrayThemeTriggerHints(trigger, mode) {
|
|
1829
|
+
const label = commentrayThemeModeLabel(mode);
|
|
1830
|
+
trigger.setAttribute("aria-label", `Color theme: ${label}. Left-click opens the menu. Right-click cycles System, Light, and Dark.`);
|
|
1831
|
+
trigger.title = `Appearance: ${label} — left-click menu, right-click cycle`;
|
|
1832
|
+
}
|
|
1833
|
+
function wireColorThemeToolbar() {
|
|
1834
|
+
const wrapEl = document.querySelector(".toolbar-theme");
|
|
1835
|
+
const triggerEl = document.getElementById("commentray-theme-trigger");
|
|
1836
|
+
const menuEl = document.getElementById("commentray-theme-menu");
|
|
1837
|
+
if (!wrapEl || !(triggerEl instanceof HTMLButtonElement) || !(menuEl instanceof HTMLElement))
|
|
1838
|
+
return;
|
|
1839
|
+
const themeToolbarWrap = wrapEl;
|
|
1840
|
+
const themeButton = triggerEl;
|
|
1841
|
+
const themeMenu = menuEl;
|
|
1842
|
+
let currentMode = parseCommentrayColorThemeMode(readWebStorageItem(localStorage, COMMENTRAY_COLOR_THEME_STORAGE_KEY));
|
|
1843
|
+
applyCommentrayColorTheme(currentMode);
|
|
1844
|
+
let menuOpen = false;
|
|
1845
|
+
function syncUi() {
|
|
1846
|
+
themeButton.dataset.commentrayTriggerMode = currentMode;
|
|
1847
|
+
themeButton.setAttribute("aria-expanded", menuOpen ? "true" : "false");
|
|
1848
|
+
setCommentrayThemeTriggerHints(themeButton, currentMode);
|
|
1849
|
+
for (const el of themeMenu.querySelectorAll("[data-commentray-theme-value]")) {
|
|
1850
|
+
const v = parseCommentrayColorThemeMode(el.dataset.commentrayThemeValue ?? "");
|
|
1851
|
+
el.setAttribute("aria-checked", v === currentMode ? "true" : "false");
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
function openMenu() {
|
|
1855
|
+
menuOpen = true;
|
|
1856
|
+
themeMenu.removeAttribute("hidden");
|
|
1857
|
+
syncUi();
|
|
1858
|
+
const checked = themeMenu.querySelector('[role="menuitemradio"][aria-checked="true"]');
|
|
1859
|
+
(checked ?? themeMenu.querySelector('[role="menuitemradio"]'))?.focus();
|
|
1860
|
+
}
|
|
1861
|
+
function closeMenu() {
|
|
1862
|
+
menuOpen = false;
|
|
1863
|
+
themeMenu.setAttribute("hidden", "");
|
|
1864
|
+
syncUi();
|
|
1865
|
+
}
|
|
1866
|
+
function persistAndApply(mode) {
|
|
1867
|
+
currentMode = mode;
|
|
1868
|
+
writeWebStorageItem(localStorage, COMMENTRAY_COLOR_THEME_STORAGE_KEY, mode);
|
|
1869
|
+
applyCommentrayColorTheme(mode);
|
|
1870
|
+
syncUi();
|
|
1871
|
+
}
|
|
1872
|
+
themeButton.addEventListener("click", (ev) => {
|
|
1873
|
+
ev.preventDefault();
|
|
1874
|
+
ev.stopPropagation();
|
|
1875
|
+
if (menuOpen) {
|
|
1876
|
+
closeMenu();
|
|
1877
|
+
}
|
|
1878
|
+
else {
|
|
1879
|
+
openMenu();
|
|
1880
|
+
}
|
|
1881
|
+
});
|
|
1882
|
+
themeButton.addEventListener("contextmenu", (ev) => {
|
|
1883
|
+
ev.preventDefault();
|
|
1884
|
+
if (menuOpen)
|
|
1885
|
+
closeMenu();
|
|
1886
|
+
persistAndApply(nextCommentrayColorThemeMode(currentMode));
|
|
1887
|
+
});
|
|
1888
|
+
for (const item of themeMenu.querySelectorAll("[data-commentray-theme-value]")) {
|
|
1889
|
+
item.addEventListener("click", (ev) => {
|
|
1890
|
+
ev.stopPropagation();
|
|
1891
|
+
const mode = parseCommentrayColorThemeMode(item.dataset.commentrayThemeValue ?? "");
|
|
1892
|
+
persistAndApply(mode);
|
|
1893
|
+
closeMenu();
|
|
1894
|
+
themeButton.focus();
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
function onDocumentPointerDown(ev) {
|
|
1898
|
+
if (!menuOpen)
|
|
1899
|
+
return;
|
|
1900
|
+
const t = ev.target;
|
|
1901
|
+
if (!(t instanceof Node))
|
|
1902
|
+
return;
|
|
1903
|
+
if (themeToolbarWrap.contains(t))
|
|
1904
|
+
return;
|
|
1905
|
+
closeMenu();
|
|
1906
|
+
}
|
|
1907
|
+
function onDocumentKeydown(ev) {
|
|
1908
|
+
if (!menuOpen || ev.key !== "Escape")
|
|
1909
|
+
return;
|
|
1910
|
+
ev.preventDefault();
|
|
1911
|
+
ev.stopPropagation();
|
|
1912
|
+
closeMenu();
|
|
1913
|
+
themeButton.focus();
|
|
1914
|
+
}
|
|
1915
|
+
document.addEventListener("mousedown", onDocumentPointerDown, true);
|
|
1916
|
+
document.addEventListener("touchstart", onDocumentPointerDown, true);
|
|
1917
|
+
document.addEventListener("keydown", onDocumentKeydown, true);
|
|
1918
|
+
syncUi();
|
|
959
1919
|
}
|
|
960
1920
|
function main() {
|
|
1921
|
+
wireColorThemeToolbar();
|
|
961
1922
|
wireDocumentedFilesTree();
|
|
962
1923
|
const shell = document.getElementById("shell");
|
|
963
1924
|
const codePane = document.getElementById("code-pane");
|