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