@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.
- package/CHANGELOG.md +8 -0
- package/README.md +53 -61
- package/dist/analyze/analyze.d.ts +2 -2
- package/dist/analyze/analyze.js +51 -35
- package/dist/analyze/badges.d.ts +2 -1
- package/dist/analyze/badges.js +14 -4
- package/dist/analyze/components.js +12 -11
- package/dist/analyze/documentation/collectDocumentationCommentsAsync.d.ts +11 -0
- package/dist/analyze/documentation/collectDocumentationCommentsAsync.js +72 -0
- package/dist/analyze/documentation/findings.d.ts +5 -0
- package/dist/analyze/documentation/findings.js +17 -0
- package/dist/analyze/documentation/validateDocumentationPolicyAsync.d.ts +13 -0
- package/dist/analyze/documentation/validateDocumentationPolicyAsync.js +150 -0
- package/dist/analyze/documentation/validateReferencesAsync.d.ts +11 -0
- package/dist/analyze/documentation/validateReferencesAsync.js +148 -0
- package/dist/analyze/exports.d.ts +4 -0
- package/dist/analyze/exports.js +14 -7
- package/dist/analyze/readmeConfig.d.ts +1 -3
- package/dist/analyze/readmeConfig.js +8 -19
- package/dist/analyze/readmeUsage.d.ts +7 -10
- package/dist/analyze/readmeUsage.js +124 -48
- package/dist/analyze/semantic/docBlocks.js +18 -48
- package/dist/analyze/semantic/exports.js +1 -3
- package/dist/analyze/semantic/model.d.ts +0 -2
- package/dist/analyze/semantic/paradoxComment.d.ts +1 -11
- package/dist/analyze/semantic/paradoxComment.js +1 -43
- package/dist/analyze/semantic/tagRegistry.js +2 -1
- package/dist/analyze/sourceFunctions.js +4 -4
- package/dist/analyze/types.d.ts +31 -24
- package/dist/analyze/usage.d.ts +2 -2
- package/dist/analyze/usage.js +3 -33
- package/dist/analyze/utils/getExportMetadata.js +11 -40
- package/dist/analyze/utils/parseParadoxComment.d.ts +12 -9
- package/dist/analyze/utils/parseParadoxComment.js +66 -78
- package/dist/cli/index.d.ts +3 -2
- package/dist/cli/index.js +3 -2
- package/dist/cli/standalone.js +11 -0
- package/dist/config/defineParadoxConfig.d.ts +1 -1
- package/dist/doc-tags/registry.d.ts +28 -32
- package/dist/doc-tags/registry.js +35 -39
- package/dist/index.d.ts +1 -1
- package/dist/model/buildModel.d.ts +26 -19
- package/dist/model/buildModel.js +33 -99
- package/dist/model/types.d.ts +27 -20
- package/dist/paths/policy.d.ts +1 -1
- package/dist/render/renderers/diagrams.js +1 -7
- package/dist/render/renderers/html.js +89 -58
- package/dist/render/renderers/markdown.js +139 -86
- package/dist/render/toFileStem.d.ts +2 -0
- package/dist/render/toFileStem.js +8 -0
- package/dist/{config/types.d.ts → types/config.d.ts} +3 -5
- package/dist/write/write.d.ts +1 -1
- package/package.json +2 -1
- package/dist/analyze/readmeCli.d.ts +0 -9
- package/dist/analyze/readmeCli.js +0 -33
- package/dist/analyze/utils/getLeadingParadoxComment.d.ts +0 -10
- package/dist/analyze/utils/getLeadingParadoxComment.js +0 -16
- /package/dist/{config/types.js → types/config.js} +0 -0
|
@@ -4,7 +4,6 @@ import { slugifyAscii } from '@ankhorage/utility/string';
|
|
|
4
4
|
*/
|
|
5
5
|
export function renderHtml({ diagrams, model, }) {
|
|
6
6
|
const sourceAreas = getSourceAreas(model);
|
|
7
|
-
const cliScenarios = getReadmeCliScenarios(model);
|
|
8
7
|
const exportsByModule = groupBy(model.exports, (item) => item.modulePath);
|
|
9
8
|
return {
|
|
10
9
|
indexHtml: `<!doctype html>
|
|
@@ -141,7 +140,7 @@ export function renderHtml({ diagrams, model, }) {
|
|
|
141
140
|
</aside>
|
|
142
141
|
<main class="content">
|
|
143
142
|
<section id="view-home" class="view" data-view="home">
|
|
144
|
-
${renderHomeView(model, diagrams,
|
|
143
|
+
${renderHomeView(model, diagrams, exportsByModule)}
|
|
145
144
|
</section>
|
|
146
145
|
${sourceAreas.map(renderSourceAreaView).join('')}
|
|
147
146
|
</main>
|
|
@@ -197,7 +196,7 @@ export function renderHtml({ diagrams, model, }) {
|
|
|
197
196
|
/***
|
|
198
197
|
* Renders the Home view that keeps public API and package-level information together.
|
|
199
198
|
*/
|
|
200
|
-
function renderHomeView(model, diagrams,
|
|
199
|
+
function renderHomeView(model, diagrams, exportsByModule) {
|
|
201
200
|
return `
|
|
202
201
|
<section class="panel">
|
|
203
202
|
<h2>Package overview</h2>
|
|
@@ -212,7 +211,8 @@ function renderHomeView(model, diagrams, cliScenarios, exportsByModule) {
|
|
|
212
211
|
${model.entrypoints.map((entrypoint) => `<li><code>${escapeHtml(entrypoint)}</code></li>`).join('')}
|
|
213
212
|
</ul>
|
|
214
213
|
</section>
|
|
215
|
-
${
|
|
214
|
+
${renderUsagePanel(model)}
|
|
215
|
+
${renderFindingsPanel(model)}
|
|
216
216
|
<section class="panel">
|
|
217
217
|
<h2>Modules</h2>
|
|
218
218
|
${model.modules.map(renderModuleCard).join('')}
|
|
@@ -237,36 +237,62 @@ function renderHomeView(model, diagrams, cliScenarios, exportsByModule) {
|
|
|
237
237
|
</section>`;
|
|
238
238
|
}
|
|
239
239
|
/***
|
|
240
|
-
* Renders
|
|
240
|
+
* Renders complete CLI and programmatic usage documentation.
|
|
241
241
|
*/
|
|
242
|
-
function
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
model.
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
return '';
|
|
259
|
-
return `<article class="item" data-search="${escapeAttribute([scenario.name, scenario.description ?? ''].join(' '))}">
|
|
260
|
-
<h3>${escapeHtml(scenario.name)}</h3>
|
|
261
|
-
${scenario.description === null ? '' : `<p>${escapeHtml(scenario.description)}</p>`}
|
|
262
|
-
${diagram === undefined ? '' : renderDiagramCard(diagram)}
|
|
263
|
-
</article>`;
|
|
264
|
-
})
|
|
265
|
-
.join('')}
|
|
242
|
+
function renderUsagePanel(model) {
|
|
243
|
+
return `<section class="panel" data-search="${escapeAttribute([
|
|
244
|
+
'usage',
|
|
245
|
+
model.usage.command,
|
|
246
|
+
...model.usageEntries.flatMap((entry) => [
|
|
247
|
+
entry.title ?? '',
|
|
248
|
+
entry.description ?? '',
|
|
249
|
+
entry.sourcePath,
|
|
250
|
+
]),
|
|
251
|
+
].join(' '))}">
|
|
252
|
+
<h2>Usage</h2>
|
|
253
|
+
<article class="item">
|
|
254
|
+
<h3>CLI</h3>
|
|
255
|
+
<pre>${escapeHtml(model.usage.command)}</pre>
|
|
256
|
+
</article>
|
|
257
|
+
${model.usageEntries.map(renderUsageEntry).join('')}
|
|
266
258
|
</section>`;
|
|
267
259
|
}
|
|
268
260
|
/***
|
|
269
|
-
* Renders one source
|
|
261
|
+
* Renders one source-backed usage entry in the complete documentation app.
|
|
262
|
+
*/
|
|
263
|
+
function renderUsageEntry(entry) {
|
|
264
|
+
return `<article class="item" data-search="${escapeAttribute([
|
|
265
|
+
entry.title ?? '',
|
|
266
|
+
entry.description ?? '',
|
|
267
|
+
entry.sourcePath,
|
|
268
|
+
...entry.see,
|
|
269
|
+
...entry.security,
|
|
270
|
+
].join(' '))}">
|
|
271
|
+
<h3>${escapeHtml(entry.title ?? 'Usage')}</h3>
|
|
272
|
+
<p class="muted"><code>${escapeHtml(entry.sourcePath)}</code></p>
|
|
273
|
+
${entry.description === null ? '' : `<p>${escapeHtml(entry.description)}</p>`}
|
|
274
|
+
${renderReferenceMetadata(entry)}
|
|
275
|
+
<pre>${escapeHtml(entry.code)}</pre>
|
|
276
|
+
</article>`;
|
|
277
|
+
}
|
|
278
|
+
/***
|
|
279
|
+
* Renders policy findings so complete docs expose the same evidence consumed by Doctor.
|
|
280
|
+
*/
|
|
281
|
+
function renderFindingsPanel(model) {
|
|
282
|
+
if (model.findings.length === 0) {
|
|
283
|
+
return '<section class="panel"><h2>Policy</h2><p>No documentation findings.</p></section>';
|
|
284
|
+
}
|
|
285
|
+
return `<section class="panel">
|
|
286
|
+
<h2>Policy findings</h2>
|
|
287
|
+
${model.findings
|
|
288
|
+
.map((finding) => `<article class="item" data-search="${escapeAttribute([finding.ruleId, finding.severity, finding.message, finding.sourcePath ?? ''].join(' '))}">
|
|
289
|
+
<h3>${escapeHtml(finding.ruleId)}</h3>
|
|
290
|
+
<p><strong>${escapeHtml(finding.severity)}</strong> — ${escapeHtml(finding.message)}</p>
|
|
291
|
+
${finding.sourcePath === null ? '' : `<p class="muted"><code>${escapeHtml(finding.sourcePath)}${finding.line === null ? '' : `:${finding.line}`}</code></p>`}
|
|
292
|
+
</article>`)
|
|
293
|
+
.join('')}
|
|
294
|
+
</section>`;
|
|
295
|
+
}
|
|
270
296
|
/***
|
|
271
297
|
* Renders one source file entry in the left navigation.
|
|
272
298
|
*/
|
|
@@ -293,10 +319,17 @@ function renderSourceAreaView(area) {
|
|
|
293
319
|
* Renders one source function card in a source-area view.
|
|
294
320
|
*/
|
|
295
321
|
function renderSourceFunctionCard(item) {
|
|
296
|
-
return `<article class="item" data-search="${escapeAttribute([
|
|
322
|
+
return `<article class="item" data-search="${escapeAttribute([
|
|
323
|
+
item.name,
|
|
324
|
+
item.sourceLocation.filePath,
|
|
325
|
+
item.description ?? '',
|
|
326
|
+
...item.see,
|
|
327
|
+
...item.security,
|
|
328
|
+
].join(' '))}">
|
|
297
329
|
<h3>${escapeHtml(item.name)}</h3>
|
|
298
330
|
<p class="muted"><code>${escapeHtml(item.sourceLocation.filePath)}:${item.sourceLocation.line}:${item.sourceLocation.column}</code></p>
|
|
299
331
|
${item.description === null ? '<p class="empty">No description available.</p>' : `<p>${escapeHtml(item.description)}</p>`}
|
|
332
|
+
${renderReferenceMetadata(item)}
|
|
300
333
|
</article>`;
|
|
301
334
|
}
|
|
302
335
|
/***
|
|
@@ -307,22 +340,6 @@ function getSourceAreas(model) {
|
|
|
307
340
|
.map(([path, functions]) => ({ path, functions }))
|
|
308
341
|
.sort((left, right) => left.path.localeCompare(right.path));
|
|
309
342
|
}
|
|
310
|
-
/***
|
|
311
|
-
* Selects bin scenarios that should be shown on the Home page.
|
|
312
|
-
*/
|
|
313
|
-
function getReadmeCliScenarios(model) {
|
|
314
|
-
if (model.readmeCli === null)
|
|
315
|
-
return [];
|
|
316
|
-
return model.sequenceScenarios.filter((scenario) => scenario.kind === 'bin');
|
|
317
|
-
}
|
|
318
|
-
/***
|
|
319
|
-
* Finds the generated Mermaid artifact for a sequence scenario.
|
|
320
|
-
/***
|
|
321
|
-
* Finds the generated Mermaid artifact for a sequence scenario.
|
|
322
|
-
*/
|
|
323
|
-
function findScenarioDiagram(diagrams, scenario) {
|
|
324
|
-
return diagrams.find((diagram) => diagram.path === `diagrams/sequences/${toFileStem(scenario.name)}.mmd`);
|
|
325
|
-
}
|
|
326
343
|
/***
|
|
327
344
|
* Renders module metadata on the Home page.
|
|
328
345
|
*/
|
|
@@ -340,15 +357,20 @@ function renderModuleCard(module) {
|
|
|
340
357
|
function renderExportCard(item) {
|
|
341
358
|
return `<article class="item" id="symbol-${slugifyAscii(item.name)}" data-search="${escapeAttribute([
|
|
342
359
|
item.name,
|
|
360
|
+
item.title ?? '',
|
|
343
361
|
item.kind,
|
|
344
362
|
item.modulePath,
|
|
345
363
|
item.description ?? '',
|
|
364
|
+
...item.see,
|
|
365
|
+
...item.security,
|
|
346
366
|
...item.relatedSymbols,
|
|
347
367
|
...item.signatures.map((signature) => signature.label),
|
|
348
368
|
].join(' '))}">
|
|
349
|
-
<h4>${escapeHtml(item.name)}</h4>
|
|
369
|
+
<h4>${escapeHtml(item.title ?? item.name)}</h4>
|
|
370
|
+
${item.title === null || item.title === item.name ? '' : `<p class="muted">Symbol: <code>${escapeHtml(item.name)}</code></p>`}
|
|
350
371
|
<p class="muted">${escapeHtml(item.kind)} • <code>${escapeHtml(item.sourceLocation.filePath)}:${item.sourceLocation.line}:${item.sourceLocation.column}</code></p>
|
|
351
372
|
${item.description ? `<p>${escapeHtml(item.description)}</p>` : ''}
|
|
373
|
+
${renderReferenceMetadata(item)}
|
|
352
374
|
<p><strong>Export paths:</strong> ${renderInlineCodeList(item.exportPaths)}</p>
|
|
353
375
|
<div><strong>Related symbols:</strong> ${item.relatedSymbols.length > 0 ? renderChipList(item.relatedSymbols) : '<span class="empty">None</span>'}</div>
|
|
354
376
|
${item.signatures.length > 0 ? renderSignatureBlock(item) : ''}
|
|
@@ -409,11 +431,14 @@ function renderComponentCard(component) {
|
|
|
409
431
|
component.name,
|
|
410
432
|
component.modulePath,
|
|
411
433
|
component.description ?? '',
|
|
434
|
+
...component.see,
|
|
435
|
+
...component.security,
|
|
412
436
|
...component.props.map((prop) => `${prop.name} ${prop.type}`),
|
|
413
437
|
].join(' '))}">
|
|
414
438
|
<h3>${escapeHtml(component.name)}</h3>
|
|
415
439
|
<p class="muted"><code>${escapeHtml(component.sourceLocation.filePath)}:${component.sourceLocation.line}:${component.sourceLocation.column}</code></p>
|
|
416
440
|
${component.description ? `<p>${escapeHtml(component.description)}</p>` : ''}
|
|
441
|
+
${renderReferenceMetadata(component)}
|
|
417
442
|
<p><strong>Export paths:</strong> ${renderInlineCodeList(component.exportPaths)}</p>
|
|
418
443
|
<table>
|
|
419
444
|
<thead><tr><th>Prop</th><th>Type</th><th>Required</th><th>Description</th></tr></thead>
|
|
@@ -444,6 +469,22 @@ function renderDiagramCard(diagram) {
|
|
|
444
469
|
</details>
|
|
445
470
|
</article>`;
|
|
446
471
|
}
|
|
472
|
+
/***
|
|
473
|
+
* Renders validated external references and security-test evidence.
|
|
474
|
+
*/
|
|
475
|
+
function renderReferenceMetadata(metadata) {
|
|
476
|
+
const see = metadata.see.length === 0
|
|
477
|
+
? ''
|
|
478
|
+
: `<p><strong>See also:</strong> ${metadata.see
|
|
479
|
+
.map((url) => `<a href="${escapeAttribute(url)}" rel="noreferrer">${escapeHtml(url)}</a>`)
|
|
480
|
+
.join(', ')}</p>`;
|
|
481
|
+
const security = metadata.security.length === 0
|
|
482
|
+
? ''
|
|
483
|
+
: `<p><strong>Security tests:</strong> ${metadata.security
|
|
484
|
+
.map((reference) => `<code>${escapeHtml(reference)}</code>`)
|
|
485
|
+
.join(', ')}</p>`;
|
|
486
|
+
return `${see}${security}`;
|
|
487
|
+
}
|
|
447
488
|
/***
|
|
448
489
|
* Renders an inline comma-separated list of code values.
|
|
449
490
|
*/
|
|
@@ -478,16 +519,6 @@ function groupBy(items, key) {
|
|
|
478
519
|
}
|
|
479
520
|
return new Map([...groups.entries()].sort(([left], [right]) => left.localeCompare(right)));
|
|
480
521
|
}
|
|
481
|
-
/***
|
|
482
|
-
* Converts a scenario name to the generated Mermaid file stem.
|
|
483
|
-
*/
|
|
484
|
-
function toFileStem(value) {
|
|
485
|
-
return value
|
|
486
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
487
|
-
.replace(/[^A-Za-z0-9]+/g, '-')
|
|
488
|
-
.replace(/^-+|-+$/g, '')
|
|
489
|
-
.toLowerCase();
|
|
490
|
-
}
|
|
491
522
|
/***
|
|
492
523
|
* Escapes user-controlled text for safe HTML rendering.
|
|
493
524
|
*/
|
|
@@ -8,6 +8,9 @@ export function renderMarkdown({ badges, diagrams, model, outputDir, }) {
|
|
|
8
8
|
components: renderComponents(model),
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
|
+
/***
|
|
12
|
+
* Renders the generated package README.
|
|
13
|
+
*/
|
|
11
14
|
function renderReadme(model, outputDir, badges, diagrams) {
|
|
12
15
|
const lines = [
|
|
13
16
|
'<!-- markdownlint-disable MD013 MD033 -->',
|
|
@@ -23,77 +26,67 @@ function renderReadme(model, outputDir, badges, diagrams) {
|
|
|
23
26
|
}
|
|
24
27
|
if (model.description)
|
|
25
28
|
lines.push(model.description, '');
|
|
26
|
-
|
|
27
|
-
renderReadmeCli(lines, model, outputDir, diagrams);
|
|
29
|
+
renderUsage(lines, model);
|
|
28
30
|
renderConfiguration(lines, model);
|
|
29
31
|
renderGeneratedDocumentation(lines, outputDir, diagrams);
|
|
30
32
|
renderReadmeApi(lines, model);
|
|
31
33
|
return `${lines.join('\n').trimEnd()}\n`;
|
|
32
34
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
/***
|
|
36
|
+
* Renders the canonical CLI-first Usage chapter.
|
|
37
|
+
*/
|
|
38
|
+
function renderUsage(lines, model) {
|
|
39
|
+
const readmeExample = model.usageEntries.find((entry) => entry.area === 'examples' && entry.isReadme);
|
|
36
40
|
lines.push('## Usage', '');
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
lines.push(entryDescription, '');
|
|
47
|
-
}
|
|
48
|
-
lines.push(`Source: \`${entry.sourcePath}\``, '');
|
|
49
|
-
lines.push(`\`\`\`${entry.language}`);
|
|
50
|
-
lines.push(entry.code);
|
|
51
|
-
lines.push('```', '');
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
function renderReadmeCli(lines, model, outputDir, diagrams) {
|
|
55
|
-
if (model.readmeCli === null)
|
|
41
|
+
lines.push('### CLI', '');
|
|
42
|
+
lines.push('Ankhorage packages expose their command-line interface through `ankh`. Use `ankh --help` to discover available package commands, or run a package command with `--help` for package-specific usage.', '');
|
|
43
|
+
lines.push('```zsh');
|
|
44
|
+
lines.push('# Install the Ankhorage CLI');
|
|
45
|
+
lines.push('bun add --global @ankhorage/ankh', '');
|
|
46
|
+
lines.push(`# Show usage information for ${getPackageDisplayName(model.packageId)}`);
|
|
47
|
+
lines.push(model.usage.command);
|
|
48
|
+
lines.push('```', '');
|
|
49
|
+
if (readmeExample === undefined)
|
|
56
50
|
return;
|
|
57
|
-
lines.push('
|
|
58
|
-
if (
|
|
59
|
-
lines.push(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const diagram = findScenarioDiagram(diagrams, scenario);
|
|
69
|
-
if (scenario.description === null && diagram === undefined)
|
|
70
|
-
continue;
|
|
71
|
-
lines.push('<details>');
|
|
72
|
-
lines.push(`<summary>${scenario.name}</summary>`, '');
|
|
73
|
-
if (scenario.description !== null)
|
|
74
|
-
lines.push(scenario.description, '');
|
|
75
|
-
if (diagram !== undefined) {
|
|
76
|
-
lines.push(`Diagram: [${diagram.title}](./${outputDir}/${diagram.path})`, '');
|
|
77
|
-
lines.push('```mermaid');
|
|
78
|
-
lines.push(diagram.content.trimEnd());
|
|
79
|
-
lines.push('```', '');
|
|
80
|
-
}
|
|
81
|
-
lines.push('</details>', '');
|
|
51
|
+
lines.push(`### ${readmeExample.title ?? 'Programmatic Usage'}`, '');
|
|
52
|
+
if (readmeExample.description !== null)
|
|
53
|
+
lines.push(readmeExample.description, '');
|
|
54
|
+
renderReferences(lines, readmeExample);
|
|
55
|
+
lines.push('```' + readmeExample.language);
|
|
56
|
+
lines.push(readmeExample.code);
|
|
57
|
+
lines.push('```', '');
|
|
58
|
+
if (model.exampleCount > 1) {
|
|
59
|
+
const additionalExamples = model.exampleCount - 1;
|
|
60
|
+
const label = additionalExamples === 1 ? 'example' : 'examples';
|
|
61
|
+
lines.push(`This package contains ${additionalExamples} additional ${label}. See the generated documentation for the complete set.`, '');
|
|
82
62
|
}
|
|
83
63
|
}
|
|
84
|
-
|
|
85
|
-
|
|
64
|
+
/***
|
|
65
|
+
* Returns a human-readable package command name.
|
|
66
|
+
*/
|
|
67
|
+
function getPackageDisplayName(packageId) {
|
|
68
|
+
return packageId.split('/').pop() ?? packageId;
|
|
86
69
|
}
|
|
70
|
+
/***
|
|
71
|
+
* Renders the canonical Configuration chapter from the tagged schema plus concrete config instance.
|
|
72
|
+
*/
|
|
87
73
|
function renderConfiguration(lines, model) {
|
|
88
74
|
const config = model.config?.isReadme ? model.config : null;
|
|
89
75
|
const example = model.readmeConfig;
|
|
90
76
|
if (config === null && example === null)
|
|
91
77
|
return;
|
|
92
78
|
lines.push('## Configuration', '');
|
|
79
|
+
if (config !== null) {
|
|
80
|
+
if (config.title !== null && config.title !== 'Configuration') {
|
|
81
|
+
lines.push(`### ${config.title}`, '');
|
|
82
|
+
}
|
|
83
|
+
if (config.description !== null)
|
|
84
|
+
lines.push(config.description, '');
|
|
85
|
+
renderReferences(lines, config);
|
|
86
|
+
}
|
|
93
87
|
if (example !== null) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
lines.push(`\`\`\`${example.language}`);
|
|
88
|
+
lines.push('### Example', '');
|
|
89
|
+
lines.push('```' + example.language);
|
|
97
90
|
lines.push(example.code);
|
|
98
91
|
lines.push('```', '');
|
|
99
92
|
}
|
|
@@ -108,15 +101,33 @@ function renderConfiguration(lines, model) {
|
|
|
108
101
|
}
|
|
109
102
|
lines.push('', '</details>', '');
|
|
110
103
|
}
|
|
104
|
+
/***
|
|
105
|
+
* Renders links and security evidence owned by one documented item.
|
|
106
|
+
*/
|
|
107
|
+
function renderReferences(lines, metadata) {
|
|
108
|
+
if (metadata.see.length > 0) {
|
|
109
|
+
lines.push(`See also: ${metadata.see.map((url) => `[${url}](${url})`).join(', ')}`, '');
|
|
110
|
+
}
|
|
111
|
+
if (metadata.security.length > 0) {
|
|
112
|
+
lines.push(`Security tests: ${metadata.security.map((reference) => `\`${reference}\``).join(', ')}`, '');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/***
|
|
116
|
+
* Renders links to generated documentation artifacts.
|
|
117
|
+
*/
|
|
111
118
|
function renderGeneratedDocumentation(lines, outputDir, diagrams) {
|
|
112
119
|
lines.push('## Generated documentation', '');
|
|
113
120
|
lines.push(`- [Interactive documentation app](./${outputDir}/index.html)`);
|
|
114
121
|
lines.push(`- [Public API reference](./${outputDir}/exports.md)`);
|
|
115
122
|
lines.push(`- [Component registry](./${outputDir}/components.md)`);
|
|
116
|
-
for (const diagram of diagrams)
|
|
123
|
+
for (const diagram of diagrams) {
|
|
117
124
|
lines.push(`- [${diagram.title}](./${outputDir}/${diagram.path})`);
|
|
125
|
+
}
|
|
118
126
|
lines.push('');
|
|
119
127
|
}
|
|
128
|
+
/***
|
|
129
|
+
* Renders README-promoted public API entries.
|
|
130
|
+
*/
|
|
120
131
|
function renderReadmeApi(lines, model) {
|
|
121
132
|
const groups = getReadmeGroups(model);
|
|
122
133
|
if (groups.length === 0)
|
|
@@ -125,20 +136,25 @@ function renderReadmeApi(lines, model) {
|
|
|
125
136
|
for (const group of groups) {
|
|
126
137
|
lines.push(`### ${group.title}`, '');
|
|
127
138
|
for (const item of group.items) {
|
|
128
|
-
if (item.kind === 'component')
|
|
139
|
+
if (item.kind === 'component') {
|
|
129
140
|
renderComponentAccordion(lines, item.component, item.exportEntry);
|
|
130
|
-
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
131
143
|
renderExportAccordion(lines, item.exportEntry);
|
|
144
|
+
}
|
|
132
145
|
}
|
|
133
146
|
}
|
|
134
147
|
}
|
|
148
|
+
/***
|
|
149
|
+
* Renders one README-promoted component.
|
|
150
|
+
*/
|
|
135
151
|
function renderComponentAccordion(lines, component, exportEntry) {
|
|
136
152
|
lines.push('<details>');
|
|
137
|
-
lines.push(`<summary>${component.name}</summary>`, '');
|
|
153
|
+
lines.push(`<summary>${exportEntry?.title ?? component.name}</summary>`, '');
|
|
138
154
|
renderSignature(lines, exportEntry);
|
|
139
155
|
if (component.description)
|
|
140
156
|
lines.push(component.description, '');
|
|
141
|
-
|
|
157
|
+
renderReferences(lines, component);
|
|
142
158
|
if (exportEntry && exportEntry.relatedSymbols.length > 0) {
|
|
143
159
|
lines.push(`Related types: ${exportEntry.relatedSymbols.map((symbol) => `\`${symbol}\``).join(', ')}`, '');
|
|
144
160
|
}
|
|
@@ -154,13 +170,16 @@ function renderComponentAccordion(lines, component, exportEntry) {
|
|
|
154
170
|
}
|
|
155
171
|
lines.push('</details>', '');
|
|
156
172
|
}
|
|
173
|
+
/***
|
|
174
|
+
* Renders one README-promoted non-component export.
|
|
175
|
+
*/
|
|
157
176
|
function renderExportAccordion(lines, item) {
|
|
158
177
|
lines.push('<details>');
|
|
159
|
-
lines.push(`<summary>${item.name}</summary>`, '');
|
|
178
|
+
lines.push(`<summary>${item.title ?? item.name}</summary>`, '');
|
|
160
179
|
renderSignature(lines, item);
|
|
161
180
|
lines.push(item.description ?? `\`${item.kind}\` export.`, '');
|
|
181
|
+
renderReferences(lines, item);
|
|
162
182
|
renderStructuredRows(lines, item);
|
|
163
|
-
renderExamples(lines, item.examples);
|
|
164
183
|
lines.push(`Module: \`${item.modulePath}\``);
|
|
165
184
|
lines.push(`Source: \`${item.sourceLocation.filePath}:${item.sourceLocation.line}:${item.sourceLocation.column}\``);
|
|
166
185
|
if (item.relatedSymbols.length > 0) {
|
|
@@ -168,6 +187,9 @@ function renderExportAccordion(lines, item) {
|
|
|
168
187
|
}
|
|
169
188
|
lines.push('', '</details>', '');
|
|
170
189
|
}
|
|
190
|
+
/***
|
|
191
|
+
* Renders the primary signature for a README public API entry.
|
|
192
|
+
*/
|
|
171
193
|
function renderSignature(lines, item) {
|
|
172
194
|
const signature = item?.signatures[0]?.label;
|
|
173
195
|
if (!signature)
|
|
@@ -176,15 +198,9 @@ function renderSignature(lines, item) {
|
|
|
176
198
|
lines.push(`${item.name}${signature}`);
|
|
177
199
|
lines.push('```', '');
|
|
178
200
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
lines.push(`#### ${example.title}`, '');
|
|
183
|
-
lines.push(`\`\`\`${example.language ?? ''}`);
|
|
184
|
-
lines.push(example.code);
|
|
185
|
-
lines.push('```', '');
|
|
186
|
-
}
|
|
187
|
-
}
|
|
201
|
+
/***
|
|
202
|
+
* Renders structured const-array rows as a Markdown table.
|
|
203
|
+
*/
|
|
188
204
|
function renderStructuredRows(lines, item) {
|
|
189
205
|
if (item.structuredRows.length === 0)
|
|
190
206
|
return;
|
|
@@ -202,6 +218,9 @@ function renderStructuredRows(lines, item) {
|
|
|
202
218
|
}
|
|
203
219
|
lines.push('');
|
|
204
220
|
}
|
|
221
|
+
/***
|
|
222
|
+
* Returns stable structured-row columns.
|
|
223
|
+
*/
|
|
205
224
|
function getStructuredColumns(item) {
|
|
206
225
|
const columns = new Set();
|
|
207
226
|
for (const row of item.structuredRows) {
|
|
@@ -210,9 +229,15 @@ function getStructuredColumns(item) {
|
|
|
210
229
|
}
|
|
211
230
|
return [...columns];
|
|
212
231
|
}
|
|
232
|
+
/***
|
|
233
|
+
* Formats one structured-row column header.
|
|
234
|
+
*/
|
|
213
235
|
function formatStructuredColumnHeader(column) {
|
|
214
236
|
return escapeTableCell(column.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase());
|
|
215
237
|
}
|
|
238
|
+
/***
|
|
239
|
+
* Formats one structured-row table cell.
|
|
240
|
+
*/
|
|
216
241
|
function formatStructuredCell(column, value) {
|
|
217
242
|
const escaped = escapeTableCell(value);
|
|
218
243
|
if (column === 'syntax' || column === 'name' || column === 'handler')
|
|
@@ -223,6 +248,9 @@ function formatStructuredCell(column, value) {
|
|
|
223
248
|
return 'no';
|
|
224
249
|
return escaped;
|
|
225
250
|
}
|
|
251
|
+
/***
|
|
252
|
+
* Groups README-promoted API entries by presentation category.
|
|
253
|
+
*/
|
|
226
254
|
function getReadmeGroups(model) {
|
|
227
255
|
const exportsByName = new Map(model.exports.map((entry) => [entry.name, entry]));
|
|
228
256
|
const componentNames = new Set(model.components.map((component) => component.name));
|
|
@@ -244,22 +272,32 @@ function getReadmeGroups(model) {
|
|
|
244
272
|
}
|
|
245
273
|
return CATEGORY_ORDER.flatMap((title) => {
|
|
246
274
|
const items = groups.get(title);
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
275
|
+
return items === undefined || items.length === 0
|
|
276
|
+
? []
|
|
277
|
+
: [{ title, items: sortReadmeItems(items) }];
|
|
250
278
|
});
|
|
251
279
|
}
|
|
280
|
+
/***
|
|
281
|
+
* Adds one public API item to a README category.
|
|
282
|
+
*/
|
|
252
283
|
function addReadmeItem(groups, title, item) {
|
|
253
|
-
|
|
254
|
-
existing.push(item);
|
|
255
|
-
groups.set(title, existing);
|
|
284
|
+
groups.set(title, [...(groups.get(title) ?? []), item]);
|
|
256
285
|
}
|
|
286
|
+
/***
|
|
287
|
+
* Sorts README category items by symbol name.
|
|
288
|
+
*/
|
|
257
289
|
function sortReadmeItems(items) {
|
|
258
290
|
return [...items].sort((left, right) => getReadmeItemName(left).localeCompare(getReadmeItemName(right)));
|
|
259
291
|
}
|
|
292
|
+
/***
|
|
293
|
+
* Returns one README item's source symbol name.
|
|
294
|
+
*/
|
|
260
295
|
function getReadmeItemName(item) {
|
|
261
296
|
return item.kind === 'component' ? item.component.name : item.exportEntry.name;
|
|
262
297
|
}
|
|
298
|
+
/***
|
|
299
|
+
* Derives a stable README public API category from a module path.
|
|
300
|
+
*/
|
|
263
301
|
function getReadmeCategory(modulePath, name) {
|
|
264
302
|
if (modulePath.includes('/config/'))
|
|
265
303
|
return 'Config';
|
|
@@ -281,24 +319,30 @@ function getReadmeCategory(modulePath, name) {
|
|
|
281
319
|
return 'Types';
|
|
282
320
|
return 'Utilities';
|
|
283
321
|
}
|
|
322
|
+
/***
|
|
323
|
+
* Renders the complete public API reference.
|
|
324
|
+
*/
|
|
284
325
|
function renderExports(model) {
|
|
285
326
|
const lines = ['# Public API', ''];
|
|
286
327
|
for (const item of model.exports) {
|
|
287
|
-
lines.push(`## ${item.name}`, '');
|
|
328
|
+
lines.push(`## ${item.title ?? item.name}`, '');
|
|
329
|
+
if (item.title !== null && item.title !== item.name)
|
|
330
|
+
lines.push(`Symbol: \`${item.name}\``, '');
|
|
288
331
|
lines.push(`Kind: \`${item.kind}\``);
|
|
289
332
|
lines.push(`Module: \`${item.modulePath}\``);
|
|
290
333
|
lines.push(`Source: \`${item.sourceLocation.filePath}:${item.sourceLocation.line}:${item.sourceLocation.column}\``, '');
|
|
291
334
|
if (item.description)
|
|
292
335
|
lines.push(item.description, '');
|
|
336
|
+
renderReferences(lines, item);
|
|
293
337
|
renderStructuredRows(lines, item);
|
|
294
338
|
if (item.signatures.length > 0) {
|
|
295
339
|
lines.push('### Signatures', '');
|
|
296
340
|
for (const signature of item.signatures) {
|
|
297
341
|
lines.push(`- \`${signature.label}\``);
|
|
298
342
|
for (const parameter of signature.parameters) {
|
|
299
|
-
lines.push(` - ${parameter.name}: \`${parameter.type}\`${parameter.required ? '' : ' (optional)'}
|
|
343
|
+
lines.push(` - ${parameter.name}: \`${parameter.type}\`${parameter.required ? '' : ' (optional)'}`);
|
|
300
344
|
}
|
|
301
|
-
lines.push(` - returns: \`${signature.returnType ?? 'void'}
|
|
345
|
+
lines.push(` - returns: \`${signature.returnType ?? 'void'}\``);
|
|
302
346
|
}
|
|
303
347
|
lines.push('');
|
|
304
348
|
}
|
|
@@ -314,6 +358,9 @@ function renderExports(model) {
|
|
|
314
358
|
}
|
|
315
359
|
return `${lines.join('\n').trimEnd()}\n`;
|
|
316
360
|
}
|
|
361
|
+
/***
|
|
362
|
+
* Renders the complete component registry.
|
|
363
|
+
*/
|
|
317
364
|
function renderComponents(model) {
|
|
318
365
|
const lines = ['# Components', ''];
|
|
319
366
|
for (const component of model.components) {
|
|
@@ -321,6 +368,7 @@ function renderComponents(model) {
|
|
|
321
368
|
lines.push(`Source: \`${component.sourceLocation.filePath}:${component.sourceLocation.line}:${component.sourceLocation.column}\``, '');
|
|
322
369
|
if (component.description)
|
|
323
370
|
lines.push(component.description, '');
|
|
371
|
+
renderReferences(lines, component);
|
|
324
372
|
if (component.exportPaths.length > 0) {
|
|
325
373
|
lines.push(`Export paths: ${component.exportPaths.map((path) => `\`${path}\``).join(', ')}`, '');
|
|
326
374
|
}
|
|
@@ -335,12 +383,21 @@ function renderComponents(model) {
|
|
|
335
383
|
}
|
|
336
384
|
return `${lines.join('\n').trimEnd()}\n`;
|
|
337
385
|
}
|
|
386
|
+
/***
|
|
387
|
+
* Escapes one Markdown table cell.
|
|
388
|
+
*/
|
|
338
389
|
function escapeTableCell(value) {
|
|
339
390
|
return value.replaceAll('|', '\\|');
|
|
340
391
|
}
|
|
392
|
+
/***
|
|
393
|
+
* Renders an optional default value.
|
|
394
|
+
*/
|
|
341
395
|
function renderDefault(value) {
|
|
342
396
|
return value === undefined ? '—' : `\`${escapeTableCell(value)}\``;
|
|
343
397
|
}
|
|
398
|
+
/***
|
|
399
|
+
* Flattens nested configuration members into dot paths.
|
|
400
|
+
*/
|
|
344
401
|
function flattenConfigMembers(members, prefix = '') {
|
|
345
402
|
return members.flatMap((member) => {
|
|
346
403
|
const path = prefix ? `${prefix}.${member.name}` : member.name;
|
|
@@ -355,6 +412,9 @@ function flattenConfigMembers(members, prefix = '') {
|
|
|
355
412
|
return [current, ...children];
|
|
356
413
|
});
|
|
357
414
|
}
|
|
415
|
+
/***
|
|
416
|
+
* Returns the accessible badge label stored in a rendered badge artifact.
|
|
417
|
+
*/
|
|
358
418
|
function badgeLabel(model, badgePath) {
|
|
359
419
|
const fileName = badgePath.split('/').pop();
|
|
360
420
|
if (!fileName)
|
|
@@ -363,13 +423,6 @@ function badgeLabel(model, badgePath) {
|
|
|
363
423
|
const badge = model.badges.find((entry) => entry.id === id);
|
|
364
424
|
return badge ? `${badge.label}: ${badge.value}` : badgePath;
|
|
365
425
|
}
|
|
366
|
-
function toFileStem(value) {
|
|
367
|
-
return value
|
|
368
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
369
|
-
.replace(/[^A-Za-z0-9]+/g, '-')
|
|
370
|
-
.replace(/^-+|-+$/g, '')
|
|
371
|
-
.toLowerCase();
|
|
372
|
-
}
|
|
373
426
|
const CATEGORY_ORDER = [
|
|
374
427
|
'Config',
|
|
375
428
|
'Documentation',
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/*** Convert a documentation label to the stable lowercase kebab-case file stem used by Paradox render artifacts. */
|
|
2
|
+
export function toFileStem(value) {
|
|
3
|
+
return value
|
|
4
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
5
|
+
.replace(/[^A-Za-z0-9]+/g, '-')
|
|
6
|
+
.replace(/^-+|-+$/g, '')
|
|
7
|
+
.toLowerCase();
|
|
8
|
+
}
|