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
169fn count_blockquote_level(line_content: &str) -> usize {
173 line_content
174 .chars()
175 .take_while(|c| *c == '>' || c.is_whitespace())
176 .filter(|&c| c == '>')
177 .count()
178}
179
180pub fn content_after_blockquote(line_content: &str, expected_bq_level: usize) -> &str {
196 if expected_bq_level == 0 {
197 return line_content;
198 }
199
200 let actual_level = count_blockquote_level(line_content);
202 if actual_level != expected_bq_level {
203 return line_content;
204 }
205
206 let mut pos = 0;
207 let mut found_markers = 0;
208 for c in line_content.chars() {
209 pos += c.len_utf8();
210 if c == '>' {
211 found_markers += 1;
212 if found_markers == expected_bq_level {
213 if line_content.get(pos..pos + 1) == Some(" ") {
215 pos += 1;
216 }
217 break;
218 }
219 }
220 }
221
222 &line_content[pos..]
223}
224
225pub fn strip_blockquote_prefix(line: &str) -> &str {
243 match parse_blockquote_prefix(line) {
244 Some(parsed) => parsed.content,
245 None => line,
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
258 fn test_parse_blockquote_prefix_compact_nested() {
259 let parsed = parse_blockquote_prefix(">> text").expect("should parse compact nested blockquote");
260 assert_eq!(parsed.indent, "");
261 assert_eq!(parsed.prefix, ">> ");
262 assert_eq!(parsed.content, "text");
263 assert_eq!(parsed.nesting_level, 2);
264 assert_eq!(parsed.spaces_after_marker, " ");
265 }
266
267 #[test]
268 fn test_parse_blockquote_prefix_spaced_nested() {
269 let parsed = parse_blockquote_prefix("> > text").expect("should parse spaced nested blockquote");
270 assert_eq!(parsed.indent, "");
271 assert_eq!(parsed.prefix, "> > ");
272 assert_eq!(parsed.content, " text");
273 assert_eq!(parsed.nesting_level, 2);
274 assert_eq!(parsed.spaces_after_marker, " ");
275 }
276
277 #[test]
278 fn test_parse_blockquote_prefix_with_indent() {
279 let parsed = parse_blockquote_prefix(" > quote").expect("should parse indented blockquote");
280 assert_eq!(parsed.indent, " ");
281 assert_eq!(parsed.prefix, " > ");
282 assert_eq!(parsed.content, "quote");
283 assert_eq!(parsed.nesting_level, 1);
284 assert_eq!(parsed.spaces_after_marker, " ");
285 }
286
287 #[test]
288 fn test_parse_blockquote_prefix_non_blockquote() {
289 assert!(parse_blockquote_prefix("plain text").is_none());
290 assert!(parse_blockquote_prefix(" plain text").is_none());
291 }
292
293 #[test]
298 fn test_effective_indent_no_blockquote_context() {
299 assert_eq!(effective_indent_in_blockquote("text", 0, 0), 0);
301 assert_eq!(effective_indent_in_blockquote(" text", 0, 3), 3);
302 assert_eq!(effective_indent_in_blockquote("> text", 0, 5), 5);
303 }
304
305 #[test]
306 fn test_effective_indent_single_level_blockquote() {
307 assert_eq!(effective_indent_in_blockquote("> text", 1, 99), 0);
309 assert_eq!(effective_indent_in_blockquote("> text", 1, 99), 1);
310 assert_eq!(effective_indent_in_blockquote("> text", 1, 99), 2);
311 assert_eq!(effective_indent_in_blockquote("> text", 1, 99), 3);
312 }
313
314 #[test]
315 fn test_effective_indent_no_space_after_marker() {
316 assert_eq!(effective_indent_in_blockquote(">text", 1, 99), 0);
318 assert_eq!(effective_indent_in_blockquote(">>text", 2, 99), 0);
319 }
320
321 #[test]
322 fn test_effective_indent_nested_blockquote_compact() {
323 assert_eq!(effective_indent_in_blockquote(">> text", 2, 99), 0);
325 assert_eq!(effective_indent_in_blockquote(">> text", 2, 99), 1);
326 assert_eq!(effective_indent_in_blockquote(">> text", 2, 99), 2);
327 }
328
329 #[test]
330 fn test_effective_indent_nested_blockquote_spaced() {
331 assert_eq!(effective_indent_in_blockquote("> > text", 2, 99), 0);
333 assert_eq!(effective_indent_in_blockquote("> > text", 2, 99), 1);
334 assert_eq!(effective_indent_in_blockquote("> > text", 2, 99), 2);
335 }
336
337 #[test]
338 fn test_effective_indent_mismatched_level() {
339 assert_eq!(effective_indent_in_blockquote("> text", 2, 42), 42);
341 assert_eq!(effective_indent_in_blockquote(">> text", 1, 42), 42);
342 assert_eq!(effective_indent_in_blockquote("text", 1, 42), 42);
343 }
344
345 #[test]
346 fn test_effective_indent_empty_blockquote() {
347 assert_eq!(effective_indent_in_blockquote(">", 1, 99), 0);
349 assert_eq!(effective_indent_in_blockquote("> ", 1, 99), 0);
350 assert_eq!(effective_indent_in_blockquote("> ", 1, 99), 1);
351 }
352
353 #[test]
354 fn test_effective_indent_issue_268_case() {
355 assert_eq!(effective_indent_in_blockquote("> Opening the app", 1, 0), 2);
358 assert_eq!(
359 effective_indent_in_blockquote("> [**See preview here!**](https://example.com)", 1, 0),
360 2
361 );
362 }
363
364 #[test]
365 fn test_effective_indent_triple_nested() {
366 assert_eq!(effective_indent_in_blockquote("> > > text", 3, 99), 0);
368 assert_eq!(effective_indent_in_blockquote("> > > text", 3, 99), 1);
369 assert_eq!(effective_indent_in_blockquote(">>> text", 3, 99), 0);
370 assert_eq!(effective_indent_in_blockquote(">>> text", 3, 99), 1);
371 }
372
373 #[test]
378 fn test_count_blockquote_level_none() {
379 assert_eq!(count_blockquote_level("regular text"), 0);
380 assert_eq!(count_blockquote_level(" indented text"), 0);
381 assert_eq!(count_blockquote_level(""), 0);
382 }
383
384 #[test]
385 fn test_count_blockquote_level_single() {
386 assert_eq!(count_blockquote_level("> text"), 1);
387 assert_eq!(count_blockquote_level(">text"), 1);
388 assert_eq!(count_blockquote_level(">"), 1);
389 }
390
391 #[test]
392 fn test_count_blockquote_level_nested() {
393 assert_eq!(count_blockquote_level(">> text"), 2);
394 assert_eq!(count_blockquote_level("> > text"), 2);
395 assert_eq!(count_blockquote_level(">>> text"), 3);
396 assert_eq!(count_blockquote_level("> > > text"), 3);
397 }
398
399 #[test]
404 fn test_content_after_blockquote_no_quote() {
405 assert_eq!(content_after_blockquote("text", 0), "text");
406 assert_eq!(content_after_blockquote(" indented", 0), " indented");
407 }
408
409 #[test]
410 fn test_content_after_blockquote_single() {
411 assert_eq!(content_after_blockquote("> text", 1), "text");
412 assert_eq!(content_after_blockquote(">text", 1), "text");
413 assert_eq!(content_after_blockquote("> indented", 1), " indented");
414 }
415
416 #[test]
417 fn test_content_after_blockquote_nested() {
418 assert_eq!(content_after_blockquote(">> text", 2), "text");
419 assert_eq!(content_after_blockquote("> > text", 2), "text");
420 assert_eq!(content_after_blockquote("> > indented", 2), " indented");
421 }
422
423 #[test]
424 fn test_content_after_blockquote_mismatched_level() {
425 assert_eq!(content_after_blockquote("> text", 2), "> text");
427 assert_eq!(content_after_blockquote(">> text", 1), ">> text");
428 }
429}