@ankhorage/paradox 0.1.27 → 0.2.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 (58) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +53 -61
  3. package/dist/analyze/analyze.d.ts +2 -2
  4. package/dist/analyze/analyze.js +51 -35
  5. package/dist/analyze/badges.d.ts +2 -1
  6. package/dist/analyze/badges.js +14 -4
  7. package/dist/analyze/components.js +12 -11
  8. package/dist/analyze/documentation/collectDocumentationCommentsAsync.d.ts +11 -0
  9. package/dist/analyze/documentation/collectDocumentationCommentsAsync.js +72 -0
  10. package/dist/analyze/documentation/findings.d.ts +5 -0
  11. package/dist/analyze/documentation/findings.js +17 -0
  12. package/dist/analyze/documentation/validateDocumentationPolicyAsync.d.ts +13 -0
  13. package/dist/analyze/documentation/validateDocumentationPolicyAsync.js +150 -0
  14. package/dist/analyze/documentation/validateReferencesAsync.d.ts +11 -0
  15. package/dist/analyze/documentation/validateReferencesAsync.js +148 -0
  16. package/dist/analyze/exports.d.ts +4 -0
  17. package/dist/analyze/exports.js +14 -7
  18. package/dist/analyze/readmeConfig.d.ts +1 -3
  19. package/dist/analyze/readmeConfig.js +8 -19
  20. package/dist/analyze/readmeUsage.d.ts +7 -10
  21. package/dist/analyze/readmeUsage.js +124 -48
  22. package/dist/analyze/semantic/docBlocks.js +18 -48
  23. package/dist/analyze/semantic/exports.js +1 -3
  24. package/dist/analyze/semantic/model.d.ts +0 -2
  25. package/dist/analyze/semantic/paradoxComment.d.ts +1 -11
  26. package/dist/analyze/semantic/paradoxComment.js +1 -43
  27. package/dist/analyze/semantic/tagRegistry.js +2 -1
  28. package/dist/analyze/sourceFunctions.js +4 -4
  29. package/dist/analyze/types.d.ts +31 -24
  30. package/dist/analyze/usage.d.ts +2 -2
  31. package/dist/analyze/usage.js +3 -33
  32. package/dist/analyze/utils/getExportMetadata.js +11 -40
  33. package/dist/analyze/utils/parseParadoxComment.d.ts +12 -9
  34. package/dist/analyze/utils/parseParadoxComment.js +66 -78
  35. package/dist/cli/index.d.ts +3 -2
  36. package/dist/cli/index.js +3 -2
  37. package/dist/cli/standalone.js +11 -0
  38. package/dist/config/defineParadoxConfig.d.ts +1 -1
  39. package/dist/doc-tags/registry.d.ts +28 -32
  40. package/dist/doc-tags/registry.js +35 -39
  41. package/dist/index.d.ts +1 -1
  42. package/dist/model/buildModel.d.ts +26 -19
  43. package/dist/model/buildModel.js +33 -99
  44. package/dist/model/types.d.ts +27 -20
  45. package/dist/paths/policy.d.ts +1 -1
  46. package/dist/render/renderers/diagrams.js +1 -7
  47. package/dist/render/renderers/html.js +89 -58
  48. package/dist/render/renderers/markdown.js +139 -86
  49. package/dist/render/toFileStem.d.ts +2 -0
  50. package/dist/render/toFileStem.js +8 -0
  51. package/dist/{config/types.d.ts → types/config.d.ts} +3 -5
  52. package/dist/write/write.d.ts +1 -1
  53. package/package.json +2 -1
  54. package/dist/analyze/readmeCli.d.ts +0 -9
  55. package/dist/analyze/readmeCli.js +0 -33
  56. package/dist/analyze/utils/getLeadingParadoxComment.d.ts +0 -10
  57. package/dist/analyze/utils/getLeadingParadoxComment.js +0 -16
  58. /package/dist/{config/types.js → types/config.js} +0 -0
@@ -16,9 +16,8 @@ export function collectDocBlocks(sourceFile, options) {
16
16
  const end = start + raw.length;
17
17
  const { line, column } = sourceFile.getLineAndColumnAtPos(start);
18
18
  const parsed = parseDocBlock(raw, tagRegistry);
19
- const id = `${sourcePath}:${line}:${column}`;
20
19
  blocks.push({
21
- id,
20
+ id: `${sourcePath}:${line}:${column}`,
22
21
  sourcePath,
23
22
  start,
24
23
  end,
@@ -26,8 +25,6 @@ export function collectDocBlocks(sourceFile, options) {
26
25
  column,
27
26
  raw,
28
27
  description: parsed.description,
29
- params: parsed.params,
30
- returns: parsed.returns,
31
28
  tags: parsed.tags,
32
29
  });
33
30
  }
@@ -37,62 +34,35 @@ export function collectDocBlocks(sourceFile, options) {
37
34
  * Extracts registered tags from a doc block.
38
35
  */
39
36
  function collectTags(raw, tagRegistry = defaultTagRegistry) {
40
- const tags = [];
41
- const lines = normalizeDocBlock(raw);
42
- for (const line of lines) {
43
- const trimmed = line.trim();
44
- if (!trimmed.startsWith('@'))
45
- continue;
46
- const [tagName, ...rest] = trimmed.slice(1).split(/\s+/);
47
- if (!tagName || !tagRegistry.has(tagName))
48
- continue;
49
- const value = rest.join(' ').trim();
50
- tags.push({
51
- name: tagName,
52
- value: value.length > 0 ? value : null,
53
- });
54
- }
55
- return tags;
37
+ return normalizeDocBlock(raw).flatMap((line) => {
38
+ const match = /^@([A-Za-z][A-Za-z0-9-]*)(?:\s+(.*))?$/.exec(line.trim());
39
+ if (match === null)
40
+ return [];
41
+ const [, name] = match;
42
+ if (!tagRegistry.has(name))
43
+ return [];
44
+ const value = match.slice(2).join('').trim();
45
+ return [{ name, value: value.length > 0 ? value : null }];
46
+ });
56
47
  }
48
+ /***
49
+ * Parses semantic description and supported tags without interpreting unsupported tag syntax.
50
+ */
57
51
  function parseDocBlock(raw, tagRegistry) {
58
52
  const lines = normalizeDocBlock(raw);
59
53
  const tags = collectTags(raw, tagRegistry);
60
- const params = {};
61
- let returns = null;
62
54
  const description = lines
63
- .filter((line) => {
64
- const trimmed = line.trim();
65
- if (!trimmed.startsWith('@'))
66
- return true;
67
- const [tagName, ...rest] = trimmed.slice(1).split(/\s+/);
68
- if (!tagName)
69
- return false;
70
- if (tagName === 'param') {
71
- const [name, ...descParts] = rest;
72
- if (name) {
73
- params[name] = descParts.join(' ').trim();
74
- }
75
- return false;
76
- }
77
- if (tagName === 'returns' || tagName === 'return') {
78
- const returnBody = rest.join(' ').trim();
79
- returns = returnBody.length > 0 ? returnBody : null;
80
- return false;
81
- }
82
- if (tagRegistry.has(tagName)) {
83
- return false;
84
- }
85
- return true;
86
- })
55
+ .filter((line) => !/^@[A-Za-z][A-Za-z0-9-]*(?:\s|$)/.test(line.trim()))
87
56
  .join('\n')
88
57
  .trim();
89
58
  return {
90
59
  description: description.length > 0 ? description : null,
91
60
  tags,
92
- params,
93
- returns,
94
61
  };
95
62
  }
63
+ /***
64
+ * Removes Paradox comment delimiters while preserving prose content.
65
+ */
96
66
  function normalizeDocBlock(raw) {
97
67
  return raw
98
68
  .replace(/^\/\*\*\*/, '')
@@ -226,9 +226,7 @@ function collectMembersFromType(program, reference, inheritedFrom, depth) {
226
226
  return [];
227
227
  const propertyType = property.getTypeAtLocation(declaration);
228
228
  const rawComment = getParadoxComment(declaration);
229
- const parsed = rawComment
230
- ? parseParadoxComment(rawComment)
231
- : { description: null, isConfig: false, params: {}, returns: null };
229
+ const parsed = rawComment ? parseParadoxComment(rawComment) : { description: null };
232
230
  const required = !property.isOptional() && !isUndefinedUnion(propertyType);
233
231
  const childReference = shouldExpandType(program, propertyType)
234
232
  ? resolveTypeReference(program, propertyType)
@@ -20,8 +20,6 @@ export interface AnalyzedDocBlock {
20
20
  column: number;
21
21
  raw: string;
22
22
  description: string | null;
23
- params: Record<string, string>;
24
- returns: string | null;
25
23
  tags: AnalyzedTag[];
26
24
  }
27
25
  export interface AnalyzedTag {
@@ -1,16 +1,6 @@
1
1
  import type { Node } from 'ts-morph';
2
+ export { parseParadoxComment } from '../utils/parseParadoxComment.js';
2
3
  /***
3
4
  * Reads the nearest Paradox doc comment attached to a declaration.
4
5
  */
5
6
  export declare function getParadoxComment(node: Node | undefined): string | null;
6
- interface ParsedParadoxComment {
7
- description: string | null;
8
- isConfig: boolean;
9
- params: Record<string, string>;
10
- returns: string | null;
11
- }
12
- /***
13
- * Parses a Paradox doc comment into structured metadata.
14
- */
15
- export declare function parseParadoxComment(rawComment: string): ParsedParadoxComment;
16
- export {};
@@ -1,3 +1,4 @@
1
+ export { parseParadoxComment } from '../utils/parseParadoxComment.js';
1
2
  /***
2
3
  * Reads the nearest Paradox doc comment attached to a declaration.
3
4
  */
@@ -19,46 +20,3 @@ export function getParadoxComment(node) {
19
20
  return null;
20
21
  return text.slice(commentStart, commentEnd + 2);
21
22
  }
22
- /***
23
- * Parses a Paradox doc comment into structured metadata.
24
- */
25
- export function parseParadoxComment(rawComment) {
26
- const lines = rawComment
27
- .replace(/^\/\*\*\*/, '')
28
- .replace(/\*\/$/, '')
29
- .split('\n')
30
- .map((line) => line.replace(/^\s*\*\s?/, '').trimEnd());
31
- let isConfig = false;
32
- const params = {};
33
- let returns = null;
34
- const description = lines
35
- .filter((line) => {
36
- const trimmed = line.trimStart();
37
- if (trimmed.startsWith('@config')) {
38
- isConfig = true;
39
- return false;
40
- }
41
- if (trimmed.startsWith('@param ')) {
42
- const paramBody = trimmed.slice('@param '.length).trim();
43
- const [name, ...descriptionParts] = paramBody.split(/\s+/);
44
- if (name) {
45
- params[name] = descriptionParts.join(' ').trim();
46
- }
47
- return false;
48
- }
49
- if (trimmed.startsWith('@returns') || trimmed.startsWith('@return')) {
50
- const returnBody = trimmed.replace(/^@returns?/, '').trim();
51
- returns = returnBody.length > 0 ? returnBody : null;
52
- return false;
53
- }
54
- return true;
55
- })
56
- .join('\n')
57
- .trim();
58
- return {
59
- description: description.length > 0 ? description : null,
60
- isConfig,
61
- params,
62
- returns,
63
- };
64
- }
@@ -1 +1,2 @@
1
- export const defaultTagRegistry = new Set(['readme', 'config']);
1
+ import { DOCUMENTATION_POLICY } from '@ankhorage/policy/documentation';
2
+ export const defaultTagRegistry = new Set(DOCUMENTATION_POLICY.tags.map((tag) => tag.name));
@@ -43,12 +43,12 @@ function createSourceFunction(name, node, root) {
43
43
  const sourceFile = node.getSourceFile();
44
44
  const { column, line } = sourceFile.getLineAndColumnAtPos(node.getStart(false));
45
45
  const rawComment = getParadoxComment(node);
46
- const parsedComment = rawComment
47
- ? parseParadoxComment(rawComment)
48
- : { description: null, isReadme: false };
46
+ const parsedComment = rawComment === null ? null : parseParadoxComment(rawComment);
49
47
  return {
50
48
  name,
51
- description: parsedComment.description,
49
+ description: parsedComment?.description ?? null,
50
+ see: parsedComment?.see ?? [],
51
+ security: parsedComment?.security ?? [],
52
52
  sourceLocation: {
53
53
  filePath: toPosixPath(relative(root, sourceFile.getFilePath())),
54
54
  line,
@@ -1,9 +1,5 @@
1
+ import type { PolicySeverity } from '@ankhorage/policy/status';
1
2
  import type { Node } from 'ts-morph';
2
- interface AnalysisExample {
3
- title: string | null;
4
- language: string | null;
5
- code: string;
6
- }
7
3
  /***
8
4
  * Describes one exported declaration discovered in a package.
9
5
  */
@@ -40,9 +36,11 @@ export interface AnalysisStructuredRow {
40
36
  export interface AnalysisExport {
41
37
  name: string;
42
38
  node: Node;
39
+ title: string | null;
43
40
  description: string | null;
44
41
  isReadme: boolean;
45
- examples: AnalysisExample[];
42
+ see: string[];
43
+ security: string[];
46
44
  kind: 'function' | 'type' | 'value' | 'unknown';
47
45
  modulePath: string;
48
46
  sourceLocation: AnalysisSourceLocation;
@@ -53,13 +51,14 @@ export interface AnalysisExport {
53
51
  structuredRows: AnalysisStructuredRow[];
54
52
  }
55
53
  /***
56
- * Describes one React component and its extracted props.
54
+ * Describes one React component and its props.
57
55
  */
58
56
  export interface AnalysisComponent {
59
57
  name: string;
60
58
  description: string | null;
61
59
  isReadme: boolean;
62
- examples: AnalysisExample[];
60
+ see: string[];
61
+ security: string[];
63
62
  modulePath: string;
64
63
  sourceLocation: AnalysisSourceLocation;
65
64
  exportPaths: string[];
@@ -73,28 +72,30 @@ export interface AnalysisComponent {
73
72
  }
74
73
  export interface AnalysisUsage {
75
74
  packageName: string;
76
- commands: AnalysisUsageCommand[];
77
- }
78
- interface AnalysisDonation {
79
- account: string;
80
- }
81
- interface AnalysisUsageCommand {
82
- name: string;
83
75
  command: string;
84
76
  }
85
- interface AnalysisReadmeUsage {
77
+ export interface AnalysisUsageEntry {
78
+ area: 'cli' | 'examples';
86
79
  title: string | null;
87
80
  description: string | null;
88
81
  language: string;
89
82
  code: string;
90
83
  sourcePath: string;
84
+ isReadme: boolean;
85
+ see: string[];
86
+ security: string[];
91
87
  }
92
- interface AnalysisReadmeCli {
93
- description: string | null;
94
- sourcePath: string;
88
+ export interface AnalysisDocumentationFinding {
89
+ ruleId: string;
90
+ severity: PolicySeverity;
91
+ message: string;
92
+ sourcePath: string | null;
93
+ line: number | null;
94
+ }
95
+ interface AnalysisDonation {
96
+ account: string;
95
97
  }
96
98
  interface AnalysisReadmeConfig {
97
- description: string | null;
98
99
  language: string;
99
100
  code: string;
100
101
  sourcePath: string;
@@ -122,6 +123,8 @@ export interface AnalysisSequenceScenario {
122
123
  export interface AnalysisSourceFunction {
123
124
  name: string;
124
125
  description: string | null;
126
+ see: string[];
127
+ security: string[];
125
128
  sourceLocation: AnalysisSourceLocation;
126
129
  }
127
130
  interface AnalysisTypeMember {
@@ -177,14 +180,18 @@ export interface AnalysisResult {
177
180
  modules: AnalysisModule[];
178
181
  badges: AnalysisBadge[];
179
182
  sequenceScenarios: AnalysisSequenceScenario[];
180
- usage: AnalysisUsage | null;
181
- readmeUsageDescription: string | null;
182
- readmeUsage: AnalysisReadmeUsage[];
183
- readmeCli: AnalysisReadmeCli | null;
183
+ usage: AnalysisUsage;
184
+ usageEntries: AnalysisUsageEntry[];
185
+ exampleCount: number;
186
+ findings: AnalysisDocumentationFinding[];
184
187
  readmeConfig: AnalysisReadmeConfig | null;
185
188
  config: {
186
189
  exportName: string;
190
+ title: string | null;
191
+ description: string | null;
187
192
  isReadme: boolean;
193
+ see: string[];
194
+ security: string[];
188
195
  members: AnalysisTypeMember[];
189
196
  } | null;
190
197
  graphs: AnalysisGraphs;
@@ -11,6 +11,6 @@ export interface PackageJsonModel {
11
11
  prettier?: unknown;
12
12
  }
13
13
  /***
14
- * Builds executable CLI commands from package metadata.
14
+ * Builds the canonical Ankh package help command from package metadata.
15
15
  */
16
- export declare function createUsageFromPackageJson(pkg: PackageJsonModel): AnalysisUsage | null;
16
+ export declare function createUsageFromPackageJson(pkg: PackageJsonModel): AnalysisUsage;
@@ -1,41 +1,11 @@
1
1
  /***
2
- * Builds executable CLI commands from package metadata.
2
+ * Builds the canonical Ankh package help command from package metadata.
3
3
  */
4
4
  export function createUsageFromPackageJson(pkg) {
5
- if (pkg.bin == null)
6
- return null;
7
- if (typeof pkg.bin === 'string') {
8
- return {
9
- packageName: pkg.name,
10
- commands: [
11
- {
12
- name: getPackageBaseName(pkg.name),
13
- command: `bunx ${pkg.name}`,
14
- },
15
- ],
16
- };
17
- }
18
- const entries = Object.keys(pkg.bin).sort((a, b) => a.localeCompare(b));
19
- if (entries.length === 0)
20
- return null;
21
- if (entries.length === 1) {
22
- const [name] = entries;
23
- return {
24
- packageName: pkg.name,
25
- commands: [
26
- {
27
- name,
28
- command: `bunx ${pkg.name}`,
29
- },
30
- ],
31
- };
32
- }
5
+ const packageName = getPackageBaseName(pkg.name);
33
6
  return {
34
7
  packageName: pkg.name,
35
- commands: entries.map((name) => ({
36
- name,
37
- command: `bunx ${pkg.name} ${name}`,
38
- })),
8
+ command: `ankh ${packageName} --help`,
39
9
  };
40
10
  }
41
11
  /***
@@ -42,8 +42,7 @@ function getSourceLocation(node, root) {
42
42
  * Extracts callable signatures for exported functions and callable values.
43
43
  */
44
44
  function getSignatures(symbol, node, root) {
45
- const parsed = readParadoxMetadata(node);
46
- const signatures = getCallableDeclarations(symbol, node).map((declaration) => getSignature(declaration, parsed.params, parsed.returns, root));
45
+ const signatures = getCallableDeclarations(symbol, node).map((declaration) => getSignature(declaration, root));
47
46
  return uniqueBy(signatures.filter((signature) => signature.parameters.length > 0 ||
48
47
  signature.returnType !== null ||
49
48
  signature.returnDescription !== null), (signature) => signature.label);
@@ -51,16 +50,13 @@ function getSignatures(symbol, node, root) {
51
50
  /***
52
51
  * Builds one normalized call signature from a callable declaration.
53
52
  */
54
- function getSignature(declaration, params, returns, root) {
55
- const normalizedParameters = declaration.getParameters().map((parameter) => {
56
- const parameterDescription = params[parameter.getName()];
57
- return {
58
- name: parameter.getName(),
59
- type: normalizeTypeText(parameter.getType().getText(parameter), root),
60
- required: !parameter.isOptional(),
61
- description: parameterDescription ? parameterDescription.trim() : null,
62
- };
63
- });
53
+ function getSignature(declaration, root) {
54
+ const normalizedParameters = declaration.getParameters().map((parameter) => ({
55
+ name: parameter.getName(),
56
+ type: normalizeTypeText(parameter.getType().getText(parameter), root),
57
+ required: !parameter.isOptional(),
58
+ description: null,
59
+ }));
64
60
  const returnType = normalizeTypeText(declaration.getReturnType().getText(declaration), root);
65
61
  const parameterLabel = normalizedParameters
66
62
  .map((parameter) => `${parameter.name}${parameter.required ? '' : '?'}: ${parameter.type}`)
@@ -69,7 +65,7 @@ function getSignature(declaration, params, returns, root) {
69
65
  label: `(${parameterLabel})${returnType === 'void' ? '' : ` => ${returnType}`}`,
70
66
  parameters: normalizedParameters,
71
67
  returnType,
72
- returnDescription: returns,
68
+ returnDescription: null,
73
69
  };
74
70
  }
75
71
  /***
@@ -99,23 +95,14 @@ function getMembersFromProperties(properties, root) {
99
95
  return [];
100
96
  }
101
97
  const rawComment = getParadoxComment(declaration);
102
- const parsed = rawComment
103
- ? parseParadoxComment(rawComment)
104
- : {
105
- description: null,
106
- isConfig: false,
107
- isReadme: false,
108
- examples: [],
109
- params: {},
110
- returns: null,
111
- };
98
+ const parsed = rawComment === null ? null : parseParadoxComment(rawComment);
112
99
  return [
113
100
  {
114
101
  name: property.getName(),
115
102
  kind: isMemberMethodDeclaration(declaration) ? 'method' : 'property',
116
103
  type: normalizeTypeText(property.getTypeAtLocation(declaration).getText(declaration), root),
117
104
  required: !property.isOptional(),
118
- description: parsed.description,
105
+ description: parsed?.description ?? null,
119
106
  },
120
107
  ];
121
108
  });
@@ -256,22 +243,6 @@ function getCallableNode(node) {
256
243
  function isMemberMethodDeclaration(node) {
257
244
  return Node.isMethodDeclaration(node) || Node.isMethodSignature(node);
258
245
  }
259
- /***
260
- * Reads Paradox comment metadata from a declaration.
261
- */
262
- function readParadoxMetadata(node) {
263
- const rawComment = getParadoxComment(node);
264
- return rawComment
265
- ? parseParadoxComment(rawComment)
266
- : {
267
- description: null,
268
- isConfig: false,
269
- isReadme: false,
270
- examples: [],
271
- params: {},
272
- returns: null,
273
- };
274
- }
275
246
  /***
276
247
  * Finds related exported symbols mentioned in signature and member type text.
277
248
  */
@@ -1,22 +1,25 @@
1
+ import { type ParadoxDocTagName } from '../../doc-tags/registry.js';
2
+ interface ParsedParadoxTag {
3
+ readonly name: ParadoxDocTagName;
4
+ readonly value: string | null;
5
+ }
1
6
  /***
2
- * Parsed representation of a Paradox doc comment.
7
+ * Parsed representation of a Paradox documentation comment.
3
8
  */
4
9
  export interface ParsedParadoxComment {
5
10
  description: string | null;
6
11
  isConfig: boolean;
7
12
  isReadme: boolean;
8
13
  isUsage: boolean;
9
- examples: ParsedExample[];
10
- params: Record<string, string>;
11
- returns: string | null;
12
- }
13
- interface ParsedExample {
14
14
  title: string | null;
15
- language: string | null;
16
- code: string;
15
+ see: string[];
16
+ security: string[];
17
+ tags: ParsedParadoxTag[];
18
+ unsupportedTags: string[];
19
+ hasCodeBlock: boolean;
17
20
  }
18
21
  /***
19
- * Parses a Paradox doc comment into structured metadata.
22
+ * Parses a Paradox comment into prose, supported tags, and validation evidence.
20
23
  */
21
24
  export declare function parseParadoxComment(rawComment: string): ParsedParadoxComment;
22
25
  export {};
@@ -1,97 +1,85 @@
1
- const USAGE_TAG = `${String.fromCharCode(64)}usage`;
1
+ import { isParadoxDocTagName } from '../../doc-tags/registry.js';
2
2
  /***
3
- * Parses a Paradox doc comment into structured metadata.
3
+ * Parses a Paradox comment into prose, supported tags, and validation evidence.
4
4
  */
5
5
  export function parseParadoxComment(rawComment) {
6
6
  const lines = normalizeCommentLines(rawComment);
7
- const descriptionLines = [];
8
- const examples = [];
9
- let isConfig = false;
10
- let isReadme = false;
11
- let isUsage = false;
12
- const params = {};
13
- let returns = null;
14
- for (let index = 0; index < lines.length; index += 1) {
15
- const line = lines[index] ?? '';
16
- const trimmed = line.trimStart();
17
- if (trimmed.startsWith('@config')) {
18
- isConfig = true;
19
- continue;
20
- }
21
- if (trimmed.startsWith('@readme')) {
22
- isReadme = true;
23
- continue;
24
- }
25
- if (trimmed.startsWith(USAGE_TAG)) {
26
- isUsage = true;
27
- continue;
28
- }
29
- if (trimmed.startsWith('@example')) {
30
- const parsed = parseExample(lines, index);
31
- examples.push(parsed.example);
32
- index = parsed.nextIndex;
33
- continue;
34
- }
35
- if (trimmed.startsWith('@param ')) {
36
- const paramBody = trimmed.slice('@param '.length).trim();
37
- const [name, ...descriptionParts] = paramBody.split(/\s+/);
38
- if (name) {
39
- params[name] = descriptionParts.join(' ').trim();
40
- }
41
- continue;
42
- }
43
- if (trimmed.startsWith('@returns') || trimmed.startsWith('@return')) {
44
- const returnBody = trimmed.replace(/^@returns?/, '').trim();
45
- returns = returnBody.length > 0 ? returnBody : null;
46
- continue;
47
- }
48
- descriptionLines.push(line);
49
- }
50
- const description = descriptionLines.join('\n').trim();
7
+ const parsedLines = lines.map(parseCommentLine);
8
+ const tags = parsedLines.flatMap((line) => line.tag ?? []);
9
+ const unsupportedTags = parsedLines.flatMap((line) => line.unsupportedTag ?? []);
10
+ const description = parsedLines
11
+ .filter((line) => line.tag === undefined && line.unsupportedTag === undefined)
12
+ .map((line) => line.text)
13
+ .join('\n')
14
+ .trim();
51
15
  return {
52
16
  description: description.length > 0 ? description : null,
53
- isConfig,
54
- isReadme,
55
- isUsage,
56
- examples,
57
- params,
58
- returns,
17
+ isConfig: hasTag(tags, 'config'),
18
+ isReadme: hasTag(tags, 'readme'),
19
+ isUsage: hasTag(tags, 'usage'),
20
+ title: getSingleTagValue(tags, 'title'),
21
+ see: getTagValues(tags, 'see'),
22
+ security: getTagValues(tags, 'security'),
23
+ tags,
24
+ unsupportedTags,
25
+ hasCodeBlock: hasCodeBlock(lines),
59
26
  };
60
27
  }
61
- function parseExample(lines, startIndex) {
62
- const header = lines[startIndex]?.trimStart() ?? '';
63
- const title = header.slice('@example'.length).trim();
64
- let language = null;
65
- const codeLines = [];
66
- let index = startIndex + 1;
67
- while (index < lines.length && (lines[index] ?? '').trim() === '') {
68
- index += 1;
69
- }
70
- const firstCodeLine = lines[index]?.trim() ?? '';
71
- if (firstCodeLine.startsWith('```')) {
72
- language = firstCodeLine.slice('```'.length).trim() || null;
73
- index += 1;
74
- while (index < lines.length) {
75
- const current = lines[index] ?? '';
76
- if (current.trim() === '```')
77
- break;
78
- codeLines.push(current);
79
- index += 1;
80
- }
28
+ /***
29
+ * Parses one normalized comment line when it has explicit tag-line syntax.
30
+ */
31
+ function parseCommentLine(text) {
32
+ const trimmed = text.trim();
33
+ const match = /^@([A-Za-z][A-Za-z0-9-]*)(?:\s+(.*))?$/.exec(trimmed);
34
+ if (match === null)
35
+ return { text };
36
+ const [, name] = match;
37
+ const value = match.slice(2).join('').trim();
38
+ if (!isParadoxDocTagName(name)) {
39
+ return { text, unsupportedTag: name };
81
40
  }
82
41
  return {
83
- example: {
84
- title: title.length > 0 ? title : null,
85
- language,
86
- code: codeLines.join('\n').trimEnd(),
42
+ text,
43
+ tag: {
44
+ name,
45
+ value: value.length > 0 ? value : null,
87
46
  },
88
- nextIndex: index,
89
47
  };
90
48
  }
49
+ /***
50
+ * Returns whether a parsed tag set contains the requested tag.
51
+ */
52
+ function hasTag(tags, name) {
53
+ return tags.some((tag) => tag.name === name);
54
+ }
55
+ /***
56
+ * Returns all non-empty values for a repeatable tag.
57
+ */
58
+ function getTagValues(tags, name) {
59
+ return tags.flatMap((tag) => (tag.name === name && tag.value !== null ? [tag.value] : []));
60
+ }
61
+ /***
62
+ * Returns the first non-empty value for a singular tag.
63
+ */
64
+ function getSingleTagValue(tags, name) {
65
+ return getTagValues(tags, name)[0] ?? null;
66
+ }
67
+ /***
68
+ * Detects fenced or Markdown-indented code blocks while leaving inline code spans untouched.
69
+ */
70
+ function hasCodeBlock(lines) {
71
+ return lines.some((line) => {
72
+ const trimmed = line.trimStart();
73
+ return trimmed.startsWith('```') || trimmed.startsWith('~~~') || /^(?: {4}|\t)\S/.test(line);
74
+ });
75
+ }
76
+ /***
77
+ * Removes Paradox comment syntax while preserving prose indentation for code-block validation.
78
+ */
91
79
  function normalizeCommentLines(rawComment) {
92
80
  return rawComment
93
81
  .replace(/^\/\*\*\*/, '')
94
82
  .replace(/\*\/$/, '')
95
83
  .split('\n')
96
- .map((line) => line.replace(/^\s*\*\s?/, '').trimEnd());
84
+ .map((line) => line.replace(/^\s*\* ?/, '').replace(/\s+$/, ''));
97
85
  }