kernel/artifacts/
speech_text.rs1use std::sync::LazyLock;
6
7use regex::Regex;
8
9static INLINE: LazyLock<Vec<(Regex, &'static str)>> = LazyLock::new(|| {
14 [
15 (r"^\s{0,3}#{1,6}\s+", ""),
16 (r"^\s{0,3}>\s?", ""),
17 (r"^(\s*)[-*+]\s+", "$1"),
18 (r"!?\[([^\]]*)\]\([^)]*\)", "$1"),
19 (r"\*{1,3}([^*]+)\*{1,3}", "$1"),
20 (r"_{1,3}([^_]+)_{1,3}", "$1"),
21 (r"`([^`]*)`", "$1"),
22 ]
23 .into_iter()
24 .filter_map(|(pattern, replacement)| Regex::new(pattern).ok().map(|re| (re, replacement)))
25 .collect()
26});
27
28static BLANK_RUN: LazyLock<Option<Regex>> = LazyLock::new(|| Regex::new(r"\n{3,}").ok());
30
31pub fn speakable(markdown: &str) -> String {
35 let mut lines: Vec<String> = Vec::new();
36 let mut inside_fence = false;
37 for line in markdown.split('\n') {
38 let trimmed = trim_spaces(line);
39 if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
40 inside_fence = !inside_fence;
41 continue;
42 }
43 if inside_fence {
44 continue;
45 }
46 if trimmed.starts_with('|') && trimmed.ends_with('|') {
47 continue;
48 }
49 lines.push(strip_inline(line));
50 }
51 let joined = lines.join("\n");
52 let collapsed = match &*BLANK_RUN {
53 Some(re) => re.replace_all(&joined, "\n\n").into_owned(),
54 None => joined,
55 };
56 collapsed.trim().to_owned()
57}
58
59fn strip_inline(line: &str) -> String {
60 let mut text = line.to_owned();
61 for (pattern, replacement) in INLINE.iter() {
62 text = pattern.replace_all(&text, *replacement).into_owned();
63 }
64 text
65}
66
67fn trim_spaces(line: &str) -> &str {
71 line.trim_matches(is_horizontal_ws)
72}
73
74fn is_horizontal_ws(c: char) -> bool {
78 c.is_whitespace()
79 && !matches!(
80 c,
81 '\n' | '\r' | '\u{0B}' | '\u{0C}' | '\u{85}' | '\u{2028}' | '\u{2029}'
82 )
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn a_code_fence_and_its_body_are_dropped() {
91 let markdown = "before\n```rust\nlet x = 1;\n```\nafter";
92 assert_eq!(speakable(markdown), "before\nafter");
93 }
94
95 #[test]
96 fn a_tilde_fence_toggles_the_same_way() {
97 let markdown = "a\n~~~\nhidden\n~~~\nb";
98 assert_eq!(speakable(markdown), "a\nb");
99 }
100
101 #[test]
102 fn table_rows_are_skipped() {
103 let markdown = "text\n| col | col |\n| --- | --- |\nmore";
104 assert_eq!(speakable(markdown), "text\nmore");
105 }
106
107 #[test]
108 fn headings_lose_their_hashes() {
109 assert_eq!(speakable("## Title"), "Title");
110 assert_eq!(speakable(" ###### Deep"), "Deep");
111 }
112
113 #[test]
114 fn blockquotes_lose_their_marker() {
115 assert_eq!(speakable("> quoted"), "quoted");
116 assert_eq!(speakable(">no space"), "no space");
117 }
118
119 #[test]
120 fn list_markers_go_but_indentation_stays() {
121 assert_eq!(speakable("- item"), "item");
122 assert_eq!(speakable("+ plus"), "plus");
123 assert_eq!(speakable("top\n * nested\nend"), "top\n nested\nend");
126 }
127
128 #[test]
129 fn links_and_images_keep_only_their_label() {
130 assert_eq!(speakable("see [the docs](https://x.y)"), "see the docs");
131 assert_eq!(speakable(""), "alt text");
132 }
133
134 #[test]
135 fn emphasis_and_inline_code_are_unwrapped() {
136 assert_eq!(speakable("**bold** and *italic*"), "bold and italic");
137 assert_eq!(speakable("_under_ and __strong__"), "under and strong");
138 assert_eq!(speakable("run `cargo test` now"), "run cargo test now");
139 }
140
141 #[test]
142 fn runs_of_blank_lines_collapse_to_one_paragraph_break() {
143 let markdown = "a\n\n\n\nb";
144 assert_eq!(speakable(markdown), "a\n\nb");
145 }
146
147 #[test]
148 fn leading_and_trailing_whitespace_is_trimmed() {
149 assert_eq!(speakable("\n\n hello \n\n"), "hello");
150 }
151
152 #[test]
153 fn an_empty_string_stays_empty() {
154 assert_eq!(speakable(""), "");
155 }
156
157 #[test]
158 fn a_fence_led_by_a_non_breaking_space_is_still_a_fence() {
159 let markdown = "\u{A0}```\ncode\n```";
162 assert_eq!(speakable(markdown), "");
163 }
164
165 #[test]
166 fn a_table_row_with_a_trailing_nbsp_is_still_skipped() {
167 let markdown = "text\n| a | b |\u{A0}\nmore";
168 assert_eq!(speakable(markdown), "text\nmore");
169 }
170}