Skip to content

Commit 80886cd

Browse files
author
sky.sun
committed
doc: 支持字数和时间显示
1 parent 69cb20b commit 80886cd

File tree

4 files changed

+262
-10
lines changed

4 files changed

+262
-10
lines changed

docs/.vitepress/config.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { defineConfig } from 'vitepress'
22
import { interactiveCodePlugin } from './plugins/interactive-code-plugin.ts'
3+
import { readingTimePlugin } from './plugins/reading-time-plugin.ts'
34

45
export default defineConfig({
56
title: 'Python 教程(进行中)',
@@ -15,6 +16,7 @@ export default defineConfig({
1516
markdown: {
1617
config: (md) => {
1718
interactiveCodePlugin(md);
19+
readingTimePlugin(md);
1820
}
1921
},
2022

@@ -54,16 +56,16 @@ export default defineConfig({
5456
{ text: '布尔值', link: '/guide/booleans' },
5557
]
5658
},
57-
// {
58-
// text: '数据结构',
59-
// collapsed: false,
60-
// items: [
61-
// { text: '列表', link: '/guide/lists' },
62-
// { text: '元组', link: '/guide/tuples' },
63-
// { text: '字典', link: '/guide/dictionaries' },
64-
// { text: '集合', link: '/guide/sets' },
65-
// ]
66-
// },
59+
{
60+
text: '数据结构',
61+
collapsed: false,
62+
items: [
63+
// { text: '列表', link: '/guide/lists' },
64+
// { text: '元组', link: '/guide/tuples' },
65+
// { text: '字典', link: '/guide/dictionaries' },
66+
// { text: '集合', link: '/guide/sets' },
67+
]
68+
},
6769
// {
6870
// text: '程序控制',
6971
// collapsed: false,
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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(/(<h1[^>]*>.*?<\/h1>)/, `$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-zA-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+
}

docs/.vitepress/theme/tailwind.css

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,70 @@ code {
1010
background-color: transparent;
1111
padding: 0;
1212
}
13+
14+
/* 阅读时间组件样式 */
15+
.reading-time-wrapper {
16+
margin: 1.5rem 0;
17+
}
18+
19+
.reading-time {
20+
display: inline-flex;
21+
align-items: center;
22+
gap: 0.5rem;
23+
color: var(--vp-c-text-2);
24+
font-size: 0.875rem;
25+
padding: 0.5rem 0.8rem;
26+
background-color: var(--vp-c-bg-soft);
27+
border-radius: 6px;
28+
border: 1px solid var(--vp-c-divider);
29+
transition: all 0.2s ease;
30+
cursor: default;
31+
}
32+
33+
.reading-time:hover {
34+
background-color: var(--vp-c-bg-mute);
35+
}
36+
37+
.reading-time-icon {
38+
width: 1rem;
39+
height: 1rem;
40+
flex-shrink: 0;
41+
stroke-width: 2;
42+
}
43+
44+
.reading-time-text {
45+
line-height: 1;
46+
font-weight: 500;
47+
}
48+
49+
.reading-time-count {
50+
margin: 0 0.15em;
51+
}
52+
53+
/* 移动端适配 */
54+
@media (max-width: 768px) {
55+
.reading-time {
56+
font-size: 0.8rem;
57+
padding: 0.4rem 0.8rem;
58+
}
59+
60+
.reading-time-icon {
61+
width: 0.875rem;
62+
height: 0.875rem;
63+
}
64+
65+
.reading-time-wrapper {
66+
margin: 1rem 0;
67+
}
68+
}
69+
70+
/* 暗色主题优化 */
71+
@media (prefers-color-scheme: dark) {
72+
.reading-time {
73+
background-color: var(--vp-c-bg-soft);
74+
}
75+
76+
.reading-time:hover {
77+
background-color: var(--vp-c-bg-mute);
78+
}
79+
}

docs/playground.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
footer: false
33
aside: false
44
editLink: false
5+
readingTime: false
56
---
67

78
<PlaygroundEditor />

0 commit comments

Comments
 (0)