czon 0.9.4 → 0.9.5
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/process/template.js
CHANGED
|
@@ -48,6 +48,7 @@ const resourceMap_1 = require("../ssg/resourceMap");
|
|
|
48
48
|
const convertMarkdownToHtml_1 = require("../utils/convertMarkdownToHtml");
|
|
49
49
|
const frontmatter_1 = require("../utils/frontmatter");
|
|
50
50
|
const isExists_1 = require("../utils/isExists");
|
|
51
|
+
const wordCount_1 = require("../utils/wordCount");
|
|
51
52
|
const writeFile_1 = require("../utils/writeFile");
|
|
52
53
|
const copyFavicon = async () => {
|
|
53
54
|
const faviconSource = path.join(paths_1.CZON_DIR, 'icons', 'favicon.ico');
|
|
@@ -103,11 +104,15 @@ const spiderStaticSiteGenerator = async () => {
|
|
|
103
104
|
for (const lang of metadata_1.MetaData.options.langs || []) {
|
|
104
105
|
const markdown = await fs.readFile(path.join(paths_1.CZON_SRC_DIR, lang, file.path), 'utf-8');
|
|
105
106
|
const body = (0, frontmatter_1.stripFrontmatter)(markdown);
|
|
107
|
+
const { total: wordCount } = (0, wordCount_1.countWords)(body);
|
|
108
|
+
const readingTimeMinutes = (0, wordCount_1.estimateReadingTime)(body);
|
|
106
109
|
const article = {
|
|
107
110
|
lang,
|
|
108
111
|
file,
|
|
109
112
|
body: '',
|
|
110
113
|
headings: [],
|
|
114
|
+
wordCount,
|
|
115
|
+
readingTimeMinutes,
|
|
111
116
|
};
|
|
112
117
|
(0, convertMarkdownToHtml_1.convertMarkdownToHtml)(article, file.path, lang, body);
|
|
113
118
|
contents.push(article);
|
|
@@ -7,6 +7,7 @@ exports.ContentMeta = void 0;
|
|
|
7
7
|
const react_1 = __importDefault(require("react"));
|
|
8
8
|
const getCategoryDisplayName_1 = require("../utils/getCategoryDisplayName");
|
|
9
9
|
const getLocalizedMetadata_1 = require("../utils/getLocalizedMetadata");
|
|
10
|
+
const formatReadingStats_1 = require("../utils/formatReadingStats");
|
|
10
11
|
const TagList_1 = require("./TagList");
|
|
11
12
|
const ContentMeta = props => {
|
|
12
13
|
const metadata = (0, getLocalizedMetadata_1.getLocalizedMetadata)(props.file, props.lang);
|
|
@@ -17,6 +18,10 @@ const ContentMeta = props => {
|
|
|
17
18
|
const date = props.file.metadata?.inferred_date || '--';
|
|
18
19
|
const tags = metadata?.tags || [];
|
|
19
20
|
const category = props.file.category;
|
|
21
|
+
// 从 ctx.contents 中查找对应的文章内容,获取字数和阅读时长
|
|
22
|
+
const content = props.ctx.contents.find(c => c.file.path === props.file.path && c.lang === props.lang);
|
|
23
|
+
const wordCount = content?.wordCount ?? 0;
|
|
24
|
+
const readingTimeMinutes = content?.readingTimeMinutes ?? 0;
|
|
20
25
|
return (react_1.default.createElement("header", { className: "content-header mb-4 pb-2 border-b" },
|
|
21
26
|
react_1.default.createElement("h2", { className: "text-2xl font-bold mb-2" },
|
|
22
27
|
react_1.default.createElement("a", { href: `${props.file.metadata?.slug}.html` }, title)),
|
|
@@ -28,9 +33,13 @@ const ContentMeta = props => {
|
|
|
28
33
|
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
34
|
"\u2728 ",
|
|
30
35
|
point))))),
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
react_1.default.createElement("div", { className: "text-sm opacity-70 my-1" },
|
|
37
|
+
date && date !== '--' && react_1.default.createElement("span", null,
|
|
38
|
+
"\uD83D\uDCC5 ",
|
|
39
|
+
date),
|
|
40
|
+
wordCount > 0 && (react_1.default.createElement("span", null,
|
|
41
|
+
date && date !== '--' ? ' · ' : '',
|
|
42
|
+
(0, formatReadingStats_1.formatReadingStats)(wordCount, readingTimeMinutes, props.lang)))),
|
|
34
43
|
react_1.default.createElement("div", { className: "tags" },
|
|
35
44
|
react_1.default.createElement(TagList_1.TagList, { tags: tags }))));
|
|
36
45
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatReadingStats = formatReadingStats;
|
|
4
|
+
/**
|
|
5
|
+
* 格式化字数和阅读时长的显示文案(多语言)
|
|
6
|
+
*/
|
|
7
|
+
function formatReadingStats(wordCount, readingTimeMinutes, lang) {
|
|
8
|
+
const formattedCount = wordCount.toLocaleString('en-US');
|
|
9
|
+
if (lang.startsWith('zh')) {
|
|
10
|
+
return `${formattedCount} 字 · 约 ${readingTimeMinutes} 分钟阅读`;
|
|
11
|
+
}
|
|
12
|
+
if (lang.startsWith('ja')) {
|
|
13
|
+
return `${formattedCount} 文字 · 約 ${readingTimeMinutes} 分で読めます`;
|
|
14
|
+
}
|
|
15
|
+
if (lang.startsWith('ko')) {
|
|
16
|
+
return `${formattedCount} 자 · 약 ${readingTimeMinutes} 분 소요`;
|
|
17
|
+
}
|
|
18
|
+
const wordLabel = wordCount === 1 ? 'word' : 'words';
|
|
19
|
+
return `${formattedCount} ${wordLabel} · ~${readingTimeMinutes} min read`;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=formatReadingStats.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 多语言感知的字数统计与阅读时长估算
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.countWords = countWords;
|
|
7
|
+
exports.estimateReadingTime = estimateReadingTime;
|
|
8
|
+
// CJK 字符正则:中文、日文假名、韩文
|
|
9
|
+
const CJK_REGEX = /[\u4e00-\u9fff\u3400-\u4dbf\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g;
|
|
10
|
+
/**
|
|
11
|
+
* 从 Markdown 文本中剥离语法标记,保留可读文本
|
|
12
|
+
*/
|
|
13
|
+
function stripMarkdown(md) {
|
|
14
|
+
return (md
|
|
15
|
+
// 移除代码块
|
|
16
|
+
.replace(/```[\s\S]*?```/g, '')
|
|
17
|
+
// 移除行内代码
|
|
18
|
+
.replace(/`[^`]*`/g, '')
|
|
19
|
+
// 移除 HTML 标签
|
|
20
|
+
.replace(/<[^>]+>/g, '')
|
|
21
|
+
// 移除图片 
|
|
22
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
|
|
23
|
+
// 链接 [text](url) -> 保留 text
|
|
24
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
25
|
+
// 移除标题标记
|
|
26
|
+
.replace(/^#{1,6}\s+/gm, '')
|
|
27
|
+
// 移除粗体/斜体标记
|
|
28
|
+
.replace(/(\*{1,3}|_{1,3})(.*?)\1/g, '$2')
|
|
29
|
+
// 移除删除线
|
|
30
|
+
.replace(/~~(.*?)~~/g, '$1')
|
|
31
|
+
// 移除引用标记
|
|
32
|
+
.replace(/^\s*>\s?/gm, '')
|
|
33
|
+
// 移除水平线
|
|
34
|
+
.replace(/^[-*_]{3,}\s*$/gm, '')
|
|
35
|
+
// 移除列表标记
|
|
36
|
+
.replace(/^\s*[-*+]\s+/gm, '')
|
|
37
|
+
.replace(/^\s*\d+\.\s+/gm, ''));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 统计 Markdown 文本的字数(多语言感知)
|
|
41
|
+
* @param markdownBody 去除 frontmatter 后的 Markdown 文本
|
|
42
|
+
*/
|
|
43
|
+
function countWords(markdownBody) {
|
|
44
|
+
const text = stripMarkdown(markdownBody);
|
|
45
|
+
// 统计 CJK 字符
|
|
46
|
+
const cjkMatches = text.match(CJK_REGEX);
|
|
47
|
+
const cjk = cjkMatches ? cjkMatches.length : 0;
|
|
48
|
+
// 移除 CJK 字符后,按空白分词统计拉丁单词
|
|
49
|
+
const latinText = text.replace(CJK_REGEX, ' ').trim();
|
|
50
|
+
const latinWords = latinText.split(/\s+/).filter(w => w.length > 0);
|
|
51
|
+
const latin = latinWords.length;
|
|
52
|
+
return { total: cjk + latin, cjk, latin };
|
|
53
|
+
}
|
|
54
|
+
// 阅读速度(字/分钟)
|
|
55
|
+
const CJK_WPM = 300;
|
|
56
|
+
const LATIN_WPM = 225;
|
|
57
|
+
/**
|
|
58
|
+
* 估算阅读时长(分钟)
|
|
59
|
+
* @param markdownBody 去除 frontmatter 后的 Markdown 文本
|
|
60
|
+
*/
|
|
61
|
+
function estimateReadingTime(markdownBody) {
|
|
62
|
+
const { cjk, latin } = countWords(markdownBody);
|
|
63
|
+
const minutes = cjk / CJK_WPM + latin / LATIN_WPM;
|
|
64
|
+
return Math.max(1, Math.ceil(minutes));
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=wordCount.js.map
|