use std::collections::HashSet;
use pagesift_net::Url;
use crate::focus::{BestFirst, Offer};
const DOCUMENT_EXT: &[&str] = &[
"pdf", "docx", "docm", "dotx", "xlsx", "xlsm", "xltx", "pptx", "pptm", "ppsx", "epub",
];
pub fn is_document_url(u: &Url) -> bool {
let last = u.path().rsplit('/').next().unwrap_or_default();
last.rsplit_once('.')
.is_some_and(|(_, ext)| DOCUMENT_EXT.contains(&ext.to_ascii_lowercase().as_str()))
}
pub struct Attachments {
queue: BestFirst,
seen: HashSet<u64>,
pub limit: usize,
pub scheduled: usize,
}
impl Attachments {
pub fn new(limit: usize) -> Self {
Self {
queue: BestFirst::default(),
seen: HashSet::new(),
limit,
scheduled: 0,
}
}
pub fn pending(&self) -> usize {
self.queue.len()
}
pub fn offer(&mut self, hash: u64, url: Url, depth: usize, score: f32) {
if self.seen.contains(&hash) {
return;
}
let room = self.limit.saturating_sub(self.scheduled);
match self.queue.offer(hash, url, depth, score, (), room) {
Offer::Queued => {
self.seen.insert(hash);
}
Offer::Evicted(old) => {
self.seen.remove(&old);
self.seen.insert(hash);
}
Offer::Rejected | Offer::AlreadyQueued => {}
}
}
pub fn next(&mut self) -> Option<(Url, usize, f32)> {
if self.scheduled >= self.limit {
return None;
}
let item = self
.queue
.pop_diverse(&Default::default(), usize::MAX, |_| false)?;
self.scheduled += 1;
Some(item)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn u(s: &str) -> Url {
Url::parse(s).unwrap()
}
#[test]
fn document_urls_are_recognised_by_extension() {
for s in [
"https://e.edu/files/fees.pdf",
"https://e.edu/x/guide.PDF",
"https://e.edu/forms/apply.docx",
"https://e.edu/data/tuition.xlsx?year=2026",
"https://e.edu/book.epub",
] {
assert!(is_document_url(&u(s)), "{s}");
}
for s in [
"https://e.edu/pdf/",
"https://e.edu/files/fees.pdf.html",
"https://e.edu/image.png",
"https://e.edu/",
] {
assert!(!is_document_url(&u(s)), "{s}");
}
}
#[test]
fn attachment_budget_is_bounded_and_ordered() {
let mut a = Attachments::new(2);
a.offer(1, u("https://e.edu/a.pdf"), 1, 0.2);
a.offer(2, u("https://e.edu/b.pdf"), 1, 0.9);
a.offer(2, u("https://e.edu/b.pdf"), 1, 0.9);
a.offer(3, u("https://e.edu/c.pdf"), 1, 0.5);
assert_eq!(a.pending(), 2, "the lowest is evicted");
assert_eq!(a.next().unwrap().0.path(), "/b.pdf");
assert_eq!(a.next().unwrap().0.path(), "/c.pdf");
assert!(a.next().is_none(), "budget spent");
}
}