1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct ParsedBlockquotePrefix<'a> {
13 pub indent: &'a str,
15 pub prefix: &'a str,
17 pub content: &'a str,
19 pub nesting_level: usize,
21 pub spaces_after_marker: &'a str,
23}
24
25#[inline]
26fn is_space_or_tab(byte: u8) -> bool {
27 byte == b' ' || byte == b'\t'
28}
29
30#[inline]
35pub fn parse_blockquote_prefix(line: &str) -> Option<ParsedBlockquotePrefix<'_>> {
36 let bytes = line.as_bytes();
37 let mut pos = 0;
38
39 while pos < bytes.len() && is_space_or_tab(bytes[pos]) {
41 pos += 1;
42 }
43 let indent_end = pos;
44
45 if pos >= bytes.len() || bytes[pos] != b'>' {
46 return None;
47 }
48
49 let mut nesting_level = 0;
50 let mut prefix_end = pos;
51 let mut spaces_after_marker_start = pos;
52 let mut spaces_after_marker_end = pos;
53
54 loop {
55 if pos >= bytes.len() || bytes[pos] != b'>' {
56 break;
57 }
58
59 nesting_level += 1;
60 pos += 1; let marker_end = pos;
62
63 if pos < bytes.len() && is_space_or_tab(bytes[pos]) {
65 pos += 1;
66 }
67 let content_start_candidate = pos;
68
69 while pos < bytes.len() && is_space_or_tab(bytes[pos]) {
71 pos += 1;
72 }
73
74 if pos < bytes.len() && bytes[pos] == b'>' {
76 continue;
77 }
78
79 prefix_end = content_start_candidate;
81 spaces_after_marker_start = marker_end;
82 spaces_after_marker_end = pos;
83 break;
84 }
85
86 Some(ParsedBlockquotePrefix {
87 indent: &line[..indent_end],
88 prefix: &line[..prefix_end],
89 content: &line[prefix_end..],
90 nesting_level,
91 spaces_after_marker: &line[spaces_after_marker_start..spaces_after_marker_end],
92 })
93}
94
95pub fn effective_indent_in_blockquote(line_content: &str, expected_bq_level: usize, fallback_indent: usize) -> usize {
132 if expected_bq_level == 0 {
133 return fallback_indent;
134 }
135
136 let line_bq_level = line_content
139 .chars()
140 .take_while(|c| *c == '>' || c.is_whitespace())
141 .filter(|&c| c == '>')
142 .count();
143
144 if line_bq_level != expected_bq_level {
145 return fallback_indent;
146 }
147
148 let mut pos = 0;
150 let mut found_markers = 0;
151 for c in line_content.chars() {
152 pos += c.len_utf8();
153 if c == '>' {
154 found_markers += 1;
155 if found_markers == line_bq_level {
156 if line_content.get(pos..pos + 1) == Some(" ") {
158 pos += 1;
159 }
160 break;
161 }
162 }
163 }
164
165 let after_bq = &line_content[pos..];
166 after_bq.len() - after_bq.trim_start().len()
167}
168
169pub fn count_blockquote_level(line_content: &str) -> usize {
184 line_content
185 .chars()
186 .take_while(|c| *c == '>' || c.is_whitespace())
187 .filter(|&c| c == '>')
188 .count()
189}
190
191pub fn content_after_blockquote(line_content: &str, expected_bq_level: usize) -> &str {
207 if expected_bq_level == 0 {
208 return line_content;
209 }
210
211 let actual_level = count_blockquote_level(line_content);
213 if actual_level != expected_bq_level {
214 return line_content;
215 }
216
217 let mut pos = 0;
218 let mut found_markers = 0;
219 for c in line_content.chars() {
220 pos += c.len_utf8();
221 if c == '>' {
222 found_markers += 1;
223 if found_markers == expected_bq_level {
224 if line_content.get(pos..pos + 1) == Some(" ") {
226 pos += 1;
227 }
228 break;
229 }
230 }
231 }
232
233 &line_content[pos..]
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
245 fn test_parse_blockquote_prefix_compact_nested() {
246 let parsed = parse_blockquote_prefix(">> text").expect("should parse compact nested blockquote");
247 assert_eq!(parsed.indent, "");
248 assert_eq!(parsed.prefix, ">> ");
249 assert_eq!(parsed.content, "text");
250 assert_eq!(parsed.nesting_level, 2);
251 assert_eq!(parsed.spaces_after_marker, " ");
252 }
253
254 #[test]
255 fn test_parse_blockquote_prefix_spaced_nested() {
256 let parsed = parse_blockquote_prefix("> > text").expect("should parse spaced nested blockquote");
257 assert_eq!(parsed.indent, "");
258 assert_eq!(parsed.prefix, "> > ");
259 assert_eq!(parsed.content, " text");
260 assert_eq!(parsed.nesting_level, 2);
261 assert_eq!(parsed.spaces_after_marker, " ");
262 }
263
264 #[test]
265 fn test_parse_blockquote_prefix_with_indent() {
266 let parsed = parse_blockquote_prefix(" > quote").expect("should parse indented blockquote");
267 assert_eq!(parsed.indent, " ");
268 assert_eq!(parsed.prefix, " > ");
269 assert_eq!(parsed.content, "quote");
270 assert_eq!(parsed.nesting_level, 1);
271 assert_eq!(parsed.spaces_after_marker, " ");
272 }
273
274 #[test]
275 fn test_parse_blockquote_prefix_non_blockquote() {
276 assert!(parse_blockquote_prefix("plain text").is_none());
277 assert!(parse_blockquote_prefix(" plain text").is_none());
278 }
279
280 #[test]
285 fn test_effective_indent_no_blockquote_context() {
286 assert_eq!(effective_indent_in_blockquote("text", 0, 0), 0);
288 assert_eq!(effective_indent_in_blockquote(" text", 0, 3), 3);
289 assert_eq!(effective_indent_in_blockquote("> text", 0, 5), 5);
290 }
291
292 #[test]
293 fn test_effective_indent_single_level_blockquote() {
294 assert_eq!(effective_indent_in_blockquote("> text", 1, 99), 0);
296 assert_eq!(effective_indent_in_blockquote("> text", 1, 99), 1);
297 assert_eq!(effective_indent_in_blockquote("> text", 1, 99), 2);
298 assert_eq!(effective_indent_in_blockquote("> text", 1, 99), 3);
299 }
300
301 #[test]
302 fn test_effective_indent_no_space_after_marker() {
303 assert_eq!(effective_indent_in_blockquote(">text", 1, 99), 0);
305 assert_eq!(effective_indent_in_blockquote(">>text", 2, 99), 0);
306 }
307
308 #[test]
309 fn test_effective_indent_nested_blockquote_compact() {
310 assert_eq!(effective_indent_in_blockquote(">> text", 2, 99), 0);
312 assert_eq!(effective_indent_in_blockquote(">> text", 2, 99), 1);
313 assert_eq!(effective_indent_in_blockquote(">> text", 2, 99), 2);
314 }
315
316 #[test]
317 fn test_effective_indent_nested_blockquote_spaced() {
318 assert_eq!(effective_indent_in_blockquote("> > text", 2, 99), 0);
320 assert_eq!(effective_indent_in_blockquote("> > text", 2, 99), 1);
321 assert_eq!(effective_indent_in_blockquote("> > text", 2, 99), 2);
322 }
323
324 #[test]
325 fn test_effective_indent_mismatched_level() {
326 assert_eq!(effective_indent_in_blockquote("> text", 2, 42), 42);
328 assert_eq!(effective_indent_in_blockquote(">> text", 1, 42), 42);
329 assert_eq!(effective_indent_in_blockquote("text", 1, 42), 42);
330 }
331
332 #[test]
333 fn test_effective_indent_empty_blockquote() {
334 assert_eq!(effective_indent_in_blockquote(">", 1, 99), 0);
336 assert_eq!(effective_indent_in_blockquote("> ", 1, 99), 0);
337 assert_eq!(effective_indent_in_blockquote("> ", 1, 99), 1);
338 }
339
340 #[test]
341 fn test_effective_indent_issue_268_case() {
342 assert_eq!(effective_indent_in_blockquote("> Opening the app", 1, 0), 2);
345 assert_eq!(
346 effective_indent_in_blockquote("> [**See preview here!**](https://example.com)", 1, 0),
347 2
348 );
349 }
350
351 #[test]
352 fn test_effective_indent_triple_nested() {
353 assert_eq!(effective_indent_in_blockquote("> > > text", 3, 99), 0);
355 assert_eq!(effective_indent_in_blockquote("> > > text", 3, 99), 1);
356 assert_eq!(effective_indent_in_blockquote(">>> text", 3, 99), 0);
357 assert_eq!(effective_indent_in_blockquote(">>> text", 3, 99), 1);
358 }
359
360 #[test]
365 fn test_count_blockquote_level_none() {
366 assert_eq!(count_blockquote_level("regular text"), 0);
367 assert_eq!(count_blockquote_level(" indented text"), 0);
368 assert_eq!(count_blockquote_level(""), 0);
369 }
370
371 #[test]
372 fn test_count_blockquote_level_single() {
373 assert_eq!(count_blockquote_level("> text"), 1);
374 assert_eq!(count_blockquote_level(">text"), 1);
375 assert_eq!(count_blockquote_level(">"), 1);
376 }
377
378 #[test]
379 fn test_count_blockquote_level_nested() {
380 assert_eq!(count_blockquote_level(">> text"), 2);
381 assert_eq!(count_blockquote_level("> > text"), 2);
382 assert_eq!(count_blockquote_level(">>> text"), 3);
383 assert_eq!(count_blockquote_level("> > > text"), 3);
384 }
385
386 #[test]
391 fn test_content_after_blockquote_no_quote() {
392 assert_eq!(content_after_blockquote("text", 0), "text");
393 assert_eq!(content_after_blockquote(" indented", 0), " indented");
394 }
395
396 #[test]
397 fn test_content_after_blockquote_single() {
398 assert_eq!(content_after_blockquote("> text", 1), "text");
399 assert_eq!(content_after_blockquote(">text", 1), "text");
400 assert_eq!(content_after_blockquote("> indented", 1), " indented");
401 }
402
403 #[test]
404 fn test_content_after_blockquote_nested() {
405 assert_eq!(content_after_blockquote(">> text", 2), "text");
406 assert_eq!(content_after_blockquote("> > text", 2), "text");
407 assert_eq!(content_after_blockquote("> > indented", 2), " indented");
408 }
409
410 #[test]
411 fn test_content_after_blockquote_mismatched_level() {
412 assert_eq!(content_after_blockquote("> text", 2), "> text");
414 assert_eq!(content_after_blockquote(">> text", 1), ">> text");
415 }
416}