Skip to main content

fastpaper/commands/
read.rs

1use super::{CommandResult, emit, failed};
2use crate::cli::{GlobalOpts, OutputFormat, ReadArgs, Section};
3use crate::read as pdf;
4
5pub fn run(args: &ReadArgs, global: &GlobalOpts) -> CommandResult {
6    if !args.path.exists() {
7        return Err(failed(format!(
8            "No such file: {}\n`read` works on a local PDF. To fetch one first: fastpaper download <id>",
9            args.path.display()
10        )));
11    }
12
13    let full_text = pdf::extract_text(&args.path).map_err(failed)?;
14
15    let text = match heading_for(args.section) {
16        None => full_text,
17        Some(heading) => pdf::extract_section(&full_text, heading).ok_or_else(|| {
18            failed(format!(
19                "No '{}' section found in {}",
20                heading,
21                args.path.display()
22            ))
23        })?,
24    };
25
26    let text = match args.max_length {
27        Some(max) => text.chars().take(max).collect::<String>(),
28        None => text,
29    };
30
31    let rendered = match global.format {
32        OutputFormat::Json => serde_json::to_string_pretty(&serde_json::json!({
33            "path": args.path.to_string_lossy(),
34            "section": section_name(args.section),
35            "content": { "full_text": text },
36        }))
37        .unwrap(),
38        _ => text,
39    };
40
41    emit(&rendered, args.output.as_deref())
42}
43
44/// The heading `extract_section` should look for, or `None` for the whole text.
45fn heading_for(section: Section) -> Option<&'static str> {
46    match section {
47        Section::Full => None,
48        Section::Abstract => Some("abstract"),
49        Section::Introduction => Some("introduction"),
50        Section::Methods => Some("methods"),
51        Section::Results => Some("results"),
52        Section::Discussion => Some("discussion"),
53        Section::Conclusion => Some("conclusion"),
54        Section::References => Some("references"),
55    }
56}
57
58fn section_name(section: Section) -> &'static str {
59    heading_for(section).unwrap_or("full")
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn full_means_no_slicing() {
68        assert_eq!(heading_for(Section::Full), None);
69    }
70
71    // Every --section value must map to a heading the extractor knows about,
72    // otherwise the flag accepts a value it silently ignores.
73    #[test]
74    fn every_section_value_maps_to_a_known_heading() {
75        for section in [
76            Section::Abstract,
77            Section::Introduction,
78            Section::Methods,
79            Section::Results,
80            Section::Discussion,
81            Section::Conclusion,
82            Section::References,
83        ] {
84            let heading = heading_for(section).expect("should map to a heading");
85            let paper = format!("Title\nAbstract\nx\n{}\ncontent here\n", heading);
86            assert!(
87                crate::read::extract_section(&paper, heading).is_some(),
88                "{:?} maps to '{}' which the extractor does not find",
89                section,
90                heading
91            );
92        }
93    }
94
95    #[test]
96    fn section_name_reports_full_for_whole_document() {
97        assert_eq!(section_name(Section::Full), "full");
98        assert_eq!(section_name(Section::Methods), "methods");
99    }
100}