@commentray/render 0.0.2 → 0.0.5

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 (59) hide show
  1. package/dist/block-stretch-layout.d.ts +30 -0
  2. package/dist/block-stretch-layout.d.ts.map +1 -0
  3. package/dist/block-stretch-layout.js +104 -0
  4. package/dist/block-stretch-layout.js.map +1 -0
  5. package/dist/build-commentray-nav-search.d.ts +62 -0
  6. package/dist/build-commentray-nav-search.d.ts.map +1 -0
  7. package/dist/build-commentray-nav-search.js +98 -0
  8. package/dist/build-commentray-nav-search.js.map +1 -0
  9. package/dist/build-stamp.d.ts +6 -0
  10. package/dist/build-stamp.d.ts.map +1 -0
  11. package/dist/build-stamp.js +23 -0
  12. package/dist/build-stamp.js.map +1 -0
  13. package/dist/code-browser-client.bundle.js +12 -5
  14. package/dist/code-browser-client.js +825 -97
  15. package/dist/code-browser-client.js.map +1 -1
  16. package/dist/code-browser-embedded-payload.d.ts +10 -0
  17. package/dist/code-browser-embedded-payload.d.ts.map +1 -0
  18. package/dist/code-browser-embedded-payload.js +18 -0
  19. package/dist/code-browser-embedded-payload.js.map +1 -0
  20. package/dist/code-browser-encoding.d.ts +9 -0
  21. package/dist/code-browser-encoding.d.ts.map +1 -0
  22. package/dist/code-browser-encoding.js +24 -0
  23. package/dist/code-browser-encoding.js.map +1 -0
  24. package/dist/code-browser-scroll-sync.d.ts +6 -0
  25. package/dist/code-browser-scroll-sync.d.ts.map +1 -1
  26. package/dist/code-browser-scroll-sync.js +1 -0
  27. package/dist/code-browser-scroll-sync.js.map +1 -1
  28. package/dist/code-browser-search.d.ts +5 -0
  29. package/dist/code-browser-search.d.ts.map +1 -1
  30. package/dist/code-browser-search.js +28 -0
  31. package/dist/code-browser-search.js.map +1 -1
  32. package/dist/code-browser-web-storage.d.ts +7 -0
  33. package/dist/code-browser-web-storage.d.ts.map +1 -0
  34. package/dist/code-browser-web-storage.js +21 -0
  35. package/dist/code-browser-web-storage.js.map +1 -0
  36. package/dist/code-browser.d.ts +76 -1
  37. package/dist/code-browser.d.ts.map +1 -1
  38. package/dist/code-browser.js +809 -111
  39. package/dist/code-browser.js.map +1 -1
  40. package/dist/highlighted-code-lines.d.ts +19 -0
  41. package/dist/highlighted-code-lines.d.ts.map +1 -0
  42. package/dist/highlighted-code-lines.js +61 -0
  43. package/dist/highlighted-code-lines.js.map +1 -0
  44. package/dist/index.d.ts +3 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +1 -0
  47. package/dist/index.js.map +1 -1
  48. package/dist/markdown-pipeline.d.ts.map +1 -1
  49. package/dist/markdown-pipeline.js +13 -1
  50. package/dist/markdown-pipeline.js.map +1 -1
  51. package/dist/mermaid-runtime-html.d.ts +7 -0
  52. package/dist/mermaid-runtime-html.d.ts.map +1 -0
  53. package/dist/mermaid-runtime-html.js +26 -0
  54. package/dist/mermaid-runtime-html.js.map +1 -0
  55. package/dist/side-by-side.d.ts +2 -0
  56. package/dist/side-by-side.d.ts.map +1 -1
  57. package/dist/side-by-side.js +7 -7
  58. package/dist/side-by-side.js.map +1 -1
  59. package/package.json +6 -4
@@ -1,57 +1,111 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { MARKER_ID_BODY, buildBlockScrollLinks, } from "@commentray/core";
5
+ import { tryBuildBlockStretchTableHtml } from "./block-stretch-layout.js";
6
+ import { formatCommentrayBuiltAtLocal } from "./build-stamp.js";
4
7
  import { escapeHtml } from "./html-utils.js";
5
- import { renderFencedCode, renderMarkdownToHtml, } from "./markdown-pipeline.js";
8
+ import { renderHighlightedCodeLineRows } from "./highlighted-code-lines.js";
9
+ import { mermaidRuntimeScriptHtml } from "./mermaid-runtime-html.js";
10
+ import { renderMarkdownToHtml } from "./markdown-pipeline.js";
11
+ import { commentrayRenderVersion } from "./package-version.js";
6
12
  function renderGeneratorMetaHtml(label) {
7
13
  const t = label?.trim();
8
14
  if (!t)
9
15
  return "";
10
16
  return `<meta name="generator" content="${escapeHtml(t)}" />\n `;
11
17
  }
12
- function extractPreCodeInner(html) {
13
- const m = /<pre(?:\s[^>]*)?>\s*<code(?:\s[^>]*)?>([\s\S]*?)<\/code>\s*<\/pre>/i.exec(html.trim());
14
- return m ? m[1] : escapeHtml(html);
18
+ /** Single capture: marker id (avoid a wrapping group around the whole comment — that shifted indices). */
19
+ const BLOCK_MARKER_HTML_LINE = new RegExp(`^<!--\\s*commentray:block\\s+id=(${MARKER_ID_BODY})\\s*-->$`, "i");
20
+ function trimEndSpacesTabs(s) {
21
+ return s.replace(/[ \t]+$/, "");
15
22
  }
16
- /** One highlighted row per source line so in-page search can scroll to a line. */
17
- async function renderCodeLineBlocks(code, language) {
18
- const lines = code.split("\n");
19
- const langAttr = escapeHtml(language);
20
- const parts = [];
21
- for (let i = 0; i < lines.length; i++) {
22
- const line = lines[i] === "" ? " " : lines[i];
23
- const fence = "```" + language + "\n" + line + "\n```\n";
24
- const block = await renderFencedCode(fence);
25
- const inner = extractPreCodeInner(block);
26
- const num = i + 1;
27
- parts.push(`<div class="code-line" id="code-line-${i}" data-line="${i}">` +
28
- `<span class="ln" aria-hidden="true">${num}</span>` +
29
- `<pre><code class="hljs language-${langAttr}">${inner}</code></pre>` +
30
- `</div>`);
31
- }
32
- return parts.join("\n");
23
+ function isSetextUnderlineLine(line) {
24
+ const t = trimEndSpacesTabs(line);
25
+ return /^\s{0,3}=+\s*$/.test(t) || /^\s{0,3}-+\s*$/.test(t);
26
+ }
27
+ function isThematicBreakLine(line) {
28
+ const t = trimEndSpacesTabs(line);
29
+ return (/^\s{0,3}(?:\*[ \t]*){3,}\s*$/.test(t) ||
30
+ /^\s{0,3}(?:-[ \t]*){3,}\s*$/.test(t) ||
31
+ /^\s{0,3}(?:_[ \t]*){3,}\s*$/.test(t));
33
32
  }
34
- /** Split a repo-relative path into its directory prefix (with trailing slash) and basename. */
35
- function splitFilePath(p) {
36
- const normalized = p.replaceAll("\\", "/").replace(/^\/+/, "");
37
- const idx = normalized.lastIndexOf("/");
38
- if (idx < 0)
39
- return { dir: "", base: normalized };
40
- return { dir: normalized.slice(0, idx + 1), base: normalized.slice(idx + 1) };
33
+ function parseFenceDelimiter(line) {
34
+ const t = trimEndSpacesTabs(line);
35
+ const m = /^(\s{0,3})(`{3,}|~{3,})(.*)$/.exec(t);
36
+ if (!m)
37
+ return null;
38
+ const run = m[2];
39
+ const head = run[0];
40
+ if (head !== "`" && head !== "~")
41
+ return null;
42
+ const ch = head === "`" ? "`" : "~";
43
+ return { ch, runLen: run.length, rest: m[3] ?? "" };
44
+ }
45
+ function isClosingFenceLine(info, open) {
46
+ if (info.ch !== open.ch || info.runLen < open.len)
47
+ return false;
48
+ return info.rest.trim() === "";
49
+ }
50
+ function lineAnchorHtml(mdLine0) {
51
+ const mdLine = String(mdLine0);
52
+ return `<span class="commentray-line-anchor" data-commentray-md-line="${mdLine}" id="commentray-md-line-${mdLine}" aria-hidden="true"></span>`;
53
+ }
54
+ function appendMdLineAnchorWhenAllowed(line, mdLine0) {
55
+ if (isSetextUnderlineLine(line) || isThematicBreakLine(line))
56
+ return line;
57
+ /** Blank lines must stay blank: a line that is only `<span …>` breaks CommonMark HTML / paragraph starts after block markers. */
58
+ if (line === "")
59
+ return "";
60
+ return `${line}${lineAnchorHtml(mdLine0)}`;
41
61
  }
42
- function renderFilePathLabel(filePath, fallbackTitle) {
43
- const shown = (filePath ?? "").trim();
44
- if (!shown) {
45
- return `<strong class="file-path file-path--title">${escapeHtml(fallbackTitle)}</strong>`;
62
+ /**
63
+ * Inserts per-line anchors for search / hash jumps and block separator anchors after each
64
+ * `<!-- commentray:block … -->` line (optional index attrs).
65
+ *
66
+ * Anchors are appended to the line when safe. A **leading** `<span>` breaks CommonMark block
67
+ * recognition (`#` headings, lists, thematic breaks, fences). Fenced code lines must not get a
68
+ * trailing anchor either (would corrupt fence delimiters or appear inside code).
69
+ */
70
+ function injectCommentrayDocAnchors(markdown, links) {
71
+ const byId = links ? new Map(links.map((l) => [l.id, l])) : undefined;
72
+ const lines = markdown.split("\n");
73
+ let fence = null;
74
+ const out = [];
75
+ for (let i = 0; i < lines.length; i++) {
76
+ const line = lines[i];
77
+ const delim = parseFenceDelimiter(line);
78
+ if (fence) {
79
+ if (delim && isClosingFenceLine(delim, fence)) {
80
+ fence = null;
81
+ out.push(line);
82
+ continue;
83
+ }
84
+ out.push(line);
85
+ continue;
86
+ }
87
+ if (delim) {
88
+ fence = { ch: delim.ch, len: delim.runLen };
89
+ out.push(line);
90
+ continue;
91
+ }
92
+ const m = BLOCK_MARKER_HTML_LINE.exec(line);
93
+ if (m?.[1]) {
94
+ const id = m[1];
95
+ const link = byId?.get(id);
96
+ const attrs = link !== undefined
97
+ ? ` data-source-start="${String(link.sourceStart)}" data-commentray-line="${String(link.commentrayLine)}"`
98
+ : "";
99
+ /** One `push` with embedded `\n\n` merged poorly with `join("\\n")`; keep real blank lines around raw `<div>`. */
100
+ out.push(`${line}${lineAnchorHtml(i)}`);
101
+ out.push("");
102
+ out.push(`<div id="commentray-block-${escapeHtml(id)}" class="commentray-block-anchor" aria-hidden="true"${attrs}></div>`);
103
+ out.push("");
104
+ continue;
105
+ }
106
+ out.push(appendMdLineAnchorWhenAllowed(line, i));
46
107
  }
47
- const { dir, base } = splitFilePath(shown);
48
- const dirHtml = dir
49
- ? `<span class="file-path__dir">${escapeHtml(dir)}</span>`
50
- : `<span class="file-path__dir file-path__dir--root" title="Repository root">/ </span>`;
51
- return (`<strong class="file-path" title="${escapeHtml(shown)}">` +
52
- dirHtml +
53
- `<span class="file-path__base">${escapeHtml(base)}</span>` +
54
- `</strong>`);
108
+ return out.join("\n");
55
109
  }
56
110
  /** GitHub “mark” glyph (Octicons-style path), MIT-licensed silhouette. */
57
111
  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">' +
@@ -65,7 +119,7 @@ function safeExternalHttpUrl(url) {
65
119
  return null;
66
120
  return t;
67
121
  }
68
- function buildToolbarEndHtml(githubRepoUrl, toolHomeUrl) {
122
+ function buildToolbarEndHtml(githubRepoUrl, toolHomeUrl, commentrayRenderSemver) {
69
123
  const gh = safeExternalHttpUrl(githubRepoUrl);
70
124
  const tool = safeExternalHttpUrl(toolHomeUrl);
71
125
  const bits = [];
@@ -75,12 +129,20 @@ function buildToolbarEndHtml(githubRepoUrl, toolHomeUrl) {
75
129
  }
76
130
  if (tool) {
77
131
  const te = escapeHtml(tool);
78
- bits.push(`<span class="toolbar-attribution" role="note">Rendered with <a href="${te}" target="_blank" rel="noopener noreferrer">Commentray</a></span>`);
132
+ const ver = escapeHtml(commentrayRenderSemver);
133
+ bits.push(`<span class="toolbar-attribution" role="note">Rendered with <a href="${te}" target="_blank" rel="noopener noreferrer">Commentray</a> <span class="toolbar-attribution__version" translate="no">v${ver}</span></span>`);
79
134
  }
80
135
  if (bits.length === 0)
81
136
  return "";
82
137
  return `<div class="toolbar__end">${bits.join("")}</div>`;
83
138
  }
139
+ function renderPageFooterHtml(builtAt) {
140
+ const iso = builtAt.toISOString();
141
+ const human = formatCommentrayBuiltAtLocal(builtAt);
142
+ return (`<footer class="app__footer" role="contentinfo">` +
143
+ `<p class="app__footer-line">HTML generated <time datetime="${escapeHtml(iso)}">${escapeHtml(human)}</time></p>` +
144
+ `</footer>`);
145
+ }
84
146
  function renderRelatedGithubNavHtml(links) {
85
147
  if (links.length === 0)
86
148
  return "";
@@ -90,6 +152,52 @@ function renderRelatedGithubNavHtml(links) {
90
152
  `<span class="toolbar-related__links">${parts.join('<span class="toolbar-related__sep" aria-hidden="true"> · </span>')}</span>` +
91
153
  `</nav>`);
92
154
  }
155
+ function renderToolbarDocHubHtml(opts) {
156
+ const nav = opts.documentedNavJsonUrl?.trim();
157
+ const hasEmbed = (opts.documentedPairsEmbeddedB64?.trim() ?? "").length > 0;
158
+ const showDocumentedTree = Boolean(nav) || hasEmbed;
159
+ const toolbarDocHubHtml = "";
160
+ const navAttr = escapeHtml(nav ?? "");
161
+ const navRailDocumentedHtml = showDocumentedTree
162
+ ? `<details class="nav-rail__doc-hub" id="documented-files-hub" data-nav-json-url="${navAttr}">
163
+ <summary class="nav-rail__doc-hub-summary">Documented files</summary>
164
+ <div class="nav-rail__doc-hub-inner">
165
+ <div id="documented-files-tree" class="documented-files-tree" role="tree"></div>
166
+ </div>
167
+ </details>`
168
+ : "";
169
+ return { toolbarDocHubHtml, navRailDocumentedHtml };
170
+ }
171
+ function renderNavRailContextHtml(filePath, commentrayPath, opts) {
172
+ const fpRaw = (filePath ?? "").trim();
173
+ const crRaw = (commentrayPath ?? "").trim();
174
+ const srcUrl = safeExternalHttpUrl(opts?.sourceOnGithubUrl);
175
+ const crUrl = safeExternalHttpUrl(opts?.commentrayOnGithubUrl);
176
+ if (fpRaw.length === 0 && crRaw.length === 0 && srcUrl === null && crUrl === null) {
177
+ return "";
178
+ }
179
+ const fp = escapeHtml(fpRaw);
180
+ const cr = escapeHtml(crRaw);
181
+ const fpDisp = fpRaw.length > 0 ? fp : "—";
182
+ const crDisp = crRaw.length > 0 ? cr : "—";
183
+ const srcGh = srcUrl !== null
184
+ ? `<a class="nav-rail__pair-gh" id="toolbar-source-github" href="${escapeHtml(srcUrl)}" target="_blank" rel="noopener noreferrer" aria-label="Source file on GitHub" title="Open source on GitHub">${GITHUB_MARK_SVG}</a>`
185
+ : "";
186
+ const crGh = crUrl !== null
187
+ ? `<a class="nav-rail__pair-gh" id="toolbar-commentray-github" href="${escapeHtml(crUrl)}" target="_blank" rel="noopener noreferrer" aria-label="Companion commentray on GitHub" title="Open companion Markdown on GitHub">${GITHUB_MARK_SVG}</a>`
188
+ : "";
189
+ return `<div class="nav-rail__context nav-rail__context--compact" aria-label="Current documentation pair">
190
+ <span class="nav-rail__pair">
191
+ <span class="nav-rail__pair-lab">Src</span>
192
+ <span class="nav-rail__pair-path" title="${fp}">${fpDisp}</span>${srcGh}
193
+ </span>
194
+ <span class="nav-rail__pair-sep" aria-hidden="true">·</span>
195
+ <span class="nav-rail__pair">
196
+ <span class="nav-rail__pair-lab">Doc</span>
197
+ <span class="nav-rail__pair-path nav-rail__pair-path--secondary" title="${cr}">${crDisp}</span>${crGh}
198
+ </span>
199
+ </div>`;
200
+ }
93
201
  /** IIFE produced by `npm run build -w @commentray/render` (esbuild of `code-browser-client.ts`). */
94
202
  function loadCodeBrowserClientBundle() {
95
203
  const here = dirname(fileURLToPath(import.meta.url));
@@ -106,6 +214,220 @@ const CODE_BROWSER_STYLES = `
106
214
  :root { color-scheme: light dark; }
107
215
  * { box-sizing: border-box; }
108
216
  body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
217
+ .app {
218
+ display: flex;
219
+ flex-direction: column;
220
+ align-items: stretch;
221
+ height: 100vh;
222
+ width: 100%;
223
+ overflow: hidden;
224
+ }
225
+ .app__chrome {
226
+ flex: 0 0 auto;
227
+ display: flex;
228
+ flex-direction: column;
229
+ gap: 8px;
230
+ padding: 8px 12px 10px;
231
+ border-bottom: 1px solid color-mix(in oklab, CanvasText 15%, Canvas);
232
+ background: color-mix(in oklab, CanvasText 4%, Canvas);
233
+ max-height: min(40vh, 420px);
234
+ min-height: 0;
235
+ overflow: auto;
236
+ }
237
+ .chrome__search-row {
238
+ display: flex;
239
+ flex-direction: row;
240
+ align-items: center;
241
+ gap: 10px;
242
+ flex-wrap: nowrap;
243
+ }
244
+ .chrome__search-row input[type="search"] {
245
+ flex: 1 1 auto;
246
+ min-width: 140px;
247
+ padding: 8px 10px;
248
+ font: inherit;
249
+ font-size: 14px;
250
+ border-radius: 8px;
251
+ border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas);
252
+ background: Canvas;
253
+ color: CanvasText;
254
+ }
255
+ .chrome__search-row #search-clear {
256
+ flex: 0 0 auto;
257
+ font: inherit;
258
+ padding: 6px 14px;
259
+ border-radius: 8px;
260
+ cursor: pointer;
261
+ border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas);
262
+ background: color-mix(in oklab, CanvasText 6%, Canvas);
263
+ color: CanvasText;
264
+ }
265
+ .chrome__search-label {
266
+ flex: 0 0 auto;
267
+ white-space: nowrap;
268
+ }
269
+ .nav-rail__context--compact {
270
+ display: flex;
271
+ flex-direction: row;
272
+ flex-wrap: wrap;
273
+ align-items: center;
274
+ gap: 6px 10px;
275
+ padding: 5px 10px;
276
+ border-radius: 8px;
277
+ border: 1px solid color-mix(in oklab, CanvasText 14%, Canvas);
278
+ background: Canvas;
279
+ font-size: 12px;
280
+ line-height: 1.3;
281
+ }
282
+ .nav-rail__pair {
283
+ display: inline-flex;
284
+ flex-direction: row;
285
+ align-items: center;
286
+ gap: 6px;
287
+ min-width: 0;
288
+ flex: 1 1 140px;
289
+ max-width: min(48%, 100%);
290
+ }
291
+ .nav-rail__pair-lab {
292
+ flex: 0 0 auto;
293
+ font-size: 9px;
294
+ font-weight: 700;
295
+ letter-spacing: 0.06em;
296
+ text-transform: uppercase;
297
+ opacity: 0.72;
298
+ }
299
+ .nav-rail__pair-path {
300
+ flex: 1 1 auto;
301
+ min-width: 0;
302
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
303
+ font-size: 11px;
304
+ color: CanvasText;
305
+ overflow: hidden;
306
+ text-overflow: ellipsis;
307
+ white-space: nowrap;
308
+ }
309
+ .nav-rail__pair-path--secondary { opacity: 0.88; }
310
+ .nav-rail__pair-sep {
311
+ flex: 0 0 auto;
312
+ opacity: 0.45;
313
+ user-select: none;
314
+ padding: 0 2px;
315
+ }
316
+ .nav-rail__pair-gh {
317
+ flex: 0 0 auto;
318
+ display: inline-flex;
319
+ align-items: center;
320
+ justify-content: center;
321
+ width: 26px;
322
+ height: 26px;
323
+ border-radius: 6px;
324
+ border: 1px solid color-mix(in oklab, CanvasText 20%, Canvas);
325
+ background: color-mix(in oklab, CanvasText 5%, Canvas);
326
+ color: CanvasText;
327
+ }
328
+ .nav-rail__pair-gh:hover {
329
+ background: color-mix(in oklab, CanvasText 10%, Canvas);
330
+ }
331
+ .nav-rail__pair-gh:focus-visible {
332
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
333
+ outline-offset: 2px;
334
+ }
335
+ .nav-rail__pair-gh svg {
336
+ width: 14px;
337
+ height: 14px;
338
+ display: block;
339
+ }
340
+ .toolbar .nav-rail__context--compact {
341
+ border: 0;
342
+ background: transparent;
343
+ padding: 0;
344
+ flex: 1 1 200px;
345
+ min-width: 0;
346
+ max-width: none;
347
+ gap: 6px 10px;
348
+ }
349
+ .toolbar .nav-rail__pair {
350
+ flex: 1 1 auto;
351
+ min-width: 0;
352
+ max-width: min(44vw, 420px);
353
+ }
354
+ .nav-rail__search-label {
355
+ font-size: 11px;
356
+ font-weight: 700;
357
+ letter-spacing: 0.05em;
358
+ text-transform: uppercase;
359
+ opacity: 0.8;
360
+ }
361
+ .nav-rail__search-hint {
362
+ margin: 0;
363
+ font-size: 11px;
364
+ line-height: 1.35;
365
+ opacity: 0.78;
366
+ }
367
+ .nav-rail__code {
368
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
369
+ font-size: 10px;
370
+ }
371
+ .nav-rail__doc-hub {
372
+ position: relative;
373
+ flex: 0 0 auto;
374
+ align-self: center;
375
+ display: block;
376
+ border: 1px solid color-mix(in oklab, CanvasText 16%, Canvas);
377
+ border-radius: 6px;
378
+ background: Canvas;
379
+ overflow: visible;
380
+ }
381
+ .nav-rail__doc-hub-summary {
382
+ cursor: pointer;
383
+ font-size: 12px;
384
+ font-weight: 600;
385
+ padding: 4px 10px;
386
+ list-style: none;
387
+ user-select: none;
388
+ line-height: 1.35;
389
+ }
390
+ .nav-rail__doc-hub-summary::-webkit-details-marker { display: none; }
391
+ .nav-rail__doc-hub-inner {
392
+ position: absolute;
393
+ left: 0;
394
+ top: calc(100% + 4px);
395
+ z-index: 60;
396
+ min-width: min(280px, 78vw);
397
+ max-width: min(440px, 94vw);
398
+ max-height: min(52vh, 400px);
399
+ overflow: auto;
400
+ padding: 8px 10px;
401
+ font-size: 12px;
402
+ border: 1px solid color-mix(in oklab, CanvasText 16%, Canvas);
403
+ border-radius: 8px;
404
+ background: Canvas;
405
+ box-shadow: 0 8px 28px color-mix(in oklab, CanvasText 12%, transparent);
406
+ }
407
+ .nav-rail__doc-hub-hint {
408
+ margin: 0 0 8px;
409
+ opacity: 0.78;
410
+ line-height: 1.4;
411
+ font-size: 12px;
412
+ }
413
+ .app__main {
414
+ flex: 1 1 auto;
415
+ min-width: 0;
416
+ min-height: 0;
417
+ display: flex;
418
+ flex-direction: column;
419
+ }
420
+ .app__footer {
421
+ flex: 0 0 auto;
422
+ padding: 6px 12px 10px;
423
+ border-top: 1px solid color-mix(in oklab, CanvasText 12%, Canvas);
424
+ background: color-mix(in oklab, CanvasText 3%, Canvas);
425
+ font-size: 11px;
426
+ line-height: 1.4;
427
+ color: color-mix(in oklab, CanvasText 72%, Canvas);
428
+ }
429
+ .app__footer-line { margin: 0; }
430
+ .app__footer time { font-variant-numeric: tabular-nums; }
109
431
  .toolbar {
110
432
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; padding: 8px 12px;
111
433
  border-bottom: 1px solid color-mix(in oklab, CanvasText 18%, Canvas);
@@ -113,7 +435,7 @@ const CODE_BROWSER_STYLES = `
113
435
  }
114
436
  .toolbar__main {
115
437
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px;
116
- flex: 1 1 280px;
438
+ flex: 0 1 auto;
117
439
  min-width: 0;
118
440
  }
119
441
  .toolbar__end {
@@ -162,13 +484,24 @@ const CODE_BROWSER_STYLES = `
162
484
  word-break: break-word;
163
485
  }
164
486
  .toolbar-related__sep { opacity: 0.55; user-select: none; }
165
- .toolbar .search-field {
166
- display: inline-flex; align-items: center; gap: 6px; flex: 1 1 220px; min-width: 160px;
487
+ .documented-files-tree ul { list-style: none; margin: 0; padding-left: 12px; }
488
+ .documented-files-tree > ul { padding-left: 0; }
489
+ .documented-files-tree li { margin: 2px 0; line-height: 1.35; }
490
+ .documented-files-tree .tree-dir { font-weight: 600; margin-top: 4px; font-size: 12px; }
491
+ .documented-files-tree .tree-file {
492
+ margin: 3px 0;
493
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
494
+ font-size: 11px;
167
495
  }
168
- .toolbar .search-field input[type="search"] {
169
- flex: 1; min-width: 0; padding: 4px 8px; font: inherit; border-radius: 6px;
170
- border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas); background: Canvas;
171
- color: CanvasText;
496
+ .documented-files-tree .tree-file-link {
497
+ color: inherit;
498
+ font-weight: 500;
499
+ text-decoration: underline;
500
+ text-underline-offset: 2px;
501
+ word-break: break-word;
502
+ }
503
+ .documented-files-tree .tree-file-link:hover {
504
+ opacity: 0.92;
172
505
  }
173
506
  .toolbar button {
174
507
  font: inherit; padding: 4px 10px; border-radius: 6px; cursor: pointer;
@@ -176,32 +509,73 @@ const CODE_BROWSER_STYLES = `
176
509
  color: CanvasText;
177
510
  }
178
511
  .search-results {
179
- flex: 0 0 auto; max-height: 160px; overflow: auto; padding: 6px 12px 8px;
180
- border-bottom: 1px solid color-mix(in oklab, CanvasText 12%, Canvas);
181
- font-size: 12px;
512
+ flex: 0 1 auto;
513
+ min-height: 0;
514
+ max-height: min(320px, 38vh);
515
+ overflow: auto;
516
+ padding: 8px 8px 10px;
517
+ border-radius: 8px;
518
+ border: 1px solid color-mix(in oklab, CanvasText 12%, Canvas);
519
+ background: Canvas;
520
+ font-size: 13px;
182
521
  }
183
522
  .search-results[hidden] { display: none !important; }
184
- .search-results .hint { opacity: 0.75; margin-bottom: 6px; }
523
+ .search-results .hint { opacity: 0.75; margin-bottom: 8px; line-height: 1.45; }
185
524
  .search-results button.hit {
186
- display: block; width: 100%; text-align: left; margin: 2px 0; padding: 6px 8px;
525
+ display: block; width: 100%; text-align: left; margin: 4px 0; padding: 8px 10px;
187
526
  border-radius: 6px; border: 1px solid color-mix(in oklab, CanvasText 14%, Canvas);
188
527
  background: color-mix(in oklab, CanvasText 5%, Canvas); color: CanvasText; cursor: pointer;
189
528
  font: inherit;
190
529
  }
191
530
  .search-results button.hit:hover { background: color-mix(in oklab, CanvasText 10%, Canvas); }
192
- .search-results button.hit .meta { opacity: 0.8; font-size: 11px; }
193
- .search-results button.hit .src-tag { opacity: 0.75; font-weight: 500; font-size: 10px; }
194
- .search-results button.hit .snippet { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace; font-size: 11px; white-space: pre-wrap; word-break: break-word; margin-top: 2px; }
531
+ .search-results button.hit .meta { opacity: 0.8; font-size: 12px; }
532
+ .search-results button.hit .src-tag { opacity: 0.75; font-weight: 500; font-size: 11px; }
533
+ .search-results button.hit .snippet {
534
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace; font-size: 13px;
535
+ line-height: 1.45; white-space: pre-wrap; word-break: break-word; margin-top: 4px;
536
+ }
537
+ .search-results mark.search-hit {
538
+ padding: 0 2px; border-radius: 3px; font: inherit;
539
+ background: color-mix(in oklab, #f5a623 70%, Canvas);
540
+ color: CanvasText;
541
+ box-decoration-break: clone;
542
+ -webkit-box-decoration-break: clone;
543
+ }
544
+ @media (prefers-color-scheme: dark) {
545
+ .search-results mark.search-hit {
546
+ background: color-mix(in oklab, #c9a227 55%, Canvas);
547
+ }
548
+ }
195
549
  .shell { display: flex; flex-direction: row; flex: 1; min-height: 0; }
550
+ .app__main .shell { flex: 1 1 auto; }
196
551
  .pane--code {
197
552
  flex: 0 0 50%;
198
553
  min-width: 120px; overflow: auto; padding: 12px 16px;
199
554
  border-right: 1px solid color-mix(in oklab, CanvasText 15%, Canvas);
555
+ --code-line-font-size: 13px;
556
+ --code-line-height: 1.5;
200
557
  }
558
+ .pane--code .code-line-stack { --code-ln-min-ch: 3; }
201
559
  .pane--code .code-line {
202
- display: grid; grid-template-columns: auto 1fr; column-gap: 12px; align-items: start;
560
+ display: grid;
561
+ grid-template-columns: max-content 1fr;
562
+ column-gap: 10px;
563
+ align-items: start;
564
+ }
565
+ .pane--code .code-line pre {
566
+ margin: 0;
567
+ min-width: 0;
568
+ padding: 0;
569
+ border: 0;
570
+ background: transparent;
571
+ }
572
+ .pane--code .code-line pre code.hljs {
573
+ display: block;
574
+ margin: 0;
575
+ padding: 0;
576
+ font-size: var(--code-line-font-size);
577
+ line-height: var(--code-line-height);
203
578
  }
204
- .pane--code .code-line pre { margin: 0; min-width: 0; }
205
579
  .pane--code .code-line .ln {
206
580
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
207
581
  font-variant-numeric: tabular-nums;
@@ -209,7 +583,11 @@ const CODE_BROWSER_STYLES = `
209
583
  color: color-mix(in oklab, CanvasText 45%, Canvas);
210
584
  padding-right: 8px;
211
585
  border-right: 1px solid color-mix(in oklab, CanvasText 12%, Canvas);
212
- min-width: 3ch;
586
+ white-space: nowrap;
587
+ font-size: var(--code-line-font-size);
588
+ line-height: var(--code-line-height);
589
+ min-width: calc(var(--code-ln-min-ch, 3) * 1ch + 0.6ch);
590
+ box-sizing: content-box;
213
591
  }
214
592
  .pane--code .code-line:target .ln,
215
593
  .pane--code .code-line:hover .ln {
@@ -230,103 +608,423 @@ const CODE_BROWSER_STYLES = `
230
608
  content: ""; position: absolute; top: 0; bottom: 0; left: -4px; right: -4px;
231
609
  }
232
610
  .pane--doc {
233
- flex: 1 1 auto; min-width: 120px; overflow: auto; padding: 12px 16px;
611
+ flex: 1 1 auto; min-width: 0; min-height: 0;
612
+ display: flex; flex-direction: column; overflow: hidden; padding: 12px 16px;
613
+ }
614
+ .doc-pane-body {
615
+ flex: 1 1 auto; min-height: 0; overflow: auto;
616
+ }
617
+ .toolbar-angle-picker {
618
+ display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto;
619
+ font-size: 12px; color: color-mix(in oklab, CanvasText 88%, Canvas);
620
+ }
621
+ .toolbar-angle-picker select {
622
+ font: inherit; font-size: 12px; padding: 3px 8px; border-radius: 6px;
623
+ border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas); background: Canvas; color: CanvasText;
234
624
  }
235
625
  .pane--doc { font-size: 15px; line-height: 1.45; }
236
626
  .pane--doc img { max-width: 100%; height: auto; }
627
+ .pane--doc .commentray-line-anchor {
628
+ display: inline;
629
+ vertical-align: baseline;
630
+ scroll-margin-top: 10px;
631
+ }
632
+ .pane--doc .commentray-block-anchor {
633
+ display: block;
634
+ height: 0;
635
+ margin: 14px 0 0;
636
+ border: 0;
637
+ border-top: 1px solid color-mix(in oklab, CanvasText 22%, Canvas);
638
+ pointer-events: none;
639
+ }
237
640
  .pane h2.pane-title { margin: 0 0 10px; font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase; opacity: 0.75; }
238
- .app { display: flex; flex-direction: column; height: 100vh; }
641
+ .shell--stretch-rows {
642
+ flex: 1;
643
+ min-height: 0;
644
+ overflow: auto;
645
+ display: block;
646
+ padding: 0 12px 20px;
647
+ }
648
+ .shell--stretch-rows .stretch-preamble {
649
+ padding: 8px 4px 16px;
650
+ margin-bottom: 8px;
651
+ border-bottom: 1px solid color-mix(in oklab, CanvasText 12%, Canvas);
652
+ font-size: 15px;
653
+ line-height: 1.45;
654
+ }
655
+ .shell--stretch-rows .stretch-preamble img { max-width: 100%; height: auto; }
656
+ .block-stretch {
657
+ width: 100%;
658
+ border-collapse: collapse;
659
+ table-layout: fixed;
660
+ }
661
+ .stretch-col-code { width: 50%; }
662
+ .stretch-col-doc { width: 50%; }
663
+ .block-stretch td.stretch-code {
664
+ vertical-align: top;
665
+ padding: 0 12px 0 0;
666
+ border-bottom: 1px solid color-mix(in oklab, CanvasText 8%, Canvas);
667
+ }
668
+ .block-stretch td.stretch-doc {
669
+ vertical-align: top;
670
+ padding: 0 0 0 12px;
671
+ border-bottom: 1px solid color-mix(in oklab, CanvasText 8%, Canvas);
672
+ }
673
+ .block-stretch td.stretch-doc .stretch-doc-inner {
674
+ font-size: 15px;
675
+ line-height: 1.45;
676
+ }
677
+ .block-stretch td.stretch-doc .stretch-doc-inner img { max-width: 100%; height: auto; }
678
+ .block-stretch td.stretch-doc--gap {
679
+ color: color-mix(in oklab, CanvasText 38%, Canvas);
680
+ font-size: 13px;
681
+ vertical-align: top;
682
+ }
683
+ .block-stretch .stretch-gap-mark { display: inline-block; padding-top: 2px; }
684
+ .block-stretch .stretch-code-stack {
685
+ display: flex;
686
+ flex-direction: column;
687
+ align-items: stretch;
688
+ min-width: 0;
689
+ }
690
+ .block-stretch .code-line {
691
+ display: grid;
692
+ grid-template-columns: max-content 1fr;
693
+ column-gap: 12px;
694
+ align-items: start;
695
+ }
696
+ .block-stretch .code-line pre { margin: 0; min-width: 0; padding: 0; border: 0; background: transparent; }
697
+ .block-stretch .code-line pre code.hljs {
698
+ display: block;
699
+ margin: 0;
700
+ padding: 0;
701
+ font-size: var(--code-line-font-size, 13px);
702
+ line-height: var(--code-line-height, 1.5);
703
+ }
704
+ .block-stretch .code-line-stack { --code-ln-min-ch: 3; }
705
+ .block-stretch .code-line .ln {
706
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
707
+ font-variant-numeric: tabular-nums;
708
+ text-align: right;
709
+ user-select: none;
710
+ -webkit-user-select: none;
711
+ color: color-mix(in oklab, CanvasText 45%, Canvas);
712
+ padding-right: 8px;
713
+ border-right: 1px solid color-mix(in oklab, CanvasText 12%, Canvas);
714
+ white-space: nowrap;
715
+ font-size: var(--code-line-font-size, 13px);
716
+ line-height: var(--code-line-height, 1.5);
717
+ min-width: calc(var(--code-ln-min-ch, 3) * 1ch + 0.6ch);
718
+ box-sizing: content-box;
719
+ }
720
+ .block-stretch.wrap .code-line pre,
721
+ .block-stretch.wrap .code-line pre code { white-space: pre-wrap; word-break: break-word; }
722
+ .block-stretch:not(.wrap) .code-line pre,
723
+ .block-stretch:not(.wrap) .code-line pre code { white-space: pre; }
724
+ .block-stretch-headings {
725
+ display: grid;
726
+ grid-template-columns: 1fr 1fr;
727
+ gap: 0 16px;
728
+ padding: 4px 12px 8px;
729
+ border-bottom: 1px solid color-mix(in oklab, CanvasText 10%, Canvas);
730
+ }
731
+ .block-stretch-headings .pane-title { margin: 0; }
239
732
  `;
733
+ /** Native tooltip on #search-q (short hint is visible under the search row). */
734
+ const CODE_BROWSER_SEARCH_INPUT_TITLE = "Filename, path, or words. Matches this pair (source + commentray lines) first; merges commentray-nav-search.json when the export includes it (indexed paths + commentray lines).";
240
735
  function buildCodeBrowserPageHtml(p) {
241
- const { title, generatorMetaHtml, filePathHtml, relatedNavHtml, toolbarEndHtml, codeHtml, commentrayHtml, rawCodeB64, rawMdB64, hljs, hljsDark, mermaidScript, } = p;
736
+ const shellClass = p.layout === "stretch" ? "shell shell--stretch-rows" : "shell";
242
737
  return `<!doctype html>
243
738
  <html lang="en">
244
739
  <head>
245
740
  <meta charset="utf-8" />
246
741
  <meta name="viewport" content="width=device-width, initial-scale=1" />
247
- ${generatorMetaHtml}<title>${escapeHtml(title)}</title>
248
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/${escapeHtml(hljs)}.min.css" media="(prefers-color-scheme: light)" />
249
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/${escapeHtml(hljsDark)}.min.css" media="(prefers-color-scheme: dark)" />
742
+ ${p.generatorMetaHtml}<title>${escapeHtml(p.title)}</title>
743
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/${escapeHtml(p.hljs)}.min.css" media="(prefers-color-scheme: light)" />
744
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/${escapeHtml(p.hljsDark)}.min.css" media="(prefers-color-scheme: dark)" />
250
745
  <style>
251
746
  ${CODE_BROWSER_STYLES}
252
747
  </style>
253
748
  </head>
254
749
  <body>
255
750
  <div class="app">
256
- <header class="toolbar" aria-label="View options">
257
- <div class="toolbar__main">
258
- ${filePathHtml}
259
- <span class="search-field">
260
- <label for="search-q">Search</label>
261
- <input type="search" id="search-q" placeholder="Whole source (ordered tokens + fuzzy lines)…" autocomplete="off" spellcheck="false" />
262
- <button type="button" id="search-clear" title="Clear search">Clear</button>
263
- </span>
264
- ${relatedNavHtml}
265
- <label><input type="checkbox" id="wrap-lines" /> Wrap code lines</label>
751
+ <header class="app__chrome" role="region" aria-label="Search and navigation">
752
+ <div class="chrome__search-row">
753
+ <label class="chrome__search-label nav-rail__search-label" for="search-q">Search</label>
754
+ <input type="search" id="search-q" placeholder="${escapeHtml(p.searchPlaceholder)}" title="${escapeHtml(CODE_BROWSER_SEARCH_INPUT_TITLE)}" autocomplete="off" spellcheck="false" />
755
+ <button type="button" id="search-clear" title="Clear search">Clear</button>
266
756
  </div>
267
- ${toolbarEndHtml}
757
+ <div class="search-results" id="search-results" hidden aria-live="polite"></div>
758
+ <p class="nav-rail__search-hint chrome__search-hint">This pair + merged <code class="nav-rail__code">commentray-nav-search.json</code> when the export ships it.</p>
268
759
  </header>
269
- <div class="search-results" id="search-results" hidden aria-live="polite"></div>
270
- <div class="shell" id="shell">
271
- <section class="pane--code" id="code-pane" aria-label="Source code" data-raw-code-b64="${escapeHtml(rawCodeB64)}" data-raw-md-b64="${escapeHtml(rawMdB64)}">
272
- <h2 class="pane-title">Code</h2>
273
- ${codeHtml}
274
- </section>
275
- <div class="gutter" id="gutter" role="separator" aria-orientation="vertical" aria-label="Resize panes"></div>
276
- <section class="pane--doc commentray" id="doc-pane" aria-label="Commentray">
277
- <h2 class="pane-title">Commentray</h2>
278
- ${commentrayHtml}
279
- </section>
760
+ <div class="app__main">
761
+ <header class="toolbar" aria-label="View options">
762
+ <div class="toolbar__main">
763
+ ${p.navRailContextHtml}
764
+ ${p.navRailDocumentedHtml}
765
+ ${p.angleSelectHtml}
766
+ ${p.toolbarDocHubHtml}
767
+ ${p.relatedNavHtml}
768
+ <label><input type="checkbox" id="wrap-lines" /> Wrap code lines</label>
769
+ </div>
770
+ ${p.toolbarEndHtml}
771
+ </header>
772
+ <div class="${shellClass}" id="shell" data-layout="${p.layout}" 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}>
773
+ ${p.shellInner}
774
+ </div>
280
775
  </div>
776
+ ${p.pageFooterHtml}
281
777
  </div>
778
+ <script type="text/plain" id="commentray-multi-angle-b64">${p.multiAngleScriptBlock}</script>
282
779
  <script>
283
780
  ${loadCodeBrowserClientBundle()}
284
781
  </script>
285
- ${mermaidScript}
782
+ ${p.mermaidScript}
286
783
  </body>
287
784
  </html>`;
288
785
  }
786
+ async function buildMultiAngleDualPaneShell(opts, multi) {
787
+ const defaultId = multi.angles.some((a) => a.id === multi.defaultAngleId)
788
+ ? multi.defaultAngleId
789
+ : (multi.angles[0]?.id ?? "main");
790
+ const jsonAngles = [];
791
+ let defaultMarkdown = opts.commentrayMarkdown;
792
+ let defaultScrollB64 = "";
793
+ let defaultPathSearch = (opts.commentrayPathForSearch ?? "").trim();
794
+ let defaultGh = opts.commentrayOnGithubUrl;
795
+ let defaultPaneHtml = "";
796
+ const codeHtml = await renderHighlightedCodeLineRows(opts.code, opts.language);
797
+ for (const spec of multi.angles) {
798
+ const rows = spec.blockStretchRows;
799
+ const links = rows !== undefined
800
+ ? buildBlockScrollLinks(rows.index, rows.sourceRelative, rows.commentrayPathRel, spec.markdown, opts.code)
801
+ : [];
802
+ const mdForDoc = injectCommentrayDocAnchors(spec.markdown, links.length > 0 ? links : undefined);
803
+ const scrollB64 = links.length > 0 ? Buffer.from(JSON.stringify(links), "utf8").toString("base64") : "";
804
+ const commentrayHtml = await renderMarkdownToHtml(mdForDoc, {
805
+ commentrayOutputUrls: opts.commentrayOutputUrls,
806
+ });
807
+ if (spec.id === defaultId) {
808
+ defaultMarkdown = spec.markdown;
809
+ defaultScrollB64 = scrollB64;
810
+ defaultPathSearch = spec.commentrayPathRel.trim();
811
+ defaultGh = spec.commentrayOnGithubUrl;
812
+ defaultPaneHtml = commentrayHtml;
813
+ }
814
+ jsonAngles.push({
815
+ id: spec.id,
816
+ title: spec.title?.trim() || spec.id,
817
+ docInnerHtmlB64: Buffer.from(commentrayHtml, "utf8").toString("base64"),
818
+ rawMdB64: Buffer.from(spec.markdown, "utf8").toString("base64"),
819
+ scrollBlockLinksB64: scrollB64,
820
+ commentrayPathForSearch: spec.commentrayPathRel.trim(),
821
+ commentrayOnGithubUrl: spec.commentrayOnGithubUrl,
822
+ });
823
+ }
824
+ const selOpts = multi.angles
825
+ .map((a) => {
826
+ const lab = escapeHtml(a.title?.trim() || a.id);
827
+ return `<option value="${escapeHtml(a.id)}"${a.id === defaultId ? " selected" : ""}>${lab}</option>`;
828
+ })
829
+ .join("");
830
+ const angleSelectHtml = `<span class="toolbar-angle-picker"><label for="angle-select">Angle</label><select id="angle-select" aria-label="Commentray angle">${selOpts}</select></span>`;
831
+ const shellInner = ` <section class="pane--code" id="code-pane" aria-label="Source code">` +
832
+ `<h2 class="pane-title">Code</h2>\n` +
833
+ ` ${codeHtml}\n` +
834
+ ` </section>\n` +
835
+ ` <div class="gutter" id="gutter" role="separator" aria-orientation="vertical" aria-label="Resize panes"></div>\n` +
836
+ ` <section class="pane--doc commentray" id="doc-pane" aria-label="Commentray">\n` +
837
+ ` <h2 class="pane-title">Commentray</h2>\n` +
838
+ ` <div id="doc-pane-body" class="doc-pane-body">\n` +
839
+ ` ${defaultPaneHtml}\n` +
840
+ ` </div>\n` +
841
+ ` </section>\n`;
842
+ const payloadObj = { defaultAngleId: defaultId, angles: jsonAngles };
843
+ const multiAnglePayloadB64 = Buffer.from(JSON.stringify(payloadObj), "utf8").toString("base64");
844
+ return {
845
+ shellInner,
846
+ multiShell: {
847
+ rawMdB64: Buffer.from(defaultMarkdown, "utf8").toString("base64"),
848
+ scrollBlockLinksB64: defaultScrollB64,
849
+ commentrayPathForSearch: defaultPathSearch,
850
+ commentrayOnGithubUrl: defaultGh,
851
+ },
852
+ angleSelectHtml,
853
+ multiAnglePayloadB64,
854
+ };
855
+ }
856
+ async function buildCodeBrowserShell(opts, layoutPref) {
857
+ let layout = "dual";
858
+ let shellInner = "";
859
+ let scrollBlockLinksB64 = "";
860
+ const multi = opts.multiAngleBrowsing;
861
+ const multiActive = Boolean(multi && multi.angles.length >= 2);
862
+ if (multiActive && multi) {
863
+ const built = await buildMultiAngleDualPaneShell(opts, multi);
864
+ const ms = built.multiShell;
865
+ return {
866
+ layout: "dual",
867
+ shellInner: built.shellInner,
868
+ scrollBlockLinksB64: ms.scrollBlockLinksB64,
869
+ angleSelectHtml: built.angleSelectHtml,
870
+ multiAnglePayloadB64: built.multiAnglePayloadB64,
871
+ multiShell: ms,
872
+ };
873
+ }
874
+ if (opts.blockStretchRows && layoutPref !== "dual") {
875
+ const stretched = await tryBuildBlockStretchTableHtml({
876
+ code: opts.code,
877
+ language: opts.language,
878
+ commentrayMarkdown: opts.commentrayMarkdown,
879
+ index: opts.blockStretchRows.index,
880
+ sourceRelative: opts.blockStretchRows.sourceRelative,
881
+ commentrayPathRel: opts.blockStretchRows.commentrayPathRel,
882
+ commentrayOutputUrls: opts.commentrayOutputUrls,
883
+ });
884
+ if (stretched) {
885
+ layout = "stretch";
886
+ shellInner =
887
+ ` <div class="block-stretch-headings">` +
888
+ `<h2 class="pane-title">Code</h2>` +
889
+ `<h2 class="pane-title">Commentray</h2>` +
890
+ `</div>\n` +
891
+ ` ${stretched.preambleHtml}\n` +
892
+ ` ${stretched.tableInnerHtml}\n`;
893
+ }
894
+ }
895
+ if (layout === "dual") {
896
+ const links = opts.blockStretchRows !== undefined
897
+ ? buildBlockScrollLinks(opts.blockStretchRows.index, opts.blockStretchRows.sourceRelative, opts.blockStretchRows.commentrayPathRel, opts.commentrayMarkdown, opts.code)
898
+ : [];
899
+ const mdForDoc = injectCommentrayDocAnchors(opts.commentrayMarkdown, links.length > 0 ? links : undefined);
900
+ if (links.length > 0) {
901
+ scrollBlockLinksB64 = Buffer.from(JSON.stringify(links), "utf8").toString("base64");
902
+ }
903
+ const [codeHtml, commentrayHtml] = await Promise.all([
904
+ renderHighlightedCodeLineRows(opts.code, opts.language),
905
+ renderMarkdownToHtml(mdForDoc, {
906
+ commentrayOutputUrls: opts.commentrayOutputUrls,
907
+ }),
908
+ ]);
909
+ shellInner =
910
+ ` <section class="pane--code" id="code-pane" aria-label="Source code">` +
911
+ `<h2 class="pane-title">Code</h2>\n` +
912
+ ` ${codeHtml}\n` +
913
+ ` </section>\n` +
914
+ ` <div class="gutter" id="gutter" role="separator" aria-orientation="vertical" aria-label="Resize panes"></div>\n` +
915
+ ` <section class="pane--doc commentray" id="doc-pane" aria-label="Commentray">\n` +
916
+ ` <h2 class="pane-title">Commentray</h2>\n` +
917
+ ` <div id="doc-pane-body" class="doc-pane-body">\n` +
918
+ ` ${commentrayHtml}\n` +
919
+ ` </div>\n` +
920
+ ` </section>\n`;
921
+ }
922
+ return {
923
+ layout,
924
+ shellInner,
925
+ scrollBlockLinksB64,
926
+ angleSelectHtml: "",
927
+ multiAnglePayloadB64: "",
928
+ };
929
+ }
930
+ function searchChromeFromOptions(opts, commentrayPathOverride) {
931
+ const crPath = (commentrayPathOverride ?? opts.commentrayPathForSearch ?? "").trim();
932
+ if (opts.staticSearchScope === "commentray-and-paths") {
933
+ return {
934
+ searchPlaceholder: "Filename, path, or keywords…",
935
+ shellSearchAttrs: ` data-search-scope="commentray-and-paths" data-search-file-path="${escapeHtml(opts.filePath ?? "")}" data-search-commentray-path="${escapeHtml(crPath)}"`,
936
+ };
937
+ }
938
+ return {
939
+ searchPlaceholder: "Filename, path, or keywords…",
940
+ shellSearchAttrs: "",
941
+ };
942
+ }
943
+ function shellDocumentedPairsAttrFromOptions(opts) {
944
+ const emb = opts.documentedPairsEmbeddedB64?.trim() ?? "";
945
+ if (emb.length === 0)
946
+ return "";
947
+ return ` data-documented-pairs-b64="${escapeHtml(emb)}"`;
948
+ }
949
+ function codeBrowserPageTitle(opts) {
950
+ return opts.title ?? opts.filePath ?? "Commentray";
951
+ }
952
+ function codeBrowserHljsThemes(opts) {
953
+ const hljs = opts.hljsTheme ?? "github";
954
+ const hljsDark = opts.hljsTheme?.includes("dark") ? opts.hljsTheme : "github-dark";
955
+ return { hljs, hljsDark };
956
+ }
957
+ function toolbarCommentrayGithubFromShell(shell, opts) {
958
+ return shell.multiShell?.commentrayOnGithubUrl ?? opts.commentrayOnGithubUrl;
959
+ }
960
+ function rawMdB64FromShell(shell, opts) {
961
+ return (shell.multiShell?.rawMdB64 ?? Buffer.from(opts.commentrayMarkdown, "utf8").toString("base64"));
962
+ }
963
+ function navRailCommentrayPathFromShell(shell, opts) {
964
+ const trimmed = (shell.multiShell?.commentrayPathForSearch ??
965
+ opts.commentrayPathForSearch ??
966
+ "").trim();
967
+ return trimmed.length > 0 ? trimmed : undefined;
968
+ }
969
+ function shellSearchAttrsWithNavJson(shellSearchAttrsBase, documentedNavJsonUrl) {
970
+ const navJson = documentedNavJsonUrl?.trim() ?? "";
971
+ if (navJson.length === 0)
972
+ return shellSearchAttrsBase;
973
+ return `${shellSearchAttrsBase} data-nav-search-json-url="${escapeHtml(navJson)}"`;
974
+ }
289
975
  /**
290
976
  * Static HTML shell for a minimal “code browser”: code + rendered commentray,
291
977
  * draggable vertical splitter, togglable line wrap for the code pane, and
292
978
  * token-in-line quick search (all non-whitespace tokens must appear on the same line).
293
979
  */
294
980
  export async function renderCodeBrowserHtml(opts) {
295
- const [codeHtml, commentrayHtml] = await Promise.all([
296
- renderCodeLineBlocks(opts.code, opts.language),
297
- renderMarkdownToHtml(opts.commentrayMarkdown, {
298
- commentrayOutputUrls: opts.commentrayOutputUrls,
299
- }),
300
- ]);
301
981
  const rawCodeB64 = Buffer.from(opts.code, "utf8").toString("base64");
302
- const rawMdB64 = Buffer.from(opts.commentrayMarkdown, "utf8").toString("base64");
303
- const title = opts.title ?? opts.filePath ?? "Commentray";
304
- const filePathHtml = renderFilePathLabel(opts.filePath, title);
305
- const toolbarEndHtml = buildToolbarEndHtml(opts.githubRepoUrl, opts.toolHomeUrl);
306
- const hljs = opts.hljsTheme ?? "github";
307
- const hljsDark = opts.hljsTheme?.includes("dark") ? opts.hljsTheme : "github-dark";
308
- const mermaidScript = opts.includeMermaidRuntime
309
- ? `<script type="module">
310
- import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
311
- mermaid.initialize({ startOnLoad: true, securityLevel: "strict" });
312
- mermaid.run({ querySelector: ".mermaid" });
313
- </script>`
314
- : "";
982
+ const title = codeBrowserPageTitle(opts);
983
+ const builtAt = opts.builtAt ?? new Date();
984
+ const renderSemver = commentrayRenderVersion();
985
+ const toolbarEndHtml = buildToolbarEndHtml(opts.githubRepoUrl, opts.toolHomeUrl, renderSemver);
986
+ const pageFooterHtml = renderPageFooterHtml(builtAt);
987
+ const { hljs, hljsDark } = codeBrowserHljsThemes(opts);
988
+ const mermaidScript = mermaidRuntimeScriptHtml(opts.includeMermaidRuntime);
315
989
  const relatedNavHtml = renderRelatedGithubNavHtml(opts.relatedGithubNav ?? []);
316
990
  const generatorMetaHtml = renderGeneratorMetaHtml(opts.generatorLabel);
991
+ const layoutPref = opts.codeBrowserLayout ?? "auto";
992
+ const shell = await buildCodeBrowserShell(opts, layoutPref);
993
+ const { toolbarDocHubHtml, navRailDocumentedHtml } = renderToolbarDocHubHtml({
994
+ documentedNavJsonUrl: opts.documentedNavJsonUrl,
995
+ documentedPairsEmbeddedB64: opts.documentedPairsEmbeddedB64,
996
+ });
997
+ const rawMdB64 = rawMdB64FromShell(shell, opts);
998
+ const scrollBlockLinksB64 = shell.scrollBlockLinksB64;
999
+ const { searchPlaceholder, shellSearchAttrs: shellSearchAttrsBase } = searchChromeFromOptions(opts, shell.multiShell?.commentrayPathForSearch);
1000
+ const shellDocumentedPairsAttr = shellDocumentedPairsAttrFromOptions(opts);
1001
+ const shellSearchAttrs = shellSearchAttrsWithNavJson(shellSearchAttrsBase, opts.documentedNavJsonUrl);
1002
+ const navRailContextHtml = renderNavRailContextHtml(opts.filePath, navRailCommentrayPathFromShell(shell, opts), {
1003
+ sourceOnGithubUrl: opts.sourceOnGithubUrl,
1004
+ commentrayOnGithubUrl: toolbarCommentrayGithubFromShell(shell, opts),
1005
+ });
317
1006
  return buildCodeBrowserPageHtml({
318
1007
  title,
319
1008
  generatorMetaHtml,
320
- filePathHtml,
1009
+ navRailContextHtml,
1010
+ angleSelectHtml: shell.angleSelectHtml,
1011
+ toolbarDocHubHtml,
1012
+ navRailDocumentedHtml,
321
1013
  relatedNavHtml,
322
1014
  toolbarEndHtml,
323
- codeHtml,
324
- commentrayHtml,
1015
+ pageFooterHtml,
1016
+ layout: shell.layout,
1017
+ shellInner: shell.shellInner,
325
1018
  rawCodeB64,
326
1019
  rawMdB64,
1020
+ scrollBlockLinksB64,
1021
+ shellDocumentedPairsAttr,
327
1022
  hljs,
328
1023
  hljsDark,
329
1024
  mermaidScript,
1025
+ searchPlaceholder,
1026
+ shellSearchAttrs,
1027
+ multiAngleScriptBlock: shell.multiAnglePayloadB64,
330
1028
  });
331
1029
  }
332
1030
  //# sourceMappingURL=code-browser.js.map