tova 0.2.9 → 0.3.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.
@@ -0,0 +1,390 @@
1
+ // Tova documentation generator
2
+ // Walks ASTs, extracts documented declarations, generates HTML or Markdown docs
3
+
4
+ export class DocGenerator {
5
+ constructor(modules) {
6
+ this.modules = modules; // Array of { name, ast }
7
+ }
8
+
9
+ generate(format = 'html') {
10
+ const allDocs = [];
11
+ for (const mod of this.modules) {
12
+ const docs = this._extractDocs(mod.ast, mod.name);
13
+ if (docs.length > 0) allDocs.push({ module: mod.name, docs });
14
+ }
15
+ return format === 'markdown' ? this._renderMarkdown(allDocs) : this._renderHtml(allDocs);
16
+ }
17
+
18
+ _extractDocs(ast, moduleName) {
19
+ const docs = [];
20
+ const walk = (nodes) => {
21
+ for (const node of nodes) {
22
+ if (!node) continue;
23
+ if (node.docstring) {
24
+ const entry = this._nodeToDoc(node, moduleName);
25
+ if (entry) docs.push(entry);
26
+ }
27
+ if (node.body && Array.isArray(node.body)) walk(node.body);
28
+ if ((node.type === 'ServerBlock' || node.type === 'ClientBlock' || node.type === 'SharedBlock') && node.body) {
29
+ walk(node.body);
30
+ }
31
+ }
32
+ };
33
+ walk(ast.body || []);
34
+ return docs;
35
+ }
36
+
37
+ _nodeToDoc(node) {
38
+ const parsed = this._parseDocstring(node.docstring);
39
+ switch (node.type) {
40
+ case 'FunctionDeclaration': return {
41
+ kind: 'function',
42
+ name: node.name,
43
+ params: (node.params || []).map(p => ({
44
+ name: typeof p === 'string' ? p : (p.name || ''),
45
+ type: p.typeAnnotation ? this._typeToString(p.typeAnnotation) : null,
46
+ default: p.defaultValue ? true : false,
47
+ })),
48
+ returnType: node.returnType ? this._typeToString(node.returnType) : null,
49
+ isAsync: node.isAsync || false,
50
+ ...parsed,
51
+ };
52
+ case 'TypeDeclaration': return {
53
+ kind: 'type',
54
+ name: node.name,
55
+ variants: (node.variants || []).map(v => ({
56
+ name: v.name,
57
+ fields: v.fields ? v.fields.map(f => ({ name: f.name, type: f.typeAnnotation ? this._typeToString(f.typeAnnotation) : null })) : [],
58
+ })),
59
+ ...parsed,
60
+ };
61
+ case 'InterfaceDeclaration': return {
62
+ kind: 'interface',
63
+ name: node.name,
64
+ methods: (node.methods || []).map(m => ({
65
+ name: m.name,
66
+ params: (m.params || []).map(p => typeof p === 'string' ? p : (p.name || '')),
67
+ returnType: m.returnType ? this._typeToString(m.returnType) : null,
68
+ })),
69
+ ...parsed,
70
+ };
71
+ case 'TraitDeclaration': return {
72
+ kind: 'trait',
73
+ name: node.name,
74
+ methods: (node.methods || []).map(m => ({
75
+ name: m.name,
76
+ params: (m.params || []).map(p => typeof p === 'string' ? p : (p.name || '')),
77
+ returnType: m.returnType ? this._typeToString(m.returnType) : null,
78
+ })),
79
+ ...parsed,
80
+ };
81
+ case 'Assignment': return {
82
+ kind: 'constant',
83
+ name: node.targets && node.targets[0] ? (typeof node.targets[0] === 'string' ? node.targets[0] : (node.targets[0].name || '')) : '',
84
+ ...parsed,
85
+ };
86
+ default: return null;
87
+ }
88
+ }
89
+
90
+ _typeToString(t) {
91
+ if (!t) return '';
92
+ if (typeof t === 'string') return t;
93
+ if (t.name) {
94
+ if (t.typeParams && t.typeParams.length > 0) {
95
+ return `${t.name}<${t.typeParams.map(p => this._typeToString(p)).join(', ')}>`;
96
+ }
97
+ return t.name;
98
+ }
99
+ if (t.type === 'ArrayTypeAnnotation') return `[${this._typeToString(t.elementType)}]`;
100
+ if (t.type === 'FunctionTypeAnnotation') {
101
+ const params = t.paramTypes.map(p => this._typeToString(p)).join(', ');
102
+ const ret = t.returnType ? this._typeToString(t.returnType) : 'Void';
103
+ return `(${params}) -> ${ret}`;
104
+ }
105
+ return String(t);
106
+ }
107
+
108
+ _parseDocstring(text) {
109
+ const lines = text.split('\n');
110
+ const description = [];
111
+ const params = [];
112
+ const returns = [];
113
+ const examples = [];
114
+ let inExample = false;
115
+ let exampleBuf = [];
116
+
117
+ for (const line of lines) {
118
+ const trimmed = line.trim();
119
+ if (trimmed.startsWith('@param')) {
120
+ inExample = false;
121
+ if (exampleBuf.length) { examples.push(exampleBuf.join('\n')); exampleBuf = []; }
122
+ const rest = trimmed.slice(6).trim();
123
+ const spaceIdx = rest.indexOf(' ');
124
+ if (spaceIdx > 0) {
125
+ params.push({ name: rest.slice(0, spaceIdx), description: rest.slice(spaceIdx + 1).trim() });
126
+ } else {
127
+ params.push({ name: rest, description: '' });
128
+ }
129
+ } else if (trimmed.startsWith('@returns') || trimmed.startsWith('@return')) {
130
+ inExample = false;
131
+ if (exampleBuf.length) { examples.push(exampleBuf.join('\n')); exampleBuf = []; }
132
+ const rest = trimmed.replace(/^@returns?\s*/, '').trim();
133
+ returns.push(rest);
134
+ } else if (trimmed.startsWith('@example')) {
135
+ inExample = true;
136
+ if (exampleBuf.length) { examples.push(exampleBuf.join('\n')); exampleBuf = []; }
137
+ } else if (inExample) {
138
+ exampleBuf.push(line);
139
+ } else {
140
+ description.push(trimmed);
141
+ }
142
+ }
143
+ if (exampleBuf.length) examples.push(exampleBuf.join('\n'));
144
+
145
+ return {
146
+ description: description.join(' ').trim(),
147
+ docParams: params,
148
+ docReturns: returns.join(' '),
149
+ docExamples: examples,
150
+ };
151
+ }
152
+
153
+ _renderHtml(allDocs) {
154
+ const pages = {};
155
+
156
+ // Index page
157
+ let indexHtml = `<!DOCTYPE html>
158
+ <html lang="en">
159
+ <head>
160
+ <meta charset="utf-8">
161
+ <title>Tova API Documentation</title>
162
+ <style>${this._getStyles()}</style>
163
+ </head>
164
+ <body>
165
+ <div class="container">
166
+ <h1>Tova API Documentation</h1>
167
+ <div class="modules">`;
168
+
169
+ for (const mod of allDocs) {
170
+ indexHtml += `\n <div class="module-card">
171
+ <h2><a href="${mod.module}.html">${mod.module}</a></h2>
172
+ <p>${mod.docs.length} documented item(s)</p>
173
+ <ul>`;
174
+ for (const doc of mod.docs) {
175
+ indexHtml += `\n <li><code>${doc.name}</code> <span class="badge">${doc.kind}</span></li>`;
176
+ }
177
+ indexHtml += `\n </ul>
178
+ </div>`;
179
+ }
180
+
181
+ indexHtml += `\n </div>
182
+ </div>
183
+ </body>
184
+ </html>`;
185
+ pages['index.html'] = indexHtml;
186
+
187
+ // Module pages
188
+ for (const mod of allDocs) {
189
+ let html = `<!DOCTYPE html>
190
+ <html lang="en">
191
+ <head>
192
+ <meta charset="utf-8">
193
+ <title>${mod.module} — Tova Docs</title>
194
+ <style>${this._getStyles()}</style>
195
+ </head>
196
+ <body>
197
+ <div class="container">
198
+ <p><a href="index.html">&larr; Back to index</a></p>
199
+ <h1>${mod.module}</h1>`;
200
+
201
+ for (const doc of mod.docs) {
202
+ html += this._renderDocEntry(doc);
203
+ }
204
+
205
+ html += `\n </div>
206
+ </body>
207
+ </html>`;
208
+ pages[`${mod.module}.html`] = html;
209
+ }
210
+
211
+ return pages;
212
+ }
213
+
214
+ _renderDocEntry(doc) {
215
+ let html = `\n <div class="doc-entry" id="${doc.name}">
216
+ <h3><span class="badge">${doc.kind}</span> ${doc.name}`;
217
+
218
+ if (doc.kind === 'function') {
219
+ const paramStr = (doc.params || []).map(p => {
220
+ let s = p.name;
221
+ if (p.type) s += ': ' + p.type;
222
+ return s;
223
+ }).join(', ');
224
+ html += `(${paramStr})`;
225
+ if (doc.returnType) html += ` -&gt; ${doc.returnType}`;
226
+ if (doc.isAsync) html = html.replace(`${doc.name}`, `async ${doc.name}`);
227
+ }
228
+
229
+ html += `</h3>`;
230
+ if (doc.description) html += `\n <p>${this._escapeHtml(doc.description)}</p>`;
231
+
232
+ if (doc.docParams && doc.docParams.length > 0) {
233
+ html += `\n <h4>Parameters</h4>\n <table><tr><th>Name</th><th>Description</th></tr>`;
234
+ for (const p of doc.docParams) {
235
+ html += `\n <tr><td><code>${p.name}</code></td><td>${this._escapeHtml(p.description)}</td></tr>`;
236
+ }
237
+ html += `\n </table>`;
238
+ }
239
+
240
+ if (doc.docReturns) {
241
+ html += `\n <h4>Returns</h4>\n <p>${this._escapeHtml(doc.docReturns)}</p>`;
242
+ }
243
+
244
+ if (doc.docExamples && doc.docExamples.length > 0) {
245
+ html += `\n <h4>Examples</h4>`;
246
+ for (const ex of doc.docExamples) {
247
+ html += `\n <pre><code>${this._escapeHtml(ex.trim())}</code></pre>`;
248
+ }
249
+ }
250
+
251
+ // Type variants
252
+ if (doc.kind === 'type' && doc.variants) {
253
+ html += `\n <h4>Variants</h4>\n <ul>`;
254
+ for (const v of doc.variants) {
255
+ const fields = v.fields.map(f => f.type ? `${f.name}: ${f.type}` : f.name).join(', ');
256
+ html += `\n <li><code>${v.name}${fields ? '(' + fields + ')' : ''}</code></li>`;
257
+ }
258
+ html += `\n </ul>`;
259
+ }
260
+
261
+ // Interface/trait methods
262
+ if ((doc.kind === 'interface' || doc.kind === 'trait') && doc.methods) {
263
+ html += `\n <h4>Methods</h4>\n <ul>`;
264
+ for (const m of doc.methods) {
265
+ html += `\n <li><code>fn ${m.name}(${m.params.join(', ')})${m.returnType ? ' -> ' + m.returnType : ''}</code></li>`;
266
+ }
267
+ html += `\n </ul>`;
268
+ }
269
+
270
+ html += `\n </div>`;
271
+ return html;
272
+ }
273
+
274
+ _renderMarkdown(allDocs) {
275
+ const pages = {};
276
+
277
+ let index = `# Tova API Documentation\n\n`;
278
+ for (const mod of allDocs) {
279
+ index += `## [${mod.module}](${mod.module}.md)\n\n`;
280
+ for (const doc of mod.docs) {
281
+ index += `- \`${doc.name}\` _(${doc.kind})_\n`;
282
+ }
283
+ index += '\n';
284
+ }
285
+ pages['index.md'] = index;
286
+
287
+ for (const mod of allDocs) {
288
+ let md = `# ${mod.module}\n\n`;
289
+ for (const doc of mod.docs) {
290
+ md += this._renderDocEntryMd(doc);
291
+ }
292
+ pages[`${mod.module}.md`] = md;
293
+ }
294
+
295
+ return pages;
296
+ }
297
+
298
+ _renderDocEntryMd(doc) {
299
+ let md = `## ${doc.name}\n\n`;
300
+ md += `**Kind:** ${doc.kind}\n\n`;
301
+
302
+ if (doc.kind === 'function') {
303
+ const paramStr = (doc.params || []).map(p => {
304
+ let s = p.name;
305
+ if (p.type) s += ': ' + p.type;
306
+ return s;
307
+ }).join(', ');
308
+ md += `\`\`\`tova\n${doc.isAsync ? 'async ' : ''}fn ${doc.name}(${paramStr})${doc.returnType ? ' -> ' + doc.returnType : ''}\n\`\`\`\n\n`;
309
+ }
310
+
311
+ if (doc.description) md += `${doc.description}\n\n`;
312
+
313
+ if (doc.docParams && doc.docParams.length > 0) {
314
+ md += `### Parameters\n\n| Name | Description |\n|------|-------------|\n`;
315
+ for (const p of doc.docParams) {
316
+ md += `| \`${p.name}\` | ${p.description} |\n`;
317
+ }
318
+ md += '\n';
319
+ }
320
+
321
+ if (doc.docReturns) md += `### Returns\n\n${doc.docReturns}\n\n`;
322
+
323
+ if (doc.docExamples && doc.docExamples.length > 0) {
324
+ md += `### Examples\n\n`;
325
+ for (const ex of doc.docExamples) {
326
+ md += `\`\`\`tova\n${ex.trim()}\n\`\`\`\n\n`;
327
+ }
328
+ }
329
+
330
+ if (doc.kind === 'type' && doc.variants) {
331
+ md += `### Variants\n\n`;
332
+ for (const v of doc.variants) {
333
+ const fields = v.fields.map(f => f.type ? `${f.name}: ${f.type}` : f.name).join(', ');
334
+ md += `- \`${v.name}${fields ? '(' + fields + ')' : ''}\`\n`;
335
+ }
336
+ md += '\n';
337
+ }
338
+
339
+ if ((doc.kind === 'interface' || doc.kind === 'trait') && doc.methods) {
340
+ md += `### Methods\n\n`;
341
+ for (const m of doc.methods) {
342
+ md += `- \`fn ${m.name}(${m.params.join(', ')})${m.returnType ? ' -> ' + m.returnType : ''}\`\n`;
343
+ }
344
+ md += '\n';
345
+ }
346
+
347
+ md += '---\n\n';
348
+ return md;
349
+ }
350
+
351
+ _escapeHtml(s) {
352
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
353
+ }
354
+
355
+ _getStyles() {
356
+ return `
357
+ :root {
358
+ --ctp-base: #1e1e2e;
359
+ --ctp-surface0: #313244;
360
+ --ctp-surface1: #45475a;
361
+ --ctp-text: #cdd6f4;
362
+ --ctp-subtext: #a6adc8;
363
+ --ctp-mauve: #cba6f7;
364
+ --ctp-blue: #89b4fa;
365
+ --ctp-green: #a6e3a1;
366
+ --ctp-peach: #fab387;
367
+ }
368
+ * { margin: 0; padding: 0; box-sizing: border-box; }
369
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--ctp-base); color: var(--ctp-text); line-height: 1.6; }
370
+ .container { max-width: 900px; margin: 0 auto; padding: 2rem; }
371
+ h1 { color: var(--ctp-mauve); margin-bottom: 1.5rem; }
372
+ h2 { color: var(--ctp-blue); margin: 1rem 0; }
373
+ h3 { color: var(--ctp-text); font-family: monospace; margin-top: 1.5rem; }
374
+ h4 { color: var(--ctp-subtext); margin: 0.8rem 0 0.3rem; font-size: 0.9rem; text-transform: uppercase; }
375
+ a { color: var(--ctp-blue); text-decoration: none; }
376
+ a:hover { text-decoration: underline; }
377
+ code { background: var(--ctp-surface0); padding: 0.15em 0.4em; border-radius: 4px; font-size: 0.9em; }
378
+ pre { background: var(--ctp-surface0); padding: 1rem; border-radius: 8px; overflow-x: auto; margin: 0.5rem 0; }
379
+ pre code { background: none; padding: 0; }
380
+ .badge { display: inline-block; background: var(--ctp-surface1); color: var(--ctp-mauve); font-size: 0.75rem; padding: 0.1em 0.5em; border-radius: 4px; font-family: sans-serif; vertical-align: middle; }
381
+ .doc-entry { border-left: 3px solid var(--ctp-surface1); padding-left: 1rem; margin: 1.5rem 0; }
382
+ .module-card { background: var(--ctp-surface0); border-radius: 8px; padding: 1rem 1.5rem; margin: 1rem 0; }
383
+ .module-card ul { list-style: none; padding-left: 0; }
384
+ .module-card li { margin: 0.3rem 0; }
385
+ table { border-collapse: collapse; width: 100%; margin: 0.5rem 0; }
386
+ th, td { border: 1px solid var(--ctp-surface1); padding: 0.4rem 0.8rem; text-align: left; }
387
+ th { background: var(--ctp-surface0); }
388
+ `;
389
+ }
390
+ }