@commentray/render 0.0.6 → 0.0.9
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/code-browser-client.bundle.js +10 -10
- package/dist/code-browser-client.js +747 -104
- 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 +8 -0
- package/dist/code-browser-pair-nav.d.ts.map +1 -1
- package/dist/code-browser-pair-nav.js +20 -2
- package/dist/code-browser-pair-nav.js.map +1 -1
- package/dist/code-browser-scroll-sync.js +1 -1
- package/dist/code-browser-scroll-sync.js.map +1 -1
- package/dist/code-browser.d.ts +2 -2
- package/dist/code-browser.d.ts.map +1 -1
- package/dist/code-browser.js +903 -228
- 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 +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- 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 +40 -3
- package/dist/markdown-pipeline.d.ts.map +1 -1
- package/dist/markdown-pipeline.js +140 -24
- package/dist/markdown-pipeline.js.map +1 -1
- package/dist/mermaid-runtime-html.d.ts.map +1 -1
- package/dist/mermaid-runtime-html.js +10 -2
- 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.d.ts +7 -0
- package/dist/side-by-side-layout-css.d.ts.map +1 -0
- package/dist/side-by-side-layout-css.js +65 -0
- package/dist/side-by-side-layout-css.js.map +1 -0
- package/dist/side-by-side-layout.css +58 -0
- package/dist/side-by-side-layout.embedded.d.ts +3 -0
- package/dist/side-by-side-layout.embedded.d.ts.map +1 -0
- package/dist/side-by-side-layout.embedded.js +3 -0
- package/dist/side-by-side-layout.embedded.js.map +1 -0
- package/dist/side-by-side.d.ts.map +1 -1
- package/dist/side-by-side.js +7 -12
- package/dist/side-by-side.js.map +1 -1
- package/package.json +2 -2
|
@@ -4,24 +4,221 @@ import { mirroredScrollTop, pickCommentrayLineForSourceScroll, pickSourceLine0Fo
|
|
|
4
4
|
import { decodeBase64Utf8 } from "./code-browser-encoding.js";
|
|
5
5
|
import { readEmbeddedRawB64Strings } from "./code-browser-embedded-payload.js";
|
|
6
6
|
import { escapeHtmlHighlightingSearchTokens, filterPairsByDocumentedTreeQuery, findOrderedTokenSpans, lineAtIndex, offsetToLineIndex, pathRowsFromDocumentedPairs, tokenizeQuery, uniqueSourceFilePreviewRows, } from "./code-browser-search.js";
|
|
7
|
-
import { findDocumentedPair, isSameDocumentedPair, normPosixPath, resolveStaticBrowseHref, } from "./code-browser-pair-nav.js";
|
|
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";
|
|
8
9
|
import { readWebStorageItem, writeWebStorageItem } from "./code-browser-web-storage.js";
|
|
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
|
+
}
|
|
9
26
|
function runMermaidOnFreshDocNodes(docBody) {
|
|
10
27
|
if (typeof globalThis.location !== "undefined" && globalThis.location.protocol === "file:")
|
|
11
28
|
return;
|
|
12
|
-
|
|
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
|
+
});
|
|
13
36
|
if (nodes.length === 0)
|
|
14
37
|
return;
|
|
15
38
|
const m = globalThis
|
|
16
39
|
.commentrayMermaid;
|
|
17
40
|
if (!m)
|
|
18
41
|
return;
|
|
19
|
-
|
|
20
|
-
|
|
42
|
+
void m.run({ nodes }).catch((err) => {
|
|
43
|
+
console.error("Commentray: mermaid.run failed", err);
|
|
44
|
+
});
|
|
21
45
|
}
|
|
22
46
|
function clamp(n, lo, hi) {
|
|
23
47
|
return Math.max(lo, Math.min(hi, n));
|
|
24
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
|
+
}
|
|
25
222
|
function escapeHtmlText(s) {
|
|
26
223
|
return s
|
|
27
224
|
.replace(/&/g, "&")
|
|
@@ -198,17 +395,16 @@ function emptySearchBrowsePreviewInnerHtml(hint, rows, ctx) {
|
|
|
198
395
|
function scrollDocToMarkdownLine0(docScrollEl, line0, mdLineCount) {
|
|
199
396
|
const el = docScrollEl.querySelector(`#commentray-md-line-${String(line0)}`);
|
|
200
397
|
if (el instanceof HTMLElement) {
|
|
201
|
-
const top =
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
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" });
|
|
205
401
|
return;
|
|
206
402
|
}
|
|
207
403
|
if (mdLineCount <= 1)
|
|
208
404
|
return;
|
|
209
405
|
const ratio = line0 / Math.max(1, mdLineCount - 1);
|
|
210
|
-
const maxScroll = docScrollEl.scrollHeight - docScrollEl.clientHeight;
|
|
211
|
-
docScrollEl.scrollTo({ top: ratio *
|
|
406
|
+
const maxScroll = Math.max(0, docScrollEl.scrollHeight - docScrollEl.clientHeight);
|
|
407
|
+
docScrollEl.scrollTo({ top: ratio * maxScroll, behavior: "smooth" });
|
|
212
408
|
}
|
|
213
409
|
function navigateToDocumentedPair(pair, mdLine0) {
|
|
214
410
|
if (pair.staticBrowseUrl?.trim()) {
|
|
@@ -424,20 +620,51 @@ function wireSearchUi(ctx) {
|
|
|
424
620
|
e.preventDefault();
|
|
425
621
|
});
|
|
426
622
|
}
|
|
427
|
-
|
|
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);
|
|
428
633
|
const wrap = readWebStorageItem(localStorage, storageWrap) === "1";
|
|
429
634
|
wrapCb.checked = wrap;
|
|
430
|
-
if (wrap)
|
|
635
|
+
if (wrap) {
|
|
431
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
|
+
}
|
|
432
645
|
wrapCb.addEventListener("change", () => {
|
|
433
646
|
if (wrapCb.checked) {
|
|
434
647
|
codePane.classList.add("wrap");
|
|
648
|
+
for (const el of docTargets)
|
|
649
|
+
el.classList.add("wrap");
|
|
435
650
|
writeWebStorageItem(localStorage, storageWrap, "1");
|
|
436
651
|
}
|
|
437
652
|
else {
|
|
438
653
|
codePane.classList.remove("wrap");
|
|
654
|
+
for (const el of docTargets)
|
|
655
|
+
el.classList.remove("wrap");
|
|
439
656
|
writeWebStorageItem(localStorage, storageWrap, "0");
|
|
440
657
|
}
|
|
658
|
+
if (!onAfterLayout)
|
|
659
|
+
return;
|
|
660
|
+
queueMicrotask(() => {
|
|
661
|
+
requestAnimationFrame(() => {
|
|
662
|
+
onAfterLayout();
|
|
663
|
+
requestAnimationFrame(() => {
|
|
664
|
+
onAfterLayout();
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
});
|
|
441
668
|
});
|
|
442
669
|
}
|
|
443
670
|
function parseScrollBlockLinksFromShell(b64) {
|
|
@@ -471,101 +698,218 @@ function parseScrollBlockLinksFromShell(b64) {
|
|
|
471
698
|
return [];
|
|
472
699
|
}
|
|
473
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
|
+
}
|
|
474
706
|
function probeCodeLine1FromViewport(codePane) {
|
|
475
|
-
const y = codePane.getBoundingClientRect().top + 2;
|
|
476
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;
|
|
477
736
|
for (const el of rows) {
|
|
478
737
|
const r = el.getBoundingClientRect();
|
|
479
|
-
if (r.bottom > y) {
|
|
738
|
+
if (r.bottom > y - 1e-3) {
|
|
480
739
|
const m = /^code-line-(\d+)$/.exec(el.id);
|
|
481
740
|
if (m)
|
|
482
741
|
return Number(m[1]) + 1;
|
|
483
742
|
return 1;
|
|
484
743
|
}
|
|
485
744
|
}
|
|
486
|
-
return rows.length
|
|
745
|
+
return rows.length;
|
|
487
746
|
}
|
|
488
747
|
function probeCommentrayLine0FromDoc(docPane) {
|
|
489
|
-
const y = docPane.getBoundingClientRect().top + 2;
|
|
490
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;
|
|
491
776
|
let best = 0;
|
|
492
777
|
for (const a of anchors) {
|
|
493
778
|
const lineAttr = a.getAttribute("data-commentray-line");
|
|
494
779
|
if (lineAttr === null || lineAttr === "")
|
|
495
780
|
continue;
|
|
496
|
-
if (a.getBoundingClientRect().top <= y + 1)
|
|
781
|
+
if (a.getBoundingClientRect().top <= y + 1 + 1e-3)
|
|
497
782
|
best = Number(lineAttr);
|
|
498
783
|
else
|
|
499
784
|
break;
|
|
500
785
|
}
|
|
501
786
|
return best;
|
|
502
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
|
+
}
|
|
503
805
|
function wireBidirectionalScroll(codePane, docPane, syncFromCode, syncFromDoc) {
|
|
504
806
|
let syncing = "none";
|
|
807
|
+
const ignoreCodeScrollFromPartnerSync = { n: 0 };
|
|
808
|
+
const ignoreDocScrollFromPartnerSync = { n: 0 };
|
|
505
809
|
codePane.addEventListener("scroll", () => {
|
|
810
|
+
if (ignoreCodeScrollFromPartnerSync.n > 0) {
|
|
811
|
+
ignoreCodeScrollFromPartnerSync.n--;
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
506
814
|
if (syncing === "doc")
|
|
507
815
|
return;
|
|
508
816
|
syncing = "code";
|
|
817
|
+
armIgnoreNextPaneScrollReaction(ignoreDocScrollFromPartnerSync);
|
|
509
818
|
syncFromCode();
|
|
510
819
|
syncing = "none";
|
|
511
820
|
}, { passive: true });
|
|
512
821
|
docPane.addEventListener("scroll", () => {
|
|
822
|
+
if (ignoreDocScrollFromPartnerSync.n > 0) {
|
|
823
|
+
ignoreDocScrollFromPartnerSync.n--;
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
513
826
|
if (syncing === "code")
|
|
514
827
|
return;
|
|
515
828
|
syncing = "doc";
|
|
829
|
+
armIgnoreNextPaneScrollReaction(ignoreCodeScrollFromPartnerSync);
|
|
516
830
|
syncFromDoc();
|
|
517
831
|
syncing = "none";
|
|
518
832
|
}, { passive: true });
|
|
519
833
|
}
|
|
520
834
|
/** Index-backed scroll sync when `data-scroll-block-links-b64` is present; else see proportional fallback. */
|
|
521
835
|
function wireBlockAwareScrollSync(codePane, docPane, getLinks) {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
560
|
-
});
|
|
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
|
+
};
|
|
561
873
|
}
|
|
562
874
|
/** Proportional scroll sync when there is no index-backed block map (GitHub Pages default). */
|
|
563
875
|
function wireProportionalScrollSync(codePane, docPane) {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
codePane
|
|
568
|
-
}
|
|
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
|
+
};
|
|
569
913
|
}
|
|
570
914
|
function centerYInViewport(el) {
|
|
571
915
|
const r = el.getBoundingClientRect();
|
|
@@ -667,6 +1011,8 @@ function drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSource
|
|
|
667
1011
|
* Splines in the gutter between each block’s source range and its commentary band (dual pane,
|
|
668
1012
|
* index-backed blocks). Emphasizes the block aligned with the current source viewport; clamps
|
|
669
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).
|
|
670
1016
|
*/
|
|
671
1017
|
function wireBlockRayConnectors(args) {
|
|
672
1018
|
const { gutter, codePane, docScrollEl, getLinks, probeTopSourceLine1Based } = args;
|
|
@@ -693,6 +1039,7 @@ function wireBlockRayConnectors(args) {
|
|
|
693
1039
|
scheduleDraw();
|
|
694
1040
|
globalThis.requestAnimationFrame(scheduleDraw);
|
|
695
1041
|
});
|
|
1042
|
+
return scheduleDraw;
|
|
696
1043
|
}
|
|
697
1044
|
function isDocumentedPairNav(x) {
|
|
698
1045
|
if (typeof x !== "object" || x === null)
|
|
@@ -905,6 +1252,52 @@ function loadDocumentedPairs(jsonUrl, embeddedB64) {
|
|
|
905
1252
|
return loaded ?? [];
|
|
906
1253
|
};
|
|
907
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
|
+
}
|
|
908
1301
|
function wireDocumentedFilesTree() {
|
|
909
1302
|
const hub = document.getElementById("documented-files-hub");
|
|
910
1303
|
const treeHost = document.getElementById("documented-files-tree");
|
|
@@ -918,6 +1311,7 @@ function wireDocumentedFilesTree() {
|
|
|
918
1311
|
const embeddedB64 = shell?.getAttribute("data-documented-pairs-b64")?.trim() ?? "";
|
|
919
1312
|
if (jsonUrl.length === 0 && embeddedB64.length === 0)
|
|
920
1313
|
return;
|
|
1314
|
+
const placeDocHubFlyout = wireDocumentedFilesTreeMobileFlyout(hub);
|
|
921
1315
|
const ensureLoaded = loadDocumentedPairs(jsonUrl, embeddedB64);
|
|
922
1316
|
let cachedPairs = null;
|
|
923
1317
|
function applyFilterAndRender() {
|
|
@@ -941,6 +1335,10 @@ function wireDocumentedFilesTree() {
|
|
|
941
1335
|
}
|
|
942
1336
|
}
|
|
943
1337
|
hub.addEventListener("toggle", () => {
|
|
1338
|
+
placeDocHubFlyout();
|
|
1339
|
+
if (hub.open) {
|
|
1340
|
+
globalThis.requestAnimationFrame(placeDocHubFlyout);
|
|
1341
|
+
}
|
|
944
1342
|
if (!hub.open)
|
|
945
1343
|
return;
|
|
946
1344
|
void hydrateTree();
|
|
@@ -964,6 +1362,7 @@ function wireSplitter(storageSplit, shell, codePane, gutter, initialPct) {
|
|
|
964
1362
|
const p = clamp((x / rect.width) * 100, 15, 85);
|
|
965
1363
|
lastPct = p;
|
|
966
1364
|
codePane.style.flex = `0 0 ${p}%`;
|
|
1365
|
+
shell.style.setProperty("--split-pct", `${String(p)}%`);
|
|
967
1366
|
}
|
|
968
1367
|
function stop() {
|
|
969
1368
|
dragging = false;
|
|
@@ -984,10 +1383,118 @@ function wireSplitter(storageSplit, shell, codePane, gutter, initialPct) {
|
|
|
984
1383
|
}
|
|
985
1384
|
const STORAGE_SPLIT_PCT = "commentray.codeCommentrayStatic.splitPct";
|
|
986
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
|
+
}
|
|
987
1492
|
function wireStretchLayoutChrome(codePane) {
|
|
988
1493
|
const wrapCb = document.getElementById("wrap-lines");
|
|
989
1494
|
if (wrapCb) {
|
|
990
|
-
wireWrapToggle(STORAGE_WRAP_LINES, codePane, wrapCb)
|
|
1495
|
+
wireWrapToggle(STORAGE_WRAP_LINES, codePane, wrapCb, () => {
|
|
1496
|
+
globalThis.dispatchEvent(new Event("resize"));
|
|
1497
|
+
});
|
|
991
1498
|
}
|
|
992
1499
|
}
|
|
993
1500
|
function parseMultiAnglePayload(script) {
|
|
@@ -1124,9 +1631,9 @@ function wireDualPaneNavSearchFetch(shell, embeddedPairs, indexState, mutable, r
|
|
|
1124
1631
|
})();
|
|
1125
1632
|
}
|
|
1126
1633
|
function wireDualPaneMultiAngleAndScroll(args) {
|
|
1127
|
-
const { codePane, docScrollEl, docBody, shell, scrollLinksRef, multiPayload, mutable, rebuildSearcher, searchInput, searchResults, } = args;
|
|
1634
|
+
const { codePane, docScrollEl, docBody, shell, scrollLinksRef, multiPayload, mutable, rebuildSearcher, searchInput, searchResults, requestBlockRayRedraw, } = args;
|
|
1128
1635
|
if (multiPayload) {
|
|
1129
|
-
wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinksRef.current);
|
|
1636
|
+
const runners = wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinksRef.current);
|
|
1130
1637
|
const angleSel = document.getElementById("angle-select");
|
|
1131
1638
|
if (angleSel && docBody) {
|
|
1132
1639
|
angleSel.addEventListener("change", () => {
|
|
@@ -1135,6 +1642,7 @@ function wireDualPaneMultiAngleAndScroll(args) {
|
|
|
1135
1642
|
return;
|
|
1136
1643
|
docBody.innerHTML = decodeBase64Utf8(a.docInnerHtmlB64);
|
|
1137
1644
|
runMermaidOnFreshDocNodes(docBody);
|
|
1645
|
+
rewriteHubRelativeBrowseAnchorsIn(docBody);
|
|
1138
1646
|
mutable.rawMd = decodeBase64Utf8(a.rawMdB64);
|
|
1139
1647
|
mutable.mdLines = mutable.rawMd.split("\n");
|
|
1140
1648
|
mutable.commentrayPathLabel = a.commentrayPathForSearch;
|
|
@@ -1151,35 +1659,38 @@ function wireDualPaneMultiAngleAndScroll(args) {
|
|
|
1151
1659
|
else
|
|
1152
1660
|
docPathEl.removeAttribute("title");
|
|
1153
1661
|
}
|
|
1154
|
-
const
|
|
1155
|
-
if (
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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);
|
|
1161
1671
|
}
|
|
1162
1672
|
else {
|
|
1163
|
-
|
|
1164
|
-
if (ghu) {
|
|
1165
|
-
gh.href = ghu;
|
|
1166
|
-
gh.target = "_blank";
|
|
1167
|
-
gh.setAttribute("rel", "noopener noreferrer");
|
|
1168
|
-
}
|
|
1673
|
+
shell.removeAttribute("data-commentray-pair-browse-href");
|
|
1169
1674
|
}
|
|
1170
1675
|
}
|
|
1171
1676
|
searchInput.value = "";
|
|
1172
1677
|
searchResults.innerHTML = "";
|
|
1173
1678
|
searchResults.hidden = true;
|
|
1679
|
+
requestBlockRayRedraw?.();
|
|
1680
|
+
globalThis.requestAnimationFrame(() => {
|
|
1681
|
+
requestBlockRayRedraw?.();
|
|
1682
|
+
globalThis.requestAnimationFrame(() => {
|
|
1683
|
+
requestBlockRayRedraw?.();
|
|
1684
|
+
});
|
|
1685
|
+
});
|
|
1174
1686
|
});
|
|
1175
1687
|
}
|
|
1176
|
-
return;
|
|
1688
|
+
return runners;
|
|
1177
1689
|
}
|
|
1178
1690
|
if (scrollLinksRef.current.length > 0) {
|
|
1179
|
-
wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinksRef.current);
|
|
1180
|
-
return;
|
|
1691
|
+
return wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinksRef.current);
|
|
1181
1692
|
}
|
|
1182
|
-
wireProportionalScrollSync(codePane, docScrollEl);
|
|
1693
|
+
return wireProportionalScrollSync(codePane, docScrollEl);
|
|
1183
1694
|
}
|
|
1184
1695
|
function wireDualPaneCommentrayLocationHash(docScrollEl, mdLineCount) {
|
|
1185
1696
|
function applyCommentrayLocationHash() {
|
|
@@ -1196,11 +1707,7 @@ function wireDualPaneCommentrayLocationHash(docScrollEl, mdLineCount) {
|
|
|
1196
1707
|
globalThis.requestAnimationFrame(applyCommentrayLocationHash);
|
|
1197
1708
|
});
|
|
1198
1709
|
}
|
|
1199
|
-
function
|
|
1200
|
-
const dom = readDualPaneDomBundle();
|
|
1201
|
-
if (!dom)
|
|
1202
|
-
return;
|
|
1203
|
-
const { docBody, docScrollEl, gutter, wrapCb, searchInput, searchClear, searchResults } = dom;
|
|
1710
|
+
function buildDualPaneSearcherBundle(shell, codePane) {
|
|
1204
1711
|
const { rawCodeB64, rawMdB64 } = readEmbeddedRawB64Strings(shell, codePane);
|
|
1205
1712
|
const rawCode = decodeBase64Utf8(rawCodeB64);
|
|
1206
1713
|
const rawMd = decodeBase64Utf8(rawMdB64);
|
|
@@ -1234,48 +1741,184 @@ function wireDualPaneCodeBrowser(shell, codePane) {
|
|
|
1234
1741
|
}));
|
|
1235
1742
|
}
|
|
1236
1743
|
rebuildSearcher();
|
|
1237
|
-
|
|
1744
|
+
return {
|
|
1745
|
+
rawCode,
|
|
1746
|
+
rawMd,
|
|
1747
|
+
scrollLinksRef,
|
|
1238
1748
|
scope,
|
|
1239
1749
|
filePathLabel,
|
|
1750
|
+
commentrayPathLabel,
|
|
1751
|
+
pathInit,
|
|
1752
|
+
indexState,
|
|
1240
1753
|
mutable,
|
|
1241
|
-
|
|
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,
|
|
1242
1769
|
searchInput,
|
|
1243
1770
|
searchClear,
|
|
1244
1771
|
searchResults,
|
|
1245
1772
|
docScrollEl,
|
|
1246
1773
|
});
|
|
1247
|
-
wireDualPaneNavSearchFetch(shell, pathInit.documentedPairs, indexState, mutable, rebuildSearcher, searchInput);
|
|
1248
|
-
const pct0 = parseFloat(readWebStorageItem(localStorage, STORAGE_SPLIT_PCT) || "
|
|
1249
|
-
const pct = clamp(Number.isFinite(pct0) ? pct0 :
|
|
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);
|
|
1250
1777
|
codePane.style.flex = `0 0 ${pct}%`;
|
|
1251
|
-
|
|
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);
|
|
1252
1787
|
wireSplitter(STORAGE_SPLIT_PCT, shell, codePane, gutter, pct);
|
|
1253
1788
|
const multiScript = document.getElementById("commentray-multi-angle-b64");
|
|
1254
1789
|
const multiPayload = parseMultiAnglePayload(multiScript);
|
|
1255
|
-
|
|
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({
|
|
1256
1802
|
codePane,
|
|
1257
1803
|
docScrollEl,
|
|
1258
1804
|
docBody,
|
|
1259
1805
|
shell,
|
|
1260
|
-
scrollLinksRef,
|
|
1806
|
+
scrollLinksRef: bundle.scrollLinksRef,
|
|
1261
1807
|
multiPayload,
|
|
1262
|
-
mutable,
|
|
1263
|
-
rebuildSearcher,
|
|
1808
|
+
mutable: bundle.mutable,
|
|
1809
|
+
rebuildSearcher: bundle.rebuildSearcher,
|
|
1264
1810
|
searchInput,
|
|
1265
1811
|
searchResults,
|
|
1812
|
+
requestBlockRayRedraw,
|
|
1266
1813
|
});
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
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();
|
|
1274
1895
|
});
|
|
1275
1896
|
}
|
|
1276
|
-
|
|
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();
|
|
1277
1919
|
}
|
|
1278
1920
|
function main() {
|
|
1921
|
+
wireColorThemeToolbar();
|
|
1279
1922
|
wireDocumentedFilesTree();
|
|
1280
1923
|
const shell = document.getElementById("shell");
|
|
1281
1924
|
const codePane = document.getElementById("code-pane");
|