@ankhorage/paradox 0.1.3 → 0.1.5
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 +12 -0
- package/README.md +31 -25
- package/dist/analyze/analyze.d.ts +3 -0
- package/dist/analyze/analyze.js +12 -0
- package/dist/analyze/badges.js +51 -0
- package/dist/analyze/exports.d.ts +1 -1
- package/dist/analyze/exports.js +24 -0
- package/dist/analyze/modules.js +6 -0
- package/dist/analyze/sourceFunctions.d.ts +6 -0
- package/dist/analyze/sourceFunctions.js +64 -0
- package/dist/analyze/types.d.ts +11 -1
- package/dist/analyze/usage.d.ts +3 -0
- package/dist/analyze/usage.js +6 -0
- package/dist/analyze/utils/getExportMetadata.d.ts +1 -1
- package/dist/analyze/utils/getExportMetadata.js +143 -2
- package/dist/analyze/utils/getParadoxComment.d.ts +2 -2
- package/dist/analyze/utils/getParadoxComment.js +26 -1
- package/dist/doc-tags/registry.d.ts +39 -0
- package/dist/doc-tags/registry.js +45 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/model/buildModel.d.ts +12 -0
- package/dist/model/buildModel.js +25 -0
- package/dist/model/types.d.ts +11 -1
- package/dist/paths/policy.d.ts +12 -0
- package/dist/paths/policy.js +27 -2
- package/dist/render/renderers/html.js +233 -60
- package/dist/render/renderers/markdown.js +47 -26
- package/package.json +1 -1
|
@@ -2,18 +2,8 @@
|
|
|
2
2
|
* Renders a deterministic static HTML documentation app.
|
|
3
3
|
*/
|
|
4
4
|
export function renderHtml({ diagrams, model, }) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
href: `#symbol-${toAnchorId(item.name)}`,
|
|
8
|
-
label: item.name,
|
|
9
|
-
meta: `${item.kind} • ${item.modulePath}`,
|
|
10
|
-
})),
|
|
11
|
-
...model.components.map((component) => ({
|
|
12
|
-
href: `#component-${toAnchorId(component.name)}`,
|
|
13
|
-
label: component.name,
|
|
14
|
-
meta: `component • ${component.modulePath}`,
|
|
15
|
-
})),
|
|
16
|
-
].sort((left, right) => left.label.localeCompare(right.label));
|
|
5
|
+
const sourceAreas = getSourceAreas(model);
|
|
6
|
+
const cliScenarios = getReadmeCliScenarios(model);
|
|
17
7
|
const exportsByModule = groupBy(model.exports, (item) => item.modulePath);
|
|
18
8
|
return {
|
|
19
9
|
indexHtml: `<!doctype html>
|
|
@@ -33,6 +23,7 @@ export function renderHtml({ diagrams, model, }) {
|
|
|
33
23
|
body { margin: 0; }
|
|
34
24
|
a { color: #2459d3; text-decoration: none; }
|
|
35
25
|
a:hover { text-decoration: underline; }
|
|
26
|
+
button { font: inherit; }
|
|
36
27
|
code, pre { font-family: "SFMono-Regular", ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
37
28
|
.layout {
|
|
38
29
|
display: grid;
|
|
@@ -48,9 +39,7 @@ export function renderHtml({ diagrams, model, }) {
|
|
|
48
39
|
max-height: 100vh;
|
|
49
40
|
overflow: auto;
|
|
50
41
|
}
|
|
51
|
-
.content {
|
|
52
|
-
padding: 2rem;
|
|
53
|
-
}
|
|
42
|
+
.content { padding: 2rem; }
|
|
54
43
|
.panel, .item {
|
|
55
44
|
background: #ffffff;
|
|
56
45
|
border: 1px solid #d8dfec;
|
|
@@ -80,7 +69,28 @@ export function renderHtml({ diagrams, model, }) {
|
|
|
80
69
|
padding: 0;
|
|
81
70
|
margin: 0;
|
|
82
71
|
}
|
|
83
|
-
.nav-list li + li { margin-top: 0.
|
|
72
|
+
.nav-list li + li { margin-top: 0.5rem; }
|
|
73
|
+
.nav-button {
|
|
74
|
+
width: 100%;
|
|
75
|
+
display: block;
|
|
76
|
+
border: 0;
|
|
77
|
+
border-radius: 0.7rem;
|
|
78
|
+
background: transparent;
|
|
79
|
+
color: #24427a;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
padding: 0.55rem 0.7rem;
|
|
82
|
+
text-align: left;
|
|
83
|
+
}
|
|
84
|
+
.nav-button:hover, .nav-button[aria-current="page"] {
|
|
85
|
+
background: #e8eefb;
|
|
86
|
+
text-decoration: none;
|
|
87
|
+
}
|
|
88
|
+
.nav-button small {
|
|
89
|
+
display: block;
|
|
90
|
+
color: #5e6d8c;
|
|
91
|
+
margin-top: 0.15rem;
|
|
92
|
+
}
|
|
93
|
+
.view[hidden] { display: none; }
|
|
84
94
|
.muted { color: #5e6d8c; }
|
|
85
95
|
.chips { display: flex; flex-wrap: wrap; gap: 0.5rem; padding: 0; list-style: none; }
|
|
86
96
|
.chip {
|
|
@@ -115,61 +125,64 @@ export function renderHtml({ diagrams, model, }) {
|
|
|
115
125
|
<p class="muted">Generated with Paradox</p>
|
|
116
126
|
<h1>${escapeHtml(model.packageName)}</h1>
|
|
117
127
|
<p>${escapeHtml(model.description ?? 'Deterministic package documentation.')}</p>
|
|
118
|
-
<input id="search" class="search" type="search" placeholder="Search
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
128
|
+
<input id="search" class="search" type="search" placeholder="Search current view" />
|
|
129
|
+
<nav aria-label="Documentation sections">
|
|
130
|
+
<ul class="nav-list">
|
|
131
|
+
<li>
|
|
132
|
+
<button class="nav-button" type="button" data-target="home" aria-current="page">
|
|
133
|
+
Home
|
|
134
|
+
<small>Overview, CLI, public API, diagrams</small>
|
|
135
|
+
</button>
|
|
136
|
+
</li>
|
|
137
|
+
${sourceAreas.map(renderSourceNavItem).join('')}
|
|
138
|
+
</ul>
|
|
139
|
+
</nav>
|
|
124
140
|
</aside>
|
|
125
141
|
<main class="content">
|
|
126
|
-
<section class="
|
|
127
|
-
|
|
128
|
-
<div class="summary">
|
|
129
|
-
<div><strong>${model.exports.length}</strong><span class="muted">public exports</span></div>
|
|
130
|
-
<div><strong>${model.components.length}</strong><span class="muted">components</span></div>
|
|
131
|
-
<div><strong>${model.modules.length}</strong><span class="muted">modules</span></div>
|
|
132
|
-
<div><strong>${model.entrypoints.length}</strong><span class="muted">entrypoints</span></div>
|
|
133
|
-
</div>
|
|
134
|
-
<h3>Entrypoints</h3>
|
|
135
|
-
<ul class="meta-list">
|
|
136
|
-
${model.entrypoints.map((entrypoint) => `<li><code>${escapeHtml(entrypoint)}</code></li>`).join('')}
|
|
137
|
-
</ul>
|
|
138
|
-
</section>
|
|
139
|
-
<section class="panel">
|
|
140
|
-
<h2>Modules</h2>
|
|
141
|
-
${model.modules.map(renderModuleCard).join('')}
|
|
142
|
-
</section>
|
|
143
|
-
<section class="panel">
|
|
144
|
-
<h2>Exports by module</h2>
|
|
145
|
-
${[...exportsByModule.entries()]
|
|
146
|
-
.map(([modulePath, exports]) => `
|
|
147
|
-
<section>
|
|
148
|
-
<h3>${escapeHtml(modulePath)}</h3>
|
|
149
|
-
${exports.map((item) => renderExportCard(item)).join('')}
|
|
150
|
-
</section>`)
|
|
151
|
-
.join('')}
|
|
152
|
-
</section>
|
|
153
|
-
<section class="panel">
|
|
154
|
-
<h2>Component registry</h2>
|
|
155
|
-
${model.components.length === 0 ? '<p class="empty">No components were detected.</p>' : model.components.map(renderComponentCard).join('')}
|
|
156
|
-
</section>
|
|
157
|
-
<section class="panel">
|
|
158
|
-
<h2>Diagrams</h2>
|
|
159
|
-
${diagrams.map(renderDiagramCard).join('')}
|
|
142
|
+
<section id="view-home" class="view" data-view="home">
|
|
143
|
+
${renderHomeView(model, diagrams, cliScenarios, exportsByModule)}
|
|
160
144
|
</section>
|
|
145
|
+
${sourceAreas.map(renderSourceAreaView).join('')}
|
|
161
146
|
</main>
|
|
162
147
|
</div>
|
|
163
148
|
<script>
|
|
164
149
|
const search = document.getElementById('search');
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
150
|
+
const navButtons = Array.from(document.querySelectorAll('[data-target]'));
|
|
151
|
+
const views = Array.from(document.querySelectorAll('[data-view]'));
|
|
152
|
+
|
|
153
|
+
function getActiveView() {
|
|
154
|
+
return document.querySelector('[data-view]:not([hidden])');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function applySearch() {
|
|
158
|
+
const value = search?.value.trim().toLowerCase() || '';
|
|
159
|
+
const activeView = getActiveView();
|
|
160
|
+
const items = Array.from(activeView?.querySelectorAll('[data-search]') || []);
|
|
161
|
+
|
|
168
162
|
for (const item of items) {
|
|
169
163
|
const haystack = (item.getAttribute('data-search') || '').toLowerCase();
|
|
170
164
|
item.style.display = value === '' || haystack.includes(value) ? '' : 'none';
|
|
171
165
|
}
|
|
172
|
-
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const button of navButtons) {
|
|
169
|
+
button.addEventListener('click', () => {
|
|
170
|
+
const target = button.getAttribute('data-target');
|
|
171
|
+
|
|
172
|
+
for (const view of views) {
|
|
173
|
+
view.hidden = view.getAttribute('data-view') !== target;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const item of navButtons) {
|
|
177
|
+
item.setAttribute('aria-current', item === button ? 'page' : 'false');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (search) search.value = '';
|
|
181
|
+
applySearch();
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
search?.addEventListener('input', applySearch);
|
|
173
186
|
</script>
|
|
174
187
|
<script type="module">
|
|
175
188
|
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
|
@@ -180,6 +193,123 @@ export function renderHtml({ diagrams, model, }) {
|
|
|
180
193
|
`,
|
|
181
194
|
};
|
|
182
195
|
}
|
|
196
|
+
/***
|
|
197
|
+
* Renders the Home view that keeps public API and package-level information together.
|
|
198
|
+
*/
|
|
199
|
+
function renderHomeView(model, diagrams, cliScenarios, exportsByModule) {
|
|
200
|
+
return `
|
|
201
|
+
<section class="panel">
|
|
202
|
+
<h2>Package overview</h2>
|
|
203
|
+
<div class="summary">
|
|
204
|
+
<div><strong>${model.exports.length}</strong><span class="muted">public exports</span></div>
|
|
205
|
+
<div><strong>${model.components.length}</strong><span class="muted">components</span></div>
|
|
206
|
+
<div><strong>${model.modules.length}</strong><span class="muted">modules</span></div>
|
|
207
|
+
<div><strong>${model.entrypoints.length}</strong><span class="muted">entrypoints</span></div>
|
|
208
|
+
</div>
|
|
209
|
+
<h3>Entrypoints</h3>
|
|
210
|
+
<ul class="meta-list">
|
|
211
|
+
${model.entrypoints.map((entrypoint) => `<li><code>${escapeHtml(entrypoint)}</code></li>`).join('')}
|
|
212
|
+
</ul>
|
|
213
|
+
</section>
|
|
214
|
+
${cliScenarios.length > 0 ? renderCliPanel(model, diagrams, cliScenarios) : ''}
|
|
215
|
+
<section class="panel">
|
|
216
|
+
<h2>Modules</h2>
|
|
217
|
+
${model.modules.map(renderModuleCard).join('')}
|
|
218
|
+
</section>
|
|
219
|
+
<section class="panel">
|
|
220
|
+
<h2>Public API</h2>
|
|
221
|
+
${[...exportsByModule.entries()]
|
|
222
|
+
.map(([modulePath, exports]) => `
|
|
223
|
+
<section>
|
|
224
|
+
<h3>${escapeHtml(modulePath)}</h3>
|
|
225
|
+
${exports.map((item) => renderExportCard(item)).join('')}
|
|
226
|
+
</section>`)
|
|
227
|
+
.join('')}
|
|
228
|
+
</section>
|
|
229
|
+
<section class="panel">
|
|
230
|
+
<h2>Component registry</h2>
|
|
231
|
+
${model.components.length === 0 ? '<p class="empty">No components were detected.</p>' : model.components.map(renderComponentCard).join('')}
|
|
232
|
+
</section>
|
|
233
|
+
<section class="panel">
|
|
234
|
+
<h2>Diagrams</h2>
|
|
235
|
+
${diagrams.map(renderDiagramCard).join('')}
|
|
236
|
+
</section>`;
|
|
237
|
+
}
|
|
238
|
+
/***
|
|
239
|
+
* Renders the Home CLI chapter for detected bin scenarios.
|
|
240
|
+
*/
|
|
241
|
+
function renderCliPanel(model, diagrams, scenarios) {
|
|
242
|
+
return `<section class="panel" data-search="cli ${scenarios.map((scenario) => scenario.name).join(' ')}">
|
|
243
|
+
<h2>CLI</h2>
|
|
244
|
+
${scenarios
|
|
245
|
+
.map((scenario) => {
|
|
246
|
+
const command = model.usage?.commands.find((item) => item.name === scenario.name);
|
|
247
|
+
const diagram = findScenarioDiagram(diagrams, scenario);
|
|
248
|
+
return `<article class="item" data-search="${escapeAttribute([scenario.name, scenario.description ?? '', command?.command ?? ''].join(' '))}">
|
|
249
|
+
<h3>${escapeHtml(scenario.name)}</h3>
|
|
250
|
+
${scenario.description === null ? '' : `<p>${escapeHtml(scenario.description)}</p>`}
|
|
251
|
+
${command === undefined ? '' : `<pre>${escapeHtml(command.command)}</pre>`}
|
|
252
|
+
${diagram === undefined ? '' : renderDiagramCard(diagram)}
|
|
253
|
+
</article>`;
|
|
254
|
+
})
|
|
255
|
+
.join('')}
|
|
256
|
+
</section>`;
|
|
257
|
+
}
|
|
258
|
+
/***
|
|
259
|
+
* Renders one source file entry in the left navigation.
|
|
260
|
+
*/
|
|
261
|
+
function renderSourceNavItem(area) {
|
|
262
|
+
return `<li>
|
|
263
|
+
<button class="nav-button" type="button" data-target="${escapeAttribute(area.path)}">
|
|
264
|
+
${escapeHtml(area.path)}
|
|
265
|
+
<small>${area.functions.length} function${area.functions.length === 1 ? '' : 's'}</small>
|
|
266
|
+
</button>
|
|
267
|
+
</li>`;
|
|
268
|
+
}
|
|
269
|
+
/***
|
|
270
|
+
* Renders the right-hand source area view for a selected file.
|
|
271
|
+
*/
|
|
272
|
+
function renderSourceAreaView(area) {
|
|
273
|
+
return `<section id="view-${toAnchorId(area.path)}" class="view" data-view="${escapeAttribute(area.path)}" hidden>
|
|
274
|
+
<section class="panel">
|
|
275
|
+
<h2>${escapeHtml(area.path)}</h2>
|
|
276
|
+
${area.functions.map(renderSourceFunctionCard).join('')}
|
|
277
|
+
</section>
|
|
278
|
+
</section>`;
|
|
279
|
+
}
|
|
280
|
+
/***
|
|
281
|
+
* Renders one source function card in a source-area view.
|
|
282
|
+
*/
|
|
283
|
+
function renderSourceFunctionCard(item) {
|
|
284
|
+
return `<article class="item" data-search="${escapeAttribute([item.name, item.sourceLocation.filePath, item.description ?? ''].join(' '))}">
|
|
285
|
+
<h3>${escapeHtml(item.name)}</h3>
|
|
286
|
+
<p class="muted"><code>${escapeHtml(item.sourceLocation.filePath)}:${item.sourceLocation.line}:${item.sourceLocation.column}</code></p>
|
|
287
|
+
${item.description === null ? '<p class="empty">No description available.</p>' : `<p>${escapeHtml(item.description)}</p>`}
|
|
288
|
+
</article>`;
|
|
289
|
+
}
|
|
290
|
+
/***
|
|
291
|
+
* Groups analyzed source functions by their source file path.
|
|
292
|
+
*/
|
|
293
|
+
function getSourceAreas(model) {
|
|
294
|
+
return [...groupBy(model.sourceFunctions, (item) => item.sourceLocation.filePath).entries()]
|
|
295
|
+
.map(([path, functions]) => ({ path, functions }))
|
|
296
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
297
|
+
}
|
|
298
|
+
/***
|
|
299
|
+
* Selects bin scenarios that should be shown on the Home page.
|
|
300
|
+
*/
|
|
301
|
+
function getReadmeCliScenarios(model) {
|
|
302
|
+
return model.sequenceScenarios.filter((scenario) => scenario.kind === 'bin' && scenario.isReadme);
|
|
303
|
+
}
|
|
304
|
+
/***
|
|
305
|
+
* Finds the generated Mermaid artifact for a sequence scenario.
|
|
306
|
+
*/
|
|
307
|
+
function findScenarioDiagram(diagrams, scenario) {
|
|
308
|
+
return diagrams.find((diagram) => diagram.path === `diagrams/sequences/${toFileStem(scenario.name)}.mmd`);
|
|
309
|
+
}
|
|
310
|
+
/***
|
|
311
|
+
* Renders module metadata on the Home page.
|
|
312
|
+
*/
|
|
183
313
|
function renderModuleCard(module) {
|
|
184
314
|
return `<article class="item" data-search="${escapeAttribute([module.path, ...module.dependencies, ...module.exports].join(' '))}">
|
|
185
315
|
<h3>${escapeHtml(module.path)}</h3>
|
|
@@ -188,6 +318,9 @@ function renderModuleCard(module) {
|
|
|
188
318
|
<p><strong>Exports:</strong> ${renderInlineCodeList(module.exports)}</p>
|
|
189
319
|
</article>`;
|
|
190
320
|
}
|
|
321
|
+
/***
|
|
322
|
+
* Renders one public API export card on the Home page.
|
|
323
|
+
*/
|
|
191
324
|
function renderExportCard(item) {
|
|
192
325
|
return `<article class="item" id="symbol-${toAnchorId(item.name)}" data-search="${escapeAttribute([
|
|
193
326
|
item.name,
|
|
@@ -206,6 +339,9 @@ function renderExportCard(item) {
|
|
|
206
339
|
${item.members.length > 0 ? renderMemberTable(item) : ''}
|
|
207
340
|
</article>`;
|
|
208
341
|
}
|
|
342
|
+
/***
|
|
343
|
+
* Renders all call signatures for a public API export.
|
|
344
|
+
*/
|
|
209
345
|
function renderSignatureBlock(item) {
|
|
210
346
|
return item.signatures
|
|
211
347
|
.map((signature) => `<div>
|
|
@@ -230,6 +366,9 @@ function renderSignatureBlock(item) {
|
|
|
230
366
|
</div>`)
|
|
231
367
|
.join('');
|
|
232
368
|
}
|
|
369
|
+
/***
|
|
370
|
+
* Renders the members table for a type-like public API export.
|
|
371
|
+
*/
|
|
233
372
|
function renderMemberTable(item) {
|
|
234
373
|
return `<table>
|
|
235
374
|
<thead><tr><th>Member</th><th>Kind</th><th>Type</th><th>Required</th><th>Description</th></tr></thead>
|
|
@@ -246,6 +385,9 @@ function renderMemberTable(item) {
|
|
|
246
385
|
</tbody>
|
|
247
386
|
</table>`;
|
|
248
387
|
}
|
|
388
|
+
/***
|
|
389
|
+
* Renders one detected component card on the Home page.
|
|
390
|
+
*/
|
|
249
391
|
function renderComponentCard(component) {
|
|
250
392
|
return `<article class="item" id="component-${toAnchorId(component.name)}" data-search="${escapeAttribute([
|
|
251
393
|
component.name,
|
|
@@ -272,6 +414,9 @@ function renderComponentCard(component) {
|
|
|
272
414
|
</table>
|
|
273
415
|
</article>`;
|
|
274
416
|
}
|
|
417
|
+
/***
|
|
418
|
+
* Renders one Mermaid diagram card.
|
|
419
|
+
*/
|
|
275
420
|
function renderDiagramCard(diagram) {
|
|
276
421
|
return `<article class="item" data-search="${escapeAttribute(`${diagram.title} ${diagram.path}`)}">
|
|
277
422
|
<h3>${escapeHtml(diagram.title)}</h3>
|
|
@@ -283,17 +428,26 @@ function renderDiagramCard(diagram) {
|
|
|
283
428
|
</details>
|
|
284
429
|
</article>`;
|
|
285
430
|
}
|
|
431
|
+
/***
|
|
432
|
+
* Renders an inline comma-separated list of code values.
|
|
433
|
+
*/
|
|
286
434
|
function renderInlineCodeList(values) {
|
|
287
435
|
if (values.length === 0) {
|
|
288
436
|
return '<span class="empty">None</span>';
|
|
289
437
|
}
|
|
290
438
|
return values.map((value) => `<code>${escapeHtml(value)}</code>`).join(', ');
|
|
291
439
|
}
|
|
440
|
+
/***
|
|
441
|
+
* Renders a compact chip list for related symbols.
|
|
442
|
+
*/
|
|
292
443
|
function renderChipList(values) {
|
|
293
444
|
return `<ul class="chips">${values
|
|
294
445
|
.map((value) => `<li class="chip">${escapeHtml(value)}</li>`)
|
|
295
446
|
.join('')}</ul>`;
|
|
296
447
|
}
|
|
448
|
+
/***
|
|
449
|
+
* Groups items by a string key and returns deterministic key order.
|
|
450
|
+
*/
|
|
297
451
|
function groupBy(items, key) {
|
|
298
452
|
const groups = new Map();
|
|
299
453
|
for (const item of items) {
|
|
@@ -308,12 +462,28 @@ function groupBy(items, key) {
|
|
|
308
462
|
}
|
|
309
463
|
return new Map([...groups.entries()].sort(([left], [right]) => left.localeCompare(right)));
|
|
310
464
|
}
|
|
465
|
+
/***
|
|
466
|
+
* Converts a label to a stable HTML anchor id fragment.
|
|
467
|
+
*/
|
|
311
468
|
function toAnchorId(value) {
|
|
312
469
|
return value
|
|
313
470
|
.toLowerCase()
|
|
314
471
|
.replace(/[^a-z0-9]+/g, '-')
|
|
315
472
|
.replace(/^-|-$/g, '');
|
|
316
473
|
}
|
|
474
|
+
/***
|
|
475
|
+
* Converts a scenario name to the generated Mermaid file stem.
|
|
476
|
+
*/
|
|
477
|
+
function toFileStem(value) {
|
|
478
|
+
return value
|
|
479
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
480
|
+
.replace(/[^A-Za-z0-9]+/g, '-')
|
|
481
|
+
.replace(/^-+|-+$/g, '')
|
|
482
|
+
.toLowerCase();
|
|
483
|
+
}
|
|
484
|
+
/***
|
|
485
|
+
* Escapes user-controlled text for safe HTML rendering.
|
|
486
|
+
*/
|
|
317
487
|
function escapeHtml(value) {
|
|
318
488
|
return value
|
|
319
489
|
.replaceAll('&', '&')
|
|
@@ -322,6 +492,9 @@ function escapeHtml(value) {
|
|
|
322
492
|
.replaceAll('"', '"')
|
|
323
493
|
.replaceAll("'", ''');
|
|
324
494
|
}
|
|
495
|
+
/***
|
|
496
|
+
* Escapes text for use inside HTML attribute values.
|
|
497
|
+
*/
|
|
325
498
|
function escapeAttribute(value) {
|
|
326
499
|
return escapeHtml(value).replaceAll('\n', ' ');
|
|
327
500
|
}
|
|
@@ -33,7 +33,6 @@ function renderReadme(model, outputDir, badges, diagrams) {
|
|
|
33
33
|
lines.push('```', '');
|
|
34
34
|
}
|
|
35
35
|
renderCliScenarios(lines, model, outputDir, diagrams);
|
|
36
|
-
renderDocumentationTags(lines);
|
|
37
36
|
if (model.config?.isReadme) {
|
|
38
37
|
renderConfiguration(lines, model);
|
|
39
38
|
}
|
|
@@ -73,15 +72,6 @@ function renderCliScenarios(lines, model, outputDir, diagrams) {
|
|
|
73
72
|
function findScenarioDiagram(diagrams, scenario) {
|
|
74
73
|
return diagrams.find((diagram) => diagram.path === `diagrams/sequences/${toFileStem(scenario.name)}.mmd`);
|
|
75
74
|
}
|
|
76
|
-
function renderDocumentationTags(lines) {
|
|
77
|
-
lines.push('## Documentation Tags', '');
|
|
78
|
-
for (const tag of DOCUMENTATION_TAGS) {
|
|
79
|
-
lines.push('<details>');
|
|
80
|
-
lines.push(`<summary>@${tag.name}</summary>`, '');
|
|
81
|
-
lines.push(tag.description, '');
|
|
82
|
-
lines.push('</details>', '');
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
75
|
function renderConfiguration(lines, model) {
|
|
86
76
|
const { config } = model;
|
|
87
77
|
if (config === null)
|
|
@@ -191,6 +181,7 @@ function renderExportAccordion(lines, item) {
|
|
|
191
181
|
lines.push(`<summary>${item.name}</summary>`, '');
|
|
192
182
|
renderSignature(lines, item);
|
|
193
183
|
lines.push(item.description ?? `\`${item.kind}\` export.`, '');
|
|
184
|
+
renderStructuredRows(lines, item);
|
|
194
185
|
renderExamples(lines, item.examples);
|
|
195
186
|
lines.push(`Module: \`${item.modulePath}\``);
|
|
196
187
|
lines.push(`Source: \`${item.sourceLocation.filePath}:${item.sourceLocation.line}:${item.sourceLocation.column}\``);
|
|
@@ -216,6 +207,46 @@ function renderExamples(lines, examples) {
|
|
|
216
207
|
lines.push('```', '');
|
|
217
208
|
}
|
|
218
209
|
}
|
|
210
|
+
function renderStructuredRows(lines, item) {
|
|
211
|
+
if (item.structuredRows.length === 0)
|
|
212
|
+
return;
|
|
213
|
+
const columns = getStructuredColumns(item);
|
|
214
|
+
if (columns.length === 0)
|
|
215
|
+
return;
|
|
216
|
+
lines.push('| ' + columns.map(formatStructuredColumnHeader).join(' | ') + ' |');
|
|
217
|
+
lines.push('| ' + columns.map(() => '---').join(' | ') + ' |');
|
|
218
|
+
for (const row of item.structuredRows) {
|
|
219
|
+
lines.push('| ' +
|
|
220
|
+
columns
|
|
221
|
+
.map((column) => formatStructuredCell(column, row.values[column] ?? ''))
|
|
222
|
+
.join(' | ') +
|
|
223
|
+
' |');
|
|
224
|
+
}
|
|
225
|
+
lines.push('');
|
|
226
|
+
}
|
|
227
|
+
function getStructuredColumns(item) {
|
|
228
|
+
const columns = new Set();
|
|
229
|
+
for (const row of item.structuredRows) {
|
|
230
|
+
for (const column of Object.keys(row.values)) {
|
|
231
|
+
columns.add(column);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return [...columns];
|
|
235
|
+
}
|
|
236
|
+
function formatStructuredColumnHeader(column) {
|
|
237
|
+
return escapeTableCell(column.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase());
|
|
238
|
+
}
|
|
239
|
+
function formatStructuredCell(column, value) {
|
|
240
|
+
const escaped = escapeTableCell(value);
|
|
241
|
+
if (column === 'syntax' || column === 'name' || column === 'handler') {
|
|
242
|
+
return `\`${escaped}\``;
|
|
243
|
+
}
|
|
244
|
+
if (value === 'true')
|
|
245
|
+
return 'yes';
|
|
246
|
+
if (value === 'false')
|
|
247
|
+
return 'no';
|
|
248
|
+
return escaped;
|
|
249
|
+
}
|
|
219
250
|
function getReadmeGroups(model) {
|
|
220
251
|
const exportsByName = new Map(model.exports.map((entry) => [entry.name, entry]));
|
|
221
252
|
const componentNames = new Set(model.components.map((component) => component.name));
|
|
@@ -255,7 +286,9 @@ function getReadmeItemName(item) {
|
|
|
255
286
|
}
|
|
256
287
|
function getReadmeCategory(modulePath, name) {
|
|
257
288
|
if (modulePath.includes('/config/'))
|
|
258
|
-
return '
|
|
289
|
+
return 'Config';
|
|
290
|
+
if (modulePath.includes('/doc-tags/'))
|
|
291
|
+
return 'Documentation';
|
|
259
292
|
if (modulePath.includes('/primitives/'))
|
|
260
293
|
return 'Primitives';
|
|
261
294
|
if (modulePath.includes('/components/'))
|
|
@@ -282,6 +315,7 @@ function renderExports(model) {
|
|
|
282
315
|
if (item.description) {
|
|
283
316
|
lines.push(item.description, '');
|
|
284
317
|
}
|
|
318
|
+
renderStructuredRows(lines, item);
|
|
285
319
|
if (item.signatures.length > 0) {
|
|
286
320
|
lines.push('### Signatures', '');
|
|
287
321
|
for (const signature of item.signatures) {
|
|
@@ -364,7 +398,8 @@ function toFileStem(value) {
|
|
|
364
398
|
.toLowerCase();
|
|
365
399
|
}
|
|
366
400
|
const CATEGORY_ORDER = [
|
|
367
|
-
'
|
|
401
|
+
'Config',
|
|
402
|
+
'Documentation',
|
|
368
403
|
'Primitives',
|
|
369
404
|
'Components',
|
|
370
405
|
'Patterns',
|
|
@@ -373,17 +408,3 @@ const CATEGORY_ORDER = [
|
|
|
373
408
|
'Utilities',
|
|
374
409
|
'Types',
|
|
375
410
|
];
|
|
376
|
-
const DOCUMENTATION_TAGS = [
|
|
377
|
-
{
|
|
378
|
-
name: 'readme',
|
|
379
|
-
description: 'Includes a documentation block or exported symbol in README output.',
|
|
380
|
-
},
|
|
381
|
-
{
|
|
382
|
-
name: 'config',
|
|
383
|
-
description: '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.',
|
|
384
|
-
},
|
|
385
|
-
{
|
|
386
|
-
name: 'example',
|
|
387
|
-
description: 'Adds a titled fenced code example to the generated documentation for a symbol.',
|
|
388
|
-
},
|
|
389
|
-
];
|