1+ // 阅读时间估算插件
2+ import type MarkdownIt from 'markdown-it'
3+
4+ interface ReadingTimeStats {
5+ words : number
6+ minutes : number
7+ text : string
8+ }
9+
10+ // 中文字符阅读速度:300字/分钟
11+ // 英文单词阅读速度:160词/分钟
12+ const CHINESE_CHARS_PER_MINUTE = 300
13+ const ENGLISH_WORDS_PER_MINUTE = 160
14+
15+ export function readingTimePlugin ( md : MarkdownIt ) {
16+ // 保存原始的 render 方法
17+ const originalRender = md . render
18+
19+ md . render = function ( src : string , env ?: any ) : string {
20+ // 计算阅读时间
21+ const stats = calculateReadingTime ( src )
22+
23+ // 调用原始渲染方法
24+ let html = originalRender . call ( this , src , env )
25+
26+ // 检查是否应该显示阅读时间
27+ if ( shouldShowReadingTime ( src , env , stats ) ) {
28+ const readingTimeHtml = `
29+ <div class="reading-time-wrapper">
30+ <div class="reading-time">
31+ <svg class="reading-time-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
32+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
33+ <path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
34+ </svg>
35+ <span class="reading-time-text">${ stats . text } </span>
36+ </div>
37+ </div>`
38+
39+ // 在第一个 h1 后插入阅读时间
40+ html = html . replace ( / ( < h 1 [ ^ > ] * > .* ?< \/ h 1 > ) / , `$1${ readingTimeHtml } ` )
41+ }
42+
43+ return html
44+ }
45+ }
46+
47+ /**
48+ * 判断是否应该显示阅读时间
49+ */
50+ function shouldShowReadingTime ( src : string , env : any , stats : ReadingTimeStats ) : boolean {
51+ // 通过 frontmatter 配置控制(优先级最高)
52+ if ( env ?. frontmatter ?. readingTime === false ) {
53+ return false
54+ }
55+
56+ return true
57+ }
58+
59+ function calculateReadingTime ( content : string ) : ReadingTimeStats {
60+ // 分别提取和统计不同类型的内容
61+ const codeBlocks = extractCodeBlocks ( content )
62+ const inlineCode = extractInlineCode ( content )
63+ const plainText = extractPlainText ( content )
64+
65+ // 计算不同内容的阅读时间
66+ const codeBlockTime = calculateCodeTime ( codeBlocks )
67+ const inlineCodeTime = calculateInlineCodeTime ( inlineCode )
68+ const textTime = calculateTextTime ( plainText )
69+
70+ // 合并统计
71+ const totalWords = codeBlocks . totalChars + inlineCode . totalChars + plainText . chineseChars + plainText . englishWords
72+ const totalMinutes = Math . ceil ( codeBlockTime + inlineCodeTime + textTime )
73+ const readingMinutes = Math . max ( 1 , totalMinutes )
74+
75+ return {
76+ words : totalWords ,
77+ minutes : readingMinutes ,
78+ text : `约<span class="reading-time-count">${ totalWords } </span>字,阅读约<span class="reading-time-count">${ readingMinutes } </span>分钟`
79+ }
80+ }
81+
82+ /**
83+ * 提取代码块内容
84+ */
85+ function extractCodeBlocks ( content : string ) : { blocks : string [ ] , totalChars : number } {
86+ const codeBlockRegex = / ` ` ` [ \s \S ] * ?` ` ` / g
87+ const blocks = content . match ( codeBlockRegex ) || [ ]
88+
89+ // 计算代码块总字符数(包括中英文)
90+ const totalChars = blocks . reduce ( ( sum , block ) => {
91+ // 移除 ``` 标记,只统计实际代码内容
92+ const codeContent = block . replace ( / ^ ` ` ` [ ^ \n ] * \n / , '' ) . replace ( / \n ` ` ` $ / , '' )
93+ return sum + codeContent . length
94+ } , 0 )
95+
96+ return { blocks, totalChars }
97+ }
98+
99+ /**
100+ * 提取行内代码内容
101+ */
102+ function extractInlineCode ( content : string ) : { codes : string [ ] , totalChars : number } {
103+ // 先移除代码块,避免重复统计
104+ const contentWithoutBlocks = content . replace ( / ` ` ` [ \s \S ] * ?` ` ` / g, '' )
105+ const inlineCodeRegex = / ` [ ^ ` ] + ` / g
106+ const codes = contentWithoutBlocks . match ( inlineCodeRegex ) || [ ]
107+
108+ const totalChars = codes . reduce ( ( sum , code ) => {
109+ // 移除反引号,只统计实际代码内容
110+ const codeContent = code . replace ( / ` / g, '' )
111+ return sum + codeContent . length
112+ } , 0 )
113+
114+ return { codes, totalChars }
115+ }
116+
117+ /**
118+ * 提取纯文本内容
119+ */
120+ function extractPlainText ( content : string ) : { chineseChars : number , englishWords : number } {
121+ // 移除所有代码内容,只保留纯文本
122+ let cleanContent = content
123+ // 移除代码块
124+ . replace ( / ` ` ` [ \s \S ] * ?` ` ` / g, '' )
125+ // 移除行内代码
126+ . replace ( / ` [ ^ ` ] * ` / g, '' )
127+ // 移除 HTML 标签
128+ . replace ( / < [ ^ > ] * > / g, '' )
129+ // 移除 Markdown 语法
130+ . replace ( / # { 1 , 6 } \s / g, '' ) // 标题
131+ . replace ( / \[ ( [ ^ \] ] + ) \] \( [ ^ ) ] + \) / g, '$1' ) // 链接
132+ . replace ( / [ * _ ~ ] / g, '' ) // 格式化符号
133+ . replace ( / ^ \s * [ - * + ] \s / gm, '' ) // 列表符号
134+ . replace ( / ^ \s * \d + \. \s / gm, '' ) // 有序列表
135+ . replace ( / ^ \s * > \s / gm, '' ) // 引用
136+ . trim ( )
137+
138+ // 统计中文字符数(包括中文标点)
139+ const chineseChars = ( cleanContent . match ( / [ \u4e00 - \u9fa5 \u3000 - \u303f \uff00 - \uffef ] / g) || [ ] ) . length
140+
141+ // 统计英文单词数
142+ const englishWords = cleanContent
143+ . replace ( / [ \u4e00 - \u9fa5 \u3000 - \u303f \uff00 - \uffef ] / g, ' ' ) // 替换中文字符为空格
144+ . split ( / \s + / )
145+ . filter ( word => word . length > 0 && / [ a - z A - Z ] / . test ( word ) ) . length
146+
147+ return { chineseChars, englishWords }
148+ }
149+
150+ /**
151+ * 计算代码块阅读时间
152+ * 代码块需要更多时间理解,设置更低的阅读速度
153+ */
154+ function calculateCodeTime ( codeBlocks : { blocks : string [ ] , totalChars : number } ) : number {
155+ // 代码阅读速度:每分钟150字符(比普通文字慢很多)
156+ const CODE_CHARS_PER_MINUTE = 150
157+ return codeBlocks . totalChars / CODE_CHARS_PER_MINUTE
158+ }
159+
160+ /**
161+ * 计算行内代码阅读时间
162+ */
163+ function calculateInlineCodeTime ( inlineCode : { codes : string [ ] , totalChars : number } ) : number {
164+ // 行内代码阅读速度:每分钟200字符(比代码块快一些,但比普通文字慢)
165+ const INLINE_CODE_CHARS_PER_MINUTE = 200
166+ return inlineCode . totalChars / INLINE_CODE_CHARS_PER_MINUTE
167+ }
168+
169+ /**
170+ * 计算纯文本阅读时间
171+ */
172+ function calculateTextTime ( plainText : { chineseChars : number , englishWords : number } ) : number {
173+ // 中文字符阅读速度:300字/分钟
174+ // 英文单词阅读速度:160词/分钟
175+ const CHINESE_CHARS_PER_MINUTE = 300
176+ const ENGLISH_WORDS_PER_MINUTE = 160
177+
178+ const chineseMinutes = plainText . chineseChars / CHINESE_CHARS_PER_MINUTE
179+ const englishMinutes = plainText . englishWords / ENGLISH_WORDS_PER_MINUTE
180+
181+ return chineseMinutes + englishMinutes
182+ }
0 commit comments