autowiki 1.2.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/LICENSE +201 -0
- package/README.md +184 -0
- package/bin/autowiki.js +89 -0
- package/lib/highlight.js +84 -0
- package/lib/renderer.js +49 -0
- package/lib/scanner.js +178 -0
- package/lib/server.js +171 -0
- package/lib/template.js +662 -0
- package/package.json +26 -0
package/lib/scanner.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function parseFrontmatter(content) {
|
|
7
|
+
if (!content.startsWith('---')) {
|
|
8
|
+
return { frontmatter: {}, body: content };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const lines = content.split('\n');
|
|
12
|
+
let endLine = -1;
|
|
13
|
+
for (let i = 1; i < lines.length; i++) {
|
|
14
|
+
if (lines[i].trim() === '---') {
|
|
15
|
+
endLine = i;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (endLine === -1) {
|
|
21
|
+
return { frontmatter: {}, body: content };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const yamlLines = lines.slice(1, endLine);
|
|
25
|
+
const body = lines.slice(endLine + 1).join('\n').trim();
|
|
26
|
+
const frontmatter = {};
|
|
27
|
+
let currentKey = null;
|
|
28
|
+
let arrayMode = false;
|
|
29
|
+
|
|
30
|
+
for (const line of yamlLines) {
|
|
31
|
+
// Multi-line array item: " - value"
|
|
32
|
+
const arrayItem = line.match(/^\s+-\s+(.+)$/);
|
|
33
|
+
if (arrayItem && currentKey && arrayMode) {
|
|
34
|
+
if (!Array.isArray(frontmatter[currentKey])) {
|
|
35
|
+
frontmatter[currentKey] = [];
|
|
36
|
+
}
|
|
37
|
+
let val = arrayItem[1].trim();
|
|
38
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
39
|
+
val = val.slice(1, -1);
|
|
40
|
+
}
|
|
41
|
+
frontmatter[currentKey].push(val);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Key: value pair
|
|
46
|
+
const kv = line.match(/^([\w-]+):\s*(.*)$/);
|
|
47
|
+
if (kv) {
|
|
48
|
+
currentKey = kv[1];
|
|
49
|
+
let value = kv[2].trim();
|
|
50
|
+
|
|
51
|
+
if (!value) {
|
|
52
|
+
arrayMode = true;
|
|
53
|
+
frontmatter[currentKey] = [];
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
arrayMode = false;
|
|
58
|
+
|
|
59
|
+
// Inline array: [a, b, c]
|
|
60
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
61
|
+
frontmatter[currentKey] = value.slice(1, -1)
|
|
62
|
+
.split(',')
|
|
63
|
+
.map(s => {
|
|
64
|
+
s = s.trim();
|
|
65
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
66
|
+
s = s.slice(1, -1);
|
|
67
|
+
}
|
|
68
|
+
return s;
|
|
69
|
+
})
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Scalar value
|
|
75
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
76
|
+
value = value.slice(1, -1);
|
|
77
|
+
}
|
|
78
|
+
frontmatter[currentKey] = value;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { frontmatter, body };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function stripMarkdown(text) {
|
|
86
|
+
return text
|
|
87
|
+
.replace(/```[\s\S]*?```/g, '')
|
|
88
|
+
.replace(/`[^`]+`/g, '')
|
|
89
|
+
.replace(/^#{1,6}\s+/gm, '')
|
|
90
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
91
|
+
.replace(/\*([^*]+)\*/g, '$1')
|
|
92
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
93
|
+
.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_, t, d) => d || t)
|
|
94
|
+
.replace(/^\s*[-*+]\s+/gm, '')
|
|
95
|
+
.replace(/^\s*\d+\.\s+/gm, '')
|
|
96
|
+
.replace(/^\s*>\s+/gm, '')
|
|
97
|
+
.replace(/\|[^|\n]+/g, '')
|
|
98
|
+
.replace(/[-=]{3,}/g, '')
|
|
99
|
+
.replace(/\n{2,}/g, ' ')
|
|
100
|
+
.replace(/\s+/g, ' ')
|
|
101
|
+
.trim();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function scanWikiDir(wikiDir) {
|
|
105
|
+
const pages = [];
|
|
106
|
+
const titleToSlug = {};
|
|
107
|
+
|
|
108
|
+
// Try to load .index.json for aliases
|
|
109
|
+
let indexData = null;
|
|
110
|
+
const indexPath = path.join(wikiDir, '.index.json');
|
|
111
|
+
if (fs.existsSync(indexPath)) {
|
|
112
|
+
try {
|
|
113
|
+
indexData = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
|
|
114
|
+
} catch (e) {
|
|
115
|
+
// Ignore parse errors
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Scan directory recursively
|
|
120
|
+
function scan(dir, prefix) {
|
|
121
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
if (entry.name.startsWith('.')) continue;
|
|
124
|
+
const fullPath = path.join(dir, entry.name);
|
|
125
|
+
if (entry.isDirectory()) {
|
|
126
|
+
scan(fullPath, prefix + entry.name + '/');
|
|
127
|
+
} else if (entry.name.endsWith('.md')) {
|
|
128
|
+
const slug = prefix + entry.name.replace(/\.md$/, '');
|
|
129
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
130
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
131
|
+
const title = frontmatter.title || slug;
|
|
132
|
+
const type = frontmatter.type || 'page';
|
|
133
|
+
const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags : [];
|
|
134
|
+
const related = Array.isArray(frontmatter.related) ? frontmatter.related : [];
|
|
135
|
+
const excerpt = stripMarkdown(body).slice(0, 300);
|
|
136
|
+
|
|
137
|
+
pages.push({ slug, title, type, tags, related, excerpt });
|
|
138
|
+
|
|
139
|
+
// Build title-to-slug map
|
|
140
|
+
titleToSlug[title.toLowerCase()] = slug;
|
|
141
|
+
titleToSlug[slug.toLowerCase()] = slug;
|
|
142
|
+
|
|
143
|
+
// Add aliases from index
|
|
144
|
+
if (indexData && indexData.pages && indexData.pages[slug]) {
|
|
145
|
+
const aliases = indexData.pages[slug].aliases || [];
|
|
146
|
+
for (const alias of aliases) {
|
|
147
|
+
titleToSlug[alias.toLowerCase()] = slug;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
scan(wikiDir, '');
|
|
155
|
+
|
|
156
|
+
// Sort pages: overview first, then alphabetically by title
|
|
157
|
+
pages.sort((a, b) => {
|
|
158
|
+
if (a.type === 'overview') return -1;
|
|
159
|
+
if (b.type === 'overview') return 1;
|
|
160
|
+
return a.title.localeCompare(b.title);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Build search index
|
|
164
|
+
const searchIndex = pages.map(p => ({
|
|
165
|
+
s: p.slug,
|
|
166
|
+
t: p.title,
|
|
167
|
+
y: p.type,
|
|
168
|
+
x: p.excerpt,
|
|
169
|
+
}));
|
|
170
|
+
|
|
171
|
+
// Determine wiki title (from overview page or fallback)
|
|
172
|
+
const overviewPage = pages.find(p => p.type === 'overview');
|
|
173
|
+
const wikiTitle = overviewPage ? overviewPage.title : 'Wiki';
|
|
174
|
+
|
|
175
|
+
return { pages, titleToSlug, searchIndex, wikiTitle };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = { scanWikiDir, parseFrontmatter };
|
package/lib/server.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { scanWikiDir, parseFrontmatter } = require('./scanner');
|
|
7
|
+
const { createRenderer } = require('./renderer');
|
|
8
|
+
const { renderPage, render404 } = require('./template');
|
|
9
|
+
|
|
10
|
+
function startServer(wikiDir, port, autoOpen) {
|
|
11
|
+
let scanResult = scanWikiDir(wikiDir);
|
|
12
|
+
let renderer = createRenderer(scanResult.titleToSlug);
|
|
13
|
+
const pageCache = new Map();
|
|
14
|
+
|
|
15
|
+
// Watch for file changes
|
|
16
|
+
try {
|
|
17
|
+
fs.watch(wikiDir, { recursive: true }, (eventType, filename) => {
|
|
18
|
+
if (filename && filename.endsWith('.md')) {
|
|
19
|
+
pageCache.clear();
|
|
20
|
+
scanResult = scanWikiDir(wikiDir);
|
|
21
|
+
renderer = createRenderer(scanResult.titleToSlug);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
} catch (e) {
|
|
25
|
+
// fs.watch may not be available on all platforms
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildPage(slug) {
|
|
29
|
+
if (pageCache.has(slug)) return pageCache.get(slug);
|
|
30
|
+
|
|
31
|
+
const filePath = path.join(wikiDir, slug + '.md');
|
|
32
|
+
if (!fs.existsSync(filePath)) return null;
|
|
33
|
+
|
|
34
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
35
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
36
|
+
const htmlBody = renderer.render(body);
|
|
37
|
+
const title = frontmatter.title || slug;
|
|
38
|
+
const type = frontmatter.type || 'page';
|
|
39
|
+
const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags : [];
|
|
40
|
+
const related = Array.isArray(frontmatter.related) ? frontmatter.related : [];
|
|
41
|
+
|
|
42
|
+
const html = renderPage({
|
|
43
|
+
title,
|
|
44
|
+
type,
|
|
45
|
+
tags,
|
|
46
|
+
related,
|
|
47
|
+
htmlBody,
|
|
48
|
+
currentSlug: slug,
|
|
49
|
+
pages: scanResult.pages,
|
|
50
|
+
searchIndex: scanResult.searchIndex,
|
|
51
|
+
wikiTitle: scanResult.wikiTitle,
|
|
52
|
+
titleToSlug: scanResult.titleToSlug,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
pageCache.set(slug, html);
|
|
56
|
+
return html;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const server = http.createServer((req, res) => {
|
|
60
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
61
|
+
let pathname = url.pathname.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
62
|
+
|
|
63
|
+
// Default to overview
|
|
64
|
+
if (!pathname) pathname = 'overview';
|
|
65
|
+
|
|
66
|
+
// Ignore favicon requests
|
|
67
|
+
if (pathname === 'favicon.ico') {
|
|
68
|
+
res.writeHead(204);
|
|
69
|
+
res.end();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Serve static files (llms.txt, llms-full.txt, etc.)
|
|
74
|
+
const staticPath = path.join(wikiDir, pathname);
|
|
75
|
+
if (fs.existsSync(staticPath) && fs.statSync(staticPath).isFile() && !pathname.endsWith('.md')) {
|
|
76
|
+
const ext = path.extname(pathname);
|
|
77
|
+
const types = { '.txt': 'text/plain', '.json': 'application/json', '.xml': 'text/xml' };
|
|
78
|
+
res.writeHead(200, { 'Content-Type': (types[ext] || 'text/plain') + '; charset=utf-8' });
|
|
79
|
+
res.end(fs.readFileSync(staticPath, 'utf-8'));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Render wiki page
|
|
84
|
+
const html = buildPage(pathname);
|
|
85
|
+
if (html) {
|
|
86
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
87
|
+
res.end(html);
|
|
88
|
+
} else {
|
|
89
|
+
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
90
|
+
res.end(render404(scanResult.wikiTitle, scanResult.pages, scanResult.searchIndex, scanResult.titleToSlug));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
server.listen(port, () => {
|
|
95
|
+
console.log('');
|
|
96
|
+
console.log(` Wiki server running at http://localhost:${port}`);
|
|
97
|
+
console.log(` Wiki directory: ${wikiDir}`);
|
|
98
|
+
console.log(` Pages: ${scanResult.pages.length}`);
|
|
99
|
+
console.log('');
|
|
100
|
+
console.log(' Press Ctrl+C to stop');
|
|
101
|
+
console.log('');
|
|
102
|
+
|
|
103
|
+
if (autoOpen) {
|
|
104
|
+
const cmd = process.platform === 'darwin' ? 'open'
|
|
105
|
+
: process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
106
|
+
require('child_process').exec(`${cmd} http://localhost:${port}`);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function exportSite(wikiDir, outputDir) {
|
|
112
|
+
const scanResult = scanWikiDir(wikiDir);
|
|
113
|
+
const renderer = createRenderer(scanResult.titleToSlug);
|
|
114
|
+
|
|
115
|
+
if (!fs.existsSync(outputDir)) {
|
|
116
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log(`Exporting wiki to ${outputDir}...`);
|
|
120
|
+
|
|
121
|
+
for (const page of scanResult.pages) {
|
|
122
|
+
const filePath = path.join(wikiDir, page.slug + '.md');
|
|
123
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
124
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
125
|
+
const htmlBody = renderer.render(body);
|
|
126
|
+
|
|
127
|
+
const html = renderPage({
|
|
128
|
+
title: page.title,
|
|
129
|
+
type: page.type,
|
|
130
|
+
tags: page.tags,
|
|
131
|
+
related: page.related,
|
|
132
|
+
htmlBody,
|
|
133
|
+
currentSlug: page.slug,
|
|
134
|
+
pages: scanResult.pages,
|
|
135
|
+
searchIndex: scanResult.searchIndex,
|
|
136
|
+
wikiTitle: scanResult.wikiTitle,
|
|
137
|
+
titleToSlug: scanResult.titleToSlug,
|
|
138
|
+
exportMode: true,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const outPath = path.join(outputDir, page.slug + '.html');
|
|
142
|
+
const outDir = path.dirname(outPath);
|
|
143
|
+
if (!fs.existsSync(outDir)) {
|
|
144
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fs.writeFileSync(outPath, html);
|
|
148
|
+
console.log(` ${page.slug}.html`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Copy static files
|
|
152
|
+
const staticFiles = ['llms.txt', 'llms-full.txt'];
|
|
153
|
+
for (const file of staticFiles) {
|
|
154
|
+
const src = path.join(wikiDir, file);
|
|
155
|
+
if (fs.existsSync(src)) {
|
|
156
|
+
fs.copyFileSync(src, path.join(outputDir, file));
|
|
157
|
+
console.log(` ${file}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Create index.html redirect
|
|
162
|
+
fs.writeFileSync(
|
|
163
|
+
path.join(outputDir, 'index.html'),
|
|
164
|
+
'<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0;url=overview.html"></head></html>\n'
|
|
165
|
+
);
|
|
166
|
+
console.log(' index.html');
|
|
167
|
+
console.log('');
|
|
168
|
+
console.log('Export complete!');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { startServer, exportSite };
|