zengen 0.1.34 → 0.1.36
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/.github/workflows/bump-version.yml +112 -0
- package/.github/workflows/ci.yml +2 -2
- package/.github/workflows/pages.yml +1 -7
- package/.zen/meta.json +57 -0
- package/.zen/translations.json +51 -0
- package/dist/ai-client.d.ts +34 -0
- package/dist/ai-client.d.ts.map +1 -0
- package/dist/ai-client.js +180 -0
- package/dist/ai-client.js.map +1 -0
- package/dist/ai-processor.d.ts +51 -0
- package/dist/ai-processor.d.ts.map +1 -0
- package/dist/ai-processor.js +215 -0
- package/dist/ai-processor.js.map +1 -0
- package/dist/ai-service.d.ts +79 -0
- package/dist/ai-service.d.ts.map +1 -0
- package/dist/ai-service.js +257 -0
- package/dist/ai-service.js.map +1 -0
- package/dist/builder.d.ts +26 -2
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +420 -9
- package/dist/builder.js.map +1 -1
- package/dist/cli.js +45 -3
- package/dist/cli.js.map +1 -1
- package/dist/gitignore.d.ts +2 -1
- package/dist/gitignore.d.ts.map +1 -1
- package/dist/gitignore.js +21 -3
- package/dist/gitignore.js.map +1 -1
- package/dist/gitignore.test.js +82 -17
- package/dist/gitignore.test.js.map +1 -1
- package/dist/markdown.d.ts +6 -1
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +31 -9
- package/dist/markdown.js.map +1 -1
- package/dist/navigation.js +5 -5
- package/dist/navigation.js.map +1 -1
- package/dist/scanner.d.ts +26 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +190 -0
- package/dist/scanner.js.map +1 -0
- package/dist/template.d.ts +6 -2
- package/dist/template.d.ts.map +1 -1
- package/dist/template.js +58 -9
- package/dist/template.js.map +1 -1
- package/dist/translation-service.d.ts +72 -0
- package/dist/translation-service.d.ts.map +1 -0
- package/dist/translation-service.js +291 -0
- package/dist/translation-service.js.map +1 -0
- package/dist/types.d.ts +35 -4
- package/dist/types.d.ts.map +1 -1
- package/docs/advanced-usage.md +39 -0
- package/docs/getting-started.md +26 -0
- package/docs/guides/best-practices.md +0 -113
- package/docs/guides/config.md +0 -233
- package/package.json +3 -2
- package/src/ai-client.ts +227 -0
- package/src/ai-processor.ts +243 -0
- package/src/ai-service.ts +281 -0
- package/src/builder.ts +543 -10
- package/src/cli.ts +49 -3
- package/src/gitignore.test.ts +82 -17
- package/src/gitignore.ts +23 -3
- package/src/markdown.ts +39 -11
- package/src/navigation.ts +5 -5
- package/src/scanner.ts +180 -0
- package/src/template.ts +69 -9
- package/src/translation-service.ts +350 -0
- package/src/types.ts +39 -3
- package/test-multilang.js +44 -0
- package/docs/ci/github-ci-cd.md +0 -127
- package/docs/guides/api.md +0 -277
package/src/cli.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { config } from 'dotenv';
|
|
3
4
|
import { Cli, Command, Option } from 'clipanion';
|
|
4
5
|
import { ZenBuilder } from './builder';
|
|
5
6
|
import { ZenConfig } from './types';
|
|
@@ -8,6 +9,9 @@ import * as fs from 'fs/promises';
|
|
|
8
9
|
import * as fsSync from 'fs';
|
|
9
10
|
import * as url from 'url';
|
|
10
11
|
|
|
12
|
+
// 加载 .env 文件中的环境变量
|
|
13
|
+
config();
|
|
14
|
+
|
|
11
15
|
// 获取版本号 - 从 package.json 读取
|
|
12
16
|
function getVersion(): string {
|
|
13
17
|
try {
|
|
@@ -55,6 +59,10 @@ class BuildCommand extends BaseCommand {
|
|
|
55
59
|
config = Option.String('-c,--config');
|
|
56
60
|
baseUrl = Option.String('--base-url');
|
|
57
61
|
clean = Option.Boolean('--clean');
|
|
62
|
+
ai = Option.Boolean('--ai', { description: 'Enable AI metadata extraction' });
|
|
63
|
+
lang = Option.Array('--lang', {
|
|
64
|
+
description: 'Target languages for translation (e.g., en-US, ja-JP)',
|
|
65
|
+
});
|
|
58
66
|
|
|
59
67
|
static usage = Command.Usage({
|
|
60
68
|
description: 'Build documentation site from Markdown files in current directory',
|
|
@@ -69,6 +77,8 @@ class BuildCommand extends BaseCommand {
|
|
|
69
77
|
$ zengen build --watch --serve --port 8080
|
|
70
78
|
$ zengen build --config zen.config.json
|
|
71
79
|
$ zengen build --clean
|
|
80
|
+
$ zengen build --ai (requires OPENAI_API_KEY environment variable)
|
|
81
|
+
$ zengen build --lang en-US --lang ja-JP (translate to English and Japanese)
|
|
72
82
|
`,
|
|
73
83
|
});
|
|
74
84
|
|
|
@@ -81,6 +91,23 @@ class BuildCommand extends BaseCommand {
|
|
|
81
91
|
const currentDir = process.cwd();
|
|
82
92
|
const outDir = this.getOutDir();
|
|
83
93
|
|
|
94
|
+
// 处理 AI 配置:如果指定了 --ai 参数,启用 AI;否则使用配置中的设置
|
|
95
|
+
const aiConfig = {
|
|
96
|
+
...config.ai,
|
|
97
|
+
enabled: this.ai ? true : config.ai?.enabled,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// 处理语言配置:命令行参数优先于配置文件
|
|
101
|
+
const targetLangs = this.lang && this.lang.length > 0 ? this.lang : config.i18n?.targetLangs;
|
|
102
|
+
const i18nConfig =
|
|
103
|
+
targetLangs && targetLangs.length > 0
|
|
104
|
+
? {
|
|
105
|
+
...config.i18n,
|
|
106
|
+
sourceLang: config.i18n?.sourceLang || 'zh-Hans', // 默认源语言为中文
|
|
107
|
+
targetLangs,
|
|
108
|
+
}
|
|
109
|
+
: undefined;
|
|
110
|
+
|
|
84
111
|
// 合并命令行参数和配置
|
|
85
112
|
const buildOptions = {
|
|
86
113
|
srcDir: currentDir,
|
|
@@ -92,12 +119,20 @@ class BuildCommand extends BaseCommand {
|
|
|
92
119
|
host: this.host,
|
|
93
120
|
verbose: this.verbose,
|
|
94
121
|
baseUrl: this.baseUrl || config.baseUrl,
|
|
122
|
+
langs: this.lang,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// 创建最终的配置,包含 AI 和 i18n 设置
|
|
126
|
+
const finalConfig = {
|
|
127
|
+
...config,
|
|
128
|
+
ai: aiConfig.enabled ? aiConfig : undefined,
|
|
129
|
+
i18n: i18nConfig,
|
|
95
130
|
};
|
|
96
131
|
|
|
97
|
-
const builder = new ZenBuilder(
|
|
132
|
+
const builder = new ZenBuilder(finalConfig);
|
|
98
133
|
|
|
99
134
|
// 验证配置
|
|
100
|
-
const errors = builder.validateConfig(
|
|
135
|
+
const errors = builder.validateConfig(finalConfig);
|
|
101
136
|
if (errors.length > 0) {
|
|
102
137
|
this.context.stderr.write('❌ Configuration errors:\n');
|
|
103
138
|
errors.forEach(error => this.context.stderr.write(` - ${error}\n`));
|
|
@@ -121,7 +156,18 @@ class BuildCommand extends BaseCommand {
|
|
|
121
156
|
if (this.watch) {
|
|
122
157
|
await builder.watch(buildOptions);
|
|
123
158
|
} else {
|
|
124
|
-
|
|
159
|
+
// 如果指定了语言参数,使用多语言构建
|
|
160
|
+
if (this.lang && this.lang.length > 0) {
|
|
161
|
+
const multiLangOptions = {
|
|
162
|
+
...buildOptions,
|
|
163
|
+
langs: this.lang,
|
|
164
|
+
useMetaData: true,
|
|
165
|
+
filterOrphans: true,
|
|
166
|
+
};
|
|
167
|
+
await builder.buildMultiLang(multiLangOptions);
|
|
168
|
+
} else {
|
|
169
|
+
await builder.build(buildOptions);
|
|
170
|
+
}
|
|
125
171
|
}
|
|
126
172
|
|
|
127
173
|
return 0;
|
package/src/gitignore.test.ts
CHANGED
|
@@ -35,10 +35,14 @@ describe('GitIgnoreProcessor', () => {
|
|
|
35
35
|
assert.strictEqual(processor['baseDir'], '/test/dir');
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
it('
|
|
38
|
+
it('应该初始化包含硬编码模式的数组', () => {
|
|
39
39
|
const processor = new GitIgnoreProcessor('/test/dir');
|
|
40
40
|
const patterns = processor.getPatterns();
|
|
41
|
-
|
|
41
|
+
// 检查是否包含硬编码模式
|
|
42
|
+
assert.ok(patterns.includes('node_modules'));
|
|
43
|
+
assert.ok(patterns.includes('**/node_modules'));
|
|
44
|
+
assert.ok(patterns.includes('.git'));
|
|
45
|
+
assert.ok(patterns.includes('.zen'));
|
|
42
46
|
});
|
|
43
47
|
});
|
|
44
48
|
|
|
@@ -57,15 +61,28 @@ dist/
|
|
|
57
61
|
await processor.loadFromFile('.gitignore');
|
|
58
62
|
|
|
59
63
|
const patterns = processor.getPatterns();
|
|
60
|
-
|
|
64
|
+
// 应该包含硬编码模式和从.gitignore加载的模式
|
|
65
|
+
assert.ok(patterns.includes('node_modules')); // 硬编码
|
|
66
|
+
assert.ok(patterns.includes('**/node_modules')); // 从.gitignore加载
|
|
67
|
+
assert.ok(patterns.includes('*.log')); // 从.gitignore加载
|
|
68
|
+
assert.ok(patterns.includes('**/dist')); // 从.gitignore加载
|
|
69
|
+
assert.ok(patterns.includes('**/.DS_Store')); // 从.gitignore加载
|
|
70
|
+
assert.ok(patterns.includes('.git')); // 硬编码
|
|
71
|
+
assert.ok(patterns.includes('.zen')); // 硬编码
|
|
61
72
|
});
|
|
62
73
|
|
|
63
|
-
it('当.gitignore
|
|
74
|
+
it('当.gitignore文件不存在时应该使用硬编码模式', async () => {
|
|
64
75
|
const processor = new GitIgnoreProcessor(testDir);
|
|
65
76
|
await processor.loadFromFile('non-existent-file');
|
|
66
77
|
|
|
67
78
|
const patterns = processor.getPatterns();
|
|
68
|
-
|
|
79
|
+
// 应该只包含硬编码模式
|
|
80
|
+
assert.ok(patterns.includes('node_modules'));
|
|
81
|
+
assert.ok(patterns.includes('**/node_modules'));
|
|
82
|
+
assert.ok(patterns.includes('.git'));
|
|
83
|
+
assert.ok(patterns.includes('.zen'));
|
|
84
|
+
// 不应该有用户定义的模式
|
|
85
|
+
assert.ok(!patterns.includes('*.log'));
|
|
69
86
|
});
|
|
70
87
|
|
|
71
88
|
it('应该跳过空行和注释', async () => {
|
|
@@ -83,7 +100,12 @@ node_modules/
|
|
|
83
100
|
await processor.loadFromFile('.gitignore');
|
|
84
101
|
|
|
85
102
|
const patterns = processor.getPatterns();
|
|
86
|
-
|
|
103
|
+
// 应该包含硬编码模式和从.gitignore加载的模式
|
|
104
|
+
assert.ok(patterns.includes('node_modules')); // 硬编码
|
|
105
|
+
assert.ok(patterns.includes('**/node_modules')); // 从.gitignore加载
|
|
106
|
+
assert.ok(patterns.includes('*.log')); // 从.gitignore加载
|
|
107
|
+
assert.ok(patterns.includes('.git')); // 硬编码
|
|
108
|
+
assert.ok(patterns.includes('.zen')); // 硬编码
|
|
87
109
|
});
|
|
88
110
|
|
|
89
111
|
it('应该跳过否定模式', async () => {
|
|
@@ -98,7 +120,12 @@ node_modules/
|
|
|
98
120
|
await processor.loadFromFile('.gitignore');
|
|
99
121
|
|
|
100
122
|
const patterns = processor.getPatterns();
|
|
101
|
-
|
|
123
|
+
// 应该包含硬编码模式和从.gitignore加载的模式(不包括否定模式)
|
|
124
|
+
assert.ok(patterns.includes('node_modules')); // 硬编码
|
|
125
|
+
assert.ok(patterns.includes('**/node_modules')); // 从.gitignore加载
|
|
126
|
+
assert.ok(patterns.includes('*.log')); // 从.gitignore加载
|
|
127
|
+
assert.ok(patterns.includes('.git')); // 硬编码
|
|
128
|
+
assert.ok(patterns.includes('.zen')); // 硬编码
|
|
102
129
|
});
|
|
103
130
|
});
|
|
104
131
|
|
|
@@ -112,7 +139,12 @@ dist/
|
|
|
112
139
|
processor['parsePatterns'](content);
|
|
113
140
|
|
|
114
141
|
const patterns = processor.getPatterns();
|
|
115
|
-
|
|
142
|
+
// 应该包含硬编码模式和解析的模式
|
|
143
|
+
assert.ok(patterns.includes('node_modules')); // 硬编码
|
|
144
|
+
assert.ok(patterns.includes('**/node_modules')); // 从内容解析
|
|
145
|
+
assert.ok(patterns.includes('**/dist')); // 从内容解析
|
|
146
|
+
assert.ok(patterns.includes('.git')); // 硬编码
|
|
147
|
+
assert.ok(patterns.includes('.zen')); // 硬编码
|
|
116
148
|
});
|
|
117
149
|
|
|
118
150
|
it('应该正确处理文件扩展名模式', () => {
|
|
@@ -124,7 +156,12 @@ dist/
|
|
|
124
156
|
processor['parsePatterns'](content);
|
|
125
157
|
|
|
126
158
|
const patterns = processor.getPatterns();
|
|
127
|
-
|
|
159
|
+
// 应该包含硬编码模式和解析的模式
|
|
160
|
+
assert.ok(patterns.includes('node_modules')); // 硬编码
|
|
161
|
+
assert.ok(patterns.includes('*.log')); // 从内容解析
|
|
162
|
+
assert.ok(patterns.includes('*.tmp')); // 从内容解析
|
|
163
|
+
assert.ok(patterns.includes('.git')); // 硬编码
|
|
164
|
+
assert.ok(patterns.includes('.zen')); // 硬编码
|
|
128
165
|
});
|
|
129
166
|
|
|
130
167
|
it('应该处理以/开头的模式', () => {
|
|
@@ -136,7 +173,11 @@ dist/
|
|
|
136
173
|
processor['parsePatterns'](content);
|
|
137
174
|
|
|
138
175
|
const patterns = processor.getPatterns();
|
|
139
|
-
|
|
176
|
+
// 应该包含硬编码模式和解析的模式
|
|
177
|
+
assert.ok(patterns.includes('node_modules')); // 硬编码
|
|
178
|
+
assert.ok(patterns.includes('dist')); // 从内容解析
|
|
179
|
+
assert.ok(patterns.includes('.git')); // 硬编码
|
|
180
|
+
assert.ok(patterns.includes('.zen')); // 硬编码
|
|
140
181
|
});
|
|
141
182
|
|
|
142
183
|
it('应该处理包含通配符的模式', () => {
|
|
@@ -148,7 +189,12 @@ dist/
|
|
|
148
189
|
processor['parsePatterns'](content);
|
|
149
190
|
|
|
150
191
|
const patterns = processor.getPatterns();
|
|
151
|
-
|
|
192
|
+
// 应该包含硬编码模式和解析的模式
|
|
193
|
+
assert.ok(patterns.includes('node_modules')); // 硬编码
|
|
194
|
+
assert.ok(patterns.includes('**/node_modules')); // 从内容解析
|
|
195
|
+
assert.ok(patterns.includes('**/.vscode')); // 从内容解析
|
|
196
|
+
assert.ok(patterns.includes('.git')); // 硬编码
|
|
197
|
+
assert.ok(patterns.includes('.zen')); // 硬编码
|
|
152
198
|
});
|
|
153
199
|
});
|
|
154
200
|
|
|
@@ -218,21 +264,35 @@ dist/
|
|
|
218
264
|
const processor = new GitIgnoreProcessor(testDir);
|
|
219
265
|
|
|
220
266
|
processor.addPattern('custom-pattern');
|
|
221
|
-
|
|
267
|
+
const patterns = processor.getPatterns();
|
|
268
|
+
// 应该包含硬编码模式和自定义模式
|
|
269
|
+
assert.ok(patterns.includes('node_modules')); // 硬编码
|
|
270
|
+
assert.ok(patterns.includes('custom-pattern')); // 自定义
|
|
222
271
|
|
|
223
272
|
processor.addPattern('another-pattern');
|
|
224
|
-
|
|
273
|
+
const updatedPatterns = processor.getPatterns();
|
|
274
|
+
assert.ok(updatedPatterns.includes('node_modules')); // 硬编码
|
|
275
|
+
assert.ok(updatedPatterns.includes('custom-pattern')); // 自定义
|
|
276
|
+
assert.ok(updatedPatterns.includes('another-pattern')); // 自定义
|
|
225
277
|
});
|
|
226
278
|
|
|
227
|
-
it('
|
|
279
|
+
it('应该能够清除所有用户模式(但保留硬编码模式)', () => {
|
|
228
280
|
const processor = new GitIgnoreProcessor(testDir);
|
|
229
281
|
|
|
230
282
|
processor.addPattern('pattern1');
|
|
231
283
|
processor.addPattern('pattern2');
|
|
232
|
-
|
|
284
|
+
// 总模式数 = 硬编码模式数 + 用户模式数
|
|
285
|
+
const totalPatterns = processor.getPatterns().length;
|
|
286
|
+
assert.ok(totalPatterns >= 2); // 至少包含硬编码模式
|
|
233
287
|
|
|
234
288
|
processor.clearPatterns();
|
|
235
|
-
|
|
289
|
+
const patternsAfterClear = processor.getPatterns();
|
|
290
|
+
// 清除后应该只包含硬编码模式
|
|
291
|
+
assert.ok(patternsAfterClear.includes('node_modules'));
|
|
292
|
+
assert.ok(patternsAfterClear.includes('.git'));
|
|
293
|
+
assert.ok(patternsAfterClear.includes('.zen'));
|
|
294
|
+
assert.ok(!patternsAfterClear.includes('pattern1'));
|
|
295
|
+
assert.ok(!patternsAfterClear.includes('pattern2'));
|
|
236
296
|
});
|
|
237
297
|
});
|
|
238
298
|
|
|
@@ -247,7 +307,12 @@ node_modules/
|
|
|
247
307
|
const processor = await createGitIgnoreProcessor(testDir);
|
|
248
308
|
const patterns = processor.getPatterns();
|
|
249
309
|
|
|
250
|
-
|
|
310
|
+
// 应该包含硬编码模式和从.gitignore加载的模式
|
|
311
|
+
assert.ok(patterns.includes('node_modules')); // 硬编码
|
|
312
|
+
assert.ok(patterns.includes('**/node_modules')); // 从.gitignore加载
|
|
313
|
+
assert.ok(patterns.includes('*.log')); // 从.gitignore加载
|
|
314
|
+
assert.ok(patterns.includes('.git')); // 硬编码
|
|
315
|
+
assert.ok(patterns.includes('.zen')); // 硬编码
|
|
251
316
|
});
|
|
252
317
|
});
|
|
253
318
|
});
|
package/src/gitignore.ts
CHANGED
|
@@ -10,6 +10,19 @@ export class GitIgnoreProcessor {
|
|
|
10
10
|
private patterns: string[] = [];
|
|
11
11
|
private baseDir: string;
|
|
12
12
|
|
|
13
|
+
// 始终忽略的硬编码模式列表
|
|
14
|
+
private readonly hardcodedPatterns: string[] = [
|
|
15
|
+
'node_modules', // Node.js 依赖目录
|
|
16
|
+
'**/node_modules', // 任意深度的 node_modules
|
|
17
|
+
'**/node_modules/**', // node_modules 中的所有内容
|
|
18
|
+
'.git', // Git 目录
|
|
19
|
+
'**/.git', // 任意深度的 .git 目录
|
|
20
|
+
'**/.git/**', // .git 目录中的所有内容
|
|
21
|
+
'.zen', // ZEN 构建输出目录
|
|
22
|
+
'**/.zen', // 任意深度的 .zen 目录
|
|
23
|
+
'**/.zen/**', // .zen 目录中的所有内容
|
|
24
|
+
];
|
|
25
|
+
|
|
13
26
|
constructor(baseDir: string) {
|
|
14
27
|
this.baseDir = baseDir;
|
|
15
28
|
}
|
|
@@ -90,7 +103,14 @@ export class GitIgnoreProcessor {
|
|
|
90
103
|
// 标准化路径分隔符为 /
|
|
91
104
|
const normalizedPath = relativePath.replace(/\\/g, '/');
|
|
92
105
|
|
|
93
|
-
//
|
|
106
|
+
// 首先检查硬编码模式(始终忽略)
|
|
107
|
+
for (const pattern of this.hardcodedPatterns) {
|
|
108
|
+
if (minimatch(normalizedPath, pattern, { dot: true })) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 然后检查用户定义的 .gitignore 模式
|
|
94
114
|
for (const pattern of this.patterns) {
|
|
95
115
|
// 首先尝试直接匹配
|
|
96
116
|
if (minimatch(normalizedPath, pattern, { dot: true })) {
|
|
@@ -142,10 +162,10 @@ export class GitIgnoreProcessor {
|
|
|
142
162
|
}
|
|
143
163
|
|
|
144
164
|
/**
|
|
145
|
-
*
|
|
165
|
+
* 获取所有忽略模式(包括硬编码模式)
|
|
146
166
|
*/
|
|
147
167
|
getPatterns(): string[] {
|
|
148
|
-
return [...this.patterns];
|
|
168
|
+
return [...this.hardcodedPatterns, ...this.patterns];
|
|
149
169
|
}
|
|
150
170
|
|
|
151
171
|
/**
|
package/src/markdown.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { marked } from 'marked';
|
|
2
2
|
import hljs from 'highlight.js';
|
|
3
|
-
import { FileInfo, MarkdownProcessor } from './types';
|
|
3
|
+
import { FileInfo, MarkdownProcessor, ScannedFile } from './types';
|
|
4
4
|
import * as fs from 'fs/promises';
|
|
5
5
|
import * as path from 'path';
|
|
6
6
|
import { GitIgnoreProcessor } from './gitignore';
|
|
@@ -123,8 +123,7 @@ export class MarkdownConverter {
|
|
|
123
123
|
const name = path.basename(filePath, ext);
|
|
124
124
|
|
|
125
125
|
const fileInfo: FileInfo = {
|
|
126
|
-
path:
|
|
127
|
-
relativePath,
|
|
126
|
+
path: relativePath,
|
|
128
127
|
name,
|
|
129
128
|
ext,
|
|
130
129
|
content,
|
|
@@ -135,14 +134,15 @@ export class MarkdownConverter {
|
|
|
135
134
|
|
|
136
135
|
/**
|
|
137
136
|
* 从目录读取所有 Markdown 文件并转换
|
|
137
|
+
* 保持向后兼容,但内部使用扫描逻辑
|
|
138
138
|
*/
|
|
139
139
|
async convertDirectory(dirPath: string): Promise<FileInfo[]> {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// 创建 GitIgnoreProcessor 并加载 .gitignore 文件
|
|
140
|
+
// 使用扫描逻辑获取文件列表
|
|
143
141
|
const gitignoreProcessor = new GitIgnoreProcessor(dirPath);
|
|
144
142
|
await gitignoreProcessor.loadFromFile();
|
|
145
143
|
|
|
144
|
+
const scannedFiles: ScannedFile[] = [];
|
|
145
|
+
|
|
146
146
|
async function scanDirectory(currentPath: string) {
|
|
147
147
|
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
148
148
|
|
|
@@ -162,23 +162,51 @@ export class MarkdownConverter {
|
|
|
162
162
|
if (entry.isDirectory()) {
|
|
163
163
|
await scanDirectory(fullPath);
|
|
164
164
|
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
165
|
-
const content = await fs.readFile(fullPath, 'utf-8');
|
|
166
165
|
const relativePath = path.relative(dirPath, fullPath);
|
|
167
166
|
const ext = path.extname(entry.name);
|
|
168
167
|
const name = path.basename(entry.name, ext);
|
|
169
168
|
|
|
170
|
-
|
|
171
|
-
path:
|
|
172
|
-
relativePath,
|
|
169
|
+
scannedFiles.push({
|
|
170
|
+
path: relativePath,
|
|
173
171
|
name,
|
|
174
172
|
ext,
|
|
175
|
-
content,
|
|
176
173
|
});
|
|
177
174
|
}
|
|
178
175
|
}
|
|
179
176
|
}
|
|
180
177
|
|
|
181
178
|
await scanDirectory(dirPath);
|
|
179
|
+
|
|
180
|
+
// 使用新的方法转换扫描的文件
|
|
181
|
+
return this.convertScannedFiles(scannedFiles, dirPath);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 从扫描的文件列表读取内容并转换
|
|
186
|
+
*/
|
|
187
|
+
async convertScannedFiles(
|
|
188
|
+
scannedFiles: ScannedFile[],
|
|
189
|
+
baseDir: string = ''
|
|
190
|
+
): Promise<FileInfo[]> {
|
|
191
|
+
const files: FileInfo[] = [];
|
|
192
|
+
|
|
193
|
+
for (const scannedFile of scannedFiles) {
|
|
194
|
+
try {
|
|
195
|
+
// 构建绝对路径
|
|
196
|
+
const absolutePath = baseDir ? path.join(baseDir, scannedFile.path) : scannedFile.path;
|
|
197
|
+
const content = await fs.readFile(absolutePath, 'utf-8');
|
|
198
|
+
files.push({
|
|
199
|
+
path: scannedFile.path,
|
|
200
|
+
name: scannedFile.name,
|
|
201
|
+
ext: scannedFile.ext,
|
|
202
|
+
content,
|
|
203
|
+
hash: scannedFile.hash, // 复制 hash 字段
|
|
204
|
+
});
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.warn(`⚠️ Failed to read file ${scannedFile.path}:`, error);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
182
210
|
return this.convertFiles(files);
|
|
183
211
|
}
|
|
184
212
|
}
|
package/src/navigation.ts
CHANGED
|
@@ -20,7 +20,7 @@ export class NavigationGenerator {
|
|
|
20
20
|
*/
|
|
21
21
|
generate(files: FileInfo[]): NavigationItem[] {
|
|
22
22
|
// 按路径排序
|
|
23
|
-
const sortedFiles = [...files].sort((a, b) => a.
|
|
23
|
+
const sortedFiles = [...files].sort((a, b) => a.path.localeCompare(b.path));
|
|
24
24
|
|
|
25
25
|
// 构建树形结构
|
|
26
26
|
const root: NavigationItem[] = [];
|
|
@@ -36,7 +36,7 @@ export class NavigationGenerator {
|
|
|
36
36
|
* 将文件添加到导航树中
|
|
37
37
|
*/
|
|
38
38
|
private addFileToNavigation(navigation: NavigationItem[], file: FileInfo): void {
|
|
39
|
-
const parts = file.
|
|
39
|
+
const parts = file.path.split('/');
|
|
40
40
|
let currentLevel = navigation;
|
|
41
41
|
|
|
42
42
|
for (let i = 0; i < parts.length; i++) {
|
|
@@ -55,7 +55,7 @@ export class NavigationGenerator {
|
|
|
55
55
|
|
|
56
56
|
// 生成路径
|
|
57
57
|
const rawPath = isMarkdownFile
|
|
58
|
-
? `/${file.
|
|
58
|
+
? `/${file.path.replace(/\.md$/, '.html')}`
|
|
59
59
|
: `/${parts.slice(0, i + 1).join('/')}`;
|
|
60
60
|
const itemPath = this.generatePath(rawPath);
|
|
61
61
|
|
|
@@ -137,7 +137,7 @@ export class NavigationGenerator {
|
|
|
137
137
|
return files
|
|
138
138
|
.map(file => {
|
|
139
139
|
const title = file.metadata?.title || this.formatTitle(file.name); // 优先使用提取的标题
|
|
140
|
-
const rawPath = `/${file.
|
|
140
|
+
const rawPath = `/${file.path.replace(/\.md$/, '.html')}`;
|
|
141
141
|
const itemPath = this.generatePath(rawPath);
|
|
142
142
|
|
|
143
143
|
return {
|
|
@@ -209,7 +209,7 @@ export class NavigationGenerator {
|
|
|
209
209
|
const effectiveBaseUrl = baseUrl || this.baseUrl || 'https://example.com';
|
|
210
210
|
const urls = files
|
|
211
211
|
.map(file => {
|
|
212
|
-
const path = `/${file.
|
|
212
|
+
const path = `/${file.path.replace(/\.md$/, '.html')}`;
|
|
213
213
|
const lastmod = new Date().toISOString().split('T')[0];
|
|
214
214
|
|
|
215
215
|
return ` <url>
|
package/src/scanner.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { ScannedFile, ZenConfig } from './types';
|
|
2
|
+
import { GitIgnoreProcessor } from './gitignore';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as minimatch from 'minimatch';
|
|
6
|
+
import * as crypto from 'crypto';
|
|
7
|
+
|
|
8
|
+
export class Scanner {
|
|
9
|
+
private config: ZenConfig;
|
|
10
|
+
|
|
11
|
+
constructor(config: ZenConfig = {}) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 计算文件内容的 sha256 hash
|
|
17
|
+
*/
|
|
18
|
+
private async calculateFileHash(filePath: string): Promise<string> {
|
|
19
|
+
try {
|
|
20
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
21
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.warn(`⚠️ Failed to calculate hash for ${filePath}:`, error);
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 扫描目录并生成文件列表
|
|
30
|
+
*/
|
|
31
|
+
async scanDirectory(dirPath: string): Promise<ScannedFile[]> {
|
|
32
|
+
const files: ScannedFile[] = [];
|
|
33
|
+
|
|
34
|
+
// 创建 GitIgnoreProcessor 并加载 .gitignore 文件
|
|
35
|
+
const gitignoreProcessor = new GitIgnoreProcessor(dirPath);
|
|
36
|
+
await gitignoreProcessor.loadFromFile();
|
|
37
|
+
|
|
38
|
+
// 获取 include/exclude 模式
|
|
39
|
+
const includePattern = this.config.includePattern || '**/*.md';
|
|
40
|
+
const excludePattern = this.config.excludePattern;
|
|
41
|
+
|
|
42
|
+
const scanDirectory = async (currentPath: string) => {
|
|
43
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
44
|
+
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
47
|
+
|
|
48
|
+
// 检查是否应该被 .gitignore 忽略
|
|
49
|
+
if (gitignoreProcessor.shouldIgnore(fullPath)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 忽略 .zen 目录(保持向后兼容)
|
|
54
|
+
if (entry.name === '.zen') {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
await scanDirectory(fullPath);
|
|
60
|
+
} else if (entry.isFile()) {
|
|
61
|
+
const relativePath = path.relative(dirPath, fullPath);
|
|
62
|
+
|
|
63
|
+
// 应用 include 模式
|
|
64
|
+
if (!minimatch.match([relativePath], includePattern).length) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 应用 exclude 模式
|
|
69
|
+
if (excludePattern && minimatch.match([relativePath], excludePattern).length > 0) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const ext = path.extname(entry.name);
|
|
74
|
+
const name = path.basename(entry.name, ext);
|
|
75
|
+
const hash = await this.calculateFileHash(fullPath);
|
|
76
|
+
|
|
77
|
+
files.push({
|
|
78
|
+
path: relativePath, // 只保存相对路径
|
|
79
|
+
name,
|
|
80
|
+
ext,
|
|
81
|
+
hash,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
await scanDirectory(dirPath);
|
|
88
|
+
return files;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 保存扫描结果到文件
|
|
93
|
+
*/
|
|
94
|
+
async saveScanResult(files: ScannedFile[], outputPath: string): Promise<void> {
|
|
95
|
+
// 确保输出目录存在
|
|
96
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
97
|
+
|
|
98
|
+
// 将扫描结果保存为 JSON
|
|
99
|
+
const scanResult = {
|
|
100
|
+
timestamp: new Date().toISOString(),
|
|
101
|
+
files,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
await fs.writeFile(outputPath, JSON.stringify(scanResult, null, 2), 'utf-8');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 从文件加载扫描结果
|
|
109
|
+
*/
|
|
110
|
+
async loadScanResult(inputPath: string): Promise<ScannedFile[]> {
|
|
111
|
+
try {
|
|
112
|
+
const content = await fs.readFile(inputPath, 'utf-8');
|
|
113
|
+
const scanResult = JSON.parse(content);
|
|
114
|
+
return scanResult.files || [];
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.warn(`⚠️ Failed to load scan result from ${inputPath}:`, error);
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 检查扫描结果是否过期
|
|
123
|
+
*/
|
|
124
|
+
async isScanResultOutdated(scanResultPath: string, dirPath: string): Promise<boolean> {
|
|
125
|
+
try {
|
|
126
|
+
// 检查扫描结果文件是否存在
|
|
127
|
+
await fs.access(scanResultPath);
|
|
128
|
+
|
|
129
|
+
// 读取扫描结果的时间戳
|
|
130
|
+
const content = await fs.readFile(scanResultPath, 'utf-8');
|
|
131
|
+
const scanResult = JSON.parse(content);
|
|
132
|
+
const scanTime = new Date(scanResult.timestamp).getTime();
|
|
133
|
+
|
|
134
|
+
// 检查源目录中是否有文件比扫描时间更新
|
|
135
|
+
const gitignoreProcessor = new GitIgnoreProcessor(dirPath);
|
|
136
|
+
await gitignoreProcessor.loadFromFile();
|
|
137
|
+
|
|
138
|
+
let isOutdated = false;
|
|
139
|
+
|
|
140
|
+
async function checkDirectory(currentPath: string) {
|
|
141
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
142
|
+
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
145
|
+
|
|
146
|
+
// 检查是否应该被 .gitignore 忽略
|
|
147
|
+
if (gitignoreProcessor.shouldIgnore(fullPath)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 忽略 .zen 目录
|
|
152
|
+
if (entry.name === '.zen') {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (entry.isDirectory()) {
|
|
157
|
+
await checkDirectory(fullPath);
|
|
158
|
+
} else if (entry.isFile()) {
|
|
159
|
+
// 检查文件修改时间
|
|
160
|
+
const stats = await fs.stat(fullPath);
|
|
161
|
+
if (stats.mtime.getTime() > scanTime) {
|
|
162
|
+
isOutdated = true;
|
|
163
|
+
return; // 提前退出
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (isOutdated) {
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await checkDirectory(dirPath);
|
|
174
|
+
return isOutdated;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
// 如果扫描结果文件不存在或读取失败,视为过期
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|