use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
const ENGINE: &str = "xelatex";
const TEX_NAME: &str = "doc.tex";
const PDF_NAME: &str = "doc.pdf";
pub fn compile(latex_source: &str, work_dir: &Path) -> Result<PathBuf, Box<dyn Error>> {
ensure_engine_available()?;
fs::create_dir_all(work_dir)?;
let tex_path = work_dir.join(TEX_NAME);
fs::write(&tex_path, latex_source)?;
for pass in 1..=2 {
run_pass(work_dir, pass)?;
}
let pdf = work_dir.join(PDF_NAME);
if !pdf.exists() {
return Err(format!(
"{ENGINE} ran without error but produced no PDF at {}",
pdf.display()
)
.into());
}
Ok(pdf)
}
pub fn tex_path(work_dir: &Path) -> PathBuf {
work_dir.join(TEX_NAME)
}
fn ensure_engine_available() -> Result<(), Box<dyn Error>> {
let probe = Command::new(ENGINE).arg("--version").output();
match probe {
Ok(o) if o.status.success() => Ok(()),
Ok(_) => Err(format!("{ENGINE} is installed but failed its --version probe").into()),
Err(_) => Err(format!(
"{ENGINE} not found on PATH. Install MacTeX (https://tug.org/mactex/) or another TeX Live distribution."
)
.into()),
}
}
fn run_pass(work_dir: &Path, pass: u32) -> Result<(), Box<dyn Error>> {
let output = Command::new(ENGINE)
.arg("-interaction=nonstopmode")
.arg("-halt-on-error")
.arg("-output-directory")
.arg(work_dir)
.arg(TEX_NAME)
.current_dir(work_dir)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let log = work_dir.join("doc.log");
let log_tail = fs::read_to_string(&log)
.map(|s| tail(&s, 60))
.unwrap_or_else(|_| String::from("(no log file produced)"));
return Err(format!(
"{ENGINE} pass {pass} failed.\n--- stderr ---\n{stderr}\n--- stdout tail ---\n{}\n--- log tail ---\n{log_tail}",
tail(&stdout, 40)
)
.into());
}
Ok(())
}
fn tail(text: &str, n: usize) -> String {
let lines: Vec<&str> = text.lines().collect();
let start = lines.len().saturating_sub(n);
lines[start..].join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tail_returns_last_n_lines() {
let s = (1..=10)
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join("\n");
assert_eq!(tail(&s, 3), "8\n9\n10");
assert_eq!(tail(&s, 100), s);
}
#[test]
fn tex_path_is_inside_work_dir() {
let dir = Path::new("/tmp/zim-pdf-test");
assert_eq!(tex_path(dir), dir.join("doc.tex"));
}
}