use crate::markdown::Token;
use lopdf::{Dictionary, Document, Object};
use std::collections::HashMap;
pub fn collect_link_tooltips(tokens: &[Token]) -> HashMap<String, String> {
let mut map = HashMap::new();
walk(tokens, &mut map);
map
}
fn walk(tokens: &[Token], map: &mut HashMap<String, String>) {
for tok in tokens {
match tok {
Token::Link {
content,
url,
title: Some(t),
} => {
map.insert(url.clone(), t.clone());
walk(content, map);
}
Token::Link { content, .. } => walk(content, map),
Token::Heading(inner, _)
| Token::Emphasis { content: inner, .. }
| Token::StrongEmphasis(inner)
| Token::Strikethrough(inner)
| Token::BlockQuote(inner)
| Token::ListItem { content: inner, .. }
| Token::FootnoteDefinition { content: inner, .. } => walk(inner, map),
Token::Image { alt, .. } => walk(alt, map),
Token::Table { headers, rows, .. } => {
for h in headers {
walk(h, map);
}
for r in rows {
for c in r {
walk(c, map);
}
}
}
Token::DefinitionList { entries } => {
for e in entries {
walk(&e.term, map);
for d in &e.definitions {
walk(d, map);
}
}
}
_ => {}
}
}
}
pub fn inject_link_tooltips(bytes: Vec<u8>, tooltips: &HashMap<String, String>) -> Vec<u8> {
if tooltips.is_empty() {
return bytes;
}
let Ok(mut doc) = Document::load_mem(&bytes) else {
return bytes;
};
let mut changed = false;
let ids: Vec<lopdf::ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let Some(Object::Dictionary(d)) = doc.objects.get_mut(&id) else {
continue;
};
if !is_link_annotation(d) {
continue;
}
let Some(uri) = link_uri(d) else { continue };
let Some(tip) = tooltips.get(&uri) else { continue };
d.set("Contents", Object::string_literal(tip.clone()));
changed = true;
}
let page_ids: Vec<lopdf::ObjectId> = doc.page_iter().collect();
for pid in page_ids {
let Some(Object::Dictionary(page)) = doc.objects.get_mut(&pid) else {
continue;
};
let Ok(annots) = page.get_mut(b"Annots") else {
continue;
};
let Object::Array(items) = annots else {
continue;
};
for item in items.iter_mut() {
if let Object::Dictionary(d) = item {
if !is_link_annotation(d) {
continue;
}
if let Some(uri) = link_uri(d) {
if let Some(tip) = tooltips.get(&uri) {
d.set("Contents", Object::string_literal(tip.clone()));
changed = true;
}
}
}
}
}
if !changed {
return bytes;
}
let mut out = Vec::new();
if doc.save_to(&mut out).is_ok() {
out
} else {
bytes
}
}
pub fn inject_lang(bytes: Vec<u8>, lang: &str) -> Vec<u8> {
if lang.trim().is_empty() {
return bytes;
}
let Ok(mut doc) = Document::load_mem(&bytes) else {
return bytes;
};
let Ok(root_ref) = doc.trailer.get(b"Root") else {
return bytes;
};
let Ok(root_id) = root_ref.as_reference() else {
return bytes;
};
let Some(Object::Dictionary(catalog)) = doc.objects.get_mut(&root_id) else {
return bytes;
};
catalog.set("Lang", Object::string_literal(lang.to_string()));
let mut out = Vec::new();
if doc.save_to(&mut out).is_ok() {
out
} else {
bytes
}
}
fn is_link_annotation(d: &Dictionary) -> bool {
let subtype_link = d
.get(b"Subtype")
.ok()
.and_then(|o| o.as_name().ok())
.map(|n| n == b"Link")
.unwrap_or(false);
if !subtype_link {
return false;
}
let type_annot = d
.get(b"Type")
.ok()
.and_then(|o| o.as_name().ok())
.map(|n| n == b"Annot")
.unwrap_or(true);
type_annot
}
fn link_uri(d: &Dictionary) -> Option<String> {
let action = d.get(b"A").ok()?;
let action_dict = action.as_dict().ok()?;
let s_uri = action_dict
.get(b"S")
.ok()
.and_then(|o| o.as_name().ok())
.map(|n| n == b"URI")
.unwrap_or(false);
if !s_uri {
return None;
}
let uri_obj = action_dict.get(b"URI").ok()?;
let bytes = uri_obj.as_str().ok()?;
std::str::from_utf8(bytes).ok().map(|s| s.to_string())
}