@commentray/render 0.0.9 → 0.1.1
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/README.md +1 -1
- package/dist/block-stretch-layout.d.ts.map +1 -1
- package/dist/block-stretch-layout.js +2 -1
- package/dist/block-stretch-layout.js.map +1 -1
- package/dist/browse-page-slug.d.ts +6 -1
- package/dist/browse-page-slug.d.ts.map +1 -1
- package/dist/browse-page-slug.js +6 -1
- package/dist/browse-page-slug.js.map +1 -1
- package/dist/browse-pair-html-test-fixtures.d.ts +10 -0
- package/dist/browse-pair-html-test-fixtures.d.ts.map +1 -0
- package/dist/browse-pair-html-test-fixtures.js +19 -0
- package/dist/browse-pair-html-test-fixtures.js.map +1 -0
- package/dist/build-commentray-nav-search.d.ts +4 -4
- package/dist/build-commentray-nav-search.js +1 -1
- package/dist/code-browser-block-rays.d.ts +29 -0
- package/dist/code-browser-block-rays.d.ts.map +1 -1
- package/dist/code-browser-block-rays.js +120 -0
- package/dist/code-browser-block-rays.js.map +1 -1
- package/dist/code-browser-client.bundle.js +24 -11
- package/dist/code-browser-client.js +766 -116
- package/dist/code-browser-client.js.map +1 -1
- package/dist/code-browser-intro.css +187 -0
- package/dist/code-browser-pair-nav.d.ts +3 -3
- package/dist/code-browser-pair-nav.d.ts.map +1 -1
- package/dist/code-browser-pair-nav.js +25 -13
- package/dist/code-browser-pair-nav.js.map +1 -1
- package/dist/code-browser-wide-intro-controller.d.ts +4 -0
- package/dist/code-browser-wide-intro-controller.d.ts.map +1 -0
- package/dist/code-browser-wide-intro-controller.js +148 -0
- package/dist/code-browser-wide-intro-controller.js.map +1 -0
- package/dist/code-browser-wide-intro-layout.d.ts +3 -0
- package/dist/code-browser-wide-intro-layout.d.ts.map +1 -0
- package/dist/code-browser-wide-intro-layout.js +84 -0
- package/dist/code-browser-wide-intro-layout.js.map +1 -0
- package/dist/code-browser-wide-intro-steps.d.ts +11 -0
- package/dist/code-browser-wide-intro-steps.d.ts.map +1 -0
- package/dist/code-browser-wide-intro-steps.js +108 -0
- package/dist/code-browser-wide-intro-steps.js.map +1 -0
- package/dist/code-browser-wide-intro-ui.d.ts +14 -0
- package/dist/code-browser-wide-intro-ui.d.ts.map +1 -0
- package/dist/code-browser-wide-intro-ui.js +67 -0
- package/dist/code-browser-wide-intro-ui.js.map +1 -0
- package/dist/code-browser.d.ts +18 -4
- package/dist/code-browser.d.ts.map +1 -1
- package/dist/code-browser.js +506 -154
- package/dist/code-browser.js.map +1 -1
- package/dist/commentray-anchor-viewport-probe.d.ts +9 -0
- package/dist/commentray-anchor-viewport-probe.d.ts.map +1 -0
- package/dist/commentray-anchor-viewport-probe.js +13 -0
- package/dist/commentray-anchor-viewport-probe.js.map +1 -0
- package/dist/commentray-preview-html.d.ts +13 -0
- package/dist/commentray-preview-html.d.ts.map +1 -0
- package/dist/commentray-preview-html.js +12 -0
- package/dist/commentray-preview-html.js.map +1 -0
- package/dist/companion-markdown-preview-entry.d.ts +7 -0
- package/dist/companion-markdown-preview-entry.d.ts.map +1 -0
- package/dist/companion-markdown-preview-entry.js +6 -0
- package/dist/companion-markdown-preview-entry.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/inject-md-line-anchors.d.ts +18 -0
- package/dist/inject-md-line-anchors.d.ts.map +1 -0
- package/dist/inject-md-line-anchors.js +250 -0
- package/dist/inject-md-line-anchors.js.map +1 -0
- package/dist/inline-favicon.js +15 -15
- package/dist/markdown-pipeline.d.ts +5 -0
- package/dist/markdown-pipeline.d.ts.map +1 -1
- package/dist/markdown-pipeline.js +47 -1
- package/dist/markdown-pipeline.js.map +1 -1
- package/dist/side-by-side-layout-css.d.ts +1 -1
- package/dist/side-by-side-layout-css.d.ts.map +1 -1
- package/dist/side-by-side-layout-css.js +48 -0
- package/dist/side-by-side-layout-css.js.map +1 -1
- package/package.json +8 -3
- package/dist/code-browser-client.d.ts +0 -2
- package/dist/side-by-side-layout.css +0 -58
- package/dist/side-by-side-layout.embedded.d.ts +0 -3
- package/dist/side-by-side-layout.embedded.d.ts.map +0 -1
- package/dist/side-by-side-layout.embedded.js +0 -3
- package/dist/side-by-side-layout.embedded.js.map +0 -1
|
@@ -1,11 +1,13 @@
|
|
|
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
|
+
import { activeBlockIdForCommentrayLine0, activeBlockIdForViewport, clampViewportYToGutterLocal, codeLineDomIndex0, dedupeBlockScrollLinksById, gutterRayBezierPaths, maxRenderableCommentaryContentBottomViewport, nextBlockLinkInCommentrayOrder, sortBlockLinksBySource, } from "./code-browser-block-rays.js";
|
|
3
3
|
import { mirroredScrollTop, pickCommentrayLineForSourceScroll, pickSourceLine0ForCommentrayScroll, } from "./code-browser-scroll-sync.js";
|
|
4
|
+
import { maxCommentrayAnchorLine0AtOrAboveViewportY } from "./commentray-anchor-viewport-probe.js";
|
|
4
5
|
import { decodeBase64Utf8 } from "./code-browser-encoding.js";
|
|
5
6
|
import { readEmbeddedRawB64Strings } from "./code-browser-embedded-payload.js";
|
|
6
7
|
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 { findDocumentedPair, isHubRelativeStaticBrowseHref, isSameDocumentedPair, normPosixPath, resolveStaticBrowseHref, siteRootPathnameFromPathname, staticBrowseHrefForShellDataAttribute, } from "./code-browser-pair-nav.js";
|
|
8
9
|
import { COMMENTRAY_COLOR_THEME_STORAGE_KEY, applyCommentrayColorTheme, nextCommentrayColorThemeMode, parseCommentrayColorThemeMode, } from "./code-browser-color-theme.js";
|
|
10
|
+
import { wireWideModeIntroTour } from "./code-browser-wide-intro-controller.js";
|
|
9
11
|
import { readWebStorageItem, writeWebStorageItem } from "./code-browser-web-storage.js";
|
|
10
12
|
/**
|
|
11
13
|
* Hub pages emit `./browse/…` relative to the site root. From `/…/browse/current.html` the browser
|
|
@@ -104,9 +106,13 @@ function windowScrollRatio() {
|
|
|
104
106
|
const maxY = Math.max(0, root.scrollHeight - root.clientHeight);
|
|
105
107
|
return maxY > 0 ? clamp(root.scrollTop / maxY, 0, 1) : 0;
|
|
106
108
|
}
|
|
107
|
-
function applyDocToCodeFlipPlanImpl(codePane, _docPane, plan) {
|
|
109
|
+
function applyDocToCodeFlipPlanImpl(codePane, _docPane, plan, lineIdPrefix = "code-line-") {
|
|
110
|
+
const narrowSinglePane = globalThis.matchMedia(DUAL_MOBILE_SINGLE_PANE_MQ).matches;
|
|
108
111
|
if (plan.k === "block") {
|
|
109
|
-
const
|
|
112
|
+
const exact = codePane.querySelector(`#${lineIdPrefix}${String(plan.src0)}`);
|
|
113
|
+
const el = exact instanceof HTMLElement
|
|
114
|
+
? exact
|
|
115
|
+
: findAnchorAtOrAfter(sourceAnchorsFromPrefix(lineIdPrefix), plan.src0);
|
|
110
116
|
if (el) {
|
|
111
117
|
applyRevealChildInPane(codePane, el, 2);
|
|
112
118
|
}
|
|
@@ -119,6 +125,8 @@ function applyDocToCodeFlipPlanImpl(codePane, _docPane, plan) {
|
|
|
119
125
|
if (paneUsesInternalYScroll(codePane)) {
|
|
120
126
|
const maxC = Math.max(0, codePane.scrollHeight - codePane.clientHeight);
|
|
121
127
|
applyScrollTopClamped(codePane, plan.ratio * maxC);
|
|
128
|
+
if (narrowSinglePane)
|
|
129
|
+
applyWindowScrollRatio(plan.ratio);
|
|
122
130
|
}
|
|
123
131
|
else {
|
|
124
132
|
applyWindowScrollRatio(plan.ratio);
|
|
@@ -128,6 +136,10 @@ function applyDocToCodeFlipPlanImpl(codePane, _docPane, plan) {
|
|
|
128
136
|
const nextTop = mirroredScrollTop(plan.docTop, plan.docSH, plan.docCH, codePane.scrollHeight, codePane.clientHeight);
|
|
129
137
|
if (paneUsesInternalYScroll(codePane)) {
|
|
130
138
|
applyScrollTopClamped(codePane, nextTop);
|
|
139
|
+
if (narrowSinglePane) {
|
|
140
|
+
const denom = Math.max(1, codePane.scrollHeight - codePane.clientHeight);
|
|
141
|
+
applyWindowScrollRatio(clamp(nextTop / denom, 0, 1));
|
|
142
|
+
}
|
|
131
143
|
return;
|
|
132
144
|
}
|
|
133
145
|
const denom = Math.max(1, codePane.scrollHeight - codePane.clientHeight);
|
|
@@ -163,7 +175,12 @@ function applyCodeToDocFlipPlanImpl(_codePane, docPane, plan) {
|
|
|
163
175
|
applyWindowScrollRatio(clamp(nextTop / denom, 0, 1));
|
|
164
176
|
}
|
|
165
177
|
function buildDocToCodeFlipPlanBlockAware(docPane, getLinks) {
|
|
166
|
-
const winRatio =
|
|
178
|
+
const winRatio = paneUsesInternalYScroll(docPane)
|
|
179
|
+
? clamp(docPane.scrollTop / Math.max(1, docPane.scrollHeight - docPane.clientHeight), 0, 1)
|
|
180
|
+
: windowScrollRatio();
|
|
181
|
+
const pulledSrc0 = pulledSourceLine0FromPageBreak(docPane);
|
|
182
|
+
if (pulledSrc0 !== null)
|
|
183
|
+
return { k: "block", src0: pulledSrc0, winRatio };
|
|
167
184
|
const links = getLinks();
|
|
168
185
|
const mdLine0 = probeCommentrayLine0FromDoc(docPane);
|
|
169
186
|
const src0 = pickSourceLine0ForCommentrayScroll(links, mdLine0);
|
|
@@ -179,10 +196,10 @@ function buildDocToCodeFlipPlanBlockAware(docPane, getLinks) {
|
|
|
179
196
|
}
|
|
180
197
|
return { k: "mirrorW", ratio: winRatio };
|
|
181
198
|
}
|
|
182
|
-
function buildCodeToDocFlipPlanBlockAware(codePane, _docPane, getLinks) {
|
|
199
|
+
function buildCodeToDocFlipPlanBlockAware(codePane, _docPane, getLinks, lineIdPrefix = "code-line-") {
|
|
183
200
|
const winRatio = windowScrollRatio();
|
|
184
201
|
const links = getLinks();
|
|
185
|
-
const line1 = probeCodeLine1FromViewport(codePane);
|
|
202
|
+
const line1 = probeCodeLine1FromViewport(codePane, lineIdPrefix);
|
|
186
203
|
const mdLine0 = pickCommentrayLineForSourceScroll(links, line1);
|
|
187
204
|
if (mdLine0 === null) {
|
|
188
205
|
if (paneUsesInternalYScroll(codePane)) {
|
|
@@ -684,11 +701,22 @@ function parseScrollBlockLinksFromShell(b64) {
|
|
|
684
701
|
typeof o.commentrayLine === "number" &&
|
|
685
702
|
typeof o.sourceStart === "number" &&
|
|
686
703
|
typeof o.sourceEnd === "number") {
|
|
704
|
+
const mvRaw = o.markerViewportHalfOpen1Based;
|
|
705
|
+
const mv = typeof mvRaw === "object" &&
|
|
706
|
+
mvRaw !== null &&
|
|
707
|
+
typeof mvRaw.lo === "number" &&
|
|
708
|
+
typeof mvRaw.hiExclusive === "number"
|
|
709
|
+
? {
|
|
710
|
+
lo: mvRaw.lo,
|
|
711
|
+
hiExclusive: mvRaw.hiExclusive,
|
|
712
|
+
}
|
|
713
|
+
: { lo: o.sourceStart, hiExclusive: o.sourceEnd + 1 };
|
|
687
714
|
out.push({
|
|
688
715
|
id: o.id,
|
|
689
716
|
commentrayLine: o.commentrayLine,
|
|
690
717
|
sourceStart: o.sourceStart,
|
|
691
718
|
sourceEnd: o.sourceEnd,
|
|
719
|
+
markerViewportHalfOpen1Based: mv,
|
|
692
720
|
});
|
|
693
721
|
}
|
|
694
722
|
}
|
|
@@ -703,14 +731,41 @@ function rootScrollNearDocumentEnd(edgePx = 56) {
|
|
|
703
731
|
const maxY = Math.max(0, root.scrollHeight - root.clientHeight);
|
|
704
732
|
return maxY > 0 && root.scrollTop >= maxY - edgePx;
|
|
705
733
|
}
|
|
706
|
-
|
|
707
|
-
|
|
734
|
+
/** When the pane itself is the scrollport (dual desktop), mirror root “near end” behavior. */
|
|
735
|
+
function paneScrollNearEnd(pane, edgePx = 56) {
|
|
736
|
+
const maxY = Math.max(0, pane.scrollHeight - pane.clientHeight);
|
|
737
|
+
return maxY > 0 && pane.scrollTop >= maxY - edgePx;
|
|
738
|
+
}
|
|
739
|
+
function readCommentrayLine0FromAnchor(el) {
|
|
740
|
+
const lineAttr = el.getAttribute("data-commentray-line");
|
|
741
|
+
if (lineAttr === null || lineAttr === "")
|
|
742
|
+
return null;
|
|
743
|
+
return Number(lineAttr);
|
|
744
|
+
}
|
|
745
|
+
function bestCommentrayAnchorLine0AtOrAboveY(anchors, y) {
|
|
746
|
+
const readings = [];
|
|
747
|
+
for (const a of anchors) {
|
|
748
|
+
const line0 = readCommentrayLine0FromAnchor(a);
|
|
749
|
+
if (line0 === null)
|
|
750
|
+
continue;
|
|
751
|
+
readings.push({ line0, top: a.getBoundingClientRect().top });
|
|
752
|
+
}
|
|
753
|
+
return maxCommentrayAnchorLine0AtOrAboveViewportY(readings, y);
|
|
754
|
+
}
|
|
755
|
+
function lastCommentrayAnchorLine0(anchors) {
|
|
756
|
+
const last = anchors[anchors.length - 1];
|
|
757
|
+
if (!last)
|
|
758
|
+
return 0;
|
|
759
|
+
return readCommentrayLine0FromAnchor(last) ?? 0;
|
|
760
|
+
}
|
|
761
|
+
function probeCodeLine1FromViewport(codePane, lineIdPrefix = "code-line-") {
|
|
762
|
+
const rows = codePane.querySelectorAll(`[id^="${lineIdPrefix}"]`);
|
|
708
763
|
if (rows.length === 0)
|
|
709
764
|
return 1;
|
|
710
765
|
if (!paneUsesInternalYScroll(codePane)) {
|
|
711
766
|
if (rootScrollNearDocumentEnd()) {
|
|
712
767
|
const last = rows[rows.length - 1];
|
|
713
|
-
const m = /^code-line-(\d+)$/.exec(last.id);
|
|
768
|
+
const m = /^(?:code-line-|code-md-line-)(\d+)$/.exec(last.id);
|
|
714
769
|
if (m)
|
|
715
770
|
return Number(m[1]) + 1;
|
|
716
771
|
return rows.length;
|
|
@@ -723,7 +778,7 @@ function probeCodeLine1FromViewport(codePane) {
|
|
|
723
778
|
for (const el of rows) {
|
|
724
779
|
const r = el.getBoundingClientRect();
|
|
725
780
|
if (r.bottom > y - 1e-3) {
|
|
726
|
-
const m = /^code-line-(\d+)$/.exec(el.id);
|
|
781
|
+
const m = /^(?:code-line-|code-md-line-)(\d+)$/.exec(el.id);
|
|
727
782
|
if (m)
|
|
728
783
|
return Number(m[1]) + 1;
|
|
729
784
|
return 1;
|
|
@@ -731,12 +786,19 @@ function probeCodeLine1FromViewport(codePane) {
|
|
|
731
786
|
}
|
|
732
787
|
return rows.length;
|
|
733
788
|
}
|
|
789
|
+
if (paneScrollNearEnd(codePane)) {
|
|
790
|
+
const last = rows[rows.length - 1];
|
|
791
|
+
const m = /^(?:code-line-|code-md-line-)(\d+)$/.exec(last.id);
|
|
792
|
+
if (m)
|
|
793
|
+
return Number(m[1]) + 1;
|
|
794
|
+
return rows.length;
|
|
795
|
+
}
|
|
734
796
|
const sr = codePane.getBoundingClientRect();
|
|
735
797
|
const y = sr.top + codePane.clientTop + 2;
|
|
736
798
|
for (const el of rows) {
|
|
737
799
|
const r = el.getBoundingClientRect();
|
|
738
800
|
if (r.bottom > y - 1e-3) {
|
|
739
|
-
const m = /^code-line-(\d+)$/.exec(el.id);
|
|
801
|
+
const m = /^(?:code-line-|code-md-line-)(\d+)$/.exec(el.id);
|
|
740
802
|
if (m)
|
|
741
803
|
return Number(m[1]) + 1;
|
|
742
804
|
return 1;
|
|
@@ -749,41 +811,75 @@ function probeCommentrayLine0FromDoc(docPane) {
|
|
|
749
811
|
if (anchors.length === 0)
|
|
750
812
|
return 0;
|
|
751
813
|
if (!paneUsesInternalYScroll(docPane)) {
|
|
752
|
-
if (rootScrollNearDocumentEnd())
|
|
753
|
-
|
|
754
|
-
const lineAttr = last.getAttribute("data-commentray-line");
|
|
755
|
-
return lineAttr !== null && lineAttr !== "" ? Number(lineAttr) : 0;
|
|
756
|
-
}
|
|
814
|
+
if (rootScrollNearDocumentEnd())
|
|
815
|
+
return lastCommentrayAnchorLine0(anchors);
|
|
757
816
|
const dr = docPane.getBoundingClientRect();
|
|
758
817
|
const vh = globalThis.innerHeight;
|
|
759
818
|
const clipT = Math.max(0, dr.top);
|
|
760
819
|
const clipB = Math.min(vh, dr.bottom);
|
|
761
820
|
const y = clipT + Math.max(2, Math.min(40, (clipB - clipT) * 0.15));
|
|
762
|
-
|
|
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;
|
|
821
|
+
return bestCommentrayAnchorLine0AtOrAboveY(anchors, y);
|
|
773
822
|
}
|
|
823
|
+
if (paneScrollNearEnd(docPane))
|
|
824
|
+
return lastCommentrayAnchorLine0(anchors);
|
|
774
825
|
const dr = docPane.getBoundingClientRect();
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
826
|
+
/** Same band as the root-scroll branch: a few px below the pane top so block anchors sit inside `top <= y` while their prose is what the reader sees first. */
|
|
827
|
+
const y = dr.top + docPane.clientTop + Math.max(2, Math.min(40, docPane.clientHeight * 0.15));
|
|
828
|
+
return bestCommentrayAnchorLine0AtOrAboveY(anchors, y);
|
|
829
|
+
}
|
|
830
|
+
function pageBreakPullEnabled() {
|
|
831
|
+
const shell = document.getElementById("shell");
|
|
832
|
+
if (!(shell instanceof HTMLElement))
|
|
833
|
+
return false;
|
|
834
|
+
return shell.getAttribute("data-page-breaks-enabled") === "true";
|
|
835
|
+
}
|
|
836
|
+
function docProbeTopY(docPane) {
|
|
837
|
+
if (!paneUsesInternalYScroll(docPane)) {
|
|
838
|
+
const dr = docPane.getBoundingClientRect();
|
|
839
|
+
const vh = globalThis.innerHeight;
|
|
840
|
+
const clipT = Math.max(0, dr.top);
|
|
841
|
+
const clipB = Math.min(vh, dr.bottom);
|
|
842
|
+
return clipT + Math.max(2, Math.min(40, (clipB - clipT) * 0.15));
|
|
843
|
+
}
|
|
844
|
+
const dr = docPane.getBoundingClientRect();
|
|
845
|
+
return dr.top + docPane.clientTop + 2;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* In long synthetic page-break gaps, shift source toward the next block once
|
|
849
|
+
* the break itself occupies the top reading position.
|
|
850
|
+
*/
|
|
851
|
+
function pulledSourceLine0FromPageBreak(docPane) {
|
|
852
|
+
if (!pageBreakPullEnabled())
|
|
853
|
+
return null;
|
|
854
|
+
const topY = docProbeTopY(docPane);
|
|
855
|
+
const breaks = Array.from(docPane.querySelectorAll(".commentray-page-break[data-next-source-start]"));
|
|
856
|
+
for (const pageBreak of breaks) {
|
|
857
|
+
const nextSourceStartRaw = pageBreak.getAttribute("data-next-source-start");
|
|
858
|
+
if (!nextSourceStartRaw)
|
|
780
859
|
continue;
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
860
|
+
const nextSourceStart = Number.parseInt(nextSourceStartRaw, 10);
|
|
861
|
+
if (!Number.isFinite(nextSourceStart) || nextSourceStart <= 0)
|
|
862
|
+
continue;
|
|
863
|
+
const breakTop = pageBreak.getBoundingClientRect().top;
|
|
864
|
+
const nextLineRaw = pageBreak.getAttribute("data-next-commentray-line");
|
|
865
|
+
const nextLine0 = nextLineRaw ? Number.parseInt(nextLineRaw, 10) : Number.NaN;
|
|
866
|
+
const nextAnchor = Number.isFinite(nextLine0) && nextLine0 >= 0
|
|
867
|
+
? docPane.querySelector(`[data-commentray-line="${String(nextLine0)}"]`)
|
|
868
|
+
: null;
|
|
869
|
+
const nextTop = nextAnchor
|
|
870
|
+
? nextAnchor.getBoundingClientRect().top
|
|
871
|
+
: breakTop + pageBreak.clientHeight;
|
|
872
|
+
if (!(breakTop <= topY && topY < nextTop))
|
|
873
|
+
continue;
|
|
874
|
+
const denom = Math.max(1, nextTop - breakTop);
|
|
875
|
+
const progress = clamp((topY - breakTop) / denom, 0, 1);
|
|
876
|
+
const narrow = globalThis.matchMedia("(max-width: 767px)").matches;
|
|
877
|
+
const pullThreshold = narrow ? 0.2 : 0.35;
|
|
878
|
+
if (progress < pullThreshold)
|
|
879
|
+
return null;
|
|
880
|
+
return nextSourceStart - 1;
|
|
785
881
|
}
|
|
786
|
-
return
|
|
882
|
+
return null;
|
|
787
883
|
}
|
|
788
884
|
/**
|
|
789
885
|
* Programmatic `scrollTop` on the partner pane can emit a `scroll` event after the
|
|
@@ -832,16 +928,20 @@ function wireBidirectionalScroll(codePane, docPane, syncFromCode, syncFromDoc) {
|
|
|
832
928
|
}, { passive: true });
|
|
833
929
|
}
|
|
834
930
|
/** Index-backed scroll sync when `data-scroll-block-links-b64` is present; else see proportional fallback. */
|
|
835
|
-
function wireBlockAwareScrollSync(codePane, docPane, getLinks) {
|
|
931
|
+
function wireBlockAwareScrollSync(codePane, docPane, getLinks, lineIdPrefix, shouldUseProportionalDocToCodeOnMobileFlip) {
|
|
836
932
|
let pendingDocToCode = null;
|
|
837
933
|
let pendingCodeToDoc = null;
|
|
838
934
|
const syncFromCodeToDoc = () => {
|
|
839
|
-
applyCodeToDocFlipPlanImpl(codePane, docPane, buildCodeToDocFlipPlanBlockAware(codePane, docPane, getLinks));
|
|
935
|
+
applyCodeToDocFlipPlanImpl(codePane, docPane, buildCodeToDocFlipPlanBlockAware(codePane, docPane, getLinks, lineIdPrefix()));
|
|
840
936
|
};
|
|
841
937
|
const syncFromDocToCode = () => {
|
|
842
|
-
applyDocToCodeFlipPlanImpl(codePane, docPane, buildDocToCodeFlipPlanBlockAware(docPane, getLinks));
|
|
938
|
+
applyDocToCodeFlipPlanImpl(codePane, docPane, buildDocToCodeFlipPlanBlockAware(docPane, getLinks), lineIdPrefix());
|
|
843
939
|
};
|
|
844
940
|
const prepareMobileFlipToCode = () => {
|
|
941
|
+
if (shouldUseProportionalDocToCodeOnMobileFlip?.() === true) {
|
|
942
|
+
pendingDocToCode = { k: "mirrorW", ratio: windowScrollRatio() };
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
845
945
|
pendingDocToCode = buildDocToCodeFlipPlanBlockAware(docPane, getLinks);
|
|
846
946
|
};
|
|
847
947
|
const finishMobileFlipToCode = () => {
|
|
@@ -849,10 +949,10 @@ function wireBlockAwareScrollSync(codePane, docPane, getLinks) {
|
|
|
849
949
|
return;
|
|
850
950
|
const p = pendingDocToCode;
|
|
851
951
|
pendingDocToCode = null;
|
|
852
|
-
applyDocToCodeFlipPlanImpl(codePane, docPane, p);
|
|
952
|
+
applyDocToCodeFlipPlanImpl(codePane, docPane, p, lineIdPrefix());
|
|
853
953
|
};
|
|
854
954
|
const prepareMobileFlipToDoc = () => {
|
|
855
|
-
pendingCodeToDoc = buildCodeToDocFlipPlanBlockAware(codePane, docPane, getLinks);
|
|
955
|
+
pendingCodeToDoc = buildCodeToDocFlipPlanBlockAware(codePane, docPane, getLinks, lineIdPrefix());
|
|
856
956
|
};
|
|
857
957
|
const finishMobileFlipToDoc = () => {
|
|
858
958
|
if (!pendingCodeToDoc)
|
|
@@ -924,18 +1024,76 @@ function centerYInViewport(el) {
|
|
|
924
1024
|
function codeLineHighlightCenterYViewport(lineEl) {
|
|
925
1025
|
return centerYInViewport(lineEl);
|
|
926
1026
|
}
|
|
927
|
-
function commentaryBandEndYViewport(docScrollEl, next, docTop) {
|
|
1027
|
+
function commentaryBandEndYViewport(docScrollEl, next, docTop, clipThroughPageBreakGaps) {
|
|
928
1028
|
if (next) {
|
|
929
1029
|
const nextEl = document.getElementById(`commentray-block-${next.id}`);
|
|
930
|
-
|
|
1030
|
+
if (!nextEl)
|
|
1031
|
+
return centerYInViewport(docTop);
|
|
1032
|
+
const nextTop = nextEl.getBoundingClientRect().top - 3;
|
|
1033
|
+
if (!clipThroughPageBreakGaps)
|
|
1034
|
+
return nextTop;
|
|
1035
|
+
const docBandTop = docTop.getBoundingClientRect().top + 4;
|
|
1036
|
+
const contentBottom = maxRenderableCommentaryContentBottomViewport(docScrollEl, docTop, nextEl);
|
|
1037
|
+
return Math.min(nextTop, Math.max(docBandTop, contentBottom));
|
|
931
1038
|
}
|
|
932
1039
|
const dr = docScrollEl.getBoundingClientRect();
|
|
933
1040
|
let bottom = dr.bottom - 4;
|
|
934
1041
|
const lastKid = docScrollEl.children[docScrollEl.children.length - 1];
|
|
935
1042
|
if (lastKid)
|
|
936
1043
|
bottom = Math.min(bottom, lastKid.getBoundingClientRect().bottom - 4);
|
|
1044
|
+
if (clipThroughPageBreakGaps) {
|
|
1045
|
+
const docBandTop = docTop.getBoundingClientRect().top + 4;
|
|
1046
|
+
const contentBottom = maxRenderableCommentaryContentBottomViewport(docScrollEl, docTop, null);
|
|
1047
|
+
bottom = Math.min(bottom, Math.max(docBandTop, contentBottom));
|
|
1048
|
+
}
|
|
937
1049
|
return bottom;
|
|
938
1050
|
}
|
|
1051
|
+
function sourceAnchorIndexFromId(id, prefix) {
|
|
1052
|
+
if (!id.startsWith(prefix))
|
|
1053
|
+
return null;
|
|
1054
|
+
const n = Number.parseInt(id.slice(prefix.length), 10);
|
|
1055
|
+
return Number.isFinite(n) ? n : null;
|
|
1056
|
+
}
|
|
1057
|
+
function findAnchorAtOrAfter(anchors, line0) {
|
|
1058
|
+
let lo = 0;
|
|
1059
|
+
let hi = anchors.length - 1;
|
|
1060
|
+
let ans = -1;
|
|
1061
|
+
while (lo <= hi) {
|
|
1062
|
+
const mid = (lo + hi) >> 1;
|
|
1063
|
+
const line = anchors[mid]?.line0 ?? -1;
|
|
1064
|
+
if (line >= line0) {
|
|
1065
|
+
ans = mid;
|
|
1066
|
+
hi = mid - 1;
|
|
1067
|
+
}
|
|
1068
|
+
else {
|
|
1069
|
+
lo = mid + 1;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return ans >= 0 ? (anchors[ans]?.el ?? null) : null;
|
|
1073
|
+
}
|
|
1074
|
+
function findAnchorAtOrBefore(anchors, line0) {
|
|
1075
|
+
let lo = 0;
|
|
1076
|
+
let hi = anchors.length - 1;
|
|
1077
|
+
let ans = -1;
|
|
1078
|
+
while (lo <= hi) {
|
|
1079
|
+
const mid = (lo + hi) >> 1;
|
|
1080
|
+
const line = anchors[mid]?.line0 ?? -1;
|
|
1081
|
+
if (line <= line0) {
|
|
1082
|
+
ans = mid;
|
|
1083
|
+
lo = mid + 1;
|
|
1084
|
+
}
|
|
1085
|
+
else {
|
|
1086
|
+
hi = mid - 1;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return ans >= 0 ? (anchors[ans]?.el ?? null) : null;
|
|
1090
|
+
}
|
|
1091
|
+
function sourceAnchorsFromPrefix(prefix) {
|
|
1092
|
+
return Array.from(document.querySelectorAll(`[id^="${prefix}"]`))
|
|
1093
|
+
.map((el) => ({ line0: sourceAnchorIndexFromId(el.id, prefix), el }))
|
|
1094
|
+
.filter((x) => x.line0 !== null)
|
|
1095
|
+
.sort((a, b) => a.line0 - b.line0);
|
|
1096
|
+
}
|
|
939
1097
|
function subscribeBlockRayRedraw(gutter, codePane, docScrollEl, scheduleDraw) {
|
|
940
1098
|
const onScrollOrResize = () => scheduleDraw();
|
|
941
1099
|
codePane.addEventListener("scroll", onScrollOrResize, { passive: true });
|
|
@@ -949,8 +1107,8 @@ function subscribeBlockRayRedraw(gutter, codePane, docScrollEl, scheduleDraw) {
|
|
|
949
1107
|
if (shell)
|
|
950
1108
|
ro.observe(shell);
|
|
951
1109
|
}
|
|
952
|
-
function drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSourceLine1Based) {
|
|
953
|
-
const links = getLinks();
|
|
1110
|
+
function drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSourceLine1Based, lineIdPrefix) {
|
|
1111
|
+
const links = dedupeBlockScrollLinksById(getLinks());
|
|
954
1112
|
const sorted = sortBlockLinksBySource(links);
|
|
955
1113
|
const gutterRect = gutter.getBoundingClientRect();
|
|
956
1114
|
const w = gutterRect.width;
|
|
@@ -959,23 +1117,33 @@ function drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSource
|
|
|
959
1117
|
svg.replaceChildren();
|
|
960
1118
|
return;
|
|
961
1119
|
}
|
|
962
|
-
|
|
1120
|
+
/** Doc-aligned active block matches visible commentary; code-only probe can lag in page gaps. */
|
|
1121
|
+
const activeId = docScrollEl.querySelector(".commentray-block-anchor") !== null
|
|
1122
|
+
? activeBlockIdForCommentrayLine0(links, probeCommentrayLine0FromDoc(docScrollEl))
|
|
1123
|
+
: activeBlockIdForViewport(links, probeTopSourceLine1Based());
|
|
1124
|
+
const clipGutterRaysThroughPageBreakGaps = pageBreakPullEnabled();
|
|
963
1125
|
svg.setAttribute("viewBox", `0 0 ${String(w)} ${String(h)}`);
|
|
964
1126
|
svg.setAttribute("preserveAspectRatio", "none");
|
|
965
1127
|
const parts = [];
|
|
1128
|
+
const sourceAnchors = Array.from(document.querySelectorAll(`[id^="${lineIdPrefix}"]`))
|
|
1129
|
+
.map((el) => ({ line0: sourceAnchorIndexFromId(el.id, lineIdPrefix), el }))
|
|
1130
|
+
.filter((x) => x.line0 !== null)
|
|
1131
|
+
.sort((a, b) => a.line0 - b.line0);
|
|
966
1132
|
for (let i = 0; i < sorted.length; i++) {
|
|
967
1133
|
const link = sorted[i];
|
|
968
1134
|
if (!link)
|
|
969
1135
|
continue;
|
|
970
|
-
const next =
|
|
1136
|
+
const next = nextBlockLinkInCommentrayOrder(links, link);
|
|
971
1137
|
const i0 = codeLineDomIndex0(link.sourceStart);
|
|
972
1138
|
const i1 = codeLineDomIndex0(link.sourceEnd);
|
|
973
|
-
const codeTop = document.getElementById(
|
|
974
|
-
|
|
1139
|
+
const codeTop = document.getElementById(`${lineIdPrefix}${String(i0)}`) ??
|
|
1140
|
+
findAnchorAtOrAfter(sourceAnchors, i0);
|
|
1141
|
+
const codeBot = document.getElementById(`${lineIdPrefix}${String(i1)}`) ??
|
|
1142
|
+
findAnchorAtOrBefore(sourceAnchors, i1);
|
|
975
1143
|
const docTop = document.getElementById(`commentray-block-${link.id}`);
|
|
976
1144
|
if (!codeTop || !codeBot || !docTop)
|
|
977
1145
|
continue;
|
|
978
|
-
const docEndYViewport = commentaryBandEndYViewport(docScrollEl, next, docTop);
|
|
1146
|
+
const docEndYViewport = commentaryBandEndYViewport(docScrollEl, next, docTop, clipGutterRaysThroughPageBreakGaps);
|
|
979
1147
|
const yCodeTop = codeLineHighlightCenterYViewport(codeTop);
|
|
980
1148
|
const yCodeBot = codeLineHighlightCenterYViewport(codeBot);
|
|
981
1149
|
const yDocTop = docTop.getBoundingClientRect().top + 2;
|
|
@@ -1009,13 +1177,14 @@ function drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSource
|
|
|
1009
1177
|
}
|
|
1010
1178
|
/**
|
|
1011
1179
|
* 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
|
|
1013
|
-
* off-screen endpoints so readers see which way to scroll.
|
|
1180
|
+
* index-backed blocks). Emphasizes the block aligned with the **doc** viewport when block anchors
|
|
1181
|
+
* exist; otherwise the source viewport. Clamps off-screen endpoints so readers see which way to scroll.
|
|
1014
1182
|
*
|
|
1015
1183
|
* @returns Request a redraw after DOM changes that do not resize the panes (e.g. multi-angle body swap).
|
|
1016
1184
|
*/
|
|
1017
1185
|
function wireBlockRayConnectors(args) {
|
|
1018
1186
|
const { gutter, codePane, docScrollEl, getLinks, probeTopSourceLine1Based } = args;
|
|
1187
|
+
const sourceLineIdPrefix = args.sourceLineIdPrefix ?? (() => "code-line-");
|
|
1019
1188
|
const svgNs = "http://www.w3.org/2000/svg";
|
|
1020
1189
|
const host = document.createElement("div");
|
|
1021
1190
|
host.className = "gutter__rays";
|
|
@@ -1029,7 +1198,7 @@ function wireBlockRayConnectors(args) {
|
|
|
1029
1198
|
return;
|
|
1030
1199
|
raf = globalThis.requestAnimationFrame(() => {
|
|
1031
1200
|
raf = 0;
|
|
1032
|
-
drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSourceLine1Based);
|
|
1201
|
+
drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSourceLine1Based, sourceLineIdPrefix());
|
|
1033
1202
|
});
|
|
1034
1203
|
}
|
|
1035
1204
|
subscribeBlockRayRedraw(gutter, codePane, docScrollEl, scheduleDraw);
|
|
@@ -1183,6 +1352,40 @@ function treeFileLinkTitle(pr) {
|
|
|
1183
1352
|
}
|
|
1184
1353
|
return pr.sourcePath;
|
|
1185
1354
|
}
|
|
1355
|
+
function clearDocumentedTreePairHighlights(tree) {
|
|
1356
|
+
for (const el of tree.querySelectorAll("a.tree-file-link")) {
|
|
1357
|
+
if (!(el instanceof HTMLAnchorElement))
|
|
1358
|
+
continue;
|
|
1359
|
+
el.classList.remove("tree-file-link--current");
|
|
1360
|
+
el.removeAttribute("aria-current");
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
function markFirstDocumentedTreeLinkMatchingPair(tree, curSrc, curCr) {
|
|
1364
|
+
for (const el of tree.querySelectorAll("a.tree-file-link")) {
|
|
1365
|
+
if (!(el instanceof HTMLAnchorElement))
|
|
1366
|
+
continue;
|
|
1367
|
+
const sp = el.getAttribute("data-pair-source-path")?.trim() ?? "";
|
|
1368
|
+
const cp = el.getAttribute("data-pair-commentray-path")?.trim() ?? "";
|
|
1369
|
+
if (!isSameDocumentedPair({ sourcePath: sp, commentrayPath: cp }, curSrc, curCr))
|
|
1370
|
+
continue;
|
|
1371
|
+
el.classList.add("tree-file-link--current");
|
|
1372
|
+
el.setAttribute("aria-current", "page");
|
|
1373
|
+
break;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
/** Marks the tree link for the pair shown in `#shell` (pair paths from server or multi-angle swap). */
|
|
1377
|
+
function applyDocumentedTreeCurrentPairHighlight() {
|
|
1378
|
+
const shell = document.getElementById("shell");
|
|
1379
|
+
const tree = document.getElementById("documented-files-tree");
|
|
1380
|
+
if (!(shell instanceof HTMLElement) || !(tree instanceof HTMLElement))
|
|
1381
|
+
return;
|
|
1382
|
+
clearDocumentedTreePairHighlights(tree);
|
|
1383
|
+
const curSrc = shell.getAttribute("data-commentray-pair-source-path")?.trim() ?? "";
|
|
1384
|
+
const curCr = shell.getAttribute("data-commentray-pair-commentray-path")?.trim() ?? "";
|
|
1385
|
+
if (curSrc.length === 0 || curCr.length === 0)
|
|
1386
|
+
return;
|
|
1387
|
+
markFirstDocumentedTreeLinkMatchingPair(tree, curSrc, curCr);
|
|
1388
|
+
}
|
|
1186
1389
|
function renderDocumentedTreeHtml(node) {
|
|
1187
1390
|
const keys = [...node.children.keys()].sort((a, b) => a.localeCompare(b));
|
|
1188
1391
|
if (keys.length === 0)
|
|
@@ -1202,10 +1405,12 @@ function renderDocumentedTreeHtml(node) {
|
|
|
1202
1405
|
const label = escapeHtmlText(treeFileLinkLabel(pr, multi));
|
|
1203
1406
|
const title = escapeHtmlText(treeFileLinkTitle(pr));
|
|
1204
1407
|
const href = escapeHtmlText(treeFileLinkHref(pr));
|
|
1408
|
+
const spAttr = escapeHtmlText(normPosixPath(pr.sourcePath));
|
|
1409
|
+
const crAttr = escapeHtmlText(normPosixPath(pr.commentrayPath));
|
|
1205
1410
|
const useSiteBrowse = (pr.staticBrowseUrl?.trim() ?? "").length > 0;
|
|
1206
1411
|
const external = useSiteBrowse ? "" : ' target="_blank" rel="noopener noreferrer"';
|
|
1207
1412
|
lis.push(`<li><div class="tree-file">` +
|
|
1208
|
-
`<a class="tree-file-link" href="${href}"${external} title="${title}">${label}</a>` +
|
|
1413
|
+
`<a class="tree-file-link" href="${href}" data-pair-source-path="${spAttr}" data-pair-commentray-path="${crAttr}"${external} title="${title}">${label}</a>` +
|
|
1209
1414
|
`</div></li>`);
|
|
1210
1415
|
}
|
|
1211
1416
|
}
|
|
@@ -1223,6 +1428,7 @@ function renderDocumentedPairsIntoHost(treeHost, pairs, emptyBecauseFilter) {
|
|
|
1223
1428
|
for (const p of pairs)
|
|
1224
1429
|
insertSourcePathTrie(root, p);
|
|
1225
1430
|
treeHost.innerHTML = renderDocumentedTreeHtml(root);
|
|
1431
|
+
applyDocumentedTreeCurrentPairHighlight();
|
|
1226
1432
|
}
|
|
1227
1433
|
function loadDocumentedPairs(jsonUrl, embeddedB64) {
|
|
1228
1434
|
let loaded = null;
|
|
@@ -1298,6 +1504,12 @@ function wireDocumentedFilesTreeMobileFlyout(hub) {
|
|
|
1298
1504
|
globalThis.addEventListener("scroll", placeFlyout, true);
|
|
1299
1505
|
return placeFlyout;
|
|
1300
1506
|
}
|
|
1507
|
+
function focusDocumentedFilesFilterInput() {
|
|
1508
|
+
const el = document.getElementById("documented-files-filter");
|
|
1509
|
+
if (!(el instanceof HTMLInputElement))
|
|
1510
|
+
return;
|
|
1511
|
+
el.focus({ preventScroll: true });
|
|
1512
|
+
}
|
|
1301
1513
|
function wireDocumentedFilesTree() {
|
|
1302
1514
|
const hub = document.getElementById("documented-files-hub");
|
|
1303
1515
|
const treeHost = document.getElementById("documented-files-tree");
|
|
@@ -1306,12 +1518,13 @@ function wireDocumentedFilesTree() {
|
|
|
1306
1518
|
if (!(hub instanceof HTMLDetailsElement) || !(treeHost instanceof HTMLElement)) {
|
|
1307
1519
|
return;
|
|
1308
1520
|
}
|
|
1521
|
+
const detailsHub = hub;
|
|
1309
1522
|
const treeMount = treeHost;
|
|
1310
|
-
const jsonUrl =
|
|
1523
|
+
const jsonUrl = detailsHub.getAttribute("data-nav-json-url")?.trim() ?? "";
|
|
1311
1524
|
const embeddedB64 = shell?.getAttribute("data-documented-pairs-b64")?.trim() ?? "";
|
|
1312
1525
|
if (jsonUrl.length === 0 && embeddedB64.length === 0)
|
|
1313
1526
|
return;
|
|
1314
|
-
const placeDocHubFlyout = wireDocumentedFilesTreeMobileFlyout(
|
|
1527
|
+
const placeDocHubFlyout = wireDocumentedFilesTreeMobileFlyout(detailsHub);
|
|
1315
1528
|
const ensureLoaded = loadDocumentedPairs(jsonUrl, embeddedB64);
|
|
1316
1529
|
let cachedPairs = null;
|
|
1317
1530
|
function applyFilterAndRender() {
|
|
@@ -1334,18 +1547,31 @@ function wireDocumentedFilesTree() {
|
|
|
1334
1547
|
'<p class="nav-rail__doc-hub-hint" role="alert">Could not load the file list.</p>';
|
|
1335
1548
|
}
|
|
1336
1549
|
}
|
|
1337
|
-
|
|
1550
|
+
detailsHub.addEventListener("toggle", () => {
|
|
1338
1551
|
placeDocHubFlyout();
|
|
1339
|
-
if (
|
|
1340
|
-
globalThis.requestAnimationFrame(
|
|
1552
|
+
if (detailsHub.open) {
|
|
1553
|
+
globalThis.requestAnimationFrame(() => {
|
|
1554
|
+
placeDocHubFlyout();
|
|
1555
|
+
focusDocumentedFilesFilterInput();
|
|
1556
|
+
});
|
|
1341
1557
|
}
|
|
1342
|
-
if (!
|
|
1558
|
+
if (!detailsHub.open)
|
|
1343
1559
|
return;
|
|
1344
1560
|
void hydrateTree();
|
|
1345
1561
|
});
|
|
1562
|
+
function onDocumentedFilesHubEscape(ev) {
|
|
1563
|
+
if (!detailsHub.open || ev.key !== "Escape")
|
|
1564
|
+
return;
|
|
1565
|
+
ev.preventDefault();
|
|
1566
|
+
detailsHub.open = false;
|
|
1567
|
+
const sum = detailsHub.querySelector("summary");
|
|
1568
|
+
if (sum instanceof HTMLElement)
|
|
1569
|
+
sum.focus({ preventScroll: true });
|
|
1570
|
+
}
|
|
1571
|
+
document.addEventListener("keydown", onDocumentedFilesHubEscape, true);
|
|
1346
1572
|
if (filterInput instanceof HTMLInputElement) {
|
|
1347
1573
|
filterInput.addEventListener("input", () => {
|
|
1348
|
-
if (!
|
|
1574
|
+
if (!detailsHub.open || cachedPairs === null)
|
|
1349
1575
|
return;
|
|
1350
1576
|
applyFilterAndRender();
|
|
1351
1577
|
});
|
|
@@ -1384,11 +1610,61 @@ function wireSplitter(storageSplit, shell, codePane, gutter, initialPct) {
|
|
|
1384
1610
|
const STORAGE_SPLIT_PCT = "commentray.codeCommentrayStatic.splitPct";
|
|
1385
1611
|
const STORAGE_WRAP_LINES = "commentray.codeCommentrayStatic.wrap";
|
|
1386
1612
|
const STORAGE_DUAL_MOBILE_PANE = "commentray.codeCommentrayStatic.dualMobilePane";
|
|
1613
|
+
const STORAGE_SOURCE_MARKDOWN_PANE_MODE = "commentray.codeCommentrayStatic.sourceMarkdownPaneMode";
|
|
1614
|
+
const STORAGE_PAGE_BREAKS_ENABLED = "commentray.codeCommentrayStatic.pageBreaksEnabled";
|
|
1387
1615
|
/** Matches `code-browser.ts` `@media (max-width: 767px)` (dual column from 768px up). */
|
|
1388
1616
|
const DUAL_MOBILE_SINGLE_PANE_MQ = "(max-width: 767px)";
|
|
1389
1617
|
function normalizedDualMobilePane(v) {
|
|
1390
1618
|
return v === "code" ? "code" : "doc";
|
|
1391
1619
|
}
|
|
1620
|
+
function isNarrowViewport() {
|
|
1621
|
+
return globalThis.matchMedia(DUAL_MOBILE_SINGLE_PANE_MQ).matches;
|
|
1622
|
+
}
|
|
1623
|
+
function wireWideModeIntroTrigger(shell) {
|
|
1624
|
+
const btn = document.getElementById("commentray-help-tour");
|
|
1625
|
+
if (!(btn instanceof HTMLButtonElement))
|
|
1626
|
+
return;
|
|
1627
|
+
btn.addEventListener("click", () => {
|
|
1628
|
+
wireWideModeIntroTour(shell, isNarrowViewport, { force: true });
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
function sourcePaneModeForShell(shell) {
|
|
1632
|
+
return shell.getAttribute("data-source-pane-mode") === "rendered-markdown"
|
|
1633
|
+
? "rendered-markdown"
|
|
1634
|
+
: "source";
|
|
1635
|
+
}
|
|
1636
|
+
function pageBreaksEnabledFromStorage(raw) {
|
|
1637
|
+
const t = (raw ?? "").trim().toLowerCase();
|
|
1638
|
+
if (t === "0" || t === "false" || t === "off")
|
|
1639
|
+
return false;
|
|
1640
|
+
return true;
|
|
1641
|
+
}
|
|
1642
|
+
function applyPageBreakFeatureToggle(shell) {
|
|
1643
|
+
const enabled = pageBreaksEnabledFromStorage(readWebStorageItem(localStorage, STORAGE_PAGE_BREAKS_ENABLED));
|
|
1644
|
+
shell.setAttribute("data-page-breaks-enabled", enabled ? "true" : "false");
|
|
1645
|
+
}
|
|
1646
|
+
function wireResponsivePageBreakHeight(shell) {
|
|
1647
|
+
const setHeight = () => {
|
|
1648
|
+
const viewportHeight = Math.max(globalThis.innerHeight, document.documentElement?.clientHeight ?? 0);
|
|
1649
|
+
if (!Number.isFinite(viewportHeight) || viewportHeight <= 0)
|
|
1650
|
+
return;
|
|
1651
|
+
const minHeightPx = Math.round(clamp(viewportHeight * 0.72, 260, 820));
|
|
1652
|
+
shell.style.setProperty("--commentray-page-break-min-height", `${String(minHeightPx)}px`);
|
|
1653
|
+
};
|
|
1654
|
+
globalThis.addEventListener("resize", setHeight, { passive: true });
|
|
1655
|
+
globalThis.addEventListener("orientationchange", setHeight, { passive: true });
|
|
1656
|
+
globalThis.visualViewport?.addEventListener("resize", setHeight, { passive: true });
|
|
1657
|
+
setHeight();
|
|
1658
|
+
}
|
|
1659
|
+
function syncWrapLinesVisibilityForSourcePaneMode(shell) {
|
|
1660
|
+
const wrapToggle = document.querySelector("label.toolbar-wrap-lines");
|
|
1661
|
+
if (!(wrapToggle instanceof HTMLLabelElement))
|
|
1662
|
+
return;
|
|
1663
|
+
wrapToggle.hidden = sourcePaneModeForShell(shell) === "rendered-markdown";
|
|
1664
|
+
}
|
|
1665
|
+
function sourceLineIdPrefixForShell(shell) {
|
|
1666
|
+
return sourcePaneModeForShell(shell) === "rendered-markdown" ? "code-md-line-" : "code-line-";
|
|
1667
|
+
}
|
|
1392
1668
|
/** When the commentary pane is visible, (re)run Mermaid so diagrams are not laid out under display:none. */
|
|
1393
1669
|
function scheduleMermaidWhenDualDocPaneVisible(shell, mq) {
|
|
1394
1670
|
const kick = () => {
|
|
@@ -1440,6 +1716,105 @@ function wireDualMobilePaneFlipScrollAffordance(primaryFlip, scrollFlip, mq) {
|
|
|
1440
1716
|
mq.addEventListener("change", tick);
|
|
1441
1717
|
globalThis.requestAnimationFrame(tick);
|
|
1442
1718
|
}
|
|
1719
|
+
function wireSourceMarkdownPaneFlipAffordance(primaryFlip, scrollFlip) {
|
|
1720
|
+
const hideScroll = () => {
|
|
1721
|
+
scrollFlip.hidden = true;
|
|
1722
|
+
scrollFlip.classList.remove("is-visible");
|
|
1723
|
+
};
|
|
1724
|
+
const showScroll = () => {
|
|
1725
|
+
scrollFlip.hidden = false;
|
|
1726
|
+
scrollFlip.classList.add("is-visible");
|
|
1727
|
+
};
|
|
1728
|
+
const tick = () => {
|
|
1729
|
+
const r = primaryFlip.getBoundingClientRect();
|
|
1730
|
+
const vh = globalThis.innerHeight;
|
|
1731
|
+
const margin = 10;
|
|
1732
|
+
const offScreen = r.bottom < margin || r.top > vh - margin;
|
|
1733
|
+
if (offScreen)
|
|
1734
|
+
showScroll();
|
|
1735
|
+
else
|
|
1736
|
+
hideScroll();
|
|
1737
|
+
};
|
|
1738
|
+
globalThis.addEventListener("scroll", tick, { passive: true });
|
|
1739
|
+
globalThis.addEventListener("resize", tick, { passive: true });
|
|
1740
|
+
globalThis.requestAnimationFrame(tick);
|
|
1741
|
+
}
|
|
1742
|
+
function closestSourceLine0ForPaneTop(codePane, idPrefix) {
|
|
1743
|
+
const rows = codePane.querySelectorAll(`[id^="${idPrefix}"]`);
|
|
1744
|
+
if (rows.length === 0)
|
|
1745
|
+
return null;
|
|
1746
|
+
const y = paneUsesInternalYScroll(codePane)
|
|
1747
|
+
? codePane.getBoundingClientRect().top + codePane.clientTop + 2
|
|
1748
|
+
: Math.max(0, codePane.getBoundingClientRect().top) + 2;
|
|
1749
|
+
for (const el of rows) {
|
|
1750
|
+
const r = el.getBoundingClientRect();
|
|
1751
|
+
if (r.bottom > y - 1e-3) {
|
|
1752
|
+
const m = /^(?:code-line-|code-md-line-)(\d+)$/.exec(el.id);
|
|
1753
|
+
if (!m?.[1])
|
|
1754
|
+
return null;
|
|
1755
|
+
return Number.parseInt(m[1], 10);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
const last = rows[rows.length - 1];
|
|
1759
|
+
if (!last)
|
|
1760
|
+
return null;
|
|
1761
|
+
const m = /^(?:code-line-|code-md-line-)(\d+)$/.exec(last.id);
|
|
1762
|
+
if (!m?.[1])
|
|
1763
|
+
return null;
|
|
1764
|
+
return Number.parseInt(m[1], 10);
|
|
1765
|
+
}
|
|
1766
|
+
function wireSourceMarkdownPaneFlip(shell, codePane, flipBtn, flipScrollBtn, onAfterFlip) {
|
|
1767
|
+
function syncSourceMarkdownFlipA11y() {
|
|
1768
|
+
const mode = sourcePaneModeForShell(shell);
|
|
1769
|
+
const renderedActive = mode === "rendered-markdown";
|
|
1770
|
+
const nextModeLabel = renderedActive ? "markdown source" : "rendered markdown";
|
|
1771
|
+
const ariaLabel = `Switch source pane to ${nextModeLabel}`;
|
|
1772
|
+
const title = `Source pane: ${renderedActive ? "rendered markdown" : "markdown source"} (click to switch)`;
|
|
1773
|
+
const apply = (btn) => {
|
|
1774
|
+
if (!(btn instanceof HTMLButtonElement))
|
|
1775
|
+
return;
|
|
1776
|
+
btn.setAttribute("aria-pressed", renderedActive ? "true" : "false");
|
|
1777
|
+
btn.setAttribute("aria-label", ariaLabel);
|
|
1778
|
+
btn.title = title;
|
|
1779
|
+
};
|
|
1780
|
+
apply(flipBtn);
|
|
1781
|
+
apply(flipScrollBtn);
|
|
1782
|
+
}
|
|
1783
|
+
// Keep initial behavior deterministic: source pane starts in rendered markdown mode.
|
|
1784
|
+
shell.setAttribute("data-source-pane-mode", "rendered-markdown");
|
|
1785
|
+
syncSourceMarkdownFlipA11y();
|
|
1786
|
+
syncWrapLinesVisibilityForSourcePaneMode(shell);
|
|
1787
|
+
const runFlip = () => {
|
|
1788
|
+
const cur = sourcePaneModeForShell(shell);
|
|
1789
|
+
const currentPrefix = cur === "rendered-markdown" ? "code-md-line-" : "code-line-";
|
|
1790
|
+
const line0 = closestSourceLine0ForPaneTop(codePane, currentPrefix);
|
|
1791
|
+
const next = cur === "rendered-markdown" ? "source" : "rendered-markdown";
|
|
1792
|
+
const nextPrefix = next === "rendered-markdown" ? "code-md-line-" : "code-line-";
|
|
1793
|
+
shell.setAttribute("data-source-pane-mode", next);
|
|
1794
|
+
writeWebStorageItem(localStorage, STORAGE_SOURCE_MARKDOWN_PANE_MODE, next);
|
|
1795
|
+
syncSourceMarkdownFlipA11y();
|
|
1796
|
+
syncWrapLinesVisibilityForSourcePaneMode(shell);
|
|
1797
|
+
if (line0 !== null) {
|
|
1798
|
+
const row = codePane.querySelector(`#${nextPrefix}${String(line0)}`);
|
|
1799
|
+
if (row instanceof HTMLElement) {
|
|
1800
|
+
applyRevealChildInPane(codePane, row, 2);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (next === "rendered-markdown") {
|
|
1804
|
+
const sourceMdBody = document.getElementById("code-pane-markdown-body");
|
|
1805
|
+
if (sourceMdBody instanceof HTMLElement) {
|
|
1806
|
+
runMermaidOnFreshDocNodes(sourceMdBody);
|
|
1807
|
+
rewriteHubRelativeBrowseAnchorsIn(sourceMdBody);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
onAfterFlip?.();
|
|
1811
|
+
};
|
|
1812
|
+
flipBtn.addEventListener("click", runFlip);
|
|
1813
|
+
if (flipScrollBtn) {
|
|
1814
|
+
flipScrollBtn.addEventListener("click", runFlip);
|
|
1815
|
+
wireSourceMarkdownPaneFlipAffordance(flipBtn, flipScrollBtn);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1443
1818
|
function wireDualMobilePaneFlip(shell, flipBtn, scrollRunners, flipScrollBtn) {
|
|
1444
1819
|
const mq = globalThis.matchMedia(DUAL_MOBILE_SINGLE_PANE_MQ);
|
|
1445
1820
|
function readStoredPane() {
|
|
@@ -1458,6 +1833,7 @@ function wireDualMobilePaneFlip(shell, flipBtn, scrollRunners, flipScrollBtn) {
|
|
|
1458
1833
|
return;
|
|
1459
1834
|
const cur = normalizedDualMobilePane(shell.getAttribute("data-dual-mobile-pane"));
|
|
1460
1835
|
const next = cur === "code" ? "doc" : "code";
|
|
1836
|
+
const rootTopBeforeFlip = rootScrollingElement().scrollTop;
|
|
1461
1837
|
if (next === "code") {
|
|
1462
1838
|
scrollRunners.prepareMobileFlipToCode();
|
|
1463
1839
|
}
|
|
@@ -1470,6 +1846,11 @@ function wireDualMobilePaneFlip(shell, flipBtn, scrollRunners, flipScrollBtn) {
|
|
|
1470
1846
|
globalThis.requestAnimationFrame(() => {
|
|
1471
1847
|
if (next === "code") {
|
|
1472
1848
|
scrollRunners.finishMobileFlipToCode();
|
|
1849
|
+
const root = rootScrollingElement();
|
|
1850
|
+
if (rootTopBeforeFlip > 5 && root.scrollTop <= 1) {
|
|
1851
|
+
const maxY = Math.max(0, root.scrollHeight - root.clientHeight);
|
|
1852
|
+
root.scrollTop = clamp(rootTopBeforeFlip, 0, maxY);
|
|
1853
|
+
}
|
|
1473
1854
|
}
|
|
1474
1855
|
else {
|
|
1475
1856
|
scrollRunners.finishMobileFlipToDoc();
|
|
@@ -1630,75 +2011,108 @@ function wireDualPaneNavSearchFetch(shell, embeddedPairs, indexState, mutable, r
|
|
|
1630
2011
|
}
|
|
1631
2012
|
})();
|
|
1632
2013
|
}
|
|
2014
|
+
function applySelectedMultiAngle(args) {
|
|
2015
|
+
const { angle, docBody, mutable, rebuildSearcher, scrollLinksRef, shell, searchInput, searchResults, requestBlockRayRedraw, } = args;
|
|
2016
|
+
docBody.innerHTML = decodeBase64Utf8(angle.docInnerHtmlB64);
|
|
2017
|
+
runMermaidOnFreshDocNodes(docBody);
|
|
2018
|
+
rewriteHubRelativeBrowseAnchorsIn(docBody);
|
|
2019
|
+
mutable.rawMd = decodeBase64Utf8(angle.rawMdB64);
|
|
2020
|
+
mutable.mdLines = mutable.rawMd.split("\n");
|
|
2021
|
+
mutable.commentrayPathLabel = angle.commentrayPathForSearch;
|
|
2022
|
+
rebuildSearcher();
|
|
2023
|
+
scrollLinksRef.current = parseScrollBlockLinksFromShell(angle.scrollBlockLinksB64);
|
|
2024
|
+
shell.setAttribute("data-scroll-block-links-b64", angle.scrollBlockLinksB64);
|
|
2025
|
+
shell.setAttribute("data-search-commentray-path", angle.commentrayPathForSearch);
|
|
2026
|
+
const crIdentity = normPosixPath(angle.commentrayPathForSearch);
|
|
2027
|
+
if (crIdentity.length > 0)
|
|
2028
|
+
shell.setAttribute("data-commentray-pair-commentray-path", crIdentity);
|
|
2029
|
+
else
|
|
2030
|
+
shell.removeAttribute("data-commentray-pair-commentray-path");
|
|
2031
|
+
applyDocumentedTreeCurrentPairHighlight();
|
|
2032
|
+
const docPathEl = document.getElementById("nav-rail-doc-path");
|
|
2033
|
+
if (docPathEl) {
|
|
2034
|
+
const path = angle.commentrayPathForSearch.trim();
|
|
2035
|
+
docPathEl.textContent = path.length > 0 ? path : "—";
|
|
2036
|
+
if (path.length > 0)
|
|
2037
|
+
docPathEl.setAttribute("title", path);
|
|
2038
|
+
else
|
|
2039
|
+
docPathEl.removeAttribute("title");
|
|
2040
|
+
}
|
|
2041
|
+
const browse = angle.staticBrowseUrl?.trim() ?? "";
|
|
2042
|
+
if (browse.length > 0) {
|
|
2043
|
+
const resolved = staticBrowseHrefForShellDataAttribute(browse, globalThis.location.pathname, globalThis.location.origin);
|
|
2044
|
+
shell.setAttribute("data-commentray-pair-browse-href", resolved);
|
|
2045
|
+
}
|
|
2046
|
+
else {
|
|
2047
|
+
const ghu = angle.commentrayOnGithubUrl?.trim();
|
|
2048
|
+
if (ghu)
|
|
2049
|
+
shell.setAttribute("data-commentray-pair-browse-href", ghu);
|
|
2050
|
+
else
|
|
2051
|
+
shell.removeAttribute("data-commentray-pair-browse-href");
|
|
2052
|
+
}
|
|
2053
|
+
searchInput.value = "";
|
|
2054
|
+
searchResults.innerHTML = "";
|
|
2055
|
+
searchResults.hidden = true;
|
|
2056
|
+
requestBlockRayRedraw?.();
|
|
2057
|
+
globalThis.requestAnimationFrame(() => {
|
|
2058
|
+
requestBlockRayRedraw?.();
|
|
2059
|
+
globalThis.requestAnimationFrame(() => {
|
|
2060
|
+
requestBlockRayRedraw?.();
|
|
2061
|
+
});
|
|
2062
|
+
});
|
|
2063
|
+
}
|
|
1633
2064
|
function wireDualPaneMultiAngleAndScroll(args) {
|
|
1634
2065
|
const { codePane, docScrollEl, docBody, shell, scrollLinksRef, multiPayload, mutable, rebuildSearcher, searchInput, searchResults, requestBlockRayRedraw, } = args;
|
|
1635
2066
|
if (multiPayload) {
|
|
1636
|
-
const runners = wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinksRef.current);
|
|
2067
|
+
const runners = wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinksRef.current, () => sourceLineIdPrefixForShell(shell), () => sourcePaneModeForShell(shell) === "rendered-markdown");
|
|
1637
2068
|
const angleSel = document.getElementById("angle-select");
|
|
1638
2069
|
if (angleSel && docBody) {
|
|
1639
2070
|
angleSel.addEventListener("change", () => {
|
|
1640
2071
|
const a = multiPayload.angles.find((x) => x.id === angleSel.value);
|
|
1641
2072
|
if (!a)
|
|
1642
2073
|
return;
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
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
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
searchInput.value = "";
|
|
1677
|
-
searchResults.innerHTML = "";
|
|
1678
|
-
searchResults.hidden = true;
|
|
1679
|
-
requestBlockRayRedraw?.();
|
|
1680
|
-
globalThis.requestAnimationFrame(() => {
|
|
1681
|
-
requestBlockRayRedraw?.();
|
|
1682
|
-
globalThis.requestAnimationFrame(() => {
|
|
1683
|
-
requestBlockRayRedraw?.();
|
|
1684
|
-
});
|
|
2074
|
+
applySelectedMultiAngle({
|
|
2075
|
+
angle: a,
|
|
2076
|
+
docBody,
|
|
2077
|
+
mutable,
|
|
2078
|
+
rebuildSearcher,
|
|
2079
|
+
scrollLinksRef,
|
|
2080
|
+
shell,
|
|
2081
|
+
searchInput,
|
|
2082
|
+
searchResults,
|
|
2083
|
+
requestBlockRayRedraw,
|
|
1685
2084
|
});
|
|
1686
2085
|
});
|
|
1687
2086
|
}
|
|
1688
2087
|
return runners;
|
|
1689
2088
|
}
|
|
1690
2089
|
if (scrollLinksRef.current.length > 0) {
|
|
1691
|
-
return wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinksRef.current);
|
|
2090
|
+
return wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinksRef.current, () => sourceLineIdPrefixForShell(shell), () => sourcePaneModeForShell(shell) === "rendered-markdown");
|
|
1692
2091
|
}
|
|
1693
2092
|
return wireProportionalScrollSync(codePane, docScrollEl);
|
|
1694
2093
|
}
|
|
1695
2094
|
function wireDualPaneCommentrayLocationHash(docScrollEl, mdLineCount) {
|
|
2095
|
+
function commentrayMdLineFromLocationHash(rawHash) {
|
|
2096
|
+
const hash = rawHash.replace(/^#/, "").trim();
|
|
2097
|
+
if (hash.length === 0)
|
|
2098
|
+
return null;
|
|
2099
|
+
const tokens = hash
|
|
2100
|
+
.split(/--|&/)
|
|
2101
|
+
.map((t) => t.trim())
|
|
2102
|
+
.filter((t) => t.length > 0);
|
|
2103
|
+
for (const token of tokens) {
|
|
2104
|
+
const m = /^commentray-md-line-(\d+)$/.exec(token);
|
|
2105
|
+
if (!m?.[1])
|
|
2106
|
+
continue;
|
|
2107
|
+
const line0 = Number.parseInt(m[1], 10);
|
|
2108
|
+
if (Number.isFinite(line0))
|
|
2109
|
+
return line0;
|
|
2110
|
+
}
|
|
2111
|
+
return null;
|
|
2112
|
+
}
|
|
1696
2113
|
function applyCommentrayLocationHash() {
|
|
1697
|
-
const
|
|
1698
|
-
if (
|
|
1699
|
-
return;
|
|
1700
|
-
const line0 = Number.parseInt(m[1], 10);
|
|
1701
|
-
if (!Number.isFinite(line0))
|
|
2114
|
+
const line0 = commentrayMdLineFromLocationHash(globalThis.location.hash);
|
|
2115
|
+
if (line0 === null)
|
|
1702
2116
|
return;
|
|
1703
2117
|
scrollDocToMarkdownLine0(docScrollEl, line0, mdLineCount());
|
|
1704
2118
|
}
|
|
@@ -1707,6 +2121,23 @@ function wireDualPaneCommentrayLocationHash(docScrollEl, mdLineCount) {
|
|
|
1707
2121
|
globalThis.requestAnimationFrame(applyCommentrayLocationHash);
|
|
1708
2122
|
});
|
|
1709
2123
|
}
|
|
2124
|
+
function initializeSourceMarkdownPane(shell) {
|
|
2125
|
+
if (sourcePaneModeForShell(shell) !== "rendered-markdown")
|
|
2126
|
+
return;
|
|
2127
|
+
const sourceMdBody = document.getElementById("code-pane-markdown-body");
|
|
2128
|
+
if (!(sourceMdBody instanceof HTMLElement))
|
|
2129
|
+
return;
|
|
2130
|
+
runMermaidOnFreshDocNodes(sourceMdBody);
|
|
2131
|
+
rewriteHubRelativeBrowseAnchorsIn(sourceMdBody);
|
|
2132
|
+
}
|
|
2133
|
+
function wireSourceMarkdownControls(shell, codePane, onAfterFlip) {
|
|
2134
|
+
const sourceMdFlip = document.getElementById("source-markdown-pane-flip");
|
|
2135
|
+
const sourceMdFlipScroll = document.getElementById("source-markdown-pane-flip-scroll");
|
|
2136
|
+
if (!(sourceMdFlip instanceof HTMLButtonElement))
|
|
2137
|
+
return;
|
|
2138
|
+
wireSourceMarkdownPaneFlip(shell, codePane, sourceMdFlip, sourceMdFlipScroll instanceof HTMLButtonElement ? sourceMdFlipScroll : null, onAfterFlip);
|
|
2139
|
+
initializeSourceMarkdownPane(shell);
|
|
2140
|
+
}
|
|
1710
2141
|
function buildDualPaneSearcherBundle(shell, codePane) {
|
|
1711
2142
|
const { rawCodeB64, rawMdB64 } = readEmbeddedRawB64Strings(shell, codePane);
|
|
1712
2143
|
const rawCode = decodeBase64Utf8(rawCodeB64);
|
|
@@ -1778,12 +2209,11 @@ function wireDualPaneCodeBrowser(shell, codePane) {
|
|
|
1778
2209
|
shell.style.setProperty("--split-pct", `${String(pct)}%`);
|
|
1779
2210
|
const docPaneEl = document.getElementById("doc-pane");
|
|
1780
2211
|
const docPaneForWrap = docPaneEl instanceof HTMLElement ? docPaneEl : null;
|
|
2212
|
+
const sourceMdBodyForWrap = document.getElementById("code-pane-markdown-body");
|
|
1781
2213
|
const blockRayRedraw = {};
|
|
1782
2214
|
wireWrapToggle(STORAGE_WRAP_LINES, codePane, wrapCb, () => {
|
|
1783
2215
|
blockRayRedraw.request?.();
|
|
1784
|
-
|
|
1785
|
-
docScrollEl.dispatchEvent(new Event("scroll"));
|
|
1786
|
-
}, docPaneForWrap, docBody);
|
|
2216
|
+
}, docPaneForWrap, docBody, sourceMdBodyForWrap instanceof HTMLElement ? sourceMdBodyForWrap : null);
|
|
1787
2217
|
wireSplitter(STORAGE_SPLIT_PCT, shell, codePane, gutter, pct);
|
|
1788
2218
|
const multiScript = document.getElementById("commentray-multi-angle-b64");
|
|
1789
2219
|
const multiPayload = parseMultiAnglePayload(multiScript);
|
|
@@ -1794,7 +2224,8 @@ function wireDualPaneCodeBrowser(shell, codePane) {
|
|
|
1794
2224
|
codePane,
|
|
1795
2225
|
docScrollEl,
|
|
1796
2226
|
getLinks: () => bundle.scrollLinksRef.current,
|
|
1797
|
-
probeTopSourceLine1Based: () => probeCodeLine1FromViewport(codePane),
|
|
2227
|
+
probeTopSourceLine1Based: () => probeCodeLine1FromViewport(codePane, sourceLineIdPrefixForShell(shell)),
|
|
2228
|
+
sourceLineIdPrefix: () => sourceLineIdPrefixForShell(shell),
|
|
1798
2229
|
})
|
|
1799
2230
|
: undefined;
|
|
1800
2231
|
blockRayRedraw.request = requestBlockRayRedraw;
|
|
@@ -1816,6 +2247,10 @@ function wireDualPaneCodeBrowser(shell, codePane) {
|
|
|
1816
2247
|
if (flipBtn instanceof HTMLButtonElement) {
|
|
1817
2248
|
wireDualMobilePaneFlip(shell, flipBtn, scrollRunners, flipScrollBtn instanceof HTMLButtonElement ? flipScrollBtn : null);
|
|
1818
2249
|
}
|
|
2250
|
+
wireSourceMarkdownControls(shell, codePane, () => {
|
|
2251
|
+
requestBlockRayRedraw?.();
|
|
2252
|
+
});
|
|
2253
|
+
wireWideModeIntroTour(shell, isNarrowViewport);
|
|
1819
2254
|
wireDualPaneCommentrayLocationHash(docScrollEl, () => bundle.mutable.mdLines.length);
|
|
1820
2255
|
}
|
|
1821
2256
|
function commentrayThemeModeLabel(mode) {
|
|
@@ -1917,7 +2352,218 @@ function wireColorThemeToolbar() {
|
|
|
1917
2352
|
document.addEventListener("keydown", onDocumentKeydown, true);
|
|
1918
2353
|
syncUi();
|
|
1919
2354
|
}
|
|
2355
|
+
function safePermalinkHref(raw) {
|
|
2356
|
+
const t = raw.trim();
|
|
2357
|
+
if (t.length === 0)
|
|
2358
|
+
return null;
|
|
2359
|
+
if (/^(javascript|data):/i.test(t))
|
|
2360
|
+
return null;
|
|
2361
|
+
try {
|
|
2362
|
+
return new URL(t, globalThis.location.href).toString();
|
|
2363
|
+
}
|
|
2364
|
+
catch {
|
|
2365
|
+
return null;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
function humaneBrowseAliasPathForSource(sourcePath) {
|
|
2369
|
+
return sourcePath
|
|
2370
|
+
.split("/")
|
|
2371
|
+
.filter((seg) => seg.length > 0)
|
|
2372
|
+
.map((seg) => seg.startsWith(".") ? `%2E${encodeURIComponent(seg.slice(1))}` : encodeURIComponent(seg))
|
|
2373
|
+
.join("/");
|
|
2374
|
+
}
|
|
2375
|
+
function companionStemFromCommentrayPath(commentrayPath) {
|
|
2376
|
+
const norm = normPosixPath(commentrayPath);
|
|
2377
|
+
const last = norm.split("/").filter(Boolean).at(-1) ?? "";
|
|
2378
|
+
return last.replace(/\.md$/i, "").trim();
|
|
2379
|
+
}
|
|
2380
|
+
function makeAbsoluteUrlAgainst(raw, baseHref) {
|
|
2381
|
+
return new URL(raw, baseHref).toString();
|
|
2382
|
+
}
|
|
2383
|
+
function shellEligibleForHumaneBackfill(shell, pathname) {
|
|
2384
|
+
if ((shell.getAttribute("data-layout") ?? "dual") !== "dual")
|
|
2385
|
+
return false;
|
|
2386
|
+
if (pathname.includes("/browse/"))
|
|
2387
|
+
return false;
|
|
2388
|
+
return pathname.endsWith("/") || pathname.endsWith("/index.html");
|
|
2389
|
+
}
|
|
2390
|
+
function nextHumaneBrowsePathForShell(shell, pathname) {
|
|
2391
|
+
const sourcePath = normPosixPath(shell.getAttribute("data-commentray-pair-source-path") ?? "");
|
|
2392
|
+
if (sourcePath.length === 0)
|
|
2393
|
+
return null;
|
|
2394
|
+
const alias = humaneBrowseAliasPathForSource(sourcePath);
|
|
2395
|
+
if (alias.length === 0)
|
|
2396
|
+
return null;
|
|
2397
|
+
const angleSel = document.getElementById("angle-select");
|
|
2398
|
+
const selectedAngle = angleSel instanceof HTMLSelectElement ? angleSel.value.trim() : "";
|
|
2399
|
+
const commentrayPath = shell.getAttribute("data-commentray-pair-commentray-path") ?? "";
|
|
2400
|
+
const stem = companionStemFromCommentrayPath(commentrayPath);
|
|
2401
|
+
const angleName = selectedAngle.length > 0 ? selectedAngle : stem;
|
|
2402
|
+
if (angleName.length === 0)
|
|
2403
|
+
return null;
|
|
2404
|
+
const angleAlias = `${alias}@${encodeURIComponent(angleName)}.html`;
|
|
2405
|
+
const siteRoot = siteRootPathnameFromPathname(pathname);
|
|
2406
|
+
return siteRoot === "/" ? `/browse/${angleAlias}` : `${siteRoot}/browse/${angleAlias}`;
|
|
2407
|
+
}
|
|
2408
|
+
function absolutizeNavJsonUrls(shell, beforeHref) {
|
|
2409
|
+
const navSearchRaw = shell.getAttribute("data-nav-search-json-url")?.trim() ?? "";
|
|
2410
|
+
if (navSearchRaw.length > 0) {
|
|
2411
|
+
shell.setAttribute("data-nav-search-json-url", makeAbsoluteUrlAgainst(navSearchRaw, beforeHref));
|
|
2412
|
+
}
|
|
2413
|
+
const navTree = document.getElementById("documented-files-hub");
|
|
2414
|
+
if (navTree instanceof HTMLElement) {
|
|
2415
|
+
const navRaw = navTree.getAttribute("data-nav-json-url")?.trim() ?? "";
|
|
2416
|
+
if (navRaw.length > 0) {
|
|
2417
|
+
navTree.setAttribute("data-nav-json-url", makeAbsoluteUrlAgainst(navRaw, beforeHref));
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
function normalizePairBrowseHrefForCurrentPath(shell, pathname) {
|
|
2422
|
+
const pairBrowseRaw = shell.getAttribute("data-commentray-pair-browse-href")?.trim() ?? "";
|
|
2423
|
+
if (pairBrowseRaw.length > 0 && isHubRelativeStaticBrowseHref(pairBrowseRaw)) {
|
|
2424
|
+
shell.setAttribute("data-commentray-pair-browse-href", resolveStaticBrowseHref(pairBrowseRaw, pathname, globalThis.location.origin));
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
function normalizeDocumentationHomeHrefForCurrentPath() {
|
|
2428
|
+
const home = document.querySelector('a[aria-label="Documentation home"]');
|
|
2429
|
+
if (!(home instanceof HTMLAnchorElement))
|
|
2430
|
+
return;
|
|
2431
|
+
const siteRoot = siteRootPathnameFromPathname(globalThis.location.pathname);
|
|
2432
|
+
const normalized = siteRoot === "/" ? "/" : `${siteRoot}/`;
|
|
2433
|
+
home.setAttribute("href", normalized);
|
|
2434
|
+
}
|
|
2435
|
+
function activeCommentrayHashTokenFromViewport() {
|
|
2436
|
+
const docPane = document.getElementById("doc-pane");
|
|
2437
|
+
if (!(docPane instanceof HTMLElement))
|
|
2438
|
+
return null;
|
|
2439
|
+
const docBody = document.getElementById("doc-pane-body");
|
|
2440
|
+
const docScrollEl = docBody instanceof HTMLElement ? docBody : docPane;
|
|
2441
|
+
const anchors = docScrollEl.querySelectorAll(".commentray-block-anchor");
|
|
2442
|
+
if (anchors.length === 0)
|
|
2443
|
+
return null;
|
|
2444
|
+
const mdLine0 = probeCommentrayLine0FromDoc(docScrollEl);
|
|
2445
|
+
if (!Number.isFinite(mdLine0) || mdLine0 < 0)
|
|
2446
|
+
return null;
|
|
2447
|
+
return `commentray-md-line-${String(mdLine0)}`;
|
|
2448
|
+
}
|
|
2449
|
+
function maybeBackfillAddressBarWithHumanePairLink() {
|
|
2450
|
+
const shell = document.getElementById("shell");
|
|
2451
|
+
if (!(shell instanceof HTMLElement))
|
|
2452
|
+
return;
|
|
2453
|
+
const pathname = globalThis.location.pathname;
|
|
2454
|
+
normalizeDocumentationHomeHrefForCurrentPath();
|
|
2455
|
+
if (!shellEligibleForHumaneBackfill(shell, pathname))
|
|
2456
|
+
return;
|
|
2457
|
+
const beforeHref = globalThis.location.href;
|
|
2458
|
+
absolutizeNavJsonUrls(shell, beforeHref);
|
|
2459
|
+
normalizePairBrowseHrefForCurrentPath(shell, pathname);
|
|
2460
|
+
/** Prefer the same `staticBrowseUrl` the static build put on `#shell` (slug or `…/index.html` shims). */
|
|
2461
|
+
const canonicalBrowsePathname = (() => {
|
|
2462
|
+
const raw = shell.getAttribute("data-commentray-pair-browse-href")?.trim() ?? "";
|
|
2463
|
+
if (raw.length === 0)
|
|
2464
|
+
return null;
|
|
2465
|
+
try {
|
|
2466
|
+
const u = new URL(raw, globalThis.location.href);
|
|
2467
|
+
if (u.origin !== globalThis.location.origin)
|
|
2468
|
+
return null;
|
|
2469
|
+
if (!u.pathname.includes("/browse/"))
|
|
2470
|
+
return null;
|
|
2471
|
+
return u.pathname;
|
|
2472
|
+
}
|
|
2473
|
+
catch {
|
|
2474
|
+
return null;
|
|
2475
|
+
}
|
|
2476
|
+
})();
|
|
2477
|
+
const nextPath = canonicalBrowsePathname ?? nextHumaneBrowsePathForShell(shell, pathname);
|
|
2478
|
+
if (nextPath === null)
|
|
2479
|
+
return;
|
|
2480
|
+
globalThis.history.replaceState(null, "", `${nextPath}${globalThis.location.search}${globalThis.location.hash}`);
|
|
2481
|
+
}
|
|
2482
|
+
function permalinkHashSuffixFromUi() {
|
|
2483
|
+
const tokens = [];
|
|
2484
|
+
const pushUnique = (token) => {
|
|
2485
|
+
const t = token.trim();
|
|
2486
|
+
if (t.length === 0)
|
|
2487
|
+
return;
|
|
2488
|
+
if (!tokens.includes(t))
|
|
2489
|
+
tokens.push(t);
|
|
2490
|
+
};
|
|
2491
|
+
const angleSel = document.getElementById("angle-select");
|
|
2492
|
+
if (angleSel instanceof HTMLSelectElement) {
|
|
2493
|
+
const id = angleSel.value.trim();
|
|
2494
|
+
if (id.length > 0) {
|
|
2495
|
+
pushUnique(`angle-${encodeURIComponent(id)}`);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
const activeAnchor = activeCommentrayHashTokenFromViewport();
|
|
2499
|
+
if (activeAnchor)
|
|
2500
|
+
pushUnique(activeAnchor);
|
|
2501
|
+
return tokens.length > 0 ? `#${tokens.join("&")}` : "";
|
|
2502
|
+
}
|
|
2503
|
+
function sharePermalinkFromShell(shell) {
|
|
2504
|
+
const raw = shell.getAttribute("data-commentray-pair-browse-href") ?? "";
|
|
2505
|
+
const canonical = isHubRelativeStaticBrowseHref(raw.trim()) && raw.trim().length > 0
|
|
2506
|
+
? resolveStaticBrowseHref(raw.trim(), globalThis.location.pathname, globalThis.location.origin)
|
|
2507
|
+
: safePermalinkHref(raw);
|
|
2508
|
+
const base = canonical ?? globalThis.location.href;
|
|
2509
|
+
const u = new URL(base, globalThis.location.href);
|
|
2510
|
+
const hash = permalinkHashSuffixFromUi();
|
|
2511
|
+
u.hash = hash.length > 0 ? hash.slice(1) : "";
|
|
2512
|
+
return u.toString();
|
|
2513
|
+
}
|
|
2514
|
+
async function writeTextToClipboard(text) {
|
|
2515
|
+
try {
|
|
2516
|
+
await navigator.clipboard.writeText(text);
|
|
2517
|
+
return true;
|
|
2518
|
+
}
|
|
2519
|
+
catch {
|
|
2520
|
+
const ta = document.createElement("textarea");
|
|
2521
|
+
ta.value = text;
|
|
2522
|
+
ta.setAttribute("readonly", "true");
|
|
2523
|
+
ta.style.position = "fixed";
|
|
2524
|
+
ta.style.top = "-1000px";
|
|
2525
|
+
ta.style.left = "-1000px";
|
|
2526
|
+
document.body.appendChild(ta);
|
|
2527
|
+
ta.select();
|
|
2528
|
+
try {
|
|
2529
|
+
return document.execCommand("copy");
|
|
2530
|
+
}
|
|
2531
|
+
catch {
|
|
2532
|
+
return false;
|
|
2533
|
+
}
|
|
2534
|
+
finally {
|
|
2535
|
+
document.body.removeChild(ta);
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
function wireSharePermalinkButton() {
|
|
2540
|
+
const shell = document.getElementById("shell");
|
|
2541
|
+
const btn = document.getElementById("commentray-share-link");
|
|
2542
|
+
if (!(shell instanceof HTMLElement) || !(btn instanceof HTMLButtonElement))
|
|
2543
|
+
return;
|
|
2544
|
+
const baseLabel = "Copy shareable permalink";
|
|
2545
|
+
let copiedTimer;
|
|
2546
|
+
btn.addEventListener("click", () => {
|
|
2547
|
+
void (async () => {
|
|
2548
|
+
const shareUrl = sharePermalinkFromShell(shell);
|
|
2549
|
+
const copied = await writeTextToClipboard(shareUrl);
|
|
2550
|
+
if (!copied)
|
|
2551
|
+
return;
|
|
2552
|
+
btn.dataset.copied = "true";
|
|
2553
|
+
btn.setAttribute("aria-label", "Permalink copied");
|
|
2554
|
+
btn.title = "Permalink copied";
|
|
2555
|
+
if (copiedTimer !== undefined)
|
|
2556
|
+
globalThis.clearTimeout(copiedTimer);
|
|
2557
|
+
copiedTimer = globalThis.setTimeout(() => {
|
|
2558
|
+
delete btn.dataset.copied;
|
|
2559
|
+
btn.setAttribute("aria-label", baseLabel);
|
|
2560
|
+
btn.title = baseLabel;
|
|
2561
|
+
}, 1200);
|
|
2562
|
+
})();
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
1920
2565
|
function main() {
|
|
2566
|
+
wireSharePermalinkButton();
|
|
1921
2567
|
wireColorThemeToolbar();
|
|
1922
2568
|
wireDocumentedFilesTree();
|
|
1923
2569
|
const shell = document.getElementById("shell");
|
|
@@ -1925,12 +2571,16 @@ function main() {
|
|
|
1925
2571
|
if (!shell || !codePane) {
|
|
1926
2572
|
return;
|
|
1927
2573
|
}
|
|
2574
|
+
applyPageBreakFeatureToggle(shell);
|
|
2575
|
+
wireResponsivePageBreakHeight(shell);
|
|
2576
|
+
wireWideModeIntroTrigger(shell);
|
|
1928
2577
|
const layout = shell.getAttribute("data-layout") || "dual";
|
|
1929
2578
|
if (layout === "stretch") {
|
|
1930
2579
|
wireStretchLayoutChrome(codePane);
|
|
1931
2580
|
return;
|
|
1932
2581
|
}
|
|
1933
2582
|
wireDualPaneCodeBrowser(shell, codePane);
|
|
2583
|
+
maybeBackfillAddressBarWithHumanePairLink();
|
|
1934
2584
|
}
|
|
1935
2585
|
if (document.readyState === "loading") {
|
|
1936
2586
|
document.addEventListener("DOMContentLoaded", main);
|