@commentray/render 0.3.4 → 0.3.6

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.
@@ -13,7 +13,7 @@ import { parseDualPaneScrollSyncStrategy } from "./code-browser-scroll-sync-stra
13
13
  import { DUAL_PANE_BLOCK_REVEAL_LEAD_CSS_PX, READING_LEAD_ALIGN_TOLERANCE_CSS_PX, READING_VIEWPORT_BOTTOM_EDGE_CSS_PX, readingViewportTopInsetCssPx, } from "./reading-viewport-comfort.js";
14
14
  import { wireWideModeIntroTour } from "./code-browser-wide-intro-controller.js";
15
15
  import { readWebStorageItem, writeWebStorageItem } from "./code-browser-web-storage.js";
16
- import { dispatchCommentrayMermaidDone, wireBlockStretchBufferSync, } from "./block-stretch-buffer-sync.js";
16
+ import { applyBlockStretchRowBuffers, dispatchCommentrayMermaidDone, wireBlockStretchBufferSync, } from "./block-stretch-buffer-sync.js";
17
17
  import { COMMENTRAY_MERMAID_MODULE_READY_EVENT } from "./commentray-mermaid-events.js";
18
18
  /**
19
19
  * Hub pages emit `./browse/…` relative to the site root. From `/…/browse/current.html` the browser
@@ -936,16 +936,16 @@ function emptySearchBrowsePreviewInnerHtml(hint, rows, ctx) {
936
936
  function scrollDocToMarkdownLine0(docScrollEl, line0, mdLineCount) {
937
937
  const el = docScrollEl.querySelector(`#commentray-md-line-${String(line0)}`);
938
938
  if (el instanceof HTMLElement) {
939
- const top = Math.round(scrollTopToAlignChildTop(docScrollEl, el, DUAL_PANE_BLOCK_REVEAL_LEAD_CSS_PX));
940
- const maxY = Math.round(Math.max(0, docScrollEl.scrollHeight - docScrollEl.clientHeight));
941
- docScrollEl.scrollTo({ top: clamp(top, 0, maxY), behavior: "smooth" });
939
+ applyRevealChildInPane(docScrollEl, el, DUAL_PANE_BLOCK_REVEAL_LEAD_CSS_PX);
942
940
  return;
943
941
  }
944
942
  if (mdLineCount <= 1)
945
943
  return;
946
- const ratio = line0 / Math.max(1, mdLineCount - 1);
947
- const maxScroll = Math.max(0, docScrollEl.scrollHeight - docScrollEl.clientHeight);
948
- docScrollEl.scrollTo({ top: ratio * maxScroll, behavior: "smooth" });
944
+ const lineClamped = clamp(line0, 0, Math.max(0, mdLineCount - 1));
945
+ const scrollTarget = paneUsesInternalYScroll(docScrollEl) ? docScrollEl : rootScrollingElement();
946
+ const maxScroll = Math.max(0, scrollTarget.scrollHeight - scrollTarget.clientHeight);
947
+ const top = clamp((lineClamped / Math.max(1, mdLineCount - 1)) * maxScroll, 0, maxScroll);
948
+ scrollTarget.scrollTo({ top, behavior: "smooth" });
949
949
  }
950
950
  function navigateToDocumentedPair(pair, mdLine0) {
951
951
  if (pair.staticBrowseUrl?.trim()) {
@@ -1028,7 +1028,10 @@ function handlePathSearchHit(button, deps) {
1028
1028
  const hitSp = (button.getAttribute("data-sp-path") ?? "").trim();
1029
1029
  const pair = findDocumentedPair(deps.mutable.documentedPairs, hitCr, hitSp);
1030
1030
  if (pair && isSameDocumentedPair(pair, deps.filePathLabel, deps.mutable.commentrayPathLabel)) {
1031
- deps.docScrollEl.scrollTo({ top: 0, behavior: "smooth" });
1031
+ const scrollTarget = paneUsesInternalYScroll(deps.docScrollEl)
1032
+ ? deps.docScrollEl
1033
+ : rootScrollingElement();
1034
+ scrollTarget.scrollTo({ top: 0, behavior: "smooth" });
1032
1035
  return;
1033
1036
  }
1034
1037
  if (pair)
@@ -2490,6 +2493,38 @@ const DUAL_MOBILE_SINGLE_PANE_MQ = "(max-width: 767px)";
2490
2493
  function normalizedDualMobilePane(v) {
2491
2494
  return v === "code" ? "code" : "doc";
2492
2495
  }
2496
+ function splitLocationHashTokens(rawHash) {
2497
+ const hash = rawHash.replace(/^#/, "").trim();
2498
+ if (hash.length === 0)
2499
+ return [];
2500
+ return hash
2501
+ .split(/--|&/)
2502
+ .map((t) => t.trim())
2503
+ .filter((t) => t.length > 0);
2504
+ }
2505
+ function mobilePaneHashToken(pane) {
2506
+ return `mobile-pane-${pane}`;
2507
+ }
2508
+ function mobilePaneFromLocationHash(rawHash) {
2509
+ const tokens = splitLocationHashTokens(rawHash);
2510
+ for (const token of tokens) {
2511
+ if (token === "mobile-pane-code")
2512
+ return "code";
2513
+ if (token === "mobile-pane-doc")
2514
+ return "doc";
2515
+ }
2516
+ return null;
2517
+ }
2518
+ function updateLocationHashToken(token, shouldReplace) {
2519
+ const tokens = splitLocationHashTokens(globalThis.location.hash).filter((t) => !shouldReplace(t));
2520
+ tokens.push(token);
2521
+ const u = new URL(globalThis.location.href);
2522
+ u.hash = tokens.length > 0 ? tokens.join("&") : "";
2523
+ globalThis.history.replaceState(globalThis.history.state, "", u.toString());
2524
+ }
2525
+ function updateLocationHashForMobilePane(pane) {
2526
+ updateLocationHashToken(mobilePaneHashToken(pane), (t) => /^mobile-pane-(?:code|doc)$/.test(t));
2527
+ }
2493
2528
  function isNarrowViewport() {
2494
2529
  return globalThis.matchMedia(DUAL_MOBILE_SINGLE_PANE_MQ).matches;
2495
2530
  }
@@ -2713,6 +2748,7 @@ function wireSourceMarkdownPaneFlip(shell, codePane, flipBtn, flipScrollBtn, sig
2713
2748
  if (curPane === "doc") {
2714
2749
  shell.setAttribute("data-dual-mobile-pane", "code");
2715
2750
  writeWebStorageItem(localStorage, STORAGE_DUAL_MOBILE_PANE, "code");
2751
+ updateLocationHashForMobilePane("code");
2716
2752
  }
2717
2753
  }
2718
2754
  syncSourceMarkdownFlipA11y();
@@ -2741,6 +2777,9 @@ function wireSourceMarkdownPaneFlip(shell, codePane, flipBtn, flipScrollBtn, sig
2741
2777
  function wireDualMobilePaneFlip(shell, flipBtn, scrollRunners, flipScrollBtn) {
2742
2778
  const mq = globalThis.matchMedia(DUAL_MOBILE_SINGLE_PANE_MQ);
2743
2779
  function readStoredPane() {
2780
+ const fromHash = mobilePaneFromLocationHash(globalThis.location.hash);
2781
+ if (fromHash !== null)
2782
+ return fromHash;
2744
2783
  return normalizedDualMobilePane(readWebStorageItem(localStorage, STORAGE_DUAL_MOBILE_PANE));
2745
2784
  }
2746
2785
  function applyForViewport() {
@@ -2767,6 +2806,7 @@ function wireDualMobilePaneFlip(shell, flipBtn, scrollRunners, flipScrollBtn) {
2767
2806
  }
2768
2807
  shell.setAttribute("data-dual-mobile-pane", next);
2769
2808
  writeWebStorageItem(localStorage, STORAGE_DUAL_MOBILE_PANE, next);
2809
+ updateLocationHashForMobilePane(next);
2770
2810
  globalThis.requestAnimationFrame(() => {
2771
2811
  globalThis.requestAnimationFrame(() => {
2772
2812
  if (next === "code") {
@@ -2793,49 +2833,64 @@ function wireDualMobilePaneFlip(shell, flipBtn, scrollRunners, flipScrollBtn) {
2793
2833
  wireDualMobilePaneFlipScrollAffordance(flipBtn, flipScrollBtn, mq);
2794
2834
  }
2795
2835
  mq.addEventListener("change", applyForViewport);
2836
+ globalThis.addEventListener("hashchange", applyForViewport);
2796
2837
  applyForViewport();
2797
2838
  }
2798
2839
  function wireStretchMobilePaneFlip(shell, codePane, flipBtn, flipScrollBtn, onAfterFlip) {
2799
- void codePane;
2800
- void onAfterFlip;
2801
- const mq = globalThis.matchMedia(DUAL_MOBILE_SINGLE_PANE_MQ);
2802
- function readStoredPane() {
2803
- return normalizedDualMobilePane(readWebStorageItem(localStorage, STORAGE_DUAL_MOBILE_PANE));
2804
- }
2805
- function applyForViewport() {
2806
- if (mq.matches) {
2807
- syncSinglePaneShellState(shell, true);
2808
- shell.setAttribute("data-dual-mobile-pane", readStoredPane());
2840
+ const stretchTable = codePane instanceof HTMLTableElement ? codePane : null;
2841
+ let anchorRow = null;
2842
+ let anchorTopBefore = 0;
2843
+ let rootTopBefore = 0;
2844
+ const captureAnchor = () => {
2845
+ const root = rootScrollingElement();
2846
+ rootTopBefore = root.scrollTop;
2847
+ if (stretchTable === null) {
2848
+ anchorRow = null;
2849
+ anchorTopBefore = 0;
2850
+ return;
2809
2851
  }
2810
- else {
2811
- syncSinglePaneShellState(shell, false);
2812
- shell.removeAttribute("data-dual-mobile-pane");
2852
+ const rows = stretchBlockRows(stretchTable);
2853
+ anchorRow = rows.find((row) => row.getBoundingClientRect().bottom > 0) ?? null;
2854
+ anchorTopBefore = anchorRow?.getBoundingClientRect().top ?? 0;
2855
+ };
2856
+ const restoreAnchor = () => {
2857
+ const root = rootScrollingElement();
2858
+ if (anchorRow !== null) {
2859
+ const delta = anchorRow.getBoundingClientRect().top - anchorTopBefore;
2860
+ if (Math.abs(delta) > 0.5) {
2861
+ const maxY = Math.max(0, root.scrollHeight - root.clientHeight);
2862
+ root.scrollTop = clamp(root.scrollTop + delta, 0, maxY);
2863
+ return;
2864
+ }
2813
2865
  }
2814
- }
2815
- const runFlip = () => {
2816
- if (!mq.matches)
2817
- return;
2818
- const cur = normalizedDualMobilePane(shell.getAttribute("data-dual-mobile-pane"));
2819
- const next = cur === "code" ? "doc" : "code";
2820
- shell.setAttribute("data-dual-mobile-pane", next);
2821
- writeWebStorageItem(localStorage, STORAGE_DUAL_MOBILE_PANE, next);
2822
- // When revealing the doc pane, (re)run Mermaid — diagrams inside display:none cells are
2823
- // skipped on initial load, so they need a triggered pass once the cells become visible.
2824
- if (next === "doc") {
2825
- queueMicrotask(() => {
2866
+ const maxY = Math.max(0, root.scrollHeight - root.clientHeight);
2867
+ root.scrollTop = clamp(rootTopBefore, 0, maxY);
2868
+ };
2869
+ const applyBuffersAndRestore = () => {
2870
+ if (stretchTable !== null)
2871
+ applyBlockStretchRowBuffers(stretchTable);
2872
+ restoreAnchor();
2873
+ };
2874
+ const runners = {
2875
+ syncFromCodeToDoc: () => { },
2876
+ syncFromDocToCode: () => { },
2877
+ prepareMobileFlipToCode: captureAnchor,
2878
+ prepareMobileFlipToDoc: captureAnchor,
2879
+ finishMobileFlipToCode: () => {
2880
+ applyBuffersAndRestore();
2881
+ onAfterFlip?.();
2882
+ },
2883
+ finishMobileFlipToDoc: () => {
2884
+ applyBuffersAndRestore();
2885
+ onAfterFlip?.();
2886
+ void runMermaidOnFreshDocNodes(shell).then(() => {
2826
2887
  requestAnimationFrame(() => {
2827
- void runMermaidOnFreshDocNodes(shell);
2888
+ applyBuffersAndRestore();
2828
2889
  });
2829
2890
  });
2830
- }
2891
+ },
2831
2892
  };
2832
- flipBtn.addEventListener("click", runFlip);
2833
- if (flipScrollBtn) {
2834
- flipScrollBtn.addEventListener("click", runFlip);
2835
- wireDualMobilePaneFlipScrollAffordance(flipBtn, flipScrollBtn, mq);
2836
- }
2837
- mq.addEventListener("change", applyForViewport);
2838
- applyForViewport();
2893
+ wireDualMobilePaneFlip(shell, flipBtn, runners, flipScrollBtn);
2839
2894
  }
2840
2895
  /** Multi-angle stretch swaps `#shell` innerHTML; disconnect the previous table observer so listeners do not accumulate. */
2841
2896
  let stretchRowBufferSyncHandle = null;
@@ -3238,6 +3293,27 @@ function initialCommentrayScopePathState(shell, scope, filePathLabel, commentray
3238
3293
  : [filePathLabel, commentrayPathLabel].filter((s) => s.trim().length > 0).join("\n");
3239
3294
  return { documentedPairs, pathRowsForOrdering, pathBlobWide };
3240
3295
  }
3296
+ function effectiveCommentrayPathLabelFromDocumentedPairs(scope, filePathLabel, commentrayPathLabel, documentedPairs) {
3297
+ if (scope !== "commentray-and-paths")
3298
+ return commentrayPathLabel;
3299
+ const sourcePath = filePathLabel.trim();
3300
+ if (sourcePath.length === 0)
3301
+ return commentrayPathLabel;
3302
+ const pair = findDocumentedPair(documentedPairs, "", sourcePath);
3303
+ if (!pair)
3304
+ return commentrayPathLabel;
3305
+ const fromShell = commentrayPathLabel.trim();
3306
+ const fromPair = pair.commentrayPath.trim();
3307
+ if (fromPair.length === 0)
3308
+ return commentrayPathLabel;
3309
+ if (fromShell.length === 0)
3310
+ return fromPair;
3311
+ // Older hub HTML can carry placeholder `commentray.md` while documentedPairs already has the real path.
3312
+ if (normPosixPath(fromShell) === "commentray.md" && normPosixPath(fromPair) !== "commentray.md") {
3313
+ return fromPair;
3314
+ }
3315
+ return commentrayPathLabel;
3316
+ }
3241
3317
  /**
3242
3318
  * Fetched `commentray-nav-search.json` sometimes omits `staticBrowseUrl` on pairs; the hub embed
3243
3319
  * carries browse URLs from the same build — merge so search hits open `_site/browse/…`, not GitHub.
@@ -3466,6 +3542,15 @@ function buildDualPaneSearcherBundle(shell, codePane) {
3466
3542
  const scrollLinksRef = { current: scrollLinks };
3467
3543
  const { scope, filePathLabel, commentrayPathLabel } = readSearchScopeFromShell(shell);
3468
3544
  const pathInit = initialCommentrayScopePathState(shell, scope, filePathLabel, commentrayPathLabel);
3545
+ const effectiveCommentrayPathLabel = effectiveCommentrayPathLabelFromDocumentedPairs(scope, filePathLabel, commentrayPathLabel, pathInit.documentedPairs);
3546
+ if (effectiveCommentrayPathLabel.trim().length > 0) {
3547
+ shell.setAttribute("data-search-commentray-path", effectiveCommentrayPathLabel);
3548
+ const docPathEl = document.getElementById("nav-rail-doc-path");
3549
+ if (docPathEl) {
3550
+ docPathEl.textContent = effectiveCommentrayPathLabel;
3551
+ docPathEl.setAttribute("title", effectiveCommentrayPathLabel);
3552
+ }
3553
+ }
3469
3554
  const indexState = {
3470
3555
  hubNavRows: [],
3471
3556
  documentedPairs: pathInit.documentedPairs,
@@ -3474,7 +3559,7 @@ function buildDualPaneSearcherBundle(shell, codePane) {
3474
3559
  const mutable = {
3475
3560
  rawMd,
3476
3561
  mdLines: rawMd.split("\n"),
3477
- commentrayPathLabel,
3562
+ commentrayPathLabel: effectiveCommentrayPathLabel,
3478
3563
  searcher: indexSearchLineRows([]),
3479
3564
  pathBlobWide: pathInit.pathBlobWide,
3480
3565
  pathRowsForOrdering: indexState.pathRowsForOrdering,
@@ -3498,7 +3583,7 @@ function buildDualPaneSearcherBundle(shell, codePane) {
3498
3583
  scrollLinksRef,
3499
3584
  scope,
3500
3585
  filePathLabel,
3501
- commentrayPathLabel,
3586
+ commentrayPathLabel: effectiveCommentrayPathLabel,
3502
3587
  pathInit,
3503
3588
  indexState,
3504
3589
  mutable,
@@ -3766,6 +3851,11 @@ function permalinkHashSuffixFromUi() {
3766
3851
  pushUnique(`angle-${encodeURIComponent(id)}`);
3767
3852
  }
3768
3853
  }
3854
+ const shell = document.getElementById("shell");
3855
+ if (shell instanceof HTMLElement && shell.getAttribute("data-mobile-single-pane") === "true") {
3856
+ const pane = normalizedDualMobilePane(shell.getAttribute("data-dual-mobile-pane"));
3857
+ pushUnique(mobilePaneHashToken(pane));
3858
+ }
3769
3859
  const activeAnchor = activeCommentrayHashTokenFromViewport();
3770
3860
  if (activeAnchor)
3771
3861
  pushUnique(activeAnchor);