botdocs 0.3.0 → 0.5.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/README.md +20 -2
- package/dist/src/builder/chunker.d.ts +18 -0
- package/dist/src/builder/chunker.js +62 -1
- package/dist/src/builder/embedder.js +5 -2
- package/dist/src/builder/index-size.d.ts +3 -0
- package/dist/src/builder/index-size.js +16 -0
- package/dist/src/builder/index.js +34 -3
- package/dist/src/builder/markdown-processor.d.ts +8 -1
- package/dist/src/builder/markdown-processor.js +40 -7
- package/dist/src/builder/paths.d.ts +6 -0
- package/dist/src/builder/paths.js +13 -0
- package/dist/src/builder/site-generator.d.ts +19 -2
- package/dist/src/builder/site-generator.js +117 -34
- package/dist/src/builder/template-engine.d.ts +4 -4
- package/dist/src/builder/template-engine.js +5 -1
- package/dist/src/builder/vector-db-builder.d.ts +1 -0
- package/dist/src/builder/vector-db-builder.js +1 -0
- package/dist/src/cli/index.js +61 -18
- package/dist/src/cli/options.d.ts +2 -0
- package/dist/src/cli/options.js +2 -1
- package/dist/src/cli/server.d.ts +6 -0
- package/dist/src/cli/server.js +79 -0
- package/dist/src/cli/watcher.d.ts +7 -0
- package/dist/src/cli/watcher.js +40 -0
- package/dist/src/shared/site-root.d.ts +2 -0
- package/dist/src/shared/site-root.js +9 -0
- package/dist/src/types/config.d.ts +4 -0
- package/dist/src/types/config.js +2 -0
- package/dist/src/types/document.d.ts +1 -1
- package/dist-client/assets/chatbox-Dw_HFrfR.js +8 -0
- package/dist-client/assets/rag-engine-B9wYRzqT.js +1 -0
- package/dist-client/bundle.js +1 -1
- package/dist-client/wasm/ort-wasm-simd-threaded.asyncify.wasm +0 -0
- package/man/botdocs.1 +193 -0
- package/package.json +18 -2
- package/src/styles/chat.css +144 -0
- package/src/styles/themes/classic.css +29 -0
- package/src/styles/themes/material.css +29 -0
- package/src/styles/themes/minimal.css +34 -0
- package/src/styles/themes/modern.css +34 -0
- package/src/styles/themes/slate.css +32 -36
- package/src/templates/layout.html +21 -2
- package/dist-client/assets/chatbox-CmTTiB2Z.js +0 -1
- package/dist-client/assets/rag-engine-L2vj3Y0G.js +0 -7
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, readdirSync, mkdirSync, cpSync } from 'fs';
|
|
2
|
-
import { join, dirname,
|
|
2
|
+
import { join, dirname, basename } from 'path';
|
|
3
3
|
import { MarkdownProcessor } from './markdown-processor.js';
|
|
4
4
|
import { TemplateEngine } from './template-engine.js';
|
|
5
|
+
import { underSrc } from './paths.js';
|
|
6
|
+
import { rootPrefix, relativeUrl } from '../shared/site-root.js';
|
|
5
7
|
import { fileURLToPath } from 'url';
|
|
6
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
9
|
const __dirname = dirname(__filename);
|
|
10
|
+
function stripHtml(value) {
|
|
11
|
+
return value.replace(/<[^>]*>/g, '');
|
|
12
|
+
}
|
|
13
|
+
export function absoluteUrl(baseUrl, urlPath) {
|
|
14
|
+
return `${baseUrl.replace(/\/+$/, '')}${urlPath.startsWith('/') ? urlPath : `/${urlPath}`}`;
|
|
15
|
+
}
|
|
8
16
|
export class SiteGenerator {
|
|
9
17
|
processor;
|
|
10
18
|
templateEngine;
|
|
@@ -42,9 +50,10 @@ export class SiteGenerator {
|
|
|
42
50
|
this.documents.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
43
51
|
// Generate navigation
|
|
44
52
|
const navigation = this.buildNavigation(this.documents);
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
|
|
53
|
+
const pageSequence = this.buildPageSequence(navigation);
|
|
54
|
+
// Load templates from the project's src/templates, whether running
|
|
55
|
+
// compiled from dist/ or directly from src/ via tsx.
|
|
56
|
+
const templatesDir = underSrc(__dirname, 'templates');
|
|
48
57
|
const layoutTemplate = readFileSync(join(templatesDir, 'layout.html'), 'utf-8');
|
|
49
58
|
const docPageTemplate = readFileSync(join(templatesDir, 'doc-page.html'), 'utf-8');
|
|
50
59
|
const indexTemplate = readFileSync(join(templatesDir, 'index.html'), 'utf-8');
|
|
@@ -53,9 +62,11 @@ export class SiteGenerator {
|
|
|
53
62
|
for (let i = 0; i < this.documents.length; i++) {
|
|
54
63
|
const doc = this.documents[i];
|
|
55
64
|
const isIndex = doc.relativePath === 'README.md' || doc.relativePath === 'index.md';
|
|
65
|
+
const pageUrl = isIndex ? '/' : doc.url;
|
|
56
66
|
// Prepare navigation data
|
|
57
|
-
const
|
|
58
|
-
const
|
|
67
|
+
const adjacent = pageSequence.get(doc.url);
|
|
68
|
+
const root = rootPrefix(doc.url);
|
|
69
|
+
const relativeLink = (link) => link && { ...link, url: relativeUrl(root, link.url) };
|
|
59
70
|
// Render document content
|
|
60
71
|
const contentTemplate = isIndex ? indexTemplate : docPageTemplate;
|
|
61
72
|
const content = this.templateEngine.renderWithLoops(contentTemplate, {
|
|
@@ -67,31 +78,31 @@ export class SiteGenerator {
|
|
|
67
78
|
? this.documents.filter(d => d !== doc).map(d => ({
|
|
68
79
|
title: d.metadata.title,
|
|
69
80
|
description: d.metadata.description,
|
|
70
|
-
url: d.url,
|
|
81
|
+
url: relativeUrl(root, d.url),
|
|
71
82
|
}))
|
|
72
83
|
: undefined,
|
|
73
|
-
prevPage:
|
|
74
|
-
|
|
75
|
-
title: prevDoc.metadata.title,
|
|
76
|
-
url: prevDoc.url,
|
|
77
|
-
}
|
|
78
|
-
: undefined,
|
|
79
|
-
nextPage: nextDoc
|
|
80
|
-
? {
|
|
81
|
-
title: nextDoc.metadata.title,
|
|
82
|
-
url: nextDoc.url,
|
|
83
|
-
}
|
|
84
|
-
: undefined,
|
|
84
|
+
prevPage: relativeLink(adjacent?.prev),
|
|
85
|
+
nextPage: relativeLink(adjacent?.next),
|
|
85
86
|
});
|
|
86
87
|
// Render full page with layout
|
|
87
88
|
const html = this.templateEngine.renderWithLoops(layoutTemplate, {
|
|
88
89
|
title: doc.metadata.title || 'Documentation',
|
|
89
|
-
|
|
90
|
+
// The template engine doesn't escape interpolated values, so
|
|
91
|
+
// config.description may contain markup (e.g. a hotlink) meant
|
|
92
|
+
// for the visible siteDescription below — strip it here since
|
|
93
|
+
// this one lands inside a <meta content="..."> attribute.
|
|
94
|
+
description: stripHtml(doc.metadata.description || config.description || ''),
|
|
90
95
|
siteTitle: config.title || 'Documentation',
|
|
91
96
|
siteDescription: config.description || '',
|
|
92
97
|
content,
|
|
93
|
-
|
|
98
|
+
root,
|
|
99
|
+
navigation: this.renderNavigation(navigation, root, doc.url),
|
|
94
100
|
chatEnabled: config.chat?.enabled,
|
|
101
|
+
ogUrl: config.baseUrl ? absoluteUrl(config.baseUrl, pageUrl) : undefined,
|
|
102
|
+
searchConfigJson: JSON.stringify({
|
|
103
|
+
topK: config.build?.topK ?? 3,
|
|
104
|
+
minScore: config.build?.minScore ?? 0.75,
|
|
105
|
+
}),
|
|
95
106
|
attribution: config.attribution !== false, // defaults to true
|
|
96
107
|
});
|
|
97
108
|
// Write HTML file
|
|
@@ -109,43 +120,115 @@ export class SiteGenerator {
|
|
|
109
120
|
}
|
|
110
121
|
}
|
|
111
122
|
console.log(`Generated ${this.documents.length} HTML pages`);
|
|
123
|
+
if (config.baseUrl) {
|
|
124
|
+
this.writeSitemap(outputDir, config.baseUrl);
|
|
125
|
+
}
|
|
112
126
|
return this.documents;
|
|
113
127
|
}
|
|
128
|
+
writeSitemap(outputDir, baseUrl) {
|
|
129
|
+
const urls = this.documents
|
|
130
|
+
.map((doc) => {
|
|
131
|
+
const isRootIndex = doc.relativePath === 'README.md' || doc.relativePath === 'index.md';
|
|
132
|
+
return absoluteUrl(baseUrl, isRootIndex ? '/' : doc.url);
|
|
133
|
+
}).map((loc) => ` <url><loc>${loc}</loc></url>`)
|
|
134
|
+
.join('\n');
|
|
135
|
+
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}\n</urlset>\n`;
|
|
136
|
+
writeFileSync(join(outputDir, 'sitemap.xml'), sitemap, 'utf-8');
|
|
137
|
+
console.log('Generated sitemap.xml');
|
|
138
|
+
}
|
|
114
139
|
/**
|
|
115
|
-
* Build navigation structure from documents
|
|
140
|
+
* Build navigation structure from documents, grouping by top-level
|
|
141
|
+
* folder so e.g. every doc under `path-of-python/` (its README plus
|
|
142
|
+
* anything in `path-of-python/docs/`) nests under one "Path of Python"
|
|
143
|
+
* entry instead of interleaving flat with every other folder's pages.
|
|
144
|
+
* Files at the root (no folder) stay flat, top-level entries.
|
|
116
145
|
*/
|
|
117
146
|
buildNavigation(documents) {
|
|
118
147
|
const nav = [];
|
|
148
|
+
const groups = new Map();
|
|
119
149
|
for (const doc of documents) {
|
|
120
150
|
const parts = doc.relativePath.split('/');
|
|
121
|
-
const title = doc.metadata.title ||
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
151
|
+
const title = doc.metadata.title || basename(doc.relativePath, '.md');
|
|
152
|
+
const isRootIndex = doc.relativePath === 'README.md' || doc.relativePath === 'index.md';
|
|
153
|
+
if (parts.length === 1) {
|
|
154
|
+
if (!isRootIndex) {
|
|
155
|
+
nav.push({ title, url: doc.url });
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const folder = parts[0];
|
|
160
|
+
const restOfPath = parts.slice(1).join('/');
|
|
161
|
+
const isFolderIndex = restOfPath === 'README.md' || restOfPath === 'index.md';
|
|
162
|
+
let group = groups.get(folder);
|
|
163
|
+
if (!group) {
|
|
164
|
+
group = {
|
|
165
|
+
title: folder.replace(/[-_]/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase()),
|
|
166
|
+
url: doc.url,
|
|
167
|
+
children: [],
|
|
168
|
+
};
|
|
169
|
+
groups.set(folder, group);
|
|
170
|
+
nav.push(group);
|
|
171
|
+
}
|
|
172
|
+
if (isFolderIndex) {
|
|
173
|
+
group.url = doc.url;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
group.children.push({ title, url: doc.url });
|
|
177
|
+
}
|
|
126
178
|
}
|
|
127
179
|
return nav;
|
|
128
180
|
}
|
|
129
181
|
/**
|
|
130
|
-
* Render navigation HTML
|
|
182
|
+
* Render navigation HTML. Folder groups only expand their children when
|
|
183
|
+
* the current page is the group's root or one of its children — every
|
|
184
|
+
* other page sees the group collapsed to a single link that leads to
|
|
185
|
+
* its root README.
|
|
131
186
|
*/
|
|
132
|
-
renderNavigation(items, currentUrl) {
|
|
187
|
+
renderNavigation(items, root, currentUrl) {
|
|
133
188
|
if (items.length === 0)
|
|
134
189
|
return '';
|
|
135
190
|
let html = '<ul class="nav-list">';
|
|
136
191
|
for (const item of items) {
|
|
137
192
|
const isActive = item.url === currentUrl;
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
193
|
+
const hasChildren = !!item.children && item.children.length > 0;
|
|
194
|
+
const isExpanded = hasChildren && (isActive || item.children.some((child) => child.url === currentUrl));
|
|
195
|
+
const liClasses = [isActive && 'active', hasChildren && 'nav-group']
|
|
196
|
+
.filter(Boolean)
|
|
197
|
+
.join(' ');
|
|
198
|
+
html += `<li${liClasses ? ` class="${liClasses}"` : ''}>`;
|
|
199
|
+
html += `<a href="${relativeUrl(root, item.url)}"${isActive ? ' class="active"' : ''}>${item.title}</a>`;
|
|
200
|
+
if (isExpanded) {
|
|
201
|
+
html += this.renderNavigation(item.children, root, currentUrl);
|
|
143
202
|
}
|
|
144
203
|
html += '</li>';
|
|
145
204
|
}
|
|
146
205
|
html += '</ul>';
|
|
147
206
|
return html;
|
|
148
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Compute prev/next page links that walk the top-level navigation
|
|
210
|
+
* (root README to root README) rather than the flat, alphabetical
|
|
211
|
+
* document list — so paging from a folder's root never dips into that
|
|
212
|
+
* folder's children. Paging from within a group's children walks those
|
|
213
|
+
* siblings first, then rolls into the next top-level entry.
|
|
214
|
+
*/
|
|
215
|
+
buildPageSequence(navigation) {
|
|
216
|
+
const positions = new Map();
|
|
217
|
+
const asLink = (item) => ({ title: item.title, url: item.url });
|
|
218
|
+
for (let i = 0; i < navigation.length; i++) {
|
|
219
|
+
const item = navigation[i];
|
|
220
|
+
const prevTop = i > 0 ? asLink(navigation[i - 1]) : undefined;
|
|
221
|
+
const nextTop = i < navigation.length - 1 ? asLink(navigation[i + 1]) : undefined;
|
|
222
|
+
positions.set(item.url, { prev: prevTop, next: nextTop });
|
|
223
|
+
const children = item.children ?? [];
|
|
224
|
+
for (let j = 0; j < children.length; j++) {
|
|
225
|
+
const prev = j === 0 ? asLink(item) : asLink(children[j - 1]);
|
|
226
|
+
const next = j < children.length - 1 ? asLink(children[j + 1]) : nextTop;
|
|
227
|
+
positions.set(children[j].url, { prev, next });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return positions;
|
|
231
|
+
}
|
|
149
232
|
/**
|
|
150
233
|
* Get all processed documents
|
|
151
234
|
*/
|
|
@@ -6,12 +6,12 @@ export declare class TemplateEngine {
|
|
|
6
6
|
* Render a template with variables
|
|
7
7
|
* Replaces {{variableName}} with values from data object
|
|
8
8
|
*/
|
|
9
|
-
render(template: string, data: Record<string,
|
|
9
|
+
render(template: string, data: Record<string, unknown>): string;
|
|
10
10
|
/**
|
|
11
11
|
* Render a template with nested variables
|
|
12
12
|
* Supports {{object.property}} syntax
|
|
13
13
|
*/
|
|
14
|
-
renderAdvanced(template: string, data: Record<string,
|
|
14
|
+
renderAdvanced(template: string, data: Record<string, unknown>): string;
|
|
15
15
|
/**
|
|
16
16
|
* Get nested property from object using dot notation
|
|
17
17
|
*/
|
|
@@ -21,10 +21,10 @@ export declare class TemplateEngine {
|
|
|
21
21
|
* Supports {{#if variable}}...{{/if}} syntax
|
|
22
22
|
* Handles nested conditionals recursively
|
|
23
23
|
*/
|
|
24
|
-
renderWithConditionals(template: string, data: Record<string,
|
|
24
|
+
renderWithConditionals(template: string, data: Record<string, unknown>): string;
|
|
25
25
|
/**
|
|
26
26
|
* Render template with loops
|
|
27
27
|
* Supports {{#each items}}...{{/each}} syntax
|
|
28
28
|
*/
|
|
29
|
-
renderWithLoops(template: string, data: Record<string,
|
|
29
|
+
renderWithLoops(template: string, data: Record<string, unknown>): string;
|
|
30
30
|
}
|
|
@@ -25,7 +25,11 @@ export class TemplateEngine {
|
|
|
25
25
|
* Get nested property from object using dot notation
|
|
26
26
|
*/
|
|
27
27
|
getNestedProperty(obj, path) {
|
|
28
|
-
return path
|
|
28
|
+
return path
|
|
29
|
+
.split('.')
|
|
30
|
+
.reduce((current, key) => current && typeof current === 'object'
|
|
31
|
+
? current[key]
|
|
32
|
+
: undefined, obj);
|
|
29
33
|
}
|
|
30
34
|
/**
|
|
31
35
|
* Render template with conditional blocks
|
package/dist/src/cli/index.js
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { build } from '../builder/index.js';
|
|
3
3
|
import { defaultOptions } from './options.js';
|
|
4
|
-
import {
|
|
4
|
+
import { startServer } from './server.js';
|
|
5
|
+
import { watchDocs } from './watcher.js';
|
|
6
|
+
import { readFileSync, existsSync } from 'fs';
|
|
5
7
|
import { resolve, dirname } from 'path';
|
|
6
8
|
import { fileURLToPath } from 'url';
|
|
7
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
10
|
const __dirname = dirname(__filename);
|
|
9
|
-
// Read package.json for version
|
|
10
|
-
//
|
|
11
|
-
|
|
11
|
+
// Read package.json for version. Walks up from this file so it resolves
|
|
12
|
+
// both from dist/src/cli (installed) and src/cli (tsx).
|
|
13
|
+
function findPackageJson(startDir) {
|
|
14
|
+
let dir = startDir;
|
|
15
|
+
while (dir !== dirname(dir)) {
|
|
16
|
+
const candidate = resolve(dir, 'package.json');
|
|
17
|
+
if (existsSync(candidate))
|
|
18
|
+
return candidate;
|
|
19
|
+
dir = dirname(dir);
|
|
20
|
+
}
|
|
21
|
+
throw new Error('package.json not found');
|
|
22
|
+
}
|
|
23
|
+
const packageJson = JSON.parse(readFileSync(findPackageJson(__dirname), 'utf-8'));
|
|
12
24
|
const program = new Command();
|
|
13
25
|
program
|
|
14
26
|
.name('botdocs')
|
|
@@ -18,29 +30,60 @@ program
|
|
|
18
30
|
.option('-o, --output <dir>', 'Output directory for generated site', defaultOptions.output)
|
|
19
31
|
.option('--no-chat', 'Disable AI chatbot functionality')
|
|
20
32
|
.option('-c, --config <file>', 'Path to config file (botdocs.config.json)')
|
|
21
|
-
.option('-t, --theme <theme>', 'Theme to use (classic, material, minimal, slate, modern)'
|
|
33
|
+
.option('-t, --theme <theme>', 'Theme to use (classic, material, minimal, slate, modern); overrides config file if set')
|
|
22
34
|
.option('-v, --verbose', 'Enable verbose logging')
|
|
35
|
+
.option('-w, --watch', 'Rebuild on changes and serve the site for live preview')
|
|
36
|
+
.option('-p, --port <number>', 'Port for the preview server (with --watch)', parseInt)
|
|
23
37
|
.action(async (input, options) => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
console.log('Botdocs starting...');
|
|
29
|
-
console.log(`Input: ${inputDir}`);
|
|
30
|
-
console.log(`Output: ${outputDir}`);
|
|
31
|
-
console.log(`Chat enabled: ${!options.noChat}`);
|
|
32
|
-
console.log(`Theme: ${options.theme || defaultOptions.theme}`);
|
|
33
|
-
}
|
|
38
|
+
const inputDir = resolve(process.cwd(), input);
|
|
39
|
+
const outputDir = resolve(process.cwd(), options.output || defaultOptions.output);
|
|
40
|
+
const verbose = options.verbose || false;
|
|
41
|
+
const runBuild = async () => {
|
|
34
42
|
await build({
|
|
35
43
|
inputDir,
|
|
36
44
|
outputDir,
|
|
37
45
|
chatEnabled: !options.noChat,
|
|
38
46
|
configPath: options.config,
|
|
39
|
-
verbose
|
|
47
|
+
verbose,
|
|
40
48
|
theme: options.theme,
|
|
41
49
|
});
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
if (verbose) {
|
|
53
|
+
console.log('Botdocs starting...');
|
|
54
|
+
console.log(`Input: ${inputDir}`);
|
|
55
|
+
console.log(`Output: ${outputDir}`);
|
|
56
|
+
console.log(`Chat enabled: ${!options.noChat}`);
|
|
57
|
+
console.log(`Theme override: ${options.theme || '(none — using config file or default)'}`);
|
|
58
|
+
}
|
|
59
|
+
await runBuild();
|
|
60
|
+
if (options.watch) {
|
|
61
|
+
const server = await startServer(outputDir, options.port ?? defaultOptions.port);
|
|
62
|
+
console.log(`Site generated at: ${outputDir}`);
|
|
63
|
+
console.log(`Preview at: ${server.url}`);
|
|
64
|
+
console.log('Watching for changes... Press Ctrl+C to stop.');
|
|
65
|
+
const watcher = watchDocs(inputDir, (changes) => {
|
|
66
|
+
const names = [...new Set(changes.map((c) => c.path.split('/').pop()))].join(', ');
|
|
67
|
+
console.log(`Changed: ${names} — rebuilding...`);
|
|
68
|
+
runBuild().catch((error) => {
|
|
69
|
+
console.error('Rebuild failed:', error);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
let shuttingDown = false;
|
|
73
|
+
const shutdown = () => {
|
|
74
|
+
if (shuttingDown)
|
|
75
|
+
return;
|
|
76
|
+
shuttingDown = true;
|
|
77
|
+
watcher.close();
|
|
78
|
+
server.close().finally(() => process.exit(0));
|
|
79
|
+
};
|
|
80
|
+
process.on('SIGINT', shutdown);
|
|
81
|
+
process.on('SIGTERM', shutdown);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log('Build complete!');
|
|
85
|
+
console.log(`Site generated at: ${outputDir}`);
|
|
86
|
+
}
|
|
44
87
|
}
|
|
45
88
|
catch (error) {
|
|
46
89
|
console.error('Build failed:', error);
|
package/dist/src/cli/options.js
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import { join, resolve, sep, extname } from 'path';
|
|
4
|
+
const MIME_TYPES = {
|
|
5
|
+
'.html': 'text/html; charset=utf-8',
|
|
6
|
+
'.css': 'text/css; charset=utf-8',
|
|
7
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
8
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
9
|
+
'.json': 'application/json; charset=utf-8',
|
|
10
|
+
'.map': 'application/json; charset=utf-8',
|
|
11
|
+
'.svg': 'image/svg+xml',
|
|
12
|
+
'.png': 'image/png',
|
|
13
|
+
'.jpg': 'image/jpeg',
|
|
14
|
+
'.jpeg': 'image/jpeg',
|
|
15
|
+
'.gif': 'image/gif',
|
|
16
|
+
'.webp': 'image/webp',
|
|
17
|
+
'.ico': 'image/x-icon',
|
|
18
|
+
'.xml': 'application/xml; charset=utf-8',
|
|
19
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
20
|
+
'.woff': 'font/woff',
|
|
21
|
+
'.woff2': 'font/woff2',
|
|
22
|
+
};
|
|
23
|
+
export async function startServer(rootDir, port = 0) {
|
|
24
|
+
const root = resolve(rootDir);
|
|
25
|
+
const server = createServer((req, res) => {
|
|
26
|
+
handleRequest(req, res, root).catch(() => {
|
|
27
|
+
respond(res, 500, 'text/plain; charset=utf-8', 'Internal server error');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
return new Promise((resolveStart, rejectStart) => {
|
|
31
|
+
server.once('error', rejectStart);
|
|
32
|
+
server.listen(port, '127.0.0.1', () => {
|
|
33
|
+
const address = server.address();
|
|
34
|
+
const actualPort = typeof address === 'object' && address ? address.port : port;
|
|
35
|
+
resolveStart({
|
|
36
|
+
url: `http://127.0.0.1:${actualPort}`,
|
|
37
|
+
port: actualPort,
|
|
38
|
+
close: () => new Promise((done) => server.close(() => done())),
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async function handleRequest(req, res, root) {
|
|
44
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
45
|
+
respond(res, 405, 'text/plain; charset=utf-8', 'Method not allowed');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
let urlPath;
|
|
49
|
+
try {
|
|
50
|
+
urlPath = decodeURIComponent(new URL(req.url ?? '/', 'http://localhost').pathname);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
respond(res, 400, 'text/plain; charset=utf-8', 'Bad request');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Complete mediation: every request resolves inside the site root or it
|
|
57
|
+
// never touches the filesystem.
|
|
58
|
+
const filePath = resolve(root, `.${sep}${urlPath}`);
|
|
59
|
+
if (filePath !== root && !filePath.startsWith(root + sep)) {
|
|
60
|
+
respond(res, 403, 'text/plain; charset=utf-8', 'Forbidden');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
let target = filePath;
|
|
64
|
+
try {
|
|
65
|
+
const stat = await fs.stat(target);
|
|
66
|
+
if (stat.isDirectory()) {
|
|
67
|
+
target = join(target, 'index.html');
|
|
68
|
+
}
|
|
69
|
+
const body = await fs.readFile(target);
|
|
70
|
+
respond(res, 200, MIME_TYPES[extname(target).toLowerCase()] ?? 'application/octet-stream', body);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
respond(res, 404, 'text/plain; charset=utf-8', 'Not found');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function respond(res, status, contentType, body) {
|
|
77
|
+
res.writeHead(status, { 'Content-Type': contentType });
|
|
78
|
+
res.end(body);
|
|
79
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { watch } from 'fs';
|
|
2
|
+
const WATCHED_EXTENSIONS = ['.md', '.markdown'];
|
|
3
|
+
const WATCHED_FILES = ['botdocs.config.json'];
|
|
4
|
+
function isWatchedPath(path) {
|
|
5
|
+
if (WATCHED_EXTENSIONS.some((ext) => path.endsWith(ext)))
|
|
6
|
+
return true;
|
|
7
|
+
return WATCHED_FILES.some((file) => path === file || path.endsWith(`/${file}`));
|
|
8
|
+
}
|
|
9
|
+
export function watchDocs(inputDir, onChange, debounceMs = 300) {
|
|
10
|
+
let pending = new Map();
|
|
11
|
+
let timer;
|
|
12
|
+
const flush = () => {
|
|
13
|
+
timer = undefined;
|
|
14
|
+
const changes = [...pending.values()];
|
|
15
|
+
pending = new Map();
|
|
16
|
+
if (changes.length > 0) {
|
|
17
|
+
onChange(changes);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const schedule = (path) => {
|
|
21
|
+
pending.set(path, { path });
|
|
22
|
+
if (!timer) {
|
|
23
|
+
timer = setTimeout(flush, debounceMs);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const watcher = watch(inputDir, { recursive: true }, (_eventType, filename) => {
|
|
27
|
+
const path = filename ?? '';
|
|
28
|
+
// Some platforms report no filename — treat as a full rebuild trigger.
|
|
29
|
+
if (!path || isWatchedPath(path)) {
|
|
30
|
+
schedule(path);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
close: () => {
|
|
35
|
+
if (timer)
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
watcher.close();
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function rootPrefix(pageUrl) {
|
|
2
|
+
const depth = pageUrl.split('/').length - 2;
|
|
3
|
+
return depth <= 0 ? './' : '../'.repeat(depth);
|
|
4
|
+
}
|
|
5
|
+
export function relativeUrl(root, url) {
|
|
6
|
+
if (!url.startsWith('/') || url.startsWith('//'))
|
|
7
|
+
return url;
|
|
8
|
+
return root + (url === '/' ? 'index.html' : url.slice(1));
|
|
9
|
+
}
|
|
@@ -3,7 +3,9 @@ export interface BotdocsConfig {
|
|
|
3
3
|
title?: string;
|
|
4
4
|
description?: string;
|
|
5
5
|
theme?: Theme;
|
|
6
|
+
customCss?: string;
|
|
6
7
|
attribution?: boolean;
|
|
8
|
+
baseUrl?: string;
|
|
7
9
|
chat?: {
|
|
8
10
|
enabled?: boolean;
|
|
9
11
|
welcomeMessage?: string;
|
|
@@ -11,7 +13,9 @@ export interface BotdocsConfig {
|
|
|
11
13
|
build?: {
|
|
12
14
|
chunkSize?: number;
|
|
13
15
|
chunkOverlap?: number;
|
|
16
|
+
minChunkSize?: number;
|
|
14
17
|
topK?: number;
|
|
18
|
+
minScore?: number;
|
|
15
19
|
};
|
|
16
20
|
}
|
|
17
21
|
export interface BuildOptions {
|
package/dist/src/types/config.js
CHANGED