use std::path::Path;
pub fn extract_text(path: &Path) -> Result<String, String> {
let bytes = std::fs::read(path).map_err(|e| format!("Failed to read file: {}", e))?;
extract_text_from_bytes(&bytes)
}
pub fn extract_text_from_bytes(bytes: &[u8]) -> Result<String, String> {
let doc = pdf_oxide::PdfDocument::from_bytes(bytes.to_vec())
.map_err(|e| format!("Failed to open PDF: {}", e))?;
let text = doc
.extract_all_text()
.map_err(|e| format!("Failed to extract text: {}", e))?;
Ok(text)
}
const SECTION_HEADINGS: &[&str] = &[
"abstract",
"introduction",
"background",
"related work",
"methods",
"methodology",
"materials and methods",
"results",
"discussion",
"conclusion",
"references",
];
pub fn extract_section(full_text: &str, heading: &str) -> Option<String> {
let lower = full_text.to_lowercase();
let heading = heading.to_lowercase();
let start = lower.find(&heading)?;
let body_start = start + heading.len();
let end = SECTION_HEADINGS
.iter()
.filter(|h| **h != heading)
.filter_map(|h| lower[body_start..].find(h).map(|i| body_start + i))
.min()
.unwrap_or(full_text.len());
let section = full_text[body_start..end].trim();
if section.is_empty() {
None
} else {
Some(section.to_string())
}
}
pub fn extract_section_abstract(full_text: &str) -> Option<String> {
extract_section(full_text, "abstract")
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn fixture_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/test.pdf")
}
#[test]
fn extract_text_returns_content() {
let text = extract_text(&fixture_path()).unwrap();
assert!(!text.is_empty(), "extracted text should not be empty");
}
#[test]
fn extract_text_contains_expected_words() {
let text = extract_text(&fixture_path()).unwrap();
assert!(
text.contains("Transformer")
|| text.contains("attention")
|| text.contains("Attention"),
"text should contain expected words, got: {}",
&text[..text.len().min(200)]
);
}
#[test]
fn extract_section_abstract_returns_content() {
let text = extract_text(&fixture_path()).unwrap();
let abstract_text = extract_section_abstract(&text);
assert!(abstract_text.is_some(), "should find abstract section");
let abs = abstract_text.unwrap();
assert!(!abs.is_empty());
}
#[test]
fn extract_section_abstract_does_not_contain_introduction() {
let text = extract_text(&fixture_path()).unwrap();
let abs = extract_section_abstract(&text).unwrap();
let lower = abs.to_lowercase();
assert!(
!lower.contains("introduction"),
"abstract should not contain introduction heading, got: {}",
abs
);
}
#[test]
fn extract_text_from_bytes_works() {
let bytes = std::fs::read(fixture_path()).unwrap();
let text = extract_text_from_bytes(&bytes).unwrap();
assert!(!text.is_empty());
}
#[test]
fn extract_section_introduction_returns_content() {
let text = extract_text(&fixture_path()).unwrap();
let intro = extract_section(&text, "introduction");
assert!(intro.is_some(), "should find the introduction");
assert!(!intro.unwrap().trim().is_empty());
}
#[test]
fn extract_section_stops_at_the_next_heading() {
let text = extract_text(&fixture_path()).unwrap();
let intro = extract_section(&text, "introduction").unwrap();
assert!(
!intro.to_lowercase().contains("\nreferences"),
"introduction should not run into the references section"
);
}
const PAPER: &str = "\
Title Here
Abstract
We present a thing.
Introduction
Prior work was slow.
Methods
We used a hammer.
Results
It worked.
Conclusion
Hammers are good.
References
[1] Someone, 2020.";
#[test]
fn extract_section_conclusion_returns_content() {
let s = extract_section(PAPER, "conclusion").unwrap();
assert!(s.contains("Hammers are good"), "got: {:?}", s);
}
#[test]
fn extract_section_conclusion_stops_before_references() {
let s = extract_section(PAPER, "conclusion").unwrap();
assert!(!s.contains("Someone, 2020"), "ran into references: {:?}", s);
}
#[test]
fn extract_section_methods_returns_only_methods() {
let s = extract_section(PAPER, "methods").unwrap();
assert!(s.contains("hammer"), "got: {:?}", s);
assert!(!s.contains("It worked"), "ran into results: {:?}", s);
}
#[test]
fn extract_section_is_case_insensitive() {
assert_eq!(
extract_section(PAPER, "METHODS"),
extract_section(PAPER, "methods")
);
}
#[test]
fn extract_section_unknown_heading_returns_none() {
assert!(extract_section(PAPER, "acknowledgements of nonexistence").is_none());
}
#[test]
fn extract_section_abstract_matches_the_generic_extractor() {
let text = extract_text(&fixture_path()).unwrap();
assert_eq!(
extract_section_abstract(&text),
extract_section(&text, "abstract")
);
}
#[test]
fn extract_text_nonexistent_file_returns_err() {
let result = extract_text(Path::new("/nonexistent/fake.pdf"));
assert!(result.is_err());
}
}