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.
Files changed (153) hide show
  1. package/.github/workflows/pages.yml +1 -1
  2. package/.zen/meta.json +128 -30
  3. package/.zen/src/en-US/01d04f7c17b4a541ead9d759d877b30b403e15b849182a49eb1f62bd29ecd18c.md +120 -0
  4. package/.zen/src/en-US/1b798c44a4f353e47296ca83d5905e37e6aba3e90bbd9bc3b3d34fc12059a2ca.md +75 -0
  5. package/.zen/src/en-US/1e96be58d76c60056b708eb5bd8b8b81d7b5845d9cfe0b879d85068a5f11df3a.md +189 -0
  6. package/.zen/src/en-US/5ec990146b35e00de2630559126ee07f7cdcddeb23b0e8cab3d85b4181353e26.md +53 -0
  7. package/.zen/src/en-US/6124ea88edec5bde737b26b21f71ecfeffe4e73151784856edf813ee231a4baa.md +11 -0
  8. package/.zen/src/en-US/80ae9bed74fc6348a7c1fe9f33e86b65f5d919169721f77bcf0e1bc29fbdb4f9.md +61 -0
  9. package/.zen/src/en-US/f0c2799126931ccd113a0c45b1e623870b0d4f4f400becf6dd877da8f1011517.md +41 -0
  10. package/.zen/src/en-US/fdfca9b960d0eaa8b2b96fe988ead7481d2c0b16f66ebc94fb477139b4178cdc.md +65 -0
  11. package/.zen/src/zh-Hans/01d04f7c17b4a541ead9d759d877b30b403e15b849182a49eb1f62bd29ecd18c.md +120 -0
  12. package/.zen/src/zh-Hans/1b798c44a4f353e47296ca83d5905e37e6aba3e90bbd9bc3b3d34fc12059a2ca.md +77 -0
  13. package/.zen/src/zh-Hans/1e96be58d76c60056b708eb5bd8b8b81d7b5845d9cfe0b879d85068a5f11df3a.md +189 -0
  14. package/.zen/src/zh-Hans/5ec990146b35e00de2630559126ee07f7cdcddeb23b0e8cab3d85b4181353e26.md +55 -0
  15. package/.zen/src/zh-Hans/6124ea88edec5bde737b26b21f71ecfeffe4e73151784856edf813ee231a4baa.md +1 -0
  16. package/.zen/src/zh-Hans/6ad8db715a1b60613fe934fefb29fa981ecad9b63145593accff144d73b44bde.md +175 -0
  17. package/.zen/src/zh-Hans/80ae9bed74fc6348a7c1fe9f33e86b65f5d919169721f77bcf0e1bc29fbdb4f9.md +63 -0
  18. package/.zen/src/zh-Hans/a1580f71c6c6c1ff4a314be72d410a8507af2f087d56360c7f5048d349c21953.md +48 -0
  19. package/.zen/src/zh-Hans/d49012f98c4367b34034063400e2f7826bf0615952210c82396920172d468e2c.md +107 -0
  20. package/.zen/src/zh-Hans/f0c2799126931ccd113a0c45b1e623870b0d4f4f400becf6dd877da8f1011517.md +41 -0
  21. package/.zen/src/zh-Hans/fdfca9b960d0eaa8b2b96fe988ead7481d2c0b16f66ebc94fb477139b4178cdc.md +65 -0
  22. package/assets/templates/default/layout.html +274 -0
  23. package/dist/ai/extractMetadataFromMarkdown.d.ts +8 -0
  24. package/dist/ai/extractMetadataFromMarkdown.d.ts.map +1 -0
  25. package/dist/ai/extractMetadataFromMarkdown.js +88 -0
  26. package/dist/ai/extractMetadataFromMarkdown.js.map +1 -0
  27. package/dist/ai/translateMarkdown.d.ts +8 -0
  28. package/dist/ai/translateMarkdown.d.ts.map +1 -0
  29. package/dist/ai/translateMarkdown.js +29 -0
  30. package/dist/ai/translateMarkdown.js.map +1 -0
  31. package/dist/build/pipeline.d.ts +6 -0
  32. package/dist/build/pipeline.d.ts.map +1 -0
  33. package/dist/build/pipeline.js +218 -0
  34. package/dist/build/pipeline.js.map +1 -0
  35. package/dist/cli.js +10 -118
  36. package/dist/cli.js.map +1 -1
  37. package/dist/findEntries.d.ts +10 -0
  38. package/dist/findEntries.d.ts.map +1 -0
  39. package/dist/findEntries.js +38 -0
  40. package/dist/findEntries.js.map +1 -0
  41. package/dist/index.d.ts +1 -32
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +1 -35
  44. package/dist/index.js.map +1 -1
  45. package/dist/metadata.d.ts +14 -0
  46. package/dist/metadata.d.ts.map +1 -0
  47. package/dist/metadata.js +78 -0
  48. package/dist/metadata.js.map +1 -0
  49. package/dist/paths.d.ts +6 -0
  50. package/dist/paths.d.ts.map +1 -0
  51. package/dist/paths.js +10 -0
  52. package/dist/paths.js.map +1 -0
  53. package/dist/process/extractMetadataByAI.d.ts +5 -0
  54. package/dist/process/extractMetadataByAI.d.ts.map +1 -0
  55. package/dist/process/extractMetadataByAI.js +31 -0
  56. package/dist/process/extractMetadataByAI.js.map +1 -0
  57. package/dist/process/template.d.ts +5 -0
  58. package/dist/process/template.d.ts.map +1 -0
  59. package/dist/process/template.js +188 -0
  60. package/dist/process/template.js.map +1 -0
  61. package/dist/scan/files.d.ts +7 -0
  62. package/dist/scan/files.d.ts.map +1 -0
  63. package/dist/scan/files.js +54 -0
  64. package/dist/scan/files.js.map +1 -0
  65. package/dist/services/openai.d.ts +41 -0
  66. package/dist/services/openai.d.ts.map +1 -0
  67. package/dist/services/openai.js +54 -0
  68. package/dist/services/openai.js.map +1 -0
  69. package/dist/types.d.ts +16 -67
  70. package/dist/types.d.ts.map +1 -1
  71. package/dist/utils/convertMarkdownToHtml.d.ts +7 -0
  72. package/dist/utils/convertMarkdownToHtml.d.ts.map +1 -0
  73. package/dist/utils/convertMarkdownToHtml.js +39 -0
  74. package/dist/utils/convertMarkdownToHtml.js.map +1 -0
  75. package/dist/utils/frontmatter.d.ts +6 -0
  76. package/dist/utils/frontmatter.d.ts.map +1 -0
  77. package/dist/utils/frontmatter.js +22 -0
  78. package/dist/utils/frontmatter.js.map +1 -0
  79. package/docs/deployment/github-pages.md +1 -2
  80. package/docs/guides/best-practices.md +4 -4
  81. package/docs/guides/config.md +0 -5
  82. package/package.json +4 -2
  83. package/src/ai/extractMetadataFromMarkdown.ts +95 -0
  84. package/src/ai/translateMarkdown.ts +29 -0
  85. package/src/build/pipeline.ts +211 -0
  86. package/src/cli.ts +10 -132
  87. package/src/findEntries.ts +37 -0
  88. package/src/index.ts +1 -40
  89. package/src/metadata.ts +44 -0
  90. package/src/paths.ts +7 -0
  91. package/src/process/extractMetadataByAI.ts +29 -0
  92. package/src/process/template.ts +201 -0
  93. package/src/scan/files.ts +17 -0
  94. package/src/services/openai.ts +92 -0
  95. package/src/types.ts +18 -72
  96. package/src/utils/convertMarkdownToHtml.ts +32 -0
  97. package/src/utils/frontmatter.ts +18 -0
  98. package/.zen/translations.json +0 -51
  99. package/dist/ai-client.d.ts +0 -34
  100. package/dist/ai-client.d.ts.map +0 -1
  101. package/dist/ai-client.js +0 -180
  102. package/dist/ai-client.js.map +0 -1
  103. package/dist/ai-processor.d.ts +0 -51
  104. package/dist/ai-processor.d.ts.map +0 -1
  105. package/dist/ai-processor.js +0 -215
  106. package/dist/ai-processor.js.map +0 -1
  107. package/dist/ai-service.d.ts +0 -79
  108. package/dist/ai-service.d.ts.map +0 -1
  109. package/dist/ai-service.js +0 -257
  110. package/dist/ai-service.js.map +0 -1
  111. package/dist/builder.d.ts +0 -70
  112. package/dist/builder.d.ts.map +0 -1
  113. package/dist/builder.js +0 -854
  114. package/dist/builder.js.map +0 -1
  115. package/dist/gitignore.d.ts +0 -41
  116. package/dist/gitignore.d.ts.map +0 -1
  117. package/dist/gitignore.js +0 -202
  118. package/dist/gitignore.js.map +0 -1
  119. package/dist/gitignore.test.d.ts +0 -2
  120. package/dist/gitignore.test.d.ts.map +0 -1
  121. package/dist/gitignore.test.js +0 -309
  122. package/dist/gitignore.test.js.map +0 -1
  123. package/dist/markdown.d.ts +0 -35
  124. package/dist/markdown.d.ts.map +0 -1
  125. package/dist/markdown.js +0 -221
  126. package/dist/markdown.js.map +0 -1
  127. package/dist/navigation.d.ts +0 -46
  128. package/dist/navigation.d.ts.map +0 -1
  129. package/dist/navigation.js +0 -196
  130. package/dist/navigation.js.map +0 -1
  131. package/dist/scanner.d.ts +0 -26
  132. package/dist/scanner.d.ts.map +0 -1
  133. package/dist/scanner.js +0 -190
  134. package/dist/scanner.js.map +0 -1
  135. package/dist/template.d.ts +0 -33
  136. package/dist/template.d.ts.map +0 -1
  137. package/dist/template.js +0 -434
  138. package/dist/template.js.map +0 -1
  139. package/dist/translation-service.d.ts +0 -72
  140. package/dist/translation-service.d.ts.map +0 -1
  141. package/dist/translation-service.js +0 -291
  142. package/dist/translation-service.js.map +0 -1
  143. package/src/ai-client.ts +0 -227
  144. package/src/ai-processor.ts +0 -243
  145. package/src/ai-service.ts +0 -281
  146. package/src/builder.ts +0 -991
  147. package/src/gitignore.test.ts +0 -318
  148. package/src/gitignore.ts +0 -193
  149. package/src/markdown.ts +0 -212
  150. package/src/navigation.ts +0 -237
  151. package/src/scanner.ts +0 -180
  152. package/src/template.ts +0 -425
  153. 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
- }