botdocs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/bin/botdocs.js +6 -0
- package/dist/src/builder/chunker.d.ts +41 -0
- package/dist/src/builder/chunker.js +139 -0
- package/dist/src/builder/embedder.d.ts +26 -0
- package/dist/src/builder/embedder.js +75 -0
- package/dist/src/builder/index.d.ts +5 -0
- package/dist/src/builder/index.js +142 -0
- package/dist/src/builder/markdown-processor.d.ts +26 -0
- package/dist/src/builder/markdown-processor.js +161 -0
- package/dist/src/builder/site-generator.d.ts +24 -0
- package/dist/src/builder/site-generator.js +152 -0
- package/dist/src/builder/template-engine.d.ts +30 -0
- package/dist/src/builder/template-engine.js +77 -0
- package/dist/src/builder/vector-db-builder.d.ts +20 -0
- package/dist/src/builder/vector-db-builder.js +63 -0
- package/dist/src/cli/index.d.ts +1 -0
- package/dist/src/cli/index.js +47 -0
- package/dist/src/cli/options.d.ts +7 -0
- package/dist/src/cli/options.js +5 -0
- package/dist/src/types/config.d.ts +24 -0
- package/dist/src/types/config.js +16 -0
- package/dist/src/types/document.d.ts +20 -0
- package/dist/src/types/document.js +1 -0
- package/dist/src/types/vector-db.d.ts +21 -0
- package/dist/src/types/vector-db.js +1 -0
- package/dist-client/assets/chatbox-CmTTiB2Z.js +1 -0
- package/dist-client/assets/rag-engine-L2vj3Y0G.js +7 -0
- package/dist-client/bundle.js +1 -0
- package/package.json +71 -0
- package/src/styles/chat.css +420 -0
- package/src/styles/main.css +532 -0
- package/src/styles/themes.css +78 -0
- package/src/templates/doc-page.html +27 -0
- package/src/templates/index.html +28 -0
- package/src/templates/layout.html +59 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import MarkdownIt from 'markdown-it';
|
|
2
|
+
import anchor from 'markdown-it-anchor';
|
|
3
|
+
import toc from 'markdown-it-toc-done-right';
|
|
4
|
+
import alerts from 'markdown-it-github-alerts';
|
|
5
|
+
import taskLists from 'markdown-it-task-lists';
|
|
6
|
+
import footnote from 'markdown-it-footnote';
|
|
7
|
+
import { full as emoji } from 'markdown-it-emoji';
|
|
8
|
+
import sub from 'markdown-it-sub';
|
|
9
|
+
import sup from 'markdown-it-sup';
|
|
10
|
+
import { bundledLanguages, getHighlighter } from 'shiki';
|
|
11
|
+
import matter from 'gray-matter';
|
|
12
|
+
import { relative, basename } from 'path';
|
|
13
|
+
export class MarkdownProcessor {
|
|
14
|
+
md;
|
|
15
|
+
shikiInitialized = false;
|
|
16
|
+
constructor() {
|
|
17
|
+
this.md = new MarkdownIt({
|
|
18
|
+
html: true,
|
|
19
|
+
linkify: true,
|
|
20
|
+
typographer: true,
|
|
21
|
+
breaks: false,
|
|
22
|
+
highlight: (code, lang, attrs) => {
|
|
23
|
+
// Fallback for when Shiki isn't initialized or lang not found
|
|
24
|
+
if (!lang) {
|
|
25
|
+
return `<pre><code>${this.escapeHtml(code)}</code></pre>`;
|
|
26
|
+
}
|
|
27
|
+
return `<pre><code class="language-${lang}">${this.escapeHtml(code)}</code></pre>`;
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
// Enable strikethrough (built-in feature)
|
|
31
|
+
.enable('strikethrough');
|
|
32
|
+
// Add anchor plugin for heading links
|
|
33
|
+
this.md.use(anchor, {
|
|
34
|
+
permalink: anchor.permalink.linkInsideHeader({
|
|
35
|
+
symbol: '#',
|
|
36
|
+
placement: 'before',
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
// Add table of contents plugin
|
|
40
|
+
this.md.use(toc, {
|
|
41
|
+
containerClass: 'toc',
|
|
42
|
+
listType: 'ul',
|
|
43
|
+
});
|
|
44
|
+
// Add GitHub alerts plugin for [!NOTE], [!WARNING], etc.
|
|
45
|
+
this.md.use(alerts);
|
|
46
|
+
// Add task lists plugin for - [ ] and - [x]
|
|
47
|
+
this.md.use(taskLists, {
|
|
48
|
+
enabled: true,
|
|
49
|
+
label: true,
|
|
50
|
+
labelAfter: true,
|
|
51
|
+
});
|
|
52
|
+
// Add footnotes plugin for [^1] style references
|
|
53
|
+
this.md.use(footnote);
|
|
54
|
+
// Add emoji shortcuts plugin for :smile: → 😄
|
|
55
|
+
this.md.use(emoji);
|
|
56
|
+
// Add subscript and superscript support
|
|
57
|
+
this.md.use(sub);
|
|
58
|
+
this.md.use(sup);
|
|
59
|
+
}
|
|
60
|
+
escapeHtml(text) {
|
|
61
|
+
return text
|
|
62
|
+
.replace(/&/g, '&')
|
|
63
|
+
.replace(/</g, '<')
|
|
64
|
+
.replace(/>/g, '>')
|
|
65
|
+
.replace(/"/g, '"')
|
|
66
|
+
.replace(/'/g, ''');
|
|
67
|
+
}
|
|
68
|
+
async setupShiki() {
|
|
69
|
+
if (this.shikiInitialized)
|
|
70
|
+
return;
|
|
71
|
+
try {
|
|
72
|
+
const highlighter = await getHighlighter({
|
|
73
|
+
themes: ['github-light', 'github-dark'],
|
|
74
|
+
langs: Object.keys(bundledLanguages),
|
|
75
|
+
});
|
|
76
|
+
// Override markdown-it highlight with Shiki, but with error handling
|
|
77
|
+
const originalHighlight = this.md.options.highlight;
|
|
78
|
+
this.md.options.highlight = (code, lang, attrs) => {
|
|
79
|
+
try {
|
|
80
|
+
if (!lang)
|
|
81
|
+
return originalHighlight(code, lang, attrs);
|
|
82
|
+
// Try to get the language, fall back to txt if not found
|
|
83
|
+
const languages = highlighter.getLoadedLanguages();
|
|
84
|
+
const safeLang = languages.includes(lang) ? lang : 'txt';
|
|
85
|
+
return highlighter.codeToHtml(code, {
|
|
86
|
+
lang: safeLang,
|
|
87
|
+
themes: {
|
|
88
|
+
light: 'github-light',
|
|
89
|
+
dark: 'github-dark',
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
// Fall back to default highlighting if Shiki fails
|
|
95
|
+
return originalHighlight(code, lang, attrs);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
this.shikiInitialized = true;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
console.warn('Failed to initialize Shiki, falling back to default code rendering');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Process a markdown file and extract front matter
|
|
106
|
+
*/
|
|
107
|
+
async processFile(filePath, inputDir, content) {
|
|
108
|
+
// Ensure Shiki is initialized
|
|
109
|
+
await this.setupShiki();
|
|
110
|
+
// Parse front matter
|
|
111
|
+
const { data: metadata, content: markdownContent } = matter(content);
|
|
112
|
+
// Convert markdown to HTML
|
|
113
|
+
const html = this.md.render(markdownContent);
|
|
114
|
+
// Generate relative path and URL
|
|
115
|
+
const relativePath = relative(inputDir, filePath);
|
|
116
|
+
const url = this.generateUrl(relativePath);
|
|
117
|
+
// Extract title from metadata or first h1
|
|
118
|
+
const title = metadata.title || this.extractTitle(markdownContent, relativePath);
|
|
119
|
+
return {
|
|
120
|
+
filePath,
|
|
121
|
+
relativePath,
|
|
122
|
+
content: markdownContent,
|
|
123
|
+
html,
|
|
124
|
+
metadata: {
|
|
125
|
+
...metadata,
|
|
126
|
+
title,
|
|
127
|
+
},
|
|
128
|
+
url,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Extract title from markdown content (first h1) or filename
|
|
133
|
+
*/
|
|
134
|
+
extractTitle(content, relativePath) {
|
|
135
|
+
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
136
|
+
if (h1Match) {
|
|
137
|
+
return h1Match[1];
|
|
138
|
+
}
|
|
139
|
+
// Fallback to filename
|
|
140
|
+
return basename(relativePath, '.md')
|
|
141
|
+
.replace(/-/g, ' ')
|
|
142
|
+
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Generate URL from relative file path
|
|
146
|
+
* e.g., "getting-started.md" -> "/getting-started.html"
|
|
147
|
+
* e.g., "api/overview.md" -> "/api/overview.html"
|
|
148
|
+
*/
|
|
149
|
+
generateUrl(relativePath) {
|
|
150
|
+
const url = relativePath
|
|
151
|
+
.replace(/\.md$/, '.html')
|
|
152
|
+
.replace(/\\/g, '/');
|
|
153
|
+
return url === 'index.html' ? '/' : `/${url}`;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Render markdown string to HTML
|
|
157
|
+
*/
|
|
158
|
+
render(markdown) {
|
|
159
|
+
return this.md.render(markdown);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ProcessedDocument } from '../types/document.js';
|
|
2
|
+
import { BotdocsConfig } from '../types/config.js';
|
|
3
|
+
export declare class SiteGenerator {
|
|
4
|
+
private processor;
|
|
5
|
+
private templateEngine;
|
|
6
|
+
private documents;
|
|
7
|
+
constructor();
|
|
8
|
+
/**
|
|
9
|
+
* Generate the complete site
|
|
10
|
+
*/
|
|
11
|
+
generate(inputDir: string, outputDir: string, config: BotdocsConfig): Promise<ProcessedDocument[]>;
|
|
12
|
+
/**
|
|
13
|
+
* Build navigation structure from documents
|
|
14
|
+
*/
|
|
15
|
+
private buildNavigation;
|
|
16
|
+
/**
|
|
17
|
+
* Render navigation HTML
|
|
18
|
+
*/
|
|
19
|
+
private renderNavigation;
|
|
20
|
+
/**
|
|
21
|
+
* Get all processed documents
|
|
22
|
+
*/
|
|
23
|
+
getDocuments(): ProcessedDocument[];
|
|
24
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { glob } from 'glob';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { join, dirname, resolve } from 'path';
|
|
5
|
+
import { MarkdownProcessor } from './markdown-processor.js';
|
|
6
|
+
import { TemplateEngine } from './template-engine.js';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
export class SiteGenerator {
|
|
11
|
+
processor;
|
|
12
|
+
templateEngine;
|
|
13
|
+
documents = [];
|
|
14
|
+
constructor() {
|
|
15
|
+
this.processor = new MarkdownProcessor();
|
|
16
|
+
this.templateEngine = new TemplateEngine();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Generate the complete site
|
|
20
|
+
*/
|
|
21
|
+
async generate(inputDir, outputDir, config) {
|
|
22
|
+
console.log('Processing markdown files...');
|
|
23
|
+
// Find all markdown files
|
|
24
|
+
const markdownFiles = await glob('**/*.md', {
|
|
25
|
+
cwd: inputDir,
|
|
26
|
+
absolute: true,
|
|
27
|
+
ignore: ['node_modules/**', '**/node_modules/**'],
|
|
28
|
+
});
|
|
29
|
+
if (markdownFiles.length === 0) {
|
|
30
|
+
throw new Error(`No markdown files found in ${inputDir}`);
|
|
31
|
+
}
|
|
32
|
+
console.log(`Found ${markdownFiles.length} markdown files`);
|
|
33
|
+
// Process all markdown files
|
|
34
|
+
for (const filePath of markdownFiles) {
|
|
35
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
36
|
+
const doc = await this.processor.processFile(filePath, inputDir, content);
|
|
37
|
+
this.documents.push(doc);
|
|
38
|
+
}
|
|
39
|
+
// Sort documents by path for consistent ordering
|
|
40
|
+
this.documents.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
41
|
+
// Generate navigation
|
|
42
|
+
const navigation = this.buildNavigation(this.documents);
|
|
43
|
+
// Load templates from source directory (not dist)
|
|
44
|
+
// From dist/src/builder, go to project root, then to src/templates
|
|
45
|
+
const templatesDir = resolve(__dirname, '../../../src/templates');
|
|
46
|
+
const layoutTemplate = readFileSync(join(templatesDir, 'layout.html'), 'utf-8');
|
|
47
|
+
const docPageTemplate = readFileSync(join(templatesDir, 'doc-page.html'), 'utf-8');
|
|
48
|
+
const indexTemplate = readFileSync(join(templatesDir, 'index.html'), 'utf-8');
|
|
49
|
+
// Generate HTML pages
|
|
50
|
+
console.log('Generating HTML pages...');
|
|
51
|
+
for (let i = 0; i < this.documents.length; i++) {
|
|
52
|
+
const doc = this.documents[i];
|
|
53
|
+
const isIndex = doc.relativePath === 'README.md' || doc.relativePath === 'index.md';
|
|
54
|
+
// Prepare navigation data
|
|
55
|
+
const prevDoc = i > 0 ? this.documents[i - 1] : null;
|
|
56
|
+
const nextDoc = i < this.documents.length - 1 ? this.documents[i + 1] : null;
|
|
57
|
+
// Render document content
|
|
58
|
+
const contentTemplate = isIndex ? indexTemplate : docPageTemplate;
|
|
59
|
+
const content = this.templateEngine.renderWithLoops(contentTemplate, {
|
|
60
|
+
html: doc.html,
|
|
61
|
+
metadata: doc.metadata,
|
|
62
|
+
title: doc.metadata.title,
|
|
63
|
+
description: doc.metadata.description,
|
|
64
|
+
pages: isIndex
|
|
65
|
+
? this.documents.filter(d => d !== doc).map(d => ({
|
|
66
|
+
title: d.metadata.title,
|
|
67
|
+
description: d.metadata.description,
|
|
68
|
+
url: d.url,
|
|
69
|
+
}))
|
|
70
|
+
: undefined,
|
|
71
|
+
prevPage: prevDoc
|
|
72
|
+
? {
|
|
73
|
+
title: prevDoc.metadata.title,
|
|
74
|
+
url: prevDoc.url,
|
|
75
|
+
}
|
|
76
|
+
: undefined,
|
|
77
|
+
nextPage: nextDoc
|
|
78
|
+
? {
|
|
79
|
+
title: nextDoc.metadata.title,
|
|
80
|
+
url: nextDoc.url,
|
|
81
|
+
}
|
|
82
|
+
: undefined,
|
|
83
|
+
});
|
|
84
|
+
// Render full page with layout
|
|
85
|
+
const html = this.templateEngine.renderWithLoops(layoutTemplate, {
|
|
86
|
+
title: doc.metadata.title || 'Documentation',
|
|
87
|
+
description: doc.metadata.description || config.description || '',
|
|
88
|
+
siteTitle: config.title || 'Documentation',
|
|
89
|
+
siteDescription: config.description || '',
|
|
90
|
+
content,
|
|
91
|
+
navigation: this.renderNavigation(navigation, doc.url),
|
|
92
|
+
chatEnabled: config.chat?.enabled,
|
|
93
|
+
});
|
|
94
|
+
// Write HTML file
|
|
95
|
+
const outputPath = join(outputDir, doc.relativePath.replace(/\.md$/, '.html'));
|
|
96
|
+
fs.ensureDirSync(dirname(outputPath));
|
|
97
|
+
writeFileSync(outputPath, html, 'utf-8');
|
|
98
|
+
}
|
|
99
|
+
// Copy index.html if README.md exists
|
|
100
|
+
const readmeDoc = this.documents.find((d) => d.relativePath === 'README.md' || d.relativePath === 'index.md');
|
|
101
|
+
if (readmeDoc) {
|
|
102
|
+
const readmePath = join(outputDir, readmeDoc.relativePath.replace(/\.md$/, '.html'));
|
|
103
|
+
const indexPath = join(outputDir, 'index.html');
|
|
104
|
+
if (readmePath !== indexPath) {
|
|
105
|
+
fs.copySync(readmePath, indexPath);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
console.log(`Generated ${this.documents.length} HTML pages`);
|
|
109
|
+
return this.documents;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Build navigation structure from documents
|
|
113
|
+
*/
|
|
114
|
+
buildNavigation(documents) {
|
|
115
|
+
const nav = [];
|
|
116
|
+
for (const doc of documents) {
|
|
117
|
+
const parts = doc.relativePath.split('/');
|
|
118
|
+
const title = doc.metadata.title || parts[parts.length - 1].replace(/\.md$/, '');
|
|
119
|
+
nav.push({
|
|
120
|
+
title,
|
|
121
|
+
url: doc.url,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return nav;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Render navigation HTML
|
|
128
|
+
*/
|
|
129
|
+
renderNavigation(items, currentUrl) {
|
|
130
|
+
if (items.length === 0)
|
|
131
|
+
return '';
|
|
132
|
+
let html = '<ul class="nav-list">';
|
|
133
|
+
for (const item of items) {
|
|
134
|
+
const isActive = item.url === currentUrl;
|
|
135
|
+
const activeClass = isActive ? ' class="active"' : '';
|
|
136
|
+
html += `<li${activeClass}>`;
|
|
137
|
+
html += `<a href="${item.url}"${activeClass}>${item.title}</a>`;
|
|
138
|
+
if (item.children && item.children.length > 0) {
|
|
139
|
+
html += this.renderNavigation(item.children, currentUrl);
|
|
140
|
+
}
|
|
141
|
+
html += '</li>';
|
|
142
|
+
}
|
|
143
|
+
html += '</ul>';
|
|
144
|
+
return html;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Get all processed documents
|
|
148
|
+
*/
|
|
149
|
+
getDocuments() {
|
|
150
|
+
return this.documents;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple template engine using string interpolation
|
|
3
|
+
*/
|
|
4
|
+
export declare class TemplateEngine {
|
|
5
|
+
/**
|
|
6
|
+
* Render a template with variables
|
|
7
|
+
* Replaces {{variableName}} with values from data object
|
|
8
|
+
*/
|
|
9
|
+
render(template: string, data: Record<string, any>): string;
|
|
10
|
+
/**
|
|
11
|
+
* Render a template with nested variables
|
|
12
|
+
* Supports {{object.property}} syntax
|
|
13
|
+
*/
|
|
14
|
+
renderAdvanced(template: string, data: Record<string, any>): string;
|
|
15
|
+
/**
|
|
16
|
+
* Get nested property from object using dot notation
|
|
17
|
+
*/
|
|
18
|
+
private getNestedProperty;
|
|
19
|
+
/**
|
|
20
|
+
* Render template with conditional blocks
|
|
21
|
+
* Supports {{#if variable}}...{{/if}} syntax
|
|
22
|
+
* Handles nested conditionals recursively
|
|
23
|
+
*/
|
|
24
|
+
renderWithConditionals(template: string, data: Record<string, any>): string;
|
|
25
|
+
/**
|
|
26
|
+
* Render template with loops
|
|
27
|
+
* Supports {{#each items}}...{{/each}} syntax
|
|
28
|
+
*/
|
|
29
|
+
renderWithLoops(template: string, data: Record<string, any>): string;
|
|
30
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple template engine using string interpolation
|
|
3
|
+
*/
|
|
4
|
+
export class TemplateEngine {
|
|
5
|
+
/**
|
|
6
|
+
* Render a template with variables
|
|
7
|
+
* Replaces {{variableName}} with values from data object
|
|
8
|
+
*/
|
|
9
|
+
render(template, data) {
|
|
10
|
+
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
11
|
+
return data[key] !== undefined ? String(data[key]) : match;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Render a template with nested variables
|
|
16
|
+
* Supports {{object.property}} syntax
|
|
17
|
+
*/
|
|
18
|
+
renderAdvanced(template, data) {
|
|
19
|
+
return template.replace(/\{\{([\w.]+)\}\}/g, (match, path) => {
|
|
20
|
+
const value = this.getNestedProperty(data, path);
|
|
21
|
+
return value !== undefined ? String(value) : match;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get nested property from object using dot notation
|
|
26
|
+
*/
|
|
27
|
+
getNestedProperty(obj, path) {
|
|
28
|
+
return path.split('.').reduce((current, key) => current?.[key], obj);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Render template with conditional blocks
|
|
32
|
+
* Supports {{#if variable}}...{{/if}} syntax
|
|
33
|
+
* Handles nested conditionals recursively
|
|
34
|
+
*/
|
|
35
|
+
renderWithConditionals(template, data) {
|
|
36
|
+
let result = template;
|
|
37
|
+
let previousResult = '';
|
|
38
|
+
// Keep processing until no more conditionals are found (handles nesting)
|
|
39
|
+
while (result !== previousResult) {
|
|
40
|
+
previousResult = result;
|
|
41
|
+
// Match innermost if blocks first (non-greedy, no nested {{#if}} inside)
|
|
42
|
+
result = result.replace(/\{\{#if\s+([\w.]+)\}\}((?:(?!\{\{#if)[\s\S])*?)\{\{\/if\}\}/g, (match, key, content) => {
|
|
43
|
+
const value = this.getNestedProperty(data, key);
|
|
44
|
+
return value ? content : '';
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// Handle variable substitution
|
|
48
|
+
result = this.renderAdvanced(result, data);
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Render template with loops
|
|
53
|
+
* Supports {{#each items}}...{{/each}} syntax
|
|
54
|
+
*/
|
|
55
|
+
renderWithLoops(template, data) {
|
|
56
|
+
// Handle each blocks
|
|
57
|
+
let result = template.replace(/\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (match, key, content) => {
|
|
58
|
+
const items = data[key];
|
|
59
|
+
if (!Array.isArray(items))
|
|
60
|
+
return '';
|
|
61
|
+
return items
|
|
62
|
+
.map((item, index) => {
|
|
63
|
+
// Process conditionals AND variables for each item
|
|
64
|
+
return this.renderWithConditionals(content, {
|
|
65
|
+
...data,
|
|
66
|
+
...item,
|
|
67
|
+
index,
|
|
68
|
+
'@index': index,
|
|
69
|
+
});
|
|
70
|
+
})
|
|
71
|
+
.join('');
|
|
72
|
+
});
|
|
73
|
+
// Handle conditionals and variables outside loops
|
|
74
|
+
result = this.renderWithConditionals(result, data);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ProcessedDocument } from '../types/document.js';
|
|
2
|
+
import { VectorDatabase } from '../types/vector-db.js';
|
|
3
|
+
export interface VectorDBBuilderOptions {
|
|
4
|
+
chunkSize?: number;
|
|
5
|
+
chunkOverlap?: number;
|
|
6
|
+
modelName?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class VectorDBBuilder {
|
|
9
|
+
private chunker;
|
|
10
|
+
private embedder;
|
|
11
|
+
constructor(options?: VectorDBBuilderOptions);
|
|
12
|
+
/**
|
|
13
|
+
* Build vector database from documents
|
|
14
|
+
*/
|
|
15
|
+
build(documents: ProcessedDocument[], outputDir: string, verbose?: boolean): Promise<VectorDatabase>;
|
|
16
|
+
/**
|
|
17
|
+
* Generate unique ID for a chunk
|
|
18
|
+
*/
|
|
19
|
+
private generateChunkId;
|
|
20
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Chunker } from './chunker.js';
|
|
2
|
+
import { Embedder } from './embedder.js';
|
|
3
|
+
import { writeFileSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { createHash } from 'crypto';
|
|
6
|
+
export class VectorDBBuilder {
|
|
7
|
+
chunker;
|
|
8
|
+
embedder;
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.chunker = new Chunker({
|
|
11
|
+
maxChunkSize: options.chunkSize || 500,
|
|
12
|
+
chunkOverlap: options.chunkOverlap || 50,
|
|
13
|
+
});
|
|
14
|
+
this.embedder = new Embedder(options.modelName);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Build vector database from documents
|
|
18
|
+
*/
|
|
19
|
+
async build(documents, outputDir, verbose = false) {
|
|
20
|
+
if (verbose) {
|
|
21
|
+
console.log('Building vector database...');
|
|
22
|
+
}
|
|
23
|
+
// Step 1: Chunk documents
|
|
24
|
+
console.log('Chunking documents...');
|
|
25
|
+
const chunks = this.chunker.chunkDocuments(documents);
|
|
26
|
+
console.log(`Created ${chunks.length} chunks`);
|
|
27
|
+
// Step 2: Generate embeddings
|
|
28
|
+
console.log('Generating embeddings...');
|
|
29
|
+
await this.embedder.initialize();
|
|
30
|
+
const texts = chunks.map((chunk) => chunk.text);
|
|
31
|
+
const embeddings = await this.embedder.embedBatch(texts, 10);
|
|
32
|
+
// Step 3: Create document chunks with embeddings
|
|
33
|
+
const documentChunks = chunks.map((chunk, index) => ({
|
|
34
|
+
id: this.generateChunkId(chunk.text, chunk.metadata.sourceFile, index),
|
|
35
|
+
text: chunk.text,
|
|
36
|
+
embedding: embeddings[index],
|
|
37
|
+
metadata: chunk.metadata,
|
|
38
|
+
}));
|
|
39
|
+
// Step 4: Create vector database
|
|
40
|
+
const vectorDB = {
|
|
41
|
+
version: '1.0',
|
|
42
|
+
model: this.embedder.getModelName(),
|
|
43
|
+
dimension: this.embedder.getDimension(),
|
|
44
|
+
chunks: documentChunks,
|
|
45
|
+
};
|
|
46
|
+
// Step 5: Write to file
|
|
47
|
+
const outputPath = join(outputDir, 'vector-db.json');
|
|
48
|
+
writeFileSync(outputPath, JSON.stringify(vectorDB, null, 2), 'utf-8');
|
|
49
|
+
console.log(`Vector database saved: ${outputPath}`);
|
|
50
|
+
console.log(`Total chunks: ${vectorDB.chunks.length}`);
|
|
51
|
+
console.log(`Embedding dimension: ${vectorDB.dimension}`);
|
|
52
|
+
return vectorDB;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Generate unique ID for a chunk
|
|
56
|
+
*/
|
|
57
|
+
generateChunkId(text, sourceFile, index) {
|
|
58
|
+
const hash = createHash('md5')
|
|
59
|
+
.update(`${sourceFile}:${index}:${text.substring(0, 100)}`)
|
|
60
|
+
.digest('hex');
|
|
61
|
+
return hash.substring(0, 16);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { build } from '../builder/index.js';
|
|
3
|
+
import { defaultOptions } from './options.js';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { resolve, dirname } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
// Read package.json for version
|
|
10
|
+
// From dist/src/cli/index.js, go up 3 levels to project root
|
|
11
|
+
const packageJson = JSON.parse(readFileSync(resolve(__dirname, '../../../package.json'), 'utf-8'));
|
|
12
|
+
const program = new Command();
|
|
13
|
+
program
|
|
14
|
+
.name('botdocs')
|
|
15
|
+
.description('Convert markdown documentation into a static website with AI chatbot')
|
|
16
|
+
.version(packageJson.version)
|
|
17
|
+
.argument('<input>', 'Input directory containing markdown files')
|
|
18
|
+
.option('-o, --output <dir>', 'Output directory for generated site', defaultOptions.output)
|
|
19
|
+
.option('--no-chat', 'Disable AI chatbot functionality')
|
|
20
|
+
.option('-c, --config <file>', 'Path to config file (botdocs.config.json)')
|
|
21
|
+
.option('-v, --verbose', 'Enable verbose logging')
|
|
22
|
+
.action(async (input, options) => {
|
|
23
|
+
try {
|
|
24
|
+
const inputDir = resolve(process.cwd(), input);
|
|
25
|
+
const outputDir = resolve(process.cwd(), options.output || defaultOptions.output);
|
|
26
|
+
if (options.verbose) {
|
|
27
|
+
console.log('Botdocs starting...');
|
|
28
|
+
console.log(`Input: ${inputDir}`);
|
|
29
|
+
console.log(`Output: ${outputDir}`);
|
|
30
|
+
console.log(`Chat enabled: ${!options.noChat}`);
|
|
31
|
+
}
|
|
32
|
+
await build({
|
|
33
|
+
inputDir,
|
|
34
|
+
outputDir,
|
|
35
|
+
chatEnabled: !options.noChat,
|
|
36
|
+
configPath: options.config,
|
|
37
|
+
verbose: options.verbose || false,
|
|
38
|
+
});
|
|
39
|
+
console.log('Build complete!');
|
|
40
|
+
console.log(`Site generated at: ${outputDir}`);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.error('Build failed:', error);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
program.parse();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface BotdocsConfig {
|
|
2
|
+
title?: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
theme?: {
|
|
5
|
+
primaryColor?: string;
|
|
6
|
+
};
|
|
7
|
+
chat?: {
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
welcomeMessage?: string;
|
|
10
|
+
};
|
|
11
|
+
build?: {
|
|
12
|
+
chunkSize?: number;
|
|
13
|
+
chunkOverlap?: number;
|
|
14
|
+
topK?: number;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export interface BuildOptions {
|
|
18
|
+
inputDir: string;
|
|
19
|
+
outputDir: string;
|
|
20
|
+
chatEnabled: boolean;
|
|
21
|
+
configPath?: string;
|
|
22
|
+
verbose: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare const defaultConfig: BotdocsConfig;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const defaultConfig = {
|
|
2
|
+
title: 'Documentation',
|
|
3
|
+
description: 'Project documentation',
|
|
4
|
+
theme: {
|
|
5
|
+
primaryColor: '#3b82f6',
|
|
6
|
+
},
|
|
7
|
+
chat: {
|
|
8
|
+
enabled: true,
|
|
9
|
+
welcomeMessage: 'Ask me anything about the docs!',
|
|
10
|
+
},
|
|
11
|
+
build: {
|
|
12
|
+
chunkSize: 500,
|
|
13
|
+
chunkOverlap: 50,
|
|
14
|
+
topK: 5,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface DocumentMetadata {
|
|
2
|
+
title?: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
date?: string;
|
|
5
|
+
author?: string;
|
|
6
|
+
[key: string]: any;
|
|
7
|
+
}
|
|
8
|
+
export interface ProcessedDocument {
|
|
9
|
+
filePath: string;
|
|
10
|
+
relativePath: string;
|
|
11
|
+
content: string;
|
|
12
|
+
html: string;
|
|
13
|
+
metadata: DocumentMetadata;
|
|
14
|
+
url: string;
|
|
15
|
+
}
|
|
16
|
+
export interface NavigationItem {
|
|
17
|
+
title: string;
|
|
18
|
+
url: string;
|
|
19
|
+
children?: NavigationItem[];
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|