@knowcode/doc-builder 1.0.0 ā 1.0.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/CHANGELOG.md +60 -0
- package/lib/builder.js +10 -57
- package/lib/core-builder.js +399 -0
- package/package.json +2 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to @knowcode/doc-builder will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.1] - 2025-01-19
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Made package completely self-contained - no longer requires cybersolstice project
|
|
12
|
+
- Embedded full build logic into the package
|
|
13
|
+
- Fixed "Build script not found" error when running in other projects
|
|
14
|
+
|
|
15
|
+
## [1.0.0] - 2025-01-19
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Initial release of @knowcode/doc-builder
|
|
19
|
+
- Zero-configuration documentation builder
|
|
20
|
+
- Markdown to HTML conversion with Notion-inspired theme
|
|
21
|
+
- Automatic navigation generation from folder structure
|
|
22
|
+
- Mermaid diagram support with title extraction
|
|
23
|
+
- Syntax highlighting for code blocks
|
|
24
|
+
- Dark mode support
|
|
25
|
+
- Optional authentication system
|
|
26
|
+
- Automatic changelog generation
|
|
27
|
+
- Live reload development server
|
|
28
|
+
- One-command Vercel deployment
|
|
29
|
+
- CLI with build, dev, deploy, and init commands
|
|
30
|
+
- Example documentation generator
|
|
31
|
+
- Cybersolstice preset for backward compatibility
|
|
32
|
+
- Programmatic API for Node.js integration
|
|
33
|
+
|
|
34
|
+
### Features
|
|
35
|
+
- Works immediately with `npx @knowcode/doc-builder`
|
|
36
|
+
- No installation or configuration required
|
|
37
|
+
- Self-contained package with all dependencies
|
|
38
|
+
- Intelligent defaults for common use cases
|
|
39
|
+
- Full customization available via config file
|
|
40
|
+
|
|
41
|
+
### Technical
|
|
42
|
+
- Built on marked for markdown parsing
|
|
43
|
+
- Commander.js for CLI framework
|
|
44
|
+
- Supports Node.js 14+
|
|
45
|
+
- CommonJS module system for compatibility
|
|
46
|
+
|
|
47
|
+
## Future Releases
|
|
48
|
+
|
|
49
|
+
### [1.1.0] - Planned
|
|
50
|
+
- Plugin system for extensibility
|
|
51
|
+
- Custom theme support
|
|
52
|
+
- Multiple language support
|
|
53
|
+
- Search functionality
|
|
54
|
+
- PDF export
|
|
55
|
+
|
|
56
|
+
### [2.0.0] - Planned
|
|
57
|
+
- Full refactor of build system
|
|
58
|
+
- ESM modules support
|
|
59
|
+
- TypeScript rewrite
|
|
60
|
+
- Performance improvements
|
package/lib/builder.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
const fs = require('fs-extra');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const chalk = require('chalk');
|
|
4
|
+
const { buildDocumentation } = require('./core-builder');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Main build function
|
|
7
|
-
*
|
|
8
|
+
* Now fully self-contained!
|
|
8
9
|
*/
|
|
9
10
|
async function build(config) {
|
|
10
11
|
console.log(chalk.blue(`\nš Building ${config.siteName}...\n`));
|
|
@@ -14,65 +15,17 @@ async function build(config) {
|
|
|
14
15
|
throw new Error('Invalid configuration provided to build function');
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// Set environment variables for the build script
|
|
26
|
-
process.env.DOC_BUILDER_CONFIG = JSON.stringify(config);
|
|
27
|
-
|
|
28
|
-
// Import and run the existing build
|
|
29
|
-
const buildModule = require(buildScriptPath);
|
|
30
|
-
|
|
31
|
-
// Run the build - it doesn't take parameters, config is passed via env
|
|
32
|
-
if (typeof buildModule.build === 'function') {
|
|
33
|
-
await buildModule.build();
|
|
34
|
-
} else {
|
|
35
|
-
// If it's not a function, it might be auto-executing
|
|
36
|
-
// Just requiring it should run it
|
|
18
|
+
try {
|
|
19
|
+
// Use the self-contained builder
|
|
20
|
+
await buildDocumentation(config);
|
|
21
|
+
console.log(chalk.green('\n⨠Build complete!\n'));
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error(chalk.red('\nā Build failed:'), error.message);
|
|
24
|
+
throw error;
|
|
37
25
|
}
|
|
38
|
-
|
|
39
|
-
// Copy assets to output directory
|
|
40
|
-
await copyAssets(config);
|
|
41
|
-
|
|
42
|
-
console.log(chalk.green('\n⨠Build complete!\n'));
|
|
43
26
|
}
|
|
44
27
|
|
|
45
|
-
|
|
46
|
-
* Copy package assets to output directory
|
|
47
|
-
*/
|
|
48
|
-
async function copyAssets(config) {
|
|
49
|
-
const outputDir = path.join(process.cwd(), config.outputDir);
|
|
50
|
-
const assetsDir = path.join(__dirname, '../assets');
|
|
51
|
-
|
|
52
|
-
// Ensure output directory exists
|
|
53
|
-
await fs.ensureDir(outputDir);
|
|
54
|
-
|
|
55
|
-
// Copy CSS
|
|
56
|
-
const cssSource = path.join(assetsDir, 'css');
|
|
57
|
-
const cssDest = path.join(outputDir, 'css');
|
|
58
|
-
if (fs.existsSync(cssSource)) {
|
|
59
|
-
await fs.copy(cssSource, cssDest, { overwrite: true });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Copy JS
|
|
63
|
-
const jsSource = path.join(assetsDir, 'js');
|
|
64
|
-
const jsDest = path.join(outputDir, 'js');
|
|
65
|
-
if (fs.existsSync(jsSource)) {
|
|
66
|
-
await fs.copy(jsSource, jsDest, { overwrite: true });
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Copy auth.js to root
|
|
70
|
-
const authSource = path.join(assetsDir, 'js', 'auth.js');
|
|
71
|
-
const authDest = path.join(outputDir, 'auth.js');
|
|
72
|
-
if (fs.existsSync(authSource)) {
|
|
73
|
-
await fs.copy(authSource, authDest, { overwrite: true });
|
|
74
|
-
}
|
|
75
|
-
}
|
|
28
|
+
// Asset copying is now handled in core-builder.js
|
|
76
29
|
|
|
77
30
|
module.exports = {
|
|
78
31
|
build
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const marked = require('marked');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
|
|
6
|
+
// Configure marked options
|
|
7
|
+
marked.setOptions({
|
|
8
|
+
highlight: function(code, lang) {
|
|
9
|
+
return `<code class="language-${lang}">${escapeHtml(code)}</code>`;
|
|
10
|
+
},
|
|
11
|
+
breaks: true,
|
|
12
|
+
gfm: true
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Helper function to escape HTML
|
|
16
|
+
function escapeHtml(text) {
|
|
17
|
+
const map = {
|
|
18
|
+
'&': '&',
|
|
19
|
+
'<': '<',
|
|
20
|
+
'>': '>',
|
|
21
|
+
'"': '"',
|
|
22
|
+
"'": '''
|
|
23
|
+
};
|
|
24
|
+
return text.replace(/[&<>"']/g, m => map[m]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Process markdown content
|
|
28
|
+
function processMarkdownContent(content) {
|
|
29
|
+
// Convert mermaid code blocks to mermaid divs with titles
|
|
30
|
+
content = content.replace(/```mermaid\n([\s\S]*?)```/g, (match, mermaidContent) => {
|
|
31
|
+
// Try to extract title from mermaid content
|
|
32
|
+
let title = 'Diagram';
|
|
33
|
+
|
|
34
|
+
// Look for title in various mermaid formats
|
|
35
|
+
const titlePatterns = [
|
|
36
|
+
/title\s+([^\n]+)/i, // gantt charts: title My Title
|
|
37
|
+
/graph\s+\w+\[["']([^"']+)["']\]/, // graph TD["My Title"]
|
|
38
|
+
/flowchart\s+\w+\[["']([^"']+)["']\]/, // flowchart TD["My Title"]
|
|
39
|
+
/---\s*title:\s*([^\n]+)\s*---/, // frontmatter style
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
for (const pattern of titlePatterns) {
|
|
43
|
+
const match = mermaidContent.match(pattern);
|
|
44
|
+
if (match) {
|
|
45
|
+
title = match[1].trim();
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return `<div class="mermaid-wrapper">
|
|
51
|
+
<div class="mermaid-title">${escapeHtml(title)}</div>
|
|
52
|
+
<div class="mermaid">${escapeHtml(mermaidContent)}</div>
|
|
53
|
+
</div>`;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return marked.parse(content);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Generate HTML from template
|
|
60
|
+
function generateHTML(title, content, navigation, currentPath = '', config = {}) {
|
|
61
|
+
const depth = currentPath.split('/').filter(p => p).length;
|
|
62
|
+
const relativePath = depth > 0 ? '../'.repeat(depth) : './';
|
|
63
|
+
|
|
64
|
+
const siteName = config.siteName || 'Documentation';
|
|
65
|
+
const siteDescription = config.siteDescription || 'Documentation site';
|
|
66
|
+
|
|
67
|
+
return `<!DOCTYPE html>
|
|
68
|
+
<html lang="en">
|
|
69
|
+
<head>
|
|
70
|
+
<meta charset="UTF-8">
|
|
71
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
72
|
+
<meta name="description" content="${siteDescription}">
|
|
73
|
+
<title>${title} - ${siteName}</title>
|
|
74
|
+
|
|
75
|
+
<!-- Fonts -->
|
|
76
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
77
|
+
|
|
78
|
+
<!-- Icons -->
|
|
79
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
|
80
|
+
|
|
81
|
+
<!-- Mermaid -->
|
|
82
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
|
|
83
|
+
|
|
84
|
+
<!-- Styles -->
|
|
85
|
+
<link rel="stylesheet" href="${relativePath}css/notion-style.css">
|
|
86
|
+
|
|
87
|
+
<!-- Favicon -->
|
|
88
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>š</text></svg>">
|
|
89
|
+
</head>
|
|
90
|
+
<body>
|
|
91
|
+
<!-- Header -->
|
|
92
|
+
<header class="header">
|
|
93
|
+
<div class="header-content">
|
|
94
|
+
<a href="${relativePath}index.html" class="logo">${siteName}</a>
|
|
95
|
+
|
|
96
|
+
<div class="header-actions">
|
|
97
|
+
<div class="deployment-info">
|
|
98
|
+
<span class="deployment-date">Last updated: ${new Date().toLocaleDateString('en-US', {
|
|
99
|
+
year: 'numeric',
|
|
100
|
+
month: 'short',
|
|
101
|
+
day: 'numeric',
|
|
102
|
+
hour: '2-digit',
|
|
103
|
+
minute: '2-digit',
|
|
104
|
+
timeZone: 'UTC'
|
|
105
|
+
})} UTC</span>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
${config.features?.authentication ? `
|
|
109
|
+
<a href="${relativePath}logout.html" class="logout-btn" title="Logout">
|
|
110
|
+
<i class="fas fa-sign-out-alt"></i>
|
|
111
|
+
</a>
|
|
112
|
+
` : ''}
|
|
113
|
+
|
|
114
|
+
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
|
|
115
|
+
<i class="fas fa-moon"></i>
|
|
116
|
+
</button>
|
|
117
|
+
|
|
118
|
+
<button id="menu-toggle" class="menu-toggle" aria-label="Toggle menu">
|
|
119
|
+
<i class="fas fa-bars"></i>
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</header>
|
|
124
|
+
|
|
125
|
+
<div class="container">
|
|
126
|
+
<!-- Navigation -->
|
|
127
|
+
<nav id="navigation" class="navigation">
|
|
128
|
+
<div class="nav-header">
|
|
129
|
+
<h3>${siteName}</h3>
|
|
130
|
+
</div>
|
|
131
|
+
${navigation}
|
|
132
|
+
</nav>
|
|
133
|
+
|
|
134
|
+
<!-- Main Content -->
|
|
135
|
+
<main class="content">
|
|
136
|
+
${content}
|
|
137
|
+
</main>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<!-- Scripts -->
|
|
141
|
+
<script src="${relativePath}js/main.js"></script>
|
|
142
|
+
${config.features?.authentication ? `<script src="${relativePath}js/auth.js"></script>` : ''}
|
|
143
|
+
</body>
|
|
144
|
+
</html>`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Build navigation structure
|
|
148
|
+
function buildNavigationStructure(files, currentFile) {
|
|
149
|
+
const tree = { files: [], folders: {} };
|
|
150
|
+
|
|
151
|
+
files.forEach(file => {
|
|
152
|
+
const parts = file.urlPath.split('/');
|
|
153
|
+
let current = tree;
|
|
154
|
+
|
|
155
|
+
// Navigate/create folder structure
|
|
156
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
157
|
+
const folder = parts[i];
|
|
158
|
+
if (!current.folders[folder]) {
|
|
159
|
+
current.folders[folder] = { files: [], folders: {} };
|
|
160
|
+
}
|
|
161
|
+
current = current.folders[folder];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Add file to current folder
|
|
165
|
+
current.files.push(file);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Generate HTML
|
|
169
|
+
const generateNavHTML = (node, path = '') => {
|
|
170
|
+
let html = '';
|
|
171
|
+
|
|
172
|
+
// Add files
|
|
173
|
+
node.files.forEach(file => {
|
|
174
|
+
const isActive = file.urlPath === currentFile;
|
|
175
|
+
const href = file.urlPath.replace(/\\/g, '/');
|
|
176
|
+
html += `<li class="${isActive ? 'active' : ''}">
|
|
177
|
+
<a href="${href}" class="nav-link ${isActive ? 'active' : ''}">
|
|
178
|
+
${file.displayName}
|
|
179
|
+
</a>
|
|
180
|
+
</li>`;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Add folders
|
|
184
|
+
Object.entries(node.folders).forEach(([folderName, folderNode]) => {
|
|
185
|
+
const folderPath = path ? `${path}/${folderName}` : folderName;
|
|
186
|
+
const hasActiveChild = checkActiveChild(folderNode, currentFile);
|
|
187
|
+
|
|
188
|
+
html += `<li class="nav-folder ${hasActiveChild ? 'active' : ''}">
|
|
189
|
+
<button class="nav-folder-toggle ${hasActiveChild ? 'active' : ''}" data-folder="${folderPath}">
|
|
190
|
+
<i class="fas fa-chevron-${hasActiveChild ? 'down' : 'right'}"></i>
|
|
191
|
+
${folderName}
|
|
192
|
+
</button>
|
|
193
|
+
<ul class="nav-folder-content ${hasActiveChild ? 'active' : ''}">
|
|
194
|
+
${generateNavHTML(folderNode, folderPath)}
|
|
195
|
+
</ul>
|
|
196
|
+
</li>`;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return html;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const checkActiveChild = (node, currentFile) => {
|
|
203
|
+
// Check files
|
|
204
|
+
if (node.files.some(f => f.urlPath === currentFile)) return true;
|
|
205
|
+
|
|
206
|
+
// Check folders recursively
|
|
207
|
+
return Object.values(node.folders).some(folder => checkActiveChild(folder, currentFile));
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
return `<ul class="nav-list">${generateNavHTML(tree)}</ul>`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Process single markdown file
|
|
214
|
+
async function processMarkdownFile(filePath, outputPath, allFiles, config) {
|
|
215
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
216
|
+
const fileName = path.basename(filePath, '.md');
|
|
217
|
+
const relativePath = path.relative(config.docsDir, filePath);
|
|
218
|
+
const urlPath = relativePath.replace(/\.md$/, '.html').replace(/\\/g, '/');
|
|
219
|
+
|
|
220
|
+
// Extract title from content
|
|
221
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
222
|
+
const title = titleMatch ? titleMatch[1] : fileName;
|
|
223
|
+
|
|
224
|
+
// Process content
|
|
225
|
+
const htmlContent = processMarkdownContent(content);
|
|
226
|
+
|
|
227
|
+
// Build navigation
|
|
228
|
+
const navigation = buildNavigationStructure(allFiles, urlPath);
|
|
229
|
+
|
|
230
|
+
// Generate full HTML
|
|
231
|
+
const html = generateHTML(title, htmlContent, navigation, urlPath, config);
|
|
232
|
+
|
|
233
|
+
// Write file
|
|
234
|
+
await fs.ensureDir(path.dirname(outputPath));
|
|
235
|
+
await fs.writeFile(outputPath, html);
|
|
236
|
+
|
|
237
|
+
return { title, urlPath };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Get all markdown files
|
|
241
|
+
async function getAllMarkdownFiles(dir, baseDir = dir) {
|
|
242
|
+
const files = [];
|
|
243
|
+
const items = await fs.readdir(dir);
|
|
244
|
+
|
|
245
|
+
for (const item of items) {
|
|
246
|
+
const fullPath = path.join(dir, item);
|
|
247
|
+
const stat = await fs.stat(fullPath);
|
|
248
|
+
|
|
249
|
+
if (stat.isDirectory() && !item.startsWith('.')) {
|
|
250
|
+
const subFiles = await getAllMarkdownFiles(fullPath, baseDir);
|
|
251
|
+
files.push(...subFiles);
|
|
252
|
+
} else if (item.endsWith('.md')) {
|
|
253
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
254
|
+
const urlPath = relativePath.replace(/\.md$/, '.html').replace(/\\/g, '/');
|
|
255
|
+
const displayName = path.basename(item, '.md')
|
|
256
|
+
.replace(/[-_]/g, ' ')
|
|
257
|
+
.replace(/\b\w/g, l => l.toUpperCase());
|
|
258
|
+
|
|
259
|
+
files.push({
|
|
260
|
+
path: fullPath,
|
|
261
|
+
relativePath,
|
|
262
|
+
urlPath,
|
|
263
|
+
displayName
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return files;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Main build function
|
|
272
|
+
async function buildDocumentation(config) {
|
|
273
|
+
const docsDir = path.join(process.cwd(), config.docsDir);
|
|
274
|
+
const outputDir = path.join(process.cwd(), config.outputDir);
|
|
275
|
+
|
|
276
|
+
console.log(chalk.blue('š Scanning for markdown files...'));
|
|
277
|
+
const files = await getAllMarkdownFiles(docsDir);
|
|
278
|
+
console.log(chalk.green(`ā
Found ${files.length} markdown files`));
|
|
279
|
+
|
|
280
|
+
console.log(chalk.blue('š Processing files...'));
|
|
281
|
+
for (const file of files) {
|
|
282
|
+
const outputPath = path.join(outputDir, file.urlPath);
|
|
283
|
+
await processMarkdownFile(file.path, outputPath, files, config);
|
|
284
|
+
console.log(chalk.green(`ā
Generated: ${outputPath}`));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Copy assets
|
|
288
|
+
const assetsDir = path.join(__dirname, '../assets');
|
|
289
|
+
const cssSource = path.join(assetsDir, 'css');
|
|
290
|
+
const jsSource = path.join(assetsDir, 'js');
|
|
291
|
+
|
|
292
|
+
if (fs.existsSync(cssSource)) {
|
|
293
|
+
await fs.copy(cssSource, path.join(outputDir, 'css'), { overwrite: true });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (fs.existsSync(jsSource)) {
|
|
297
|
+
await fs.copy(jsSource, path.join(outputDir, 'js'), { overwrite: true });
|
|
298
|
+
|
|
299
|
+
// Copy auth.js to root if authentication is enabled
|
|
300
|
+
if (config.features?.authentication) {
|
|
301
|
+
const authSource = path.join(jsSource, 'auth.js');
|
|
302
|
+
const authDest = path.join(outputDir, 'auth.js');
|
|
303
|
+
if (fs.existsSync(authSource)) {
|
|
304
|
+
await fs.copy(authSource, authDest, { overwrite: true });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Create auth pages if needed
|
|
310
|
+
if (config.features?.authentication) {
|
|
311
|
+
await createAuthPages(outputDir, config);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
console.log(chalk.green('ā
Documentation build complete!'));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Create login/logout pages
|
|
318
|
+
async function createAuthPages(outputDir, config) {
|
|
319
|
+
// Login page
|
|
320
|
+
const loginHTML = `<!DOCTYPE html>
|
|
321
|
+
<html lang="en">
|
|
322
|
+
<head>
|
|
323
|
+
<meta charset="UTF-8">
|
|
324
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
325
|
+
<title>Login - ${config.siteName}</title>
|
|
326
|
+
<link rel="stylesheet" href="css/notion-style.css">
|
|
327
|
+
</head>
|
|
328
|
+
<body>
|
|
329
|
+
<div class="auth-container">
|
|
330
|
+
<div class="auth-box">
|
|
331
|
+
<h1>Login to ${config.siteName}</h1>
|
|
332
|
+
<form id="login-form">
|
|
333
|
+
<div class="form-group">
|
|
334
|
+
<label for="username">Username</label>
|
|
335
|
+
<input type="text" id="username" name="username" required>
|
|
336
|
+
</div>
|
|
337
|
+
<div class="form-group">
|
|
338
|
+
<label for="password">Password</label>
|
|
339
|
+
<input type="password" id="password" name="password" required>
|
|
340
|
+
</div>
|
|
341
|
+
<button type="submit" class="auth-button">Login</button>
|
|
342
|
+
</form>
|
|
343
|
+
<div id="error-message" class="error-message"></div>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
<script>
|
|
347
|
+
document.getElementById('login-form').addEventListener('submit', function(e) {
|
|
348
|
+
e.preventDefault();
|
|
349
|
+
const username = document.getElementById('username').value;
|
|
350
|
+
const password = document.getElementById('password').value;
|
|
351
|
+
|
|
352
|
+
// Validate credentials
|
|
353
|
+
if (username === '${config.auth.username}' && password === '${config.auth.password}') {
|
|
354
|
+
// Set auth cookie
|
|
355
|
+
const token = btoa(username + ':' + password);
|
|
356
|
+
document.cookie = 'juno-auth=' + token + '; path=/';
|
|
357
|
+
|
|
358
|
+
// Redirect
|
|
359
|
+
const params = new URLSearchParams(window.location.search);
|
|
360
|
+
const redirect = params.get('redirect') || '/';
|
|
361
|
+
window.location.href = redirect;
|
|
362
|
+
} else {
|
|
363
|
+
document.getElementById('error-message').textContent = 'Invalid username or password';
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
</script>
|
|
367
|
+
</body>
|
|
368
|
+
</html>`;
|
|
369
|
+
|
|
370
|
+
await fs.writeFile(path.join(outputDir, 'login.html'), loginHTML);
|
|
371
|
+
|
|
372
|
+
// Logout page
|
|
373
|
+
const logoutHTML = `<!DOCTYPE html>
|
|
374
|
+
<html lang="en">
|
|
375
|
+
<head>
|
|
376
|
+
<meta charset="UTF-8">
|
|
377
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
378
|
+
<title>Logged Out - ${config.siteName}</title>
|
|
379
|
+
<link rel="stylesheet" href="css/notion-style.css">
|
|
380
|
+
</head>
|
|
381
|
+
<body>
|
|
382
|
+
<div class="auth-container">
|
|
383
|
+
<div class="auth-box">
|
|
384
|
+
<h1>You have been logged out</h1>
|
|
385
|
+
<p>Thank you for using ${config.siteName}.</p>
|
|
386
|
+
<a href="login.html" class="auth-button">Login Again</a>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
</body>
|
|
390
|
+
</html>`;
|
|
391
|
+
|
|
392
|
+
await fs.writeFile(path.join(outputDir, 'logout.html'), logoutHTML);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
module.exports = {
|
|
396
|
+
buildDocumentation,
|
|
397
|
+
processMarkdownContent,
|
|
398
|
+
generateHTML
|
|
399
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knowcode/doc-builder",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Reusable documentation builder for markdown-based sites with Vercel deployment support",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -32,4 +32,4 @@
|
|
|
32
32
|
"engines": {
|
|
33
33
|
"node": ">=14.0.0"
|
|
34
34
|
}
|
|
35
|
-
}
|
|
35
|
+
}
|