1use std::sync::LazyLock;
6
7static CODE_BLOCK_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
9 regex::Regex::new(r"```[\s\S]*?```").expect("CODE_BLOCK_RE 正则模式应在编译期通过校验")
10});
11
12static INLINE_CODE_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
14 regex::Regex::new(r"`[^`]*`").expect("INLINE_CODE_RE 正则模式应在编译期通过校验")
15});
16
17static LINK_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
19 regex::Regex::new(r"\[([^\]]*)\]\([^)]*\)").expect("LINK_RE 正则模式应在编译期通过校验")
20});
21
22static HEADING_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
24 regex::Regex::new(r"^#{1,6}\s*").expect("HEADING_RE 正则模式应在编译期通过校验")
25});
26
27static IMAGE_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
29 regex::Regex::new(r"!\[([^\]]*)\]\([^)]*\)").expect("IMAGE_RE 正则模式应在编译期通过校验")
30});
31
32static WHITESPACE_RE: LazyLock<regex::Regex> =
34 LazyLock::new(|| regex::Regex::new(r"\s+").expect("WHITESPACE_RE 正则模式应在编译期通过校验"));
35
36pub fn strip_markdown(md: &str) -> String {
40 let mut plain = CODE_BLOCK_RE.replace_all(md, "").to_string();
41 plain = INLINE_CODE_RE.replace_all(&plain, "").to_string();
42 plain = IMAGE_RE.replace_all(&plain, "").to_string();
44 plain = LINK_RE.replace_all(&plain, "$1").to_string();
45 plain = HEADING_RE.replace_all(&plain, "").to_string();
46 plain = plain
47 .replace("**", "")
48 .replace('*', "")
49 .replace("__", "")
50 .replace('_', "");
51 plain = WHITESPACE_RE.replace_all(&plain, " ").to_string();
52 plain.trim().to_string()
53}
54
55pub fn count_words(md: &str) -> u32 {
60 let plain = strip_markdown(md);
61 let mut count = 0u32;
62 let mut in_word = false;
63
64 for c in plain.chars() {
65 if c as u32 >= 0x4E00 && c as u32 <= 0x9FFF {
67 count += 1;
68 in_word = false;
69 } else if c.is_alphabetic() {
70 if !in_word {
71 count += 1;
72 in_word = true;
73 }
74 } else {
75 in_word = false;
76 }
77 }
78 count.max(1)
79}
80
81pub fn reading_time(word_count: u32) -> u32 {
85 (word_count / 200).max(1)
86}
87
88pub fn auto_summary(md: &str) -> String {
90 let plain = strip_markdown(md);
91 plain.chars().take(200).collect()
92}
93
94#[cfg(all(test, feature = "server"))]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn strip_markdown_removes_code_blocks() {
100 let input = "before```code here```after";
101 assert_eq!(strip_markdown(input), "beforeafter");
102 }
103
104 #[test]
105 fn strip_markdown_removes_inline_code() {
106 assert_eq!(strip_markdown("text `code` more"), "text more");
107 }
108
109 #[test]
110 fn strip_markdown_removes_images() {
111 assert_eq!(strip_markdown(""), "");
112 }
113
114 #[test]
115 fn strip_markdown_keeps_link_text() {
116 assert_eq!(
117 strip_markdown("[click me](https://example.com)"),
118 "click me"
119 );
120 }
121
122 #[test]
123 fn strip_markdown_removes_headings() {
124 assert_eq!(strip_markdown("## Hello"), "Hello");
125 }
126
127 #[test]
128 fn strip_markdown_removes_bold_and_italic() {
129 assert_eq!(
130 strip_markdown("**bold** *italic* __bold__ _italic_"),
131 "bold italic bold italic"
132 );
133 }
134
135 #[test]
136 fn strip_markdown_empty_input() {
137 assert_eq!(strip_markdown(""), "");
138 }
139
140 #[test]
141 fn strip_markdown_mixed() {
142 let md = "# Title\n\nSome **bold** and `code` [link](url)\n\n";
143 let result = strip_markdown(md);
144 assert_eq!(result, "Title Some bold and link");
145 }
146
147 #[test]
148 fn count_words_english() {
149 assert_eq!(count_words("hello world"), 2);
150 }
151
152 #[test]
153 fn count_words_chinese() {
154 assert_eq!(count_words("你好世界"), 4);
155 }
156
157 #[test]
158 fn count_words_mixed() {
159 let count = count_words("Hello 你好 world 世界");
160 assert_eq!(count, 6);
161 }
162
163 #[test]
164 fn count_words_with_markdown() {
165 let count = count_words("# Hello **World**\n\nSome `code` here");
166 assert_eq!(count, 4);
167 }
168
169 #[test]
170 fn count_words_empty_returns_one() {
171 assert_eq!(count_words(""), 1);
172 }
173
174 #[test]
175 fn reading_time_defaults_to_one() {
176 assert_eq!(reading_time(0), 1);
177 assert_eq!(reading_time(1), 1);
178 assert_eq!(reading_time(199), 1);
179 }
180
181 #[test]
182 fn reading_time_scales_by_two_hundred() {
183 assert_eq!(reading_time(200), 1);
184 assert_eq!(reading_time(201), 1);
185 assert_eq!(reading_time(400), 2);
186 assert_eq!(reading_time(1000), 5);
187 }
188
189 #[test]
190 fn auto_summary_truncates_at_200_chars() {
191 let long_md: String = "a ".repeat(200);
192 let summary = auto_summary(&long_md);
193 assert_eq!(summary.chars().count(), 200);
194 }
195
196 #[test]
197 fn auto_summary_short_input() {
198 assert_eq!(auto_summary("short"), "short");
199 }
200
201 #[test]
202 fn auto_summary_strips_markdown() {
203 let summary = auto_summary("**bold** and `code`");
204 assert_eq!(summary, "bold and");
205 }
206}