@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.
Files changed (82) hide show
  1. package/README.md +1 -1
  2. package/dist/block-stretch-layout.d.ts.map +1 -1
  3. package/dist/block-stretch-layout.js +2 -1
  4. package/dist/block-stretch-layout.js.map +1 -1
  5. package/dist/browse-page-slug.d.ts +6 -1
  6. package/dist/browse-page-slug.d.ts.map +1 -1
  7. package/dist/browse-page-slug.js +6 -1
  8. package/dist/browse-page-slug.js.map +1 -1
  9. package/dist/browse-pair-html-test-fixtures.d.ts +10 -0
  10. package/dist/browse-pair-html-test-fixtures.d.ts.map +1 -0
  11. package/dist/browse-pair-html-test-fixtures.js +19 -0
  12. package/dist/browse-pair-html-test-fixtures.js.map +1 -0
  13. package/dist/build-commentray-nav-search.d.ts +4 -4
  14. package/dist/build-commentray-nav-search.js +1 -1
  15. package/dist/code-browser-block-rays.d.ts +29 -0
  16. package/dist/code-browser-block-rays.d.ts.map +1 -1
  17. package/dist/code-browser-block-rays.js +120 -0
  18. package/dist/code-browser-block-rays.js.map +1 -1
  19. package/dist/code-browser-client.bundle.js +24 -11
  20. package/dist/code-browser-client.js +766 -116
  21. package/dist/code-browser-client.js.map +1 -1
  22. package/dist/code-browser-intro.css +187 -0
  23. package/dist/code-browser-pair-nav.d.ts +3 -3
  24. package/dist/code-browser-pair-nav.d.ts.map +1 -1
  25. package/dist/code-browser-pair-nav.js +25 -13
  26. package/dist/code-browser-pair-nav.js.map +1 -1
  27. package/dist/code-browser-wide-intro-controller.d.ts +4 -0
  28. package/dist/code-browser-wide-intro-controller.d.ts.map +1 -0
  29. package/dist/code-browser-wide-intro-controller.js +148 -0
  30. package/dist/code-browser-wide-intro-controller.js.map +1 -0
  31. package/dist/code-browser-wide-intro-layout.d.ts +3 -0
  32. package/dist/code-browser-wide-intro-layout.d.ts.map +1 -0
  33. package/dist/code-browser-wide-intro-layout.js +84 -0
  34. package/dist/code-browser-wide-intro-layout.js.map +1 -0
  35. package/dist/code-browser-wide-intro-steps.d.ts +11 -0
  36. package/dist/code-browser-wide-intro-steps.d.ts.map +1 -0
  37. package/dist/code-browser-wide-intro-steps.js +108 -0
  38. package/dist/code-browser-wide-intro-steps.js.map +1 -0
  39. package/dist/code-browser-wide-intro-ui.d.ts +14 -0
  40. package/dist/code-browser-wide-intro-ui.d.ts.map +1 -0
  41. package/dist/code-browser-wide-intro-ui.js +67 -0
  42. package/dist/code-browser-wide-intro-ui.js.map +1 -0
  43. package/dist/code-browser.d.ts +18 -4
  44. package/dist/code-browser.d.ts.map +1 -1
  45. package/dist/code-browser.js +506 -154
  46. package/dist/code-browser.js.map +1 -1
  47. package/dist/commentray-anchor-viewport-probe.d.ts +9 -0
  48. package/dist/commentray-anchor-viewport-probe.d.ts.map +1 -0
  49. package/dist/commentray-anchor-viewport-probe.js +13 -0
  50. package/dist/commentray-anchor-viewport-probe.js.map +1 -0
  51. package/dist/commentray-preview-html.d.ts +13 -0
  52. package/dist/commentray-preview-html.d.ts.map +1 -0
  53. package/dist/commentray-preview-html.js +12 -0
  54. package/dist/commentray-preview-html.js.map +1 -0
  55. package/dist/companion-markdown-preview-entry.d.ts +7 -0
  56. package/dist/companion-markdown-preview-entry.d.ts.map +1 -0
  57. package/dist/companion-markdown-preview-entry.js +6 -0
  58. package/dist/companion-markdown-preview-entry.js.map +1 -0
  59. package/dist/index.d.ts +2 -0
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +2 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/inject-md-line-anchors.d.ts +18 -0
  64. package/dist/inject-md-line-anchors.d.ts.map +1 -0
  65. package/dist/inject-md-line-anchors.js +250 -0
  66. package/dist/inject-md-line-anchors.js.map +1 -0
  67. package/dist/inline-favicon.js +15 -15
  68. package/dist/markdown-pipeline.d.ts +5 -0
  69. package/dist/markdown-pipeline.d.ts.map +1 -1
  70. package/dist/markdown-pipeline.js +47 -1
  71. package/dist/markdown-pipeline.js.map +1 -1
  72. package/dist/side-by-side-layout-css.d.ts +1 -1
  73. package/dist/side-by-side-layout-css.d.ts.map +1 -1
  74. package/dist/side-by-side-layout-css.js +48 -0
  75. package/dist/side-by-side-layout-css.js.map +1 -1
  76. package/package.json +8 -3
  77. package/dist/code-browser-client.d.ts +0 -2
  78. package/dist/side-by-side-layout.css +0 -58
  79. package/dist/side-by-side-layout.embedded.d.ts +0 -3
  80. package/dist/side-by-side-layout.embedded.d.ts.map +0 -1
  81. package/dist/side-by-side-layout.embedded.js +0 -3
  82. package/dist/side-by-side-layout.embedded.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { MARKER_ID_BODY, buildBlockScrollLinks, findMonorepoPackagesDir, monorepoLayoutStartDir, } from "@commentray/core";
2
+ import path, { join } from "node:path";
3
+ import { buildBlockScrollLinks, findMonorepoPackagesDir, monorepoLayoutStartDir, normalizeRepoRelativePath, } from "@commentray/core";
4
4
  import { tryBuildBlockStretchTableHtml } from "./block-stretch-layout.js";
5
5
  import { formatCommentrayBuiltAtLocal } from "./build-stamp.js";
6
6
  import { escapeHtml } from "./html-utils.js";
@@ -11,12 +11,28 @@ import { COMMENTRAY_FAVICON_LINK_HTML } from "./inline-favicon.js";
11
11
  import { mermaidRuntimeScriptHtml } from "./mermaid-runtime-html.js";
12
12
  import { renderMarkdownToHtml } from "./markdown-pipeline.js";
13
13
  import { commentrayRenderVersion } from "./package-version.js";
14
+ import { normPosixPath } from "./code-browser-pair-nav.js";
15
+ import { injectCommentrayDocAnchors, injectSourceMarkdownAnchors, } from "./inject-md-line-anchors.js";
14
16
  function renderGeneratorMetaHtml(label) {
15
17
  const t = label?.trim();
16
18
  if (!t)
17
19
  return "";
18
20
  return `<meta name="generator" content="${escapeHtml(t)}" />\n `;
19
21
  }
22
+ /** Accepts short or full SHA; returns lowercase hex or undefined if the string is not a Git object name. */
23
+ function normalizePagesBuildCommitSha(raw) {
24
+ const t = raw?.trim();
25
+ if (!t)
26
+ return undefined;
27
+ const lower = t.toLowerCase();
28
+ if (!/^[0-9a-f]{7,40}$/.test(lower))
29
+ return undefined;
30
+ return lower;
31
+ }
32
+ function footerCommitSuffixHtml(commitSha) {
33
+ const esc = escapeHtml(commitSha);
34
+ return ` · <code class="app__footer-attribution__sha" translate="no">${esc}</code>`;
35
+ }
20
36
  const META_DESCRIPTION_MAX_LEN = 320;
21
37
  function codeBrowserMetaDescription(opts, title) {
22
38
  const custom = opts.metaDescription?.trim();
@@ -29,105 +45,6 @@ function renderMetaDescriptionHtml(opts, title) {
29
45
  const content = codeBrowserMetaDescription(opts, title);
30
46
  return `<meta name="description" content="${escapeHtml(content)}" />\n `;
31
47
  }
32
- /** Single capture: marker id (avoid a wrapping group around the whole comment — that shifted indices). */
33
- const BLOCK_MARKER_HTML_LINE = new RegExp(`^<!--\\s*commentray:block\\s+id=(${MARKER_ID_BODY})\\s*-->$`, "i");
34
- function trimEndSpacesTabs(s) {
35
- let end = s.length;
36
- while (end > 0) {
37
- const c = s[end - 1];
38
- if (c !== " " && c !== "\t")
39
- break;
40
- end--;
41
- }
42
- return s.slice(0, end);
43
- }
44
- function isSetextUnderlineLine(line) {
45
- const t = trimEndSpacesTabs(line);
46
- return /^\s{0,3}=+\s*$/.test(t) || /^\s{0,3}-+\s*$/.test(t);
47
- }
48
- function isThematicBreakLine(line) {
49
- const t = trimEndSpacesTabs(line);
50
- return (/^\s{0,3}(?:\*[ \t]*){3,}\s*$/.test(t) ||
51
- /^\s{0,3}(?:-[ \t]*){3,}\s*$/.test(t) ||
52
- /^\s{0,3}(?:_[ \t]*){3,}\s*$/.test(t));
53
- }
54
- function parseFenceDelimiter(line) {
55
- const t = trimEndSpacesTabs(line);
56
- const m = /^(\s{0,3})(`{3,}|~{3,})(.*)$/.exec(t);
57
- if (!m)
58
- return null;
59
- const run = m[2];
60
- const head = run[0];
61
- if (head !== "`" && head !== "~")
62
- return null;
63
- const ch = head === "`" ? "`" : "~";
64
- return { ch, runLen: run.length, rest: m[3] ?? "" };
65
- }
66
- function isClosingFenceLine(info, open) {
67
- if (info.ch !== open.ch || info.runLen < open.len)
68
- return false;
69
- return info.rest.trim() === "";
70
- }
71
- function lineAnchorHtml(mdLine0) {
72
- const mdLine = String(mdLine0);
73
- return `<span class="commentray-line-anchor" data-commentray-md-line="${mdLine}" id="commentray-md-line-${mdLine}" aria-hidden="true"></span>`;
74
- }
75
- function appendMdLineAnchorWhenAllowed(line, mdLine0) {
76
- if (isSetextUnderlineLine(line) || isThematicBreakLine(line))
77
- return line;
78
- /** Blank lines must stay blank: a line that is only `<span …>` breaks CommonMark HTML / paragraph starts after block markers. */
79
- if (line === "")
80
- return "";
81
- return `${line}${lineAnchorHtml(mdLine0)}`;
82
- }
83
- /**
84
- * Inserts per-line anchors for search / hash jumps and block separator anchors after each
85
- * `<!-- commentray:block … -->` line (optional index attrs).
86
- *
87
- * Anchors are appended to the line when safe. A **leading** `<span>` breaks CommonMark block
88
- * recognition (`#` headings, lists, thematic breaks, fences). Fenced code lines must not get a
89
- * trailing anchor either (would corrupt fence delimiters or appear inside code).
90
- */
91
- function injectCommentrayDocAnchors(markdown, links) {
92
- const byId = links ? new Map(links.map((l) => [l.id, l])) : undefined;
93
- const lines = markdown.split("\n");
94
- let fence = null;
95
- const out = [];
96
- for (let i = 0; i < lines.length; i++) {
97
- const line = lines[i];
98
- const delim = parseFenceDelimiter(line);
99
- if (fence) {
100
- if (delim && isClosingFenceLine(delim, fence)) {
101
- fence = null;
102
- out.push(line);
103
- continue;
104
- }
105
- out.push(line);
106
- continue;
107
- }
108
- if (delim) {
109
- fence = { ch: delim.ch, len: delim.runLen };
110
- out.push(line);
111
- continue;
112
- }
113
- const m = BLOCK_MARKER_HTML_LINE.exec(line);
114
- if (m?.[1]) {
115
- const id = m[1];
116
- const link = byId?.get(id);
117
- const attrs = link !== undefined
118
- ? ` data-source-start="${String(link.sourceStart)}" data-commentray-line="${String(link.commentrayLine)}"`
119
- : "";
120
- /** One `push` with embedded `\n\n` merged poorly with `join("\\n")`; keep real blank lines around raw `<div>`. */
121
- out.push(`${line}${lineAnchorHtml(i)}`);
122
- out.push("");
123
- out.push(`<div id="commentray-block-${escapeHtml(id)}" class="commentray-block-anchor" aria-hidden="true"${attrs}></div>`);
124
- out.push("");
125
- continue;
126
- }
127
- out.push(appendMdLineAnchorWhenAllowed(line, i));
128
- }
129
- return out.join("\n");
130
- }
131
48
  /** GitHub “mark” glyph (Octicons-style path), MIT-licensed silhouette. */
132
49
  const GITHUB_MARK_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="20" height="20" fill="currentColor" aria-hidden="true">' +
133
50
  '<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>' +
@@ -159,6 +76,25 @@ const TOOLBAR_ICON_FLIP_PANES_SVG = '<svg xmlns="http://www.w3.org/2000/svg" vie
159
76
  '<path d="M10.5 12H6l2.5-2.5M6 12l2.5 2.5"/>' +
160
77
  '<path d="M13.5 12H18l-2.5-2.5M18 12l-2.5 2.5"/>' +
161
78
  "</svg>";
79
+ /** Source markdown mode flip: rendered page <-> plain markdown rows. */
80
+ const TOOLBAR_ICON_FLIP_SOURCE_MARKDOWN_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
81
+ '<rect x="3" y="4" width="8" height="16" rx="1.5"/>' +
82
+ '<path d="M6 8h2M6 11h2M6 14h2"/>' +
83
+ '<rect x="13" y="4" width="8" height="16" rx="1.5"/>' +
84
+ '<path d="m15.5 12 2-2 2 2"/>' +
85
+ '<path d="m19.5 12-2 2-2-2"/>' +
86
+ "</svg>";
87
+ /** Link/share glyph for copying a permalink to the current documentation pair. */
88
+ const TOOLBAR_ICON_SHARE_LINK_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
89
+ '<path d="M10 14a5 5 0 0 0 7.07 0l2.83-2.83a5 5 0 0 0-7.07-7.07L10 6"/>' +
90
+ '<path d="M14 10a5 5 0 0 0-7.07 0L4.1 12.83a5 5 0 0 0 7.07 7.07L14 17"/>' +
91
+ "</svg>";
92
+ /** Help glyph for re-running the onboarding walkthrough. */
93
+ const TOOLBAR_ICON_HELP_TOUR_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
94
+ '<circle cx="12" cy="12" r="9"/>' +
95
+ '<path d="M9.1 9a3 3 0 1 1 4.92 2.3c-.8.6-1.52 1.08-1.52 2.2"/>' +
96
+ '<circle cx="12" cy="17" r="0.8" fill="currentColor" stroke="none"/>' +
97
+ "</svg>";
162
98
  function safeExternalHttpUrl(url) {
163
99
  const t = url?.trim();
164
100
  if (!t)
@@ -194,9 +130,10 @@ function buildToolbarEndHtml(githubRepoUrl, siteHubUrl) {
194
130
  return "";
195
131
  }
196
132
  function renderPageFooterHtml(input) {
197
- const { builtAt, toolHomeUrl, commentrayRenderSemver } = input;
133
+ const { builtAt, toolHomeUrl, commentrayRenderSemver, pagesBuildCommitSha } = input;
198
134
  const iso = builtAt.toISOString();
199
135
  const human = formatCommentrayBuiltAtLocal(builtAt);
136
+ const commitSuffix = pagesBuildCommitSha ? footerCommitSuffixHtml(pagesBuildCommitSha) : "";
200
137
  const tool = safeExternalHttpUrl(toolHomeUrl);
201
138
  if (tool) {
202
139
  const te = escapeHtml(tool);
@@ -206,11 +143,14 @@ function renderPageFooterHtml(input) {
206
143
  `Rendered with <a href="${te}" target="_blank" rel="noopener noreferrer">Commentray</a> ` +
207
144
  `<span class="app__footer-attribution__version" translate="no">v${ver}</span>: ` +
208
145
  `<time datetime="${escapeHtml(iso)}">${escapeHtml(human)}</time>` +
146
+ commitSuffix +
209
147
  `</p>` +
210
148
  `</footer>`);
211
149
  }
212
150
  return (`<footer class="app__footer" role="contentinfo">` +
213
- `<p class="app__footer-line">HTML generated <time datetime="${escapeHtml(iso)}">${escapeHtml(human)}</time></p>` +
151
+ `<p class="app__footer-line">HTML generated <time datetime="${escapeHtml(iso)}">${escapeHtml(human)}</time>` +
152
+ commitSuffix +
153
+ `</p>` +
214
154
  `</footer>`);
215
155
  }
216
156
  function renderRelatedGithubNavHtml(links) {
@@ -242,9 +182,13 @@ function renderToolbarDocHubHtml(opts) {
242
182
  : "";
243
183
  return { toolbarDocHubHtml, navRailDocumentedHtml };
244
184
  }
245
- function dualPanePanesInnerHtml(codeHtml, commentrayHtml) {
185
+ function dualPanePanesInnerHtml(codeHtml, commentrayHtml, sourceMarkdownRenderedHtml) {
186
+ const sourceRenderedPaneHtml = typeof sourceMarkdownRenderedHtml === "string" && sourceMarkdownRenderedHtml.trim().length > 0
187
+ ? ` <div class="source-pane source-pane--rendered-md" id="code-pane-markdown-body">${sourceMarkdownRenderedHtml}</div>\n`
188
+ : "";
246
189
  return (` <section class="pane--code" id="code-pane" aria-label="Source code">` +
247
- ` ${codeHtml}\n` +
190
+ ` <div class="source-pane source-pane--code" id="code-pane-code-body">${codeHtml}</div>\n` +
191
+ sourceRenderedPaneHtml +
248
192
  ` </section>\n` +
249
193
  ` <div class="gutter" id="gutter" role="separator" aria-orientation="vertical" aria-label="Resize panes"></div>\n` +
250
194
  ` <section class="pane--doc commentray" id="doc-pane" aria-label="Commentray">\n` +
@@ -253,7 +197,42 @@ function dualPanePanesInnerHtml(codeHtml, commentrayHtml) {
253
197
  ` </div>\n` +
254
198
  ` </section>\n`);
255
199
  }
256
- /** Plain-text Src/Doc labels above the panes; column widths track the resizable split via `--split-pct`. */
200
+ function sourceMarkdownToggleControlsHtml(enabled) {
201
+ if (!enabled) {
202
+ return { sourceMarkdownToggleHtml: "", sourceMarkdownFlipScrollAffordanceHtml: "" };
203
+ }
204
+ const label = "Switch source pane between rendered markdown and markdown source";
205
+ const title = "Switch source pane between rendered markdown and markdown source";
206
+ const btn = `<button type="button" id="source-markdown-pane-flip" class="toolbar-source-render-toggle" aria-controls="code-pane" aria-pressed="false" aria-label="${label}" title="${title}"><span class="toolbar-source-render-toggle__box" aria-hidden="true"></span><span class="toolbar-source-render-toggle__face" aria-hidden="true">${TOOLBAR_ICON_FLIP_SOURCE_MARKDOWN_SVG}</span><span class="toolbar-source-render-toggle__caption">Render</span></button>`;
207
+ const floating = `<button type="button" id="source-markdown-pane-flip-scroll" class="toolbar-icon-btn toolbar-icon-btn--source-markdown-scroll-narrow" hidden aria-controls="code-pane" aria-pressed="false" aria-label="${label}" title="${title}">${TOOLBAR_ICON_FLIP_SOURCE_MARKDOWN_SVG}</button>`;
208
+ return {
209
+ sourceMarkdownToggleHtml: btn,
210
+ sourceMarkdownFlipScrollAffordanceHtml: floating,
211
+ };
212
+ }
213
+ function isMarkdownLikeSource(opts) {
214
+ const lang = opts.language.trim().toLowerCase();
215
+ if (lang === "md" || lang === "markdown" || lang === "mdx")
216
+ return true;
217
+ const filePath = (opts.filePath ?? "").trim().toLowerCase();
218
+ return filePath.endsWith(".md") || filePath.endsWith(".mdx") || filePath.endsWith(".markdown");
219
+ }
220
+ /** For source-pane Markdown, resolve local links from the source file directory (repo tree), not companion storage. */
221
+ function sourcePaneOutputUrls(opts) {
222
+ const out = opts.commentrayOutputUrls;
223
+ if (!out)
224
+ return undefined;
225
+ const srcRel = (opts.filePath ?? "").trim();
226
+ if (srcRel.length === 0)
227
+ return out;
228
+ const repoRoot = path.resolve(out.repoRootAbs);
229
+ const candidate = path.resolve(repoRoot, srcRel);
230
+ const rel = path.relative(repoRoot, candidate);
231
+ if (rel.startsWith("..") || path.isAbsolute(rel))
232
+ return out;
233
+ return { ...out, markdownUrlBaseDirAbs: path.dirname(candidate) };
234
+ }
235
+ /** Pair paths above the panes; column widths track the resizable split via `--split-pct`. */
257
236
  function renderShellPairContextHtml(filePath, commentrayPath) {
258
237
  const fpRaw = (filePath ?? "").trim();
259
238
  const crRaw = (commentrayPath ?? "").trim();
@@ -265,12 +244,10 @@ function renderShellPairContextHtml(filePath, commentrayPath) {
265
244
  const crDisp = crRaw.length > 0 ? cr : "—";
266
245
  return `<div class="shell__pair-context" aria-label="Current documentation pair">
267
246
  <div class="shell__pair-cell shell__pair-cell--src">
268
- <span class="shell__pair-lab">Src</span>
269
247
  <span class="shell__pair-path" title="${fp}">${fpDisp}</span>
270
248
  </div>
271
249
  <div class="shell__pair-gutter-spacer" aria-hidden="true"></div>
272
250
  <div class="shell__pair-cell shell__pair-cell--doc">
273
- <span class="shell__pair-lab">Doc</span>
274
251
  <span class="shell__pair-path shell__pair-path--secondary" id="nav-rail-doc-path" title="${cr}">${crDisp}</span>
275
252
  </div>
276
253
  </div>`;
@@ -292,6 +269,19 @@ function loadCodeBrowserClientBundle() {
292
269
  }
293
270
  throw new Error("Missing code-browser-client.bundle.js. Run `npm run build -w @commentray/render` to bundle the browser client.");
294
271
  }
272
+ /** Intro-tour specific stylesheet; kept in a dedicated CSS file for easier tweaking. */
273
+ function loadCodeBrowserIntroStyles() {
274
+ const packagesDir = findMonorepoPackagesDir(monorepoLayoutStartDir(import.meta.url));
275
+ const renderDistDir = join(packagesDir, "render", "dist");
276
+ const inDist = join(renderDistDir, "code-browser-intro.css");
277
+ const fromSrc = join(packagesDir, "render", "src", "code-browser-intro.css");
278
+ for (const p of [inDist, fromSrc]) {
279
+ if (existsSync(p)) {
280
+ return readFileSync(p, "utf8");
281
+ }
282
+ }
283
+ throw new Error("Missing code-browser-intro.css. Ensure the render package includes intro tour styles.");
284
+ }
295
285
  /**
296
286
  * Compact theme control: primary click opens a menu (readme.io–style), secondary click cycles
297
287
  * system → light → dark. Paired with {@link ./code-browser-color-theme.ts} and the client bundle.
@@ -309,6 +299,11 @@ const TOOLBAR_COLOR_THEME_HTML = ` <div class="toolbar-theme">
309
299
  </div>
310
300
  </div>
311
301
  `;
302
+ const TOOLBAR_SHARE_LINK_HTML = ` <button type="button" id="commentray-share-link" class="toolbar-theme__trigger toolbar-share-link-btn" aria-label="Copy shareable permalink" title="Copy shareable permalink">${TOOLBAR_ICON_SHARE_LINK_SVG}</button>
303
+ `;
304
+ const TOOLBAR_HELP_TOUR_HTML = ` <button type="button" id="commentray-help-tour" class="toolbar-theme__trigger toolbar-help-tour-btn" aria-label="Restart onboarding walkthrough" title="Restart onboarding walkthrough">${TOOLBAR_ICON_HELP_TOUR_SVG}</button>
305
+ `;
306
+ const CODE_BROWSER_INTRO_STYLES = loadCodeBrowserIntroStyles();
312
307
  const CODE_BROWSER_STYLES = `
313
308
  :root {
314
309
  --cr-control-h: 32px;
@@ -653,7 +648,15 @@ const CODE_BROWSER_STYLES = `
653
648
  text-underline-offset: 2px;
654
649
  }
655
650
  .app__footer-attribution__version { font-weight: 600; }
651
+ .app__footer-attribution__sha {
652
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
653
+ font-size: 0.95em;
654
+ font-weight: 500;
655
+ padding: 0 2px;
656
+ word-break: break-all;
657
+ }
656
658
  .toolbar label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
659
+ .toolbar label[hidden] { display: none !important; }
657
660
  .toolbar-wrap-lines {
658
661
  position: relative;
659
662
  margin: 0;
@@ -772,6 +775,80 @@ const CODE_BROWSER_STYLES = `
772
775
  outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
773
776
  outline-offset: 2px;
774
777
  }
778
+ .toolbar-source-render-toggle {
779
+ position: relative;
780
+ margin: 0;
781
+ min-height: var(--cr-control-h);
782
+ padding: 0 12px 0 10px;
783
+ border-radius: var(--cr-control-radius);
784
+ border: 1px solid color-mix(in oklab, CanvasText 16%, Canvas);
785
+ background: Canvas;
786
+ display: inline-flex;
787
+ flex-direction: row;
788
+ align-items: center;
789
+ justify-content: flex-start;
790
+ gap: 8px;
791
+ font-size: var(--cr-ui-fs);
792
+ font-weight: 500;
793
+ color: color-mix(in oklab, CanvasText 88%, Canvas);
794
+ cursor: pointer;
795
+ }
796
+ .toolbar-source-render-toggle:hover {
797
+ background: color-mix(in oklab, CanvasText 6%, Canvas);
798
+ }
799
+ .toolbar-source-render-toggle__box {
800
+ flex: 0 0 auto;
801
+ width: 16px;
802
+ height: 16px;
803
+ box-sizing: border-box;
804
+ border: 1.5px solid color-mix(in oklab, CanvasText 38%, Canvas);
805
+ border-radius: 3px;
806
+ background: Canvas;
807
+ display: inline-flex;
808
+ align-items: center;
809
+ justify-content: center;
810
+ color: CanvasText;
811
+ }
812
+ .toolbar-source-render-toggle[aria-pressed="true"] .toolbar-source-render-toggle__box {
813
+ border-color: color-mix(in oklab, CanvasText 52%, Canvas);
814
+ background: color-mix(in oklab, CanvasText 6%, Canvas);
815
+ }
816
+ .toolbar-source-render-toggle__box::after {
817
+ content: "";
818
+ display: none;
819
+ width: 4px;
820
+ height: 9px;
821
+ margin-top: -2px;
822
+ border: solid currentColor;
823
+ border-width: 0 2px 2px 0;
824
+ transform: rotate(45deg);
825
+ }
826
+ .toolbar-source-render-toggle[aria-pressed="true"] .toolbar-source-render-toggle__box::after {
827
+ display: block;
828
+ }
829
+ .toolbar-source-render-toggle__face {
830
+ display: none;
831
+ align-items: center;
832
+ justify-content: center;
833
+ min-height: var(--cr-control-h);
834
+ min-width: var(--cr-control-h);
835
+ color: color-mix(in oklab, CanvasText 82%, Canvas);
836
+ }
837
+ .toolbar-source-render-toggle__caption {
838
+ white-space: nowrap;
839
+ }
840
+ .toolbar-source-render-toggle[aria-pressed="true"] {
841
+ color: CanvasText;
842
+ background: color-mix(in oklab, CanvasText 10%, Canvas);
843
+ }
844
+ .toolbar-source-render-toggle:focus-visible {
845
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
846
+ outline-offset: 2px;
847
+ }
848
+ .toolbar-icon-btn--source-markdown {
849
+ display: inline-flex;
850
+ }
851
+ ${CODE_BROWSER_INTRO_STYLES}
775
852
  .toolbar label input:focus-visible {
776
853
  outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
777
854
  outline-offset: 2px;
@@ -810,6 +887,16 @@ const CODE_BROWSER_STYLES = `
810
887
  outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
811
888
  outline-offset: 2px;
812
889
  }
890
+ .toolbar-theme__trigger svg {
891
+ width: var(--cr-icon-inner);
892
+ height: var(--cr-icon-inner);
893
+ display: block;
894
+ flex: 0 0 auto;
895
+ }
896
+ .toolbar-share-link-btn[data-copied="true"] {
897
+ background: color-mix(in oklab, #2ea043 24%, Canvas);
898
+ border-color: color-mix(in oklab, #2ea043 48%, CanvasText);
899
+ }
813
900
  .toolbar-theme__trigger .toolbar-theme__icon {
814
901
  display: none;
815
902
  flex: 0 0 auto;
@@ -917,6 +1004,14 @@ const CODE_BROWSER_STYLES = `
917
1004
  .documented-files-tree .tree-file-link:hover {
918
1005
  opacity: 0.92;
919
1006
  }
1007
+ .documented-files-tree .tree-file-link.tree-file-link--current {
1008
+ font-weight: 600;
1009
+ text-decoration-thickness: 2px;
1010
+ border-radius: 3px;
1011
+ padding: 1px 3px;
1012
+ margin: -1px -3px;
1013
+ background: color-mix(in oklab, CanvasText 10%, Canvas);
1014
+ }
920
1015
  .toolbar button {
921
1016
  font: inherit;
922
1017
  font-size: var(--cr-ui-fs);
@@ -1018,14 +1113,6 @@ const CODE_BROWSER_STYLES = `
1018
1113
  min-width: 0;
1019
1114
  padding-left: var(--cr-pane-inline-pad);
1020
1115
  }
1021
- .shell__pair-lab {
1022
- flex: 0 0 auto;
1023
- font-size: var(--cr-label-caps-fs);
1024
- font-weight: 700;
1025
- letter-spacing: var(--cr-label-caps-track);
1026
- text-transform: uppercase;
1027
- opacity: 0.72;
1028
- }
1029
1116
  .shell__pair-path {
1030
1117
  flex: 1 1 auto;
1031
1118
  min-width: 0;
@@ -1044,6 +1131,31 @@ const CODE_BROWSER_STYLES = `
1044
1131
  --code-line-font-size: 13px;
1045
1132
  --code-line-height: 1.5;
1046
1133
  }
1134
+ .source-pane {
1135
+ min-width: 0;
1136
+ }
1137
+ .source-pane--rendered-md {
1138
+ font-size: 15px;
1139
+ line-height: 1.45;
1140
+ }
1141
+ .source-pane--rendered-md img {
1142
+ max-width: 100%;
1143
+ height: auto;
1144
+ }
1145
+ .source-pane--rendered-md .commentray-mermaid {
1146
+ overflow-x: auto;
1147
+ max-width: 100%;
1148
+ }
1149
+ .source-pane--rendered-md .commentray-line-anchor--source {
1150
+ display: inline;
1151
+ vertical-align: baseline;
1152
+ }
1153
+ #shell[data-source-pane-mode="rendered-markdown"] .source-pane--code {
1154
+ display: none;
1155
+ }
1156
+ #shell[data-source-pane-mode="source"] .source-pane--rendered-md {
1157
+ display: none;
1158
+ }
1047
1159
  .pane--code .code-line-stack { --code-ln-min-ch: 3; }
1048
1160
  .pane--code .code-line {
1049
1161
  display: grid;
@@ -1105,7 +1217,12 @@ const CODE_BROWSER_STYLES = `
1105
1217
  .gutter__rays {
1106
1218
  position: absolute; inset: 0; pointer-events: none; z-index: 1;
1107
1219
  }
1108
- .gutter__rays svg { width: 100%; height: 100%; display: block; overflow: visible; }
1220
+ .gutter__rays svg {
1221
+ width: 100%;
1222
+ height: 100%;
1223
+ display: block;
1224
+ overflow: hidden;
1225
+ }
1109
1226
  .gutter__rays-path {
1110
1227
  fill: none; stroke-linecap: round; vector-effect: non-scaling-stroke;
1111
1228
  stroke: color-mix(in oklab, var(--commentray-ray-accent) 72%, CanvasText);
@@ -1144,11 +1261,95 @@ const CODE_BROWSER_STYLES = `
1144
1261
  .doc-pane-body {
1145
1262
  flex: 1 1 auto; min-height: 0; overflow: auto;
1146
1263
  }
1147
- /** Wide GFM tables: intrinsic width so the doc pane scrolls sideways instead of squeezing columns. */
1148
- .pane--doc .doc-pane-body :where(table) {
1264
+ /* Inline backtick code chips (GitHub-like): prose context only, never fenced pre/code blocks. */
1265
+ .pane--doc .doc-pane-body :where(p, li, blockquote, td, th, h1, h2, h3, h4, h5, h6) > code,
1266
+ .shell--stretch-rows .stretch-preamble :where(p, li, blockquote, td, th, h1, h2, h3, h4, h5, h6) > code,
1267
+ .block-stretch td.stretch-doc .stretch-doc-inner :where(p, li, blockquote, td, th, h1, h2, h3, h4, h5, h6) > code {
1268
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
1269
+ font-size: 0.92em;
1270
+ padding: 0.12em 0.36em;
1271
+ border-radius: 6px;
1272
+ border: 1px solid color-mix(in oklab, CanvasText 12%, Canvas);
1273
+ background: color-mix(in oklab, CanvasText 8%, Canvas);
1274
+ color: inherit;
1275
+ }
1276
+ @media (prefers-color-scheme: dark) {
1277
+ :root:is(:not([data-commentray-theme]), [data-commentray-theme="system"]) .pane--doc .doc-pane-body :where(p, li, blockquote, td, th, h1, h2, h3, h4, h5, h6) > code,
1278
+ :root:is(:not([data-commentray-theme]), [data-commentray-theme="system"]) .shell--stretch-rows .stretch-preamble :where(p, li, blockquote, td, th, h1, h2, h3, h4, h5, h6) > code,
1279
+ :root:is(:not([data-commentray-theme]), [data-commentray-theme="system"]) .block-stretch td.stretch-doc .stretch-doc-inner :where(p, li, blockquote, td, th, h1, h2, h3, h4, h5, h6) > code {
1280
+ border-color: color-mix(in oklab, CanvasText 26%, Canvas);
1281
+ background: color-mix(in oklab, CanvasText 16%, Canvas);
1282
+ }
1283
+ }
1284
+ :root[data-commentray-theme="dark"] .pane--doc .doc-pane-body :where(p, li, blockquote, td, th, h1, h2, h3, h4, h5, h6) > code,
1285
+ :root[data-commentray-theme="dark"] .shell--stretch-rows .stretch-preamble :where(p, li, blockquote, td, th, h1, h2, h3, h4, h5, h6) > code,
1286
+ :root[data-commentray-theme="dark"] .block-stretch td.stretch-doc .stretch-doc-inner :where(p, li, blockquote, td, th, h1, h2, h3, h4, h5, h6) > code {
1287
+ border-color: color-mix(in oklab, CanvasText 26%, Canvas);
1288
+ background: color-mix(in oklab, CanvasText 16%, Canvas);
1289
+ }
1290
+ /**
1291
+ * GFM tables in rendered Markdown (doc pane, stretch preamble, per-block doc cells).
1292
+ * Intrinsic width so the pane scrolls sideways instead of squeezing columns; borders
1293
+ * and padding match familiar GitHub-style readability.
1294
+ */
1295
+ .pane--doc .doc-pane-body :where(table),
1296
+ .shell--stretch-rows .stretch-preamble :where(table),
1297
+ .block-stretch td.stretch-doc .stretch-doc-inner :where(table) {
1149
1298
  width: max-content;
1150
1299
  max-width: none;
1151
1300
  border-collapse: collapse;
1301
+ margin: 0.85em 0;
1302
+ font-size: inherit;
1303
+ line-height: inherit;
1304
+ }
1305
+ .pane--doc .doc-pane-body :where(th, td),
1306
+ .shell--stretch-rows .stretch-preamble :where(th, td),
1307
+ .block-stretch td.stretch-doc .stretch-doc-inner :where(th, td) {
1308
+ border: 1px solid color-mix(in oklab, CanvasText 22%, Canvas);
1309
+ padding: 8px 12px;
1310
+ vertical-align: top;
1311
+ }
1312
+ .pane--doc .doc-pane-body :where(thead th),
1313
+ .shell--stretch-rows .stretch-preamble :where(thead th),
1314
+ .block-stretch td.stretch-doc .stretch-doc-inner :where(thead th) {
1315
+ font-weight: 600;
1316
+ background: color-mix(in oklab, CanvasText 7%, Canvas);
1317
+ }
1318
+ .pane--doc .doc-pane-body tbody tr:nth-child(even) :where(td),
1319
+ .shell--stretch-rows .stretch-preamble tbody tr:nth-child(even) :where(td),
1320
+ .block-stretch td.stretch-doc .stretch-doc-inner tbody tr:nth-child(even) :where(td) {
1321
+ background: color-mix(in oklab, CanvasText 3.5%, Canvas);
1322
+ }
1323
+ .pane--doc .doc-pane-body :where(ul.contains-task-list),
1324
+ .shell--stretch-rows .stretch-preamble :where(ul.contains-task-list),
1325
+ .block-stretch td.stretch-doc .stretch-doc-inner :where(ul.contains-task-list) {
1326
+ list-style: none;
1327
+ padding-inline-start: 1.2em;
1328
+ }
1329
+ .pane--doc .doc-pane-body :where(li.task-list-item),
1330
+ .shell--stretch-rows .stretch-preamble :where(li.task-list-item),
1331
+ .block-stretch td.stretch-doc .stretch-doc-inner :where(li.task-list-item) {
1332
+ position: relative;
1333
+ }
1334
+ .pane--doc .doc-pane-body :where(li.task-list-item input[type="checkbox"]),
1335
+ .shell--stretch-rows .stretch-preamble :where(li.task-list-item input[type="checkbox"]),
1336
+ .block-stretch td.stretch-doc .stretch-doc-inner :where(li.task-list-item input[type="checkbox"]) {
1337
+ position: absolute;
1338
+ margin-inline-start: -1.35em;
1339
+ margin-top: 0.2em;
1340
+ }
1341
+ .pane--doc .doc-pane-body :where(del),
1342
+ .shell--stretch-rows .stretch-preamble :where(del),
1343
+ .block-stretch td.stretch-doc .stretch-doc-inner :where(del) {
1344
+ opacity: 0.82;
1345
+ }
1346
+ .pane--doc .doc-pane-body :where(section.footnotes),
1347
+ .shell--stretch-rows .stretch-preamble :where(section.footnotes),
1348
+ .block-stretch td.stretch-doc .stretch-doc-inner :where(section.footnotes) {
1349
+ margin-top: 1.5em;
1350
+ padding-top: 0.75em;
1351
+ border-top: 1px solid color-mix(in oklab, CanvasText 18%, Canvas);
1352
+ font-size: 0.92em;
1152
1353
  }
1153
1354
  .pane--doc .doc-pane-body .commentray-mermaid {
1154
1355
  overflow-x: auto;
@@ -1166,6 +1367,23 @@ const CODE_BROWSER_STYLES = `
1166
1367
  overflow-wrap: normal;
1167
1368
  word-break: normal;
1168
1369
  }
1370
+ #doc-pane-body .commentray-page-break {
1371
+ position: relative;
1372
+ min-height: var(--commentray-page-break-min-height, clamp(260px, 56vh, 620px));
1373
+ margin: 24px 0;
1374
+ display: flex;
1375
+ align-items: center;
1376
+ justify-content: center;
1377
+ pointer-events: none;
1378
+ }
1379
+ #doc-pane-body .commentray-page-break__rule {
1380
+ width: 100%;
1381
+ border-top: 1px dashed var(--border);
1382
+ opacity: 0.38;
1383
+ }
1384
+ #shell[data-page-breaks-enabled="false"] .commentray-page-break {
1385
+ display: none;
1386
+ }
1169
1387
  .toolbar-angle-picker {
1170
1388
  display: inline-flex;
1171
1389
  align-items: center;
@@ -1385,6 +1603,19 @@ const CODE_BROWSER_STYLES = `
1385
1603
  0 1px 2px color-mix(in oklab, CanvasText 12%, transparent),
1386
1604
  0 4px 14px color-mix(in oklab, CanvasText 18%, transparent);
1387
1605
  }
1606
+ .toolbar-icon-btn--source-markdown-scroll-narrow {
1607
+ display: none;
1608
+ }
1609
+ #source-markdown-pane-flip-scroll.toolbar-icon-btn--source-markdown-scroll-narrow.is-visible {
1610
+ display: inline-flex;
1611
+ position: fixed;
1612
+ top: calc(10px + env(safe-area-inset-top, 0px));
1613
+ left: calc(12px + env(safe-area-inset-left, 0px));
1614
+ z-index: 50;
1615
+ box-shadow:
1616
+ 0 1px 2px color-mix(in oklab, CanvasText 12%, transparent),
1617
+ 0 4px 14px color-mix(in oklab, CanvasText 18%, transparent);
1618
+ }
1388
1619
  /** Region connector lines are not needed on the narrow single-pane layout (gutter is hidden). */
1389
1620
  .shell:not(.shell--stretch-rows) .gutter .gutter__rays {
1390
1621
  opacity: 0 !important;
@@ -1450,6 +1681,47 @@ const CODE_BROWSER_STYLES = `
1450
1681
  color: CanvasText;
1451
1682
  text-shadow: 0 0 2px Canvas, 0 0 3px Canvas;
1452
1683
  }
1684
+ .toolbar-source-render-toggle {
1685
+ min-width: var(--cr-control-h);
1686
+ width: var(--cr-control-h);
1687
+ height: var(--cr-control-h);
1688
+ padding: 0;
1689
+ justify-content: center;
1690
+ gap: 0;
1691
+ }
1692
+ .toolbar-source-render-toggle__caption {
1693
+ position: absolute;
1694
+ width: 1px;
1695
+ height: 1px;
1696
+ padding: 0;
1697
+ margin: -1px;
1698
+ overflow: hidden;
1699
+ clip: rect(0, 0, 0, 0);
1700
+ white-space: nowrap;
1701
+ border: 0;
1702
+ }
1703
+ .toolbar-source-render-toggle__box {
1704
+ display: none;
1705
+ }
1706
+ .toolbar-source-render-toggle__face {
1707
+ display: inline-flex;
1708
+ position: relative;
1709
+ width: 100%;
1710
+ height: 100%;
1711
+ min-height: var(--cr-control-h);
1712
+ min-width: var(--cr-control-h);
1713
+ }
1714
+ .toolbar-source-render-toggle[aria-pressed="true"] .toolbar-source-render-toggle__face::after {
1715
+ content: "✓";
1716
+ position: absolute;
1717
+ right: 1px;
1718
+ bottom: 0;
1719
+ font-size: 11px;
1720
+ line-height: 1;
1721
+ font-weight: 800;
1722
+ color: CanvasText;
1723
+ text-shadow: 0 0 2px Canvas, 0 0 3px Canvas;
1724
+ }
1453
1725
  .shell:not(.shell--stretch-rows)[data-dual-mobile-pane="code"] .pane--doc,
1454
1726
  .shell:not(.shell--stretch-rows)[data-dual-mobile-pane="code"] .gutter {
1455
1727
  display: none !important;
@@ -1512,11 +1784,6 @@ const CODE_BROWSER_STYLES = `
1512
1784
  max-width: 100%;
1513
1785
  }
1514
1786
  .shell--stretch-rows .stretch-preamble img { max-width: 100%; height: auto; }
1515
- .shell--stretch-rows .stretch-preamble :where(table) {
1516
- width: max-content;
1517
- max-width: none;
1518
- border-collapse: collapse;
1519
- }
1520
1787
  .block-stretch {
1521
1788
  width: 100%;
1522
1789
  border-collapse: collapse;
@@ -1541,11 +1808,6 @@ const CODE_BROWSER_STYLES = `
1541
1808
  overflow-x: auto;
1542
1809
  }
1543
1810
  .block-stretch td.stretch-doc .stretch-doc-inner img { max-width: 100%; height: auto; }
1544
- .block-stretch td.stretch-doc .stretch-doc-inner :where(table) {
1545
- width: max-content;
1546
- max-width: none;
1547
- border-collapse: collapse;
1548
- }
1549
1811
  .block-stretch td.stretch-doc .stretch-doc-inner .commentray-mermaid {
1550
1812
  overflow-x: auto;
1551
1813
  max-width: 100%;
@@ -1660,16 +1922,20 @@ ${CODE_BROWSER_STYLES}
1660
1922
  <span class="toolbar-wrap-lines__caption">Wrap lines</span>
1661
1923
  </label>
1662
1924
  ${dualFlipControlHtml}
1925
+ ${p.sourceMarkdownToggleHtml}
1663
1926
  ${p.toolbarDocHubHtml}
1664
1927
  ${p.relatedNavHtml}
1665
1928
  </div>
1666
1929
  <div class="toolbar__primary-trail">
1667
1930
  ${p.toolbarEndHtml}
1931
+ ${TOOLBAR_SHARE_LINK_HTML}
1932
+ ${TOOLBAR_HELP_TOUR_HTML}
1668
1933
  ${TOOLBAR_COLOR_THEME_HTML}
1669
1934
  </div>
1670
1935
  </div>
1671
1936
  </header>
1672
1937
  ${dualFlipScrollAffordanceHtml}
1938
+ ${p.sourceMarkdownFlipScrollAffordanceHtml}
1673
1939
  <header class="app__chrome" role="region" aria-label="Search">
1674
1940
  <div class="chrome__search-row">
1675
1941
  <label class="chrome__search-label" for="search-q" aria-label="Search" title="Search"><span class="chrome__search-label__caption nav-rail__search-label">Search</span><span class="chrome__search-label__glyph" aria-hidden="true">${CHROME_ICON_SEARCH_SVG}</span></label>
@@ -1679,7 +1945,7 @@ ${TOOLBAR_COLOR_THEME_HTML}
1679
1945
  <div class="search-results" id="search-results" hidden aria-live="polite"></div>
1680
1946
  </header>
1681
1947
  <main id="main-content" class="app__main" tabindex="-1">
1682
- <div class="${shellClass}" id="shell" data-layout="${p.layout}"${p.layout === "dual" ? ' data-dual-mobile-pane="doc"' : ""} data-raw-code-b64="${escapeHtml(p.rawCodeB64)}" data-raw-md-b64="${escapeHtml(p.rawMdB64)}" data-scroll-block-links-b64="${escapeHtml(p.scrollBlockLinksB64)}"${p.shellDocumentedPairsAttr}${p.shellSearchAttrs}${p.shellPairDocDataAttr}>
1948
+ <div class="${shellClass}" id="shell" data-layout="${p.layout}"${p.layout === "dual" ? ' data-dual-mobile-pane="doc"' : ""}${p.sourcePaneModeAttr} data-raw-code-b64="${escapeHtml(p.rawCodeB64)}" data-raw-md-b64="${escapeHtml(p.rawMdB64)}" data-scroll-block-links-b64="${escapeHtml(p.scrollBlockLinksB64)}"${p.shellDocumentedPairsAttr}${p.shellSearchAttrs}${p.shellPairIdentityDataAttrs}${p.shellPairDocDataAttr}>
1683
1949
  ${p.shellInner}
1684
1950
  </div>
1685
1951
  </main>
@@ -1693,10 +1959,53 @@ ${loadCodeBrowserClientBundle()}
1693
1959
  </body>
1694
1960
  </html>`;
1695
1961
  }
1962
+ function firstNonEmpty(values) {
1963
+ return values.find((v) => v.trim().length > 0);
1964
+ }
1965
+ function resolveMultiAngleDefaultSelection(args) {
1966
+ const { multi, defaultId, opts, builtAngles } = args;
1967
+ let defaultMarkdown = opts.commentrayMarkdown;
1968
+ let defaultScrollB64 = "";
1969
+ let defaultPathSearch = (opts.commentrayPathForSearch ?? "").trim();
1970
+ let defaultGh = opts.commentrayOnGithubUrl;
1971
+ let defaultStaticBrowse = (opts.commentrayStaticBrowseUrl ?? "").trim();
1972
+ let defaultPaneHtml = "";
1973
+ for (const b of builtAngles) {
1974
+ if (b.spec.id !== defaultId)
1975
+ continue;
1976
+ defaultMarkdown = b.spec.markdown;
1977
+ defaultScrollB64 = b.scrollB64;
1978
+ defaultPathSearch = b.spec.commentrayPathRel.trim();
1979
+ defaultGh = b.spec.commentrayOnGithubUrl;
1980
+ defaultStaticBrowse = (b.spec.staticBrowseUrl ?? "").trim();
1981
+ defaultPaneHtml = b.commentrayHtml;
1982
+ break;
1983
+ }
1984
+ if (defaultStaticBrowse.length === 0) {
1985
+ defaultStaticBrowse =
1986
+ firstNonEmpty(multi.angles.map((a) => (a.staticBrowseUrl ?? "").trim())) ?? "";
1987
+ }
1988
+ if ((defaultGh ?? "").trim().length === 0) {
1989
+ defaultGh = firstNonEmpty(multi.angles.map((a) => (a.commentrayOnGithubUrl ?? "").trim()));
1990
+ }
1991
+ return {
1992
+ defaultMarkdown,
1993
+ defaultScrollB64,
1994
+ defaultPathSearch,
1995
+ defaultGh,
1996
+ defaultStaticBrowse,
1997
+ defaultPaneHtml,
1998
+ };
1999
+ }
1696
2000
  async function multiAngleJsonRowAndDocHtml(opts, spec) {
1697
2001
  const rows = spec.blockStretchRows;
1698
- const links = rows !== undefined
1699
- ? buildBlockScrollLinks(rows.index, rows.sourceRelative, rows.commentrayPathRel, spec.markdown, opts.code)
2002
+ const angleCrNorm = normalizeRepoRelativePath(spec.commentrayPathRel.replaceAll("\\", "/"));
2003
+ const primaryNorm = normalizeRepoRelativePath((opts.filePath ?? "").replaceAll("\\", "/"));
2004
+ const rowsPathOk = rows !== undefined &&
2005
+ normalizeRepoRelativePath(rows.commentrayPathRel.replaceAll("\\", "/")) === angleCrNorm &&
2006
+ normalizeRepoRelativePath(rows.sourceRelative.replaceAll("\\", "/")) === primaryNorm;
2007
+ const links = rows !== undefined && rowsPathOk
2008
+ ? buildBlockScrollLinks(rows.index, rows.sourceRelative, angleCrNorm, spec.markdown, opts.code)
1700
2009
  : [];
1701
2010
  const mdForDoc = injectCommentrayDocAnchors(spec.markdown, links.length > 0 ? links : undefined);
1702
2011
  const scrollB64 = links.length > 0 ? Buffer.from(JSON.stringify(links), "utf8").toString("base64") : "";
@@ -1723,29 +2032,24 @@ async function buildMultiAngleDualPaneShell(opts, multi) {
1723
2032
  ? multi.defaultAngleId
1724
2033
  : (multi.angles[0]?.id ?? "main");
1725
2034
  const jsonAngles = [];
1726
- let defaultMarkdown = opts.commentrayMarkdown;
1727
- let defaultScrollB64 = "";
1728
- let defaultPathSearch = (opts.commentrayPathForSearch ?? "").trim();
1729
- let defaultGh = opts.commentrayOnGithubUrl;
1730
- let defaultStaticBrowse = (opts.commentrayStaticBrowseUrl ?? "").trim();
1731
- let defaultPaneHtml = "";
1732
- const codeHtml = await renderHighlightedCodeLineRows(opts.code, opts.language);
2035
+ const builtAngles = [];
2036
+ const sourceMarkdownEnabled = isMarkdownLikeSource(opts);
2037
+ const sourceMdForPane = sourceMarkdownEnabled ? injectSourceMarkdownAnchors(opts.code) : "";
2038
+ const sourcePaneUrls = sourcePaneOutputUrls(opts);
2039
+ const [codeHtml, sourceMarkdownPaneHtml] = await Promise.all([
2040
+ renderHighlightedCodeLineRows(opts.code, opts.language),
2041
+ sourceMarkdownEnabled
2042
+ ? renderMarkdownToHtml(sourceMdForPane, {
2043
+ commentrayOutputUrls: sourcePaneUrls,
2044
+ })
2045
+ : Promise.resolve(""),
2046
+ ]);
1733
2047
  for (const spec of multi.angles) {
1734
2048
  const { jsonRow, commentrayHtml, scrollB64 } = await multiAngleJsonRowAndDocHtml(opts, spec);
1735
- if (spec.id === defaultId) {
1736
- defaultMarkdown = spec.markdown;
1737
- defaultScrollB64 = scrollB64;
1738
- defaultPathSearch = spec.commentrayPathRel.trim();
1739
- defaultGh = spec.commentrayOnGithubUrl;
1740
- {
1741
- const sb = (spec.staticBrowseUrl ?? "").trim();
1742
- if (sb.length > 0)
1743
- defaultStaticBrowse = sb;
1744
- }
1745
- defaultPaneHtml = commentrayHtml;
1746
- }
2049
+ builtAngles.push({ spec, commentrayHtml, scrollB64 });
1747
2050
  jsonAngles.push(jsonRow);
1748
2051
  }
2052
+ const { defaultMarkdown, defaultScrollB64, defaultPathSearch, defaultGh, defaultStaticBrowse, defaultPaneHtml, } = resolveMultiAngleDefaultSelection({ multi, defaultId, opts, builtAngles });
1749
2053
  const selOpts = multi.angles
1750
2054
  .map((a) => {
1751
2055
  const lab = escapeHtml(a.title?.trim() || a.id);
@@ -1754,7 +2058,7 @@ async function buildMultiAngleDualPaneShell(opts, multi) {
1754
2058
  .join("");
1755
2059
  const angleSelectHtml = `<span class="toolbar-angle-picker"><label class="toolbar-angle-picker__lab nav-rail__search-label" for="angle-select">Angle</label><select id="angle-select" aria-label="Commentray angle">${selOpts}</select></span>`;
1756
2060
  const pairHtml = renderShellPairContextHtml(opts.filePath, defaultPathSearch);
1757
- const shellInner = wrapDualShellInner(pairHtml, dualPanePanesInnerHtml(codeHtml, defaultPaneHtml));
2061
+ const shellInner = wrapDualShellInner(pairHtml, dualPanePanesInnerHtml(codeHtml, defaultPaneHtml, sourceMarkdownPaneHtml));
1758
2062
  const payloadObj = { defaultAngleId: defaultId, angles: jsonAngles };
1759
2063
  const multiAnglePayloadB64 = Buffer.from(JSON.stringify(payloadObj), "utf8").toString("base64");
1760
2064
  return {
@@ -1768,6 +2072,8 @@ async function buildMultiAngleDualPaneShell(opts, multi) {
1768
2072
  },
1769
2073
  angleSelectHtml,
1770
2074
  multiAnglePayloadB64,
2075
+ sourceMarkdownToggleEnabled: sourceMarkdownEnabled,
2076
+ sourcePaneDefaultMode: sourceMarkdownEnabled ? "rendered-markdown" : "source",
1771
2077
  };
1772
2078
  }
1773
2079
  async function buildCodeBrowserShell(opts, layoutPref) {
@@ -1785,6 +2091,8 @@ async function buildCodeBrowserShell(opts, layoutPref) {
1785
2091
  scrollBlockLinksB64: ms.scrollBlockLinksB64,
1786
2092
  angleSelectHtml: built.angleSelectHtml,
1787
2093
  multiAnglePayloadB64: built.multiAnglePayloadB64,
2094
+ sourceMarkdownToggleEnabled: built.sourceMarkdownToggleEnabled,
2095
+ sourcePaneDefaultMode: built.sourcePaneDefaultMode,
1788
2096
  multiShell: ms,
1789
2097
  };
1790
2098
  }
@@ -1811,14 +2119,31 @@ async function buildCodeBrowserShell(opts, layoutPref) {
1811
2119
  if (links.length > 0) {
1812
2120
  scrollBlockLinksB64 = Buffer.from(JSON.stringify(links), "utf8").toString("base64");
1813
2121
  }
1814
- const [codeHtml, commentrayHtml] = await Promise.all([
2122
+ const sourceMarkdownEnabled = isMarkdownLikeSource(opts);
2123
+ const sourceMdForPane = sourceMarkdownEnabled ? injectSourceMarkdownAnchors(opts.code) : "";
2124
+ const sourcePaneUrls = sourcePaneOutputUrls(opts);
2125
+ const [codeHtml, commentrayHtml, sourceMarkdownPaneHtml] = await Promise.all([
1815
2126
  renderHighlightedCodeLineRows(opts.code, opts.language),
1816
2127
  renderMarkdownToHtml(mdForDoc, {
1817
2128
  commentrayOutputUrls: opts.commentrayOutputUrls,
1818
2129
  }),
2130
+ sourceMarkdownEnabled
2131
+ ? renderMarkdownToHtml(sourceMdForPane, {
2132
+ commentrayOutputUrls: sourcePaneUrls,
2133
+ })
2134
+ : Promise.resolve(""),
1819
2135
  ]);
1820
2136
  const pairHtml = renderShellPairContextHtml(opts.filePath, (opts.commentrayPathForSearch ?? "").trim());
1821
- shellInner = wrapDualShellInner(pairHtml, dualPanePanesInnerHtml(codeHtml, commentrayHtml));
2137
+ shellInner = wrapDualShellInner(pairHtml, dualPanePanesInnerHtml(codeHtml, commentrayHtml, sourceMarkdownPaneHtml));
2138
+ return {
2139
+ layout,
2140
+ shellInner,
2141
+ scrollBlockLinksB64,
2142
+ angleSelectHtml: "",
2143
+ multiAnglePayloadB64: "",
2144
+ sourceMarkdownToggleEnabled: sourceMarkdownEnabled,
2145
+ sourcePaneDefaultMode: sourceMarkdownEnabled ? "rendered-markdown" : "source",
2146
+ };
1822
2147
  }
1823
2148
  return {
1824
2149
  layout,
@@ -1826,6 +2151,8 @@ async function buildCodeBrowserShell(opts, layoutPref) {
1826
2151
  scrollBlockLinksB64,
1827
2152
  angleSelectHtml: "",
1828
2153
  multiAnglePayloadB64: "",
2154
+ sourceMarkdownToggleEnabled: false,
2155
+ sourcePaneDefaultMode: "source",
1829
2156
  };
1830
2157
  }
1831
2158
  function searchChromeFromOptions(opts, commentrayPathOverride) {
@@ -1856,6 +2183,23 @@ function toolbarCommentrayGithubFromShell(shell, opts) {
1856
2183
  function rawMdB64FromShell(shell, opts) {
1857
2184
  return (shell.multiShell?.rawMdB64 ?? Buffer.from(opts.commentrayMarkdown, "utf8").toString("base64"));
1858
2185
  }
2186
+ function currentPairCommentrayPathRel(shell, opts) {
2187
+ return (shell.multiShell?.commentrayPathForSearch ??
2188
+ opts.commentrayPathForSearch ??
2189
+ opts.blockStretchRows?.commentrayPathRel ??
2190
+ "").trim();
2191
+ }
2192
+ /**
2193
+ * Repo-relative source + companion Markdown paths for matching the current page to nav pairs
2194
+ * (see `code-browser-client.ts` documented-files tree).
2195
+ */
2196
+ function shellPairIdentityDataAttrs(shell, opts) {
2197
+ const src = normPosixPath(opts.filePath ?? "");
2198
+ const cr = normPosixPath(currentPairCommentrayPathRel(shell, opts));
2199
+ if (src.length === 0 || cr.length === 0)
2200
+ return "";
2201
+ return ` data-commentray-pair-source-path="${escapeHtml(src)}" data-commentray-pair-commentray-path="${escapeHtml(cr)}"`;
2202
+ }
1859
2203
  /** Canonical doc target for static validation: same-site `./browse/…` when present, else GitHub blob. */
1860
2204
  function shellPairDocDataAttr(shell, opts) {
1861
2205
  if (shell.layout !== "dual")
@@ -1898,6 +2242,7 @@ export async function renderCodeBrowserHtml(opts) {
1898
2242
  builtAt,
1899
2243
  toolHomeUrl: opts.toolHomeUrl,
1900
2244
  commentrayRenderSemver: renderSemver,
2245
+ pagesBuildCommitSha: normalizePagesBuildCommitSha(opts.pagesBuildCommitSha),
1901
2246
  });
1902
2247
  const { hljsLight, hljsDark } = hljsStylesheetThemes(opts.hljsTheme);
1903
2248
  const mermaidScript = mermaidRuntimeScriptHtml(opts.includeMermaidRuntime);
@@ -1915,11 +2260,15 @@ export async function renderCodeBrowserHtml(opts) {
1915
2260
  const shellDocumentedPairsAttr = shellDocumentedPairsAttrFromOptions(opts);
1916
2261
  const shellSearchAttrs = shellSearchAttrsWithNavJson(shellSearchAttrsBase, opts.documentedNavJsonUrl);
1917
2262
  const pairDocDataAttr = shellPairDocDataAttr(shell, opts);
2263
+ const pairIdentityDataAttrs = shellPairIdentityDataAttrs(shell, opts);
2264
+ const sourceMarkdownToggles = sourceMarkdownToggleControlsHtml(shell.sourceMarkdownToggleEnabled);
2265
+ const sourcePaneModeAttr = ` data-source-pane-mode="${shell.sourcePaneDefaultMode}"`;
1918
2266
  return buildCodeBrowserPageHtml({
1919
2267
  title,
1920
2268
  metaDescriptionHtml,
1921
2269
  generatorMetaHtml,
1922
2270
  toolbarSiteHubHtml,
2271
+ shellPairIdentityDataAttrs: pairIdentityDataAttrs,
1923
2272
  shellPairDocDataAttr: pairDocDataAttr,
1924
2273
  angleSelectHtml: shell.angleSelectHtml,
1925
2274
  toolbarDocHubHtml,
@@ -1939,6 +2288,9 @@ export async function renderCodeBrowserHtml(opts) {
1939
2288
  searchPlaceholder,
1940
2289
  shellSearchAttrs,
1941
2290
  multiAngleScriptBlock: shell.multiAnglePayloadB64,
2291
+ sourceMarkdownToggleHtml: sourceMarkdownToggles.sourceMarkdownToggleHtml,
2292
+ sourceMarkdownFlipScrollAffordanceHtml: sourceMarkdownToggles.sourceMarkdownFlipScrollAffordanceHtml,
2293
+ sourcePaneModeAttr,
1942
2294
  });
1943
2295
  }
1944
2296
  //# sourceMappingURL=code-browser.js.map