czon 0.5.7 → 0.6.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/dist/build/pipeline.js +24 -13
- package/dist/process/category.js +5 -9
- package/dist/process/enhanceMarkdownSource.js +3 -24
- package/dist/process/processTranslations.js +4 -4
- package/dist/process/scanSourceFiles.js +7 -4
- package/dist/process/template.js +8 -40
- package/dist/services/opencode.js +8 -0
- package/dist/ssg/ContentPage.js +5 -5
- package/dist/ssg/IndexPage.js +1 -2
- package/dist/ssg/app.js +1 -1
- package/dist/ssg/components/ContentMeta.js +1 -1
- package/dist/ssg/components/Navigator.js +3 -3
- package/dist/utils/convertMarkdownToHtml.js +60 -1
- package/package.json +1 -1
package/dist/build/pipeline.js
CHANGED
|
@@ -73,24 +73,35 @@ async function buildPipeline(options) {
|
|
|
73
73
|
// 清理输出目录
|
|
74
74
|
await fs.rm(paths_1.CZON_DIST_DIR, { recursive: true, force: true });
|
|
75
75
|
// 确保 .czon/.gitignore 文件
|
|
76
|
-
await (0, writeFile_1.writeFile)(path.join(paths_1.CZON_DIR, '.gitignore'),
|
|
76
|
+
await (0, writeFile_1.writeFile)(path.join(paths_1.CZON_DIR, '.gitignore'), [
|
|
77
|
+
'dist',
|
|
78
|
+
'tmp',
|
|
79
|
+
// 忽略所有非 md 文件: 先忽略所有文件,再排除 Markdown 文件不忽略
|
|
80
|
+
'src/**/*.*',
|
|
81
|
+
'!src/**/*.md',
|
|
82
|
+
].join('\n'));
|
|
77
83
|
// 扫描源文件
|
|
78
84
|
await (0, scanSourceFiles_1.scanSourceFiles)();
|
|
79
|
-
//
|
|
85
|
+
// 链接资源文件 (非翻译文件)
|
|
80
86
|
for (const file of metadata_1.MetaData.files) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const targetPath = path.join(paths_1.
|
|
87
|
+
if (file.path.endsWith('.md'))
|
|
88
|
+
continue; // 仅处理非 Markdown 文件
|
|
89
|
+
for (const lang of metadata_1.MetaData.options.langs || []) {
|
|
90
|
+
// 创建硬链接以节省磁盘空间
|
|
91
|
+
const targetPath = path.join(paths_1.CZON_SRC_DIR, lang, file.path);
|
|
86
92
|
const sourcePath = path.join(paths_1.INPUT_DIR, file.path);
|
|
87
|
-
console.info(
|
|
88
|
-
|
|
89
|
-
await (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
console.warn(`⚠️ Failed to write raw content for file ${file.path}:`, error);
|
|
93
|
+
console.info(`🔗 Linking file ${file.path} to ${targetPath} ...`);
|
|
94
|
+
// 确保 link 成功
|
|
95
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
96
|
+
await fs.rm(targetPath, { force: true });
|
|
97
|
+
await fs.link(sourcePath, targetPath);
|
|
93
98
|
}
|
|
99
|
+
// 拷贝一份到 __raw__ 目录以供 dist 使用
|
|
100
|
+
const rawTargetPath = path.join(paths_1.CZON_DIST_RAW_CONTENT_DIR, file.path);
|
|
101
|
+
const rawSourcePath = path.join(paths_1.INPUT_DIR, file.path);
|
|
102
|
+
console.info(`📄 Copying raw file ${file.path} to ${rawTargetPath} ...`);
|
|
103
|
+
await fs.mkdir(path.dirname(rawTargetPath), { recursive: true });
|
|
104
|
+
await fs.copyFile(rawSourcePath, rawTargetPath);
|
|
94
105
|
}
|
|
95
106
|
// 运行 AI 元数据提取
|
|
96
107
|
await (0, extractMetadataByAI_1.extractMetadataByAI)();
|
package/dist/process/category.js
CHANGED
|
@@ -6,7 +6,6 @@ const openai_1 = require("../services/openai");
|
|
|
6
6
|
const formatFileForCategoryExtraction = (file) => {
|
|
7
7
|
return [
|
|
8
8
|
//
|
|
9
|
-
`Hash: ${file.hash}`,
|
|
10
9
|
`Path: ${file.path}`,
|
|
11
10
|
`Metadata: ${JSON.stringify(file.metadata)}`,
|
|
12
11
|
].join('\n');
|
|
@@ -19,14 +18,11 @@ const processExtractCategory = async () => {
|
|
|
19
18
|
console.info('ℹ️ All files already have categories, skipping category extraction.');
|
|
20
19
|
return;
|
|
21
20
|
}
|
|
22
|
-
// 这意味着,只要有一个新文件,就需要重新生成所有类别标签
|
|
23
|
-
// 这倒也合理,因为类别标签是整体相关的
|
|
24
|
-
// 如果是内容改动导致的呢?
|
|
25
21
|
const markdownFiles = metadata_1.MetaData.files.filter(f => f.path.endsWith('.md') && f.metadata);
|
|
26
22
|
if (verbose) {
|
|
27
23
|
console.info(`📂 Extracting categories for ${markdownFiles.length} markdown files...`);
|
|
28
24
|
for (const file of markdownFiles) {
|
|
29
|
-
console.info(` - File: ${file.path}
|
|
25
|
+
console.info(` - File: ${file.path}`);
|
|
30
26
|
}
|
|
31
27
|
}
|
|
32
28
|
// 提取类别标签列表
|
|
@@ -46,7 +42,7 @@ const processExtractCategory = async () => {
|
|
|
46
42
|
'请优先考虑尚未被分类的文件。',
|
|
47
43
|
'请以 JSON 格式返回类别标签列表。',
|
|
48
44
|
'示例输出格式:',
|
|
49
|
-
'{ "mappings": { "
|
|
45
|
+
'{ "mappings": { "path1": "tag1", "path2": "tag2" } }',
|
|
50
46
|
].join('\n'),
|
|
51
47
|
},
|
|
52
48
|
{
|
|
@@ -66,9 +62,9 @@ const processExtractCategory = async () => {
|
|
|
66
62
|
const json = categories.choices[0].message.content;
|
|
67
63
|
const parsed = JSON.parse(json);
|
|
68
64
|
for (const file of metadata_1.MetaData.files) {
|
|
69
|
-
const
|
|
70
|
-
if (parsed.mappings[
|
|
71
|
-
file.category = parsed.mappings[
|
|
65
|
+
const path = file.path;
|
|
66
|
+
if (parsed.mappings[path]) {
|
|
67
|
+
file.category = parsed.mappings[path];
|
|
72
68
|
}
|
|
73
69
|
}
|
|
74
70
|
console.info('✅ Extracted categories:', categories.choices[0].message.content);
|
|
@@ -10,25 +10,6 @@ const metadata_1 = require("../metadata");
|
|
|
10
10
|
const paths_1 = require("../paths");
|
|
11
11
|
const frontmatter_1 = require("../utils/frontmatter");
|
|
12
12
|
const writeFile_1 = require("../utils/writeFile");
|
|
13
|
-
const replaceInnerLinks = (file, markdownContent) => {
|
|
14
|
-
let content = markdownContent;
|
|
15
|
-
for (const link of file.links) {
|
|
16
|
-
if (URL.canParse(link))
|
|
17
|
-
continue; // 跳过绝对 URL
|
|
18
|
-
const targetPath = path_1.default.resolve('/', path_1.default.dirname(file.path), link).slice(1);
|
|
19
|
-
const targetFile = metadata_1.MetaData.files.find(f => f.path === targetPath);
|
|
20
|
-
if (!targetFile) {
|
|
21
|
-
console.warn(`⚠️ Link target not found for ${link} in file ${file.path}`);
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
// 替换链接 (使用相对链接)
|
|
25
|
-
const targetLink = `czon://${targetFile.hash}`;
|
|
26
|
-
// 全局替换链接
|
|
27
|
-
const linksRegex = new RegExp(`\\]\\(${link.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\)`, 'g');
|
|
28
|
-
content = content.replace(linksRegex, `](${targetLink})`);
|
|
29
|
-
}
|
|
30
|
-
return content;
|
|
31
|
-
};
|
|
32
13
|
/**
|
|
33
14
|
* 存储母语文件到 .czon/src
|
|
34
15
|
*/
|
|
@@ -41,11 +22,9 @@ async function storeNativeFiles() {
|
|
|
41
22
|
continue;
|
|
42
23
|
}
|
|
43
24
|
try {
|
|
44
|
-
if (!file.hash)
|
|
45
|
-
throw new Error(`Missing hash`);
|
|
46
25
|
if (!file.metadata?.inferred_lang)
|
|
47
26
|
throw new Error(`Missing inferred language`);
|
|
48
|
-
const filePath = path_1.default.join(paths_1.CZON_SRC_DIR, file.metadata.inferred_lang, file.
|
|
27
|
+
const filePath = path_1.default.join(paths_1.CZON_SRC_DIR, file.metadata.inferred_lang, file.path);
|
|
49
28
|
const originalContent = await (0, promises_1.readFile)(path_1.default.join(paths_1.INPUT_DIR, file.path), 'utf-8');
|
|
50
29
|
// 增强 YAML Frontmatter
|
|
51
30
|
const enhancedContent = (0, frontmatter_1.updateFrontmatter)(originalContent, {
|
|
@@ -55,8 +34,8 @@ async function storeNativeFiles() {
|
|
|
55
34
|
date: file.metadata.inferred_date,
|
|
56
35
|
});
|
|
57
36
|
// 进行内链接替换, 将相对链接替换为基于 czon://hash 的链接
|
|
58
|
-
const replacedContent = replaceInnerLinks(file, enhancedContent);
|
|
59
|
-
await (0, writeFile_1.writeFile)(filePath,
|
|
37
|
+
// const replacedContent = replaceInnerLinks(file, enhancedContent);
|
|
38
|
+
await (0, writeFile_1.writeFile)(filePath, enhancedContent);
|
|
60
39
|
}
|
|
61
40
|
catch (error) {
|
|
62
41
|
console.warn(`⚠️ Failed to store native file ${file.path}:`, error);
|
|
@@ -106,8 +106,8 @@ async function processTranslations() {
|
|
|
106
106
|
if (verbose)
|
|
107
107
|
console.log(`🌐 Translating to ${lang}...`);
|
|
108
108
|
// 存储翻译文件到 .czon/src/{lang}
|
|
109
|
-
const sourcePath = path_1.default.join(paths_1.CZON_SRC_DIR, file.metadata.inferred_lang, file.
|
|
110
|
-
const targetPath = path_1.default.join(paths_1.CZON_SRC_DIR, lang, file.
|
|
109
|
+
const sourcePath = path_1.default.join(paths_1.CZON_SRC_DIR, file.metadata.inferred_lang, file.path); // 使用已经加强的母语文件路径
|
|
110
|
+
const targetPath = path_1.default.join(paths_1.CZON_SRC_DIR, lang, file.path);
|
|
111
111
|
try {
|
|
112
112
|
const content = await (0, promises_1.readFile)(sourcePath, 'utf-8');
|
|
113
113
|
if (file.metadata.inferred_lang === lang) {
|
|
@@ -122,8 +122,8 @@ async function processTranslations() {
|
|
|
122
122
|
console.info(`ℹ️ Content unchanged for ${file.path}, skipping translation.`);
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
|
-
|
|
126
|
-
await translateWithOpenCode(sourcePath, targetPath, lang);
|
|
125
|
+
await translateWithLLMCall(sourcePath, targetPath, lang);
|
|
126
|
+
// await translateWithOpenCode(sourcePath, targetPath, lang);
|
|
127
127
|
// const translationMeta = ((file.translations ??= {})[lang] ??= {});
|
|
128
128
|
// translationMeta.content_length = translatedContent.length; // 记录翻译后内容长度
|
|
129
129
|
// translationMeta.token_used = translatedResponse.usage; // 记录 token 使用情况
|
|
@@ -30,7 +30,7 @@ async function scanSourceFiles() {
|
|
|
30
30
|
for (const filePath of markdownFiles) {
|
|
31
31
|
queue.push(filePath);
|
|
32
32
|
}
|
|
33
|
-
const
|
|
33
|
+
const paths = new Set();
|
|
34
34
|
while (queue.length > 0) {
|
|
35
35
|
const relativePath = queue.shift();
|
|
36
36
|
const fullPath = path_1.default.join(paths_1.INPUT_DIR, relativePath);
|
|
@@ -51,12 +51,15 @@ async function scanSourceFiles() {
|
|
|
51
51
|
}
|
|
52
52
|
const contentBuffer = await (0, promises_1.readFile)(fullPath);
|
|
53
53
|
const hash = (0, sha256_1.sha256)(contentBuffer);
|
|
54
|
-
|
|
55
|
-
let meta = metadata_1.MetaData.files.find(f => f.
|
|
54
|
+
paths.add(relativePath);
|
|
55
|
+
let meta = metadata_1.MetaData.files.find(f => f.path === relativePath);
|
|
56
56
|
if (!meta) {
|
|
57
57
|
meta = { hash, path: relativePath, links: [] };
|
|
58
58
|
metadata_1.MetaData.files.push(meta);
|
|
59
59
|
}
|
|
60
|
+
else {
|
|
61
|
+
meta.hash = hash;
|
|
62
|
+
}
|
|
60
63
|
// 处理 Markdown 文件
|
|
61
64
|
if (fullPath.endsWith('.md')) {
|
|
62
65
|
const content = contentBuffer.toString('utf-8');
|
|
@@ -76,7 +79,7 @@ async function scanSourceFiles() {
|
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
81
|
// 移除不再存在的文件元数据
|
|
79
|
-
metadata_1.MetaData.files = metadata_1.MetaData.files.filter(f =>
|
|
82
|
+
metadata_1.MetaData.files = metadata_1.MetaData.files.filter(f => paths.has(f.path));
|
|
80
83
|
// 按路径降序排序 (通常外层目录优先)
|
|
81
84
|
metadata_1.MetaData.files.sort((a, b) =>
|
|
82
85
|
// 第一级按目录排序
|
package/dist/process/template.js
CHANGED
|
@@ -56,17 +56,17 @@ const spiderStaticSiteGenerator = async () => {
|
|
|
56
56
|
}
|
|
57
57
|
const isVisited = new Set();
|
|
58
58
|
const contents = [];
|
|
59
|
-
// 预加载所有 Markdown
|
|
59
|
+
// 预加载所有 Markdown 内容,因为 React 内部异步渲染比较麻烦
|
|
60
60
|
for (const file of metadata_1.MetaData.files) {
|
|
61
61
|
if (!file.path.endsWith('.md'))
|
|
62
62
|
continue;
|
|
63
63
|
for (const lang of metadata_1.MetaData.options.langs || []) {
|
|
64
|
-
const markdown = await fs.readFile(path.join(paths_1.CZON_SRC_DIR, lang, file.
|
|
64
|
+
const markdown = await fs.readFile(path.join(paths_1.CZON_SRC_DIR, lang, file.path), 'utf-8');
|
|
65
65
|
const { frontmatter, body } = (0, frontmatter_1.parseFrontmatter)(markdown);
|
|
66
|
-
const markdownHtml = (0, convertMarkdownToHtml_1.convertMarkdownToHtml)(body);
|
|
66
|
+
const markdownHtml = (0, convertMarkdownToHtml_1.convertMarkdownToHtml)(file.path, lang, body);
|
|
67
67
|
contents.push({
|
|
68
68
|
lang,
|
|
69
|
-
|
|
69
|
+
file,
|
|
70
70
|
body: markdownHtml,
|
|
71
71
|
frontmatter,
|
|
72
72
|
});
|
|
@@ -83,42 +83,6 @@ const spiderStaticSiteGenerator = async () => {
|
|
|
83
83
|
site: metadata_1.MetaData,
|
|
84
84
|
contents,
|
|
85
85
|
});
|
|
86
|
-
// 内部链接: czon://hash 格式的链接替换为 /{lang}/{slug}.html
|
|
87
|
-
html = html.replace(/href="([^"]+)"/g, (match, link) => {
|
|
88
|
-
console.info(`🕷️ Processing link: ${link} in path: ${currentPath}`);
|
|
89
|
-
if (link.startsWith('czon://')) {
|
|
90
|
-
const hash = link.replace('czon://', '');
|
|
91
|
-
console.info(` 🔗 Replacing internal link for hash: ${hash}`);
|
|
92
|
-
const file = metadata_1.MetaData.files.find(f => f.hash === hash);
|
|
93
|
-
if (!file || !file.metadata) {
|
|
94
|
-
console.warn(`⚠️ Link target not found for hash ${hash} in path ${currentPath}`);
|
|
95
|
-
return match;
|
|
96
|
-
}
|
|
97
|
-
const slug = file.metadata.slug;
|
|
98
|
-
const targetPath = path.resolve('/', path.dirname(currentPath), `${slug}.html`);
|
|
99
|
-
const href = path.relative(path.dirname(currentPath), targetPath);
|
|
100
|
-
return `href="${href}"`;
|
|
101
|
-
}
|
|
102
|
-
return match;
|
|
103
|
-
});
|
|
104
|
-
// 替换 src 中的 czon://hash 链接
|
|
105
|
-
html = html.replace(/src="([^"]+)"/g, (match, link) => {
|
|
106
|
-
console.info(`🕷️ Processing src link: ${link} in path: ${currentPath}`);
|
|
107
|
-
if (link.startsWith('czon://')) {
|
|
108
|
-
const hash = link.replace('czon://', '');
|
|
109
|
-
console.info(` 🔗 Replacing internal src link for hash: ${hash}`);
|
|
110
|
-
const file = metadata_1.MetaData.files.find(f => f.hash === hash);
|
|
111
|
-
if (!file) {
|
|
112
|
-
console.warn(`⚠️ Src link target not found for hash ${hash} in path ${currentPath}`);
|
|
113
|
-
return match;
|
|
114
|
-
}
|
|
115
|
-
const ext = path.extname(file.path);
|
|
116
|
-
const targetPath = path.join(paths_1.CZON_DIST_RAW_CONTENT_DIR, file.hash + ext);
|
|
117
|
-
const href = path.relative(path.join(paths_1.CZON_DIST_DIR, path.dirname(currentPath)), targetPath);
|
|
118
|
-
return `src="${href}"`;
|
|
119
|
-
}
|
|
120
|
-
return match;
|
|
121
|
-
});
|
|
122
86
|
console.info(`🕷️ Crawled ${currentPath}`);
|
|
123
87
|
// 收集 URL 用于 sitemap
|
|
124
88
|
const urlMatch = currentPath.match(/^\/([^/]+)\/(.+)\.html$/);
|
|
@@ -143,7 +107,11 @@ const spiderStaticSiteGenerator = async () => {
|
|
|
143
107
|
const link = match[1];
|
|
144
108
|
if (URL.canParse(link))
|
|
145
109
|
continue; // 跳过绝对 URL
|
|
110
|
+
if (link.startsWith('#'))
|
|
111
|
+
continue; // 跳过页面内锚点链接
|
|
146
112
|
const resolvedPath = path.resolve('/', path.dirname(currentPath), link);
|
|
113
|
+
if (resolvedPath.startsWith('/__raw__/'))
|
|
114
|
+
continue; // 跳过原始内容目录
|
|
147
115
|
console.info(` ➕ Found link: ${link} -> ${resolvedPath} (${isVisited.has(resolvedPath) ? 'visited' : 'new'})`);
|
|
148
116
|
if (!isVisited.has(resolvedPath)) {
|
|
149
117
|
queue.push(resolvedPath);
|
|
@@ -70,6 +70,14 @@ const runOpenCode = (prompt, options) => {
|
|
|
70
70
|
const session = await client.session.create();
|
|
71
71
|
if (!session.data?.id)
|
|
72
72
|
throw new Error('Failed to create OpenCode session', { cause: session.error });
|
|
73
|
+
options?.signal?.addEventListener('abort', () => {
|
|
74
|
+
console.info(`🛑 Aborting OpenCode session ${session.data.id}...`);
|
|
75
|
+
client.session.abort({
|
|
76
|
+
path: {
|
|
77
|
+
id: session.data.id,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
});
|
|
73
81
|
const directoryBase64 = Buffer.from(session.data.directory).toString('base64');
|
|
74
82
|
const url = `${baseUrl}/${directoryBase64}/session/${session.data.id}`;
|
|
75
83
|
console.info('OpenCode Session Created', url);
|
package/dist/ssg/ContentPage.js
CHANGED
|
@@ -21,7 +21,7 @@ const ContentPage = props => {
|
|
|
21
21
|
const date = frontmatter.date || '--';
|
|
22
22
|
const tags = frontmatter.tags || [];
|
|
23
23
|
const category = props.file.category;
|
|
24
|
-
const relatedContents = props.ctx.site.files.filter(f => f.category === category && f.
|
|
24
|
+
const relatedContents = props.ctx.site.files.filter(f => f.category === category && f.path !== props.file.path);
|
|
25
25
|
// 查找指向当前文章的其他文章
|
|
26
26
|
const thisPath = (0, node_path_1.resolve)('/', props.file.path);
|
|
27
27
|
const referencedFiles = props.ctx.site.files.filter(f => f.links.some(link => (0, node_path_1.resolve)('/', (0, node_path_1.dirname)(f.path), link) === thisPath));
|
|
@@ -56,15 +56,15 @@ const ContentPage = props => {
|
|
|
56
56
|
relatedContents.length > 0 && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
57
57
|
react_1.default.createElement("h2", null, "See Also"),
|
|
58
58
|
react_1.default.createElement("ul", null, relatedContents.map(f => {
|
|
59
|
-
const theContent = props.ctx.contents.find(c => c.lang === props.lang && c.
|
|
60
|
-
return (react_1.default.createElement("li", { key: f.
|
|
59
|
+
const theContent = props.ctx.contents.find(c => c.lang === props.lang && c.file === f);
|
|
60
|
+
return (react_1.default.createElement("li", { key: f.path },
|
|
61
61
|
react_1.default.createElement("a", { href: `${f.metadata?.slug}.html` }, theContent?.frontmatter.title)));
|
|
62
62
|
})))),
|
|
63
63
|
referencedFiles.length > 0 && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
64
64
|
react_1.default.createElement("h2", null, "Referenced By"),
|
|
65
65
|
react_1.default.createElement("ul", null, referencedFiles.map(f => {
|
|
66
|
-
const theContent = props.ctx.contents.find(c => c.lang === props.lang && c.
|
|
67
|
-
return (react_1.default.createElement("li", { key: f.
|
|
66
|
+
const theContent = props.ctx.contents.find(c => c.lang === props.lang && c.file === f);
|
|
67
|
+
return (react_1.default.createElement("li", { key: f.path },
|
|
68
68
|
react_1.default.createElement("a", { href: `${f.metadata?.slug}.html` }, theContent?.frontmatter.title)));
|
|
69
69
|
}))))),
|
|
70
70
|
react_1.default.createElement("footer", { className: "footer" },
|
package/dist/ssg/IndexPage.js
CHANGED
|
@@ -64,8 +64,7 @@ const IndexPage = props => {
|
|
|
64
64
|
")"))));
|
|
65
65
|
}))),
|
|
66
66
|
react_1.default.createElement("div", null, contents.map(file => {
|
|
67
|
-
|
|
68
|
-
return (react_1.default.createElement("div", { className: "mb-6", key: file.hash },
|
|
67
|
+
return (react_1.default.createElement("div", { className: "mb-6", key: file.path },
|
|
69
68
|
react_1.default.createElement(ContentMeta_1.ContentMeta, { ctx: props.ctx, file: file, lang: props.lang })));
|
|
70
69
|
})),
|
|
71
70
|
react_1.default.createElement("footer", null,
|
package/dist/ssg/app.js
CHANGED
|
@@ -28,7 +28,7 @@ const App = (props) => {
|
|
|
28
28
|
// 渲染文章页面
|
|
29
29
|
for (const file of props.site.files) {
|
|
30
30
|
if (props.path === `/${lang}/${file.metadata?.slug}.html`) {
|
|
31
|
-
const theContent = props.contents.find(c => c.lang === lang && c.
|
|
31
|
+
const theContent = props.contents.find(c => c.lang === lang && c.file === file);
|
|
32
32
|
if (!theContent)
|
|
33
33
|
return react_1.default.createElement(RedirectPage_1.RedirectPage, { from: props.path, to: `/index.html` });
|
|
34
34
|
return react_1.default.createElement(ContentPage_1.ContentPage, { ctx: props, file: file, lang: lang, content: theContent });
|
|
@@ -7,7 +7,7 @@ exports.ContentMeta = void 0;
|
|
|
7
7
|
const react_1 = __importDefault(require("react"));
|
|
8
8
|
const TagList_1 = require("./TagList");
|
|
9
9
|
const ContentMeta = props => {
|
|
10
|
-
const content = props.ctx.contents.find(c => c.
|
|
10
|
+
const content = props.ctx.contents.find(c => c.file === props.file && c.lang === props.lang);
|
|
11
11
|
const frontmatter = content?.frontmatter || {};
|
|
12
12
|
const title = frontmatter.title;
|
|
13
13
|
const summary = frontmatter.summary;
|
|
@@ -22,10 +22,10 @@ const Navigator = props => {
|
|
|
22
22
|
react_1.default.createElement("li", { className: "nav-item font-bold", key: categoryKey }, category || '--'),
|
|
23
23
|
filesInCategory.map(file => {
|
|
24
24
|
const link = file.metadata.slug + '.html';
|
|
25
|
-
const isActive = props.file
|
|
26
|
-
const theContent = props.ctx.contents.find(c => c.lang === props.lang && c.
|
|
25
|
+
const isActive = props.file === file;
|
|
26
|
+
const theContent = props.ctx.contents.find(c => c.lang === props.lang && c.file === file);
|
|
27
27
|
const theTitle = theContent?.frontmatter?.title || file.metadata.title || '(no title)';
|
|
28
|
-
return (react_1.default.createElement("li", { className: "nav-item", key: file.
|
|
28
|
+
return (react_1.default.createElement("li", { className: "nav-item", key: file.path },
|
|
29
29
|
react_1.default.createElement("a", { href: link, className: `nav-link ${isActive ? 'active' : ''}` },
|
|
30
30
|
theTitle,
|
|
31
31
|
" ",
|
|
@@ -8,6 +8,8 @@ const highlight_js_1 = __importDefault(require("highlight.js"));
|
|
|
8
8
|
const marked_1 = require("marked");
|
|
9
9
|
const marked_katex_extension_1 = __importDefault(require("marked-katex-extension"));
|
|
10
10
|
const marked_footnote_1 = __importDefault(require("marked-footnote"));
|
|
11
|
+
const metadata_1 = require("../metadata");
|
|
12
|
+
const path_1 = require("path");
|
|
11
13
|
// 辅助函数:转义 HTML 特殊字符
|
|
12
14
|
function escapeHtml(unsafe) {
|
|
13
15
|
return unsafe
|
|
@@ -24,10 +26,67 @@ marked_1.marked.use((0, marked_footnote_1.default)());
|
|
|
24
26
|
* @param mdContent Markdown 内容字符串
|
|
25
27
|
* @returns 转换后的 HTML 字符串
|
|
26
28
|
*/
|
|
27
|
-
const convertMarkdownToHtml = (mdContent) => {
|
|
29
|
+
const convertMarkdownToHtml = (path, lang, mdContent) => {
|
|
30
|
+
const sourceFileMeta = metadata_1.MetaData.files.find(f => f.path === path);
|
|
28
31
|
// 创建自定义渲染器
|
|
29
32
|
const renderer = new marked_1.marked.Renderer();
|
|
30
33
|
const originalCodeRenderer = renderer.code;
|
|
34
|
+
const originalLinkRenderer = renderer.link;
|
|
35
|
+
renderer.link = function (link) {
|
|
36
|
+
console.info(`🔗 #### Processing link in Markdown: ${link.href} in file: ${path}`);
|
|
37
|
+
if (URL.canParse(link.href)) {
|
|
38
|
+
// 保持原有链接渲染行为
|
|
39
|
+
return originalLinkRenderer.call(this, link);
|
|
40
|
+
}
|
|
41
|
+
if (!sourceFileMeta?.metadata?.slug) {
|
|
42
|
+
console.warn(`⚠️ Source file metadata slug not found for path ${path}`);
|
|
43
|
+
return originalLinkRenderer.call(this, link);
|
|
44
|
+
}
|
|
45
|
+
const sourceFileHtmlPath = (0, path_1.resolve)('/', lang, `${sourceFileMeta.metadata.slug}.html`);
|
|
46
|
+
const resolvedPath = (0, path_1.join)((0, path_1.dirname)(path), link.href);
|
|
47
|
+
const file = metadata_1.MetaData.files.find(f => f.path === resolvedPath);
|
|
48
|
+
if (!file) {
|
|
49
|
+
console.warn(`⚠️ Link target not found for path ${resolvedPath} in file ${path}`);
|
|
50
|
+
return originalLinkRenderer.call(this, link);
|
|
51
|
+
}
|
|
52
|
+
if (link.href.endsWith('.md')) {
|
|
53
|
+
if (!file.metadata?.slug) {
|
|
54
|
+
console.warn(`⚠️ Missing slug metadata for file ${file.path}`);
|
|
55
|
+
return originalLinkRenderer.call(this, link);
|
|
56
|
+
}
|
|
57
|
+
const slug = file.metadata.slug;
|
|
58
|
+
const targetPath = (0, path_1.resolve)('/', lang, `${slug}.html`);
|
|
59
|
+
// 将 .md 链接转换为对应的 HTML 文件链接
|
|
60
|
+
const href = (0, path_1.relative)((0, path_1.dirname)(sourceFileHtmlPath), targetPath);
|
|
61
|
+
const modifiedLink = {
|
|
62
|
+
...link,
|
|
63
|
+
href,
|
|
64
|
+
};
|
|
65
|
+
return originalLinkRenderer.call(this, modifiedLink);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.info(` 🔗 #### Processing resource link in Markdown: ${link.href} in file: ${path}`);
|
|
69
|
+
// 其他资源链接到 __raw__ 目录
|
|
70
|
+
const resourcePath = (0, path_1.resolve)('/', (0, path_1.dirname)(path), link.href);
|
|
71
|
+
const href = (0, path_1.relative)((0, path_1.dirname)(sourceFileHtmlPath), (0, path_1.join)('/', '__raw__', resourcePath));
|
|
72
|
+
console.info(` ➡️ Converted resource link to: ${href}`);
|
|
73
|
+
const modifiedLink = {
|
|
74
|
+
...link,
|
|
75
|
+
href: href,
|
|
76
|
+
};
|
|
77
|
+
return originalLinkRenderer.call(this, modifiedLink);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const originalImageRenderer = renderer.image;
|
|
81
|
+
renderer.image = function (image) {
|
|
82
|
+
console.info(`🖼️ #### Processing image in Markdown: ${image.href} in file: ${path}`);
|
|
83
|
+
if (URL.canParse(image.href)) {
|
|
84
|
+
// 保持原有图片渲染行为
|
|
85
|
+
return originalImageRenderer.call(this, image);
|
|
86
|
+
}
|
|
87
|
+
const imagePath = (0, path_1.join)((0, path_1.dirname)(path), image.href);
|
|
88
|
+
return `<img src="${(0, path_1.join)('..', '__raw__', imagePath)}" alt="${image.text}" />`;
|
|
89
|
+
};
|
|
31
90
|
// 重写代码块渲染器以支持 Mermaid - 使用 any 类型绕过类型检查
|
|
32
91
|
renderer.code = function (code, language, isEscaped) {
|
|
33
92
|
// 在 marked 17+ 中,code 参数是一个对象,包含 text 和 lang 属性
|