use crate::ir::nodes::InlineContent;
pub fn extract_anchor_for_reference(
content: &[InlineContent],
ref_index: usize,
) -> Option<(String, String, Vec<InlineContent>)> {
if ref_index >= content.len() {
return None;
}
let reference = match &content[ref_index] {
InlineContent::Reference(href) => href.clone(),
_ => return None,
};
if let Some((anchor, modified)) = extract_word_before(content, ref_index) {
return Some((anchor, reference, modified));
}
if let Some((anchor, modified)) = extract_word_after(content, ref_index) {
return Some((anchor, reference, modified));
}
let mut modified = content.to_vec();
modified.remove(ref_index);
Some((reference.clone(), reference, modified))
}
fn extract_word_before(
content: &[InlineContent],
ref_index: usize,
) -> Option<(String, Vec<InlineContent>)> {
for i in (0..ref_index).rev() {
if let InlineContent::Text(text) = &content[i] {
let trimmed = text.trim_end();
if trimmed.is_empty() {
continue;
}
let last_space = trimmed.rfind(char::is_whitespace);
let (prefix, word) = match last_space {
Some(pos) => (&trimmed[..=pos], &trimmed[pos + 1..]),
None => ("", trimmed),
};
if word.is_empty() {
continue;
}
let mut modified = Vec::new();
for (idx, item) in content.iter().enumerate() {
if idx == i {
if !prefix.is_empty() {
modified.push(InlineContent::Text(prefix.to_string()));
}
} else if idx != ref_index {
modified.push(item.clone());
}
}
return Some((word.to_string(), modified));
}
}
None
}
fn extract_word_after(
content: &[InlineContent],
ref_index: usize,
) -> Option<(String, Vec<InlineContent>)> {
for i in (ref_index + 1)..content.len() {
if let InlineContent::Text(text) = &content[i] {
let trimmed = text.trim_start();
if trimmed.is_empty() {
continue;
}
let first_space = trimmed.find(char::is_whitespace);
let (word, suffix) = match first_space {
Some(pos) => (&trimmed[..pos], &trimmed[pos..]),
None => (trimmed, ""),
};
if word.is_empty() {
continue;
}
let mut modified = Vec::new();
for (idx, item) in content.iter().enumerate() {
if idx == i {
if !suffix.is_empty() {
modified.push(InlineContent::Text(suffix.to_string()));
}
} else if idx != ref_index {
modified.push(item.clone());
}
}
return Some((word.to_string(), modified));
}
}
None
}
pub fn insert_reference_with_anchor(
mut content: Vec<InlineContent>,
anchor: String,
href: String,
) -> Vec<InlineContent> {
content.push(InlineContent::Text(anchor));
content.push(InlineContent::Text(" ".to_string()));
content.push(InlineContent::Reference(href));
content
}
fn is_linkable_reference(raw: &str) -> bool {
raw.starts_with("http://")
|| raw.starts_with("https://")
|| raw.starts_with("mailto:")
|| raw.starts_with("./")
|| raw.starts_with('/')
|| raw.starts_with('#')
}
pub fn resolve_implicit_anchors(content: Vec<InlineContent>) -> Vec<InlineContent> {
let ref_indices: Vec<usize> = content
.iter()
.enumerate()
.filter_map(|(i, item)| match item {
InlineContent::Reference(raw) if is_linkable_reference(raw) => Some(i),
_ => None,
})
.collect();
if ref_indices.is_empty() {
return content;
}
let mut result = content;
for &ref_idx in ref_indices.iter().rev() {
if let Some((anchor, href, modified)) = extract_anchor_for_reference(&result, ref_idx) {
let insert_at = ref_idx.min(modified.len());
let mut new_result = Vec::with_capacity(modified.len() + 1);
let mut inserted = false;
for (i, item) in modified.into_iter().enumerate() {
if !inserted && i >= insert_at {
new_result.push(InlineContent::Link {
text: anchor.clone(),
href: href.clone(),
});
inserted = true;
}
new_result.push(item);
}
if !inserted {
new_result.push(InlineContent::Link { text: anchor, href });
}
result = new_result;
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_anchor_word_before() {
let content = vec![
InlineContent::Text("visit the ".to_string()),
InlineContent::Text("bahamas ".to_string()),
InlineContent::Reference("bahamas.gov".to_string()),
];
let result = extract_anchor_for_reference(&content, 2);
assert!(result.is_some());
let (anchor, href, modified) = result.unwrap();
assert_eq!(anchor, "bahamas");
assert_eq!(href, "bahamas.gov");
assert_eq!(modified.len(), 1);
assert!(matches!(&modified[0], InlineContent::Text(t) if t == "visit the "));
}
#[test]
fn test_extract_anchor_word_after() {
let content = vec![
InlineContent::Reference("wikipedia.org".to_string()),
InlineContent::Text(" Wikipedia is useful".to_string()),
];
let result = extract_anchor_for_reference(&content, 0);
assert!(result.is_some());
let (anchor, href, modified) = result.unwrap();
assert_eq!(anchor, "Wikipedia");
assert_eq!(href, "wikipedia.org");
assert_eq!(modified.len(), 1);
assert!(matches!(&modified[0], InlineContent::Text(t) if t == " is useful"));
}
#[test]
fn test_extract_anchor_no_text() {
let content = vec![
InlineContent::Bold(vec![InlineContent::Text("bold".to_string())]),
InlineContent::Reference("example.com".to_string()),
];
let result = extract_anchor_for_reference(&content, 1);
assert!(result.is_some());
let (anchor, href, _modified) = result.unwrap();
assert_eq!(anchor, "example.com");
assert_eq!(href, "example.com");
}
#[test]
fn test_insert_reference_with_anchor() {
let content = vec![InlineContent::Text("visit ".to_string())];
let modified =
insert_reference_with_anchor(content, "bahamas".to_string(), "bahamas.gov".to_string());
assert_eq!(modified.len(), 4);
assert!(matches!(&modified[0], InlineContent::Text(t) if t == "visit "));
assert!(matches!(&modified[1], InlineContent::Text(t) if t == "bahamas"));
assert!(matches!(&modified[2], InlineContent::Text(t) if t == " "));
assert!(matches!(&modified[3], InlineContent::Reference(r) if r == "bahamas.gov"));
}
#[test]
fn test_resolve_implicit_anchors_word_before() {
let content = vec![
InlineContent::Text("visit the website ".to_string()),
InlineContent::Reference("https://example.com".to_string()),
];
let resolved = resolve_implicit_anchors(content);
assert!(resolved
.iter()
.any(|i| matches!(i, InlineContent::Link { text, href }
if text == "website" && href == "https://example.com")));
assert!(resolved
.iter()
.any(|i| matches!(i, InlineContent::Text(t) if t == "visit the ")));
}
#[test]
fn test_resolve_implicit_anchors_word_after() {
let content = vec![
InlineContent::Reference("https://example.com".to_string()),
InlineContent::Text(" Example is great".to_string()),
];
let resolved = resolve_implicit_anchors(content);
assert!(resolved
.iter()
.any(|i| matches!(i, InlineContent::Link { text, href }
if text == "Example" && href == "https://example.com")));
}
#[test]
fn test_resolve_implicit_anchors_only_ref() {
let content = vec![InlineContent::Reference("https://example.com".to_string())];
let resolved = resolve_implicit_anchors(content);
assert!(resolved
.iter()
.any(|i| matches!(i, InlineContent::Link { text, href }
if text == "https://example.com" && href == "https://example.com")));
}
#[test]
fn test_resolve_non_linkable_references_untouched() {
let content = vec![
InlineContent::Text("See ".to_string()),
InlineContent::Reference("@smith2023".to_string()),
];
let resolved = resolve_implicit_anchors(content);
assert_eq!(resolved.len(), 2);
assert!(matches!(&resolved[1], InlineContent::Reference(r) if r == "@smith2023"));
}
#[test]
fn test_resolve_session_reference() {
let content = vec![
InlineContent::Text("See section ".to_string()),
InlineContent::Reference("#introduction".to_string()),
];
let resolved = resolve_implicit_anchors(content);
assert!(resolved
.iter()
.any(|i| matches!(i, InlineContent::Link { text, href }
if text == "section" && href == "#introduction")));
}
}