zengen 0.1.36 → 0.2.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/.github/workflows/pages.yml +1 -1
- package/.zen/meta.json +128 -30
- package/.zen/src/en-US/01d04f7c17b4a541ead9d759d877b30b403e15b849182a49eb1f62bd29ecd18c.md +120 -0
- package/.zen/src/en-US/1b798c44a4f353e47296ca83d5905e37e6aba3e90bbd9bc3b3d34fc12059a2ca.md +75 -0
- package/.zen/src/en-US/1e96be58d76c60056b708eb5bd8b8b81d7b5845d9cfe0b879d85068a5f11df3a.md +189 -0
- package/.zen/src/en-US/5ec990146b35e00de2630559126ee07f7cdcddeb23b0e8cab3d85b4181353e26.md +53 -0
- package/.zen/src/en-US/6124ea88edec5bde737b26b21f71ecfeffe4e73151784856edf813ee231a4baa.md +11 -0
- package/.zen/src/en-US/80ae9bed74fc6348a7c1fe9f33e86b65f5d919169721f77bcf0e1bc29fbdb4f9.md +61 -0
- package/.zen/src/en-US/f0c2799126931ccd113a0c45b1e623870b0d4f4f400becf6dd877da8f1011517.md +41 -0
- package/.zen/src/en-US/fdfca9b960d0eaa8b2b96fe988ead7481d2c0b16f66ebc94fb477139b4178cdc.md +65 -0
- package/.zen/src/zh-Hans/01d04f7c17b4a541ead9d759d877b30b403e15b849182a49eb1f62bd29ecd18c.md +120 -0
- package/.zen/src/zh-Hans/1b798c44a4f353e47296ca83d5905e37e6aba3e90bbd9bc3b3d34fc12059a2ca.md +77 -0
- package/.zen/src/zh-Hans/1e96be58d76c60056b708eb5bd8b8b81d7b5845d9cfe0b879d85068a5f11df3a.md +189 -0
- package/.zen/src/zh-Hans/5ec990146b35e00de2630559126ee07f7cdcddeb23b0e8cab3d85b4181353e26.md +55 -0
- package/.zen/src/zh-Hans/6124ea88edec5bde737b26b21f71ecfeffe4e73151784856edf813ee231a4baa.md +1 -0
- package/.zen/src/zh-Hans/6ad8db715a1b60613fe934fefb29fa981ecad9b63145593accff144d73b44bde.md +175 -0
- package/.zen/src/zh-Hans/80ae9bed74fc6348a7c1fe9f33e86b65f5d919169721f77bcf0e1bc29fbdb4f9.md +63 -0
- package/.zen/src/zh-Hans/a1580f71c6c6c1ff4a314be72d410a8507af2f087d56360c7f5048d349c21953.md +48 -0
- package/.zen/src/zh-Hans/d49012f98c4367b34034063400e2f7826bf0615952210c82396920172d468e2c.md +107 -0
- package/.zen/src/zh-Hans/f0c2799126931ccd113a0c45b1e623870b0d4f4f400becf6dd877da8f1011517.md +41 -0
- package/.zen/src/zh-Hans/fdfca9b960d0eaa8b2b96fe988ead7481d2c0b16f66ebc94fb477139b4178cdc.md +65 -0
- package/assets/templates/default/layout.html +274 -0
- package/dist/ai/extractMetadataFromMarkdown.d.ts +8 -0
- package/dist/ai/extractMetadataFromMarkdown.d.ts.map +1 -0
- package/dist/ai/extractMetadataFromMarkdown.js +88 -0
- package/dist/ai/extractMetadataFromMarkdown.js.map +1 -0
- package/dist/ai/translateMarkdown.d.ts +8 -0
- package/dist/ai/translateMarkdown.d.ts.map +1 -0
- package/dist/ai/translateMarkdown.js +29 -0
- package/dist/ai/translateMarkdown.js.map +1 -0
- package/dist/build/pipeline.d.ts +6 -0
- package/dist/build/pipeline.d.ts.map +1 -0
- package/dist/build/pipeline.js +218 -0
- package/dist/build/pipeline.js.map +1 -0
- package/dist/cli.js +10 -118
- package/dist/cli.js.map +1 -1
- package/dist/findEntries.d.ts +10 -0
- package/dist/findEntries.d.ts.map +1 -0
- package/dist/findEntries.js +38 -0
- package/dist/findEntries.js.map +1 -0
- package/dist/index.d.ts +1 -32
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -35
- package/dist/index.js.map +1 -1
- package/dist/metadata.d.ts +14 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +78 -0
- package/dist/metadata.js.map +1 -0
- package/dist/paths.d.ts +6 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +10 -0
- package/dist/paths.js.map +1 -0
- package/dist/process/extractMetadataByAI.d.ts +5 -0
- package/dist/process/extractMetadataByAI.d.ts.map +1 -0
- package/dist/process/extractMetadataByAI.js +31 -0
- package/dist/process/extractMetadataByAI.js.map +1 -0
- package/dist/process/template.d.ts +5 -0
- package/dist/process/template.d.ts.map +1 -0
- package/dist/process/template.js +188 -0
- package/dist/process/template.js.map +1 -0
- package/dist/scan/files.d.ts +7 -0
- package/dist/scan/files.d.ts.map +1 -0
- package/dist/scan/files.js +54 -0
- package/dist/scan/files.js.map +1 -0
- package/dist/services/openai.d.ts +41 -0
- package/dist/services/openai.d.ts.map +1 -0
- package/dist/services/openai.js +54 -0
- package/dist/services/openai.js.map +1 -0
- package/dist/types.d.ts +16 -67
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/convertMarkdownToHtml.d.ts +7 -0
- package/dist/utils/convertMarkdownToHtml.d.ts.map +1 -0
- package/dist/utils/convertMarkdownToHtml.js +39 -0
- package/dist/utils/convertMarkdownToHtml.js.map +1 -0
- package/dist/utils/frontmatter.d.ts +6 -0
- package/dist/utils/frontmatter.d.ts.map +1 -0
- package/dist/utils/frontmatter.js +22 -0
- package/dist/utils/frontmatter.js.map +1 -0
- package/docs/deployment/github-pages.md +1 -2
- package/docs/guides/best-practices.md +4 -4
- package/docs/guides/config.md +0 -5
- package/package.json +4 -2
- package/src/ai/extractMetadataFromMarkdown.ts +95 -0
- package/src/ai/translateMarkdown.ts +29 -0
- package/src/build/pipeline.ts +211 -0
- package/src/cli.ts +10 -132
- package/src/findEntries.ts +37 -0
- package/src/index.ts +1 -40
- package/src/metadata.ts +44 -0
- package/src/paths.ts +7 -0
- package/src/process/extractMetadataByAI.ts +29 -0
- package/src/process/template.ts +201 -0
- package/src/scan/files.ts +17 -0
- package/src/services/openai.ts +92 -0
- package/src/types.ts +18 -72
- package/src/utils/convertMarkdownToHtml.ts +32 -0
- package/src/utils/frontmatter.ts +18 -0
- package/.zen/translations.json +0 -51
- package/dist/ai-client.d.ts +0 -34
- package/dist/ai-client.d.ts.map +0 -1
- package/dist/ai-client.js +0 -180
- package/dist/ai-client.js.map +0 -1
- package/dist/ai-processor.d.ts +0 -51
- package/dist/ai-processor.d.ts.map +0 -1
- package/dist/ai-processor.js +0 -215
- package/dist/ai-processor.js.map +0 -1
- package/dist/ai-service.d.ts +0 -79
- package/dist/ai-service.d.ts.map +0 -1
- package/dist/ai-service.js +0 -257
- package/dist/ai-service.js.map +0 -1
- package/dist/builder.d.ts +0 -70
- package/dist/builder.d.ts.map +0 -1
- package/dist/builder.js +0 -854
- package/dist/builder.js.map +0 -1
- package/dist/gitignore.d.ts +0 -41
- package/dist/gitignore.d.ts.map +0 -1
- package/dist/gitignore.js +0 -202
- package/dist/gitignore.js.map +0 -1
- package/dist/gitignore.test.d.ts +0 -2
- package/dist/gitignore.test.d.ts.map +0 -1
- package/dist/gitignore.test.js +0 -309
- package/dist/gitignore.test.js.map +0 -1
- package/dist/markdown.d.ts +0 -35
- package/dist/markdown.d.ts.map +0 -1
- package/dist/markdown.js +0 -221
- package/dist/markdown.js.map +0 -1
- package/dist/navigation.d.ts +0 -46
- package/dist/navigation.d.ts.map +0 -1
- package/dist/navigation.js +0 -196
- package/dist/navigation.js.map +0 -1
- package/dist/scanner.d.ts +0 -26
- package/dist/scanner.d.ts.map +0 -1
- package/dist/scanner.js +0 -190
- package/dist/scanner.js.map +0 -1
- package/dist/template.d.ts +0 -33
- package/dist/template.d.ts.map +0 -1
- package/dist/template.js +0 -434
- package/dist/template.js.map +0 -1
- package/dist/translation-service.d.ts +0 -72
- package/dist/translation-service.d.ts.map +0 -1
- package/dist/translation-service.js +0 -291
- package/dist/translation-service.js.map +0 -1
- package/src/ai-client.ts +0 -227
- package/src/ai-processor.ts +0 -243
- package/src/ai-service.ts +0 -281
- package/src/builder.ts +0 -991
- package/src/gitignore.test.ts +0 -318
- package/src/gitignore.ts +0 -193
- package/src/markdown.ts +0 -212
- package/src/navigation.ts +0 -237
- package/src/scanner.ts +0 -180
- package/src/template.ts +0 -425
- package/src/translation-service.ts +0 -350
package/src/markdown.ts
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { marked } from 'marked';
|
|
2
|
-
import hljs from 'highlight.js';
|
|
3
|
-
import { FileInfo, MarkdownProcessor, ScannedFile } from './types';
|
|
4
|
-
import * as fs from 'fs/promises';
|
|
5
|
-
import * as path from 'path';
|
|
6
|
-
import { GitIgnoreProcessor } from './gitignore';
|
|
7
|
-
|
|
8
|
-
// 配置 marked 使用 highlight.js 进行代码高亮
|
|
9
|
-
marked.setOptions({
|
|
10
|
-
highlight: function (code: string, lang: string) {
|
|
11
|
-
if (lang && hljs.getLanguage(lang)) {
|
|
12
|
-
try {
|
|
13
|
-
return hljs.highlight(code, { language: lang }).value;
|
|
14
|
-
} catch (err) {
|
|
15
|
-
console.warn(`Failed to highlight code with language ${lang}:`, err);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return hljs.highlightAuto(code).value;
|
|
19
|
-
},
|
|
20
|
-
pedantic: false,
|
|
21
|
-
gfm: true,
|
|
22
|
-
breaks: false,
|
|
23
|
-
sanitize: false,
|
|
24
|
-
smartLists: true,
|
|
25
|
-
smartypants: false,
|
|
26
|
-
xhtml: false,
|
|
27
|
-
} as any);
|
|
28
|
-
|
|
29
|
-
export class MarkdownConverter {
|
|
30
|
-
private processors: MarkdownProcessor[] = [];
|
|
31
|
-
|
|
32
|
-
constructor(processors: MarkdownProcessor[] = []) {
|
|
33
|
-
this.processors = processors;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* 添加处理器
|
|
38
|
-
*/
|
|
39
|
-
addProcessor(processor: MarkdownProcessor): void {
|
|
40
|
-
this.processors.push(processor);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* 从内容中提取标题
|
|
45
|
-
*/
|
|
46
|
-
private extractTitle(content: string): string {
|
|
47
|
-
// 查找第一个一级标题
|
|
48
|
-
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
49
|
-
if (h1Match) {
|
|
50
|
-
return h1Match[1].trim();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// 如果没有一级标题,查找第一个二级标题
|
|
54
|
-
const h2Match = content.match(/^##\s+(.+)$/m);
|
|
55
|
-
if (h2Match) {
|
|
56
|
-
return h2Match[1].trim();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return 'Untitled';
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* 转换 Markdown 文件
|
|
64
|
-
*/
|
|
65
|
-
async convert(fileInfo: FileInfo): Promise<FileInfo> {
|
|
66
|
-
let content = fileInfo.content;
|
|
67
|
-
|
|
68
|
-
// 应用前置处理器
|
|
69
|
-
for (const processor of this.processors) {
|
|
70
|
-
if (processor.beforeParse) {
|
|
71
|
-
content = await processor.beforeParse(content, fileInfo);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// 转换 Markdown 为 HTML
|
|
76
|
-
let html = marked.parse(content) as string;
|
|
77
|
-
|
|
78
|
-
// 应用后置处理器
|
|
79
|
-
for (const processor of this.processors) {
|
|
80
|
-
if (processor.afterParse) {
|
|
81
|
-
html = await processor.afterParse(html, fileInfo);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// 提取标题
|
|
86
|
-
const title = this.extractTitle(content);
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
...fileInfo,
|
|
90
|
-
content,
|
|
91
|
-
html,
|
|
92
|
-
metadata: { title }, // 只保留标题作为 metadata
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* 批量转换文件
|
|
98
|
-
*/
|
|
99
|
-
async convertFiles(files: FileInfo[]): Promise<FileInfo[]> {
|
|
100
|
-
const results: FileInfo[] = [];
|
|
101
|
-
|
|
102
|
-
for (const file of files) {
|
|
103
|
-
try {
|
|
104
|
-
const result = await this.convert(file);
|
|
105
|
-
results.push(result);
|
|
106
|
-
} catch (error) {
|
|
107
|
-
console.error(`Failed to convert file ${file.path}:`, error);
|
|
108
|
-
// 即使转换失败,也保留原始文件信息
|
|
109
|
-
results.push(file);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return results;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* 从文件路径读取并转换
|
|
118
|
-
*/
|
|
119
|
-
async convertFromPath(filePath: string, baseDir: string = ''): Promise<FileInfo> {
|
|
120
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
121
|
-
const relativePath = baseDir ? path.relative(baseDir, filePath) : filePath;
|
|
122
|
-
const ext = path.extname(filePath);
|
|
123
|
-
const name = path.basename(filePath, ext);
|
|
124
|
-
|
|
125
|
-
const fileInfo: FileInfo = {
|
|
126
|
-
path: relativePath,
|
|
127
|
-
name,
|
|
128
|
-
ext,
|
|
129
|
-
content,
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
return this.convert(fileInfo);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* 从目录读取所有 Markdown 文件并转换
|
|
137
|
-
* 保持向后兼容,但内部使用扫描逻辑
|
|
138
|
-
*/
|
|
139
|
-
async convertDirectory(dirPath: string): Promise<FileInfo[]> {
|
|
140
|
-
// 使用扫描逻辑获取文件列表
|
|
141
|
-
const gitignoreProcessor = new GitIgnoreProcessor(dirPath);
|
|
142
|
-
await gitignoreProcessor.loadFromFile();
|
|
143
|
-
|
|
144
|
-
const scannedFiles: ScannedFile[] = [];
|
|
145
|
-
|
|
146
|
-
async function scanDirectory(currentPath: string) {
|
|
147
|
-
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
148
|
-
|
|
149
|
-
for (const entry of entries) {
|
|
150
|
-
const fullPath = path.join(currentPath, entry.name);
|
|
151
|
-
|
|
152
|
-
// 检查是否应该被 .gitignore 忽略
|
|
153
|
-
if (gitignoreProcessor.shouldIgnore(fullPath)) {
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// 忽略 .zen 目录(保持向后兼容)
|
|
158
|
-
if (entry.name === '.zen') {
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (entry.isDirectory()) {
|
|
163
|
-
await scanDirectory(fullPath);
|
|
164
|
-
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
165
|
-
const relativePath = path.relative(dirPath, fullPath);
|
|
166
|
-
const ext = path.extname(entry.name);
|
|
167
|
-
const name = path.basename(entry.name, ext);
|
|
168
|
-
|
|
169
|
-
scannedFiles.push({
|
|
170
|
-
path: relativePath,
|
|
171
|
-
name,
|
|
172
|
-
ext,
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
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
|
-
|
|
210
|
-
return this.convertFiles(files);
|
|
211
|
-
}
|
|
212
|
-
}
|
package/src/navigation.ts
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import { NavigationItem, FileInfo } from './types';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
|
|
4
|
-
export class NavigationGenerator {
|
|
5
|
-
private baseUrl: string;
|
|
6
|
-
|
|
7
|
-
constructor(baseUrl: string = '') {
|
|
8
|
-
this.baseUrl = baseUrl;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* 更新 baseUrl
|
|
13
|
-
*/
|
|
14
|
-
setBaseUrl(baseUrl: string): void {
|
|
15
|
-
this.baseUrl = baseUrl;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* 从文件信息生成导航结构
|
|
20
|
-
*/
|
|
21
|
-
generate(files: FileInfo[]): NavigationItem[] {
|
|
22
|
-
// 按路径排序
|
|
23
|
-
const sortedFiles = [...files].sort((a, b) => a.path.localeCompare(b.path));
|
|
24
|
-
|
|
25
|
-
// 构建树形结构
|
|
26
|
-
const root: NavigationItem[] = [];
|
|
27
|
-
|
|
28
|
-
for (const file of sortedFiles) {
|
|
29
|
-
this.addFileToNavigation(root, file);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return root;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 将文件添加到导航树中
|
|
37
|
-
*/
|
|
38
|
-
private addFileToNavigation(navigation: NavigationItem[], file: FileInfo): void {
|
|
39
|
-
const parts = file.path.split('/');
|
|
40
|
-
let currentLevel = navigation;
|
|
41
|
-
|
|
42
|
-
for (let i = 0; i < parts.length; i++) {
|
|
43
|
-
const part = parts[i];
|
|
44
|
-
const isLastPart = i === parts.length - 1;
|
|
45
|
-
const isMarkdownFile = part.endsWith('.md');
|
|
46
|
-
|
|
47
|
-
// 如果是 Markdown 文件,移除扩展名
|
|
48
|
-
const displayName = isMarkdownFile ? part.replace(/\.md$/, '') : part;
|
|
49
|
-
|
|
50
|
-
// 生成标题(对于 Markdown 文件优先使用提取的标题)
|
|
51
|
-
const title =
|
|
52
|
-
isMarkdownFile && file.metadata?.title
|
|
53
|
-
? file.metadata.title
|
|
54
|
-
: this.formatTitle(displayName);
|
|
55
|
-
|
|
56
|
-
// 生成路径
|
|
57
|
-
const rawPath = isMarkdownFile
|
|
58
|
-
? `/${file.path.replace(/\.md$/, '.html')}`
|
|
59
|
-
: `/${parts.slice(0, i + 1).join('/')}`;
|
|
60
|
-
const itemPath = this.generatePath(rawPath);
|
|
61
|
-
|
|
62
|
-
if (isLastPart) {
|
|
63
|
-
// 添加文件节点
|
|
64
|
-
currentLevel.push({
|
|
65
|
-
title,
|
|
66
|
-
path: itemPath,
|
|
67
|
-
});
|
|
68
|
-
} else {
|
|
69
|
-
// 查找或创建目录节点
|
|
70
|
-
// 首先尝试通过路径查找(最准确)
|
|
71
|
-
let dirItem = currentLevel.find(item => item.path === itemPath);
|
|
72
|
-
|
|
73
|
-
// 如果没找到,尝试通过格式化后的标题查找
|
|
74
|
-
if (!dirItem) {
|
|
75
|
-
const formattedTitle = this.formatTitle(displayName);
|
|
76
|
-
dirItem = currentLevel.find(
|
|
77
|
-
item => item.title === formattedTitle && item.children !== undefined
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (!dirItem) {
|
|
82
|
-
dirItem = {
|
|
83
|
-
title: this.formatTitle(displayName),
|
|
84
|
-
path: itemPath,
|
|
85
|
-
children: [],
|
|
86
|
-
};
|
|
87
|
-
currentLevel.push(dirItem);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// 确保 children 存在
|
|
91
|
-
if (!dirItem.children) {
|
|
92
|
-
dirItem.children = [];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// 进入下一层
|
|
96
|
-
currentLevel = dirItem.children;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* 生成带 baseUrl 的路径
|
|
103
|
-
*/
|
|
104
|
-
private generatePath(path: string): string {
|
|
105
|
-
if (!this.baseUrl) {
|
|
106
|
-
return path;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// 确保 baseUrl 不以斜杠结尾,路径以斜杠开头
|
|
110
|
-
const cleanBaseUrl = this.baseUrl.replace(/\/$/, '');
|
|
111
|
-
const cleanPath = path.startsWith('/') ? path : `/${path}`;
|
|
112
|
-
|
|
113
|
-
return `${cleanBaseUrl}${cleanPath}`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* 格式化标题(将连字符/下划线转换为空格并首字母大写)
|
|
118
|
-
*/
|
|
119
|
-
private formatTitle(name: string): string {
|
|
120
|
-
// 移除扩展名
|
|
121
|
-
const baseName = name.replace(/\.[^/.]+$/, '');
|
|
122
|
-
|
|
123
|
-
// 将连字符、下划线、点替换为空格
|
|
124
|
-
const withSpaces = baseName.replace(/[-_.]/g, ' ');
|
|
125
|
-
|
|
126
|
-
// 首字母大写每个单词
|
|
127
|
-
return withSpaces
|
|
128
|
-
.split(' ')
|
|
129
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
130
|
-
.join(' ');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* 生成扁平化导航(所有页面在同一层级)
|
|
135
|
-
*/
|
|
136
|
-
generateFlat(files: FileInfo[]): NavigationItem[] {
|
|
137
|
-
return files
|
|
138
|
-
.map(file => {
|
|
139
|
-
const title = file.metadata?.title || this.formatTitle(file.name); // 优先使用提取的标题
|
|
140
|
-
const rawPath = `/${file.path.replace(/\.md$/, '.html')}`;
|
|
141
|
-
const itemPath = this.generatePath(rawPath);
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
title,
|
|
145
|
-
path: itemPath,
|
|
146
|
-
};
|
|
147
|
-
})
|
|
148
|
-
.sort((a, b) => a.title.localeCompare(b.title));
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* 生成面包屑导航
|
|
153
|
-
*/
|
|
154
|
-
generateBreadcrumbs(filePath: string, navigation: NavigationItem[]): NavigationItem[] {
|
|
155
|
-
const parts = filePath.split('/').filter(part => part);
|
|
156
|
-
const breadcrumbs: NavigationItem[] = [];
|
|
157
|
-
let currentNav = navigation;
|
|
158
|
-
|
|
159
|
-
for (let i = 0; i < parts.length; i++) {
|
|
160
|
-
const part = parts[i];
|
|
161
|
-
const isLast = i === parts.length - 1;
|
|
162
|
-
const searchPath = `/${parts.slice(0, i + 1).join('/')}`;
|
|
163
|
-
|
|
164
|
-
// 在当前层级查找匹配的导航项
|
|
165
|
-
const foundItem = this.findNavigationItem(currentNav, searchPath);
|
|
166
|
-
|
|
167
|
-
if (foundItem) {
|
|
168
|
-
breadcrumbs.push({
|
|
169
|
-
title: foundItem.title,
|
|
170
|
-
path: foundItem.path,
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
if (foundItem.children && !isLast) {
|
|
174
|
-
currentNav = foundItem.children;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return breadcrumbs;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* 在导航树中查找项目
|
|
184
|
-
*/
|
|
185
|
-
private findNavigationItem(
|
|
186
|
-
navigation: NavigationItem[],
|
|
187
|
-
searchPath: string
|
|
188
|
-
): NavigationItem | null {
|
|
189
|
-
for (const item of navigation) {
|
|
190
|
-
if (item.path === searchPath) {
|
|
191
|
-
return item;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (item.children) {
|
|
195
|
-
const found = this.findNavigationItem(item.children, searchPath);
|
|
196
|
-
if (found) {
|
|
197
|
-
return found;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* 生成站点地图 XML
|
|
207
|
-
*/
|
|
208
|
-
generateSitemap(files: FileInfo[], baseUrl?: string): string {
|
|
209
|
-
const effectiveBaseUrl = baseUrl || this.baseUrl || 'https://example.com';
|
|
210
|
-
const urls = files
|
|
211
|
-
.map(file => {
|
|
212
|
-
const path = `/${file.path.replace(/\.md$/, '.html')}`;
|
|
213
|
-
const lastmod = new Date().toISOString().split('T')[0];
|
|
214
|
-
|
|
215
|
-
return ` <url>
|
|
216
|
-
<loc>${effectiveBaseUrl}${path}</loc>
|
|
217
|
-
<lastmod>${lastmod}</lastmod>
|
|
218
|
-
<changefreq>weekly</changefreq>
|
|
219
|
-
<priority>0.8</priority>
|
|
220
|
-
</url>`;
|
|
221
|
-
})
|
|
222
|
-
.join('\n');
|
|
223
|
-
|
|
224
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
225
|
-
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
226
|
-
${urls}
|
|
227
|
-
</urlset>`;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* 生成 JSON 格式的导航数据(用于前端动态加载)
|
|
232
|
-
*/
|
|
233
|
-
generateJsonNavigation(files: FileInfo[]): string {
|
|
234
|
-
const navigation = this.generate(files);
|
|
235
|
-
return JSON.stringify(navigation, null, 2);
|
|
236
|
-
}
|
|
237
|
-
}
|
package/src/scanner.ts
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
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
|
-
}
|