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/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 };