use std::ops::Range;
use crate::markdown::scan_code_span_candidates;
pub(super) struct Candidate {
pub(super) span: Range<usize>,
pub(super) ident: String,
}
pub(super) fn collect_candidates(rendered: &str) -> Vec<Candidate> {
scan_code_span_candidates(rendered)
.into_iter()
.filter_map(|span| {
let ident = take_backticked_ident(&rendered[span.clone()])?;
Some(Candidate { span, ident })
})
.collect()
}
fn take_backticked_ident(code_span: &str) -> Option<String> {
if code_span.contains('\n') {
return None;
}
let body = strip_code_fences(code_span)?;
let body = strip_one_padding_space(body);
if !is_plain_ident(body) {
return None;
}
Some(body.to_owned())
}
fn strip_one_padding_space(body: &str) -> &str {
if body.len() >= 2 && body.starts_with(' ') && body.ends_with(' ') && !body.trim().is_empty() {
&body[1..body.len() - 1]
} else {
body
}
}
fn strip_code_fences(code_span: &str) -> Option<&str> {
let bytes = code_span.as_bytes();
let mut fence = 0;
while fence < bytes.len() && bytes[fence] == b'`' {
fence += 1;
}
if fence == 0 || code_span.len() < fence * 2 {
return None;
}
if bytes[code_span.len() - fence..]
.iter()
.any(|byte| *byte != b'`')
{
return None;
}
Some(&code_span[fence..code_span.len() - fence])
}
fn is_plain_ident(text: &str) -> bool {
let mut chars = text.chars();
let Some(first) = chars.next() else {
return false;
};
if !(first.is_ascii_alphabetic() || first == '_') {
return false;
}
if !chars
.clone()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
{
return false;
}
text.bytes().any(|byte| byte != b'_')
}
#[cfg(test)]
mod tests;