@ankhorage/paradox 0.0.10 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 3a7c982: Render sequence diagrams from call flow instead of import topology.
8
+
9
+ ## 0.1.0
10
+
11
+ ### Minor Changes
12
+
13
+ - dd41897: Generate compact README docs from `@readme` symbols.
14
+
3
15
  ## 0.0.10
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -1,15 +1,55 @@
1
+ <!-- markdownlint-disable MD013 MD033 -->
2
+ <!-- This file is generated by Paradox. Do not edit manually. -->
3
+
1
4
  # @ankhorage/paradox
2
5
 
3
- ![license: MIT](./paradox/badges/license.svg) ![npm: v0.0.9](./paradox/badges/npm.svg) ![runtime: bun](./paradox/badges/runtime.svg) ![typescript: strict](./paradox/badges/typescript.svg) ![eslint: checked](./paradox/badges/eslint.svg) ![prettier: checked](./paradox/badges/prettier.svg) ![build: checked](./paradox/badges/build.svg) ![tests: checked](./paradox/badges/tests.svg) ![docs: paradox](./paradox/badges/docs.svg)
6
+ ![license: MIT](./paradox/badges/license.svg) ![npm: v0.1.0](./paradox/badges/npm.svg) ![runtime: bun](./paradox/badges/runtime.svg) ![typescript: strict](./paradox/badges/typescript.svg) ![eslint: checked](./paradox/badges/eslint.svg) ![prettier: checked](./paradox/badges/prettier.svg) ![build: checked](./paradox/badges/build.svg) ![tests: checked](./paradox/badges/tests.svg) ![docs: paradox](./paradox/badges/docs.svg)
4
7
 
5
8
  Deterministic documentation generator for TypeScript packages.
6
9
 
7
- ## Usage
10
+ ## Installation
11
+
12
+ ```bash
13
+ bunx @ankhorage/paradox
14
+ ```
15
+
16
+ ## CLI
17
+
18
+ ### paradox
19
+
20
+ Runs the Paradox CLI.
21
+
22
+ The command discovers the nearest Paradox config, resolves the package and output roots,
23
+ analyzes the package, builds the documentation model, renders all documentation artifacts,
24
+ and writes them to the configured output directory.
8
25
 
9
26
  ```bash
10
27
  bunx @ankhorage/paradox
11
28
  ```
12
29
 
30
+ ## Documentation Tags
31
+
32
+ <details>
33
+ <summary>@readme</summary>
34
+
35
+ Includes a documentation block or exported symbol in README output.
36
+
37
+ </details>
38
+
39
+ <details>
40
+ <summary>@config</summary>
41
+
42
+ Marks a type or interface as part of the Paradox configuration model. `@config` alone does not imply README inclusion; use `@config` plus `@readme` for README output.
43
+
44
+ </details>
45
+
46
+ <details>
47
+ <summary>@example</summary>
48
+
49
+ Adds a titled fenced code example to the generated documentation for a symbol.
50
+
51
+ </details>
52
+
13
53
  ## Configuration
14
54
 
15
55
  Create a `paradox.config.ts` file:
@@ -22,14 +62,17 @@ export default defineParadoxConfig({
22
62
  });
23
63
  ```
24
64
 
25
- ### Configuration options
65
+ <details>
66
+ <summary>Configuration options</summary>
26
67
 
27
68
  | Field | Type | Required | Default | Description |
28
69
  | ------- | --------------------------------------------------------- | -------- | ------- | ----------- |
29
- | mode | `'safe' \| 'write' \| undefined` | no | | |
30
- | docs | `{ title?: string; description?: string; } \| undefined` | no | | |
31
- | package | `{ root?: string; entrypoints?: string[]; } \| undefined` | no | | |
32
- | output | `{ dir?: string; } \| undefined` | no | | |
70
+ | mode | `'safe' \| 'write' \| undefined` | no | — | |
71
+ | docs | `{ title?: string; description?: string; } \| undefined` | no | — | |
72
+ | package | `{ root?: string; entrypoints?: string[]; } \| undefined` | no | — | |
73
+ | output | `{ dir?: string; } \| undefined` | no | — | |
74
+
75
+ </details>
33
76
 
34
77
  ## Generated documentation
35
78
 
@@ -39,10 +82,12 @@ export default defineParadoxConfig({
39
82
  - [Architecture overview](./paradox/diagrams/architecture-overview.mmd)
40
83
  - [Module relationships](./paradox/diagrams/module-relationships.mmd)
41
84
  - [Export graph](./paradox/diagrams/export-graph.mmd)
42
- - [Entrypoint sequence](./paradox/diagrams/entrypoint-sequence.mmd)
43
85
 
44
86
  ## Architecture preview
45
87
 
88
+ <details>
89
+ <summary>Architecture overview</summary>
90
+
46
91
  ```mermaid
47
92
  graph TD
48
93
  package__ankhorage_paradox["@ankhorage/paradox"]
@@ -58,6 +103,7 @@ graph TD
58
103
  module_src_analyze_analyze_ts --> module_src_analyze_semantic_createTypeScriptProgram_ts
59
104
  module_src_analyze_analyze_ts --> module_src_analyze_semantic_exports_ts
60
105
  module_src_analyze_analyze_ts --> module_src_analyze_semantic_graphs_ts
106
+ module_src_analyze_analyze_ts --> module_src_analyze_sequenceScenarios_ts
61
107
  module_src_analyze_analyze_ts --> module_src_analyze_types_ts
62
108
  module_src_analyze_analyze_ts --> module_src_analyze_usage_ts
63
109
  module_src_analyze_analyze_ts --> module_src_config_types_ts
@@ -133,6 +179,13 @@ graph TD
133
179
  package__ankhorage_paradox -.-> module_src_analyze_semantic_tagRegistry_ts
134
180
  module_src_analyze_semantic_utils_ts["src/analyze/semantic/utils.ts"]
135
181
  package__ankhorage_paradox -.-> module_src_analyze_semantic_utils_ts
182
+ module_src_analyze_sequenceScenarios_ts["src/analyze/sequenceScenarios.ts"]
183
+ package__ankhorage_paradox -.-> module_src_analyze_sequenceScenarios_ts
184
+ module_src_analyze_sequenceScenarios_ts --> module_src_analyze_semantic_utils_ts
185
+ module_src_analyze_sequenceScenarios_ts --> module_src_analyze_types_ts
186
+ module_src_analyze_sequenceScenarios_ts --> module_src_analyze_usage_ts
187
+ module_src_analyze_sequenceScenarios_ts --> module_src_analyze_utils_getParadoxComment_ts
188
+ module_src_analyze_sequenceScenarios_ts --> module_src_analyze_utils_parseParadoxComment_ts
136
189
  module_src_analyze_types_ts["src/analyze/types.ts"]
137
190
  package__ankhorage_paradox -.-> module_src_analyze_types_ts
138
191
  module_src_analyze_usage_ts["src/analyze/usage.ts"]
@@ -212,6 +265,8 @@ graph TD
212
265
  module_src_write_write_ts --> module_src_render_types_ts
213
266
  ```
214
267
 
268
+ </details>
269
+
215
270
  ## Path resolution
216
271
 
217
272
  - Config discovery: searches upward from `process.cwd()` for `paradox.config.ts/js/mjs/cjs` (required; no fallback).
@@ -223,21 +278,29 @@ graph TD
223
278
 
224
279
  ## Public API
225
280
 
226
- ### defineParadoxConfig
281
+ ### Configuration
282
+
283
+ <details>
284
+ <summary>defineParadoxConfig</summary>
285
+
286
+ ```ts
287
+ defineParadoxConfig(config: ParadoxConfig) => ParadoxConfig
288
+ ```
227
289
 
228
290
  Defines a Paradox configuration object without changing its shape.
229
291
 
230
- - Kind: `function`
231
- - Module: `src/config/defineParadoxConfig.ts`
232
- - Source: `src/config/defineParadoxConfig.ts:6:1`
233
- - Export paths: `src/index.ts`
234
- - Related symbols: `ParadoxConfig`
292
+ Module: `src/config/defineParadoxConfig.ts`
293
+ Source: `src/config/defineParadoxConfig.ts:8:1`
294
+ Related symbols: `ParadoxConfig`
295
+
296
+ </details>
235
297
 
236
- ### ParadoxConfig
298
+ <details>
299
+ <summary>ParadoxConfig</summary>
237
300
 
238
301
  Configuration for running Paradox.
239
302
 
240
- - Kind: `type`
241
- - Module: `src/config/types.ts`
242
- - Source: `src/config/types.ts:6:1`
243
- - Export paths: `src/index.ts`
303
+ Module: `src/config/types.ts`
304
+ Source: `src/config/types.ts:7:1`
305
+
306
+ </details>
@@ -1,8 +1,5 @@
1
1
  import type { ParadoxConfig } from '../config/types.js';
2
2
  import type { AnalysisResult } from './types.js';
3
- /***
4
- * Runs the source analysis pipeline for a configured package.
5
- */
6
3
  export declare function analyze(config: ParadoxConfig, runtime: {
7
4
  packageRoot: string;
8
5
  }): Promise<AnalysisResult>;
@@ -8,10 +8,8 @@ import { createProject } from './project.js';
8
8
  import { createTypeScriptProgram } from './semantic/createTypeScriptProgram.js';
9
9
  import { collectTypeMembers, resolveTypeReference } from './semantic/exports.js';
10
10
  import { collectCallGraph, collectComponentCompositionGraph, collectImportGraph, } from './semantic/graphs.js';
11
+ import { analyzeSequenceScenarios } from './sequenceScenarios.js';
11
12
  import { createUsageFromPackageJson } from './usage.js';
12
- /***
13
- * Runs the source analysis pipeline for a configured package.
14
- */
15
13
  export async function analyze(config, runtime) {
16
14
  const root = runtime.packageRoot;
17
15
  const pkg = await readPackageJson(root);
@@ -20,15 +18,10 @@ export async function analyze(config, runtime) {
20
18
  const project = createProject(root);
21
19
  const entrypoints = config.package?.entrypoints ?? ['src/index.ts'];
22
20
  const program = createTypeScriptProgram({ root, entrypoints, project });
23
- const { config: configMetadata, exports } = analyzeExports(project, {
24
- root,
25
- entrypoints,
26
- });
21
+ const { config: configMetadata, exports } = analyzeExports(project, { root, entrypoints });
27
22
  const components = analyzeComponents(exports, { program });
28
- const modules = analyzeModules(project, {
29
- root,
30
- entrypoints,
31
- });
23
+ const modules = analyzeModules(project, { root, entrypoints });
24
+ const sequenceScenarios = analyzeSequenceScenarios({ project, root, pkg, exports });
32
25
  const configExport = configMetadata
33
26
  ? (exports.find((entry) => entry.name === configMetadata.exportName) ?? null)
34
27
  : null;
@@ -59,10 +52,12 @@ export async function analyze(config, runtime) {
59
52
  entrypoints: entrypoints.map((entrypoint) => entrypoint.replaceAll('\\', '/')).sort(),
60
53
  modules,
61
54
  badges,
55
+ sequenceScenarios,
62
56
  usage,
63
57
  config: configMetadata
64
58
  ? {
65
59
  exportName: configMetadata.exportName,
60
+ isReadme: configMetadata.isReadme,
66
61
  members: mapTypeMembers(configMembers),
67
62
  }
68
63
  : null,
@@ -17,6 +17,7 @@ export function analyzeComponents(exports, options = {}) {
17
17
  name: member.name,
18
18
  type: member.type,
19
19
  required: member.required,
20
+ ...(member.defaultValue !== undefined ? { defaultValue: member.defaultValue } : {}),
20
21
  description: member.description ?? null,
21
22
  })) ?? [];
22
23
  const propsType = getComponentPropsType(e.node);
@@ -25,6 +26,8 @@ export function analyzeComponents(exports, options = {}) {
25
26
  components.push({
26
27
  name: e.name,
27
28
  description: e.description,
29
+ isReadme: e.isReadme,
30
+ examples: e.examples,
28
31
  modulePath: e.modulePath,
29
32
  sourceLocation: e.sourceLocation,
30
33
  exportPaths: e.exportPaths,
@@ -4,6 +4,7 @@ interface AnalyzeExportsResult {
4
4
  exports: AnalysisExport[];
5
5
  config: {
6
6
  exportName: string;
7
+ isReadme: boolean;
7
8
  } | null;
8
9
  }
9
10
  /***
@@ -19,13 +19,12 @@ export function analyzeExports(project, options) {
19
19
  continue;
20
20
  }
21
21
  const rawComment = getParadoxComment(decl);
22
- const parsed = rawComment
23
- ? parseParadoxComment(rawComment)
24
- : { description: null, isConfig: false, params: {}, returns: null };
22
+ const parsed = rawComment ? parseParadoxComment(rawComment) : createEmptyMetadata();
25
23
  const name = resolved.getName();
26
24
  if (parsed.isConfig) {
27
25
  config = {
28
26
  exportName: name,
27
+ isReadme: parsed.isReadme,
29
28
  };
30
29
  }
31
30
  const metadata = getExportMetadata({
@@ -40,6 +39,8 @@ export function analyzeExports(project, options) {
40
39
  ? {
41
40
  ...existing,
42
41
  description: existing.description ?? parsed.description,
42
+ isReadme: existing.isReadme || parsed.isReadme,
43
+ examples: existing.examples.length > 0 ? existing.examples : parsed.examples,
43
44
  exportPaths: uniqueSorted([...existing.exportPaths, ...metadata.exportPaths]),
44
45
  relatedSymbols: uniqueSorted([
45
46
  ...existing.relatedSymbols,
@@ -52,6 +53,8 @@ export function analyzeExports(project, options) {
52
53
  name,
53
54
  node: decl,
54
55
  description: parsed.description,
56
+ isReadme: parsed.isReadme,
57
+ examples: parsed.examples,
55
58
  kind: inferKind(decl),
56
59
  ...metadata,
57
60
  });
@@ -87,3 +90,13 @@ function uniqueSorted(values) {
87
90
  function toPosixPath(path) {
88
91
  return path.replaceAll('\\', '/');
89
92
  }
93
+ function createEmptyMetadata() {
94
+ return {
95
+ description: null,
96
+ isConfig: false,
97
+ isReadme: false,
98
+ examples: [],
99
+ params: {},
100
+ returns: null,
101
+ };
102
+ }
@@ -0,0 +1,14 @@
1
+ import { type Project } from 'ts-morph';
2
+ import type { AnalysisExport, AnalysisSequenceScenario } from './types.js';
3
+ import type { PackageJsonModel } from './usage.js';
4
+ interface AnalyzeSequenceScenariosOptions {
5
+ project: Project;
6
+ root: string;
7
+ pkg: PackageJsonModel;
8
+ exports: readonly AnalysisExport[];
9
+ }
10
+ /***
11
+ * Finds scenario roots that can be rendered as sequence diagrams.
12
+ */
13
+ export declare function analyzeSequenceScenarios({ project, root, pkg, exports, }: AnalyzeSequenceScenariosOptions): AnalysisSequenceScenario[];
14
+ export {};
@@ -0,0 +1,174 @@
1
+ import { isAbsolute, join, normalize } from 'node:path';
2
+ import { Node as MorphNode, } from 'ts-morph';
3
+ import { relativeToRoot, toPosixPath } from './semantic/utils.js';
4
+ import { getParadoxComment } from './utils/getParadoxComment.js';
5
+ import { parseParadoxComment } from './utils/parseParadoxComment.js';
6
+ /***
7
+ * Finds scenario roots that can be rendered as sequence diagrams.
8
+ */
9
+ export function analyzeSequenceScenarios({ project, root, pkg, exports, }) {
10
+ return uniqueScenarios([
11
+ ...analyzeBinSequenceScenarios(project, root, pkg),
12
+ ...analyzeExportSequenceScenarios(exports),
13
+ ]);
14
+ }
15
+ function analyzeBinSequenceScenarios(project, root, pkg) {
16
+ return getBinEntries(pkg).flatMap((entry) => {
17
+ const sourceFile = resolveBinSourceFile(project, root, entry.targetPath);
18
+ if (!sourceFile)
19
+ return [];
20
+ const callableRoot = findTopLevelInvokedLocalCallable(sourceFile);
21
+ if (callableRoot === null)
22
+ return [];
23
+ return [
24
+ {
25
+ kind: 'bin',
26
+ name: entry.name,
27
+ sourcePath: relativeToRoot(root, sourceFile.getFilePath()),
28
+ symbolName: callableRoot.symbolName,
29
+ description: callableRoot.description,
30
+ isReadme: callableRoot.isReadme,
31
+ },
32
+ ];
33
+ });
34
+ }
35
+ function analyzeExportSequenceScenarios(exports) {
36
+ return exports.flatMap((entry) => {
37
+ if (entry.signatures.length === 0)
38
+ return [];
39
+ return [
40
+ {
41
+ kind: 'export',
42
+ name: entry.name,
43
+ sourcePath: entry.modulePath,
44
+ symbolName: entry.name,
45
+ description: entry.description,
46
+ isReadme: entry.isReadme,
47
+ },
48
+ ];
49
+ });
50
+ }
51
+ function getBinEntries(pkg) {
52
+ if (pkg.bin == null)
53
+ return [];
54
+ if (typeof pkg.bin === 'string') {
55
+ return [
56
+ {
57
+ name: getPackageBaseName(pkg.name),
58
+ targetPath: pkg.bin,
59
+ },
60
+ ];
61
+ }
62
+ return Object.entries(pkg.bin)
63
+ .map(([name, targetPath]) => ({ name, targetPath }))
64
+ .sort((left, right) => left.name.localeCompare(right.name));
65
+ }
66
+ function resolveBinSourceFile(project, root, targetPath) {
67
+ for (const candidate of getBinSourceCandidates(targetPath)) {
68
+ const sourceFile = getSourceFileByRelativePath(project, root, candidate);
69
+ if (sourceFile)
70
+ return sourceFile;
71
+ }
72
+ return null;
73
+ }
74
+ function getBinSourceCandidates(targetPath) {
75
+ const normalized = toPosixPath(targetPath).replace(/^\.\//, '');
76
+ const candidates = [];
77
+ if (/^src\/.*\.tsx?$/.test(normalized)) {
78
+ candidates.push(normalized);
79
+ }
80
+ if (/^dist\/.*\.jsx?$/.test(normalized)) {
81
+ candidates.push(normalized.replace(/^dist\//, 'src/').replace(/\.jsx?$/, '.ts'));
82
+ candidates.push(normalized.replace(/^dist\//, 'src/').replace(/\.jsx?$/, '.tsx'));
83
+ }
84
+ if (/\.jsx?$/.test(normalized)) {
85
+ candidates.push(normalized.replace(/\.jsx?$/, '.ts'));
86
+ candidates.push(normalized.replace(/\.jsx?$/, '.tsx'));
87
+ }
88
+ return uniqueSorted(candidates);
89
+ }
90
+ function getSourceFileByRelativePath(project, root, relativePath) {
91
+ const absolutePath = normalize(isAbsolute(relativePath) ? relativePath : join(root, relativePath));
92
+ return project.getSourceFile(absolutePath) ?? null;
93
+ }
94
+ function findTopLevelInvokedLocalCallable(sourceFile) {
95
+ const candidates = [];
96
+ sourceFile.forEachDescendant((node) => {
97
+ if (!MorphNode.isCallExpression(node))
98
+ return;
99
+ if (isInsideCallable(node))
100
+ return;
101
+ const callableDeclaration = getLocalFunctionDeclarationForCall(sourceFile, node);
102
+ if (callableDeclaration !== null) {
103
+ candidates.push(callableDeclaration);
104
+ }
105
+ });
106
+ const uniqueCandidates = uniqueByFunctionName(candidates);
107
+ const declaration = uniqueCandidates.length === 1 ? uniqueCandidates[0] : undefined;
108
+ if (declaration === undefined)
109
+ return null;
110
+ const parsedComment = getParsedParadoxComment(declaration);
111
+ return {
112
+ symbolName: declaration.getName() ?? 'main',
113
+ description: parsedComment.description,
114
+ isReadme: parsedComment.isReadme,
115
+ };
116
+ }
117
+ function getParsedParadoxComment(declaration) {
118
+ const comment = getParadoxComment(declaration);
119
+ if (comment === null) {
120
+ return { description: null, isReadme: false };
121
+ }
122
+ const parsed = parseParadoxComment(comment);
123
+ return {
124
+ description: parsed.description,
125
+ isReadme: parsed.isReadme,
126
+ };
127
+ }
128
+ function getLocalFunctionDeclarationForCall(sourceFile, node) {
129
+ const expression = node.getExpression();
130
+ const symbol = expression.getSymbol() ?? expression.getType().getSymbol();
131
+ if (!symbol)
132
+ return null;
133
+ for (const declaration of symbol.getDeclarations()) {
134
+ if (declaration.getSourceFile().getFilePath() !== sourceFile.getFilePath())
135
+ continue;
136
+ if (!MorphNode.isFunctionDeclaration(declaration))
137
+ continue;
138
+ return declaration;
139
+ }
140
+ return null;
141
+ }
142
+ function isInsideCallable(node) {
143
+ return Boolean(node.getFirstAncestor((candidate) => MorphNode.isFunctionDeclaration(candidate) ||
144
+ MorphNode.isMethodDeclaration(candidate) ||
145
+ MorphNode.isFunctionExpression(candidate) ||
146
+ MorphNode.isArrowFunction(candidate) ||
147
+ MorphNode.isClassDeclaration(candidate)));
148
+ }
149
+ function getPackageBaseName(packageName) {
150
+ return packageName.split('/').pop() ?? packageName;
151
+ }
152
+ function uniqueScenarios(scenarios) {
153
+ const seen = new Set();
154
+ return scenarios.filter((scenario) => {
155
+ const key = `${scenario.kind}:${scenario.name}:${scenario.sourcePath}:${scenario.symbolName}`;
156
+ if (seen.has(key))
157
+ return false;
158
+ seen.add(key);
159
+ return true;
160
+ });
161
+ }
162
+ function uniqueByFunctionName(declarations) {
163
+ const seen = new Set();
164
+ return declarations.filter((declaration) => {
165
+ const name = declaration.getName();
166
+ if (name === undefined || seen.has(name))
167
+ return false;
168
+ seen.add(name);
169
+ return true;
170
+ });
171
+ }
172
+ function uniqueSorted(values) {
173
+ return [...new Set(values)].sort((left, right) => left.localeCompare(right));
174
+ }
@@ -1,4 +1,9 @@
1
1
  import type { Node } from 'ts-morph';
2
+ interface AnalysisExample {
3
+ title: string | null;
4
+ language: string | null;
5
+ code: string;
6
+ }
2
7
  /***
3
8
  * Describes one exported declaration discovered in a package.
4
9
  */
@@ -33,6 +38,8 @@ export interface AnalysisExport {
33
38
  name: string;
34
39
  node: Node;
35
40
  description: string | null;
41
+ isReadme: boolean;
42
+ examples: AnalysisExample[];
36
43
  kind: 'function' | 'type' | 'unknown';
37
44
  modulePath: string;
38
45
  sourceLocation: AnalysisSourceLocation;
@@ -47,6 +54,8 @@ export interface AnalysisExport {
47
54
  export interface AnalysisComponent {
48
55
  name: string;
49
56
  description: string | null;
57
+ isReadme: boolean;
58
+ examples: AnalysisExample[];
50
59
  modulePath: string;
51
60
  sourceLocation: AnalysisSourceLocation;
52
61
  exportPaths: string[];
@@ -54,6 +63,7 @@ export interface AnalysisComponent {
54
63
  name: string;
55
64
  type: string;
56
65
  required: boolean;
66
+ defaultValue?: string;
57
67
  description: string | null;
58
68
  }[];
59
69
  }
@@ -77,6 +87,14 @@ export interface AnalysisModule {
77
87
  dependencies: string[];
78
88
  exports: string[];
79
89
  }
90
+ export interface AnalysisSequenceScenario {
91
+ kind: 'bin' | 'export';
92
+ name: string;
93
+ sourcePath: string;
94
+ symbolName: string;
95
+ description: string | null;
96
+ isReadme: boolean;
97
+ }
80
98
  interface AnalysisTypeMember {
81
99
  name: string;
82
100
  type: string;
@@ -126,9 +144,11 @@ export interface AnalysisResult {
126
144
  entrypoints: string[];
127
145
  modules: AnalysisModule[];
128
146
  badges: AnalysisBadge[];
147
+ sequenceScenarios: AnalysisSequenceScenario[];
129
148
  usage: AnalysisUsage | null;
130
149
  config: {
131
150
  exportName: string;
151
+ isReadme: boolean;
132
152
  members: AnalysisTypeMember[];
133
153
  } | null;
134
154
  graphs: AnalysisGraphs;
@@ -4,9 +4,16 @@
4
4
  interface ParsedParadoxComment {
5
5
  description: string | null;
6
6
  isConfig: boolean;
7
+ isReadme: boolean;
8
+ examples: ParsedExample[];
7
9
  params: Record<string, string>;
8
10
  returns: string | null;
9
11
  }
12
+ interface ParsedExample {
13
+ title: string | null;
14
+ language: string | null;
15
+ code: string;
16
+ }
10
17
  /***
11
18
  * Parses a Paradox doc comment into structured metadata.
12
19
  */
@@ -2,20 +2,29 @@
2
2
  * Parses a Paradox doc comment into structured metadata.
3
3
  */
4
4
  export function parseParadoxComment(rawComment) {
5
- const lines = rawComment
6
- .replace(/^\/\*\*\*/, '')
7
- .replace(/\*\/$/, '')
8
- .split('\n')
9
- .map((line) => line.replace(/^\s*\*\s?/, '').trimEnd());
5
+ const lines = normalizeCommentLines(rawComment);
6
+ const descriptionLines = [];
7
+ const examples = [];
10
8
  let isConfig = false;
9
+ let isReadme = false;
11
10
  const params = {};
12
11
  let returns = null;
13
- const description = lines
14
- .filter((line) => {
12
+ for (let index = 0; index < lines.length; index += 1) {
13
+ const line = lines[index] ?? '';
15
14
  const trimmed = line.trimStart();
16
15
  if (trimmed.startsWith('@config')) {
17
16
  isConfig = true;
18
- return false;
17
+ continue;
18
+ }
19
+ if (trimmed.startsWith('@readme')) {
20
+ isReadme = true;
21
+ continue;
22
+ }
23
+ if (trimmed.startsWith('@example')) {
24
+ const parsed = parseExample(lines, index);
25
+ examples.push(parsed.example);
26
+ index = parsed.nextIndex;
27
+ continue;
19
28
  }
20
29
  if (trimmed.startsWith('@param ')) {
21
30
  const paramBody = trimmed.slice('@param '.length).trim();
@@ -23,21 +32,59 @@ export function parseParadoxComment(rawComment) {
23
32
  if (name) {
24
33
  params[name] = descriptionParts.join(' ').trim();
25
34
  }
26
- return false;
35
+ continue;
27
36
  }
28
37
  if (trimmed.startsWith('@returns') || trimmed.startsWith('@return')) {
29
38
  const returnBody = trimmed.replace(/^@returns?/, '').trim();
30
39
  returns = returnBody.length > 0 ? returnBody : null;
31
- return false;
40
+ continue;
32
41
  }
33
- return true;
34
- })
35
- .join('\n')
36
- .trim();
42
+ descriptionLines.push(line);
43
+ }
44
+ const description = descriptionLines.join('\n').trim();
37
45
  return {
38
46
  description: description.length > 0 ? description : null,
39
47
  isConfig,
48
+ isReadme,
49
+ examples,
40
50
  params,
41
51
  returns,
42
52
  };
43
53
  }
54
+ function parseExample(lines, startIndex) {
55
+ const header = lines[startIndex]?.trimStart() ?? '';
56
+ const title = header.slice('@example'.length).trim();
57
+ let language = null;
58
+ const codeLines = [];
59
+ let index = startIndex + 1;
60
+ while (index < lines.length && (lines[index] ?? '').trim() === '') {
61
+ index += 1;
62
+ }
63
+ const firstCodeLine = lines[index]?.trim() ?? '';
64
+ if (firstCodeLine.startsWith('```')) {
65
+ language = firstCodeLine.slice('```'.length).trim() || null;
66
+ index += 1;
67
+ while (index < lines.length) {
68
+ const current = lines[index] ?? '';
69
+ if (current.trim() === '```')
70
+ break;
71
+ codeLines.push(current);
72
+ index += 1;
73
+ }
74
+ }
75
+ return {
76
+ example: {
77
+ title: title.length > 0 ? title : null,
78
+ language,
79
+ code: codeLines.join('\n').trimEnd(),
80
+ },
81
+ nextIndex: index,
82
+ };
83
+ }
84
+ function normalizeCommentLines(rawComment) {
85
+ return rawComment
86
+ .replace(/^\/\*\*\*/, '')
87
+ .replace(/\*\/$/, '')
88
+ .split('\n')
89
+ .map((line) => line.replace(/^\s*\*\s?/, '').trimEnd());
90
+ }