ted-mosby 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +175 -20
- package/dist/cli.js +433 -16
- package/dist/cli.js.map +1 -1
- package/dist/prompts/wiki-system.d.ts +1 -1
- package/dist/prompts/wiki-system.d.ts.map +1 -1
- package/dist/prompts/wiki-system.js +52 -0
- package/dist/prompts/wiki-system.js.map +1 -1
- package/dist/rag/index.d.ts +68 -9
- package/dist/rag/index.d.ts.map +1 -1
- package/dist/rag/index.js +384 -79
- package/dist/rag/index.js.map +1 -1
- package/dist/site/scripts.d.ts +22 -0
- package/dist/site/scripts.d.ts.map +1 -0
- package/dist/site/scripts.js +855 -0
- package/dist/site/scripts.js.map +1 -0
- package/dist/site/styles.d.ts +11 -0
- package/dist/site/styles.d.ts.map +1 -0
- package/dist/site/styles.js +1572 -0
- package/dist/site/styles.js.map +1 -0
- package/dist/site/templates.d.ts +40 -0
- package/dist/site/templates.d.ts.map +1 -0
- package/dist/site/templates.js +336 -0
- package/dist/site/templates.js.map +1 -0
- package/dist/site-generator.d.ts +197 -0
- package/dist/site-generator.d.ts.map +1 -0
- package/dist/site-generator.js +694 -0
- package/dist/site-generator.js.map +1 -0
- package/dist/wiki-agent.d.ts +73 -0
- package/dist/wiki-agent.d.ts.map +1 -1
- package/dist/wiki-agent.js +1133 -7
- package/dist/wiki-agent.js.map +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static Site Generator for Architectural Wiki
|
|
3
|
+
*
|
|
4
|
+
* Transforms markdown wiki into an interactive, experiential static site
|
|
5
|
+
* with guided tours, code exploration, and magical onboarding features.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import matter from 'gray-matter';
|
|
10
|
+
import { marked } from 'marked';
|
|
11
|
+
import { getTemplates } from './site/templates.js';
|
|
12
|
+
import { getStyles } from './site/styles.js';
|
|
13
|
+
import { getClientScripts } from './site/scripts.js';
|
|
14
|
+
export class SiteGenerator {
|
|
15
|
+
options;
|
|
16
|
+
pages = [];
|
|
17
|
+
navigation = { sections: [] };
|
|
18
|
+
tours = [];
|
|
19
|
+
mermaidPlaceholders = new Map();
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.options = {
|
|
22
|
+
wikiDir: path.resolve(options.wikiDir),
|
|
23
|
+
outputDir: path.resolve(options.outputDir),
|
|
24
|
+
title: options.title || 'Architecture Wiki',
|
|
25
|
+
description: options.description || 'Interactive architectural documentation',
|
|
26
|
+
theme: options.theme || 'auto',
|
|
27
|
+
features: {
|
|
28
|
+
guidedTour: true,
|
|
29
|
+
codeExplorer: true,
|
|
30
|
+
search: true,
|
|
31
|
+
progressTracking: true,
|
|
32
|
+
keyboardNav: true,
|
|
33
|
+
...options.features
|
|
34
|
+
},
|
|
35
|
+
repoUrl: options.repoUrl || '',
|
|
36
|
+
repoPath: options.repoPath || ''
|
|
37
|
+
};
|
|
38
|
+
this.configureMarked();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Configure marked with custom renderers for enhanced features
|
|
42
|
+
*/
|
|
43
|
+
configureMarked() {
|
|
44
|
+
const renderer = new marked.Renderer();
|
|
45
|
+
// Enhanced code block rendering with copy button and source links
|
|
46
|
+
renderer.code = (code, language) => {
|
|
47
|
+
const lang = language || 'text';
|
|
48
|
+
const escapedCode = this.escapeHtml(code);
|
|
49
|
+
// Check for source reference in the code block
|
|
50
|
+
const sourceMatch = code.match(/^\/\/\s*Source:\s*(.+)$/m) ||
|
|
51
|
+
code.match(/^#\s*Source:\s*(.+)$/m);
|
|
52
|
+
const sourceRef = sourceMatch ? sourceMatch[1].trim() : '';
|
|
53
|
+
return `
|
|
54
|
+
<div class="code-block" data-language="${lang}">
|
|
55
|
+
<div class="code-header">
|
|
56
|
+
<span class="code-language">${lang}</span>
|
|
57
|
+
${sourceRef ? `<a href="#" class="code-source" data-source="${sourceRef}">${sourceRef}</a>` : ''}
|
|
58
|
+
<button class="code-copy" title="Copy code">
|
|
59
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
60
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
|
61
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
|
62
|
+
</svg>
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
<pre><code class="language-${lang}">${escapedCode}</code></pre>
|
|
66
|
+
</div>
|
|
67
|
+
`;
|
|
68
|
+
};
|
|
69
|
+
// Enhanced heading rendering with anchor links
|
|
70
|
+
renderer.heading = (text, level) => {
|
|
71
|
+
const id = this.slugify(text);
|
|
72
|
+
return `
|
|
73
|
+
<h${level} id="${id}" class="heading-anchor">
|
|
74
|
+
<a href="#${id}" class="anchor-link" aria-hidden="true">#</a>
|
|
75
|
+
${text}
|
|
76
|
+
</h${level}>
|
|
77
|
+
`;
|
|
78
|
+
};
|
|
79
|
+
// Enhanced link rendering
|
|
80
|
+
renderer.link = (href, title, text) => {
|
|
81
|
+
const isExternal = href.startsWith('http://') || href.startsWith('https://');
|
|
82
|
+
const isSourceLink = href.includes(':') && !isExternal && href.match(/\.(ts|js|py|go|rs|java|rb|php|c|cpp|swift):/);
|
|
83
|
+
if (isSourceLink) {
|
|
84
|
+
return `<a href="#" class="source-link" data-source="${href}" title="${title || 'View source'}">${text}</a>`;
|
|
85
|
+
}
|
|
86
|
+
if (isExternal) {
|
|
87
|
+
return `<a href="${href}" title="${title || ''}" target="_blank" rel="noopener noreferrer" class="external-link">${text}<svg class="external-icon" width="12" height="12" viewBox="0 0 24 24"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3" stroke="currentColor" stroke-width="2" fill="none"/></svg></a>`;
|
|
88
|
+
}
|
|
89
|
+
// Convert .md links to .html
|
|
90
|
+
const htmlHref = href.replace(/\.md(#.*)?$/, '.html$1');
|
|
91
|
+
return `<a href="${htmlHref}" title="${title || ''}" class="internal-link">${text}</a>`;
|
|
92
|
+
};
|
|
93
|
+
// Enhanced image rendering
|
|
94
|
+
renderer.image = (href, title, text) => {
|
|
95
|
+
return `
|
|
96
|
+
<figure class="image-figure">
|
|
97
|
+
<img src="${href}" alt="${text}" title="${title || ''}" loading="lazy" />
|
|
98
|
+
${title ? `<figcaption>${title}</figcaption>` : ''}
|
|
99
|
+
</figure>
|
|
100
|
+
`;
|
|
101
|
+
};
|
|
102
|
+
// Enhanced blockquote rendering (for callouts)
|
|
103
|
+
renderer.blockquote = (quote) => {
|
|
104
|
+
// Check for callout type markers
|
|
105
|
+
const calloutMatch = quote.match(/^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]/i);
|
|
106
|
+
if (calloutMatch) {
|
|
107
|
+
const type = calloutMatch[1].toLowerCase();
|
|
108
|
+
const content = quote.replace(calloutMatch[0], '').trim();
|
|
109
|
+
return `<div class="callout callout-${type}"><div class="callout-title">${calloutMatch[1]}</div><div class="callout-content">${content}</div></div>`;
|
|
110
|
+
}
|
|
111
|
+
return `<blockquote>${quote}</blockquote>`;
|
|
112
|
+
};
|
|
113
|
+
marked.use({ renderer });
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Generate the complete static site
|
|
117
|
+
*/
|
|
118
|
+
async generate() {
|
|
119
|
+
console.log('🌐 Generating static site...');
|
|
120
|
+
// Step 1: Discover and parse all wiki pages
|
|
121
|
+
await this.discoverPages();
|
|
122
|
+
console.log(` Found ${this.pages.length} wiki pages`);
|
|
123
|
+
// Step 2: Build navigation structure
|
|
124
|
+
this.buildNavigation();
|
|
125
|
+
// Step 3: Generate guided tours from content analysis
|
|
126
|
+
if (this.options.features.guidedTour) {
|
|
127
|
+
this.generateTours();
|
|
128
|
+
}
|
|
129
|
+
// Step 4: Create output directory structure
|
|
130
|
+
this.createOutputDirs();
|
|
131
|
+
// Step 5: Write static assets (CSS, JS)
|
|
132
|
+
await this.writeAssets();
|
|
133
|
+
// Step 6: Generate HTML pages
|
|
134
|
+
await this.generatePages();
|
|
135
|
+
// Step 7: Generate site manifest for client-side features
|
|
136
|
+
await this.generateManifest();
|
|
137
|
+
console.log(`✅ Static site generated at: ${this.options.outputDir}`);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Discover and parse all markdown files in the wiki directory
|
|
141
|
+
*/
|
|
142
|
+
async discoverPages() {
|
|
143
|
+
const files = this.findMarkdownFiles(this.options.wikiDir);
|
|
144
|
+
for (const filePath of files) {
|
|
145
|
+
const page = await this.parsePage(filePath);
|
|
146
|
+
if (page) {
|
|
147
|
+
this.pages.push(page);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Sort pages: README first, then alphabetically
|
|
151
|
+
this.pages.sort((a, b) => {
|
|
152
|
+
if (a.relativePath === 'README.md')
|
|
153
|
+
return -1;
|
|
154
|
+
if (b.relativePath === 'README.md')
|
|
155
|
+
return 1;
|
|
156
|
+
return a.relativePath.localeCompare(b.relativePath);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Recursively find all markdown files
|
|
161
|
+
*/
|
|
162
|
+
findMarkdownFiles(dir) {
|
|
163
|
+
const files = [];
|
|
164
|
+
if (!fs.existsSync(dir))
|
|
165
|
+
return files;
|
|
166
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
const fullPath = path.join(dir, entry.name);
|
|
169
|
+
if (entry.isDirectory()) {
|
|
170
|
+
// Skip hidden directories and cache
|
|
171
|
+
if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
172
|
+
files.push(...this.findMarkdownFiles(fullPath));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
176
|
+
files.push(fullPath);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return files;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Parse a single markdown page
|
|
183
|
+
*/
|
|
184
|
+
async parsePage(filePath) {
|
|
185
|
+
try {
|
|
186
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
187
|
+
const { data: frontmatter, content: markdownContent } = matter(content);
|
|
188
|
+
const relativePath = path.relative(this.options.wikiDir, filePath);
|
|
189
|
+
// Extract headings
|
|
190
|
+
const headings = this.extractHeadings(markdownContent);
|
|
191
|
+
// Extract code blocks
|
|
192
|
+
const codeBlocks = this.extractCodeBlocks(markdownContent);
|
|
193
|
+
// Extract mermaid diagrams
|
|
194
|
+
const mermaidDiagrams = this.extractMermaidDiagrams(markdownContent);
|
|
195
|
+
// Process mermaid blocks before markdown conversion (replaces with placeholders)
|
|
196
|
+
const processedMarkdown = this.processMermaidBlocks(markdownContent);
|
|
197
|
+
// Convert markdown to HTML
|
|
198
|
+
const parsedHtml = await marked.parse(processedMarkdown);
|
|
199
|
+
// Restore mermaid diagrams after markdown processing to preserve their syntax
|
|
200
|
+
const htmlContent = this.restoreMermaidBlocks(parsedHtml);
|
|
201
|
+
return {
|
|
202
|
+
path: filePath,
|
|
203
|
+
relativePath,
|
|
204
|
+
title: frontmatter.title || this.extractTitle(markdownContent) || path.basename(filePath, '.md'),
|
|
205
|
+
description: frontmatter.description,
|
|
206
|
+
content: markdownContent,
|
|
207
|
+
htmlContent,
|
|
208
|
+
frontmatter,
|
|
209
|
+
sources: frontmatter.sources,
|
|
210
|
+
related: frontmatter.related,
|
|
211
|
+
headings,
|
|
212
|
+
codeBlocks,
|
|
213
|
+
mermaidDiagrams
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
console.error(`Failed to parse ${filePath}:`, error);
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Extract headings from markdown content
|
|
223
|
+
*/
|
|
224
|
+
extractHeadings(content) {
|
|
225
|
+
const headings = [];
|
|
226
|
+
const regex = /^(#{1,6})\s+(.+)$/gm;
|
|
227
|
+
let match;
|
|
228
|
+
while ((match = regex.exec(content)) !== null) {
|
|
229
|
+
const level = match[1].length;
|
|
230
|
+
const text = match[2].trim();
|
|
231
|
+
headings.push({
|
|
232
|
+
level,
|
|
233
|
+
text,
|
|
234
|
+
id: this.slugify(text)
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return headings;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Extract code blocks from markdown
|
|
241
|
+
*/
|
|
242
|
+
extractCodeBlocks(content) {
|
|
243
|
+
const blocks = [];
|
|
244
|
+
const regex = /```(\w*)\n([\s\S]*?)```/g;
|
|
245
|
+
let match;
|
|
246
|
+
while ((match = regex.exec(content)) !== null) {
|
|
247
|
+
const language = match[1] || 'text';
|
|
248
|
+
const code = match[2];
|
|
249
|
+
// Skip mermaid blocks
|
|
250
|
+
if (language === 'mermaid')
|
|
251
|
+
continue;
|
|
252
|
+
// Look for source reference
|
|
253
|
+
const sourceMatch = code.match(/(?:\/\/|#)\s*Source:\s*(.+)/);
|
|
254
|
+
blocks.push({
|
|
255
|
+
language,
|
|
256
|
+
code,
|
|
257
|
+
sourceRef: sourceMatch ? sourceMatch[1].trim() : undefined
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return blocks;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Extract mermaid diagram definitions
|
|
264
|
+
*/
|
|
265
|
+
extractMermaidDiagrams(content) {
|
|
266
|
+
const diagrams = [];
|
|
267
|
+
const regex = /```mermaid\n([\s\S]*?)```/g;
|
|
268
|
+
let match;
|
|
269
|
+
while ((match = regex.exec(content)) !== null) {
|
|
270
|
+
diagrams.push(match[1].trim());
|
|
271
|
+
}
|
|
272
|
+
return diagrams;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Process mermaid blocks by replacing them with placeholders before markdown processing.
|
|
276
|
+
* This prevents marked from corrupting the mermaid diagram syntax.
|
|
277
|
+
*/
|
|
278
|
+
processMermaidBlocks(content) {
|
|
279
|
+
this.mermaidPlaceholders.clear();
|
|
280
|
+
let diagramIndex = 0;
|
|
281
|
+
return content.replace(/```mermaid\n([\s\S]*?)```/g, (_, diagram) => {
|
|
282
|
+
const id = `mermaid-${diagramIndex++}`;
|
|
283
|
+
const placeholder = `MERMAID_PLACEHOLDER_${id}_END`;
|
|
284
|
+
// Store the original diagram content with its HTML wrapper
|
|
285
|
+
this.mermaidPlaceholders.set(placeholder, `<div class="mermaid-container" id="${id}">
|
|
286
|
+
<div class="mermaid">${diagram.trim()}</div>
|
|
287
|
+
<button class="mermaid-fullscreen" title="Fullscreen" data-diagram="${id}">
|
|
288
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
289
|
+
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
|
|
290
|
+
</svg>
|
|
291
|
+
</button>
|
|
292
|
+
</div>`);
|
|
293
|
+
return placeholder;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Restore mermaid diagrams after markdown processing
|
|
298
|
+
*/
|
|
299
|
+
restoreMermaidBlocks(html) {
|
|
300
|
+
let result = html;
|
|
301
|
+
for (const [placeholder, diagram] of this.mermaidPlaceholders) {
|
|
302
|
+
// The placeholder might be wrapped in <p> tags by marked, so handle both cases
|
|
303
|
+
result = result.replace(new RegExp(`<p>${placeholder}</p>`, 'g'), diagram);
|
|
304
|
+
result = result.replace(new RegExp(placeholder, 'g'), diagram);
|
|
305
|
+
}
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Extract title from first H1 heading
|
|
310
|
+
*/
|
|
311
|
+
extractTitle(content) {
|
|
312
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
313
|
+
return match ? match[1].trim() : null;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Build navigation structure from pages
|
|
317
|
+
*/
|
|
318
|
+
buildNavigation() {
|
|
319
|
+
const sections = new Map();
|
|
320
|
+
for (const page of this.pages) {
|
|
321
|
+
const parts = page.relativePath.split(path.sep);
|
|
322
|
+
const section = parts.length > 1 ? parts[0] : 'Overview';
|
|
323
|
+
const htmlPath = page.relativePath.replace(/\.md$/, '.html');
|
|
324
|
+
if (!sections.has(section)) {
|
|
325
|
+
sections.set(section, []);
|
|
326
|
+
}
|
|
327
|
+
sections.get(section).push({
|
|
328
|
+
title: page.title,
|
|
329
|
+
path: htmlPath,
|
|
330
|
+
description: page.description
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
// Define section order
|
|
334
|
+
const sectionOrder = ['Overview', 'architecture', 'components', 'guides', 'api', 'reference'];
|
|
335
|
+
this.navigation.sections = Array.from(sections.entries())
|
|
336
|
+
.sort((a, b) => {
|
|
337
|
+
const aIndex = sectionOrder.indexOf(a[0]);
|
|
338
|
+
const bIndex = sectionOrder.indexOf(b[0]);
|
|
339
|
+
if (aIndex === -1 && bIndex === -1)
|
|
340
|
+
return a[0].localeCompare(b[0]);
|
|
341
|
+
if (aIndex === -1)
|
|
342
|
+
return 1;
|
|
343
|
+
if (bIndex === -1)
|
|
344
|
+
return -1;
|
|
345
|
+
return aIndex - bIndex;
|
|
346
|
+
})
|
|
347
|
+
.map(([title, pages]) => ({
|
|
348
|
+
title: this.formatSectionTitle(title),
|
|
349
|
+
path: pages[0]?.path || '',
|
|
350
|
+
pages
|
|
351
|
+
}));
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Format section title for display
|
|
355
|
+
*/
|
|
356
|
+
formatSectionTitle(title) {
|
|
357
|
+
return title
|
|
358
|
+
.split('-')
|
|
359
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
360
|
+
.join(' ');
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Generate guided tours based on content analysis
|
|
364
|
+
*/
|
|
365
|
+
generateTours() {
|
|
366
|
+
// Architecture Overview Tour
|
|
367
|
+
const architectureTour = {
|
|
368
|
+
id: 'architecture-overview',
|
|
369
|
+
name: 'Architecture Overview',
|
|
370
|
+
description: 'Get a bird\'s-eye view of the system architecture',
|
|
371
|
+
steps: []
|
|
372
|
+
};
|
|
373
|
+
// Find architecture pages and create tour steps
|
|
374
|
+
const archPages = this.pages.filter(p => p.relativePath.includes('architecture') ||
|
|
375
|
+
p.relativePath === 'README.md');
|
|
376
|
+
for (const page of archPages.slice(0, 5)) {
|
|
377
|
+
architectureTour.steps.push({
|
|
378
|
+
id: `arch-${this.slugify(page.title)}`,
|
|
379
|
+
title: page.title,
|
|
380
|
+
description: page.description || `Learn about ${page.title}`,
|
|
381
|
+
targetSelector: '.main-content',
|
|
382
|
+
page: page.relativePath.replace(/\.md$/, '.html')
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
if (architectureTour.steps.length > 0) {
|
|
386
|
+
this.tours.push(architectureTour);
|
|
387
|
+
}
|
|
388
|
+
// Getting Started Tour
|
|
389
|
+
const gettingStartedTour = {
|
|
390
|
+
id: 'getting-started',
|
|
391
|
+
name: 'Getting Started',
|
|
392
|
+
description: 'Quick introduction to navigating this documentation',
|
|
393
|
+
steps: [
|
|
394
|
+
{
|
|
395
|
+
id: 'welcome',
|
|
396
|
+
title: 'Welcome!',
|
|
397
|
+
description: 'This is your interactive architecture wiki. Let\'s take a quick tour!',
|
|
398
|
+
targetSelector: '.site-header',
|
|
399
|
+
position: 'bottom'
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
id: 'navigation',
|
|
403
|
+
title: 'Navigation',
|
|
404
|
+
description: 'Use the sidebar to browse different sections of the documentation.',
|
|
405
|
+
targetSelector: '.sidebar-nav',
|
|
406
|
+
position: 'right'
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
id: 'search',
|
|
410
|
+
title: 'Quick Search',
|
|
411
|
+
description: 'Press "/" or click here to search across all documentation.',
|
|
412
|
+
targetSelector: '.search-trigger',
|
|
413
|
+
position: 'bottom'
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
id: 'code-blocks',
|
|
417
|
+
title: 'Interactive Code',
|
|
418
|
+
description: 'Code blocks are syntax highlighted. Click the copy button or source link to explore.',
|
|
419
|
+
targetSelector: '.code-block',
|
|
420
|
+
position: 'top'
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
id: 'diagrams',
|
|
424
|
+
title: 'Architecture Diagrams',
|
|
425
|
+
description: 'Diagrams are interactive! Click to zoom, or use fullscreen mode.',
|
|
426
|
+
targetSelector: '.mermaid-container',
|
|
427
|
+
position: 'top'
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
id: 'progress',
|
|
431
|
+
title: 'Track Your Progress',
|
|
432
|
+
description: 'Your reading progress is saved. Pages you\'ve visited are marked in the sidebar.',
|
|
433
|
+
targetSelector: '.progress-indicator',
|
|
434
|
+
position: 'left'
|
|
435
|
+
}
|
|
436
|
+
]
|
|
437
|
+
};
|
|
438
|
+
this.tours.push(gettingStartedTour);
|
|
439
|
+
// Component Deep Dive Tour
|
|
440
|
+
const componentPages = this.pages.filter(p => p.relativePath.includes('component'));
|
|
441
|
+
if (componentPages.length > 0) {
|
|
442
|
+
const componentTour = {
|
|
443
|
+
id: 'component-deep-dive',
|
|
444
|
+
name: 'Component Deep Dive',
|
|
445
|
+
description: 'Explore the core components of the system',
|
|
446
|
+
steps: componentPages.slice(0, 8).map(page => ({
|
|
447
|
+
id: `comp-${this.slugify(page.title)}`,
|
|
448
|
+
title: page.title,
|
|
449
|
+
description: page.description || `Understand the ${page.title} component`,
|
|
450
|
+
targetSelector: '.main-content',
|
|
451
|
+
page: page.relativePath.replace(/\.md$/, '.html')
|
|
452
|
+
}))
|
|
453
|
+
};
|
|
454
|
+
this.tours.push(componentTour);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Create output directory structure
|
|
459
|
+
*/
|
|
460
|
+
createOutputDirs() {
|
|
461
|
+
if (!fs.existsSync(this.options.outputDir)) {
|
|
462
|
+
fs.mkdirSync(this.options.outputDir, { recursive: true });
|
|
463
|
+
}
|
|
464
|
+
// Create subdirectories for pages
|
|
465
|
+
const dirs = new Set();
|
|
466
|
+
for (const page of this.pages) {
|
|
467
|
+
const dir = path.dirname(page.relativePath);
|
|
468
|
+
if (dir !== '.') {
|
|
469
|
+
dirs.add(dir);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
for (const dir of dirs) {
|
|
473
|
+
const fullPath = path.join(this.options.outputDir, dir);
|
|
474
|
+
if (!fs.existsSync(fullPath)) {
|
|
475
|
+
fs.mkdirSync(fullPath, { recursive: true });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// Create assets directory
|
|
479
|
+
const assetsDir = path.join(this.options.outputDir, 'assets');
|
|
480
|
+
if (!fs.existsSync(assetsDir)) {
|
|
481
|
+
fs.mkdirSync(assetsDir, { recursive: true });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Write static assets (CSS, JS)
|
|
486
|
+
*/
|
|
487
|
+
async writeAssets() {
|
|
488
|
+
const assetsDir = path.join(this.options.outputDir, 'assets');
|
|
489
|
+
// Write CSS
|
|
490
|
+
const styles = getStyles();
|
|
491
|
+
fs.writeFileSync(path.join(assetsDir, 'styles.css'), styles);
|
|
492
|
+
// Write JavaScript
|
|
493
|
+
const scripts = getClientScripts(this.options.features);
|
|
494
|
+
fs.writeFileSync(path.join(assetsDir, 'app.js'), scripts);
|
|
495
|
+
console.log(' Wrote static assets');
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Generate HTML pages
|
|
499
|
+
*/
|
|
500
|
+
async generatePages() {
|
|
501
|
+
const templates = getTemplates();
|
|
502
|
+
for (const page of this.pages) {
|
|
503
|
+
const htmlPath = page.relativePath.replace(/\.md$/, '.html');
|
|
504
|
+
const outputPath = path.join(this.options.outputDir, htmlPath);
|
|
505
|
+
// Calculate relative path to root for asset links
|
|
506
|
+
const depth = page.relativePath.split(path.sep).length - 1;
|
|
507
|
+
const rootPath = depth > 0 ? '../'.repeat(depth) : './';
|
|
508
|
+
// Generate table of contents
|
|
509
|
+
const toc = this.generateTOC(page.headings);
|
|
510
|
+
// Generate breadcrumbs
|
|
511
|
+
const breadcrumbs = this.generateBreadcrumbs(page.relativePath);
|
|
512
|
+
// Generate related pages section
|
|
513
|
+
const relatedPages = this.generateRelatedPages(page);
|
|
514
|
+
// Build the full HTML page
|
|
515
|
+
const html = templates.page({
|
|
516
|
+
title: page.title,
|
|
517
|
+
siteTitle: this.options.title,
|
|
518
|
+
description: page.description || this.options.description,
|
|
519
|
+
content: page.htmlContent,
|
|
520
|
+
toc,
|
|
521
|
+
breadcrumbs,
|
|
522
|
+
relatedPages,
|
|
523
|
+
navigation: this.navigation,
|
|
524
|
+
rootPath,
|
|
525
|
+
currentPath: htmlPath,
|
|
526
|
+
features: this.options.features,
|
|
527
|
+
theme: this.options.theme
|
|
528
|
+
});
|
|
529
|
+
fs.writeFileSync(outputPath, html);
|
|
530
|
+
}
|
|
531
|
+
// Generate index.html (redirect to README.html or first page)
|
|
532
|
+
const indexPage = this.pages.find(p => p.relativePath === 'README.md') || this.pages[0];
|
|
533
|
+
if (indexPage) {
|
|
534
|
+
const indexHtml = `<!DOCTYPE html>
|
|
535
|
+
<html>
|
|
536
|
+
<head>
|
|
537
|
+
<meta charset="utf-8">
|
|
538
|
+
<meta http-equiv="refresh" content="0; url=${indexPage.relativePath.replace(/\.md$/, '.html')}">
|
|
539
|
+
<title>${this.options.title}</title>
|
|
540
|
+
</head>
|
|
541
|
+
<body>
|
|
542
|
+
<p>Redirecting to <a href="${indexPage.relativePath.replace(/\.md$/, '.html')}">${indexPage.title}</a>...</p>
|
|
543
|
+
</body>
|
|
544
|
+
</html>`;
|
|
545
|
+
fs.writeFileSync(path.join(this.options.outputDir, 'index.html'), indexHtml);
|
|
546
|
+
}
|
|
547
|
+
console.log(` Generated ${this.pages.length} HTML pages`);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Generate table of contents from headings
|
|
551
|
+
*/
|
|
552
|
+
generateTOC(headings) {
|
|
553
|
+
if (headings.length === 0)
|
|
554
|
+
return '';
|
|
555
|
+
// Filter to h2 and h3 only for cleaner TOC
|
|
556
|
+
const tocHeadings = headings.filter(h => h.level >= 2 && h.level <= 3);
|
|
557
|
+
if (tocHeadings.length < 2)
|
|
558
|
+
return '';
|
|
559
|
+
let html = '<nav class="toc"><h3 class="toc-title">On This Page</h3><ul class="toc-list">';
|
|
560
|
+
for (const heading of tocHeadings) {
|
|
561
|
+
const indent = heading.level - 2;
|
|
562
|
+
html += `<li class="toc-item toc-level-${indent}"><a href="#${heading.id}">${heading.text}</a></li>`;
|
|
563
|
+
}
|
|
564
|
+
html += '</ul></nav>';
|
|
565
|
+
return html;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Generate breadcrumb navigation
|
|
569
|
+
*/
|
|
570
|
+
generateBreadcrumbs(relativePath) {
|
|
571
|
+
const parts = relativePath.split(path.sep);
|
|
572
|
+
if (parts.length === 1)
|
|
573
|
+
return '';
|
|
574
|
+
let html = '<nav class="breadcrumbs" aria-label="Breadcrumb"><ol>';
|
|
575
|
+
let currentPath = '';
|
|
576
|
+
// Add home link
|
|
577
|
+
html += `<li><a href="${'../'.repeat(parts.length - 1)}index.html">Home</a></li>`;
|
|
578
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
579
|
+
currentPath += parts[i] + '/';
|
|
580
|
+
html += `<li><a href="${'../'.repeat(parts.length - 1 - i)}${currentPath}index.html">${this.formatSectionTitle(parts[i])}</a></li>`;
|
|
581
|
+
}
|
|
582
|
+
// Current page (not a link)
|
|
583
|
+
const page = this.pages.find(p => p.relativePath === relativePath);
|
|
584
|
+
html += `<li aria-current="page">${page?.title || parts[parts.length - 1]}</li>`;
|
|
585
|
+
html += '</ol></nav>';
|
|
586
|
+
return html;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Generate related pages section
|
|
590
|
+
*/
|
|
591
|
+
generateRelatedPages(page) {
|
|
592
|
+
const related = [];
|
|
593
|
+
// Add explicitly related pages
|
|
594
|
+
if (page.related) {
|
|
595
|
+
for (const relPath of page.related) {
|
|
596
|
+
const relPage = this.pages.find(p => p.relativePath === relPath || p.relativePath === relPath + '.md');
|
|
597
|
+
if (relPage)
|
|
598
|
+
related.push(relPage);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// Add pages in same section
|
|
602
|
+
const section = path.dirname(page.relativePath);
|
|
603
|
+
if (section !== '.') {
|
|
604
|
+
const sectionPages = this.pages.filter(p => path.dirname(p.relativePath) === section &&
|
|
605
|
+
p.relativePath !== page.relativePath);
|
|
606
|
+
for (const sp of sectionPages.slice(0, 3)) {
|
|
607
|
+
if (!related.includes(sp))
|
|
608
|
+
related.push(sp);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (related.length === 0)
|
|
612
|
+
return '';
|
|
613
|
+
let html = '<aside class="related-pages"><h3>Related Pages</h3><ul>';
|
|
614
|
+
for (const rp of related.slice(0, 5)) {
|
|
615
|
+
const href = this.getRelativeLink(page.relativePath, rp.relativePath);
|
|
616
|
+
html += `<li><a href="${href}">${rp.title}</a>${rp.description ? `<span class="related-desc">${rp.description}</span>` : ''}</li>`;
|
|
617
|
+
}
|
|
618
|
+
html += '</ul></aside>';
|
|
619
|
+
return html;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Get relative link between two pages
|
|
623
|
+
*/
|
|
624
|
+
getRelativeLink(fromPath, toPath) {
|
|
625
|
+
const fromDir = path.dirname(fromPath);
|
|
626
|
+
const toHtml = toPath.replace(/\.md$/, '.html');
|
|
627
|
+
if (fromDir === '.') {
|
|
628
|
+
return toHtml;
|
|
629
|
+
}
|
|
630
|
+
const depth = fromDir.split(path.sep).length;
|
|
631
|
+
return '../'.repeat(depth) + toHtml;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Generate site manifest for client-side features
|
|
635
|
+
*/
|
|
636
|
+
async generateManifest() {
|
|
637
|
+
const manifest = {
|
|
638
|
+
title: this.options.title,
|
|
639
|
+
description: this.options.description,
|
|
640
|
+
generated: new Date().toISOString(),
|
|
641
|
+
pages: this.pages.map(p => ({
|
|
642
|
+
path: p.relativePath.replace(/\.md$/, '.html'),
|
|
643
|
+
title: p.title,
|
|
644
|
+
description: p.description
|
|
645
|
+
})),
|
|
646
|
+
navigation: this.navigation,
|
|
647
|
+
tours: this.tours,
|
|
648
|
+
searchIndex: this.pages.map(p => ({
|
|
649
|
+
path: p.relativePath.replace(/\.md$/, '.html'),
|
|
650
|
+
title: p.title,
|
|
651
|
+
content: this.stripMarkdown(p.content).slice(0, 5000), // Limit for performance
|
|
652
|
+
headings: p.headings.map(h => h.text)
|
|
653
|
+
}))
|
|
654
|
+
};
|
|
655
|
+
fs.writeFileSync(path.join(this.options.outputDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
656
|
+
console.log(' Generated site manifest');
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Strip markdown formatting from content
|
|
660
|
+
*/
|
|
661
|
+
stripMarkdown(content) {
|
|
662
|
+
return content
|
|
663
|
+
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
|
|
664
|
+
.replace(/`[^`]+`/g, '') // Remove inline code
|
|
665
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Remove links
|
|
666
|
+
.replace(/[*_~]+/g, '') // Remove emphasis
|
|
667
|
+
.replace(/^#+\s*/gm, '') // Remove headings
|
|
668
|
+
.replace(/\n{3,}/g, '\n\n') // Normalize newlines
|
|
669
|
+
.trim();
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Create URL-safe slug from text
|
|
673
|
+
*/
|
|
674
|
+
slugify(text) {
|
|
675
|
+
return text
|
|
676
|
+
.toLowerCase()
|
|
677
|
+
.replace(/[^\w\s-]/g, '')
|
|
678
|
+
.replace(/\s+/g, '-')
|
|
679
|
+
.replace(/-+/g, '-')
|
|
680
|
+
.trim();
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Escape HTML entities
|
|
684
|
+
*/
|
|
685
|
+
escapeHtml(text) {
|
|
686
|
+
return text
|
|
687
|
+
.replace(/&/g, '&')
|
|
688
|
+
.replace(/</g, '<')
|
|
689
|
+
.replace(/>/g, '>')
|
|
690
|
+
.replace(/"/g, '"')
|
|
691
|
+
.replace(/'/g, ''');
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
//# sourceMappingURL=site-generator.js.map
|