#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParsedBlockquotePrefix<'a> {
pub indent: &'a str,
pub prefix: &'a str,
pub content: &'a str,
pub nesting_level: usize,
pub spaces_after_marker: &'a str,
}
#[inline]
fn is_space_or_tab(byte: u8) -> bool {
byte == b' ' || byte == b'\t'
}
#[inline]
pub fn parse_blockquote_prefix(line: &str) -> Option<ParsedBlockquotePrefix<'_>> {
let bytes = line.as_bytes();
let mut pos = 0;
while pos < bytes.len() && is_space_or_tab(bytes[pos]) {
pos += 1;
}
let indent_end = pos;
if pos >= bytes.len() || bytes[pos] != b'>' {
return None;
}
let mut nesting_level = 0;
let mut prefix_end = pos;
let mut spaces_after_marker_start = pos;
let mut spaces_after_marker_end = pos;
loop {
if pos >= bytes.len() || bytes[pos] != b'>' {
break;
}
nesting_level += 1;
pos += 1; let marker_end = pos;
if pos < bytes.len() && is_space_or_tab(bytes[pos]) {
pos += 1;
}
let content_start_candidate = pos;
while pos < bytes.len() && is_space_or_tab(bytes[pos]) {
pos += 1;
}
if pos < bytes.len() && bytes[pos] == b'>' {
continue;
}
prefix_end = content_start_candidate;
spaces_after_marker_start = marker_end;
spaces_after_marker_end = pos;
break;
}
Some(ParsedBlockquotePrefix {
indent: &line[..indent_end],
prefix: &line[..prefix_end],
content: &line[prefix_end..],
nesting_level,
spaces_after_marker: &line[spaces_after_marker_start..spaces_after_marker_end],
})
}
pub fn effective_indent_in_blockquote(line_content: &str, expected_bq_level: usize, fallback_indent: usize) -> usize {
if expected_bq_level == 0 {
return fallback_indent;
}
let line_bq_level = line_content
.chars()
.take_while(|c| *c == '>' || c.is_whitespace())
.filter(|&c| c == '>')
.count();
if line_bq_level != expected_bq_level {
return fallback_indent;
}
let mut pos = 0;
let mut found_markers = 0;
for c in line_content.chars() {
pos += c.len_utf8();
if c == '>' {
found_markers += 1;
if found_markers == line_bq_level {
if line_content.get(pos..pos + 1) == Some(" ") {
pos += 1;
}
break;
}
}
}
let after_bq = &line_content[pos..];
after_bq.len() - after_bq.trim_start().len()
}
pub fn count_blockquote_level(line_content: &str) -> usize {
line_content
.chars()
.take_while(|c| *c == '>' || c.is_whitespace())
.filter(|&c| c == '>')
.count()
}
pub fn content_after_blockquote(line_content: &str, expected_bq_level: usize) -> &str {
if expected_bq_level == 0 {
return line_content;
}
let actual_level = count_blockquote_level(line_content);
if actual_level != expected_bq_level {
return line_content;
}
let mut pos = 0;
let mut found_markers = 0;
for c in line_content.chars() {
pos += c.len_utf8();
if c == '>' {
found_markers += 1;
if found_markers == expected_bq_level {
if line_content.get(pos..pos + 1) == Some(" ") {
pos += 1;
}
break;
}
}
}
&line_content[pos..]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_blockquote_prefix_compact_nested() {
let parsed = parse_blockquote_prefix(">> text").expect("should parse compact nested blockquote");
assert_eq!(parsed.indent, "");
assert_eq!(parsed.prefix, ">> ");
assert_eq!(parsed.content, "text");
assert_eq!(parsed.nesting_level, 2);
assert_eq!(parsed.spaces_after_marker, " ");
}
#[test]
fn test_parse_blockquote_prefix_spaced_nested() {
let parsed = parse_blockquote_prefix("> > text").expect("should parse spaced nested blockquote");
assert_eq!(parsed.indent, "");
assert_eq!(parsed.prefix, "> > ");
assert_eq!(parsed.content, " text");
assert_eq!(parsed.nesting_level, 2);
assert_eq!(parsed.spaces_after_marker, " ");
}
#[test]
fn test_parse_blockquote_prefix_with_indent() {
let parsed = parse_blockquote_prefix(" > quote").expect("should parse indented blockquote");
assert_eq!(parsed.indent, " ");
assert_eq!(parsed.prefix, " > ");
assert_eq!(parsed.content, "quote");
assert_eq!(parsed.nesting_level, 1);
assert_eq!(parsed.spaces_after_marker, " ");
}
#[test]
fn test_parse_blockquote_prefix_non_blockquote() {
assert!(parse_blockquote_prefix("plain text").is_none());
assert!(parse_blockquote_prefix(" plain text").is_none());
}
#[test]
fn test_effective_indent_no_blockquote_context() {
assert_eq!(effective_indent_in_blockquote("text", 0, 0), 0);
assert_eq!(effective_indent_in_blockquote(" text", 0, 3), 3);
assert_eq!(effective_indent_in_blockquote("> text", 0, 5), 5);
}
#[test]
fn test_effective_indent_single_level_blockquote() {
assert_eq!(effective_indent_in_blockquote("> text", 1, 99), 0);
assert_eq!(effective_indent_in_blockquote("> text", 1, 99), 1);
assert_eq!(effective_indent_in_blockquote("> text", 1, 99), 2);
assert_eq!(effective_indent_in_blockquote("> text", 1, 99), 3);
}
#[test]
fn test_effective_indent_no_space_after_marker() {
assert_eq!(effective_indent_in_blockquote(">text", 1, 99), 0);
assert_eq!(effective_indent_in_blockquote(">>text", 2, 99), 0);
}
#[test]
fn test_effective_indent_nested_blockquote_compact() {
assert_eq!(effective_indent_in_blockquote(">> text", 2, 99), 0);
assert_eq!(effective_indent_in_blockquote(">> text", 2, 99), 1);
assert_eq!(effective_indent_in_blockquote(">> text", 2, 99), 2);
}
#[test]
fn test_effective_indent_nested_blockquote_spaced() {
assert_eq!(effective_indent_in_blockquote("> > text", 2, 99), 0);
assert_eq!(effective_indent_in_blockquote("> > text", 2, 99), 1);
assert_eq!(effective_indent_in_blockquote("> > text", 2, 99), 2);
}
#[test]
fn test_effective_indent_mismatched_level() {
assert_eq!(effective_indent_in_blockquote("> text", 2, 42), 42);
assert_eq!(effective_indent_in_blockquote(">> text", 1, 42), 42);
assert_eq!(effective_indent_in_blockquote("text", 1, 42), 42);
}
#[test]
fn test_effective_indent_empty_blockquote() {
assert_eq!(effective_indent_in_blockquote(">", 1, 99), 0);
assert_eq!(effective_indent_in_blockquote("> ", 1, 99), 0);
assert_eq!(effective_indent_in_blockquote("> ", 1, 99), 1);
}
#[test]
fn test_effective_indent_issue_268_case() {
assert_eq!(effective_indent_in_blockquote("> Opening the app", 1, 0), 2);
assert_eq!(
effective_indent_in_blockquote("> [**See preview here!**](https://example.com)", 1, 0),
2
);
}
#[test]
fn test_effective_indent_triple_nested() {
assert_eq!(effective_indent_in_blockquote("> > > text", 3, 99), 0);
assert_eq!(effective_indent_in_blockquote("> > > text", 3, 99), 1);
assert_eq!(effective_indent_in_blockquote(">>> text", 3, 99), 0);
assert_eq!(effective_indent_in_blockquote(">>> text", 3, 99), 1);
}
#[test]
fn test_count_blockquote_level_none() {
assert_eq!(count_blockquote_level("regular text"), 0);
assert_eq!(count_blockquote_level(" indented text"), 0);
assert_eq!(count_blockquote_level(""), 0);
}
#[test]
fn test_count_blockquote_level_single() {
assert_eq!(count_blockquote_level("> text"), 1);
assert_eq!(count_blockquote_level(">text"), 1);
assert_eq!(count_blockquote_level(">"), 1);
}
#[test]
fn test_count_blockquote_level_nested() {
assert_eq!(count_blockquote_level(">> text"), 2);
assert_eq!(count_blockquote_level("> > text"), 2);
assert_eq!(count_blockquote_level(">>> text"), 3);
assert_eq!(count_blockquote_level("> > > text"), 3);
}
#[test]
fn test_content_after_blockquote_no_quote() {
assert_eq!(content_after_blockquote("text", 0), "text");
assert_eq!(content_after_blockquote(" indented", 0), " indented");
}
#[test]
fn test_content_after_blockquote_single() {
assert_eq!(content_after_blockquote("> text", 1), "text");
assert_eq!(content_after_blockquote(">text", 1), "text");
assert_eq!(content_after_blockquote("> indented", 1), " indented");
}
#[test]
fn test_content_after_blockquote_nested() {
assert_eq!(content_after_blockquote(">> text", 2), "text");
assert_eq!(content_after_blockquote("> > text", 2), "text");
assert_eq!(content_after_blockquote("> > indented", 2), " indented");
}
#[test]
fn test_content_after_blockquote_mismatched_level() {
assert_eq!(content_after_blockquote("> text", 2), "> text");
assert_eq!(content_after_blockquote(">> text", 1), ">> text");
}
}