1pub fn sanitize_ambient_text(raw: &str) -> String {
17 let mut s = raw.trim().to_string();
18 for _ in 0..4 {
19 let before = s.clone();
20 if let Some(inner) = strip_wrapping_fence(&s) {
21 s = inner;
22 }
23 if let Some(inner) = strip_wrapping_quotes(&s) {
24 s = inner;
25 }
26 if let Some(rest) = strip_conversational_preamble(&s) {
27 s = rest;
28 }
29 if s == before {
30 break;
31 }
32 }
33 if !s.is_empty() && s.chars().all(|c| c == '`') {
34 s.clear();
35 }
36 s
37}
38
39fn strip_wrapping_fence(s: &str) -> Option<String> {
44 let s = s.trim();
45 for fence in ["```", "`"] {
46 if s.len() >= fence.len() * 2 && s.starts_with(fence) && s.ends_with(fence) {
47 let mut inner = &s[fence.len()..s.len() - fence.len()];
48 if fence == "```" {
49 if let Some(nl) = inner.find('\n') {
50 inner = &inner[nl + 1..];
51 }
52 }
53 return Some(inner.trim().to_string());
54 }
55 }
56 None
57}
58
59fn strip_wrapping_quotes(s: &str) -> Option<String> {
62 const PAIRS: &[(char, char)] = &[('"', '"'), ('\'', '\''), ('\u{201C}', '\u{201D}'), ('\u{2018}', '\u{2019}')];
63 let s = s.trim();
64 for &(open, close) in PAIRS {
65 if let Some(inner) = s.strip_prefix(open).and_then(|r| r.strip_suffix(close)) {
66 if !inner.contains(open) && !inner.contains(close) {
67 return Some(inner.trim().to_string());
68 }
69 }
70 }
71 None
72}
73
74const CONVERSATIONAL_PREAMBLES: &[&str] = &[
82 "let's go ahead and ",
83 "lets go ahead and ",
84 "yeah, let's ",
85 "yeah let's ",
86 "yeah, lets ",
87 "yeah lets ",
88 "sure, let's ",
89 "sure let's ",
90 "ok, let's ",
91 "okay, let's ",
92 "alright, let's ",
93 "go ahead and ",
94 "let's ",
95 "lets ",
96 "sure, i'll ",
97 "sure, i will ",
98 "sure, ",
99 "yeah, ",
100 "yeah ",
101 "ok, ",
102 "okay, ",
103 "alright, ",
104 "i'll ",
105 "i will ",
106 "i should ",
107 "we should ",
108 "next up, ",
109 "next, ",
110];
111
112fn strip_conversational_preamble(s: &str) -> Option<String> {
120 for prefix in CONVERSATIONAL_PREAMBLES {
121 if let Some(byte_len) = case_insensitive_prefix_byte_len(s, prefix) {
122 let rest = &s[byte_len..];
123 let mut chars = rest.chars();
124 return Some(match chars.next() {
125 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
126 None => String::new(),
127 });
128 }
129 }
130 None
131}
132
133fn case_insensitive_prefix_byte_len(s: &str, prefix: &str) -> Option<usize> {
141 let mut prefix_chars = prefix.chars().peekable();
142 for (byte_idx, c) in s.char_indices() {
143 if prefix_chars.peek().is_none() {
144 return Some(byte_idx);
145 }
146 for lc in c.to_lowercase() {
147 if prefix_chars.next_if_eq(&lc).is_none() {
148 return None;
149 }
150 }
151 }
152 if prefix_chars.peek().is_none() {
153 Some(s.len())
154 } else {
155 None
156 }
157}
158
159#[cfg(test)]
160mod sanitize_ambient_text_tests {
161 use super::*;
162
163 #[test]
164 fn passes_plain_text_through_unchanged() {
165 assert_eq!(sanitize_ambient_text("Run the tests"), "Run the tests");
166 assert_eq!(sanitize_ambient_text("ANSWER: Run the tests"), "ANSWER: Run the tests");
167 }
168
169 #[test]
170 fn strips_a_triple_backtick_fence() {
171 assert_eq!(sanitize_ambient_text("```\nRun the tests\n```"), "Run the tests");
172 }
173
174 #[test]
175 fn strips_a_fence_with_a_language_tag() {
176 assert_eq!(sanitize_ambient_text("```text\nRun the tests\n```"), "Run the tests");
177 }
178
179 #[test]
180 fn empty_fence_collapses_to_empty_string() {
181 assert_eq!(sanitize_ambient_text("```\n```"), "");
182 assert_eq!(sanitize_ambient_text("``````"), "");
183 }
184
185 #[test]
186 fn strips_wrapping_single_backticks() {
187 assert_eq!(sanitize_ambient_text("`Run the tests`"), "Run the tests");
188 }
189
190 #[test]
191 fn strips_one_wrapping_pair_of_quotes() {
192 assert_eq!(sanitize_ambient_text("\"Run the tests\""), "Run the tests");
193 assert_eq!(sanitize_ambient_text("'Run the tests'"), "Run the tests");
194 assert_eq!(sanitize_ambient_text("\u{201C}Run the tests\u{201D}"), "Run the tests");
195 }
196
197 #[test]
200 fn a_quote_at_only_one_end_is_text() {
201 assert_eq!(sanitize_ambient_text("Rename it to \"foo\""), "Rename it to \"foo\"");
202 assert_eq!(sanitize_ambient_text("\"foo\" is the new name"), "\"foo\" is the new name");
203 assert_eq!(sanitize_ambient_text("\"foo\" and \"bar\""), "\"foo\" and \"bar\"");
204 }
205
206 #[test]
207 fn strips_nested_fence_and_quotes() {
208 assert_eq!(sanitize_ambient_text("```\n\"Run the tests\"\n```"), "Run the tests");
209 }
210
211 #[test]
212 fn leaves_an_embedded_fence_like_substring_alone() {
213 let s = "Run `npm test` next";
214 assert_eq!(sanitize_ambient_text(s), s);
215 }
216
217 #[test]
218 fn lone_backticks_with_no_content_collapse_to_empty() {
219 assert_eq!(sanitize_ambient_text("```"), "");
220 }
221
222 #[test]
223 fn strips_yeah_lets_preamble() {
224 assert_eq!(
225 sanitize_ambient_text("Yeah, let's debug the blank preview bug next"),
226 "Debug the blank preview bug next"
227 );
228 assert_eq!(sanitize_ambient_text("Yeah let's fix the login bug"), "Fix the login bug");
229 }
230
231 #[test]
232 fn strips_go_ahead_and_preamble() {
233 assert_eq!(
234 sanitize_ambient_text("Go ahead and add tests for the parser"),
235 "Add tests for the parser"
236 );
237 }
238
239 #[test]
240 fn strips_sure_ok_alright_preamble() {
241 assert_eq!(sanitize_ambient_text("Sure, fix the typo"), "Fix the typo");
242 assert_eq!(sanitize_ambient_text("OK, run the tests"), "Run the tests");
243 assert_eq!(sanitize_ambient_text("Alright, let's ship it"), "Ship it");
244 }
245
246 #[test]
247 fn strips_preamble_from_inside_quotes_and_fences() {
248 assert_eq!(sanitize_ambient_text("\"Yeah, let's fix the bug\""), "Fix the bug");
249 assert_eq!(sanitize_ambient_text("```\nYeah, let's fix the bug\n```"), "Fix the bug");
250 }
251
252 #[test]
253 fn leaves_a_mid_sentence_lets_alone() {
254 let s = "Check whether the retry logic still lets errors through";
255 assert_eq!(sanitize_ambient_text(s), s);
256 }
257
258 #[test]
259 fn a_filler_word_with_no_trailing_content_is_left_alone() {
260 assert_eq!(sanitize_ambient_text("Yeah,"), "Yeah,");
266 }
267
268 #[test]
269 fn preamble_strip_handles_byte_length_changing_case_folding() {
270 let s = "O\u{212A}, run the tests";
276 assert_eq!(sanitize_ambient_text(s), "Run the tests");
277 }
278
279 #[test]
280 fn preamble_strip_handles_char_count_changing_case_folding() {
281 let s = "\u{0130} think we should refactor this";
286 assert_eq!(sanitize_ambient_text(s), s);
287 }
288}