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.
@@ -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, '&amp;')
688
+ .replace(/</g, '&lt;')
689
+ .replace(/>/g, '&gt;')
690
+ .replace(/"/g, '&quot;')
691
+ .replace(/'/g, '&#039;');
692
+ }
693
+ }
694
+ //# sourceMappingURL=site-generator.js.map