@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 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
- * This wraps the existing build.js functionality
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
- // For now, we'll use the existing build.js file
18
- // In a future refactor, we'll move all the logic here
19
- const buildScriptPath = path.join(__dirname, '../../../html/build.js');
20
-
21
- if (!fs.existsSync(buildScriptPath)) {
22
- throw new Error('Build script not found. This package must be run from the cybersolstice project.');
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
+ '&': '&amp;',
19
+ '<': '&lt;',
20
+ '>': '&gt;',
21
+ '"': '&quot;',
22
+ "'": '&#039;'
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.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
+ }