@rarusoft/dendrite-wiki 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +79 -0
  2. package/dist/api-extractor/extract.js +269 -0
  3. package/dist/api-extractor/language-extractor.js +15 -0
  4. package/dist/api-extractor/python-extractor.js +358 -0
  5. package/dist/api-extractor/render.js +195 -0
  6. package/dist/api-extractor/tree-sitter-extractor.js +1079 -0
  7. package/dist/api-extractor/types.js +11 -0
  8. package/dist/api-extractor/typescript-extractor.js +50 -0
  9. package/dist/api-extractor/walk.js +178 -0
  10. package/dist/api-reference.js +438 -0
  11. package/dist/benchmark-events.js +129 -0
  12. package/dist/benchmark.js +270 -0
  13. package/dist/binder-export.js +381 -0
  14. package/dist/canonical-target.js +168 -0
  15. package/dist/chart-insert.js +377 -0
  16. package/dist/chart-prompts.js +414 -0
  17. package/dist/context-cache.js +98 -0
  18. package/dist/contradicts-shipped-memory.js +232 -0
  19. package/dist/diff-context.js +142 -0
  20. package/dist/doctor.js +220 -0
  21. package/dist/generated-docs.js +219 -0
  22. package/dist/i18n.js +71 -0
  23. package/dist/index.js +49 -0
  24. package/dist/librarian.js +255 -0
  25. package/dist/maintenance-actions.js +244 -0
  26. package/dist/maintenance-inbox.js +842 -0
  27. package/dist/maintenance-runner.js +62 -0
  28. package/dist/page-drift.js +225 -0
  29. package/dist/page-inbox.js +168 -0
  30. package/dist/report-export.js +339 -0
  31. package/dist/review-bridge.js +1386 -0
  32. package/dist/search-index.js +199 -0
  33. package/dist/store.js +1617 -0
  34. package/dist/telemetry-defaults.js +44 -0
  35. package/dist/telemetry-report.js +263 -0
  36. package/dist/telemetry.js +544 -0
  37. package/dist/wiki-synthesis.js +901 -0
  38. package/package.json +35 -0
  39. package/src/api-extractor/extract.ts +333 -0
  40. package/src/api-extractor/language-extractor.ts +37 -0
  41. package/src/api-extractor/python-extractor.ts +380 -0
  42. package/src/api-extractor/render.ts +267 -0
  43. package/src/api-extractor/tree-sitter-extractor.ts +1210 -0
  44. package/src/api-extractor/types.ts +41 -0
  45. package/src/api-extractor/typescript-extractor.ts +56 -0
  46. package/src/api-extractor/walk.ts +209 -0
  47. package/src/api-reference.ts +552 -0
  48. package/src/benchmark-events.ts +216 -0
  49. package/src/benchmark.ts +376 -0
  50. package/src/binder-export.ts +437 -0
  51. package/src/canonical-target.ts +192 -0
  52. package/src/chart-insert.ts +478 -0
  53. package/src/chart-prompts.ts +417 -0
  54. package/src/context-cache.ts +129 -0
  55. package/src/contradicts-shipped-memory.ts +311 -0
  56. package/src/diff-context.ts +187 -0
  57. package/src/doctor.ts +260 -0
  58. package/src/generated-docs.ts +316 -0
  59. package/src/i18n.ts +106 -0
  60. package/src/index.ts +59 -0
  61. package/src/librarian.ts +331 -0
  62. package/src/maintenance-actions.ts +314 -0
  63. package/src/maintenance-inbox.ts +1132 -0
  64. package/src/maintenance-runner.ts +85 -0
  65. package/src/page-drift.ts +292 -0
  66. package/src/page-inbox.ts +254 -0
  67. package/src/report-export.ts +392 -0
  68. package/src/review-bridge.ts +1729 -0
  69. package/src/search-index.ts +266 -0
  70. package/src/store.ts +2171 -0
  71. package/src/telemetry-defaults.ts +50 -0
  72. package/src/telemetry-report.ts +365 -0
  73. package/src/telemetry.ts +757 -0
  74. package/src/wiki-synthesis.ts +1307 -0
@@ -0,0 +1,358 @@
1
+ /**
2
+ * Python `LanguageExtractor` — the second built-in, validating the A7 pluggability layer.
3
+ *
4
+ * Implements the same `LanguageExtractor` contract as `./typescript-extractor.ts`, but
5
+ * for Python source trees. Detection looks for the conventional Python project signals
6
+ * (`pyproject.toml`, `setup.py`, `setup.cfg`, `requirements.txt`); extraction shells out
7
+ * to a local Python 3.9+ interpreter with a small embedded helper script that walks the
8
+ * Python AST via the standard-library `ast` module and emits an `ApiFileReference` shape
9
+ * directly. The Python interpreter is found via PATH (`python3` then `python`), and its
10
+ * version is verified before the extractor claims a project — projects with Python
11
+ * sources but no usable Python on the developer's machine cleanly fall through to the
12
+ * "no extractor claims this project" path in the orchestrator.
13
+ *
14
+ * Mapping from Python kinds to the language-agnostic `ApiSymbolKind`: `def` and
15
+ * `async def` → `function`; `class` → `class` (or `enum` when the class subclasses
16
+ * `Enum`/`IntEnum`/`StrEnum`/`Flag`); module-level assignments → `variable` (or
17
+ * `type-alias` when the target is PascalCase or annotated with `TypeAlias`). Names with
18
+ * a leading underscore are dropped per Python's privacy convention. `@deprecated`
19
+ * decorators flow through to the `isDeprecated` flag the renderer's callout uses.
20
+ *
21
+ * Docstrings come back as the `docComment` field verbatim — Python's docstring
22
+ * conventions (Google, NumPy, Sphinx) vary, so first-cut rendering keeps the prose as-is
23
+ * rather than guessing at a specific style. A future polish pass could parse Google-
24
+ * style sections and convert them to the same `ApiDocTag` shape JSDoc uses.
25
+ *
26
+ * KNOWN LIMITATION (tracked in docs/wiki/api-reference-roadmap.md): only top-level
27
+ * symbols are extracted. Class bodies are NOT recursed into, so methods, properties,
28
+ * `@classmethod`s, and `@staticmethod`s of a documented class do not appear on the page.
29
+ * The class itself surfaces with its docstring, but its members do not. This is
30
+ * deliberately scoped out of the v0 extractor — proper handling needs design choices
31
+ * around how to render `Class.method` (flat namespaced symbol vs. nested section), how
32
+ * to surface decorator metadata, and whether `@property` getters should map to
33
+ * `kind: 'variable'` or `kind: 'function'`. Until that design pass lands, Python class
34
+ * pages are deliberately thinner than their TypeScript counterparts.
35
+ */
36
+ import { spawn } from 'node:child_process';
37
+ import { promises as fs } from 'node:fs';
38
+ import path from 'node:path';
39
+ import { walkProjectSources } from './walk.js';
40
+ // Embedded Python helper script. Sent to the interpreter via stdin so command-line length
41
+ // limits never become a concern (Windows cmd.exe caps total command line at ~8KB). The
42
+ // script reads one source path from argv and emits the ApiFileReference JSON to stdout.
43
+ const PYTHON_HELPER_SCRIPT = `import ast
44
+ import json
45
+ import sys
46
+
47
+
48
+ def is_public(name):
49
+ return bool(name) and not name.startswith('_')
50
+
51
+
52
+ def get_decorator_names(node):
53
+ names = []
54
+ decorators = getattr(node, 'decorator_list', []) or []
55
+ for dec in decorators:
56
+ if isinstance(dec, ast.Name):
57
+ names.append(dec.id)
58
+ elif isinstance(dec, ast.Attribute):
59
+ names.append(dec.attr)
60
+ elif isinstance(dec, ast.Call):
61
+ if isinstance(dec.func, ast.Name):
62
+ names.append(dec.func.id)
63
+ elif isinstance(dec.func, ast.Attribute):
64
+ names.append(dec.func.attr)
65
+ return names
66
+
67
+
68
+ def is_deprecated(decorator_names):
69
+ return any(name.lower() == 'deprecated' for name in decorator_names)
70
+
71
+
72
+ def render_function_signature(node):
73
+ is_async = isinstance(node, ast.AsyncFunctionDef)
74
+ args = ast.unparse(node.args) if node.args else ''
75
+ returns = ''
76
+ if node.returns is not None:
77
+ returns = ' -> ' + ast.unparse(node.returns)
78
+ prefix = 'async def' if is_async else 'def'
79
+ return prefix + ' ' + node.name + '(' + args + ')' + returns
80
+
81
+
82
+ def render_class_signature(node):
83
+ parts = []
84
+ for base in node.bases:
85
+ parts.append(ast.unparse(base))
86
+ for kw in node.keywords:
87
+ if kw.arg:
88
+ parts.append(kw.arg + '=' + ast.unparse(kw.value))
89
+ else:
90
+ parts.append('**' + ast.unparse(kw.value))
91
+ head = 'class ' + node.name
92
+ if parts:
93
+ head = head + '(' + ', '.join(parts) + ')'
94
+ return head
95
+
96
+
97
+ ENUM_BASE_NAMES = {'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag'}
98
+
99
+
100
+ def is_enum_class(node):
101
+ for base in node.bases:
102
+ if isinstance(base, ast.Name) and base.id in ENUM_BASE_NAMES:
103
+ return True
104
+ if isinstance(base, ast.Attribute) and base.attr in ENUM_BASE_NAMES:
105
+ return True
106
+ return False
107
+
108
+
109
+ def render_assign_signature(node):
110
+ try:
111
+ return ast.unparse(node)
112
+ except Exception:
113
+ return None
114
+
115
+
116
+ def is_type_alias(node, target_name):
117
+ # PEP 613 TypeAlias annotation
118
+ if isinstance(node, ast.AnnAssign):
119
+ ann = node.annotation
120
+ if isinstance(ann, ast.Name) and ann.id == 'TypeAlias':
121
+ return True
122
+ if isinstance(ann, ast.Attribute) and ann.attr == 'TypeAlias':
123
+ return True
124
+ # PEP 695 type statement (Python 3.12+)
125
+ if hasattr(ast, 'TypeAlias') and isinstance(node, getattr(ast, 'TypeAlias')):
126
+ return True
127
+ # Heuristic fallback: PascalCase target name on a value-only assignment. We require
128
+ # at least one lowercase letter so SCREAMING_CASE constants (DEFAULT_NAME, MAX_RETRIES,
129
+ # etc.) stay classified as variables instead of being treated as type aliases.
130
+ if isinstance(node, ast.Assign) and target_name and target_name[0].isupper():
131
+ has_lowercase = any(c.islower() for c in target_name)
132
+ return has_lowercase
133
+ return False
134
+
135
+
136
+ def collect_assign_target(node):
137
+ if isinstance(node, ast.Assign):
138
+ if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
139
+ return node.targets[0].id
140
+ return None
141
+ if isinstance(node, ast.AnnAssign):
142
+ if isinstance(node.target, ast.Name):
143
+ return node.target.id
144
+ return None
145
+ if hasattr(ast, 'TypeAlias') and isinstance(node, getattr(ast, 'TypeAlias')):
146
+ return node.name.id
147
+ return None
148
+
149
+
150
+ def parse_file(path):
151
+ with open(path, 'r', encoding='utf-8') as handle:
152
+ source = handle.read()
153
+ tree = ast.parse(source, filename=path)
154
+ file_doc = ast.get_docstring(tree)
155
+
156
+ symbols = []
157
+ for node in tree.body:
158
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
159
+ if not is_public(node.name):
160
+ continue
161
+ decorator_names = get_decorator_names(node)
162
+ symbols.append({
163
+ 'name': node.name,
164
+ 'kind': 'function',
165
+ 'signature': render_function_signature(node),
166
+ 'docComment': ast.get_docstring(node),
167
+ 'tags': [],
168
+ 'sourceLine': node.lineno,
169
+ 'isDeprecated': is_deprecated(decorator_names),
170
+ })
171
+ elif isinstance(node, ast.ClassDef):
172
+ if not is_public(node.name):
173
+ continue
174
+ decorator_names = get_decorator_names(node)
175
+ kind = 'enum' if is_enum_class(node) else 'class'
176
+ symbols.append({
177
+ 'name': node.name,
178
+ 'kind': kind,
179
+ 'signature': render_class_signature(node),
180
+ 'docComment': ast.get_docstring(node),
181
+ 'tags': [],
182
+ 'sourceLine': node.lineno,
183
+ 'isDeprecated': is_deprecated(decorator_names),
184
+ })
185
+ elif isinstance(node, (ast.Assign, ast.AnnAssign)) or (
186
+ hasattr(ast, 'TypeAlias') and isinstance(node, getattr(ast, 'TypeAlias'))
187
+ ):
188
+ target_name = collect_assign_target(node)
189
+ if not target_name or not is_public(target_name):
190
+ continue
191
+ sig = render_assign_signature(node)
192
+ if not sig:
193
+ continue
194
+ kind = 'type-alias' if is_type_alias(node, target_name) else 'variable'
195
+ symbols.append({
196
+ 'name': target_name,
197
+ 'kind': kind,
198
+ 'signature': sig,
199
+ 'docComment': None,
200
+ 'tags': [],
201
+ 'sourceLine': node.lineno,
202
+ 'isDeprecated': False,
203
+ })
204
+
205
+ symbols.sort(key=lambda s: s['sourceLine'])
206
+
207
+ return {
208
+ 'sourcePath': '',
209
+ 'moduleSlug': '',
210
+ 'symbols': symbols,
211
+ 'fileDocComment': file_doc,
212
+ }
213
+
214
+
215
+ if __name__ == '__main__':
216
+ if len(sys.argv) < 2:
217
+ print('usage: helper.py <source-path>', file=sys.stderr)
218
+ sys.exit(2)
219
+ source_path = sys.argv[1]
220
+ result = parse_file(source_path)
221
+ json.dump(result, sys.stdout, ensure_ascii=False)
222
+ `;
223
+ const PYTHON_DEFAULT_INCLUDE = ['**/*.py'];
224
+ const PYTHON_DEFAULT_EXCLUDE = [
225
+ '**/tests/**',
226
+ '**/test_*.py',
227
+ '**/*_test.py',
228
+ '**/__pycache__/**',
229
+ '**/.venv/**',
230
+ '**/venv/**',
231
+ '**/env/**',
232
+ '**/build/**',
233
+ '**/dist/**',
234
+ '**/site-packages/**',
235
+ '**/node_modules/**'
236
+ ];
237
+ async function runCommand(command, args, stdin) {
238
+ return new Promise((resolve, reject) => {
239
+ const child = spawn(command, args, { shell: false });
240
+ let stdout = '';
241
+ let stderr = '';
242
+ child.stdout.on('data', (chunk) => {
243
+ stdout += chunk.toString('utf8');
244
+ });
245
+ child.stderr.on('data', (chunk) => {
246
+ stderr += chunk.toString('utf8');
247
+ });
248
+ child.on('error', reject);
249
+ child.on('close', (code) => resolve({ stdout, stderr, exitCode: code ?? -1 }));
250
+ // Suppress EPIPE on the stdin stream — fired synchronously when the child process
251
+ // dies before reading (interpreter crash on import, version mismatch, etc.). Without
252
+ // this handler the error becomes an uncaught exception. The child's exit code +
253
+ // stderr surface the underlying problem through the regular `close` path.
254
+ child.stdin.on('error', () => { });
255
+ if (stdin) {
256
+ child.stdin.write(stdin);
257
+ }
258
+ child.stdin.end();
259
+ });
260
+ }
261
+ async function exists(filePath) {
262
+ try {
263
+ await fs.access(filePath);
264
+ return true;
265
+ }
266
+ catch {
267
+ return false;
268
+ }
269
+ }
270
+ async function findPythonInterpreter() {
271
+ const candidates = process.platform === 'win32' ? ['python', 'python3'] : ['python3', 'python'];
272
+ for (const candidate of candidates) {
273
+ try {
274
+ const result = await runCommand(candidate, ['--version']);
275
+ if (result.exitCode !== 0)
276
+ continue;
277
+ // `python --version` writes to stdout on 3.4+, sometimes to stderr on older releases.
278
+ const versionText = `${result.stdout}\n${result.stderr}`;
279
+ const match = versionText.match(/Python (\d+)\.(\d+)/);
280
+ if (!match)
281
+ continue;
282
+ const major = Number(match[1]);
283
+ const minor = Number(match[2]);
284
+ // ast.unparse requires Python 3.9+. Below that the helper script can't run.
285
+ if (major > 3 || (major === 3 && minor >= 9)) {
286
+ return candidate;
287
+ }
288
+ }
289
+ catch {
290
+ // Spawn failed (interpreter not found or not executable); try the next candidate.
291
+ }
292
+ }
293
+ return null;
294
+ }
295
+ // Memoize the interpreter lookup so we don't pay --version overhead on every extract().
296
+ let cachedInterpreter;
297
+ async function getPython() {
298
+ if (cachedInterpreter !== undefined) {
299
+ return cachedInterpreter;
300
+ }
301
+ cachedInterpreter = await findPythonInterpreter();
302
+ return cachedInterpreter;
303
+ }
304
+ // Test-only escape hatch: clear the memoized interpreter so tests can simulate "Python
305
+ // disappears mid-run" or run multiple isolated detection scenarios.
306
+ export function resetPythonInterpreterCache() {
307
+ cachedInterpreter = undefined;
308
+ }
309
+ export const pythonExtractor = {
310
+ id: 'python',
311
+ async detect(rootDir) {
312
+ // Only claim a project that BOTH (a) shows a Python project signal in its root and
313
+ // (b) has a usable Python 3.9+ interpreter on PATH. If Python isn't installed, the
314
+ // orchestrator falls through to the next extractor (e.g., the TS one) or returns an
315
+ // empty result — we never throw at detect-time.
316
+ const signals = ['pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt'];
317
+ let hasSignal = false;
318
+ for (const signal of signals) {
319
+ if (await exists(path.join(rootDir, signal))) {
320
+ hasSignal = true;
321
+ break;
322
+ }
323
+ }
324
+ if (!hasSignal) {
325
+ return false;
326
+ }
327
+ return (await getPython()) !== null;
328
+ },
329
+ async walk(rootDir, options) {
330
+ return walkProjectSources(rootDir, {
331
+ include: options?.include ?? PYTHON_DEFAULT_INCLUDE,
332
+ exclude: options?.exclude ?? PYTHON_DEFAULT_EXCLUDE,
333
+ // The @internal-tag walker convention is JSDoc-specific; Python's privacy convention
334
+ // is the leading-underscore name. The extract step honors that already.
335
+ respectInternalConvention: false
336
+ });
337
+ },
338
+ async extract(sourcePath, options) {
339
+ const python = await getPython();
340
+ if (!python) {
341
+ throw new Error('pythonExtractor.extract: Python 3.9+ is required but no usable interpreter was found on PATH');
342
+ }
343
+ const rootDir = options?.rootDir ?? process.cwd();
344
+ const absolute = path.isAbsolute(sourcePath) ? sourcePath : path.resolve(rootDir, sourcePath);
345
+ const relative = path.relative(rootDir, absolute).replace(/\\/g, '/');
346
+ const result = await runCommand(python, ['-', absolute], PYTHON_HELPER_SCRIPT);
347
+ if (result.exitCode !== 0) {
348
+ throw new Error(`pythonExtractor.extract: helper failed for ${relative} (exit ${result.exitCode}): ${result.stderr.trim()}`);
349
+ }
350
+ const parsed = JSON.parse(result.stdout);
351
+ parsed.sourcePath = relative;
352
+ // Mirror typeScriptExtractor's slug shape: `api/<dir>/<basename>` with the language
353
+ // extension stripped. Python files outside a top-level `src/` keep their full path.
354
+ const stripped = relative.replace(/^src\//, '').replace(/\.py$/i, '');
355
+ parsed.moduleSlug = `api/${stripped}`;
356
+ return parsed;
357
+ }
358
+ };
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Renders an `ApiFileReference` to a markdown page body.
3
+ *
4
+ * Pure module — no IO, no global state, no `Date.now()`. Every source of non-determinism
5
+ * (timestamp, source-link path base, link resolver) is passed in by the caller. That keeps
6
+ * the byte-stable round-trip property easy to enforce in tests: identical input produces
7
+ * identical output, and `npm run wiki:refresh` run twice produces zero diffs.
8
+ *
9
+ * The output shape is the contract: frontmatter (lifecycle: generated + source-coverage:
10
+ * api-reference + source-file: ...), an H1 of the file path, the file-level doc comment if
11
+ * any, an "Exports" table-of-contents, then per-symbol sections with kind, source link,
12
+ * signature code block, doc body, and per-tag sub-sections (`@param` table, `@returns`,
13
+ * `@throws`, `@example`, `@see`, `@since`, unknown tags). Cross-reference resolution for
14
+ * `{@link Foo}` is handled via an optional `LinkResolver` callback supplied by the
15
+ * orchestrator — this module never knows about other files.
16
+ */
17
+ const LINK_PATTERN = /\{@link\s+([^\s|}]+)(?:\s*[|]?\s*([^}]+))?\}/g;
18
+ function applyLinkResolution(text, resolver) {
19
+ if (!resolver) {
20
+ return text;
21
+ }
22
+ return text.replace(LINK_PATTERN, (_, target, displayRaw) => {
23
+ const display = displayRaw?.trim();
24
+ const resolution = resolver(target, display && display.length > 0 ? display : undefined);
25
+ const rendered = resolution.url ? `[${resolution.display}](${resolution.url})` : resolution.display;
26
+ return resolution.comment ? `${rendered}<!-- ${resolution.comment} -->` : rendered;
27
+ });
28
+ }
29
+ export function renderApiPage(ref, options = {}) {
30
+ const sourceLinkBase = options.sourceLinkBase ?? '../..';
31
+ const lines = [];
32
+ lines.push('---');
33
+ lines.push('lifecycle: generated');
34
+ lines.push('source-coverage: api-reference');
35
+ lines.push(`source-file: ${ref.sourcePath}`);
36
+ if (options.generatedAt) {
37
+ lines.push(`last-generated: ${options.generatedAt}`);
38
+ }
39
+ lines.push('---');
40
+ lines.push('');
41
+ lines.push(`# \`${ref.sourcePath}\``);
42
+ lines.push('');
43
+ if (ref.fileDocComment) {
44
+ lines.push(applyLinkResolution(ref.fileDocComment, options.resolveLink));
45
+ lines.push('');
46
+ }
47
+ if (ref.symbols.length === 0) {
48
+ lines.push('_No documented exports._');
49
+ lines.push('');
50
+ return lines.join('\n');
51
+ }
52
+ lines.push('## Exports');
53
+ lines.push('');
54
+ for (const symbol of ref.symbols) {
55
+ lines.push(`- [\`${symbol.name}\`](#${anchorFor(symbol.name)}) — ${kindLabel(symbol.kind)}`);
56
+ }
57
+ lines.push('');
58
+ for (const symbol of ref.symbols) {
59
+ lines.push('---');
60
+ lines.push('');
61
+ lines.push(...renderSymbol(symbol, ref.sourcePath, sourceLinkBase, options.sourceLinkResolver, options.resolveLink));
62
+ }
63
+ return lines.join('\n');
64
+ }
65
+ function renderSymbol(symbol, sourcePath, sourceLinkBase, sourceLinkResolver, resolveLink) {
66
+ const lines = [];
67
+ lines.push(`### \`${symbol.name}\``);
68
+ lines.push('');
69
+ if (symbol.isDeprecated) {
70
+ const reason = (symbol.tags.find((tag) => tag.name === 'deprecated')?.text ?? '').trim();
71
+ const resolvedReason = applyLinkResolution(reason, resolveLink);
72
+ lines.push(`> ⚠️ **Deprecated:** ${resolvedReason || 'this symbol is deprecated.'}`);
73
+ lines.push('');
74
+ }
75
+ // Source-link URL strategy:
76
+ // 1. If a resolver was supplied, use its output. A non-null string becomes a markdown
77
+ // link; null becomes plain text (no link). The orchestrator builds full
78
+ // https://github.com/... URLs when it can detect the repository, otherwise returns
79
+ // null so the page never emits a 404-prone relative link.
80
+ // 2. If no resolver was supplied, fall back to the legacy relative-path link from
81
+ // `sourceLinkBase`. This works on GitHub web view of the raw markdown and in some
82
+ // IDEs; it does NOT work in VitePress's static site, which is why the resolver path
83
+ // is the production default.
84
+ let sourceLine;
85
+ if (sourceLinkResolver) {
86
+ const url = sourceLinkResolver(sourcePath, symbol.sourceLine);
87
+ sourceLine = url
88
+ ? `[${sourcePath}:${symbol.sourceLine}](${url})`
89
+ : `${sourcePath}:${symbol.sourceLine}`;
90
+ }
91
+ else {
92
+ sourceLine = `[${sourcePath}:${symbol.sourceLine}](${sourceLinkBase}/${sourcePath}#L${symbol.sourceLine})`;
93
+ }
94
+ lines.push(`**Kind:** ${kindLabel(symbol.kind)} · **Source:** ${sourceLine}`);
95
+ lines.push('');
96
+ lines.push('```ts');
97
+ lines.push(symbol.signature);
98
+ lines.push('```');
99
+ lines.push('');
100
+ if (symbol.docComment) {
101
+ lines.push(applyLinkResolution(symbol.docComment, resolveLink));
102
+ lines.push('');
103
+ }
104
+ const paramTags = symbol.tags.filter((tag) => tag.name === 'param');
105
+ if (paramTags.length > 0) {
106
+ lines.push('#### Parameters');
107
+ lines.push('');
108
+ lines.push('| Name | Description |');
109
+ lines.push('|---|---|');
110
+ for (const tag of paramTags) {
111
+ const name = tag.paramName ?? '';
112
+ const description = escapeTableCell(applyLinkResolution(tag.text, resolveLink));
113
+ lines.push(`| \`${name}\` | ${description} |`);
114
+ }
115
+ lines.push('');
116
+ }
117
+ const returnsTag = symbol.tags.find((tag) => tag.name === 'returns' || tag.name === 'return');
118
+ if (returnsTag) {
119
+ lines.push('#### Returns');
120
+ lines.push('');
121
+ lines.push(applyLinkResolution(returnsTag.text, resolveLink) || '_(no description)_');
122
+ lines.push('');
123
+ }
124
+ const throwsTags = symbol.tags.filter((tag) => tag.name === 'throws' || tag.name === 'throw');
125
+ if (throwsTags.length > 0) {
126
+ lines.push('#### Throws');
127
+ lines.push('');
128
+ for (const tag of throwsTags) {
129
+ lines.push(`- ${applyLinkResolution(tag.text, resolveLink)}`);
130
+ }
131
+ lines.push('');
132
+ }
133
+ // @example bodies are code; do not run them through the link resolver.
134
+ const exampleTags = symbol.tags.filter((tag) => tag.name === 'example');
135
+ for (const tag of exampleTags) {
136
+ lines.push('#### Example');
137
+ lines.push('');
138
+ lines.push('```ts');
139
+ lines.push(tag.text);
140
+ lines.push('```');
141
+ lines.push('');
142
+ }
143
+ const seeTags = symbol.tags.filter((tag) => tag.name === 'see');
144
+ if (seeTags.length > 0) {
145
+ lines.push('#### See');
146
+ lines.push('');
147
+ for (const tag of seeTags) {
148
+ lines.push(`- ${applyLinkResolution(tag.text, resolveLink)}`);
149
+ }
150
+ lines.push('');
151
+ }
152
+ const sinceTag = symbol.tags.find((tag) => tag.name === 'since');
153
+ if (sinceTag) {
154
+ lines.push(`**Since:** ${applyLinkResolution(sinceTag.text, resolveLink)}`);
155
+ lines.push('');
156
+ }
157
+ const reservedTags = new Set(['param', 'returns', 'return', 'throws', 'throw', 'example', 'see', 'since', 'deprecated', 'internal']);
158
+ const unknownTags = symbol.tags.filter((tag) => !reservedTags.has(tag.name));
159
+ if (unknownTags.length > 0) {
160
+ lines.push('#### Tags');
161
+ lines.push('');
162
+ for (const tag of unknownTags) {
163
+ const resolved = applyLinkResolution(tag.text, resolveLink);
164
+ const text = resolved ? `: ${resolved}` : '';
165
+ lines.push(`- **@${tag.name}**${text}`);
166
+ }
167
+ lines.push('');
168
+ }
169
+ return lines;
170
+ }
171
+ function kindLabel(kind) {
172
+ switch (kind) {
173
+ case 'function':
174
+ return 'function';
175
+ case 'class':
176
+ return 'class';
177
+ case 'interface':
178
+ return 'interface';
179
+ case 'type-alias':
180
+ return 'type alias';
181
+ case 'enum':
182
+ return 'enum';
183
+ case 'variable':
184
+ return 'variable';
185
+ }
186
+ }
187
+ export function anchorFor(name) {
188
+ return name
189
+ .toLowerCase()
190
+ .replace(/[^a-z0-9]+/g, '-')
191
+ .replace(/^-+|-+$/g, '');
192
+ }
193
+ function escapeTableCell(value) {
194
+ return value.replace(/\|/g, '\\|').replace(/\r?\n/g, ' ').trim();
195
+ }