zengen 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,196 @@
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 * as yaml from 'yaml';
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
+ * 解析 YAML frontmatter
45
+ */
46
+ private parseFrontmatter(content: string): {
47
+ metadata: Record<string, any>;
48
+ content: string;
49
+ } {
50
+ const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/;
51
+ const match = content.match(frontmatterRegex);
52
+
53
+ if (!match) {
54
+ return { metadata: {}, content };
55
+ }
56
+
57
+ try {
58
+ const metadata = yaml.parse(match[1]) || {};
59
+ const remainingContent = content.slice(match[0].length);
60
+ return { metadata, content: remainingContent };
61
+ } catch (error) {
62
+ console.warn('Failed to parse frontmatter:', error);
63
+ return { metadata: {}, content };
64
+ }
65
+ }
66
+
67
+ /**
68
+ * 从内容中提取标题
69
+ */
70
+ private extractTitle(content: string): string {
71
+ // 查找第一个一级标题
72
+ const h1Match = content.match(/^#\s+(.+)$/m);
73
+ if (h1Match) {
74
+ return h1Match[1].trim();
75
+ }
76
+
77
+ // 如果没有一级标题,查找第一个二级标题
78
+ const h2Match = content.match(/^##\s+(.+)$/m);
79
+ if (h2Match) {
80
+ return h2Match[1].trim();
81
+ }
82
+
83
+ return 'Untitled';
84
+ }
85
+
86
+ /**
87
+ * 转换 Markdown 文件
88
+ */
89
+ async convert(fileInfo: FileInfo): Promise<FileInfo> {
90
+ let { content, metadata } = this.parseFrontmatter(fileInfo.content);
91
+
92
+ // 应用前置处理器
93
+ for (const processor of this.processors) {
94
+ if (processor.beforeParse) {
95
+ content = await processor.beforeParse(content, fileInfo);
96
+ }
97
+ }
98
+
99
+ // 转换 Markdown 为 HTML
100
+ let html = marked.parse(content) as string;
101
+
102
+ // 应用后置处理器
103
+ for (const processor of this.processors) {
104
+ if (processor.afterParse) {
105
+ html = await processor.afterParse(html, fileInfo);
106
+ }
107
+ }
108
+
109
+ // 提取标题(如果 metadata 中没有)
110
+ if (!metadata.title) {
111
+ metadata.title = this.extractTitle(content);
112
+ }
113
+
114
+ return {
115
+ ...fileInfo,
116
+ content,
117
+ html,
118
+ metadata
119
+ };
120
+ }
121
+
122
+ /**
123
+ * 批量转换文件
124
+ */
125
+ async convertFiles(files: FileInfo[]): Promise<FileInfo[]> {
126
+ const results: FileInfo[] = [];
127
+
128
+ for (const file of files) {
129
+ try {
130
+ const result = await this.convert(file);
131
+ results.push(result);
132
+ } catch (error) {
133
+ console.error(`Failed to convert file ${file.path}:`, error);
134
+ // 即使转换失败,也保留原始文件信息
135
+ results.push(file);
136
+ }
137
+ }
138
+
139
+ return results;
140
+ }
141
+
142
+ /**
143
+ * 从文件路径读取并转换
144
+ */
145
+ async convertFromPath(filePath: string, baseDir: string = ''): Promise<FileInfo> {
146
+ const content = await fs.readFile(filePath, 'utf-8');
147
+ const relativePath = baseDir ? path.relative(baseDir, filePath) : filePath;
148
+ const ext = path.extname(filePath);
149
+ const name = path.basename(filePath, ext);
150
+
151
+ const fileInfo: FileInfo = {
152
+ path: filePath,
153
+ relativePath,
154
+ name,
155
+ ext,
156
+ content
157
+ };
158
+
159
+ return this.convert(fileInfo);
160
+ }
161
+
162
+ /**
163
+ * 从目录读取所有 Markdown 文件并转换
164
+ */
165
+ async convertDirectory(dirPath: string): Promise<FileInfo[]> {
166
+ const files: FileInfo[] = [];
167
+
168
+ async function scanDirectory(currentPath: string) {
169
+ const entries = await fs.readdir(currentPath, { withFileTypes: true });
170
+
171
+ for (const entry of entries) {
172
+ const fullPath = path.join(currentPath, entry.name);
173
+
174
+ if (entry.isDirectory()) {
175
+ await scanDirectory(fullPath);
176
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
177
+ const content = await fs.readFile(fullPath, 'utf-8');
178
+ const relativePath = path.relative(dirPath, fullPath);
179
+ const ext = path.extname(entry.name);
180
+ const name = path.basename(entry.name, ext);
181
+
182
+ files.push({
183
+ path: fullPath,
184
+ relativePath,
185
+ name,
186
+ ext,
187
+ content
188
+ });
189
+ }
190
+ }
191
+ }
192
+
193
+ await scanDirectory(dirPath);
194
+ return this.convertFiles(files);
195
+ }
196
+ }
@@ -0,0 +1,191 @@
1
+ import { NavigationItem, FileInfo } from './types';
2
+ import * as path from 'path';
3
+
4
+ export class NavigationGenerator {
5
+ /**
6
+ * 从文件信息生成导航结构
7
+ */
8
+ generate(files: FileInfo[]): NavigationItem[] {
9
+ // 按路径排序
10
+ const sortedFiles = [...files].sort((a, b) =>
11
+ a.relativePath.localeCompare(b.relativePath)
12
+ );
13
+
14
+ // 构建树形结构
15
+ const root: NavigationItem[] = [];
16
+
17
+ for (const file of sortedFiles) {
18
+ this.addFileToNavigation(root, file);
19
+ }
20
+
21
+ return root;
22
+ }
23
+
24
+ /**
25
+ * 将文件添加到导航树中
26
+ */
27
+ private addFileToNavigation(navigation: NavigationItem[], file: FileInfo): void {
28
+ const parts = file.relativePath.split('/');
29
+ let currentLevel = navigation;
30
+
31
+ for (let i = 0; i < parts.length; i++) {
32
+ const part = parts[i];
33
+ const isLastPart = i === parts.length - 1;
34
+ const isMarkdownFile = part.endsWith('.md');
35
+
36
+ // 如果是 Markdown 文件,移除扩展名
37
+ const displayName = isMarkdownFile ? part.replace(/\.md$/, '') : part;
38
+
39
+ // 生成标题(使用文件名或 metadata 中的标题)
40
+ const title = file.metadata?.title || this.formatTitle(displayName);
41
+
42
+ // 生成路径
43
+ const itemPath = isMarkdownFile
44
+ ? `/${file.relativePath.replace(/\.md$/, '.html')}`
45
+ : `/${parts.slice(0, i + 1).join('/')}`;
46
+
47
+ if (isLastPart) {
48
+ // 添加文件节点
49
+ currentLevel.push({
50
+ title,
51
+ path: itemPath
52
+ });
53
+ } else {
54
+ // 查找或创建目录节点
55
+ let dirItem = currentLevel.find(item =>
56
+ item.title === displayName && !item.path.endsWith('.html')
57
+ );
58
+
59
+ if (!dirItem) {
60
+ dirItem = {
61
+ title: this.formatTitle(displayName),
62
+ path: itemPath,
63
+ children: []
64
+ };
65
+ currentLevel.push(dirItem);
66
+ }
67
+
68
+ // 确保 children 存在
69
+ if (!dirItem.children) {
70
+ dirItem.children = [];
71
+ }
72
+
73
+ // 进入下一层
74
+ currentLevel = dirItem.children;
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * 格式化标题(将连字符/下划线转换为空格并首字母大写)
81
+ */
82
+ private formatTitle(name: string): string {
83
+ // 移除扩展名
84
+ const baseName = name.replace(/\.[^/.]+$/, '');
85
+
86
+ // 将连字符、下划线、点替换为空格
87
+ const withSpaces = baseName.replace(/[-_.]/g, ' ');
88
+
89
+ // 首字母大写每个单词
90
+ return withSpaces
91
+ .split(' ')
92
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
93
+ .join(' ');
94
+ }
95
+
96
+ /**
97
+ * 生成扁平化导航(所有页面在同一层级)
98
+ */
99
+ generateFlat(files: FileInfo[]): NavigationItem[] {
100
+ return files.map(file => {
101
+ const title = file.metadata?.title || this.formatTitle(file.name);
102
+ const itemPath = `/${file.relativePath.replace(/\.md$/, '.html')}`;
103
+
104
+ return {
105
+ title,
106
+ path: itemPath
107
+ };
108
+ }).sort((a, b) => a.title.localeCompare(b.title));
109
+ }
110
+
111
+ /**
112
+ * 生成面包屑导航
113
+ */
114
+ generateBreadcrumbs(filePath: string, navigation: NavigationItem[]): NavigationItem[] {
115
+ const parts = filePath.split('/').filter(part => part);
116
+ const breadcrumbs: NavigationItem[] = [];
117
+ let currentNav = navigation;
118
+
119
+ for (let i = 0; i < parts.length; i++) {
120
+ const part = parts[i];
121
+ const isLast = i === parts.length - 1;
122
+ const searchPath = `/${parts.slice(0, i + 1).join('/')}`;
123
+
124
+ // 在当前层级查找匹配的导航项
125
+ const foundItem = this.findNavigationItem(currentNav, searchPath);
126
+
127
+ if (foundItem) {
128
+ breadcrumbs.push({
129
+ title: foundItem.title,
130
+ path: foundItem.path
131
+ });
132
+
133
+ if (foundItem.children && !isLast) {
134
+ currentNav = foundItem.children;
135
+ }
136
+ }
137
+ }
138
+
139
+ return breadcrumbs;
140
+ }
141
+
142
+ /**
143
+ * 在导航树中查找项目
144
+ */
145
+ private findNavigationItem(navigation: NavigationItem[], searchPath: string): NavigationItem | null {
146
+ for (const item of navigation) {
147
+ if (item.path === searchPath) {
148
+ return item;
149
+ }
150
+
151
+ if (item.children) {
152
+ const found = this.findNavigationItem(item.children, searchPath);
153
+ if (found) {
154
+ return found;
155
+ }
156
+ }
157
+ }
158
+
159
+ return null;
160
+ }
161
+
162
+ /**
163
+ * 生成站点地图 XML
164
+ */
165
+ generateSitemap(files: FileInfo[], baseUrl: string = 'https://example.com'): string {
166
+ const urls = files.map(file => {
167
+ const path = `/${file.relativePath.replace(/\.md$/, '.html')}`;
168
+ const lastmod = file.metadata?.last_modified || file.metadata?.date || new Date().toISOString().split('T')[0];
169
+
170
+ return ` <url>
171
+ <loc>${baseUrl}${path}</loc>
172
+ <lastmod>${lastmod}</lastmod>
173
+ <changefreq>weekly</changefreq>
174
+ <priority>0.8</priority>
175
+ </url>`;
176
+ }).join('\n');
177
+
178
+ return `<?xml version="1.0" encoding="UTF-8"?>
179
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
180
+ ${urls}
181
+ </urlset>`;
182
+ }
183
+
184
+ /**
185
+ * 生成 JSON 格式的导航数据(用于前端动态加载)
186
+ */
187
+ generateJsonNavigation(files: FileInfo[]): string {
188
+ const navigation = this.generate(files);
189
+ return JSON.stringify(navigation, null, 2);
190
+ }
191
+ }