czon 0.9.3 → 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.
@@ -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);
@@ -61,13 +61,13 @@ const ContentPage = props => {
61
61
  react_1.default.createElement(PageLayout_1.PageLayout, { header: react_1.default.createElement(CZONHeader_1.CZONHeader, { ctx: props.ctx, lang: props.lang, file: props.file }), navigator: react_1.default.createElement("nav", { className: "sidebar border-r" },
62
62
  react_1.default.createElement(Navigator_1.Navigator, { ctx: props.ctx, file: props.file, lang: props.lang })), rightSidebar: react_1.default.createElement("aside", { className: "sidebar border-l" },
63
63
  react_1.default.createElement("h2", { className: "text-2xl font-semibold mb-2" }, "Table of Contents"),
64
- props.content.headings.map(heading => (react_1.default.createElement("a", { key: heading.id, href: `#${heading.id}`, className: `block ms-${(heading.depth - 1) * 4} mb-2 border-b` }, heading.text)))), main: react_1.default.createElement("main", { className: "content max-w-4xl mx-auto my-8 px-4" },
64
+ props.content.headings.map(heading => (react_1.default.createElement("a", { key: heading.id, href: `#${heading.id}`, className: `block ms-${(heading.depth - 1) * 4} mb-2 border-b`, dangerouslySetInnerHTML: { __html: heading.html } })))), main: react_1.default.createElement("main", { className: "content max-w-4xl mx-auto my-8 px-4" },
65
65
  react_1.default.createElement(ContentMeta_1.ContentMeta, { ctx: props.ctx, file: props.file, lang: props.lang }),
66
66
  react_1.default.createElement("div", { className: "border-b mb-4 pb-2 xl:hidden" },
67
67
  react_1.default.createElement("h2", { className: "text-2xl font-semibold mb-2" }, "Table of Contents"),
68
68
  props.content.headings.map(heading => (react_1.default.createElement("a", { key: heading.id, href: `#${heading.id}`,
69
69
  // 按照 heading.depth 设置缩进
70
- className: `block ps-${heading.depth * 4} mb-2` }, heading.text)))),
70
+ className: `block ps-${heading.depth * 4} mb-2`, dangerouslySetInnerHTML: { __html: heading.html } })))),
71
71
  react_1.default.createElement("div", { className: "content-body" },
72
72
  react_1.default.createElement("article", { dangerouslySetInnerHTML: { __html: props.content.body } }),
73
73
  relatedContents.length > 0 && (react_1.default.createElement(react_1.default.Fragment, null,
@@ -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
- date && date !== '--' && react_1.default.createElement("div", null,
32
- "\uD83D\uDCC5 ",
33
- date),
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
@@ -100,13 +100,16 @@ const convertMarkdownToHtml = (article, path, lang, mdContent) => {
100
100
  }
101
101
  }
102
102
  })();
103
+ // 使用 parseInline 渲染 inline tokens(KaTeX、加粗、斜体等)
104
+ const renderedHtml = this.parser.parseInline(heading.tokens);
103
105
  headings.push({
104
106
  id,
105
107
  text: heading.text,
108
+ html: renderedHtml,
106
109
  depth: heading.depth,
107
110
  });
108
111
  // TODO: 处理重复的标题文本以避免重复的 id
109
- return `<h${heading.depth} id="${id}" class="anchor-heading">${heading.text}</h${heading.depth}>`;
112
+ return `<h${heading.depth} id="${id}" class="anchor-heading">${renderedHtml}</h${heading.depth}>`;
110
113
  };
111
114
  // 重写代码块渲染器以支持 Mermaid - 使用 any 类型绕过类型检查
112
115
  renderer.code = function (code, language, isEscaped) {
@@ -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
+ // 移除图片 ![alt](url)
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "czon",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "CZON - AI enhanced Markdown content engine",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -61,7 +61,6 @@
61
61
  "marked": "^17.0.1",
62
62
  "marked-footnote": "^1.4.0",
63
63
  "marked-katex-extension": "^5.1.6",
64
- "minimatch": "^10.1.1",
65
64
  "react": "^19.2.3",
66
65
  "react-dom": "^19.2.3",
67
66
  "yaml": "^2.8.2"