czon 0.8.4 → 0.8.6
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/ai/extractMetadataFromMarkdown.js +1 -1
- package/dist/build/pipeline.js +6 -0
- package/dist/process/enhanceMarkdownSource.js +2 -11
- package/dist/process/template.js +1 -2
- package/dist/process/translateMetadata.js +172 -0
- package/dist/process/translateNavLinks.js +79 -0
- package/dist/ssg/ContentPage.js +14 -10
- package/dist/ssg/components/CZONHeader.js +2 -2
- package/dist/ssg/components/ContentMeta.js +16 -8
- package/dist/ssg/components/NavLinks.js +6 -5
- package/dist/ssg/components/Navigator.js +3 -2
- package/dist/ssg/utils/getLocalizedMetadata.js +20 -0
- package/dist/ssg/utils/getNavLinkDisplayTitle.js +7 -0
- package/dist/utils/frontmatter.js +12 -7
- package/package.json +1 -1
|
@@ -21,7 +21,7 @@ async function extractMetadataFromMarkdown(filePath, content) {
|
|
|
21
21
|
'summary: 文档中型摘要(用一段话总结文章,包含关键论点和结论,控制在 300 字以内),用于 邮件推送内容,newsletter 介绍',
|
|
22
22
|
'inferred_date: 文档中隐含的创建日期(如果有的话,格式:YYYY-MM-DD,没有就留空字符串)',
|
|
23
23
|
'inferred_lang: 文档使用的语言代码(例如:zh-Hans 表示简体中文,en-US 表示美式英语)',
|
|
24
|
-
'key_points:
|
|
24
|
+
'key_points: 文章的关键要点列表(最重要的 3-5 个要点,简洁明了)',
|
|
25
25
|
'audience: 目标读者描述(简短描述,50 字以内)',
|
|
26
26
|
'short_summary: 文档的超短摘要(用 2-3 句话概括文章主要内容,突出核心观点),用于文章列表页摘要,RSS feed 描述',
|
|
27
27
|
...(hasExistingSlug
|
package/dist/build/pipeline.js
CHANGED
|
@@ -40,6 +40,8 @@ const metadata_1 = require("../metadata");
|
|
|
40
40
|
const paths_1 = require("../paths");
|
|
41
41
|
const category_1 = require("../process/category");
|
|
42
42
|
const translateCategories_1 = require("../process/translateCategories");
|
|
43
|
+
const translateMetadata_1 = require("../process/translateMetadata");
|
|
44
|
+
const translateNavLinks_1 = require("../process/translateNavLinks");
|
|
43
45
|
const enhanceMarkdownSource_1 = require("../process/enhanceMarkdownSource");
|
|
44
46
|
const extractMetadataByAI_1 = require("../process/extractMetadataByAI");
|
|
45
47
|
const processTranslations_1 = require("../process/processTranslations");
|
|
@@ -102,10 +104,14 @@ async function buildPipeline(options) {
|
|
|
102
104
|
}
|
|
103
105
|
// 运行 AI 元数据提取
|
|
104
106
|
await (0, extractMetadataByAI_1.extractMetadataByAI)();
|
|
107
|
+
// 翻译 AI 提取的 metadata JSON
|
|
108
|
+
await (0, translateMetadata_1.processTranslateMetadata)();
|
|
105
109
|
// 提取分类信息
|
|
106
110
|
await (0, category_1.processExtractCategory)();
|
|
107
111
|
// 翻译分类
|
|
108
112
|
await (0, translateCategories_1.processTranslateCategories)();
|
|
113
|
+
// 翻译 navLinks
|
|
114
|
+
await (0, translateNavLinks_1.processTranslateNavLinks)();
|
|
109
115
|
// 存储母语文件,并进行内容增强预处理
|
|
110
116
|
await (0, enhanceMarkdownSource_1.storeNativeFiles)();
|
|
111
117
|
// 处理翻译
|
|
@@ -8,10 +8,10 @@ const promises_1 = require("fs/promises");
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const metadata_1 = require("../metadata");
|
|
10
10
|
const paths_1 = require("../paths");
|
|
11
|
-
const frontmatter_1 = require("../utils/frontmatter");
|
|
12
11
|
const writeFile_1 = require("../utils/writeFile");
|
|
13
12
|
/**
|
|
14
13
|
* 存储母语文件到 .czon/src
|
|
14
|
+
* 直接复制原始文件,metadata 从 meta.json 获取
|
|
15
15
|
*/
|
|
16
16
|
async function storeNativeFiles() {
|
|
17
17
|
const { files } = metadata_1.MetaData;
|
|
@@ -25,16 +25,7 @@ async function storeNativeFiles() {
|
|
|
25
25
|
throw new Error(`Missing inferred language`);
|
|
26
26
|
const filePath = path_1.default.join(paths_1.CZON_SRC_DIR, file.metadata.inferred_lang, file.path);
|
|
27
27
|
const originalContent = await (0, promises_1.readFile)(path_1.default.join(paths_1.INPUT_DIR, file.path), 'utf-8');
|
|
28
|
-
|
|
29
|
-
const enhancedContent = (0, frontmatter_1.updateFrontmatter)(originalContent, {
|
|
30
|
-
title: file.metadata.title,
|
|
31
|
-
summary: file.metadata.summary,
|
|
32
|
-
tags: file.metadata.tags,
|
|
33
|
-
date: file.metadata.inferred_date,
|
|
34
|
-
});
|
|
35
|
-
// 进行内链接替换, 将相对链接替换为基于 czon://hash 的链接
|
|
36
|
-
// const replacedContent = replaceInnerLinks(file, enhancedContent);
|
|
37
|
-
await (0, writeFile_1.writeFile)(filePath, enhancedContent);
|
|
28
|
+
await (0, writeFile_1.writeFile)(filePath, originalContent);
|
|
38
29
|
}
|
|
39
30
|
catch (error) {
|
|
40
31
|
console.warn(`⚠️ Failed to store native file ${file.path}:`, error);
|
package/dist/process/template.js
CHANGED
|
@@ -102,12 +102,11 @@ const spiderStaticSiteGenerator = async () => {
|
|
|
102
102
|
continue;
|
|
103
103
|
for (const lang of metadata_1.MetaData.options.langs || []) {
|
|
104
104
|
const markdown = await fs.readFile(path.join(paths_1.CZON_SRC_DIR, lang, file.path), 'utf-8');
|
|
105
|
-
const
|
|
105
|
+
const body = (0, frontmatter_1.stripFrontmatter)(markdown);
|
|
106
106
|
const article = {
|
|
107
107
|
lang,
|
|
108
108
|
file,
|
|
109
109
|
body: '',
|
|
110
|
-
frontmatter,
|
|
111
110
|
headings: [],
|
|
112
111
|
};
|
|
113
112
|
(0, convertMarkdownToHtml_1.convertMarkdownToHtml)(article, file.path, lang, body);
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.processTranslateMetadata = processTranslateMetadata;
|
|
4
|
+
const metadata_1 = require("../metadata");
|
|
5
|
+
const openai_1 = require("../services/openai");
|
|
6
|
+
const languages_1 = require("../languages");
|
|
7
|
+
const sha256_1 = require("../utils/sha256");
|
|
8
|
+
/**
|
|
9
|
+
* 计算 metadata 中需要翻译字段的哈希值
|
|
10
|
+
* 用于增量检测
|
|
11
|
+
*/
|
|
12
|
+
function computeTranslatableFieldsHash(metadata) {
|
|
13
|
+
const translatableFields = {
|
|
14
|
+
title: metadata.title,
|
|
15
|
+
description: metadata.description,
|
|
16
|
+
summary: metadata.summary,
|
|
17
|
+
short_summary: metadata.short_summary,
|
|
18
|
+
key_points: metadata.key_points,
|
|
19
|
+
audience: metadata.audience,
|
|
20
|
+
tags: metadata.tags,
|
|
21
|
+
};
|
|
22
|
+
return (0, sha256_1.sha256)(JSON.stringify(translatableFields));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 检查单个文件的单个语言是否需要翻译
|
|
26
|
+
*/
|
|
27
|
+
function needsTranslationForLang(file, lang) {
|
|
28
|
+
if (!file.metadata)
|
|
29
|
+
return false;
|
|
30
|
+
// 没有该语言的翻译 -> 需要翻译
|
|
31
|
+
if (!file.metadataTranslations?.[lang])
|
|
32
|
+
return true;
|
|
33
|
+
// hash 变化 -> 源 metadata 已更新,需要重新翻译
|
|
34
|
+
const currentHash = computeTranslatableFieldsHash(file.metadata);
|
|
35
|
+
return file.metadataTranslationHash !== currentHash;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 翻译单个文件的 metadata 到单个目标语言
|
|
39
|
+
*/
|
|
40
|
+
async function translateFileMetadataToLang(file, targetLang) {
|
|
41
|
+
const { metadata } = file;
|
|
42
|
+
if (!metadata)
|
|
43
|
+
return;
|
|
44
|
+
// 提取需要翻译的字段
|
|
45
|
+
const sourceMetadata = {
|
|
46
|
+
title: metadata.title,
|
|
47
|
+
description: metadata.description,
|
|
48
|
+
summary: metadata.summary,
|
|
49
|
+
short_summary: metadata.short_summary,
|
|
50
|
+
key_points: metadata.key_points,
|
|
51
|
+
audience: metadata.audience,
|
|
52
|
+
tags: metadata.tags,
|
|
53
|
+
};
|
|
54
|
+
const sourceLangName = languages_1.LANGUAGE_NAMES[metadata.inferred_lang] || metadata.inferred_lang;
|
|
55
|
+
const targetLangName = languages_1.LANGUAGE_NAMES[targetLang] || targetLang;
|
|
56
|
+
const response = await (0, openai_1.completeMessages)([
|
|
57
|
+
{
|
|
58
|
+
role: 'system',
|
|
59
|
+
content: [
|
|
60
|
+
'你是专业的多语言翻译助手,擅长准确翻译技术文档的元数据。',
|
|
61
|
+
'',
|
|
62
|
+
'任务:将给定的 metadata JSON 翻译成目标语言。',
|
|
63
|
+
'',
|
|
64
|
+
'要求:',
|
|
65
|
+
'1. 保持 JSON 结构完全不变,只翻译文本内容',
|
|
66
|
+
'2. title 翻译要简洁明了,不超过 30 个字',
|
|
67
|
+
'3. description 和 summary 翻译要准确传达原意',
|
|
68
|
+
'4. tags 翻译要符合目标语言的常用术语',
|
|
69
|
+
'5. key_points 数组中的每个要点都需要翻译',
|
|
70
|
+
'6. 技术术语保持准确,必要时可保留原文',
|
|
71
|
+
'',
|
|
72
|
+
'直接返回翻译后的 JSON 对象,格式如下:',
|
|
73
|
+
'{',
|
|
74
|
+
' "title": "...",',
|
|
75
|
+
' "description": "...",',
|
|
76
|
+
' "summary": "...",',
|
|
77
|
+
' "short_summary": "...",',
|
|
78
|
+
' "key_points": ["...", "..."],',
|
|
79
|
+
' "audience": "...",',
|
|
80
|
+
' "tags": ["...", "..."]',
|
|
81
|
+
'}',
|
|
82
|
+
].join('\n'),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
role: 'user',
|
|
86
|
+
content: [
|
|
87
|
+
`源语言: ${metadata.inferred_lang} (${sourceLangName})`,
|
|
88
|
+
`目标语言: ${targetLang} (${targetLangName})`,
|
|
89
|
+
'',
|
|
90
|
+
'待翻译的 metadata:',
|
|
91
|
+
JSON.stringify(sourceMetadata, null, 2),
|
|
92
|
+
].join('\n'),
|
|
93
|
+
},
|
|
94
|
+
], {
|
|
95
|
+
response_format: { type: 'json_object' },
|
|
96
|
+
task_id: `translate-metadata:${file.path}:${targetLang}`,
|
|
97
|
+
});
|
|
98
|
+
const translated = JSON.parse(response.choices[0].message.content);
|
|
99
|
+
// 初始化存储结构
|
|
100
|
+
if (!file.metadataTranslations) {
|
|
101
|
+
file.metadataTranslations = {};
|
|
102
|
+
}
|
|
103
|
+
// 验证并存储翻译结果
|
|
104
|
+
if (translated && translated.title) {
|
|
105
|
+
file.metadataTranslations[targetLang] = {
|
|
106
|
+
title: translated.title?.trim() || '',
|
|
107
|
+
description: translated.description?.trim() || '',
|
|
108
|
+
summary: translated.summary?.trim() || '',
|
|
109
|
+
short_summary: translated.short_summary?.trim() || '',
|
|
110
|
+
key_points: Array.isArray(translated.key_points)
|
|
111
|
+
? translated.key_points.map((p) => p?.trim()).filter(Boolean)
|
|
112
|
+
: [],
|
|
113
|
+
audience: translated.audience?.trim() || '',
|
|
114
|
+
tags: Array.isArray(translated.tags)
|
|
115
|
+
? translated.tags.map((t) => t?.trim()).filter(Boolean)
|
|
116
|
+
: [],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 处理所有文件的 metadata 翻译
|
|
122
|
+
* 采用逐文件逐语言的翻译策略,避免输出 JSON 过长被截断
|
|
123
|
+
*/
|
|
124
|
+
async function processTranslateMetadata() {
|
|
125
|
+
const langs = metadata_1.MetaData.options.langs || [];
|
|
126
|
+
if (langs.length === 0) {
|
|
127
|
+
console.info('ℹ️ No target languages configured, skipping metadata translation.');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// 收集所有需要翻译的任务 (file, lang) 组合
|
|
131
|
+
const tasks = [];
|
|
132
|
+
for (const file of metadata_1.MetaData.files) {
|
|
133
|
+
if (!file.path.endsWith('.md') || !file.metadata)
|
|
134
|
+
continue;
|
|
135
|
+
for (const lang of langs) {
|
|
136
|
+
if (needsTranslationForLang(file, lang)) {
|
|
137
|
+
tasks.push({ file, lang });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (tasks.length === 0) {
|
|
142
|
+
console.info('ℹ️ All metadata already translated, skipping.');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const fileCount = new Set(tasks.map(t => t.file.path)).size;
|
|
146
|
+
console.info(`🌐 Translating metadata: ${tasks.length} tasks (${fileCount} files × up to ${langs.length} languages)...`);
|
|
147
|
+
// 并行执行所有翻译任务
|
|
148
|
+
const results = await Promise.allSettled(tasks.map(async ({ file, lang }) => {
|
|
149
|
+
await translateFileMetadataToLang(file, lang);
|
|
150
|
+
console.info(`✅ Translated metadata: ${file.path} -> ${lang}`);
|
|
151
|
+
}));
|
|
152
|
+
// 统计结果
|
|
153
|
+
const succeeded = results.filter(r => r.status === 'fulfilled').length;
|
|
154
|
+
const failed = results.filter(r => r.status === 'rejected').length;
|
|
155
|
+
if (failed > 0) {
|
|
156
|
+
console.warn(`⚠️ ${failed} translation tasks failed:`);
|
|
157
|
+
results.forEach((r, i) => {
|
|
158
|
+
if (r.status === 'rejected') {
|
|
159
|
+
console.error(` ❌ ${tasks[i].file.path} -> ${tasks[i].lang}:`, r.reason?.message || r.reason);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
// 更新所有成功翻译文件的 hash
|
|
164
|
+
const translatedFiles = new Set(tasks.filter((_, i) => results[i].status === 'fulfilled').map(t => t.file));
|
|
165
|
+
for (const file of translatedFiles) {
|
|
166
|
+
if (file.metadata) {
|
|
167
|
+
file.metadataTranslationHash = computeTranslatableFieldsHash(file.metadata);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
console.info(`✅ Metadata translation completed: ${succeeded}/${tasks.length} succeeded`);
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=translateMetadata.js.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.processTranslateNavLinks = void 0;
|
|
4
|
+
const metadata_1 = require("../metadata");
|
|
5
|
+
const openai_1 = require("../services/openai");
|
|
6
|
+
const processTranslateNavLinks = async () => {
|
|
7
|
+
const langs = metadata_1.MetaData.options.langs || [];
|
|
8
|
+
const navLinks = metadata_1.MetaData.options.site?.navLinks || [];
|
|
9
|
+
if (langs.length === 0 || navLinks.length === 0) {
|
|
10
|
+
console.info('ℹ️ No languages or navLinks configured, skipping navLink translation.');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (!metadata_1.MetaData.navLinkTranslations) {
|
|
14
|
+
metadata_1.MetaData.navLinkTranslations = {};
|
|
15
|
+
}
|
|
16
|
+
const allTitles = navLinks.map(link => link.title);
|
|
17
|
+
const titlesNeedingTranslation = allTitles.filter(title => {
|
|
18
|
+
const translations = metadata_1.MetaData.navLinkTranslations[title];
|
|
19
|
+
return !translations || langs.some(lang => !translations[lang]);
|
|
20
|
+
});
|
|
21
|
+
if (titlesNeedingTranslation.length === 0) {
|
|
22
|
+
console.info('ℹ️ All navLink titles already have translations, skipping.');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
console.info(`🌐 Translating ${titlesNeedingTranslation.length} navLink titles...`);
|
|
26
|
+
const response = await (0, openai_1.completeMessages)([
|
|
27
|
+
{
|
|
28
|
+
role: 'system',
|
|
29
|
+
content: [
|
|
30
|
+
'你是一个专业的翻译助手。',
|
|
31
|
+
'请将给定的导航链接标题翻译成指定的目标语言。',
|
|
32
|
+
'翻译应该自然、准确,符合目标语言的表达习惯。',
|
|
33
|
+
'导航链接标题通常是简短的词语,翻译时保持简洁。',
|
|
34
|
+
'',
|
|
35
|
+
'请以 JSON 格式返回,格式如下:',
|
|
36
|
+
'{',
|
|
37
|
+
' "translations": {',
|
|
38
|
+
' "原标题": { "zh-Hans": "中文翻译", "en-US": "English" }',
|
|
39
|
+
' }',
|
|
40
|
+
'}',
|
|
41
|
+
].join('\n'),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
role: 'user',
|
|
45
|
+
content: [
|
|
46
|
+
`需要翻译的导航链接标题: ${JSON.stringify(titlesNeedingTranslation)}`,
|
|
47
|
+
`目标语言: ${JSON.stringify(langs)}`,
|
|
48
|
+
].join('\n'),
|
|
49
|
+
},
|
|
50
|
+
], { response_format: { type: 'json_object' }, task_id: 'translate-nav-links' });
|
|
51
|
+
const json = response.choices[0].message.content;
|
|
52
|
+
const parsed = JSON.parse(json);
|
|
53
|
+
if (parsed.translations) {
|
|
54
|
+
for (const [title, translations] of Object.entries(parsed.translations)) {
|
|
55
|
+
if (!metadata_1.MetaData.navLinkTranslations[title]) {
|
|
56
|
+
metadata_1.MetaData.navLinkTranslations[title] = {};
|
|
57
|
+
}
|
|
58
|
+
for (const [lang, translation] of Object.entries(translations)) {
|
|
59
|
+
metadata_1.MetaData.navLinkTranslations[title][lang] = translation;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const stillMissingTranslation = allTitles.filter(title => {
|
|
64
|
+
const translations = metadata_1.MetaData.navLinkTranslations[title];
|
|
65
|
+
return !translations || langs.some(lang => !translations[lang]);
|
|
66
|
+
});
|
|
67
|
+
if (stillMissingTranslation.length > 0) {
|
|
68
|
+
const details = stillMissingTranslation.map(title => {
|
|
69
|
+
const missing = langs.filter(lang => !metadata_1.MetaData.navLinkTranslations[title]?.[lang]);
|
|
70
|
+
return ` - "${title}": 缺少 ${missing.join(', ')}`;
|
|
71
|
+
});
|
|
72
|
+
console.error(`❌ 以下 navLink 标题缺少翻译:\n${details.join('\n')}`);
|
|
73
|
+
console.error('这是一个可重试的错误,请重新运行命令。');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
console.info('✅ NavLink translations completed.');
|
|
77
|
+
};
|
|
78
|
+
exports.processTranslateNavLinks = processTranslateNavLinks;
|
|
79
|
+
//# sourceMappingURL=translateNavLinks.js.map
|
package/dist/ssg/ContentPage.js
CHANGED
|
@@ -15,12 +15,13 @@ const Navigator_1 = require("./components/Navigator");
|
|
|
15
15
|
const PageLayout_1 = require("./layouts/PageLayout");
|
|
16
16
|
const resourceMap_1 = require("./resourceMap");
|
|
17
17
|
const style_1 = require("./style");
|
|
18
|
+
const getLocalizedMetadata_1 = require("./utils/getLocalizedMetadata");
|
|
18
19
|
const ContentPage = props => {
|
|
19
|
-
const
|
|
20
|
-
const title =
|
|
21
|
-
const summary =
|
|
22
|
-
const date =
|
|
23
|
-
const tags =
|
|
20
|
+
const metadata = (0, getLocalizedMetadata_1.getLocalizedMetadata)(props.file, props.lang);
|
|
21
|
+
const title = metadata?.title || '(no title)';
|
|
22
|
+
const summary = metadata?.summary || '';
|
|
23
|
+
const date = props.file.metadata?.inferred_date || '--';
|
|
24
|
+
const tags = metadata?.tags || [];
|
|
24
25
|
const category = props.file.category;
|
|
25
26
|
const relatedContents = props.ctx.site.files.filter(f => f.category === category && f.path !== props.file.path);
|
|
26
27
|
// 查找指向当前文章的其他文章
|
|
@@ -34,7 +35,10 @@ const ContentPage = props => {
|
|
|
34
35
|
react_1.default.createElement("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
|
|
35
36
|
react_1.default.createElement("title", null, title),
|
|
36
37
|
react_1.default.createElement("link", { rel: "icon", href: faviconUrl, type: "image/x-icon" }),
|
|
37
|
-
|
|
38
|
+
// 使用 short_summary 作为 description,有利于 SEO 和社交分享预览
|
|
39
|
+
metadata?.short_summary && (react_1.default.createElement("meta", { name: "description", content: metadata?.short_summary.slice(0, 150) })),
|
|
40
|
+
// Keywords meta tag for SEO (using tags, if available)
|
|
41
|
+
tags.length > 0 && react_1.default.createElement("meta", { name: "keywords", content: tags.join(', ') }),
|
|
38
42
|
react_1.default.createElement(Analytics_1.Analytics, { ctx: props.ctx }),
|
|
39
43
|
react_1.default.createElement("script", { src: (0, resourceMap_1.getResourceUrlFrom)(props.ctx.path, 'tailwindcss.js') }),
|
|
40
44
|
react_1.default.createElement("style", null, style_1.style),
|
|
@@ -69,16 +73,16 @@ const ContentPage = props => {
|
|
|
69
73
|
relatedContents.length > 0 && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
70
74
|
react_1.default.createElement("h2", null, "See Also"),
|
|
71
75
|
react_1.default.createElement("ul", null, relatedContents.map(f => {
|
|
72
|
-
const
|
|
76
|
+
const theMetadata = (0, getLocalizedMetadata_1.getLocalizedMetadata)(f, props.lang);
|
|
73
77
|
return (react_1.default.createElement("li", { key: f.path },
|
|
74
|
-
react_1.default.createElement("a", { href: `${f.metadata?.slug}.html` },
|
|
78
|
+
react_1.default.createElement("a", { href: `${f.metadata?.slug}.html` }, theMetadata?.title || '(no title)')));
|
|
75
79
|
})))),
|
|
76
80
|
referencedFiles.length > 0 && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
77
81
|
react_1.default.createElement("h2", null, "Referenced By"),
|
|
78
82
|
react_1.default.createElement("ul", null, referencedFiles.map(f => {
|
|
79
|
-
const
|
|
83
|
+
const theMetadata = (0, getLocalizedMetadata_1.getLocalizedMetadata)(f, props.lang);
|
|
80
84
|
return (react_1.default.createElement("li", { key: f.path },
|
|
81
|
-
react_1.default.createElement("a", { href: `${f.metadata?.slug}.html` },
|
|
85
|
+
react_1.default.createElement("a", { href: `${f.metadata?.slug}.html` }, theMetadata?.title || '(no title)')));
|
|
82
86
|
}))))),
|
|
83
87
|
react_1.default.createElement("footer", { className: "footer" },
|
|
84
88
|
react_1.default.createElement(LanguageSwitcher_1.LanguageSwitcher, { ctx: props.ctx, lang: props.lang, file: props.file }),
|
|
@@ -14,10 +14,10 @@ const CZONHeader = props => {
|
|
|
14
14
|
const home = props.ctx.site.options.site?.home ?? 'index.html';
|
|
15
15
|
return (react_1.default.createElement("header", { className: "czon-header py-4 border-b flex justify-between items-center px-6" },
|
|
16
16
|
react_1.default.createElement("div", { className: "flex items-center gap-4" },
|
|
17
|
-
hasNavLinks && react_1.default.createElement(NavLinks_1.NavLinksMobile, { navLinks: navLinks }),
|
|
17
|
+
hasNavLinks && (react_1.default.createElement(NavLinks_1.NavLinksMobile, { navLinks: navLinks, site: props.ctx.site, lang: props.lang })),
|
|
18
18
|
react_1.default.createElement("h1", { className: "text-2xl font-bold" },
|
|
19
19
|
react_1.default.createElement("a", { href: home }, props.ctx.site.options.site?.title ?? 'CZON')),
|
|
20
|
-
hasNavLinks && react_1.default.createElement(NavLinks_1.NavLinksDesktop, { navLinks: navLinks })),
|
|
20
|
+
hasNavLinks && (react_1.default.createElement(NavLinks_1.NavLinksDesktop, { navLinks: navLinks, site: props.ctx.site, lang: props.lang }))),
|
|
21
21
|
react_1.default.createElement("div", { className: "flex items-center gap-4" },
|
|
22
22
|
react_1.default.createElement(DarkModeSwitch_1.DarkModeSwitch, null),
|
|
23
23
|
props.lang && react_1.default.createElement(LanguageSwitch_1.LanguageSwitch, { ctx: props.ctx, lang: props.lang, file: props.file }))));
|
|
@@ -6,21 +6,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.ContentMeta = void 0;
|
|
7
7
|
const react_1 = __importDefault(require("react"));
|
|
8
8
|
const getCategoryDisplayName_1 = require("../utils/getCategoryDisplayName");
|
|
9
|
+
const getLocalizedMetadata_1 = require("../utils/getLocalizedMetadata");
|
|
9
10
|
const TagList_1 = require("./TagList");
|
|
10
11
|
const ContentMeta = props => {
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
12
|
+
const metadata = (0, getLocalizedMetadata_1.getLocalizedMetadata)(props.file, props.lang);
|
|
13
|
+
const title = metadata?.title || '(no title)';
|
|
14
|
+
const summary = metadata?.summary || '';
|
|
15
|
+
const audience = metadata?.audience || '';
|
|
16
|
+
const keyPoints = metadata?.key_points || [];
|
|
17
|
+
const date = props.file.metadata?.inferred_date || '--';
|
|
18
|
+
const tags = metadata?.tags || [];
|
|
17
19
|
const category = props.file.category;
|
|
18
20
|
return (react_1.default.createElement("header", { className: "content-header mb-4 pb-2 border-b" },
|
|
19
21
|
react_1.default.createElement("h2", { className: "text-2xl font-bold mb-2" },
|
|
20
22
|
react_1.default.createElement("a", { href: `${props.file.metadata?.slug}.html` }, title)),
|
|
21
|
-
react_1.default.createElement("p", { className: "font-semibold" }, (0, getCategoryDisplayName_1.getCategoryDisplayName)(props.ctx.site, category, props.lang)),
|
|
23
|
+
react_1.default.createElement("p", { className: "font-semibold mb-2" }, (0, getCategoryDisplayName_1.getCategoryDisplayName)(props.ctx.site, category, props.lang)),
|
|
24
|
+
audience && react_1.default.createElement("div", null,
|
|
25
|
+
"\uD83D\uDC64 ",
|
|
26
|
+
audience),
|
|
22
27
|
react_1.default.createElement("blockquote", null, summary),
|
|
23
|
-
react_1.default.createElement("
|
|
28
|
+
keyPoints.length > 0 && (react_1.default.createElement("ul", { className: "key-points list-inside my-2" }, keyPoints.slice(0, 5).map((point, index) => (react_1.default.createElement("li", { key: index },
|
|
29
|
+
"\u2728 ",
|
|
30
|
+
point))))),
|
|
31
|
+
date && date !== '--' && react_1.default.createElement("div", null,
|
|
24
32
|
"\uD83D\uDCC5 ",
|
|
25
33
|
date),
|
|
26
34
|
react_1.default.createElement("div", { className: "tags" },
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.NavLinksDesktop = exports.NavLinksMobile = void 0;
|
|
7
7
|
const react_1 = __importDefault(require("react"));
|
|
8
|
+
const getNavLinkDisplayTitle_1 = require("../utils/getNavLinkDisplayTitle");
|
|
8
9
|
const mobileStyle = `
|
|
9
10
|
/* Mobile hamburger menu styles */
|
|
10
11
|
.nav-links-mobile {
|
|
@@ -233,7 +234,7 @@ const desktopStyle = `
|
|
|
233
234
|
* 移动端汉堡菜单导航组件
|
|
234
235
|
* 仅在移动端显示(< 768px)
|
|
235
236
|
*/
|
|
236
|
-
const NavLinksMobile = ({ navLinks }) => {
|
|
237
|
+
const NavLinksMobile = ({ navLinks, site, lang }) => {
|
|
237
238
|
if (!navLinks || navLinks.length === 0) {
|
|
238
239
|
return null;
|
|
239
240
|
}
|
|
@@ -246,7 +247,7 @@ const NavLinksMobile = ({ navLinks }) => {
|
|
|
246
247
|
react_1.default.createElement("span", null),
|
|
247
248
|
react_1.default.createElement("span", null))),
|
|
248
249
|
react_1.default.createElement("div", { className: "nav-links-dropdown", role: "menu", "aria-label": "Navigation links" },
|
|
249
|
-
react_1.default.createElement("div", { className: "nav-links-dropdown-list" }, navLinks.map((link, index) => (react_1.default.createElement("a", { key: index, href: link.href, className: "nav-link-mobile-item", role: "menuitem" }, link.title)))))));
|
|
250
|
+
react_1.default.createElement("div", { className: "nav-links-dropdown-list" }, navLinks.map((link, index) => (react_1.default.createElement("a", { key: index, href: link.href, className: "nav-link-mobile-item", role: "menuitem" }, (0, getNavLinkDisplayTitle_1.getNavLinkDisplayTitle)(site, link.title, lang || ''))))))));
|
|
250
251
|
};
|
|
251
252
|
exports.NavLinksMobile = NavLinksMobile;
|
|
252
253
|
/**
|
|
@@ -259,7 +260,7 @@ const MAX_VISIBLE_LINKS = 5;
|
|
|
259
260
|
* 桌面端导航组件
|
|
260
261
|
* 仅在桌面端显示(>= 768px),最大宽度 40vw
|
|
261
262
|
*/
|
|
262
|
-
const NavLinksDesktop = ({ navLinks }) => {
|
|
263
|
+
const NavLinksDesktop = ({ navLinks, site, lang }) => {
|
|
263
264
|
if (!navLinks || navLinks.length === 0) {
|
|
264
265
|
return null;
|
|
265
266
|
}
|
|
@@ -268,7 +269,7 @@ const NavLinksDesktop = ({ navLinks }) => {
|
|
|
268
269
|
const hasOverflow = overflowLinks.length > 0;
|
|
269
270
|
return (react_1.default.createElement("nav", { className: "nav-links-desktop", "aria-label": "Main navigation" },
|
|
270
271
|
react_1.default.createElement("style", null, desktopStyle),
|
|
271
|
-
react_1.default.createElement("div", { className: "nav-links-desktop-list" }, visibleLinks.map((link, index) => (react_1.default.createElement("a", { key: index, href: link.href, className: "nav-link-item" }, link.title)))),
|
|
272
|
+
react_1.default.createElement("div", { className: "nav-links-desktop-list" }, visibleLinks.map((link, index) => (react_1.default.createElement("a", { key: index, href: link.href, className: "nav-link-item" }, (0, getNavLinkDisplayTitle_1.getNavLinkDisplayTitle)(site, link.title, lang || ''))))),
|
|
272
273
|
hasOverflow && (react_1.default.createElement("div", { className: "nav-links-more-container" },
|
|
273
274
|
react_1.default.createElement("input", { id: "nav-links-more-toggle", type: "checkbox", className: "hidden", "aria-hidden": "true" }),
|
|
274
275
|
react_1.default.createElement("label", { htmlFor: "nav-links-more-toggle", className: "nav-links-more-trigger", "aria-label": "More navigation links", "aria-haspopup": "true", "aria-expanded": "false" },
|
|
@@ -276,7 +277,7 @@ const NavLinksDesktop = ({ navLinks }) => {
|
|
|
276
277
|
react_1.default.createElement("svg", { className: "nav-links-more-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true" },
|
|
277
278
|
react_1.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" }))),
|
|
278
279
|
react_1.default.createElement("div", { className: "nav-links-more-dropdown", role: "menu", "aria-label": "Additional navigation links" },
|
|
279
|
-
react_1.default.createElement("div", { className: "nav-links-more-list" }, overflowLinks.map((link, index) => (react_1.default.createElement("a", { key: index, href: link.href, className: "nav-link-more-item", role: "menuitem" }, link.title)))))))));
|
|
280
|
+
react_1.default.createElement("div", { className: "nav-links-more-list" }, overflowLinks.map((link, index) => (react_1.default.createElement("a", { key: index, href: link.href, className: "nav-link-more-item", role: "menuitem" }, (0, getNavLinkDisplayTitle_1.getNavLinkDisplayTitle)(site, link.title, lang || ''))))))))));
|
|
280
281
|
};
|
|
281
282
|
exports.NavLinksDesktop = NavLinksDesktop;
|
|
282
283
|
//# sourceMappingURL=NavLinks.js.map
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.Navigator = void 0;
|
|
7
7
|
const react_1 = __importDefault(require("react"));
|
|
8
8
|
const getCategoryDisplayName_1 = require("../utils/getCategoryDisplayName");
|
|
9
|
+
const getLocalizedMetadata_1 = require("../utils/getLocalizedMetadata");
|
|
9
10
|
const sortBy_1 = require("../../utils/sortBy");
|
|
10
11
|
const Navigator = props => {
|
|
11
12
|
const categories = [...new Set(props.ctx.site.files.map(f => f.category))];
|
|
@@ -24,8 +25,8 @@ const Navigator = props => {
|
|
|
24
25
|
filesInCategory.map(file => {
|
|
25
26
|
const link = file.metadata.slug + '.html';
|
|
26
27
|
const isActive = props.file === file;
|
|
27
|
-
const
|
|
28
|
-
const theTitle =
|
|
28
|
+
const theMetadata = (0, getLocalizedMetadata_1.getLocalizedMetadata)(file, props.lang);
|
|
29
|
+
const theTitle = theMetadata?.title || '(no title)';
|
|
29
30
|
return (react_1.default.createElement("li", { className: "nav-item", key: file.path },
|
|
30
31
|
react_1.default.createElement("a", { href: link, className: `nav-link ${isActive ? 'active' : ''}` },
|
|
31
32
|
theTitle,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getLocalizedMetadata = getLocalizedMetadata;
|
|
4
|
+
/**
|
|
5
|
+
* 获取文件在指定语言下的 metadata
|
|
6
|
+
* - 如果是原语言,返回 file.metadata
|
|
7
|
+
* - 如果是翻译语言,返回 file.metadataTranslations[lang]
|
|
8
|
+
*/
|
|
9
|
+
function getLocalizedMetadata(file, lang) {
|
|
10
|
+
const metadata = file.metadata;
|
|
11
|
+
if (!metadata)
|
|
12
|
+
return undefined;
|
|
13
|
+
// 如果是原语言,直接返回原始 metadata
|
|
14
|
+
if (lang === metadata.inferred_lang) {
|
|
15
|
+
return metadata;
|
|
16
|
+
}
|
|
17
|
+
// 否则返回翻译版本
|
|
18
|
+
return file.metadataTranslations?.[lang];
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=getLocalizedMetadata.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getNavLinkDisplayTitle = getNavLinkDisplayTitle;
|
|
4
|
+
function getNavLinkDisplayTitle(site, title, lang) {
|
|
5
|
+
return site.navLinkTranslations?.[title]?.[lang] || title;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=getNavLinkDisplayTitle.js.map
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.stripFrontmatter = exports.parseFrontmatter = void 0;
|
|
4
4
|
const yaml_1 = require("yaml");
|
|
5
|
+
const frontmatterRegex = /^---\n([\s\S]*?)\n---/;
|
|
5
6
|
const parseFrontmatter = (content) => {
|
|
6
|
-
const frontmatterRegex = /^---\n([\s\S]*?)\n---/;
|
|
7
7
|
const match = content.match(frontmatterRegex);
|
|
8
8
|
if (match) {
|
|
9
9
|
const frontmatterContent = match[1];
|
|
@@ -13,10 +13,15 @@ const parseFrontmatter = (content) => {
|
|
|
13
13
|
return { frontmatter: {}, body: content };
|
|
14
14
|
};
|
|
15
15
|
exports.parseFrontmatter = parseFrontmatter;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
/**
|
|
17
|
+
* 移除 Markdown 内容中的 YAML FrontMatter,只返回正文
|
|
18
|
+
*/
|
|
19
|
+
const stripFrontmatter = (content) => {
|
|
20
|
+
const match = content.match(frontmatterRegex);
|
|
21
|
+
if (match) {
|
|
22
|
+
return content.slice(match[0].length).trim();
|
|
23
|
+
}
|
|
24
|
+
return content;
|
|
20
25
|
};
|
|
21
|
-
exports.
|
|
26
|
+
exports.stripFrontmatter = stripFrontmatter;
|
|
22
27
|
//# sourceMappingURL=frontmatter.js.map
|