zengen 0.1.35 → 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 (134) hide show
  1. package/.github/workflows/bump-version.yml +112 -0
  2. package/.github/workflows/ci.yml +2 -2
  3. package/.github/workflows/pages.yml +1 -7
  4. package/.zen/meta.json +155 -0
  5. package/.zen/src/en-US/01d04f7c17b4a541ead9d759d877b30b403e15b849182a49eb1f62bd29ecd18c.md +120 -0
  6. package/.zen/src/en-US/1b798c44a4f353e47296ca83d5905e37e6aba3e90bbd9bc3b3d34fc12059a2ca.md +75 -0
  7. package/.zen/src/en-US/1e96be58d76c60056b708eb5bd8b8b81d7b5845d9cfe0b879d85068a5f11df3a.md +189 -0
  8. package/.zen/src/en-US/5ec990146b35e00de2630559126ee07f7cdcddeb23b0e8cab3d85b4181353e26.md +53 -0
  9. package/.zen/src/en-US/6124ea88edec5bde737b26b21f71ecfeffe4e73151784856edf813ee231a4baa.md +11 -0
  10. package/.zen/src/en-US/80ae9bed74fc6348a7c1fe9f33e86b65f5d919169721f77bcf0e1bc29fbdb4f9.md +61 -0
  11. package/.zen/src/en-US/f0c2799126931ccd113a0c45b1e623870b0d4f4f400becf6dd877da8f1011517.md +41 -0
  12. package/.zen/src/en-US/fdfca9b960d0eaa8b2b96fe988ead7481d2c0b16f66ebc94fb477139b4178cdc.md +65 -0
  13. package/.zen/src/zh-Hans/01d04f7c17b4a541ead9d759d877b30b403e15b849182a49eb1f62bd29ecd18c.md +120 -0
  14. package/.zen/src/zh-Hans/1b798c44a4f353e47296ca83d5905e37e6aba3e90bbd9bc3b3d34fc12059a2ca.md +77 -0
  15. package/.zen/src/zh-Hans/1e96be58d76c60056b708eb5bd8b8b81d7b5845d9cfe0b879d85068a5f11df3a.md +189 -0
  16. package/.zen/src/zh-Hans/5ec990146b35e00de2630559126ee07f7cdcddeb23b0e8cab3d85b4181353e26.md +55 -0
  17. package/.zen/src/zh-Hans/6124ea88edec5bde737b26b21f71ecfeffe4e73151784856edf813ee231a4baa.md +1 -0
  18. package/.zen/src/zh-Hans/6ad8db715a1b60613fe934fefb29fa981ecad9b63145593accff144d73b44bde.md +175 -0
  19. package/.zen/src/zh-Hans/80ae9bed74fc6348a7c1fe9f33e86b65f5d919169721f77bcf0e1bc29fbdb4f9.md +63 -0
  20. package/.zen/src/zh-Hans/a1580f71c6c6c1ff4a314be72d410a8507af2f087d56360c7f5048d349c21953.md +48 -0
  21. package/.zen/src/zh-Hans/d49012f98c4367b34034063400e2f7826bf0615952210c82396920172d468e2c.md +107 -0
  22. package/.zen/src/zh-Hans/f0c2799126931ccd113a0c45b1e623870b0d4f4f400becf6dd877da8f1011517.md +41 -0
  23. package/.zen/src/zh-Hans/fdfca9b960d0eaa8b2b96fe988ead7481d2c0b16f66ebc94fb477139b4178cdc.md +65 -0
  24. package/assets/templates/default/layout.html +274 -0
  25. package/dist/ai/extractMetadataFromMarkdown.d.ts +8 -0
  26. package/dist/ai/extractMetadataFromMarkdown.d.ts.map +1 -0
  27. package/dist/ai/extractMetadataFromMarkdown.js +88 -0
  28. package/dist/ai/extractMetadataFromMarkdown.js.map +1 -0
  29. package/dist/ai/translateMarkdown.d.ts +8 -0
  30. package/dist/ai/translateMarkdown.d.ts.map +1 -0
  31. package/dist/ai/translateMarkdown.js +29 -0
  32. package/dist/ai/translateMarkdown.js.map +1 -0
  33. package/dist/build/pipeline.d.ts +6 -0
  34. package/dist/build/pipeline.d.ts.map +1 -0
  35. package/dist/build/pipeline.js +218 -0
  36. package/dist/build/pipeline.js.map +1 -0
  37. package/dist/cli.js +17 -83
  38. package/dist/cli.js.map +1 -1
  39. package/dist/findEntries.d.ts +10 -0
  40. package/dist/findEntries.d.ts.map +1 -0
  41. package/dist/findEntries.js +38 -0
  42. package/dist/findEntries.js.map +1 -0
  43. package/dist/index.d.ts +1 -32
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +1 -35
  46. package/dist/index.js.map +1 -1
  47. package/dist/metadata.d.ts +14 -0
  48. package/dist/metadata.d.ts.map +1 -0
  49. package/dist/metadata.js +78 -0
  50. package/dist/metadata.js.map +1 -0
  51. package/dist/paths.d.ts +6 -0
  52. package/dist/paths.d.ts.map +1 -0
  53. package/dist/paths.js +10 -0
  54. package/dist/paths.js.map +1 -0
  55. package/dist/process/extractMetadataByAI.d.ts +5 -0
  56. package/dist/process/extractMetadataByAI.d.ts.map +1 -0
  57. package/dist/process/extractMetadataByAI.js +31 -0
  58. package/dist/process/extractMetadataByAI.js.map +1 -0
  59. package/dist/process/template.d.ts +5 -0
  60. package/dist/process/template.d.ts.map +1 -0
  61. package/dist/process/template.js +188 -0
  62. package/dist/process/template.js.map +1 -0
  63. package/dist/scan/files.d.ts +7 -0
  64. package/dist/scan/files.d.ts.map +1 -0
  65. package/dist/scan/files.js +54 -0
  66. package/dist/scan/files.js.map +1 -0
  67. package/dist/services/openai.d.ts +41 -0
  68. package/dist/services/openai.d.ts.map +1 -0
  69. package/dist/services/openai.js +54 -0
  70. package/dist/services/openai.js.map +1 -0
  71. package/dist/types.d.ts +26 -46
  72. package/dist/types.d.ts.map +1 -1
  73. package/dist/utils/convertMarkdownToHtml.d.ts +7 -0
  74. package/dist/utils/convertMarkdownToHtml.d.ts.map +1 -0
  75. package/dist/utils/convertMarkdownToHtml.js +39 -0
  76. package/dist/utils/convertMarkdownToHtml.js.map +1 -0
  77. package/dist/utils/frontmatter.d.ts +6 -0
  78. package/dist/utils/frontmatter.d.ts.map +1 -0
  79. package/dist/utils/frontmatter.js +22 -0
  80. package/dist/utils/frontmatter.js.map +1 -0
  81. package/docs/advanced-usage.md +39 -0
  82. package/docs/deployment/github-pages.md +1 -2
  83. package/docs/getting-started.md +26 -0
  84. package/docs/guides/best-practices.md +4 -117
  85. package/docs/guides/config.md +0 -238
  86. package/package.json +5 -2
  87. package/src/ai/extractMetadataFromMarkdown.ts +95 -0
  88. package/src/ai/translateMarkdown.ts +29 -0
  89. package/src/build/pipeline.ts +211 -0
  90. package/src/cli.ts +18 -94
  91. package/src/findEntries.ts +37 -0
  92. package/src/index.ts +1 -40
  93. package/src/metadata.ts +44 -0
  94. package/src/paths.ts +7 -0
  95. package/src/process/extractMetadataByAI.ts +29 -0
  96. package/src/process/template.ts +201 -0
  97. package/src/scan/files.ts +17 -0
  98. package/src/services/openai.ts +92 -0
  99. package/src/types.ts +29 -47
  100. package/src/utils/convertMarkdownToHtml.ts +32 -0
  101. package/src/utils/frontmatter.ts +18 -0
  102. package/test-multilang.js +44 -0
  103. package/dist/builder.d.ts +0 -46
  104. package/dist/builder.d.ts.map +0 -1
  105. package/dist/builder.js +0 -443
  106. package/dist/builder.js.map +0 -1
  107. package/dist/gitignore.d.ts +0 -40
  108. package/dist/gitignore.d.ts.map +0 -1
  109. package/dist/gitignore.js +0 -184
  110. package/dist/gitignore.js.map +0 -1
  111. package/dist/gitignore.test.d.ts +0 -2
  112. package/dist/gitignore.test.d.ts.map +0 -1
  113. package/dist/gitignore.test.js +0 -244
  114. package/dist/gitignore.test.js.map +0 -1
  115. package/dist/markdown.d.ts +0 -30
  116. package/dist/markdown.d.ts.map +0 -1
  117. package/dist/markdown.js +0 -199
  118. package/dist/markdown.js.map +0 -1
  119. package/dist/navigation.d.ts +0 -46
  120. package/dist/navigation.d.ts.map +0 -1
  121. package/dist/navigation.js +0 -196
  122. package/dist/navigation.js.map +0 -1
  123. package/dist/template.d.ts +0 -29
  124. package/dist/template.d.ts.map +0 -1
  125. package/dist/template.js +0 -385
  126. package/dist/template.js.map +0 -1
  127. package/docs/ci/github-ci-cd.md +0 -127
  128. package/docs/guides/api.md +0 -277
  129. package/src/builder.ts +0 -458
  130. package/src/gitignore.test.ts +0 -253
  131. package/src/gitignore.ts +0 -173
  132. package/src/markdown.ts +0 -184
  133. package/src/navigation.ts +0 -237
  134. package/src/template.ts +0 -365
package/src/gitignore.ts DELETED
@@ -1,173 +0,0 @@
1
- import * as fs from 'fs/promises';
2
- import * as path from 'path';
3
- import { minimatch } from 'minimatch';
4
-
5
- /**
6
- * GitIgnore 处理器
7
- * 用于读取和解析 .gitignore 文件,并提供文件过滤功能
8
- */
9
- export class GitIgnoreProcessor {
10
- private patterns: string[] = [];
11
- private baseDir: string;
12
-
13
- constructor(baseDir: string) {
14
- this.baseDir = baseDir;
15
- }
16
-
17
- /**
18
- * 从 .gitignore 文件加载模式
19
- */
20
- async loadFromFile(gitignorePath: string = '.gitignore'): Promise<void> {
21
- try {
22
- const fullPath = path.join(this.baseDir, gitignorePath);
23
- const content = await fs.readFile(fullPath, 'utf-8');
24
- this.parsePatterns(content);
25
- } catch (error) {
26
- // 如果 .gitignore 文件不存在,使用空模式
27
- console.debug(`No .gitignore file found at ${gitignorePath}, using empty patterns`);
28
- }
29
- }
30
-
31
- /**
32
- * 解析 .gitignore 内容为模式数组
33
- */
34
- private parsePatterns(content: string): void {
35
- const lines = content.split('\n');
36
- this.patterns = [];
37
-
38
- for (let line of lines) {
39
- line = line.trim();
40
-
41
- // 跳过空行和注释
42
- if (!line || line.startsWith('#')) {
43
- continue;
44
- }
45
-
46
- // 处理否定模式(以 ! 开头)
47
- if (line.startsWith('!')) {
48
- // 暂时不支持否定模式,可以后续扩展
49
- continue;
50
- }
51
-
52
- // 标准化模式
53
- let pattern = line;
54
-
55
- // 如果模式以 / 结尾,表示目录
56
- if (pattern.endsWith('/')) {
57
- pattern = pattern.slice(0, -1);
58
- }
59
-
60
- // 如果模式不以 * 开头,添加 **/ 前缀以匹配任意子目录
61
- if (!pattern.includes('*') && !pattern.startsWith('/')) {
62
- pattern = `**/${pattern}`;
63
- }
64
-
65
- // 如果模式以 / 开头,从根目录开始匹配
66
- if (pattern.startsWith('/')) {
67
- pattern = pattern.slice(1);
68
- }
69
-
70
- this.patterns.push(pattern);
71
- }
72
-
73
- console.debug(`Loaded ${this.patterns.length} patterns from .gitignore`);
74
- }
75
-
76
- /**
77
- * 检查文件是否应该被忽略
78
- * @param filePath 文件的绝对路径
79
- * @returns 如果应该被忽略返回 true
80
- */
81
- shouldIgnore(filePath: string): boolean {
82
- // 获取相对于 baseDir 的相对路径
83
- const relativePath = path.relative(this.baseDir, filePath);
84
-
85
- // 如果文件在 baseDir 之外,不应用规则
86
- if (relativePath.startsWith('..')) {
87
- return false;
88
- }
89
-
90
- // 标准化路径分隔符为 /
91
- const normalizedPath = relativePath.replace(/\\/g, '/');
92
-
93
- // 检查每个模式
94
- for (const pattern of this.patterns) {
95
- // 首先尝试直接匹配
96
- if (minimatch(normalizedPath, pattern, { dot: true })) {
97
- return true;
98
- }
99
-
100
- // 对于目录模式,检查文件是否在该目录下
101
- // 目录模式通常以 / 结尾或没有扩展名
102
- if (pattern.endsWith('/') || (!pattern.includes('.') && !pattern.includes('*'))) {
103
- const dirPattern = pattern.endsWith('/') ? pattern.slice(0, -1) : pattern;
104
-
105
- // 检查文件是否在目录中
106
- if (normalizedPath.startsWith(dirPattern + '/')) {
107
- return true;
108
- }
109
-
110
- // 检查是否就是目录本身
111
- if (normalizedPath === dirPattern) {
112
- return true;
113
- }
114
- }
115
-
116
- // 对于包含通配符的目录模式(如 **/node_modules, **/.vscode)
117
- // 提取目录名并检查
118
- if (pattern.includes('*')) {
119
- // 尝试提取目录名(最后一个非通配符部分)
120
- const parts = pattern.split('/');
121
- const lastPart = parts[parts.length - 1];
122
-
123
- // 如果最后一部分不包含通配符,可能是目录名
124
- // 注意:.vscode 以点开头,但仍然是目录名
125
- if (!lastPart.includes('*')) {
126
- // 检查是否是文件扩展名(包含点但不是以点开头的目录)
127
- const hasFileExtension = lastPart.includes('.') && !lastPart.startsWith('.');
128
-
129
- if (!hasFileExtension) {
130
- if (normalizedPath.startsWith(lastPart + '/')) {
131
- return true;
132
- }
133
- if (normalizedPath === lastPart) {
134
- return true;
135
- }
136
- }
137
- }
138
- }
139
- }
140
-
141
- return false;
142
- }
143
-
144
- /**
145
- * 获取所有忽略模式
146
- */
147
- getPatterns(): string[] {
148
- return [...this.patterns];
149
- }
150
-
151
- /**
152
- * 添加自定义忽略模式
153
- */
154
- addPattern(pattern: string): void {
155
- this.patterns.push(pattern);
156
- }
157
-
158
- /**
159
- * 清除所有模式
160
- */
161
- clearPatterns(): void {
162
- this.patterns = [];
163
- }
164
- }
165
-
166
- /**
167
- * 创建 GitIgnoreProcessor 实例并加载 .gitignore 文件
168
- */
169
- export async function createGitIgnoreProcessor(baseDir: string): Promise<GitIgnoreProcessor> {
170
- const processor = new GitIgnoreProcessor(baseDir);
171
- await processor.loadFromFile();
172
- return processor;
173
- }
package/src/markdown.ts DELETED
@@ -1,184 +0,0 @@
1
- import { marked } from 'marked';
2
- import hljs from 'highlight.js';
3
- import { FileInfo, MarkdownProcessor } 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: filePath,
127
- relativePath,
128
- name,
129
- ext,
130
- content,
131
- };
132
-
133
- return this.convert(fileInfo);
134
- }
135
-
136
- /**
137
- * 从目录读取所有 Markdown 文件并转换
138
- */
139
- async convertDirectory(dirPath: string): Promise<FileInfo[]> {
140
- const files: FileInfo[] = [];
141
-
142
- // 创建 GitIgnoreProcessor 并加载 .gitignore 文件
143
- const gitignoreProcessor = new GitIgnoreProcessor(dirPath);
144
- await gitignoreProcessor.loadFromFile();
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 content = await fs.readFile(fullPath, 'utf-8');
166
- const relativePath = path.relative(dirPath, fullPath);
167
- const ext = path.extname(entry.name);
168
- const name = path.basename(entry.name, ext);
169
-
170
- files.push({
171
- path: fullPath,
172
- relativePath,
173
- name,
174
- ext,
175
- content,
176
- });
177
- }
178
- }
179
- }
180
-
181
- await scanDirectory(dirPath);
182
- return this.convertFiles(files);
183
- }
184
- }
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.relativePath.localeCompare(b.relativePath));
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.relativePath.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.relativePath.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.relativePath.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.relativePath.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
- }