@knowcode/doc-builder 1.0.0 → 1.0.2
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 +75 -0
- package/cli.js +64 -10
- 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,75 @@
|
|
|
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.2] - 2025-01-19
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Fixed remaining JUNO references in CLI help text
|
|
12
|
+
- Enhanced help documentation with detailed Vercel CLI setup instructions
|
|
13
|
+
- Added comprehensive deployment troubleshooting guide
|
|
14
|
+
- Improved Quick Start section with step-by-step instructions
|
|
15
|
+
- Added "What gets created" explanations for init command
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Requirements section in main help
|
|
19
|
+
- Example for creating docs from scratch
|
|
20
|
+
- Vercel installation options (npm and Homebrew)
|
|
21
|
+
- Clear instructions for disabling Vercel deployment protection
|
|
22
|
+
|
|
23
|
+
## [1.0.1] - 2025-01-19
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- Made package completely self-contained - no longer requires cybersolstice project
|
|
27
|
+
- Embedded full build logic into the package
|
|
28
|
+
- Fixed "Build script not found" error when running in other projects
|
|
29
|
+
|
|
30
|
+
## [1.0.0] - 2025-01-19
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- Initial release of @knowcode/doc-builder
|
|
34
|
+
- Zero-configuration documentation builder
|
|
35
|
+
- Markdown to HTML conversion with Notion-inspired theme
|
|
36
|
+
- Automatic navigation generation from folder structure
|
|
37
|
+
- Mermaid diagram support with title extraction
|
|
38
|
+
- Syntax highlighting for code blocks
|
|
39
|
+
- Dark mode support
|
|
40
|
+
- Optional authentication system
|
|
41
|
+
- Automatic changelog generation
|
|
42
|
+
- Live reload development server
|
|
43
|
+
- One-command Vercel deployment
|
|
44
|
+
- CLI with build, dev, deploy, and init commands
|
|
45
|
+
- Example documentation generator
|
|
46
|
+
- Cybersolstice preset for backward compatibility
|
|
47
|
+
- Programmatic API for Node.js integration
|
|
48
|
+
|
|
49
|
+
### Features
|
|
50
|
+
- Works immediately with `npx @knowcode/doc-builder`
|
|
51
|
+
- No installation or configuration required
|
|
52
|
+
- Self-contained package with all dependencies
|
|
53
|
+
- Intelligent defaults for common use cases
|
|
54
|
+
- Full customization available via config file
|
|
55
|
+
|
|
56
|
+
### Technical
|
|
57
|
+
- Built on marked for markdown parsing
|
|
58
|
+
- Commander.js for CLI framework
|
|
59
|
+
- Supports Node.js 14+
|
|
60
|
+
- CommonJS module system for compatibility
|
|
61
|
+
|
|
62
|
+
## Future Releases
|
|
63
|
+
|
|
64
|
+
### [1.1.0] - Planned
|
|
65
|
+
- Plugin system for extensibility
|
|
66
|
+
- Custom theme support
|
|
67
|
+
- Multiple language support
|
|
68
|
+
- Search functionality
|
|
69
|
+
- PDF export
|
|
70
|
+
|
|
71
|
+
### [2.0.0] - Planned
|
|
72
|
+
- Full refactor of build system
|
|
73
|
+
- ESM modules support
|
|
74
|
+
- TypeScript rewrite
|
|
75
|
+
- Performance improvements
|
package/cli.js
CHANGED
|
@@ -25,7 +25,7 @@ program
|
|
|
25
25
|
.description(packageJson.description)
|
|
26
26
|
.version(packageJson.version)
|
|
27
27
|
.addHelpText('before', `
|
|
28
|
-
${chalk.cyan('🚀 @
|
|
28
|
+
${chalk.cyan('🚀 @knowcode/doc-builder')} - Transform your markdown into beautiful documentation sites
|
|
29
29
|
|
|
30
30
|
${chalk.yellow('What it does:')}
|
|
31
31
|
• Converts markdown files to static HTML with a beautiful Notion-inspired theme
|
|
@@ -34,10 +34,24 @@ ${chalk.yellow('What it does:')}
|
|
|
34
34
|
• Deploys to Vercel with one command (zero configuration)
|
|
35
35
|
• Optional authentication to protect private documentation
|
|
36
36
|
|
|
37
|
+
${chalk.yellow('Requirements:')}
|
|
38
|
+
• Node.js 14+ installed
|
|
39
|
+
• A ${chalk.cyan('docs/')} folder with markdown files (or specify custom folder)
|
|
40
|
+
• Vercel CLI for deployment (optional)
|
|
41
|
+
|
|
37
42
|
${chalk.yellow('Quick Start:')}
|
|
38
|
-
${chalk.
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
${chalk.cyan('1. Create your docs:')}
|
|
44
|
+
${chalk.gray('$')} mkdir docs
|
|
45
|
+
${chalk.gray('$')} echo "# My Documentation" > docs/README.md
|
|
46
|
+
|
|
47
|
+
${chalk.cyan('2. Build and deploy:')}
|
|
48
|
+
${chalk.gray('$')} npx @knowcode/doc-builder ${chalk.gray('# Build and deploy to Vercel')}
|
|
49
|
+
${chalk.gray(' or')}
|
|
50
|
+
${chalk.gray('$')} npx @knowcode/doc-builder build ${chalk.gray('# Build HTML files only')}
|
|
51
|
+
${chalk.gray('$')} npx @knowcode/doc-builder dev ${chalk.gray('# Start development server')}
|
|
52
|
+
|
|
53
|
+
${chalk.yellow('No docs folder yet?')}
|
|
54
|
+
${chalk.gray('$')} npx @knowcode/doc-builder init --example ${chalk.gray('# Create example docs')}
|
|
41
55
|
`);
|
|
42
56
|
|
|
43
57
|
// Build command
|
|
@@ -113,13 +127,40 @@ ${chalk.yellow('Examples:')}
|
|
|
113
127
|
${chalk.gray('$')} doc-builder deploy --prod ${chalk.gray('# Deploy to production')}
|
|
114
128
|
${chalk.gray('$')} doc-builder deploy --no-build ${chalk.gray('# Deploy existing build')}
|
|
115
129
|
|
|
116
|
-
${chalk.yellow('First-time
|
|
117
|
-
|
|
118
|
-
1.
|
|
119
|
-
|
|
120
|
-
|
|
130
|
+
${chalk.yellow('First-time Vercel Setup:')}
|
|
131
|
+
|
|
132
|
+
${chalk.cyan('1. Install Vercel CLI:')}
|
|
133
|
+
${chalk.gray('$')} npm install -g vercel
|
|
134
|
+
${chalk.gray(' or')}
|
|
135
|
+
${chalk.gray('$')} brew install vercel ${chalk.gray('# macOS with Homebrew')}
|
|
136
|
+
|
|
137
|
+
${chalk.cyan('2. Login to Vercel:')}
|
|
138
|
+
${chalk.gray('$')} vercel login
|
|
139
|
+
${chalk.gray(' This will open your browser to authenticate')}
|
|
121
140
|
|
|
122
|
-
${chalk.
|
|
141
|
+
${chalk.cyan('3. Run doc-builder deploy:')}
|
|
142
|
+
${chalk.gray('$')} npx @knowcode/doc-builder deploy
|
|
143
|
+
|
|
144
|
+
The tool will:
|
|
145
|
+
• Create a new Vercel project
|
|
146
|
+
• Link it to your documentation
|
|
147
|
+
• Deploy your site
|
|
148
|
+
• Provide you with a URL
|
|
149
|
+
|
|
150
|
+
${chalk.cyan('4. Configure Access (Important!):')}
|
|
151
|
+
After deployment, go to your Vercel dashboard:
|
|
152
|
+
• Navigate to Project Settings → General
|
|
153
|
+
• Under "Deployment Protection", set to ${chalk.yellow('Disabled')}
|
|
154
|
+
• This allows public access to your docs
|
|
155
|
+
|
|
156
|
+
${chalk.yellow('Subsequent Deployments:')}
|
|
157
|
+
${chalk.gray('$')} npx @knowcode/doc-builder ${chalk.gray('# Deploy preview')}
|
|
158
|
+
${chalk.gray('$')} npx @knowcode/doc-builder deploy --prod ${chalk.gray('# Deploy to production')}
|
|
159
|
+
|
|
160
|
+
${chalk.yellow('Troubleshooting:')}
|
|
161
|
+
• ${chalk.cyan('Command not found:')} Install Vercel CLI globally
|
|
162
|
+
• ${chalk.cyan('Not authenticated:')} Run ${chalk.gray('vercel login')}
|
|
163
|
+
• ${chalk.cyan('Project not linked:')} Delete ${chalk.gray('.vercel')} folder and redeploy
|
|
123
164
|
`)
|
|
124
165
|
.action(async (options) => {
|
|
125
166
|
const spinner = ora('Deploying to Vercel...').start();
|
|
@@ -169,6 +210,19 @@ program
|
|
|
169
210
|
${chalk.yellow('Examples:')}
|
|
170
211
|
${chalk.gray('$')} doc-builder init --config ${chalk.gray('# Create doc-builder.config.js')}
|
|
171
212
|
${chalk.gray('$')} doc-builder init --example ${chalk.gray('# Create example docs folder')}
|
|
213
|
+
${chalk.gray('$')} doc-builder init --example --config ${chalk.gray('# Create both')}
|
|
214
|
+
|
|
215
|
+
${chalk.yellow('What gets created:')}
|
|
216
|
+
${chalk.cyan('--example:')} Creates a docs/ folder with:
|
|
217
|
+
• README.md with welcome message and mermaid diagram
|
|
218
|
+
• getting-started.md with setup instructions
|
|
219
|
+
• guides/configuration.md with config options
|
|
220
|
+
|
|
221
|
+
${chalk.cyan('--config:')} Creates doc-builder.config.js with:
|
|
222
|
+
• Site name and description
|
|
223
|
+
• Feature toggles (auth, dark mode, etc.)
|
|
224
|
+
• Directory paths
|
|
225
|
+
• Authentication settings
|
|
172
226
|
`)
|
|
173
227
|
.action(async (options) => {
|
|
174
228
|
if (options.config) {
|
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.2",
|
|
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
|
+
}
|