zengen 0.1.0

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/src/builder.ts ADDED
@@ -0,0 +1,292 @@
1
+ import { BuildOptions, FileInfo, NavigationItem, ZenConfig } from './types';
2
+ import { MarkdownConverter } from './markdown';
3
+ import { TemplateEngine } from './template';
4
+ import { NavigationGenerator } from './navigation';
5
+ import * as fs from 'fs/promises';
6
+ import * as path from 'path';
7
+ import * as chokidar from 'chokidar';
8
+
9
+ export class ZenBuilder {
10
+ private markdownConverter: MarkdownConverter;
11
+ private templateEngine: TemplateEngine;
12
+ private navigationGenerator: NavigationGenerator;
13
+ private config: ZenConfig = {};
14
+
15
+ constructor(config: ZenConfig = {}) {
16
+ this.config = config;
17
+ this.markdownConverter = new MarkdownConverter(config.processors || []);
18
+ this.templateEngine = new TemplateEngine();
19
+ this.navigationGenerator = new NavigationGenerator();
20
+ }
21
+
22
+ /**
23
+ * 构建文档站点
24
+ */
25
+ async build(options: BuildOptions): Promise<void> {
26
+ const startTime = Date.now();
27
+ const { srcDir, outDir, template, verbose = false } = options;
28
+
29
+ if (verbose) {
30
+ console.log(`🚀 Starting ZEN build...`);
31
+ console.log(`📁 Source: ${srcDir}`);
32
+ console.log(`📁 Output: ${outDir}`);
33
+ }
34
+
35
+ // 验证源目录
36
+ try {
37
+ await fs.access(srcDir);
38
+ } catch (error) {
39
+ throw new Error(`Source directory does not exist: ${srcDir}`);
40
+ }
41
+
42
+ // 确保输出目录存在
43
+ await fs.mkdir(outDir, { recursive: true });
44
+
45
+ // 读取并转换 Markdown 文件
46
+ if (verbose) console.log(`📄 Reading Markdown files...`);
47
+ const files = await this.markdownConverter.convertDirectory(srcDir);
48
+
49
+ if (files.length === 0) {
50
+ console.warn(`⚠️ No Markdown files found in ${srcDir}`);
51
+ return;
52
+ }
53
+
54
+ if (verbose) console.log(`✅ Found ${files.length} Markdown files`);
55
+
56
+ // 生成导航
57
+ if (verbose) console.log(`🗺️ Generating navigation...`);
58
+ const navigation = this.navigationGenerator.generate(files);
59
+
60
+ // 处理每个文件
61
+ if (verbose) console.log(`⚡ Processing files...`);
62
+ let processedCount = 0;
63
+
64
+ for (const file of files) {
65
+ try {
66
+ // 生成模板数据
67
+ const templateData = this.templateEngine.generateTemplateData(file, navigation);
68
+
69
+ // 渲染模板
70
+ const html = await this.templateEngine.render(templateData, template);
71
+
72
+ // 生成输出路径
73
+ const outputPath = this.templateEngine.getOutputPath(file, outDir);
74
+
75
+ // 保存文件
76
+ await this.templateEngine.saveToFile(html, outputPath);
77
+
78
+ processedCount++;
79
+
80
+ if (verbose && processedCount % 10 === 0) {
81
+ console.log(` Processed ${processedCount}/${files.length} files...`);
82
+ }
83
+ } catch (error) {
84
+ console.error(`❌ Failed to process ${file.relativePath}:`, error);
85
+ }
86
+ }
87
+
88
+ // 生成站点地图
89
+ if (verbose) console.log(`🗺️ Generating sitemap...`);
90
+ await this.generateSitemap(files, outDir);
91
+
92
+ // 生成导航 JSON 文件
93
+ if (verbose) console.log(`📊 Generating navigation data...`);
94
+ await this.generateNavigationJson(files, outDir);
95
+
96
+ // 复制静态资源(如果存在)
97
+ await this.copyStaticAssets(srcDir, outDir);
98
+
99
+ const duration = Date.now() - startTime;
100
+ if (verbose) {
101
+ console.log(`🎉 Build completed!`);
102
+ console.log(` Files processed: ${processedCount}/${files.length}`);
103
+ console.log(` Duration: ${duration}ms`);
104
+ console.log(` Output directory: ${outDir}`);
105
+ } else {
106
+ console.log(`✅ Built ${processedCount} files to ${outDir} in ${duration}ms`);
107
+ }
108
+ }
109
+
110
+ /**
111
+ * 监听文件变化并自动重建
112
+ */
113
+ async watch(options: BuildOptions): Promise<void> {
114
+ const { srcDir, outDir, template, verbose = false } = options;
115
+
116
+ console.log(`👀 Watching for changes in ${srcDir}...`);
117
+ console.log(`Press Ctrl+C to stop watching`);
118
+
119
+ // 初始构建
120
+ await this.build(options);
121
+
122
+ // 设置文件监听
123
+ const watcher = chokidar.watch(srcDir, {
124
+ ignored: /(^|[\/\\])\../, // 忽略隐藏文件
125
+ persistent: true,
126
+ ignoreInitial: true
127
+ });
128
+
129
+ let isBuilding = false;
130
+ let buildQueue: string[] = [];
131
+
132
+ const debouncedBuild = async () => {
133
+ if (isBuilding) {
134
+ return;
135
+ }
136
+
137
+ isBuilding = true;
138
+ const changedFiles = [...buildQueue];
139
+ buildQueue = [];
140
+
141
+ try {
142
+ if (verbose) {
143
+ console.log(`\n🔄 Rebuilding due to changes in: ${changedFiles.join(', ')}`);
144
+ } else {
145
+ console.log(`\n🔄 Rebuilding...`);
146
+ }
147
+
148
+ await this.build(options);
149
+ console.log(`✅ Rebuild complete. Watching for changes...`);
150
+ } catch (error) {
151
+ console.error(`❌ Rebuild failed:`, error);
152
+ } finally {
153
+ isBuilding = false;
154
+
155
+ // 如果队列中有新文件,立即处理
156
+ if (buildQueue.length > 0) {
157
+ setTimeout(debouncedBuild, 100);
158
+ }
159
+ }
160
+ };
161
+
162
+ watcher
163
+ .on('add', (filePath: string) => {
164
+ if (filePath.endsWith('.md')) {
165
+ if (verbose) console.log(`📄 File added: ${filePath}`);
166
+ buildQueue.push(filePath);
167
+ setTimeout(debouncedBuild, 300);
168
+ }
169
+ })
170
+ .on('change', (filePath: string) => {
171
+ if (filePath.endsWith('.md')) {
172
+ if (verbose) console.log(`📄 File changed: ${filePath}`);
173
+ buildQueue.push(filePath);
174
+ setTimeout(debouncedBuild, 300);
175
+ }
176
+ })
177
+ .on('unlink', (filePath: string) => {
178
+ if (filePath.endsWith('.md')) {
179
+ if (verbose) console.log(`📄 File removed: ${filePath}`);
180
+ buildQueue.push(filePath);
181
+ setTimeout(debouncedBuild, 300);
182
+ }
183
+ })
184
+ .on('error', (error: unknown) => {
185
+ console.error(`❌ Watcher error:`, error);
186
+ });
187
+
188
+ // 处理退出信号
189
+ process.on('SIGINT', () => {
190
+ console.log(`\n👋 Stopping watcher...`);
191
+ watcher.close();
192
+ process.exit(0);
193
+ });
194
+ }
195
+
196
+ /**
197
+ * 生成站点地图
198
+ */
199
+ private async generateSitemap(files: FileInfo[], outDir: string): Promise<void> {
200
+ try {
201
+ const sitemapXml = this.navigationGenerator.generateSitemap(files);
202
+ const sitemapPath = path.join(outDir, 'sitemap.xml');
203
+ await fs.writeFile(sitemapPath, sitemapXml, 'utf-8');
204
+ } catch (error) {
205
+ console.warn(`⚠️ Failed to generate sitemap:`, error);
206
+ }
207
+ }
208
+
209
+ /**
210
+ * 生成导航 JSON 文件
211
+ */
212
+ private async generateNavigationJson(files: FileInfo[], outDir: string): Promise<void> {
213
+ try {
214
+ const navigationJson = this.navigationGenerator.generateJsonNavigation(files);
215
+ const navPath = path.join(outDir, 'navigation.json');
216
+ await fs.writeFile(navPath, navigationJson, 'utf-8');
217
+ } catch (error) {
218
+ console.warn(`⚠️ Failed to generate navigation JSON:`, error);
219
+ }
220
+ }
221
+
222
+ /**
223
+ * 复制静态资源
224
+ */
225
+ private async copyStaticAssets(srcDir: string, outDir: string): Promise<void> {
226
+ const staticDir = path.join(srcDir, 'static');
227
+
228
+ try {
229
+ await fs.access(staticDir);
230
+
231
+ // 简单的递归复制
232
+ async function copyDir(source: string, target: string) {
233
+ await fs.mkdir(target, { recursive: true });
234
+ const entries = await fs.readdir(source, { withFileTypes: true });
235
+
236
+ for (const entry of entries) {
237
+ const srcPath = path.join(source, entry.name);
238
+ const destPath = path.join(target, entry.name);
239
+
240
+ if (entry.isDirectory()) {
241
+ await copyDir(srcPath, destPath);
242
+ } else {
243
+ await fs.copyFile(srcPath, destPath);
244
+ }
245
+ }
246
+ }
247
+
248
+ await copyDir(staticDir, path.join(outDir, 'static'));
249
+ } catch (error) {
250
+ // 静态目录不存在是正常的,忽略错误
251
+ }
252
+ }
253
+
254
+ /**
255
+ * 清理输出目录
256
+ */
257
+ async clean(outDir: string): Promise<void> {
258
+ try {
259
+ await fs.rm(outDir, { recursive: true, force: true });
260
+ console.log(`🧹 Cleaned output directory: ${outDir}`);
261
+ } catch (error) {
262
+ console.error(`❌ Failed to clean output directory:`, error);
263
+ }
264
+ }
265
+
266
+ /**
267
+ * 验证配置
268
+ */
269
+ validateConfig(config: ZenConfig): string[] {
270
+ const errors: string[] = [];
271
+
272
+ if (config.srcDir && !path.isAbsolute(config.srcDir)) {
273
+ errors.push('srcDir must be an absolute path');
274
+ }
275
+
276
+ if (config.outDir && !path.isAbsolute(config.outDir)) {
277
+ errors.push('outDir must be an absolute path');
278
+ }
279
+
280
+ if (config.i18n) {
281
+ if (!config.i18n.sourceLang) {
282
+ errors.push('i18n.sourceLang is required');
283
+ }
284
+
285
+ if (!config.i18n.targetLangs || config.i18n.targetLangs.length === 0) {
286
+ errors.push('i18n.targetLangs must have at least one language');
287
+ }
288
+ }
289
+
290
+ return errors;
291
+ }
292
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { ZenBuilder } from './builder';
5
+ import { ZenConfig } from './types';
6
+ import * as path from 'path';
7
+ import * as fs from 'fs/promises';
8
+
9
+ const program = new Command();
10
+
11
+ program
12
+ .name('zengen')
13
+ .description('ZEN - A minimalist Markdown documentation site builder')
14
+ .version('0.1.0');
15
+
16
+ // 构建命令
17
+ program
18
+ .command('build')
19
+ .description('Build documentation site from Markdown files')
20
+ .argument('<src-dir>', 'Source directory containing Markdown files')
21
+ .option('-o, --out <dir>', 'Output directory for generated HTML', 'dist')
22
+ .option('-t, --template <file>', 'Custom template file')
23
+ .option('-w, --watch', 'Watch for changes and rebuild automatically')
24
+ .option('-v, --verbose', 'Show detailed output')
25
+ .option('-c, --config <file>', 'Configuration file')
26
+ .option('--clean', 'Clean output directory before building')
27
+ .action(async (srcDir, options) => {
28
+ try {
29
+ // 加载配置文件
30
+ let config: ZenConfig = {};
31
+ if (options.config) {
32
+ try {
33
+ const configPath = path.resolve(options.config);
34
+ const configContent = await fs.readFile(configPath, 'utf-8');
35
+ config = JSON.parse(configContent);
36
+ } catch (error) {
37
+ console.error(`❌ Failed to load config file:`, error);
38
+ process.exit(1);
39
+ }
40
+ }
41
+
42
+ // 合并命令行参数和配置
43
+ const buildOptions = {
44
+ srcDir: path.resolve(srcDir),
45
+ outDir: path.resolve(options.out),
46
+ template: options.template ? path.resolve(options.template) : undefined,
47
+ watch: options.watch,
48
+ verbose: options.verbose
49
+ };
50
+
51
+ const builder = new ZenBuilder(config);
52
+
53
+ // 验证配置
54
+ const errors = builder.validateConfig(config);
55
+ if (errors.length > 0) {
56
+ console.error('❌ Configuration errors:');
57
+ errors.forEach(error => console.error(` - ${error}`));
58
+ process.exit(1);
59
+ }
60
+
61
+ // 清理输出目录
62
+ if (options.clean) {
63
+ await builder.clean(buildOptions.outDir);
64
+ }
65
+
66
+ // 构建或监听
67
+ if (options.watch) {
68
+ await builder.watch(buildOptions);
69
+ } else {
70
+ await builder.build(buildOptions);
71
+ }
72
+ } catch (error) {
73
+ console.error('❌ Build failed:', error);
74
+ process.exit(1);
75
+ }
76
+ });
77
+
78
+ // 清理命令
79
+ program
80
+ .command('clean')
81
+ .description('Clean output directory')
82
+ .argument('<dir>', 'Directory to clean')
83
+ .action(async (dir) => {
84
+ try {
85
+ const builder = new ZenBuilder();
86
+ await builder.clean(path.resolve(dir));
87
+ } catch (error) {
88
+ console.error('❌ Clean failed:', error);
89
+ process.exit(1);
90
+ }
91
+ });
92
+
93
+ // 初始化命令
94
+ program
95
+ .command('init')
96
+ .description('Initialize a new ZEN project')
97
+ .option('-d, --dir <directory>', 'Target directory', '.')
98
+ .action(async (options) => {
99
+ try {
100
+ const targetDir = path.resolve(options.dir);
101
+
102
+ // 创建目录结构
103
+ await fs.mkdir(path.join(targetDir, 'docs'), { recursive: true });
104
+ await fs.mkdir(path.join(targetDir, 'static'), { recursive: true });
105
+
106
+ // 创建示例文档
107
+ const exampleDoc = `# Welcome to ZEN
108
+
109
+ This is an example documentation page generated by ZEN.
110
+
111
+ ## Getting Started
112
+
113
+ 1. Write your documentation in Markdown format
114
+ 2. Run \`zengen build docs --out dist\`
115
+ 3. Open the generated HTML files in your browser
116
+
117
+ ## Features
118
+
119
+ - **Minimal configuration**: Focus on writing, not configuration
120
+ - **Smart navigation**: Automatic navigation generation
121
+ - **Beautiful templates**: Clean, responsive design
122
+ - **Code highlighting**: Syntax highlighting for code blocks
123
+
124
+ ## Example Code
125
+
126
+ \`\`\`javascript
127
+ // This is a JavaScript example
128
+ console.log('Hello ZEN!');
129
+ \`\`\`
130
+
131
+ ---
132
+
133
+ *Happy documenting!*`;
134
+
135
+ await fs.writeFile(
136
+ path.join(targetDir, 'docs', 'index.md'),
137
+ exampleDoc,
138
+ 'utf-8'
139
+ );
140
+
141
+ // 创建配置文件
142
+ const config = {
143
+ srcDir: './docs',
144
+ outDir: './dist',
145
+ template: undefined,
146
+ i18n: {
147
+ sourceLang: 'en-US',
148
+ targetLangs: ['zh-CN', 'ja-JP']
149
+ }
150
+ };
151
+
152
+ await fs.writeFile(
153
+ path.join(targetDir, 'zen.config.json'),
154
+ JSON.stringify(config, null, 2),
155
+ 'utf-8'
156
+ );
157
+
158
+ // 创建 package.json 脚本(如果不存在)
159
+ const packageJsonPath = path.join(targetDir, 'package.json');
160
+ try {
161
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
162
+
163
+ if (!packageJson.scripts) {
164
+ packageJson.scripts = {};
165
+ }
166
+
167
+ packageJson.scripts.build = 'zengen build docs --out dist';
168
+ packageJson.scripts['build:watch'] = 'zengen build docs --out dist --watch';
169
+ packageJson.scripts.clean = 'zengen clean dist';
170
+
171
+ await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf-8');
172
+ } catch (error) {
173
+ // package.json 不存在,创建简单的版本
174
+ const simplePackageJson = {
175
+ name: 'zen-docs',
176
+ version: '1.0.0',
177
+ scripts: {
178
+ build: 'zengen build docs --out dist',
179
+ 'build:watch': 'zengen build docs --out dist --watch',
180
+ clean: 'zengen clean dist'
181
+ }
182
+ };
183
+
184
+ await fs.writeFile(
185
+ packageJsonPath,
186
+ JSON.stringify(simplePackageJson, null, 2),
187
+ 'utf-8'
188
+ );
189
+ }
190
+
191
+ console.log(`
192
+ 🎉 ZEN project initialized successfully!
193
+
194
+ Next steps:
195
+ 1. Add your Markdown files to the 'docs' directory
196
+ 2. Run 'npm run build' to generate the site
197
+ 3. Run 'npm run build:watch' for development with auto-reload
198
+
199
+ Project structure:
200
+ ${targetDir}/
201
+ ├── docs/ # Your documentation files
202
+ │ └── index.md # Example document
203
+ ├── static/ # Static assets (images, CSS, JS)
204
+ ├── zen.config.json # Configuration file
205
+ └── package.json # npm scripts
206
+
207
+ For more information, visit: https://github.com/yourusername/zen
208
+ `);
209
+ } catch (error) {
210
+ console.error('❌ Initialization failed:', error);
211
+ process.exit(1);
212
+ }
213
+ });
214
+
215
+ // 信息命令
216
+ program
217
+ .command('info')
218
+ .description('Show information about ZEN')
219
+ .action(() => {
220
+ console.log(`
221
+ 🤖 ZEN - A minimalist Markdown documentation site builder
222
+
223
+ Version: 0.1.0
224
+ Description: Build beautiful documentation sites from Markdown files
225
+
226
+ Features:
227
+ • Minimal configuration required
228
+ • Smart navigation generation
229
+ • Beautiful, responsive templates
230
+ • Code syntax highlighting
231
+ • Watch mode for development
232
+ • Sitemap generation
233
+ • Static asset support
234
+
235
+ Commands:
236
+ build Build documentation site
237
+ clean Clean output directory
238
+ init Initialize new project
239
+ info Show this information
240
+
241
+ Examples:
242
+ $ zengen build ./docs --out ./dist
243
+ $ zengen build ./docs --out ./dist --watch
244
+ $ zengen init --dir ./my-docs
245
+ $ zengen clean ./dist
246
+
247
+ For more help, run: zengen --help
248
+ `);
249
+ });
250
+
251
+ // 默认命令(兼容旧格式)
252
+ program
253
+ .argument('[src-dir]', 'Source directory')
254
+ .option('-o, --out <dir>', 'Output directory')
255
+ .option('-t, --template <file>', 'Custom template file')
256
+ .option('-w, --watch', 'Watch for changes')
257
+ .option('-v, --verbose', 'Show detailed output')
258
+ .action(async (srcDir, options) => {
259
+ if (!srcDir) {
260
+ program.help();
261
+ return;
262
+ }
263
+
264
+ try {
265
+ const builder = new ZenBuilder();
266
+
267
+ const buildOptions = {
268
+ srcDir: path.resolve(srcDir),
269
+ outDir: path.resolve(options.out || 'dist'),
270
+ template: options.template ? path.resolve(options.template) : undefined,
271
+ watch: options.watch,
272
+ verbose: options.verbose
273
+ };
274
+
275
+ if (options.watch) {
276
+ await builder.watch(buildOptions);
277
+ } else {
278
+ await builder.build(buildOptions);
279
+ }
280
+ } catch (error) {
281
+ console.error('❌ Build failed:', error);
282
+ process.exit(1);
283
+ }
284
+ });
285
+
286
+ // 错误处理
287
+ program.showHelpAfterError();
288
+ program.showSuggestionAfterError();
289
+
290
+ // 解析命令行参数
291
+ program.parse(process.argv);
292
+
293
+ // 如果没有参数,显示帮助
294
+ if (process.argv.length <= 2) {
295
+ program.help();
296
+ }
package/src/index.ts ADDED
@@ -0,0 +1,40 @@
1
+ export { ZenBuilder } from './builder';
2
+ export { MarkdownConverter } from './markdown';
3
+ export { TemplateEngine } from './template';
4
+ export { NavigationGenerator } from './navigation';
5
+ export type {
6
+ BuildOptions,
7
+ FileInfo,
8
+ NavigationItem,
9
+ TemplateData,
10
+ MarkdownProcessor,
11
+ ZenConfig
12
+ } from './types';
13
+
14
+ /**
15
+ * ZEN 文档构建工具
16
+ *
17
+ * 一个极简主义的 Markdown 文档站点生成器
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * import { ZenBuilder } from 'zengen';
22
+ *
23
+ * const builder = new ZenBuilder();
24
+ * await builder.build({
25
+ * srcDir: './docs',
26
+ * outDir: './dist'
27
+ * });
28
+ * ```
29
+ */
30
+ import { ZenBuilder } from './builder';
31
+ import { MarkdownConverter } from './markdown';
32
+ import { TemplateEngine } from './template';
33
+ import { NavigationGenerator } from './navigation';
34
+
35
+ export default {
36
+ Builder: ZenBuilder,
37
+ MarkdownConverter: MarkdownConverter,
38
+ TemplateEngine: TemplateEngine,
39
+ NavigationGenerator: NavigationGenerator
40
+ };