doiget_cli/commands/
tex_source.rs1use std::io::Write;
14
15use anyhow::{Context, Result};
16
17use doiget_core::paper_tex_source::{paper_tex_source, resolve_arxiv_src_base, PaperTexSource};
18use doiget_core::{ArxivId, ErrorCode, Ref};
19
20use super::fetch::{build_resolve_context, cli_exit_code, CliExit};
21use super::output::print_err;
22use super::output::OutputMode;
23
24pub async fn run(
30 ref_: String,
31 max_chars: Option<usize>,
32 no_cache: bool,
33 mode: OutputMode,
34 quiet_was_explicit: bool,
35) -> Result<()> {
36 let parsed = super::parse_ref_or_exit(&ref_)?;
42 let id: ArxivId = match parsed {
43 Ref::Arxiv(a) => a,
44 Ref::Doi(_) => {
45 let code = ErrorCode::NoOaAvailable;
46 print_err(format_args!(
47 "error[{}]: no TeX source for a bare DOI — if an arXiv preprint exists, \
48 pass its id (e.g. `doiget tex-source arxiv:2401.12345`)",
49 code.as_wire()
50 ));
51 return Err(anyhow::Error::new(CliExit(cli_exit_code(code))));
52 }
53 };
54
55 let base = resolve_arxiv_src_base().map_err(|e| anyhow::anyhow!("{e}"))?;
56 let mut ctx = build_resolve_context().context("building fetch context")?;
57 if no_cache {
58 ctx.cache_root = None;
59 }
60
61 let tex = match paper_tex_source(&base, &id, max_chars, &ctx).await {
62 Ok(t) => t,
63 Err(e) => {
64 let code = ErrorCode::from(&e);
65 print_err(format_args!("error[{}]: {e}", code.as_wire()));
66 if code == ErrorCode::TextUnavailable {
67 print_err(format_args!(
68 " = note: no TeX source available (PDF-only or no .tex files). \
69 Fetch the PDF instead: `doiget fetch arxiv:{}`",
70 id.as_str()
71 ));
72 }
73 return Err(anyhow::Error::new(CliExit(cli_exit_code(code))));
74 }
75 };
76
77 if mode == OutputMode::Quiet && quiet_was_explicit {
80 return Ok(());
81 }
82
83 let stdout = std::io::stdout();
84 let mut out = stdout.lock();
85 if mode == OutputMode::Json {
86 let s = serde_json::to_string_pretty(&tex).context("serializing tex-source JSON")?;
87 writeln!(out, "{s}").context("writing tex-source JSON to stdout")?;
88 return Ok(());
89 }
90
91 render_human(&mut out, &tex)?;
92 Ok(())
93}
94
95fn render_human(out: &mut impl Write, tex: &PaperTexSource) -> Result<()> {
96 if let Some(f) = &tex.main_file {
97 writeln!(out, "% source: {f}").context("writing file header")?;
98 }
99 writeln!(out, "{}", tex.tex_source).context("writing tex source to stdout")?;
100 if tex.truncated {
101 print_err(format_args!(
102 "note: output truncated to {} chars (raise or drop --max-chars for the full source)",
103 tex.char_count
104 ));
105 }
106 Ok(())
107}
108
109#[cfg(test)]
110#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic, missing_docs)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn human_render_emits_file_header_and_source() {
116 let tex = PaperTexSource {
117 arxiv_id: "2401.12345".into(),
118 main_file: Some("main.tex".into()),
119 tex_source: "\\documentclass{article}".into(),
120 char_count: 23,
121 truncated: false,
122 retrieved_from: "https://export.arxiv.org/src/2401.12345".into(),
123 };
124 let mut buf: Vec<u8> = Vec::new();
125 render_human(&mut buf, &tex).expect("render");
126 let s = String::from_utf8(buf).expect("utf8");
127 assert!(s.contains("% source: main.tex"), "got: {s}");
128 assert!(s.contains("\\documentclass"), "got: {s}");
129 }
130
131 #[test]
132 fn json_envelope_has_expected_fields() {
133 let tex = PaperTexSource {
134 arxiv_id: "2401.12345".into(),
135 main_file: None,
136 tex_source: "\\documentclass{article}".into(),
137 char_count: 23,
138 truncated: false,
139 retrieved_from: "https://export.arxiv.org/src/2401.12345".into(),
140 };
141 let v = serde_json::to_value(&tex).expect("serialize");
142 assert_eq!(v["arxiv_id"], "2401.12345");
143 assert_eq!(v["truncated"], false);
144 assert_eq!(v["char_count"], 23);
145 }
146}