jsdoc-scribe 1.7.0 → 1.11.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,138 @@
3
3
  All notable changes to `jsdoc-scribe` are documented here.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
5
 
6
+ ## [1.11.0] - 2026-06-30
7
+
8
+ ### Changed — Code Quality (Phase D)
9
+
10
+ **`extractor.js` robustness**
11
+ - Added `sourceFile` null guard: if `ts.createSourceFile()` returns a falsy value the function logs to stderr and returns an empty-but-valid module object instead of throwing.
12
+ - Wrapped the entire `visit(node)` body in `try/catch`: a malformed or unexpected AST node now logs a warning (`jsdoc-scribe: skipped node in <file> — <message>`) and continues visiting sibling nodes instead of aborting the whole module.
13
+
14
+ **`renderer.js` refactor**
15
+ - Extracted `buildSymbolMap(modules)` from `buildSite()` — builds the `{name → {anchorId, modulePath}}` cross-reference map used by `{@link}` resolution.
16
+ - Extracted `buildIndexBody(modules)` — builds the module-grid HTML block for the index page (previously inlined in `buildSite()`).
17
+ - Extracted `buildModuleBody(mod, sourceUrl, symbolMap)` — builds the sections HTML for a single module page (previously inlined in `buildSite()`).
18
+ - `buildSite()` is now a slim coordinator (~25 lines) delegating to the three helpers above.
19
+
20
+ **`docs.js` async upgrade**
21
+ - `extractModules(files)` is now `async` and uses `Promise.allSettled`: all files are attempted in parallel; fulfilled results are collected, rejected ones are logged to stderr and skipped.
22
+ - `generateSite(inputPaths, options)` is now `async` accordingly.
23
+
24
+ ---
25
+
26
+ ## [1.10.0] - 2026-06-30
27
+
28
+ ### Added — Test coverage (Phase C)
29
+
30
+ Expanded `npm test` from 7 tests to **25 tests** across three suites.
31
+
32
+ **`test/extractor.test.js` — 10 new tests** (exercises `lib/extractor.js`):
33
+ - Module-level `@module` description and `@since` extracted from top-of-file JSDoc
34
+ - `@param` type and description parsed from JSDoc block
35
+ - `@returns` type and description parsed
36
+ - `@since` and `@deprecated` on individual items
37
+ - `@throws` with type and description
38
+ - 1-based source line numbers on all functions (ordered)
39
+ - Class with constructor, methods, properties, and static members
40
+ - Interface with optional properties
41
+ - Enum members and their values
42
+ - Type alias and exported `const` variable
43
+
44
+ **`test/renderer.test.js` — 8 new tests** (exercises `lib/renderer.js` via mock module objects):
45
+ - `buildSite` returns exactly 3 shared assets + `index.html` + one page per module
46
+ - Search index contains every symbol (function, class) with root-relative URLs
47
+ - Module page includes right-side TOC with `data-anchor` attributes and "On this page" title
48
+ - `@deprecated` badge and notice rendered for deprecated items
49
+ - Source link contains GitHub URL and `#L42` line anchor when `sourceUrl` set
50
+ - `@example` blocks rendered with `tok-kw` syntax-highlighting spans
51
+ - Sidebar symbol tree includes `sym-rows` and kind pills for the active module
52
+ - `{@link Symbol}` in descriptions resolves to `<a class="link-ref">` with `href`
53
+
54
+ ### Fixed — `@description` tag in JSDoc blocks
55
+
56
+ `parseJSDocBlock()` now recognises `@description <text>` as an alias for the plain-text
57
+ description that precedes the first `@tag`. Previously `@description` was silently discarded as
58
+ an unknown tag, leaving `mod.description === null` for any file that used it (including all
59
+ built-in sample files).
60
+
61
+ ---
62
+
63
+ ## [1.9.0] - 2026-06-30
64
+
65
+ ### Changed — Enterprise HTML Redesign (Phase B)
66
+
67
+ Complete visual overhaul of the generated documentation site.
68
+
69
+ **Three-column layout**
70
+ - Left sidebar (272 px) · Center content (flexible) · Right TOC (224 px)
71
+ - CSS Grid replaces the old flexbox layout: `.layout { grid-template-columns: 272px 1fr 224px }`
72
+ - Index page uses two-column grid (no right TOC); module pages use three columns automatically via the `has-toc` class
73
+
74
+ **Right-side "On this page" TOC**
75
+ - New `buildToc(mod)` function generates a per-section TOC (Functions, Classes, Interfaces, etc.) with every symbol as a clickable anchor
76
+ - `IntersectionObserver` scroll spy in `app.js` tracks which card is on screen and highlights the matching TOC item with an accent-colored left border
77
+ - TOC hides at ≤1280 px viewport width; three columns collapse to two
78
+
79
+ **Symbol tree in sidebar**
80
+ - Active module expands to show every symbol underneath its file link
81
+ - Each symbol is prefixed with a color-coded pill badge (`fn`, `cls`, `if`, `ty`, `en`, `$`) matching its kind
82
+ - New `.sym-rows`, `.sym-row`, `.sym-pill`, `.sym-link` CSS classes
83
+
84
+ **Color-coded cards**
85
+ - Each card now has a 3 px left accent border: green=function, blue=class, purple=interface, orange=enum, teal=type alias, gray=variable/const
86
+ - Added `card-fn`, `card-cls`, `card-iface`, `card-enum`, `card-type`, `card-var` CSS classes to all render functions
87
+
88
+ **Server-side syntax highlighting for `@example` blocks**
89
+ - New `highlightCode(raw)` function in `renderer.js` tokenizes JS/TS code at build time
90
+ - Handles line comments, block comments, strings (single/double/template), numbers, and 40+ keywords
91
+ - Produces `<span class="tok-kw|tok-str|tok-cmt|tok-num">` spans; colors adapt to dark/light theme via CSS vars
92
+
93
+ **Responsive layout**
94
+ - `@media (max-width: 1280px)`: right TOC hidden, grid collapses to two columns
95
+ - `@media (max-width: 860px)`: sidebar becomes a fixed overlay (off-screen by default), hamburger button appears in the top-left, main content uses mobile padding
96
+ - Hamburger toggle with animated open/close icon (three-bar → X)
97
+
98
+ **Print styles**
99
+ - `@media print`: sidebar, TOC, hamburger, copy buttons, theme toggle, and search box all hidden
100
+ - Cards get `break-inside: avoid` and a neutral border for clean PDF output
101
+
102
+ **Section counts**
103
+ - Section headings now show item count: `Functions (3)` using a `.section-count` monospace chip
104
+
105
+ **Other polish**
106
+ - `html { scroll-behavior: smooth }` for smooth anchor navigation
107
+ - Sidebar active link gets a 2 px accent left border instead of just a background change
108
+ - Card hover adds a subtle box-shadow
109
+ - All `section()` calls updated with count display
110
+ - CSS ~15 KB (up from ~9 KB), app.js ~4 KB (up from ~2.6 KB) — still cached after first load
111
+
112
+ ---
113
+
114
+ ## [1.8.0] - 2026-06-30
115
+
116
+ ### Changed — Performance: Shared Static Assets (Phase A)
117
+
118
+ Previously every generated HTML page inlined the same 9 KB CSS block, 3 KB client JS, and 34 KB search index. On a 9-page site that wasted 630+ KB of duplicate payload.
119
+
120
+ - **CSS extracted** to `assets/style.css` — written once per build, shared by all pages via `<link rel="stylesheet">`.
121
+ - **Client JS extracted** to `assets/app.js` — written once, shared via `<script src>`. Browsers cache it after the first page load.
122
+ - **Search index extracted** to `search-index.js` — written once as `window.__SEARCH_INDEX__=[...]`, loaded before `app.js` via `<script src>`. No more 34 KB inline JSON on every page.
123
+ - `app.js` auto-detects its location (`/modules/` in the path) and adjusts search result URLs at runtime, so a single shared index file works for both the index page and all module pages.
124
+
125
+ ### Result
126
+ | Metric | Before | After |
127
+ |---|---|---|
128
+ | Total site size | 628 KB | 225 KB (−64%) |
129
+ | Per-page HTML (avg) | ~70 KB | ~20 KB |
130
+ | Cache benefit (2nd+ page) | ~70 KB reload | ~20 KB reload |
131
+ | Shared assets (loaded once) | — | 46 KB |
132
+
133
+ ### Upgraded — `buildSite()` output
134
+ The return array now includes three additional entries: `{ path: 'assets/style.css', html: '...' }`, `{ path: 'assets/app.js', html: '...' }`, `{ path: 'search-index.js', html: '...' }`. The CLI handles these automatically. Programmatic API users should use `fs.mkdirSync(path.dirname(dest), { recursive: true })` before writing each file (the README example is updated).
135
+
136
+ ---
137
+
6
138
  ## [1.7.0] - 2026-06-29
7
139
 
8
140
  ### Added
package/README.md CHANGED
@@ -230,7 +230,20 @@ gen-docs src --out docs --title "My Project"
230
230
  # Open docs/index.html in your browser
231
231
  ```
232
232
 
233
- The output is a self-contained static site — no server required.
233
+ The output is a self-contained static site — no server required. Output structure:
234
+
235
+ ```
236
+ docs/
237
+ index.html # project index (module cards)
238
+ search-index.js # shared search index (fetched once)
239
+ assets/
240
+ style.css # shared CSS — cached after first page load
241
+ app.js # shared JS (search, theme toggle, copy)
242
+ modules/
243
+ api.html
244
+ utils.html
245
+ ...
246
+ ```
234
247
 
235
248
  ### CLI flags
236
249
 
@@ -349,13 +362,19 @@ const {
349
362
 
350
363
  ```js
351
364
  const { generateSite } = require('jsdoc-scribe/docs');
365
+ const fs = require('fs'), path = require('path');
352
366
 
353
- generateSite(['src'], {
354
- out: 'docs',
355
- title: 'My Project',
356
- theme: 'default',
357
- sourceUrl: 'https://github.com/org/repo/blob/main',
367
+ const pages = generateSite(['src'], {
368
+ projectName: 'My Project',
369
+ version: '1.0.0',
358
370
  });
371
+
372
+ // pages = [{ path: 'index.html', html }, { path: 'assets/style.css', html }, ...]
373
+ for (const p of pages) {
374
+ const dest = path.join('docs', p.path);
375
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
376
+ fs.writeFileSync(dest, p.html, 'utf8');
377
+ }
359
378
  ```
360
379
 
361
380
  ### Step-by-step
@@ -390,13 +409,15 @@ const pages = buildSite(modules, {
390
409
  sourceUrl: 'https://github.com/org/repo/blob/main',
391
410
  });
392
411
 
393
- // 5. Write pages yourself
412
+ // 5. Write all files
413
+ // pages = [{ path, html }] and includes HTML pages + shared assets:
414
+ // assets/style.css, assets/app.js, search-index.js
415
+ // Always use mkdirSync so assets/ and modules/ are created automatically.
394
416
  const outDir = 'docs';
395
- fs.mkdirSync(outDir, { recursive: true });
396
- for (const [filePath, html] of Object.entries(pages)) {
397
- const dest = path.join(outDir, filePath);
417
+ for (const p of pages) {
418
+ const dest = path.join(outDir, p.path);
398
419
  fs.mkdirSync(path.dirname(dest), { recursive: true });
399
- fs.writeFileSync(dest, html);
420
+ fs.writeFileSync(dest, p.html, 'utf8');
400
421
  }
401
422
  ```
402
423
 
package/lib/docs.js CHANGED
@@ -1,71 +1,57 @@
1
1
  "use strict";
2
2
 
3
- /**
4
- * jsdoc-scribe/docs — programmatic API
5
- *
6
- * Use this when you want to drive jsdoc-scribe from Node code rather than
7
- * the CLI. All lower-level pieces are also exported so you can build a
8
- * custom renderer on top of the same extraction pipeline.
9
- *
10
- * @example
11
- * const { extractModule, buildSite, collectFiles } = require('jsdoc-scribe/docs');
12
- *
13
- * const files = collectFiles('./src');
14
- * const modules = files.map(f => extractModule(f));
15
- * const pages = buildSite(modules, { projectName: 'My API', version: '1.0.0' });
16
- *
17
- * // pages is [{ path: 'index.html', html: '...' }, { path: 'modules/...html', html: '...' }]
18
- * pages.forEach(p => require('fs').writeFileSync(require('path').join('./docs', p.path), p.html));
19
- */
20
-
21
3
  const { collectFiles, DEFAULT_EXTENSIONS, DEFAULT_IGNORE_DIRS } = require("./index.js");
22
4
  const { extractModule } = require("./extractor.js");
23
5
  const { buildSite, moduleLabel, moduleHtmlPath } = require("./renderer.js");
24
6
 
25
7
  /**
26
- * Extract structured documentation from every file in an array.
27
- * Skips files that fail to parse and logs the error to stderr.
28
- * @param {string[]} files - absolute or relative file paths
29
- * @returns {object[]} array of module models (same shape as extractModule())
8
+ * Extract documentation models from an array of file paths.
9
+ * All files are attempted; failures are logged to stderr and omitted from results.
10
+ * Uses Promise.allSettled so a single bad file never aborts the batch.
11
+ *
12
+ * @param {string[]} files
13
+ * @returns {Promise<object[]>}
30
14
  */
31
- function extractModules(files) {
15
+ async function extractModules(files) {
16
+ const results = await Promise.allSettled(
17
+ files.map(f => Promise.resolve().then(() => extractModule(f)))
18
+ );
32
19
  const modules = [];
33
- for (const file of files) {
34
- try {
35
- modules.push(extractModule(file));
36
- } catch (err) {
37
- process.stderr.write(`jsdoc-scribe/docs: skipped ${file} ${err.message}\n`);
20
+ for (let i = 0; i < results.length; i++) {
21
+ const r = results[i];
22
+ if (r.status === "fulfilled") {
23
+ modules.push(r.value);
24
+ } else {
25
+ process.stderr.write("jsdoc-scribe/docs: skipped " + files[i] + ": " + r.reason.message + "\n");
38
26
  }
39
27
  }
40
28
  return modules;
41
29
  }
42
30
 
43
31
  /**
44
- * One-shot: collect files, extract docs, build site, return pages.
45
- * @param {string|string[]} inputPaths - file or directory paths to scan
46
- * @param {{ projectName?: string, version?: string, extensions?: string[], ignoreDirs?: Set<string> }} [options]
47
- * @returns {{ path: string, html: string }[]}
32
+ * One-shot convenience: collect files, extract docs, build HTML site.
33
+ *
34
+ * @param {string|string[]} inputPaths Source file or directory paths
35
+ * @param {object} [options] Same options as buildSite()
36
+ * @returns {Promise<Array<{path:string,html:string}>>}
48
37
  */
49
- function generateSite(inputPaths, options) {
38
+ async function generateSite(inputPaths, options) {
50
39
  const paths = Array.isArray(inputPaths) ? inputPaths : [inputPaths];
51
40
  const opts = options || {};
52
41
  const files = [].concat(...paths.map(p => collectFiles(p, opts.extensions, opts.ignoreDirs)));
53
42
  const unique = [...new Set(files)];
54
- const modules = extractModules(unique);
43
+ const modules = await extractModules(unique);
55
44
  return buildSite(modules, { projectName: opts.projectName, version: opts.version });
56
45
  }
57
46
 
58
47
  module.exports = {
59
- // Core pipeline
60
48
  collectFiles,
61
49
  extractModule,
62
50
  extractModules,
63
51
  buildSite,
64
52
  generateSite,
65
- // Helpers
66
53
  moduleLabel,
67
54
  moduleHtmlPath,
68
- // Constants
69
55
  DEFAULT_EXTENSIONS,
70
56
  DEFAULT_IGNORE_DIRS,
71
57
  };
package/lib/extractor.js CHANGED
@@ -159,6 +159,10 @@ function parseJSDocBlock(raw) {
159
159
  } else if (/^@deprecated\b/.test(line)) {
160
160
  inExample = false;
161
161
  deprecated = line.slice("@deprecated".length).trim() || "";
162
+ } else if (/^@description\b/.test(line)) {
163
+ inExample = false;
164
+ const descText = line.slice("@description".length).trim();
165
+ if (descText) descLines.push(descText);
162
166
  } else if (/^@\w/.test(line)) {
163
167
  inExample = false;
164
168
  } else if (inExample) {
@@ -395,10 +399,15 @@ function extractModule(filePath) {
395
399
  const sourceText = fs.readFileSync(filePath, "utf8");
396
400
  const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(filePath));
397
401
 
402
+ if (!sourceFile) {
403
+ process.stderr.write("jsdoc-scribe: could not parse " + filePath + "\n");
404
+ return { filePath, moduleName: null, description: null, since: null, functions: [], classes: [], interfaces: [], typeAliases: [], enums: [], variables: [] };
405
+ }
398
406
  const moduleDoc = extractModuleDoc(sourceFile);
399
407
  const result = { filePath, moduleName: moduleDoc.moduleName, description: moduleDoc.description, since: moduleDoc.since, functions: [], classes: [], interfaces: [], typeAliases: [], enums: [], variables: [] };
400
408
 
401
409
  function visit(node) {
410
+ try {
402
411
  if (ts.isFunctionDeclaration(node) && node.body && node.name) {
403
412
  result.functions.push(extractFunction(node, sourceFile));
404
413
  } else if (ts.isClassDeclaration(node)) {
@@ -446,6 +455,9 @@ function extractModule(filePath) {
446
455
  }
447
456
  }
448
457
  }
458
+ } catch (err) {
459
+ process.stderr.write("jsdoc-scribe: skipped node in " + filePath + " — " + err.message + "\n");
460
+ }
449
461
  ts.forEachChild(node, visit);
450
462
  }
451
463
 
package/lib/renderer.js CHANGED
@@ -8,7 +8,7 @@ const path = require("path");
8
8
 
9
9
  const THEMES = {
10
10
  default: {
11
- light: `:root{--bg:#f8f9fc;--surface:#fff;--border:#e0e4f0;--text:#1a1a2e;--text2:#666;--text3:#888;--sidebar-bg:#1a1a2e;--sidebar-text:#c8d3f0;--sidebar-active:#2d2d4e;--sidebar-title:#7986cb;--accent:#4361ee;--accent2:#e8eaf6;--search-bg:#2d2d4e;--search-border:#3a3a5e;--search-text:#e0e4f8;--search-panel:#252543;--th-bg:#f5f6fa;--code-bg:#f5f6fa;--sub-label:#7986cb;--method-border:#f0f0f0;--dep-bg:#fff8e1;--dep-text:#e65100;--throws-bg:#fce4ec;--throws-text:#c62828}`,
11
+ light: `:root{--bg:#f8f9fc;--surface:#fff;--border:#e0e4f0;--text:#1a1a2e;--text2:#555;--text3:#888;--sidebar-bg:#1a1a2e;--sidebar-text:#c8d3f0;--sidebar-active:#2d2d4e;--sidebar-title:#7986cb;--accent:#4361ee;--accent2:#e8eaf6;--search-bg:#2d2d4e;--search-border:#3a3a5e;--search-text:#e0e4f8;--search-panel:#252543;--th-bg:#f5f6fa;--code-bg:#f5f6fa;--sub-label:#7986cb;--method-border:#f0f0f0;--dep-bg:#fff8e1;--dep-text:#e65100;--throws-bg:#fce4ec;--throws-text:#c62828}`,
12
12
  dark: `[data-theme=dark]{--bg:#0f0f1a;--surface:#1a1a2e;--border:#2d2d4e;--text:#e0e4f8;--text2:#9aa5c8;--text3:#5a6494;--sidebar-bg:#0a0a14;--sidebar-text:#9aa5c8;--sidebar-active:#1a1a2e;--sidebar-title:#5a6494;--accent:#6b8cff;--accent2:#1a1f3a;--search-bg:#1a1a2e;--search-border:#2d2d4e;--search-text:#c8d3f0;--search-panel:#0f0f1a;--th-bg:#1a1a2e;--code-bg:#1a1a2e;--sub-label:#5a6494;--method-border:#2d2d4e;--dep-bg:#2d2000;--dep-text:#ffb300;--throws-bg:#2d0a14;--throws-text:#ef9a9a}`,
13
13
  toggleBtn: true,
14
14
  },
@@ -25,26 +25,37 @@ const THEMES = {
25
25
  };
26
26
 
27
27
  // ---------------------------------------------------------------------------
28
- // CSS (shared structural rules; colors come from theme vars above)
28
+ // CSS layout, components, responsive, print
29
29
  // ---------------------------------------------------------------------------
30
30
 
31
31
  const CSS_STRUCTURE = `
32
+ /* == Reset & base == */
32
33
  *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
34
+ html{scroll-behavior:smooth}
33
35
  body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;font-size:15px;line-height:1.6;color:var(--text);background:var(--bg);transition:background .2s,color .2s}
34
- a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}
36
+ a{color:var(--accent);text-decoration:none}
37
+ a:hover{text-decoration:underline}
35
38
  code,pre{font-family:'SFMono-Regular',Consolas,'Liberation Mono',Menlo,monospace}
36
- .layout{display:flex;min-height:100vh}
37
- .sidebar{width:272px;min-width:272px;background:var(--sidebar-bg);color:var(--sidebar-text);padding:0;display:flex;flex-direction:column;position:sticky;top:0;height:100vh;overflow-y:auto}
38
- .sidebar-header{padding:18px 20px 12px;display:flex;align-items:flex-start;justify-content:space-between;gap:8px}
39
- .sidebar-header a{color:var(--text);font-size:15px;font-weight:700}
40
- .sidebar-header .version{display:block;font-size:11px;color:var(--sidebar-title);margin-top:2px}
41
- .theme-btn{background:none;border:1px solid var(--search-border);border-radius:5px;padding:3px 8px;font-size:12px;color:var(--sidebar-text);cursor:pointer;white-space:nowrap;flex-shrink:0;margin-top:2px}
39
+
40
+ /* == Three-column layout == */
41
+ .layout{display:grid;grid-template-columns:272px 1fr;min-height:100vh}
42
+ .layout.has-toc{grid-template-columns:272px 1fr 224px}
43
+
44
+ /* == Left sidebar == */
45
+ .sidebar{grid-column:1;background:var(--sidebar-bg);color:var(--sidebar-text);position:sticky;top:0;height:100vh;overflow-y:auto;display:flex;flex-direction:column;border-right:1px solid rgba(255,255,255,.06)}
46
+ .sidebar-header{padding:16px 14px 12px;display:flex;align-items:flex-start;justify-content:space-between;gap:8px;border-bottom:1px solid var(--search-border);flex-shrink:0}
47
+ .sidebar-header a{color:var(--sidebar-text);font-size:14px;font-weight:700;letter-spacing:-.01em}
48
+ .sidebar-header .version{display:block;font-size:11px;color:var(--sidebar-title);margin-top:2px;font-family:monospace}
49
+ .theme-btn{background:none;border:1px solid var(--search-border);border-radius:4px;padding:2px 7px;font-size:11px;color:var(--sidebar-text);cursor:pointer;white-space:nowrap;flex-shrink:0;margin-top:2px}
42
50
  .theme-btn:hover{background:var(--sidebar-active)}
43
- .search-wrap{position:relative;padding:0 14px 12px;border-bottom:1px solid var(--search-border)}
44
- .search-box{width:100%;background:var(--search-bg);border:1px solid var(--search-border);border-radius:6px;padding:7px 10px 7px 32px;color:var(--search-text);font-size:13px;outline:none}
51
+
52
+ /* == Search == */
53
+ .search-wrap{position:relative;padding:10px 12px;border-bottom:1px solid var(--search-border);flex-shrink:0}
54
+ .search-icon{position:absolute;left:20px;top:50%;transform:translateY(-50%);width:14px;height:14px;opacity:.5;pointer-events:none}
55
+ .search-box{width:100%;background:var(--search-bg);border:1px solid var(--search-border);border-radius:6px;padding:6px 10px 6px 30px;color:var(--search-text);font-size:13px;outline:none}
45
56
  .search-box::placeholder{color:var(--sidebar-title)}
46
57
  .search-box:focus{border-color:var(--accent)}
47
- .search-results{display:none;position:absolute;left:14px;right:14px;background:var(--search-panel);border:1px solid var(--search-border);border-radius:6px;max-height:320px;overflow-y:auto;z-index:100;margin-top:2px}
58
+ .search-results{display:none;position:absolute;left:12px;right:12px;background:var(--search-panel);border:1px solid var(--search-border);border-radius:6px;max-height:320px;overflow-y:auto;z-index:100;margin-top:2px;box-shadow:0 4px 20px rgba(0,0,0,.18)}
48
59
  .search-results.visible{display:block}
49
60
  .search-result-item{display:block;padding:8px 12px;cursor:pointer;border-bottom:1px solid var(--search-border);text-decoration:none}
50
61
  .search-result-item:hover{background:var(--sidebar-active)}
@@ -52,31 +63,65 @@ code,pre{font-family:'SFMono-Regular',Consolas,'Liberation Mono',Menlo,monospace
52
63
  .sr-kind{font-size:11px;color:var(--sidebar-title);margin-left:6px}
53
64
  .sr-module{font-size:11px;color:var(--sidebar-title);display:block;margin-top:1px;opacity:.7}
54
65
  .search-no-results{padding:10px 12px;color:var(--sidebar-title);font-size:13px}
55
- .sidebar-section{padding:10px 0 4px}
56
- .sidebar-section-title{padding:4px 20px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--sidebar-title)}
57
- .sidebar-link{display:block;padding:5px 20px;font-size:13px;color:var(--sidebar-text);transition:background .1s}
58
- .sidebar-link:hover,.sidebar-link.active{background:var(--sidebar-active);color:var(--text);text-decoration:none}
59
- .sidebar-dir-toggle{display:flex;align-items:center;gap:4px;cursor:pointer;list-style:none;padding:5px 20px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--sidebar-title);user-select:none}
66
+ .sr-preview{font-size:11px;color:var(--text3);font-style:italic;display:block;margin-top:2px}
67
+
68
+ /* == Sidebar tree == */
69
+ .sidebar-section{padding:4px 0 12px;flex:1;overflow-y:auto}
70
+ .sidebar-section-title{padding:10px 14px 4px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:var(--sidebar-title)}
71
+ .sidebar-link{display:block;padding:4px 14px;font-size:13px;color:var(--sidebar-text);transition:background .1s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
72
+ .sidebar-link:hover{background:var(--sidebar-active);color:var(--text);text-decoration:none}
73
+ .sidebar-link.active{background:var(--sidebar-active);color:var(--text);text-decoration:none;border-left:2px solid var(--accent);padding-left:12px}
74
+ .sidebar-dir-toggle{display:flex;align-items:center;gap:5px;cursor:pointer;list-style:none;padding:4px 14px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--sidebar-title);user-select:none}
60
75
  .sidebar-dir-toggle::-webkit-details-marker{display:none}
61
- .sidebar-dir-toggle::before{content:'▶';font-size:8px;transition:transform .15s}
76
+ .sidebar-dir-toggle::before{content:'▶';font-size:8px;transition:transform .15s;flex-shrink:0}
62
77
  details[open] .sidebar-dir-toggle::before{transform:rotate(90deg)}
63
- .sidebar-link-indent{padding-left:32px}
64
- .main{flex:1;padding:40px 48px;max-width:960px}
65
- .page-title{font-size:28px;font-weight:700;color:var(--text);margin-bottom:4px}
66
- .page-subtitle{color:var(--text2);font-size:13px;margin-bottom:12px}
67
- .module-desc{color:var(--text2);font-size:14px;line-height:1.6;margin-bottom:28px;max-width:700px}
68
- .section{margin-bottom:40px}
69
- .section-title{font-size:18px;font-weight:700;color:var(--text);margin-bottom:14px;padding-bottom:8px;border-bottom:2px solid var(--accent2)}
70
- .card{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:18px 22px;margin-bottom:10px;scroll-margin-top:24px}
71
- .card-header{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}
72
- .card-name{font-size:15px;font-weight:700;color:var(--text);font-family:monospace}
73
- .card-sig{font-size:12px;color:var(--text2);margin-top:3px;font-family:monospace;word-break:break-all}
74
- .card-desc{font-size:13px;color:var(--text2);margin-top:8px;line-height:1.5}
75
- .card-example{margin-top:10px}
76
- .card-example pre{background:var(--code-bg);border:1px solid var(--border);border-radius:6px;padding:10px 14px;font-size:12px;overflow-x:auto;color:var(--text)}
78
+ .sidebar-link-indent{padding-left:26px}
79
+ .sidebar-link-indent.active{padding-left:24px}
80
+
81
+ /* Symbol rows under active module */
82
+ .sym-rows{padding:2px 0 6px}
83
+ .sym-row{display:flex;align-items:center;gap:5px;padding:2px 12px 2px 36px;min-width:0}
84
+ .sym-pill{font-size:9px;font-weight:700;padding:1px 4px;border-radius:3px;font-family:monospace;flex-shrink:0;letter-spacing:.01em;line-height:1.4}
85
+ .sym-fn{background:#1b3a1e;color:#81c784}
86
+ .sym-cls{background:#0d2137;color:#64b5f6}
87
+ .sym-iface{background:#1a0a2e;color:#ce93d8}
88
+ .sym-enum{background:#2d1500;color:#ffb74d}
89
+ .sym-type{background:#00211f;color:#4db6ac}
90
+ .sym-var{background:#1f1f1f;color:#9e9e9e}
91
+ [data-theme=light] .sym-fn,[data-theme=default] .sym-fn,.sym-fn{background:#1b3a1e;color:#81c784}
92
+ .sym-link{font-size:12px;color:var(--sidebar-text);text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;opacity:.85}
93
+ .sym-link:hover{color:var(--text);opacity:1;text-decoration:none}
94
+ .sym-link.active{color:var(--accent);opacity:1;font-weight:600}
95
+
96
+ /* == Main content area == */
97
+ .main{padding:40px 48px 80px;min-width:0;overflow-x:hidden}
98
+ .page-title{font-size:26px;font-weight:700;color:var(--text);margin-bottom:4px;letter-spacing:-.02em}
99
+ .page-subtitle{color:var(--text2);font-size:12px;margin-bottom:16px;font-family:monospace}
100
+ .module-desc{color:var(--text2);font-size:14px;line-height:1.7;margin-bottom:28px;max-width:700px}
101
+ .section{margin-bottom:44px}
102
+ .section-title{font-size:16px;font-weight:700;color:var(--text);margin-bottom:14px;padding-bottom:8px;border-bottom:2px solid var(--accent2);display:flex;align-items:center;gap:8px}
103
+ .section-count{font-size:12px;font-weight:400;color:var(--text3);font-family:monospace}
104
+
105
+ /* == Cards == */
106
+ .card{background:var(--surface);border:1px solid var(--border);border-left:3px solid var(--border);border-radius:8px;padding:18px 20px 16px;margin-bottom:10px;scroll-margin-top:24px;transition:box-shadow .15s,border-left-color .15s}
107
+ .card:hover{box-shadow:0 2px 12px rgba(0,0,0,.08)}
108
+ .card-fn{border-left-color:#43a047}
109
+ .card-cls{border-left-color:#1e88e5}
110
+ .card-iface{border-left-color:#8e24aa}
111
+ .card-enum{border-left-color:#fb8c00}
112
+ .card-type{border-left-color:#00897b}
113
+ .card-var{border-left-color:#757575}
114
+ .card-header{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin-bottom:2px}
115
+ .card-name{font-size:15px;font-weight:700;color:var(--text);font-family:monospace;display:flex;align-items:center;gap:4px;flex-wrap:wrap}
116
+ .card-sig{font-size:12px;color:var(--text2);margin-top:3px;font-family:monospace;word-break:break-all;line-height:1.5}
117
+ .card-desc{font-size:13px;color:var(--text2);margin-top:8px;line-height:1.6}
118
+ .card-example{margin-top:12px}
119
+ .card-example pre{background:var(--code-bg);border:1px solid var(--border);border-radius:6px;padding:12px 16px;font-size:12px;overflow-x:auto;color:var(--text);line-height:1.6;tab-size:2}
77
120
  .copy-btn{flex-shrink:0;background:none;border:1px solid var(--border);border-radius:5px;padding:3px 8px;font-size:11px;color:var(--text3);cursor:pointer;transition:all .15s;white-space:nowrap}
78
121
  .copy-btn:hover{background:var(--accent2);border-color:var(--accent);color:var(--accent)}
79
122
  .copy-btn.copied{background:#e8f5e9;border-color:#43a047;color:#2e7d32}
123
+
124
+ /* == Badges == */
80
125
  .badge{display:inline-block;padding:2px 7px;border-radius:4px;font-size:11px;font-weight:600;margin-right:3px;margin-top:5px}
81
126
  .badge-exported{background:#e8f5e9;color:#2e7d32}
82
127
  .badge-async{background:#e3f2fd;color:#1565c0}
@@ -94,6 +139,8 @@ details[open] .sidebar-dir-toggle::before{transform:rotate(90deg)}
94
139
  .badge-since{background:#e8f5e9;color:#2e7d32}
95
140
  .deprecated-notice{background:var(--dep-bg);color:var(--dep-text);border-radius:5px;padding:6px 12px;font-size:12px;margin-top:8px}
96
141
  .since-label{font-size:11px;color:var(--text3);margin-top:4px}
142
+
143
+ /* == Tables == */
97
144
  .throws-table{width:100%;border-collapse:collapse;margin-top:8px;font-size:13px;border:1px solid var(--throws-bg)}
98
145
  .throws-table th{text-align:left;padding:5px 10px;background:var(--throws-bg);color:var(--throws-text);font-weight:600;font-size:12px}
99
146
  .throws-table td{padding:5px 10px;border-top:1px solid var(--border);color:var(--text);vertical-align:top}
@@ -104,6 +151,8 @@ details[open] .sidebar-dir-toggle::before{transform:rotate(90deg)}
104
151
  .params-table td code{background:var(--code-bg);padding:1px 5px;border-radius:3px;font-size:12px}
105
152
  .returns{margin-top:8px;font-size:13px;color:var(--text2)}
106
153
  .returns code{background:var(--code-bg);padding:1px 6px;border-radius:3px;font-family:monospace}
154
+
155
+ /* == Collapsible sections == */
107
156
  .collapse-toggle{display:flex;align-items:center;gap:6px;cursor:pointer;user-select:none;list-style:none;margin-top:14px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--sub-label);padding:0}
108
157
  .collapse-toggle::-webkit-details-marker{display:none}
109
158
  .collapse-toggle::before{content:'▶';font-size:9px;display:inline-block;transition:transform .15s;color:var(--sub-label)}
@@ -112,30 +161,88 @@ details[open] .collapse-toggle::before{transform:rotate(90deg)}
112
161
  .method-row{margin-top:8px;padding:10px 0;border-top:1px solid var(--method-border)}
113
162
  .method-sig{font-family:monospace;font-size:13px;color:var(--text)}
114
163
  .method-desc{font-size:12px;color:var(--text2);margin-top:4px}
115
- .module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px;margin-top:4px}
116
- .module-card{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:16px 20px;transition:border-color .15s,box-shadow .15s}
117
- .module-card:hover{border-color:var(--accent);box-shadow:0 2px 8px rgba(67,97,238,.1);text-decoration:none}
118
- .module-card-name{font-size:14px;font-weight:700;color:var(--text);font-family:monospace}
119
- .module-card-stats{font-size:12px;color:var(--text3);margin-top:4px}
164
+
165
+ /* == Module index grid == */
166
+ .module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:10px;margin-top:4px}
167
+ .module-card{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:16px 20px;transition:border-color .15s,box-shadow .15s;display:block}
168
+ .module-card:hover{border-color:var(--accent);box-shadow:0 2px 12px rgba(67,97,238,.1);text-decoration:none}
169
+ .module-card-name{font-size:14px;font-weight:700;color:var(--text);font-family:monospace;display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:4px}
170
+ .module-card-stats{font-size:12px;color:var(--text3)}
120
171
  .module-card-desc{font-size:12px;color:var(--text3);margin-top:6px;line-height:1.4}
172
+
173
+ /* == Breadcrumb == */
121
174
  .breadcrumb{font-size:13px;color:var(--text3);margin-bottom:20px}
122
175
  .breadcrumb a{color:var(--accent)}
176
+
177
+ /* == Misc == */
123
178
  .anchor-link{color:var(--sidebar-title);opacity:0;font-size:13px;margin-left:6px;transition:opacity .15s}
124
179
  .card:hover .anchor-link{opacity:.6}
125
180
  .anchor-link:hover{opacity:1;text-decoration:none}
126
181
  .empty{color:var(--text3);font-size:13px;font-style:italic}
127
182
  .link-ref{color:var(--accent);text-decoration:none;font-size:inherit}
128
183
  .link-ref:hover{text-decoration:underline}
129
- .sr-body{font-size:11px;color:var(--sidebar-title);display:block;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:220px}
130
- .sr-preview{font-size:11px;color:var(--text3);font-style:italic;display:block;margin-top:2px}
131
-
132
184
  .source-link{font-size:11px;color:var(--text3);font-family:monospace;text-decoration:none;margin-left:6px;opacity:.7}
133
185
  .source-link:hover{opacity:1;text-decoration:underline;color:var(--accent)}
134
186
 
187
+ /* == Right TOC == */
188
+ .toc{grid-column:3;position:sticky;top:0;height:100vh;overflow-y:auto;padding:40px 16px 40px 20px;border-left:1px solid var(--border);background:var(--bg)}
189
+ .toc-title{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:var(--text3);margin-bottom:14px}
190
+ .toc-section{margin-bottom:14px}
191
+ .toc-section-label{font-size:11px;font-weight:600;color:var(--text2);margin-bottom:4px;padding-bottom:3px;border-bottom:1px solid var(--border)}
192
+ .toc-item{display:block;font-size:12px;color:var(--text3);text-decoration:none;padding:3px 0 3px 8px;border-left:2px solid transparent;transition:color .1s,border-left-color .1s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.5}
193
+ .toc-item:hover{color:var(--text);border-left-color:var(--border);text-decoration:none}
194
+ .toc-item.active{color:var(--accent);border-left-color:var(--accent);font-weight:600}
195
+ .toc-dep{font-size:9px;color:var(--dep-text);margin-left:3px;font-weight:700;vertical-align:middle}
196
+
197
+ /* == Hamburger (mobile only) == */
198
+ .hamburger{display:none;flex-direction:column;gap:5px;background:var(--sidebar-bg);border:none;padding:8px 10px;cursor:pointer;position:fixed;top:10px;left:10px;z-index:300;border-radius:6px;box-shadow:0 2px 8px rgba(0,0,0,.2)}
199
+ .hamburger span{width:20px;height:2px;background:var(--sidebar-text);border-radius:2px;display:block;transition:all .2s}
200
+ .hamburger.open span:nth-child(1){transform:translateY(7px) rotate(45deg)}
201
+ .hamburger.open span:nth-child(2){opacity:0}
202
+ .hamburger.open span:nth-child(3){transform:translateY(-7px) rotate(-45deg)}
203
+
204
+ /* == Code token highlighting == */
205
+ .tok-kw{color:#6b8cff;font-weight:600}
206
+ .tok-str{color:#2e7d32}
207
+ .tok-cmt{color:#888;font-style:italic}
208
+ .tok-num{color:#e65100}
209
+ .tok-type{color:#6a1b9a}
210
+ [data-theme=dark] .tok-kw{color:#89b4ff}
211
+ [data-theme=dark] .tok-str{color:#a8d5a2}
212
+ [data-theme=dark] .tok-cmt{color:#5a6494}
213
+ [data-theme=dark] .tok-num{color:#ffb74d}
214
+ [data-theme=dark] .tok-type{color:#ce93d8}
215
+
216
+ /* == Responsive == */
217
+ @media (max-width:1280px){
218
+ .toc{display:none}
219
+ .layout.has-toc{grid-template-columns:272px 1fr}
220
+ }
221
+ @media (max-width:860px){
222
+ .layout,.layout.has-toc{grid-template-columns:1fr}
223
+ .sidebar{position:fixed;top:0;left:0;height:100vh;z-index:200;transform:translateX(-100%);transition:transform .25s ease}
224
+ .sidebar.open{transform:translateX(0);box-shadow:4px 0 24px rgba(0,0,0,.28)}
225
+ .hamburger{display:flex}
226
+ .main{padding:56px 20px 60px}
227
+ }
228
+
229
+ /* == Print == */
230
+ @media print{
231
+ .sidebar,.toc,.hamburger,.copy-btn,.theme-btn,.search-wrap,.anchor-link{display:none!important}
232
+ .layout,.layout.has-toc{grid-template-columns:1fr!important;display:block}
233
+ .main{padding:0}
234
+ .card{break-inside:avoid;border:1px solid #ccc;border-left:3px solid #999!important;margin-bottom:16px;box-shadow:none}
235
+ }
135
236
  `;
136
237
 
238
+ // ---------------------------------------------------------------------------
239
+ // App JS (written to assets/app.js)
240
+ // ---------------------------------------------------------------------------
241
+
137
242
  const CLIENT_JS = `
138
243
  (function(){
244
+ var _pfx=(window.location.pathname.replace(/\\/g,'/').indexOf('/modules/')!==-1)?'../':'';
245
+ // Theme
139
246
  var THEME_KEY='jsdoc-scribe-theme';
140
247
  var saved=localStorage.getItem(THEME_KEY);
141
248
  if(saved) document.documentElement.setAttribute('data-theme',saved);
@@ -150,6 +257,7 @@ const CLIENT_JS = `
150
257
  btn.textContent=next==='dark'?'Light':'Dark';
151
258
  });
152
259
  }
260
+ // Copy button
153
261
  document.addEventListener('click',function(e){
154
262
  var b=e.target.closest('.copy-btn');
155
263
  if(!b) return;
@@ -158,33 +266,66 @@ const CLIENT_JS = `
158
266
  setTimeout(function(){b.textContent='Copy';b.classList.remove('copied');},1500);
159
267
  });
160
268
  });
161
- var INDEX=window.__SEARCH_INDEX__||[];
269
+ // Search
270
+ var INDEX=(window.__SEARCH_INDEX__||[]).map(function(r){return{name:r.name,kind:r.kind,module:r.module,body:r.body,url:_pfx+r.url};});
162
271
  var box=document.getElementById('search-box');
163
272
  var panel=document.getElementById('search-results');
164
- if(!box||!panel) return;
165
- function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
166
- function render(items){
167
- panel.innerHTML=items.length
168
- ?items.slice(0,20).map(function(r){return'<a class="search-result-item" href="'+r.url+'"><span class="sr-name">'+esc(r.name)+'</span><span class="sr-kind">'+esc(r.kind)+'</span><span class="sr-module">'+esc(r.module)+'</span>+(r.body?'<span class="sr-preview">'+esc(r.body.slice(0,80))+'</span>':'')+'</a>';}).join('')
169
- :'<div class="search-no-results">No results</div>';
170
- panel.classList.add('visible');
273
+ if(box&&panel){
274
+ function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
275
+ function render(items){
276
+ panel.innerHTML=items.length
277
+ ?items.slice(0,20).map(function(r){return'<a class="search-result-item" href="'+r.url+'"><span class="sr-name">'+esc(r.name)+'</span><span class="sr-kind">'+esc(r.kind)+'</span><span class="sr-module">'+esc(r.module)+'</span>'+(r.body?'<span class="sr-preview">'+esc(r.body.slice(0,80))+'</span>':'')+'</a>';}).join('')
278
+ :'<div class="search-no-results">No results</div>';
279
+ panel.classList.add('visible');
280
+ }
281
+ function search(q){
282
+ q=q.trim().toLowerCase();
283
+ if(!q){panel.classList.remove('visible');return;}
284
+ render(INDEX.filter(function(r){
285
+ return r.name.toLowerCase().includes(q)||r.module.toLowerCase().includes(q)||(r.body&&r.body.toLowerCase().includes(q));
286
+ }));
287
+ }
288
+ box.addEventListener('input',function(){search(box.value);});
289
+ box.addEventListener('focus',function(){if(box.value.trim())search(box.value);});
290
+ document.addEventListener('click',function(e){if(!box.contains(e.target)&&!panel.contains(e.target))panel.classList.remove('visible');});
291
+ document.addEventListener('keydown',function(e){
292
+ if((e.metaKey||e.ctrlKey)&&e.key==='k'){e.preventDefault();box.focus();box.select();}
293
+ if(e.key==='Escape')panel.classList.remove('visible');
294
+ });
171
295
  }
172
- function search(q){
173
- q=q.trim().toLowerCase();
174
- if(!q){panel.classList.remove('visible');return;}
175
- render(INDEX.filter(function(r){
176
- return r.name.toLowerCase().includes(q)
177
- ||r.module.toLowerCase().includes(q)
178
- ||(r.body&&r.body.toLowerCase().includes(q));
179
- }));
296
+ // Right TOC scroll spy via IntersectionObserver
297
+ var toc=document.getElementById('toc');
298
+ if(toc&&typeof IntersectionObserver!=='undefined'){
299
+ var cards=document.querySelectorAll('.card[id]');
300
+ var tocLinks={};
301
+ toc.querySelectorAll('[data-anchor]').forEach(function(a){tocLinks[a.dataset.anchor]=a;});
302
+ var current='';
303
+ var obs=new IntersectionObserver(function(entries){
304
+ entries.forEach(function(e){
305
+ if(e.isIntersecting){
306
+ if(current&&tocLinks[current])tocLinks[current].classList.remove('active');
307
+ current=e.target.id;
308
+ if(tocLinks[current])tocLinks[current].classList.add('active');
309
+ }
310
+ });
311
+ },{rootMargin:'-10% 0px -70% 0px',threshold:0});
312
+ cards.forEach(function(c){obs.observe(c);});
313
+ }
314
+ // Mobile hamburger
315
+ var hamburger=document.getElementById('hamburger');
316
+ var sidebar=document.querySelector('.sidebar');
317
+ if(hamburger&&sidebar){
318
+ hamburger.addEventListener('click',function(){
319
+ var open=sidebar.classList.toggle('open');
320
+ hamburger.classList.toggle('open',open);
321
+ });
322
+ document.addEventListener('click',function(e){
323
+ if(sidebar.classList.contains('open')&&!sidebar.contains(e.target)&&!hamburger.contains(e.target)){
324
+ sidebar.classList.remove('open');
325
+ hamburger.classList.remove('open');
326
+ }
327
+ });
180
328
  }
181
- box.addEventListener('input',function(){search(box.value);});
182
- box.addEventListener('focus',function(){if(box.value.trim())search(box.value);});
183
- document.addEventListener('click',function(e){if(!box.contains(e.target)&&!panel.contains(e.target))panel.classList.remove('visible');});
184
- document.addEventListener('keydown',function(e){
185
- if((e.metaKey||e.ctrlKey)&&e.key==='k'){e.preventDefault();box.focus();box.select();}
186
- if(e.key==='Escape')panel.classList.remove('visible');
187
- });
188
329
  })();
189
330
  `;
190
331
 
@@ -204,17 +345,44 @@ function metaHtml(item){
204
345
  return out;
205
346
  }
206
347
 
348
+ /**
349
+ * Server-side tokenizer for JS/TS @example blocks.
350
+ * Returns HTML with tok-* spans. Processes strings, comments, numbers, keywords.
351
+ */
352
+ function highlightCode(raw){
353
+ if(!raw) return '';
354
+ var out='';
355
+ var KW=/\b(function|class|const|let|var|return|new|if|else|for|while|import|export|from|async|await|try|catch|throw|extends|implements|type|interface|enum|this|super|null|undefined|true|false|typeof|instanceof|of|in|do|switch|case|default|break|continue|static|readonly|abstract|private|protected|public|override|declare|namespace|as|satisfies)\b/g;
356
+ var re=/(\/\/[^\n]*)|(\/\*[\s\S]*?\*\/)|(["'`])(?:(?!\3)[^\\]|\\.)*\3|\b(\d+\.?\d*(?:[eE][+-]?\d+)?)\b/g;
357
+ var last=0;
358
+ var m;
359
+ while((m=re.exec(raw))!==null){
360
+ if(m.index>last){
361
+ // Tokenize the plain segment for keywords
362
+ out+=esc(raw.slice(last,m.index)).replace(KW,function(kw){return'<span class="tok-kw">'+kw+'</span>';});
363
+ }
364
+ if(m[1]||m[2]) out+='<span class="tok-cmt">'+esc(m[0])+'</span>';
365
+ else if(m[3]) out+='<span class="tok-str">'+esc(m[0])+'</span>';
366
+ else if(m[4]) out+='<span class="tok-num">'+esc(m[0])+'</span>';
367
+ else out+=esc(m[0]);
368
+ last=m.index+m[0].length;
369
+ }
370
+ if(last<raw.length){
371
+ out+=esc(raw.slice(last)).replace(KW,function(kw){return'<span class="tok-kw">'+kw+'</span>';});
372
+ }
373
+ return out;
374
+ }
375
+
207
376
  function descHtml(item,symbolMap,filePath){
208
377
  var out='';
209
378
  if(item.description) out+='<div class="card-desc">'+(symbolMap?resolveLinks(item.description,symbolMap,filePath):esc(item.description))+'</div>';
210
379
  out+=metaHtml(item);
211
- if(item.example) out+='<div class="card-example"><pre>'+esc(item.example)+'</pre></div>';
380
+ if(item.example) out+='<div class="card-example"><pre>'+highlightCode(item.example)+'</pre></div>';
212
381
  return out;
213
382
  }
214
383
 
215
384
  function renderParams(params, jsdocParams){
216
385
  if(!params||!params.length) return '';
217
- // Build a lookup of JSDoc @param enrichments keyed by name
218
386
  var jmap={};
219
387
  (jsdocParams||[]).forEach(function(p){jmap[p.name]=p;});
220
388
  var html='<table class="params-table"><thead><tr><th>Parameter</th><th>Type</th><th>Optional</th><th>Description</th></tr></thead><tbody>';
@@ -228,7 +396,7 @@ function renderParams(params, jsdocParams){
228
396
 
229
397
  function renderReturns(returnType, returnsTag){
230
398
  var type=returnsTag&&returnsTag.type&&returnsTag.type!=='any'?returnsTag.type:returnType;
231
- var desc=returnsTag&&returnsTag.description?' '+esc(returnsTag.description):'';
399
+ var desc=returnsTag&&returnsTag.description?'&mdash; '+esc(returnsTag.description):'';
232
400
  return'<div class="returns">Returns: <code>'+esc(type)+'</code> '+desc+'</div>';
233
401
  }
234
402
 
@@ -241,14 +409,12 @@ function renderThrows(throws){
241
409
 
242
410
  function resolveLinks(text, symbolMap, filePath, moduleHtmlPathFn, modules){
243
411
  if(!text||!symbolMap) return esc(text);
244
- // Replace {@link Symbol} and {@link Symbol#method} with anchor tags
245
412
  return esc(text).replace(/\{@link ([^}]+)\}/g, function(_, ref){
246
413
  var parts=ref.trim().split('#');
247
414
  var sym=parts[0].trim();
248
415
  var method=parts[1]?parts[1].trim():null;
249
416
  var entry=symbolMap[sym];
250
417
  if(!entry) return'<code>'+esc(ref)+'</code>';
251
- // Are we on the same module page?
252
418
  var targetPath=entry.modulePath;
253
419
  var href=targetPath===filePath?'':(targetPath||'');
254
420
  if(method) href+='#meth-'+sym+'_'+method;
@@ -278,10 +444,10 @@ function buildCss(theme){
278
444
  return t.light+(t.dark||'')+CSS_STRUCTURE;
279
445
  }
280
446
 
281
- function page(title,sidebarHtml,bodyHtml,searchIndex,theme){
282
- var t=THEMES[theme]||THEMES.default;
283
- var themeJs=t.toggleBtn?CLIENT_JS:'(function(){var INDEX=window.__SEARCH_INDEX__||[];var box=document.getElementById("search-box");var panel=document.getElementById("search-results");if(!box||!panel)return;function esc(s){return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");}function render(items){panel.innerHTML=items.length?items.slice(0,20).map(function(r){return\'<a class="search-result-item" href="\'+r.url+\'"><span class="sr-name">\'+esc(r.name)+\'</span><span class="sr-kind">\'+esc(r.kind)+\'</span><span class="sr-module">\'+esc(r.module)+\'</span></a>\';}).join(""):\'<div class="search-no-results">No results</div>\';panel.classList.add("visible");}function search(q){q=q.trim().toLowerCase();if(!q){panel.classList.remove("visible");return;}render(INDEX.filter(function(r){return r.name.toLowerCase().includes(q)||r.module.toLowerCase().includes(q);}));}box.addEventListener("input",function(){search(box.value);});box.addEventListener("focus",function(){if(box.value.trim())search(box.value);});document.addEventListener("click",function(e){if(!box.contains(e.target)&&!panel.contains(e.target))panel.classList.remove("visible");});document.addEventListener("keydown",function(e){if((e.metaKey||e.ctrlKey)&&e.key==="k"){e.preventDefault();box.focus();box.select();}if(e.key==="Escape")panel.classList.remove("visible");});document.addEventListener("click",function(e){var b=e.target.closest(".copy-btn");if(!b)return;navigator.clipboard.writeText(b.dataset.sig||"").then(function(){b.textContent="Copied!";b.classList.add("copied");setTimeout(function(){b.textContent="Copy";b.classList.remove("copied");},1500);});});})();';
284
- return'<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width,initial-scale=1">\n<title>'+esc(title)+'</title>\n<style>'+buildCss(theme)+'</style>\n</head>\n<body>\n<div class="layout">\n<nav class="sidebar">'+sidebarHtml+'</nav>\n<main class="main">'+bodyHtml+'</main>\n</div>\n<script>window.__SEARCH_INDEX__='+JSON.stringify(searchIndex)+';</script>\n<script>'+themeJs+'</script>\n</body>\n</html>';
447
+ function page(title,sidebarHtml,bodyHtml,theme,assetPrefix,tocHtml){
448
+ var p=assetPrefix||'';
449
+ var hasToc=!!tocHtml;
450
+ return'<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width,initial-scale=1">\n<title>'+esc(title)+'</title>\n<link rel="stylesheet" href="'+p+'assets/style.css">\n</head>\n<body>\n<button id="hamburger" class="hamburger" aria-label="Menu"><span></span><span></span><span></span></button>\n<div class="layout'+(hasToc?' has-toc':'')+'">\n<nav class="sidebar">'+sidebarHtml+'</nav>\n<main class="main">'+bodyHtml+'</main>\n'+(hasToc?'<aside class="toc" id="toc">'+tocHtml+'</aside>\n':'')+'</div>\n<script src="'+p+'search-index.js"></script>\n<script src="'+p+'assets/app.js"></script>\n</body>\n</html>';
285
451
  }
286
452
 
287
453
  // ---------------------------------------------------------------------------
@@ -341,10 +507,10 @@ function buildSearchIndex(modules,prefix){
341
507
  }
342
508
 
343
509
  // ---------------------------------------------------------------------------
344
- // Sidebar
510
+ // Sidebar (with symbol tree under active module)
345
511
  // ---------------------------------------------------------------------------
346
512
 
347
- function buildSidebar(modules,projectName,version,activePath,rootPrefix,showToggle){
513
+ function buildSidebar(modules,projectName,version,activePath,rootPrefix,showToggle,activeModule){
348
514
  var prefix=rootPrefix||'';
349
515
  var html='<div class="sidebar-header">'
350
516
  +'<div><a href="'+prefix+'index.html">'+esc(projectName)+'</a>'+(version?'<span class="version">v'+esc(version)+'</span>':'')+'</div>'
@@ -363,6 +529,26 @@ function buildSidebar(modules,projectName,version,activePath,rootPrefix,showTogg
363
529
  });
364
530
  var hasGroups=order.some(function(d){return d!=='';});
365
531
 
532
+ function symRows(mod){
533
+ var rows='<div class="sym-rows">';
534
+ var specs=[
535
+ {list:mod.functions,kind:'fn',label:'fn'},
536
+ {list:mod.classes,kind:'cls',label:'cls'},
537
+ {list:mod.interfaces,kind:'iface',label:'if'},
538
+ {list:mod.typeAliases,kind:'type',label:'ty'},
539
+ {list:mod.enums,kind:'enum',label:'en'},
540
+ {list:mod.variables,kind:'var',label:'$'},
541
+ ];
542
+ specs.forEach(function(s){
543
+ (s.list||[]).forEach(function(item){
544
+ var anchor=anchorId(s.kind,item.name);
545
+ rows+='<div class="sym-row"><span class="sym-pill sym-'+s.kind+'">'+s.label+'</span>'
546
+ +'<a class="sym-link" href="#'+anchor+'">'+esc(item.name)+'</a></div>';
547
+ });
548
+ });
549
+ return rows+'</div>';
550
+ }
551
+
366
552
  order.forEach(function(dir){
367
553
  var mods=groups[dir];
368
554
  if(hasGroups&&dir){
@@ -372,8 +558,9 @@ function buildSidebar(modules,projectName,version,activePath,rootPrefix,showTogg
372
558
  var rel=moduleHtmlPath(mod.filePath,modules);
373
559
  var label=moduleLabel(mod.filePath,modules);
374
560
  var name=label.slice(dir.length+1);
375
- var active=activePath===rel?' active':'';
376
- html+='<a class="sidebar-link sidebar-link-indent'+active+'" href="'+esc(prefix+rel)+'">'+esc(name)+'</a>';
561
+ var isActive=activePath===rel;
562
+ html+='<a class="sidebar-link sidebar-link-indent'+(isActive?' active':'')+'" href="'+esc(prefix+rel)+'">'+esc(name)+'</a>';
563
+ if(isActive&&activeModule) html+=symRows(activeModule);
377
564
  });
378
565
  html+='</details>';
379
566
  } else {
@@ -381,14 +568,42 @@ function buildSidebar(modules,projectName,version,activePath,rootPrefix,showTogg
381
568
  var rel=moduleHtmlPath(mod.filePath,modules);
382
569
  var label=moduleLabel(mod.filePath,modules);
383
570
  var name=dir?label.slice(dir.length+1):label;
384
- var active=activePath===rel?' active':'';
385
- html+='<a class="sidebar-link'+active+'" href="'+esc(prefix+rel)+'">'+esc(name)+'</a>';
571
+ var isActive=activePath===rel;
572
+ html+='<a class="sidebar-link'+(isActive?' active':'')+'" href="'+esc(prefix+rel)+'">'+esc(name)+'</a>';
573
+ if(isActive&&activeModule) html+=symRows(activeModule);
386
574
  });
387
575
  }
388
576
  });
389
577
  return html+'</div>';
390
578
  }
391
579
 
580
+ // ---------------------------------------------------------------------------
581
+ // Right TOC builder
582
+ // ---------------------------------------------------------------------------
583
+
584
+ function buildToc(mod){
585
+ var specs=[
586
+ {title:'Functions',list:mod.functions,kind:'fn'},
587
+ {title:'Classes',list:mod.classes,kind:'cls'},
588
+ {title:'Interfaces',list:mod.interfaces,kind:'iface'},
589
+ {title:'Type Aliases',list:mod.typeAliases,kind:'type'},
590
+ {title:'Enums',list:mod.enums,kind:'enum'},
591
+ {title:'Variables',list:mod.variables,kind:'var'},
592
+ ].filter(function(s){return s.list&&s.list.length;});
593
+ if(!specs.length) return '';
594
+ var html='<div class="toc-title">On this page</div>';
595
+ specs.forEach(function(s){
596
+ html+='<div class="toc-section"><div class="toc-section-label">'+esc(s.title)+'</div>';
597
+ s.list.forEach(function(item){
598
+ var anchor=anchorId(s.kind,item.name);
599
+ var dep=item.deprecated!=null?'<span class="toc-dep">dep</span>':'';
600
+ html+='<a class="toc-item" href="#'+anchor+'" data-anchor="'+anchor+'">'+esc(item.name)+dep+'</a>';
601
+ });
602
+ html+='</div>';
603
+ });
604
+ return html;
605
+ }
606
+
392
607
  // ---------------------------------------------------------------------------
393
608
  // Item renderers
394
609
  // ---------------------------------------------------------------------------
@@ -398,7 +613,7 @@ function renderFunction(fn,filePath,sourceUrl,symbolMap){
398
613
  var sig=fn.name+'('+paramStr+'): '+fn.returnType;
399
614
  var id=anchorId('fn',fn.name);
400
615
  var badges=[fn.isExported&&badge('exported','exported'),fn.isAsync&&badge('async','async'),fn.isGenerator&&badge('generator','generator'),fn.deprecated!=null&&badge('deprecated','deprecated')].filter(Boolean).join('');
401
- return'<div class="card" id="'+id+'">'
616
+ return'<div class="card card-fn" id="'+id+'">'
402
617
  +'<div class="card-header"><div><div class="card-name"><a class="anchor-link" href="#'+id+'">#</a>'+esc(fn.name)+sourceLink(fn,filePath,sourceUrl)+'</div><div class="card-sig">'+esc(sig)+'</div></div>'+copyBtn(sig)+'</div>'
403
618
  +'<div>'+badges+'</div>'+descHtml(fn,symbolMap,filePath)
404
619
  +renderParams(fn.params,fn.jsdocParams)
@@ -410,7 +625,7 @@ function renderFunction(fn,filePath,sourceUrl,symbolMap){
410
625
  function renderClass(cls,filePath,sourceUrl,symbolMap){
411
626
  var id=anchorId('cls',cls.name);
412
627
  var badges=[cls.isExported&&badge('exported','exported'),cls.isAbstract&&badge('abstract','abstract'),cls.extends.length&&badge('extends '+cls.extends.join(', '),'exported'),cls.implements.length&&badge('implements '+cls.implements.join(', '),'async'),cls.deprecated!=null&&badge('deprecated','deprecated')].filter(Boolean).join('');
413
- var inner='<div class="card" id="'+id+'">'
628
+ var inner='<div class="card card-cls" id="'+id+'">'
414
629
  +'<div class="card-header"><div><div class="card-name"><a class="anchor-link" href="#'+id+'">#</a>'+esc(cls.name)+sourceLink(cls,filePath,sourceUrl)+'</div></div>'
415
630
  +copyBtn('class '+cls.name+(cls.extends.length?' extends '+cls.extends.join(', '):''))+'</div>'
416
631
  +'<div>'+badges+'</div>'+descHtml(cls,symbolMap,filePath);
@@ -449,7 +664,7 @@ function renderClass(cls,filePath,sourceUrl,symbolMap){
449
664
 
450
665
  function renderInterface(iface,filePath,sourceUrl,symbolMap){
451
666
  var id=anchorId('iface',iface.name);
452
- var html='<div class="card" id="'+id+'">'
667
+ var html='<div class="card card-iface" id="'+id+'">'
453
668
  +'<div class="card-header"><div><div class="card-name"><a class="anchor-link" href="#'+id+'">#</a>'+esc(iface.name)+sourceLink(iface,filePath,sourceUrl)+'</div></div>'+copyBtn('interface '+iface.name)+'</div>'
454
669
  +(iface.isExported?badge('exported','exported'):'')+descHtml(iface,symbolMap,filePath);
455
670
  if(iface.properties.length){
@@ -467,7 +682,7 @@ function renderInterface(iface,filePath,sourceUrl,symbolMap){
467
682
 
468
683
  function renderEnum(enm,filePath,sourceUrl,symbolMap){
469
684
  var id=anchorId('enum',enm.name);
470
- var html='<div class="card" id="'+id+'">'
685
+ var html='<div class="card card-enum" id="'+id+'">'
471
686
  +'<div class="card-header"><div><div class="card-name"><a class="anchor-link" href="#'+id+'">#</a>'+esc(enm.name)+sourceLink(enm,filePath,sourceUrl)+'</div></div>'+copyBtn('enum '+enm.name)+'</div>'
472
687
  +(enm.isExported?badge('exported','exported'):'')+descHtml(enm,symbolMap,filePath)
473
688
  +'<table class="params-table" style="margin-top:10px"><thead><tr><th>Member</th><th>Value</th></tr></thead><tbody>';
@@ -477,7 +692,7 @@ function renderEnum(enm,filePath,sourceUrl,symbolMap){
477
692
 
478
693
  function renderTypeAlias(ta,filePath,sourceUrl,symbolMap){
479
694
  var id=anchorId('type',ta.name);
480
- return'<div class="card" id="'+id+'">'
695
+ return'<div class="card card-type" id="'+id+'">'
481
696
  +'<div class="card-header"><div><div class="card-name"><a class="anchor-link" href="#'+id+'">#</a>'+esc(ta.name)+sourceLink(ta,filePath,sourceUrl)+'</div><div class="card-sig">type '+esc(ta.name)+' = '+esc(ta.type)+'</div></div>'+copyBtn('type '+ta.name+' = '+ta.type)+'</div>'
482
697
  +(ta.isExported?badge('exported','exported'):'')+descHtml(ta,symbolMap,filePath)+'</div>';
483
698
  }
@@ -485,91 +700,132 @@ function renderTypeAlias(ta,filePath,sourceUrl,symbolMap){
485
700
  function renderVariable(v,filePath,sourceUrl,symbolMap){
486
701
  var id=anchorId('var',v.name);
487
702
  var decl=(v.isConst?'const':'let')+' '+v.name+': '+v.type;
488
- return'<div class="card" id="'+id+'">'
703
+ return'<div class="card card-var" id="'+id+'">'
489
704
  +'<div class="card-header"><div><div class="card-name"><a class="anchor-link" href="#'+id+'">#</a>'+esc(v.name)+sourceLink(v,filePath,sourceUrl)+'</div><div class="card-sig">'+esc(decl)+'</div></div>'+copyBtn(decl)+'</div>'
490
705
  +(v.isExported?badge('exported','exported'):'')+badge(v.isConst?'const':'var',v.isConst?'const':'var')+(v.deprecated!=null?badge('deprecated','deprecated'):'')+descHtml(v,symbolMap,filePath)+'</div>';
491
706
  }
492
707
 
493
708
  function section(title,items,renderFn,filePath,sourceUrl,symbolMap){
494
709
  if(!items||!items.length) return '';
495
- return'<div class="section"><div class="section-title">'+esc(title)+'</div>'+items.map(function(item){return renderFn(item,filePath,sourceUrl,symbolMap);}).join('\n')+'</div>';
710
+ var count='<span class="section-count">('+items.length+')</span>';
711
+ return'<div class="section"><div class="section-title">'+esc(title)+count+'</div>'+items.map(function(item){return renderFn(item,filePath,sourceUrl,symbolMap);}).join('\n')+'</div>';
496
712
  }
497
713
 
498
714
  // ---------------------------------------------------------------------------
499
- // Site builder
715
+ // Site builder — helpers
500
716
  // ---------------------------------------------------------------------------
501
717
 
502
- function buildSite(modules,options){
503
- options=options||{};
504
- var projectName=options.projectName||'Documentation';
505
- var version=options.version||'';
506
- var theme=options.theme||'default';
507
- var sourceUrl=options.sourceUrl||null;
508
- // Build a symbol → {anchorId, modulePath} map for @link resolution
509
- var symbolMap={};
718
+ /** Build the symbol map for @link cross-reference resolution. */
719
+ function buildSymbolMap(modules){
720
+ var map={};
510
721
  modules.forEach(function(mod){
511
722
  var rel=moduleHtmlPath(mod.filePath,modules);
512
- function reg(name,aid){symbolMap[name]={anchorId:aid,modulePath:rel};}
723
+ function reg(name,aid){map[name]={anchorId:aid,modulePath:rel};}
513
724
  mod.functions.forEach(function(f){reg(f.name,anchorId('fn',f.name));});
514
- mod.classes.forEach(function(c){reg(c.name,anchorId('cls',c.name));c.methods.forEach(function(m){reg(c.name+'.'+m.name,anchorId('cls',c.name));});});
725
+ mod.classes.forEach(function(c){
726
+ reg(c.name,anchorId('cls',c.name));
727
+ c.methods.forEach(function(m){reg(c.name+'.'+m.name,anchorId('cls',c.name));});
728
+ });
515
729
  mod.interfaces.forEach(function(i){reg(i.name,anchorId('iface',i.name));});
516
730
  mod.typeAliases.forEach(function(t){reg(t.name,anchorId('type',t.name));});
517
731
  mod.enums.forEach(function(e){reg(e.name,anchorId('enum',e.name));});
518
732
  mod.variables.forEach(function(v){reg(v.name,anchorId('var',v.name));});
519
733
  });
734
+ return map;
735
+ }
736
+
737
+ /** Build the module-grid HTML for the index page. */
738
+ function buildIndexBody(modules){
739
+ var body='<div class="section"><div class="section-title">Modules</div><div class="module-grid">';
740
+ modules.forEach(function(mod){
741
+ var rel=moduleHtmlPath(mod.filePath,modules);
742
+ var label=moduleLabel(mod.filePath,modules);
743
+ var parts=[
744
+ mod.functions.length&&mod.functions.length+' fn',
745
+ mod.classes.length&&mod.classes.length+' class',
746
+ mod.interfaces.length&&mod.interfaces.length+' iface',
747
+ mod.enums.length&&mod.enums.length+' enum',
748
+ mod.variables.length&&mod.variables.length+' const',
749
+ ].filter(Boolean);
750
+ var allItems=[].concat(mod.functions,mod.classes,mod.interfaces,mod.typeAliases,mod.enums,mod.variables);
751
+ var depCount=allItems.filter(function(i){return i.deprecated!=null;}).length;
752
+ var sinces=allItems.map(function(i){return i.since;}).filter(Boolean).sort();
753
+ var sinceStr=sinces.length
754
+ ? (' · since v'+sinces[0]+(sinces.length>1&&sinces[sinces.length-1]!==sinces[0]?'–v'+sinces[sinces.length-1]:''))
755
+ : '';
756
+ var depBadge=depCount?'<span class="badge badge-deprecated" style="font-size:10px;padding:1px 5px">'+depCount+' dep</span>':'';
757
+ var descHtml=mod.description?'<div class="module-card-desc">'+esc(mod.description.slice(0,100))+(mod.description.length>100?'…':'')+'</div>':'';
758
+ body+='<a class="module-card" href="'+esc(rel)+'">';
759
+ body+='<div class="module-card-name">'+esc(label)+depBadge+'</div>';
760
+ body+='<div class="module-card-stats">'+(parts.join(' · ')||'no exported items')+esc(sinceStr)+'</div>';
761
+ body+=descHtml+'</a>';
762
+ });
763
+ return body+'</div></div>';
764
+ }
765
+
766
+ /** Build a single module page body (sections only, no header). */
767
+ function buildModuleBody(mod,sourceUrl,symbolMap){
768
+ var isEmpty=!mod.functions.length&&!mod.classes.length&&!mod.interfaces.length
769
+ &&!mod.typeAliases.length&&!mod.enums.length&&!mod.variables.length;
770
+ if(isEmpty) return '<p class="empty" style="margin-top:24px">No documented items found.</p>';
771
+ var body='';
772
+ body+=section('Functions',mod.functions,renderFunction,mod.filePath,sourceUrl,symbolMap);
773
+ body+=section('Classes',mod.classes,renderClass,mod.filePath,sourceUrl,symbolMap);
774
+ body+=section('Interfaces',mod.interfaces,renderInterface,mod.filePath,sourceUrl,symbolMap);
775
+ body+=section('Type Aliases',mod.typeAliases,renderTypeAlias,mod.filePath,sourceUrl,symbolMap);
776
+ body+=section('Enums',mod.enums,renderEnum,mod.filePath,sourceUrl,symbolMap);
777
+ body+=section('Variables & Constants',mod.variables,renderVariable,mod.filePath,sourceUrl,symbolMap);
778
+ return body;
779
+ }
780
+
781
+ // ---------------------------------------------------------------------------
782
+ // Site builder
783
+ // ---------------------------------------------------------------------------
784
+
785
+ function buildSite(modules,options){
786
+ options=options||{};
787
+ var projectName=options.projectName||'Documentation';
788
+ var version=options.version||'';
789
+ var theme=options.theme||'default';
790
+ var sourceUrl=options.sourceUrl||null;
791
+ var symbolMap=buildSymbolMap(modules);
520
792
  var showToggle=(THEMES[theme]||THEMES.default).toggleBtn;
521
793
  var pages=[];
522
- var idxIdx=buildSearchIndex(modules,'');
523
- var modIdx=buildSearchIndex(modules,'../');
524
794
 
525
- // index.html
526
- var sidebar=buildSidebar(modules,projectName,version,'index.html','',showToggle);
795
+ // Shared static assets
796
+ pages.push({path:'assets/style.css', html:buildCss(theme)});
797
+ pages.push({path:'assets/app.js', html:CLIENT_JS});
798
+ pages.push({path:'search-index.js', html:'window.__SEARCH_INDEX__='+JSON.stringify(buildSearchIndex(modules,''))+';'});
799
+
800
+ // Index page
527
801
  var totalFns=modules.reduce(function(s,m){return s+m.functions.length;},0);
528
802
  var totalCls=modules.reduce(function(s,m){return s+m.classes.length;},0);
529
- var body='<div class="page-title">'+esc(projectName)+'</div>'
530
- +'<div class="page-subtitle">'+modules.length+' module(s) &middot; '+totalFns+' function(s) &middot; '+totalCls+' class(es)</div>'
531
- +'<div class="section"><div class="section-title">Modules</div><div class="module-grid">';
532
- modules.forEach(function(mod){
533
- var rel=moduleHtmlPath(mod.filePath,modules);
534
- var label=moduleLabel(mod.filePath,modules);
535
- var total=mod.functions.length+mod.classes.length+mod.interfaces.length+mod.typeAliases.length+mod.enums.length+mod.variables.length;
536
- var parts=[mod.functions.length&&mod.functions.length+' fn',mod.classes.length&&mod.classes.length+' class',mod.interfaces.length&&mod.interfaces.length+' iface',mod.enums.length&&mod.enums.length+' enum',mod.variables.length&&mod.variables.length+' const'].filter(Boolean);
537
- // deprecated count across all items
538
- var depCount=[].concat(mod.functions,mod.classes,mod.interfaces,mod.typeAliases,mod.enums,mod.variables).filter(function(i){return i.deprecated!=null;}).length;
539
- // @since version range
540
- var sinces=[].concat(mod.functions,mod.classes,mod.interfaces,mod.typeAliases,mod.enums,mod.variables).map(function(i){return i.since;}).filter(Boolean);
541
- var sinceStr=sinces.length?(' · since v'+sinces.sort()[0])+(sinces.length>1&&sinces[sinces.length-1]!==sinces[0]?'–v'+sinces[sinces.length-1]:''):''
542
- var depStr=depCount?'<span class="badge badge-deprecated" style="font-size:10px;padding:1px 5px;vertical-align:middle">'+depCount+' deprecated</span>':'';
543
- var desc=mod.description?'<div class="module-card-desc">'+esc(mod.description.slice(0,100))+(mod.description.length>100?'…':'')+'</div>':'';
544
- body+='<a class="module-card" href="'+esc(rel)+'"><div class="module-card-name">'+esc(label)+depStr+'</div><div class="module-card-stats">'+(parts.join(' · ')||'no exported items')+esc(sinceStr)+'</div>'+desc+'</a>';
803
+ var idxHeader='<div class="page-title">'+esc(projectName)+'</div>'
804
+ +'<div class="page-subtitle">'+modules.length+' module(s) &middot; '+totalFns+' function(s) &middot; '+totalCls+' class(es)</div>';
805
+ pages.push({
806
+ path:'index.html',
807
+ html:page(projectName,buildSidebar(modules,projectName,version,'index.html','',showToggle,null),idxHeader+buildIndexBody(modules),theme,'',null),
545
808
  });
546
- body+='</div></div>';
547
- pages.push({path:'index.html',html:page(projectName,sidebar,body,idxIdx,theme)});
548
809
 
549
- // per-module pages
810
+ // Per-module pages
550
811
  modules.forEach(function(mod){
551
812
  var rel=moduleHtmlPath(mod.filePath,modules);
552
813
  var label=moduleLabel(mod.filePath,modules);
553
- var modSidebar=buildSidebar(modules,projectName,version,rel,'../',showToggle);
554
- var mbody='<div class="breadcrumb"><a href="../index.html">'+esc(projectName)+'</a> / '+esc(label)+'</div>'
814
+ var modHeader='<div class="breadcrumb"><a href="../index.html">'+esc(projectName)+'</a> / '+esc(label)+'</div>'
555
815
  +'<div class="page-title">'+esc(label)+'</div>'
556
- +'<div class="page-subtitle" style="font-family:monospace;font-size:12px">'+esc(mod.filePath)+'</div>'
816
+ +'<div class="page-subtitle">'+esc(mod.filePath)+'</div>'
557
817
  +(mod.description?'<p class="module-desc">'+esc(mod.description)+'</p>':'');
558
- var isEmpty=!mod.functions.length&&!mod.classes.length&&!mod.interfaces.length&&!mod.typeAliases.length&&!mod.enums.length&&!mod.variables.length;
559
- if(isEmpty){
560
- mbody+='<p class="empty" style="margin-top:24px">No documented items found.</p>';
561
- } else {
562
- mbody+=section('Functions',mod.functions,renderFunction,mod.filePath,sourceUrl,symbolMap);
563
- mbody+=section('Classes',mod.classes,renderClass,mod.filePath,sourceUrl,symbolMap);
564
- mbody+=section('Interfaces',mod.interfaces,renderInterface,mod.filePath,sourceUrl,symbolMap);
565
- mbody+=section('Type Aliases',mod.typeAliases,renderTypeAlias,mod.filePath,sourceUrl,symbolMap);
566
- mbody+=section('Enums',mod.enums,renderEnum,mod.filePath,sourceUrl,symbolMap);
567
- mbody+=section('Variables & Constants',mod.variables,renderVariable,mod.filePath,sourceUrl,symbolMap);
568
- }
569
- pages.push({path:rel,html:page(label+' - '+projectName,modSidebar,mbody,modIdx,theme)});
818
+ pages.push({
819
+ path:rel,
820
+ html:page(
821
+ label+' - '+projectName,
822
+ buildSidebar(modules,projectName,version,rel,'../',showToggle,mod),
823
+ modHeader+buildModuleBody(mod,sourceUrl,symbolMap),
824
+ theme,'../',buildToc(mod)
825
+ ),
826
+ });
570
827
  });
571
828
 
572
829
  return pages;
573
830
  }
574
-
575
831
  module.exports = { buildSite, moduleLabel, moduleHtmlPath };
package/package.json CHANGED
@@ -1,58 +1,58 @@
1
1
  {
2
- "name": "jsdoc-scribe",
3
- "version": "1.7.0",
4
- "description": "Pure, deterministic, AST-based JSDoc comment generator and documentation site builder for JavaScript and TypeScript. No AI involved.",
5
- "main": "lib/index.js",
6
- "exports": {
7
- ".": "./lib/index.js",
8
- "./docs": "./lib/docs.js"
9
- },
10
- "bin": {
11
- "gen-comments": "./bin/cli.js",
12
- "gen-docs": "./bin/gen-docs.js"
13
- },
14
- "files": [
15
- "bin",
16
- "lib",
17
- "README.md",
18
- "CHANGELOG.md",
19
- "LICENSE"
20
- ],
21
- "scripts": {
22
- "demo": "node bin/cli.js sample",
23
- "docs": "node bin/gen-docs.js sample --out docs --title jsdoc-scribe",
24
- "test": "node test/run.js",
25
- "prepublishOnly": "npm test"
26
- },
27
- "keywords": [
28
- "jsdoc",
29
- "comments",
30
- "documentation",
31
- "typescript",
32
- "javascript",
33
- "cli",
34
- "ast",
35
- "code-documentation",
36
- "comment-generator",
37
- "autodoc",
38
- "docs-generator"
39
- ],
40
- "author": {
41
- "name": "Chintan Goswami"
42
- },
43
- "license": "MIT",
44
- "engines": {
45
- "node": ">=14"
46
- },
47
- "repository": {
48
- "type": "git",
49
- "url": "git+https://github.com/imchintoo/jsdoc-scribe.git"
50
- },
51
- "homepage": "https://github.com/imchintoo/jsdoc-scribe#readme",
52
- "bugs": {
53
- "url": "https://github.com/imchintoo/jsdoc-scribe/issues"
54
- },
55
- "dependencies": {
56
- "typescript": ">=5.0.0"
57
- }
2
+ "name": "jsdoc-scribe",
3
+ "version": "1.11.0",
4
+ "description": "Pure, deterministic, AST-based JSDoc comment generator and documentation site builder for JavaScript and TypeScript. No AI involved.",
5
+ "main": "lib/index.js",
6
+ "exports": {
7
+ ".": "./lib/index.js",
8
+ "./docs": "./lib/docs.js"
9
+ },
10
+ "bin": {
11
+ "gen-comments": "./bin/cli.js",
12
+ "gen-docs": "./bin/gen-docs.js"
13
+ },
14
+ "files": [
15
+ "bin",
16
+ "lib",
17
+ "README.md",
18
+ "CHANGELOG.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "demo": "node bin/cli.js sample",
23
+ "docs": "node bin/gen-docs.js sample --out docs --title jsdoc-scribe",
24
+ "test": "node test/run.js",
25
+ "prepublishOnly": "npm test"
26
+ },
27
+ "keywords": [
28
+ "jsdoc",
29
+ "comments",
30
+ "documentation",
31
+ "typescript",
32
+ "javascript",
33
+ "cli",
34
+ "ast",
35
+ "code-documentation",
36
+ "comment-generator",
37
+ "autodoc",
38
+ "docs-generator"
39
+ ],
40
+ "author": {
41
+ "name": "Chintan Goswami"
42
+ },
43
+ "license": "MIT",
44
+ "engines": {
45
+ "node": ">=14"
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/imchintoo/jsdoc-scribe.git"
50
+ },
51
+ "homepage": "https://github.com/imchintoo/jsdoc-scribe#readme",
52
+ "bugs": {
53
+ "url": "https://github.com/imchintoo/jsdoc-scribe/issues"
54
+ },
55
+ "dependencies": {
56
+ "typescript": ">=5.0.0"
57
+ }
58
58
  }