use crate::scan::{is_python_space, match_blockquote_prefix};
pub const LINK_BLOCK_FLOOR: usize = 2;
#[must_use]
pub fn matches_link_only_line(text: &str) -> bool {
let bytes = text.as_bytes();
let Some(mut at) = match_link_token(bytes, skip_whitespace(text, 0)) else {
return false;
};
loop {
let separated = skip_whitespace(text, at);
if separated == at {
break;
}
match match_link_token(bytes, separated) {
Some(end) => at = end,
None => break,
}
}
skip_whitespace(text, at) == text.len()
}
#[must_use]
pub fn is_link_only_line(body: &str) -> bool {
let rest = match match_blockquote_prefix(body) {
Some(end) => {
let rest = &body[end..];
if rest.starts_with(" ") || rest.starts_with('\t') {
return false;
}
rest
}
None => body,
};
matches_link_only_line(rest)
}
#[must_use]
pub fn link_block_indexes(bodies: &[&str]) -> Vec<bool> {
let mut inside = vec![false; bodies.len()];
let mut run_start = 0;
let mut run_quoted = false;
for index in 0..=bodies.len() {
let body = bodies.get(index).copied().unwrap_or("");
if !is_link_only_line(body) {
close_run(&mut inside, run_start, index);
run_start = index + 1;
continue;
}
let quoted = match_blockquote_prefix(body).is_some();
if index == run_start {
run_quoted = quoted;
} else if quoted && !run_quoted {
close_run(&mut inside, run_start, index);
run_start = index;
run_quoted = true;
}
}
inside
}
fn close_run(inside: &mut [bool], start: usize, end: usize) {
if end - start >= LINK_BLOCK_FLOOR {
for flag in &mut inside[start..end] {
*flag = true;
}
}
}
fn skip_whitespace(text: &str, at: usize) -> usize {
let mut end = at;
for c in text[at..].chars() {
if !is_python_space(c) {
break;
}
end += c.len_utf8();
}
end
}
fn match_link_token(bytes: &[u8], start: usize) -> Option<usize> {
let mut at = start;
if bytes.get(at) == Some(&b'!') {
at += 1;
}
at = match_link_text(bytes, at)?;
match_link_target(bytes, at).or_else(|| match_flat_group(bytes, at, b'[', b']'))
}
fn match_link_text(bytes: &[u8], start: usize) -> Option<usize> {
match_nesting_group(bytes, start, b'[', b']')
}
fn match_link_target(bytes: &[u8], start: usize) -> Option<usize> {
match_nesting_group(bytes, start, b'(', b')')
}
fn match_nesting_group(bytes: &[u8], start: usize, open: u8, close: u8) -> Option<usize> {
if bytes.get(start) != Some(&open) {
return None;
}
let mut at = start + 1;
loop {
match bytes.get(at) {
None => return None,
Some(b) if *b == close => return Some(at + 1),
Some(b) if *b == open => at = match_flat_group(bytes, at, open, close)?,
Some(_) => at += 1,
}
}
}
fn match_flat_group(bytes: &[u8], start: usize, open: u8, close: u8) -> Option<usize> {
if bytes.get(start) != Some(&open) {
return None;
}
let mut at = start + 1;
loop {
match bytes.get(at) {
None => return None,
Some(b) if *b == close => return Some(at + 1),
Some(b) if *b == open => return None,
Some(_) => at += 1,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_link_token_admits_exactly_one_level_of_nesting() {
assert!(matches_link_only_line("[][ref]"));
assert!(matches_link_only_line("[a](/wiki/Ruby_(rock))"));
assert!(matches_link_only_line("[a[b]](x)"));
assert!(matches_link_only_line("[a](x(y)z)"));
assert!(!matches_link_only_line("[a[b[c]]](x)"));
assert!(!matches_link_only_line("[a](x))"));
}
#[test]
fn a_link_only_line_is_links_and_whitespace_and_nothing_else() {
assert!(matches_link_only_line("[a](x)"));
assert!(matches_link_only_line(""));
assert!(matches_link_only_line("[a][b]"));
assert!(matches_link_only_line("[a](x) [b](y)"));
assert!(matches_link_only_line(" [a](x)"));
assert!(matches_link_only_line("[a](x) "));
assert!(!matches_link_only_line("[a](x) text"));
assert!(!matches_link_only_line("text [a](x)"));
assert!(!matches_link_only_line("[a]"));
assert!(!matches_link_only_line(""));
assert!(!matches_link_only_line(" "));
}
#[test]
fn tokens_have_to_be_separated_by_whitespace() {
assert!(!matches_link_only_line("[a](x)[b](y)"));
}
#[test]
fn indented_code_under_a_quote_marker_is_not_a_badge() {
assert!(!is_link_only_line("> [a](x)"));
assert!(!is_link_only_line(">\t[a](x)"));
assert!(is_link_only_line(" [a](x)"));
assert!(is_link_only_line("\t[a](x)"));
assert!(is_link_only_line("> [a](x)"));
assert!(is_link_only_line("> > [a](x)"));
assert!(!is_link_only_line("> text"));
}
#[test]
fn a_run_needs_two_lines_to_be_structural() {
assert_eq!(link_block_indexes(&["[a](x)", "text"]), [false, false]);
assert_eq!(link_block_indexes(&["[a](x)"]), [false]);
assert_eq!(link_block_indexes(&["[a](x)", "[b](y)"]), [true, true]);
assert_eq!(
link_block_indexes(&["text", "[a](x)", "[b](y)"]),
[false, true, true]
);
assert_eq!(
link_block_indexes(&["[a](x)", "", "[b](y)"]),
[false, false, false]
);
assert!(link_block_indexes(&[]).is_empty());
}
#[test]
fn a_quoted_line_on_top_of_an_unquoted_run_opens_a_new_run() {
assert_eq!(
link_block_indexes(&["[a](x)", "[b](y)", "> [c](z)", "> [d](w)"]),
[true, true, true, true]
);
assert_eq!(link_block_indexes(&["[a](x)", "> [b](y)"]), [false, false]);
assert_eq!(link_block_indexes(&["> [a](x)", "[b](y)"]), [true, true]);
}
}