use crate::analysis::findings::Finding;
use crate::docs::{Check, Line, finding};
use crate::text::excerpt;
const URL_EXCERPT_MAX: usize = 60;
pub fn check(
line: &Line<'_>,
chars: &[char],
scratch: &mut Vec<char>,
file_path: &str,
out: &mut Vec<Finding>,
) {
if line.in_fence {
return;
}
let blanked: &[char] = if line.text.contains('`') || line.text.contains('[') {
scratch.clear();
scratch.extend_from_slice(chars);
blank_inline_code(scratch);
blank_links(scratch);
blank_reference_definition(scratch);
scratch
} else {
chars
};
bare_url(line, blanked, file_path, out);
link_syntax(line, blanked, file_path, out);
}
fn blank_inline_code(chars: &mut [char]) {
let mut i = 0;
while i < chars.len() {
if chars[i] == '`' {
let Some(close) = (i + 1..chars.len()).find(|j| chars[*j] == '`') else {
return;
};
chars[i..=close].fill(' ');
i = close;
}
i += 1;
}
}
fn blank_links(chars: &mut [char]) {
let mut i = 0;
while i < chars.len() {
if chars[i] == '['
&& let Some(end) = match_link(chars, i)
{
chars[i..=end].fill(' ');
i = end;
}
i += 1;
}
}
fn match_link(chars: &[char], start: usize) -> Option<usize> {
debug_assert_eq!(chars[start], '[');
let mut j = start + 1;
loop {
match chars.get(j)? {
'[' => {
let close = (j..chars.len()).find(|k| chars[*k] == ']')?;
j = close + 1;
}
']' => {
if chars.get(j + 1) != Some(&'(') {
return None;
}
return (j + 2..chars.len()).find(|m| chars[*m] == ')');
}
_ => j += 1,
}
}
}
fn blank_reference_definition(chars: &mut [char]) {
let Some(open) = chars.iter().position(|c| !c.is_whitespace()) else {
return;
};
if chars[open] != '[' {
return;
}
let Some(close) = (open + 1..chars.len()).find(|i| matches!(chars[*i], '[' | ']')) else {
return;
};
if chars[close] != ']' || chars.get(close + 1) != Some(&':') {
return;
}
let Some(destination) = (close + 2..chars.len()).find(|i| !chars[*i].is_whitespace()) else {
return;
};
chars[destination..].fill(' ');
}
fn bare_url(line: &Line<'_>, blanked: &[char], file_path: &str, out: &mut Vec<Finding>) {
let Some(start) = find_url(blanked) else {
return;
};
let end = (start..blanked.len())
.find(|i| blanked[*i].is_whitespace())
.unwrap_or(blanked.len());
let url: String = blanked[start..end].iter().collect();
out.push(finding(
Check::BareUrl,
file_path,
line.number,
start as u32 + 1,
format!("bare URL: {}", excerpt(&url, URL_EXCERPT_MAX)),
));
}
fn find_url(chars: &[char]) -> Option<usize> {
const SCHEMES: [&[char]; 2] = [
&['h', 't', 't', 'p', 's', ':', '/', '/'],
&['h', 't', 't', 'p', ':', '/', '/'],
];
(0..chars.len()).find(|start| {
chars[*start] == 'h'
&& SCHEMES.iter().any(|scheme| {
chars[*start..].starts_with(scheme)
&& chars
.get(start + scheme.len())
.is_some_and(|c| !c.is_whitespace())
})
})
}
fn link_syntax(line: &Line<'_>, blanked: &[char], file_path: &str, out: &mut Vec<Finding>) {
let count = |target: char| blanked.iter().filter(|c| **c == target).count();
let (open_square, close_square) = (count('['), count(']'));
let (open_round, close_round) = (count('('), count(')'));
let detail = if open_square != close_square {
format!("{open_square} `[` against {close_square} `]`")
} else if blanked.windows(2).any(|w| w == [']', '(']) && open_round != close_round {
format!("{open_round} `(` against {close_round} `)`")
} else {
return;
};
out.push(finding(
Check::LinkSyntaxInvalid,
file_path,
line.number,
1,
format!("unbalanced link syntax: {detail}"),
));
}