@commentray/render 0.0.9 → 0.1.0

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 (32) hide show
  1. package/README.md +1 -1
  2. package/dist/code-browser-client.bundle.js +24 -11
  3. package/dist/code-browser-client.js +697 -100
  4. package/dist/code-browser-client.js.map +1 -1
  5. package/dist/code-browser-intro.css +187 -0
  6. package/dist/code-browser-wide-intro-controller.d.ts +4 -0
  7. package/dist/code-browser-wide-intro-controller.d.ts.map +1 -0
  8. package/dist/code-browser-wide-intro-controller.js +148 -0
  9. package/dist/code-browser-wide-intro-controller.js.map +1 -0
  10. package/dist/code-browser-wide-intro-layout.d.ts +3 -0
  11. package/dist/code-browser-wide-intro-layout.d.ts.map +1 -0
  12. package/dist/code-browser-wide-intro-layout.js +84 -0
  13. package/dist/code-browser-wide-intro-layout.js.map +1 -0
  14. package/dist/code-browser-wide-intro-steps.d.ts +11 -0
  15. package/dist/code-browser-wide-intro-steps.d.ts.map +1 -0
  16. package/dist/code-browser-wide-intro-steps.js +108 -0
  17. package/dist/code-browser-wide-intro-steps.js.map +1 -0
  18. package/dist/code-browser-wide-intro-ui.d.ts +14 -0
  19. package/dist/code-browser-wide-intro-ui.d.ts.map +1 -0
  20. package/dist/code-browser-wide-intro-ui.js +67 -0
  21. package/dist/code-browser-wide-intro-ui.js.map +1 -0
  22. package/dist/code-browser.d.ts.map +1 -1
  23. package/dist/code-browser.js +597 -42
  24. package/dist/code-browser.js.map +1 -1
  25. package/dist/markdown-pipeline.d.ts.map +1 -1
  26. package/dist/markdown-pipeline.js +7 -1
  27. package/dist/markdown-pipeline.js.map +1 -1
  28. package/dist/side-by-side-layout-css.d.ts +1 -1
  29. package/dist/side-by-side-layout-css.d.ts.map +1 -1
  30. package/dist/side-by-side-layout-css.js +48 -0
  31. package/dist/side-by-side-layout-css.js.map +1 -1
  32. package/package.json +4 -3
@@ -11,6 +11,7 @@ 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";
14
15
  function renderGeneratorMetaHtml(label) {
15
16
  const t = label?.trim();
16
17
  if (!t)
@@ -31,6 +32,7 @@ function renderMetaDescriptionHtml(opts, title) {
31
32
  }
32
33
  /** Single capture: marker id (avoid a wrapping group around the whole comment — that shifted indices). */
33
34
  const BLOCK_MARKER_HTML_LINE = new RegExp(`^<!--\\s*commentray:block\\s+id=(${MARKER_ID_BODY})\\s*-->$`, "i");
35
+ const PAGE_BREAK_MARKER_HTML_LINE = /^<!--\s*commentray:page-break\s*-->$/i;
34
36
  function trimEndSpacesTabs(s) {
35
37
  let end = s.length;
36
38
  while (end > 0) {
@@ -68,10 +70,72 @@ function isClosingFenceLine(info, open) {
68
70
  return false;
69
71
  return info.rest.trim() === "";
70
72
  }
73
+ /**
74
+ * GFM delimiter row: cells between pipes contain only colons, hyphens, and spaces; each cell has
75
+ * at least three hyphens (same rule remark-gfm uses). Used so we do not append raw HTML to table
76
+ * lines — trailing `<span>` breaks GFM table recognition in the Markdown parser.
77
+ */
78
+ function isGfmTableDelimiterRow(line) {
79
+ const t = trimEndSpacesTabs(line);
80
+ if (!t.includes("|"))
81
+ return false;
82
+ const cells = t
83
+ .split("|")
84
+ .map((s) => s.trim())
85
+ .filter((s) => s.length > 0);
86
+ if (cells.length === 0)
87
+ return false;
88
+ for (const cell of cells) {
89
+ if (!/^:?-{3,}:?$/.test(cell))
90
+ return false;
91
+ }
92
+ return true;
93
+ }
94
+ /**
95
+ * 0-based line indices that must not receive a trailing line-anchor span: they belong to a GFM
96
+ * table (header + delimiter + following rows until a blank line). Scans full `lines` so indices
97
+ * align with {@link injectCommentrayDocAnchors}; lines inside fenced code are harmless to mark
98
+ * because that pass never appends anchors there anyway.
99
+ */
100
+ function gfmTableLineIndicesWithoutAnchors(lines) {
101
+ const skip = new Set();
102
+ const n = lines.length;
103
+ for (let i = 0; i < n - 1; i++) {
104
+ const header = lines[i] ?? "";
105
+ const delim = lines[i + 1] ?? "";
106
+ if (header === "")
107
+ continue;
108
+ if (!trimEndSpacesTabs(header).includes("|"))
109
+ continue;
110
+ if (isSetextUnderlineLine(header) || isThematicBreakLine(header))
111
+ continue;
112
+ if (!isGfmTableDelimiterRow(delim))
113
+ continue;
114
+ skip.add(i);
115
+ skip.add(i + 1);
116
+ let j = i + 2;
117
+ while (j < n) {
118
+ const row = lines[j] ?? "";
119
+ if (row === "")
120
+ break;
121
+ if (isSetextUnderlineLine(row) || isThematicBreakLine(row))
122
+ break;
123
+ if (isGfmTableDelimiterRow(row))
124
+ break;
125
+ skip.add(j);
126
+ j++;
127
+ }
128
+ }
129
+ return skip;
130
+ }
71
131
  function lineAnchorHtml(mdLine0) {
72
132
  const mdLine = String(mdLine0);
73
133
  return `<span class="commentray-line-anchor" data-commentray-md-line="${mdLine}" id="commentray-md-line-${mdLine}" aria-hidden="true"></span>`;
74
134
  }
135
+ function sourceLineAnchorHtml(line0) {
136
+ const s = String(line0);
137
+ return `<span class="commentray-line-anchor commentray-line-anchor--source" data-source-md-line="${s}" id="code-md-line-${s}" aria-hidden="true"></span>`;
138
+ }
75
139
  function appendMdLineAnchorWhenAllowed(line, mdLine0) {
76
140
  if (isSetextUnderlineLine(line) || isThematicBreakLine(line))
77
141
  return line;
@@ -80,17 +144,59 @@ function appendMdLineAnchorWhenAllowed(line, mdLine0) {
80
144
  return "";
81
145
  return `${line}${lineAnchorHtml(mdLine0)}`;
82
146
  }
147
+ function appendSourceMdLineAnchorWhenAllowed(line, line0) {
148
+ if (isSetextUnderlineLine(line) || isThematicBreakLine(line))
149
+ return line;
150
+ if (line === "")
151
+ return "";
152
+ return `${line}${sourceLineAnchorHtml(line0)}`;
153
+ }
154
+ function pageBreakNextBlockMetaByLine(lines, byId) {
155
+ const out = new Map();
156
+ let nextMeta = null;
157
+ for (let i = lines.length - 1; i >= 0; i--) {
158
+ const line = lines[i] ?? "";
159
+ const blockMatch = BLOCK_MARKER_HTML_LINE.exec(line);
160
+ if (blockMatch?.[1]) {
161
+ const id = blockMatch[1];
162
+ const sourceStart = byId?.get(id)?.sourceStart;
163
+ nextMeta =
164
+ sourceStart !== undefined ? { commentrayLine: i, sourceStart } : { commentrayLine: i };
165
+ continue;
166
+ }
167
+ if (!PAGE_BREAK_MARKER_HTML_LINE.test(line) || nextMeta === null)
168
+ continue;
169
+ out.set(i, nextMeta);
170
+ }
171
+ return out;
172
+ }
173
+ function blockAnchorAttrs(link) {
174
+ if (link === undefined)
175
+ return "";
176
+ return ` data-source-start="${String(link.sourceStart)}" data-commentray-line="${String(link.commentrayLine)}"`;
177
+ }
178
+ function pageBreakNextAttrs(next) {
179
+ if (next === undefined)
180
+ return "";
181
+ const nextCommentrayAttr = ` data-next-commentray-line="${String(next.commentrayLine)}"`;
182
+ const nextSourceAttr = next.sourceStart !== undefined ? ` data-next-source-start="${String(next.sourceStart)}"` : "";
183
+ return `${nextCommentrayAttr}${nextSourceAttr}`;
184
+ }
83
185
  /**
84
186
  * Inserts per-line anchors for search / hash jumps and block separator anchors after each
85
187
  * `<!-- commentray:block … -->` line (optional index attrs).
86
188
  *
87
189
  * Anchors are appended to the line when safe. A **leading** `<span>` breaks CommonMark block
88
190
  * 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).
191
+ * trailing anchor either (would corrupt fence delimiters or appear inside code). **GFM pipe
192
+ * tables** must not get a trailing anchor: extra HTML after the row breaks `remark-gfm` table
193
+ * detection, so tables would render as plain text.
90
194
  */
91
195
  function injectCommentrayDocAnchors(markdown, links) {
92
196
  const byId = links ? new Map(links.map((l) => [l.id, l])) : undefined;
93
197
  const lines = markdown.split("\n");
198
+ const pageBreakNextByLine = pageBreakNextBlockMetaByLine(lines, byId);
199
+ const skipLineAnchor = gfmTableLineIndicesWithoutAnchors(lines);
94
200
  let fence = null;
95
201
  const out = [];
96
202
  for (let i = 0; i < lines.length; i++) {
@@ -113,10 +219,7 @@ function injectCommentrayDocAnchors(markdown, links) {
113
219
  const m = BLOCK_MARKER_HTML_LINE.exec(line);
114
220
  if (m?.[1]) {
115
221
  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
- : "";
222
+ const attrs = blockAnchorAttrs(byId?.get(id));
120
223
  /** One `push` with embedded `\n\n` merged poorly with `join("\\n")`; keep real blank lines around raw `<div>`. */
121
224
  out.push(`${line}${lineAnchorHtml(i)}`);
122
225
  out.push("");
@@ -124,10 +227,56 @@ function injectCommentrayDocAnchors(markdown, links) {
124
227
  out.push("");
125
228
  continue;
126
229
  }
230
+ if (PAGE_BREAK_MARKER_HTML_LINE.test(line)) {
231
+ const nextAttrs = pageBreakNextAttrs(pageBreakNextByLine.get(i));
232
+ out.push(`${line}${lineAnchorHtml(i)}`);
233
+ out.push("");
234
+ out.push(`<div class="commentray-page-break" data-commentray-page-break="true"${nextAttrs} aria-hidden="true"><div class="commentray-page-break__rule"></div></div>`);
235
+ out.push("");
236
+ continue;
237
+ }
238
+ if (skipLineAnchor.has(i)) {
239
+ out.push(line);
240
+ continue;
241
+ }
127
242
  out.push(appendMdLineAnchorWhenAllowed(line, i));
128
243
  }
129
244
  return out.join("\n");
130
245
  }
246
+ /**
247
+ * Adds stable source-line anchors (`id="code-line-N"`) to Markdown so rendered-source mode can
248
+ * preserve block-aware scroll sync and block ray geometry.
249
+ */
250
+ function injectSourceMarkdownAnchors(markdown) {
251
+ const lines = markdown.split("\n");
252
+ const skipLineAnchor = gfmTableLineIndicesWithoutAnchors(lines);
253
+ let fence = null;
254
+ const out = [];
255
+ for (let i = 0; i < lines.length; i++) {
256
+ const line = lines[i] ?? "";
257
+ const delim = parseFenceDelimiter(line);
258
+ if (fence) {
259
+ if (delim && isClosingFenceLine(delim, fence)) {
260
+ fence = null;
261
+ out.push(line);
262
+ continue;
263
+ }
264
+ out.push(line);
265
+ continue;
266
+ }
267
+ if (delim) {
268
+ fence = { ch: delim.ch, len: delim.runLen };
269
+ out.push(line);
270
+ continue;
271
+ }
272
+ if (skipLineAnchor.has(i)) {
273
+ out.push(line);
274
+ continue;
275
+ }
276
+ out.push(appendSourceMdLineAnchorWhenAllowed(line, i));
277
+ }
278
+ return out.join("\n");
279
+ }
131
280
  /** GitHub “mark” glyph (Octicons-style path), MIT-licensed silhouette. */
132
281
  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
282
  '<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 +308,25 @@ const TOOLBAR_ICON_FLIP_PANES_SVG = '<svg xmlns="http://www.w3.org/2000/svg" vie
159
308
  '<path d="M10.5 12H6l2.5-2.5M6 12l2.5 2.5"/>' +
160
309
  '<path d="M13.5 12H18l-2.5-2.5M18 12l-2.5 2.5"/>' +
161
310
  "</svg>";
311
+ /** Source markdown mode flip: rendered page <-> plain markdown rows. */
312
+ 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">' +
313
+ '<rect x="3" y="4" width="8" height="16" rx="1.5"/>' +
314
+ '<path d="M6 8h2M6 11h2M6 14h2"/>' +
315
+ '<rect x="13" y="4" width="8" height="16" rx="1.5"/>' +
316
+ '<path d="m15.5 12 2-2 2 2"/>' +
317
+ '<path d="m19.5 12-2 2-2-2"/>' +
318
+ "</svg>";
319
+ /** Link/share glyph for copying a permalink to the current documentation pair. */
320
+ 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">' +
321
+ '<path d="M10 14a5 5 0 0 0 7.07 0l2.83-2.83a5 5 0 0 0-7.07-7.07L10 6"/>' +
322
+ '<path d="M14 10a5 5 0 0 0-7.07 0L4.1 12.83a5 5 0 0 0 7.07 7.07L14 17"/>' +
323
+ "</svg>";
324
+ /** Help glyph for re-running the onboarding walkthrough. */
325
+ 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">' +
326
+ '<circle cx="12" cy="12" r="9"/>' +
327
+ '<path d="M9.1 9a3 3 0 1 1 4.92 2.3c-.8.6-1.52 1.08-1.52 2.2"/>' +
328
+ '<circle cx="12" cy="17" r="0.8" fill="currentColor" stroke="none"/>' +
329
+ "</svg>";
162
330
  function safeExternalHttpUrl(url) {
163
331
  const t = url?.trim();
164
332
  if (!t)
@@ -242,9 +410,13 @@ function renderToolbarDocHubHtml(opts) {
242
410
  : "";
243
411
  return { toolbarDocHubHtml, navRailDocumentedHtml };
244
412
  }
245
- function dualPanePanesInnerHtml(codeHtml, commentrayHtml) {
413
+ function dualPanePanesInnerHtml(codeHtml, commentrayHtml, sourceMarkdownRenderedHtml) {
414
+ const sourceRenderedPaneHtml = typeof sourceMarkdownRenderedHtml === "string" && sourceMarkdownRenderedHtml.trim().length > 0
415
+ ? ` <div class="source-pane source-pane--rendered-md" id="code-pane-markdown-body">${sourceMarkdownRenderedHtml}</div>\n`
416
+ : "";
246
417
  return (` <section class="pane--code" id="code-pane" aria-label="Source code">` +
247
- ` ${codeHtml}\n` +
418
+ ` <div class="source-pane source-pane--code" id="code-pane-code-body">${codeHtml}</div>\n` +
419
+ sourceRenderedPaneHtml +
248
420
  ` </section>\n` +
249
421
  ` <div class="gutter" id="gutter" role="separator" aria-orientation="vertical" aria-label="Resize panes"></div>\n` +
250
422
  ` <section class="pane--doc commentray" id="doc-pane" aria-label="Commentray">\n` +
@@ -253,6 +425,26 @@ function dualPanePanesInnerHtml(codeHtml, commentrayHtml) {
253
425
  ` </div>\n` +
254
426
  ` </section>\n`);
255
427
  }
428
+ function sourceMarkdownToggleControlsHtml(enabled) {
429
+ if (!enabled) {
430
+ return { sourceMarkdownToggleHtml: "", sourceMarkdownFlipScrollAffordanceHtml: "" };
431
+ }
432
+ const label = "Switch source pane between rendered markdown and markdown source";
433
+ const title = "Switch source pane between rendered markdown and markdown source";
434
+ 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>`;
435
+ 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>`;
436
+ return {
437
+ sourceMarkdownToggleHtml: btn,
438
+ sourceMarkdownFlipScrollAffordanceHtml: floating,
439
+ };
440
+ }
441
+ function isMarkdownLikeSource(opts) {
442
+ const lang = opts.language.trim().toLowerCase();
443
+ if (lang === "md" || lang === "markdown" || lang === "mdx")
444
+ return true;
445
+ const path = (opts.filePath ?? "").trim().toLowerCase();
446
+ return path.endsWith(".md") || path.endsWith(".mdx") || path.endsWith(".markdown");
447
+ }
256
448
  /** Plain-text Src/Doc labels above the panes; column widths track the resizable split via `--split-pct`. */
257
449
  function renderShellPairContextHtml(filePath, commentrayPath) {
258
450
  const fpRaw = (filePath ?? "").trim();
@@ -292,6 +484,19 @@ function loadCodeBrowserClientBundle() {
292
484
  }
293
485
  throw new Error("Missing code-browser-client.bundle.js. Run `npm run build -w @commentray/render` to bundle the browser client.");
294
486
  }
487
+ /** Intro-tour specific stylesheet; kept in a dedicated CSS file for easier tweaking. */
488
+ function loadCodeBrowserIntroStyles() {
489
+ const packagesDir = findMonorepoPackagesDir(monorepoLayoutStartDir(import.meta.url));
490
+ const renderDistDir = join(packagesDir, "render", "dist");
491
+ const inDist = join(renderDistDir, "code-browser-intro.css");
492
+ const fromSrc = join(packagesDir, "render", "src", "code-browser-intro.css");
493
+ for (const p of [inDist, fromSrc]) {
494
+ if (existsSync(p)) {
495
+ return readFileSync(p, "utf8");
496
+ }
497
+ }
498
+ throw new Error("Missing code-browser-intro.css. Ensure the render package includes intro tour styles.");
499
+ }
295
500
  /**
296
501
  * Compact theme control: primary click opens a menu (readme.io–style), secondary click cycles
297
502
  * system → light → dark. Paired with {@link ./code-browser-color-theme.ts} and the client bundle.
@@ -309,6 +514,11 @@ const TOOLBAR_COLOR_THEME_HTML = ` <div class="toolbar-theme">
309
514
  </div>
310
515
  </div>
311
516
  `;
517
+ 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>
518
+ `;
519
+ 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>
520
+ `;
521
+ const CODE_BROWSER_INTRO_STYLES = loadCodeBrowserIntroStyles();
312
522
  const CODE_BROWSER_STYLES = `
313
523
  :root {
314
524
  --cr-control-h: 32px;
@@ -654,6 +864,7 @@ const CODE_BROWSER_STYLES = `
654
864
  }
655
865
  .app__footer-attribution__version { font-weight: 600; }
656
866
  .toolbar label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
867
+ .toolbar label[hidden] { display: none !important; }
657
868
  .toolbar-wrap-lines {
658
869
  position: relative;
659
870
  margin: 0;
@@ -772,6 +983,80 @@ const CODE_BROWSER_STYLES = `
772
983
  outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
773
984
  outline-offset: 2px;
774
985
  }
986
+ .toolbar-source-render-toggle {
987
+ position: relative;
988
+ margin: 0;
989
+ min-height: var(--cr-control-h);
990
+ padding: 0 12px 0 10px;
991
+ border-radius: var(--cr-control-radius);
992
+ border: 1px solid color-mix(in oklab, CanvasText 16%, Canvas);
993
+ background: Canvas;
994
+ display: inline-flex;
995
+ flex-direction: row;
996
+ align-items: center;
997
+ justify-content: flex-start;
998
+ gap: 8px;
999
+ font-size: var(--cr-ui-fs);
1000
+ font-weight: 500;
1001
+ color: color-mix(in oklab, CanvasText 88%, Canvas);
1002
+ cursor: pointer;
1003
+ }
1004
+ .toolbar-source-render-toggle:hover {
1005
+ background: color-mix(in oklab, CanvasText 6%, Canvas);
1006
+ }
1007
+ .toolbar-source-render-toggle__box {
1008
+ flex: 0 0 auto;
1009
+ width: 16px;
1010
+ height: 16px;
1011
+ box-sizing: border-box;
1012
+ border: 1.5px solid color-mix(in oklab, CanvasText 38%, Canvas);
1013
+ border-radius: 3px;
1014
+ background: Canvas;
1015
+ display: inline-flex;
1016
+ align-items: center;
1017
+ justify-content: center;
1018
+ color: CanvasText;
1019
+ }
1020
+ .toolbar-source-render-toggle[aria-pressed="true"] .toolbar-source-render-toggle__box {
1021
+ border-color: color-mix(in oklab, CanvasText 52%, Canvas);
1022
+ background: color-mix(in oklab, CanvasText 6%, Canvas);
1023
+ }
1024
+ .toolbar-source-render-toggle__box::after {
1025
+ content: "";
1026
+ display: none;
1027
+ width: 4px;
1028
+ height: 9px;
1029
+ margin-top: -2px;
1030
+ border: solid currentColor;
1031
+ border-width: 0 2px 2px 0;
1032
+ transform: rotate(45deg);
1033
+ }
1034
+ .toolbar-source-render-toggle[aria-pressed="true"] .toolbar-source-render-toggle__box::after {
1035
+ display: block;
1036
+ }
1037
+ .toolbar-source-render-toggle__face {
1038
+ display: none;
1039
+ align-items: center;
1040
+ justify-content: center;
1041
+ min-height: var(--cr-control-h);
1042
+ min-width: var(--cr-control-h);
1043
+ color: color-mix(in oklab, CanvasText 82%, Canvas);
1044
+ }
1045
+ .toolbar-source-render-toggle__caption {
1046
+ white-space: nowrap;
1047
+ }
1048
+ .toolbar-source-render-toggle[aria-pressed="true"] {
1049
+ color: CanvasText;
1050
+ background: color-mix(in oklab, CanvasText 10%, Canvas);
1051
+ }
1052
+ .toolbar-source-render-toggle:focus-visible {
1053
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
1054
+ outline-offset: 2px;
1055
+ }
1056
+ .toolbar-icon-btn--source-markdown {
1057
+ display: inline-flex;
1058
+ }
1059
+ ${CODE_BROWSER_INTRO_STYLES}
775
1060
  .toolbar label input:focus-visible {
776
1061
  outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
777
1062
  outline-offset: 2px;
@@ -810,6 +1095,16 @@ const CODE_BROWSER_STYLES = `
810
1095
  outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
811
1096
  outline-offset: 2px;
812
1097
  }
1098
+ .toolbar-theme__trigger svg {
1099
+ width: var(--cr-icon-inner);
1100
+ height: var(--cr-icon-inner);
1101
+ display: block;
1102
+ flex: 0 0 auto;
1103
+ }
1104
+ .toolbar-share-link-btn[data-copied="true"] {
1105
+ background: color-mix(in oklab, #2ea043 24%, Canvas);
1106
+ border-color: color-mix(in oklab, #2ea043 48%, CanvasText);
1107
+ }
813
1108
  .toolbar-theme__trigger .toolbar-theme__icon {
814
1109
  display: none;
815
1110
  flex: 0 0 auto;
@@ -917,6 +1212,14 @@ const CODE_BROWSER_STYLES = `
917
1212
  .documented-files-tree .tree-file-link:hover {
918
1213
  opacity: 0.92;
919
1214
  }
1215
+ .documented-files-tree .tree-file-link.tree-file-link--current {
1216
+ font-weight: 600;
1217
+ text-decoration-thickness: 2px;
1218
+ border-radius: 3px;
1219
+ padding: 1px 3px;
1220
+ margin: -1px -3px;
1221
+ background: color-mix(in oklab, CanvasText 10%, Canvas);
1222
+ }
920
1223
  .toolbar button {
921
1224
  font: inherit;
922
1225
  font-size: var(--cr-ui-fs);
@@ -1044,6 +1347,31 @@ const CODE_BROWSER_STYLES = `
1044
1347
  --code-line-font-size: 13px;
1045
1348
  --code-line-height: 1.5;
1046
1349
  }
1350
+ .source-pane {
1351
+ min-width: 0;
1352
+ }
1353
+ .source-pane--rendered-md {
1354
+ font-size: 15px;
1355
+ line-height: 1.45;
1356
+ }
1357
+ .source-pane--rendered-md img {
1358
+ max-width: 100%;
1359
+ height: auto;
1360
+ }
1361
+ .source-pane--rendered-md .commentray-mermaid {
1362
+ overflow-x: auto;
1363
+ max-width: 100%;
1364
+ }
1365
+ .source-pane--rendered-md .commentray-line-anchor--source {
1366
+ display: inline;
1367
+ vertical-align: baseline;
1368
+ }
1369
+ #shell[data-source-pane-mode="rendered-markdown"] .source-pane--code {
1370
+ display: none;
1371
+ }
1372
+ #shell[data-source-pane-mode="source"] .source-pane--rendered-md {
1373
+ display: none;
1374
+ }
1047
1375
  .pane--code .code-line-stack { --code-ln-min-ch: 3; }
1048
1376
  .pane--code .code-line {
1049
1377
  display: grid;
@@ -1144,11 +1472,95 @@ const CODE_BROWSER_STYLES = `
1144
1472
  .doc-pane-body {
1145
1473
  flex: 1 1 auto; min-height: 0; overflow: auto;
1146
1474
  }
1147
- /** Wide GFM tables: intrinsic width so the doc pane scrolls sideways instead of squeezing columns. */
1148
- .pane--doc .doc-pane-body :where(table) {
1475
+ /* Inline backtick code chips (GitHub-like): prose context only, never fenced pre/code blocks. */
1476
+ .pane--doc .doc-pane-body :where(p, li, blockquote, td, th, h1, h2, h3, h4, h5, h6) > code,
1477
+ .shell--stretch-rows .stretch-preamble :where(p, li, blockquote, td, th, h1, h2, h3, h4, h5, h6) > code,
1478
+ .block-stretch td.stretch-doc .stretch-doc-inner :where(p, li, blockquote, td, th, h1, h2, h3, h4, h5, h6) > code {
1479
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
1480
+ font-size: 0.92em;
1481
+ padding: 0.12em 0.36em;
1482
+ border-radius: 6px;
1483
+ border: 1px solid color-mix(in oklab, CanvasText 12%, Canvas);
1484
+ background: color-mix(in oklab, CanvasText 8%, Canvas);
1485
+ color: inherit;
1486
+ }
1487
+ @media (prefers-color-scheme: dark) {
1488
+ :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,
1489
+ :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,
1490
+ :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 {
1491
+ border-color: color-mix(in oklab, CanvasText 26%, Canvas);
1492
+ background: color-mix(in oklab, CanvasText 16%, Canvas);
1493
+ }
1494
+ }
1495
+ :root[data-commentray-theme="dark"] .pane--doc .doc-pane-body :where(p, li, blockquote, td, th, h1, h2, h3, h4, h5, h6) > code,
1496
+ :root[data-commentray-theme="dark"] .shell--stretch-rows .stretch-preamble :where(p, li, blockquote, td, th, h1, h2, h3, h4, h5, h6) > code,
1497
+ :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 {
1498
+ border-color: color-mix(in oklab, CanvasText 26%, Canvas);
1499
+ background: color-mix(in oklab, CanvasText 16%, Canvas);
1500
+ }
1501
+ /**
1502
+ * GFM tables in rendered Markdown (doc pane, stretch preamble, per-block doc cells).
1503
+ * Intrinsic width so the pane scrolls sideways instead of squeezing columns; borders
1504
+ * and padding match familiar GitHub-style readability.
1505
+ */
1506
+ .pane--doc .doc-pane-body :where(table),
1507
+ .shell--stretch-rows .stretch-preamble :where(table),
1508
+ .block-stretch td.stretch-doc .stretch-doc-inner :where(table) {
1149
1509
  width: max-content;
1150
1510
  max-width: none;
1151
1511
  border-collapse: collapse;
1512
+ margin: 0.85em 0;
1513
+ font-size: inherit;
1514
+ line-height: inherit;
1515
+ }
1516
+ .pane--doc .doc-pane-body :where(th, td),
1517
+ .shell--stretch-rows .stretch-preamble :where(th, td),
1518
+ .block-stretch td.stretch-doc .stretch-doc-inner :where(th, td) {
1519
+ border: 1px solid color-mix(in oklab, CanvasText 22%, Canvas);
1520
+ padding: 8px 12px;
1521
+ vertical-align: top;
1522
+ }
1523
+ .pane--doc .doc-pane-body :where(thead th),
1524
+ .shell--stretch-rows .stretch-preamble :where(thead th),
1525
+ .block-stretch td.stretch-doc .stretch-doc-inner :where(thead th) {
1526
+ font-weight: 600;
1527
+ background: color-mix(in oklab, CanvasText 7%, Canvas);
1528
+ }
1529
+ .pane--doc .doc-pane-body tbody tr:nth-child(even) :where(td),
1530
+ .shell--stretch-rows .stretch-preamble tbody tr:nth-child(even) :where(td),
1531
+ .block-stretch td.stretch-doc .stretch-doc-inner tbody tr:nth-child(even) :where(td) {
1532
+ background: color-mix(in oklab, CanvasText 3.5%, Canvas);
1533
+ }
1534
+ .pane--doc .doc-pane-body :where(ul.contains-task-list),
1535
+ .shell--stretch-rows .stretch-preamble :where(ul.contains-task-list),
1536
+ .block-stretch td.stretch-doc .stretch-doc-inner :where(ul.contains-task-list) {
1537
+ list-style: none;
1538
+ padding-inline-start: 1.2em;
1539
+ }
1540
+ .pane--doc .doc-pane-body :where(li.task-list-item),
1541
+ .shell--stretch-rows .stretch-preamble :where(li.task-list-item),
1542
+ .block-stretch td.stretch-doc .stretch-doc-inner :where(li.task-list-item) {
1543
+ position: relative;
1544
+ }
1545
+ .pane--doc .doc-pane-body :where(li.task-list-item input[type="checkbox"]),
1546
+ .shell--stretch-rows .stretch-preamble :where(li.task-list-item input[type="checkbox"]),
1547
+ .block-stretch td.stretch-doc .stretch-doc-inner :where(li.task-list-item input[type="checkbox"]) {
1548
+ position: absolute;
1549
+ margin-inline-start: -1.35em;
1550
+ margin-top: 0.2em;
1551
+ }
1552
+ .pane--doc .doc-pane-body :where(del),
1553
+ .shell--stretch-rows .stretch-preamble :where(del),
1554
+ .block-stretch td.stretch-doc .stretch-doc-inner :where(del) {
1555
+ opacity: 0.82;
1556
+ }
1557
+ .pane--doc .doc-pane-body :where(section.footnotes),
1558
+ .shell--stretch-rows .stretch-preamble :where(section.footnotes),
1559
+ .block-stretch td.stretch-doc .stretch-doc-inner :where(section.footnotes) {
1560
+ margin-top: 1.5em;
1561
+ padding-top: 0.75em;
1562
+ border-top: 1px solid color-mix(in oklab, CanvasText 18%, Canvas);
1563
+ font-size: 0.92em;
1152
1564
  }
1153
1565
  .pane--doc .doc-pane-body .commentray-mermaid {
1154
1566
  overflow-x: auto;
@@ -1166,6 +1578,23 @@ const CODE_BROWSER_STYLES = `
1166
1578
  overflow-wrap: normal;
1167
1579
  word-break: normal;
1168
1580
  }
1581
+ #doc-pane-body .commentray-page-break {
1582
+ position: relative;
1583
+ min-height: var(--commentray-page-break-min-height, clamp(260px, 56vh, 620px));
1584
+ margin: 24px 0;
1585
+ display: flex;
1586
+ align-items: center;
1587
+ justify-content: center;
1588
+ pointer-events: none;
1589
+ }
1590
+ #doc-pane-body .commentray-page-break__rule {
1591
+ width: 100%;
1592
+ border-top: 1px dashed var(--border);
1593
+ opacity: 0.38;
1594
+ }
1595
+ #shell[data-page-breaks-enabled="false"] .commentray-page-break {
1596
+ display: none;
1597
+ }
1169
1598
  .toolbar-angle-picker {
1170
1599
  display: inline-flex;
1171
1600
  align-items: center;
@@ -1385,6 +1814,19 @@ const CODE_BROWSER_STYLES = `
1385
1814
  0 1px 2px color-mix(in oklab, CanvasText 12%, transparent),
1386
1815
  0 4px 14px color-mix(in oklab, CanvasText 18%, transparent);
1387
1816
  }
1817
+ .toolbar-icon-btn--source-markdown-scroll-narrow {
1818
+ display: none;
1819
+ }
1820
+ #source-markdown-pane-flip-scroll.toolbar-icon-btn--source-markdown-scroll-narrow.is-visible {
1821
+ display: inline-flex;
1822
+ position: fixed;
1823
+ top: calc(10px + env(safe-area-inset-top, 0px));
1824
+ left: calc(12px + env(safe-area-inset-left, 0px));
1825
+ z-index: 50;
1826
+ box-shadow:
1827
+ 0 1px 2px color-mix(in oklab, CanvasText 12%, transparent),
1828
+ 0 4px 14px color-mix(in oklab, CanvasText 18%, transparent);
1829
+ }
1388
1830
  /** Region connector lines are not needed on the narrow single-pane layout (gutter is hidden). */
1389
1831
  .shell:not(.shell--stretch-rows) .gutter .gutter__rays {
1390
1832
  opacity: 0 !important;
@@ -1450,6 +1892,47 @@ const CODE_BROWSER_STYLES = `
1450
1892
  color: CanvasText;
1451
1893
  text-shadow: 0 0 2px Canvas, 0 0 3px Canvas;
1452
1894
  }
1895
+ .toolbar-source-render-toggle {
1896
+ min-width: var(--cr-control-h);
1897
+ width: var(--cr-control-h);
1898
+ height: var(--cr-control-h);
1899
+ padding: 0;
1900
+ justify-content: center;
1901
+ gap: 0;
1902
+ }
1903
+ .toolbar-source-render-toggle__caption {
1904
+ position: absolute;
1905
+ width: 1px;
1906
+ height: 1px;
1907
+ padding: 0;
1908
+ margin: -1px;
1909
+ overflow: hidden;
1910
+ clip: rect(0, 0, 0, 0);
1911
+ white-space: nowrap;
1912
+ border: 0;
1913
+ }
1914
+ .toolbar-source-render-toggle__box {
1915
+ display: none;
1916
+ }
1917
+ .toolbar-source-render-toggle__face {
1918
+ display: inline-flex;
1919
+ position: relative;
1920
+ width: 100%;
1921
+ height: 100%;
1922
+ min-height: var(--cr-control-h);
1923
+ min-width: var(--cr-control-h);
1924
+ }
1925
+ .toolbar-source-render-toggle[aria-pressed="true"] .toolbar-source-render-toggle__face::after {
1926
+ content: "✓";
1927
+ position: absolute;
1928
+ right: 1px;
1929
+ bottom: 0;
1930
+ font-size: 11px;
1931
+ line-height: 1;
1932
+ font-weight: 800;
1933
+ color: CanvasText;
1934
+ text-shadow: 0 0 2px Canvas, 0 0 3px Canvas;
1935
+ }
1453
1936
  .shell:not(.shell--stretch-rows)[data-dual-mobile-pane="code"] .pane--doc,
1454
1937
  .shell:not(.shell--stretch-rows)[data-dual-mobile-pane="code"] .gutter {
1455
1938
  display: none !important;
@@ -1512,11 +1995,6 @@ const CODE_BROWSER_STYLES = `
1512
1995
  max-width: 100%;
1513
1996
  }
1514
1997
  .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
1998
  .block-stretch {
1521
1999
  width: 100%;
1522
2000
  border-collapse: collapse;
@@ -1541,11 +2019,6 @@ const CODE_BROWSER_STYLES = `
1541
2019
  overflow-x: auto;
1542
2020
  }
1543
2021
  .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
2022
  .block-stretch td.stretch-doc .stretch-doc-inner .commentray-mermaid {
1550
2023
  overflow-x: auto;
1551
2024
  max-width: 100%;
@@ -1660,16 +2133,20 @@ ${CODE_BROWSER_STYLES}
1660
2133
  <span class="toolbar-wrap-lines__caption">Wrap lines</span>
1661
2134
  </label>
1662
2135
  ${dualFlipControlHtml}
2136
+ ${p.sourceMarkdownToggleHtml}
1663
2137
  ${p.toolbarDocHubHtml}
1664
2138
  ${p.relatedNavHtml}
1665
2139
  </div>
1666
2140
  <div class="toolbar__primary-trail">
1667
2141
  ${p.toolbarEndHtml}
2142
+ ${TOOLBAR_SHARE_LINK_HTML}
2143
+ ${TOOLBAR_HELP_TOUR_HTML}
1668
2144
  ${TOOLBAR_COLOR_THEME_HTML}
1669
2145
  </div>
1670
2146
  </div>
1671
2147
  </header>
1672
2148
  ${dualFlipScrollAffordanceHtml}
2149
+ ${p.sourceMarkdownFlipScrollAffordanceHtml}
1673
2150
  <header class="app__chrome" role="region" aria-label="Search">
1674
2151
  <div class="chrome__search-row">
1675
2152
  <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 +2156,7 @@ ${TOOLBAR_COLOR_THEME_HTML}
1679
2156
  <div class="search-results" id="search-results" hidden aria-live="polite"></div>
1680
2157
  </header>
1681
2158
  <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}>
2159
+ <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
2160
  ${p.shellInner}
1684
2161
  </div>
1685
2162
  </main>
@@ -1693,6 +2170,44 @@ ${loadCodeBrowserClientBundle()}
1693
2170
  </body>
1694
2171
  </html>`;
1695
2172
  }
2173
+ function firstNonEmpty(values) {
2174
+ return values.find((v) => v.trim().length > 0);
2175
+ }
2176
+ function resolveMultiAngleDefaultSelection(args) {
2177
+ const { multi, defaultId, opts, builtAngles } = args;
2178
+ let defaultMarkdown = opts.commentrayMarkdown;
2179
+ let defaultScrollB64 = "";
2180
+ let defaultPathSearch = (opts.commentrayPathForSearch ?? "").trim();
2181
+ let defaultGh = opts.commentrayOnGithubUrl;
2182
+ let defaultStaticBrowse = (opts.commentrayStaticBrowseUrl ?? "").trim();
2183
+ let defaultPaneHtml = "";
2184
+ for (const b of builtAngles) {
2185
+ if (b.spec.id !== defaultId)
2186
+ continue;
2187
+ defaultMarkdown = b.spec.markdown;
2188
+ defaultScrollB64 = b.scrollB64;
2189
+ defaultPathSearch = b.spec.commentrayPathRel.trim();
2190
+ defaultGh = b.spec.commentrayOnGithubUrl;
2191
+ defaultStaticBrowse = (b.spec.staticBrowseUrl ?? "").trim();
2192
+ defaultPaneHtml = b.commentrayHtml;
2193
+ break;
2194
+ }
2195
+ if (defaultStaticBrowse.length === 0) {
2196
+ defaultStaticBrowse =
2197
+ firstNonEmpty(multi.angles.map((a) => (a.staticBrowseUrl ?? "").trim())) ?? "";
2198
+ }
2199
+ if ((defaultGh ?? "").trim().length === 0) {
2200
+ defaultGh = firstNonEmpty(multi.angles.map((a) => (a.commentrayOnGithubUrl ?? "").trim()));
2201
+ }
2202
+ return {
2203
+ defaultMarkdown,
2204
+ defaultScrollB64,
2205
+ defaultPathSearch,
2206
+ defaultGh,
2207
+ defaultStaticBrowse,
2208
+ defaultPaneHtml,
2209
+ };
2210
+ }
1696
2211
  async function multiAngleJsonRowAndDocHtml(opts, spec) {
1697
2212
  const rows = spec.blockStretchRows;
1698
2213
  const links = rows !== undefined
@@ -1723,29 +2238,23 @@ async function buildMultiAngleDualPaneShell(opts, multi) {
1723
2238
  ? multi.defaultAngleId
1724
2239
  : (multi.angles[0]?.id ?? "main");
1725
2240
  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);
2241
+ const builtAngles = [];
2242
+ const sourceMarkdownEnabled = isMarkdownLikeSource(opts);
2243
+ const sourceMdForPane = sourceMarkdownEnabled ? injectSourceMarkdownAnchors(opts.code) : "";
2244
+ const [codeHtml, sourceMarkdownPaneHtml] = await Promise.all([
2245
+ renderHighlightedCodeLineRows(opts.code, opts.language),
2246
+ sourceMarkdownEnabled
2247
+ ? renderMarkdownToHtml(sourceMdForPane, {
2248
+ commentrayOutputUrls: opts.commentrayOutputUrls,
2249
+ })
2250
+ : Promise.resolve(""),
2251
+ ]);
1733
2252
  for (const spec of multi.angles) {
1734
2253
  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
- }
2254
+ builtAngles.push({ spec, commentrayHtml, scrollB64 });
1747
2255
  jsonAngles.push(jsonRow);
1748
2256
  }
2257
+ const { defaultMarkdown, defaultScrollB64, defaultPathSearch, defaultGh, defaultStaticBrowse, defaultPaneHtml, } = resolveMultiAngleDefaultSelection({ multi, defaultId, opts, builtAngles });
1749
2258
  const selOpts = multi.angles
1750
2259
  .map((a) => {
1751
2260
  const lab = escapeHtml(a.title?.trim() || a.id);
@@ -1754,7 +2263,7 @@ async function buildMultiAngleDualPaneShell(opts, multi) {
1754
2263
  .join("");
1755
2264
  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
2265
  const pairHtml = renderShellPairContextHtml(opts.filePath, defaultPathSearch);
1757
- const shellInner = wrapDualShellInner(pairHtml, dualPanePanesInnerHtml(codeHtml, defaultPaneHtml));
2266
+ const shellInner = wrapDualShellInner(pairHtml, dualPanePanesInnerHtml(codeHtml, defaultPaneHtml, sourceMarkdownPaneHtml));
1758
2267
  const payloadObj = { defaultAngleId: defaultId, angles: jsonAngles };
1759
2268
  const multiAnglePayloadB64 = Buffer.from(JSON.stringify(payloadObj), "utf8").toString("base64");
1760
2269
  return {
@@ -1768,6 +2277,8 @@ async function buildMultiAngleDualPaneShell(opts, multi) {
1768
2277
  },
1769
2278
  angleSelectHtml,
1770
2279
  multiAnglePayloadB64,
2280
+ sourceMarkdownToggleEnabled: sourceMarkdownEnabled,
2281
+ sourcePaneDefaultMode: sourceMarkdownEnabled ? "rendered-markdown" : "source",
1771
2282
  };
1772
2283
  }
1773
2284
  async function buildCodeBrowserShell(opts, layoutPref) {
@@ -1785,6 +2296,8 @@ async function buildCodeBrowserShell(opts, layoutPref) {
1785
2296
  scrollBlockLinksB64: ms.scrollBlockLinksB64,
1786
2297
  angleSelectHtml: built.angleSelectHtml,
1787
2298
  multiAnglePayloadB64: built.multiAnglePayloadB64,
2299
+ sourceMarkdownToggleEnabled: built.sourceMarkdownToggleEnabled,
2300
+ sourcePaneDefaultMode: built.sourcePaneDefaultMode,
1788
2301
  multiShell: ms,
1789
2302
  };
1790
2303
  }
@@ -1811,14 +2324,30 @@ async function buildCodeBrowserShell(opts, layoutPref) {
1811
2324
  if (links.length > 0) {
1812
2325
  scrollBlockLinksB64 = Buffer.from(JSON.stringify(links), "utf8").toString("base64");
1813
2326
  }
1814
- const [codeHtml, commentrayHtml] = await Promise.all([
2327
+ const sourceMarkdownEnabled = isMarkdownLikeSource(opts);
2328
+ const sourceMdForPane = sourceMarkdownEnabled ? injectSourceMarkdownAnchors(opts.code) : "";
2329
+ const [codeHtml, commentrayHtml, sourceMarkdownPaneHtml] = await Promise.all([
1815
2330
  renderHighlightedCodeLineRows(opts.code, opts.language),
1816
2331
  renderMarkdownToHtml(mdForDoc, {
1817
2332
  commentrayOutputUrls: opts.commentrayOutputUrls,
1818
2333
  }),
2334
+ sourceMarkdownEnabled
2335
+ ? renderMarkdownToHtml(sourceMdForPane, {
2336
+ commentrayOutputUrls: opts.commentrayOutputUrls,
2337
+ })
2338
+ : Promise.resolve(""),
1819
2339
  ]);
1820
2340
  const pairHtml = renderShellPairContextHtml(opts.filePath, (opts.commentrayPathForSearch ?? "").trim());
1821
- shellInner = wrapDualShellInner(pairHtml, dualPanePanesInnerHtml(codeHtml, commentrayHtml));
2341
+ shellInner = wrapDualShellInner(pairHtml, dualPanePanesInnerHtml(codeHtml, commentrayHtml, sourceMarkdownPaneHtml));
2342
+ return {
2343
+ layout,
2344
+ shellInner,
2345
+ scrollBlockLinksB64,
2346
+ angleSelectHtml: "",
2347
+ multiAnglePayloadB64: "",
2348
+ sourceMarkdownToggleEnabled: sourceMarkdownEnabled,
2349
+ sourcePaneDefaultMode: sourceMarkdownEnabled ? "rendered-markdown" : "source",
2350
+ };
1822
2351
  }
1823
2352
  return {
1824
2353
  layout,
@@ -1826,6 +2355,8 @@ async function buildCodeBrowserShell(opts, layoutPref) {
1826
2355
  scrollBlockLinksB64,
1827
2356
  angleSelectHtml: "",
1828
2357
  multiAnglePayloadB64: "",
2358
+ sourceMarkdownToggleEnabled: false,
2359
+ sourcePaneDefaultMode: "source",
1829
2360
  };
1830
2361
  }
1831
2362
  function searchChromeFromOptions(opts, commentrayPathOverride) {
@@ -1856,6 +2387,23 @@ function toolbarCommentrayGithubFromShell(shell, opts) {
1856
2387
  function rawMdB64FromShell(shell, opts) {
1857
2388
  return (shell.multiShell?.rawMdB64 ?? Buffer.from(opts.commentrayMarkdown, "utf8").toString("base64"));
1858
2389
  }
2390
+ function currentPairCommentrayPathRel(shell, opts) {
2391
+ return (shell.multiShell?.commentrayPathForSearch ??
2392
+ opts.commentrayPathForSearch ??
2393
+ opts.blockStretchRows?.commentrayPathRel ??
2394
+ "").trim();
2395
+ }
2396
+ /**
2397
+ * Repo-relative source + companion Markdown paths for matching the current page to nav pairs
2398
+ * (see `code-browser-client.ts` documented-files tree).
2399
+ */
2400
+ function shellPairIdentityDataAttrs(shell, opts) {
2401
+ const src = normPosixPath(opts.filePath ?? "");
2402
+ const cr = normPosixPath(currentPairCommentrayPathRel(shell, opts));
2403
+ if (src.length === 0 || cr.length === 0)
2404
+ return "";
2405
+ return ` data-commentray-pair-source-path="${escapeHtml(src)}" data-commentray-pair-commentray-path="${escapeHtml(cr)}"`;
2406
+ }
1859
2407
  /** Canonical doc target for static validation: same-site `./browse/…` when present, else GitHub blob. */
1860
2408
  function shellPairDocDataAttr(shell, opts) {
1861
2409
  if (shell.layout !== "dual")
@@ -1915,11 +2463,15 @@ export async function renderCodeBrowserHtml(opts) {
1915
2463
  const shellDocumentedPairsAttr = shellDocumentedPairsAttrFromOptions(opts);
1916
2464
  const shellSearchAttrs = shellSearchAttrsWithNavJson(shellSearchAttrsBase, opts.documentedNavJsonUrl);
1917
2465
  const pairDocDataAttr = shellPairDocDataAttr(shell, opts);
2466
+ const pairIdentityDataAttrs = shellPairIdentityDataAttrs(shell, opts);
2467
+ const sourceMarkdownToggles = sourceMarkdownToggleControlsHtml(shell.sourceMarkdownToggleEnabled);
2468
+ const sourcePaneModeAttr = ` data-source-pane-mode="${shell.sourcePaneDefaultMode}"`;
1918
2469
  return buildCodeBrowserPageHtml({
1919
2470
  title,
1920
2471
  metaDescriptionHtml,
1921
2472
  generatorMetaHtml,
1922
2473
  toolbarSiteHubHtml,
2474
+ shellPairIdentityDataAttrs: pairIdentityDataAttrs,
1923
2475
  shellPairDocDataAttr: pairDocDataAttr,
1924
2476
  angleSelectHtml: shell.angleSelectHtml,
1925
2477
  toolbarDocHubHtml,
@@ -1939,6 +2491,9 @@ export async function renderCodeBrowserHtml(opts) {
1939
2491
  searchPlaceholder,
1940
2492
  shellSearchAttrs,
1941
2493
  multiAngleScriptBlock: shell.multiAnglePayloadB64,
2494
+ sourceMarkdownToggleHtml: sourceMarkdownToggles.sourceMarkdownToggleHtml,
2495
+ sourceMarkdownFlipScrollAffordanceHtml: sourceMarkdownToggles.sourceMarkdownFlipScrollAffordanceHtml,
2496
+ sourcePaneModeAttr,
1942
2497
  });
1943
2498
  }
1944
2499
  //# sourceMappingURL=code-browser.js.map