use clap::{App, Arg};
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
mod io;
mod templates;
fn main() -> std::io::Result<()> {
match parse_cli_args() {
Ok(x) => {
std::io::stdout().write_all(x.as_bytes())?;
std::process::exit(0)
}
Err(x) => {
std::io::stderr().write_all(x.as_bytes())?;
std::process::exit(1)
}
};
}
fn parse_cli_args() -> Result<String, String> {
let matches = App::new("manus")
.version("0.1.0")
.author("Erik Mannerfelt")
.about("Handle tex manuscripts.")
.arg(
Arg::new("verbosity")
.short('v')
.long("verbose")
.multiple(true)
.takes_value(false)
.global(true)
.about("Print non-error messages. -vv is more verbose."),
)
.subcommand(
App::new("build")
.about("Render the manuscript with tectonic.")
.arg(
Arg::new("INPUT")
.about("The input root tex file. If '-', read from stdin.")
.required(true),
)
.arg(
Arg::new("OUTPUT")
.about("The output pdf path. Defaults to the current directory.")
.required(false),
)
.arg(
Arg::new("DATA")
.about("Data filepath. If '-', read from stdin.")
.short('d')
.long("data")
.takes_value(true),
)
.arg(
Arg::new("KEEP_INTERMEDIATES")
.about("Keep intermediate files.")
.short('k')
.long("keep-intermediates"),
)
.arg(
Arg::new("SYNCTEX")
.about("Generate synctex data")
.short('s')
.long("synctex"),
),
)
.subcommand(
App::new("convert")
.about("Convert to different formats.")
.arg(
Arg::new("INPUT")
.about("The input root tex file. If '-', read from stdin.")
.required(true)
.index(1),
)
.arg(
Arg::new("DATA")
.about("Data filepath. If '-', read from stdin.")
.short('d')
.long("data")
.takes_value(true),
)
.arg(
Arg::new("FORMAT")
.about("Format. Choices: [tex]. Defaults to tex.")
.short('f')
.long("format"),
),
)
.subcommand(
App::new("merge").about("Merge 'input' clauses.").arg(
Arg::new("INPUT")
.about("The input root tex file.")
.required(true)
.index(1),
),
)
.get_matches();
let verbosity = match matches.occurrences_of("verbosity") {
x if x < 3 => x,
x => return Err(format!("Invalid verbosity level: {}. Max: 2", x)),
};
if let Some(ref matches) = matches.subcommand_matches("build") {
let path_str = matches
.value_of("INPUT")
.expect("It's a required argument so this shouldn't fail.");
let (mut lines, pdf_filepath) =
match io::get_lines_and_output_path(path_str, matches.value_of("OUTPUT")) {
Ok(x) => x,
Err(e) => return Err(e.to_string()),
};
if let Some(datafile) = matches.value_of("DATA") {
if (datafile.trim() == "-") & (path_str.trim() == "-") {
return Err("Input tex and data cannot both be from stdin.".into());
};
let data = match io::get_data_from_str(&datafile) {
Ok(v) => v,
Err(e) => return Err(e.to_string()),
};
lines = templates::fill_data(&lines, &data)?;
};
let keep_intermediates = matches.is_present("KEEP_INTERMEDIATES");
let synctex = matches.is_present("SYNCTEX");
if let Some(parent) = pdf_filepath.parent() {
if !parent.is_dir() & !parent.to_str().unwrap().is_empty() {
return Err(format!(
"Parent directory '{}' does not exist",
parent.to_str().unwrap()
));
}
}
match run_tectonic(
&lines.join("\n"),
&pdf_filepath,
verbosity > 0,
keep_intermediates,
synctex) {
Ok(_) => (),
Err(_) if verbosity == 0 => return Err("Tectonic exited with an error. Run the command with --verbose to find out what went wrong.".into()),
Err(_) => ()
};
return Ok("".into());
}
if let Some(ref matches) = matches.subcommand_matches("convert") {
let path_str = matches
.value_of("INPUT")
.expect("It's a reqired argument so this won't fail.");
let (mut lines, _) =
match io::get_lines_and_output_path(path_str, matches.value_of("OUTPUT")) {
Ok(x) => x,
Err(e) => return Err(e.to_string()),
};
if let Some(datafile) = matches.value_of("DATA") {
if (datafile.trim() == "-") & (path_str.trim() == "-") {
return Err("Input tex and data cannot both be from stdin.".into());
};
let data = match io::get_data_from_str(&datafile) {
Ok(v) => v,
Err(e) => return Err(e.to_string()),
};
lines = templates::fill_data(&lines, &data)?;
};
return Ok(lines.join("\n"));
}
if let Some(ref matches) = matches.subcommand_matches("merge") {
let path_str = matches
.value_of("INPUT")
.expect("It's a reqired argument so this won't fail.");
let filepath = match io::parse_filepath(&path_str, Some("tex")) {
Ok(fp) => fp,
Err(e) => return Err(format!("{:?}", e)),
};
match merge_tex(&filepath) {
Ok(lines) => return Ok(lines.join("\n")),
Err(message) => return Err(format!("{:?}", message)),
};
}
Err("".into())
}
fn run_tectonic(
tex_string: &str,
output_path: &Path,
verbose: bool,
keep_intermediates: bool,
synctex: bool,
) -> tectonic::errors::Result<()> {
let mut status = tectonic::status::NoopStatusBackend::default();
let auto_create_config_file = false;
let config = tectonic::ctry!(tectonic::config::PersistentConfig::open(auto_create_config_file);
"failed to open the default configuration file");
let only_cached = false;
let bundle = tectonic::ctry!(config.default_bundle(only_cached, &mut status);
"failed to load the default resource bundle");
let format_cache_path = tectonic::ctry!(config.format_cache_path();
"failed to set up the format cache");
let mut files = {
let mut sb = tectonic::driver::ProcessingSessionBuilder::default();
sb.bundle(bundle)
.primary_input_buffer(tex_string.as_bytes())
.tex_input_name("texput.tex")
.format_name("latex")
.format_cache_path(format_cache_path)
.keep_logs(false)
.keep_intermediates(keep_intermediates)
.print_stdout(verbose)
.synctex(synctex)
.output_format(tectonic::driver::OutputFormat::Pdf)
.do_not_write_output_files();
let mut sess = tectonic::ctry!(sb.create(&mut status); "failed to initialize the LaTeX processing session");
tectonic::ctry!(sess.run(&mut status); "the LaTeX engine failed");
sess.into_file_data()
};
let file_data = match files.remove(&std::ffi::OsString::from(&"texput.pdf")) {
Some(file) => file.data,
None => {
return Err(tectonic::errmsg!(
"LaTeX didn't report failure, but no PDF was created (??)"
))
}
};
let mut file = File::create(&output_path).expect("");
file.write_all(&file_data).expect("");
if keep_intermediates | synctex {
for (filename_os, data) in files {
let filename = PathBuf::from(filename_os);
let extension = match filename == std::ffi::OsString::from(&"texput.synctex.gz") {
true => std::ffi::OsString::from("synctex.gz"),
false => match filename.extension() {
Some(x) => x.to_os_string(),
None => continue,
},
};
if !keep_intermediates & (extension != std::ffi::OsString::from("synctex.gz")) {
continue;
};
let mut path = PathBuf::from(output_path.file_stem().unwrap());
path.set_extension(extension);
if let Some(parent) = output_path.parent() {
path = parent.join(path);
}
let mut file = File::create(&path)
.unwrap_or_else(|_| panic!("Could not open {} to write", path.to_str().unwrap()));
file.write_all(&data.data)
.unwrap_or_else(|_| panic!("Could not write to {}.", path.to_str().unwrap()));
}
}
Ok(())
}
fn merge_tex(filepath: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let mut lines: Vec<String> = Vec::new();
let main_lines = io::read_tex(&filepath)?;
let mut i = 0;
for line in main_lines {
if !line.contains(r"\input{") {
lines.push(line);
i += 1;
continue;
}
let mut trimmed_line = line[(line.find(r"\input{").unwrap() + 7)..].to_owned();
trimmed_line = trimmed_line[..trimmed_line
.find('}')
.unwrap_or_else(|| panic!("Unclosed delimiter at line {}", i))]
.to_owned();
let mut input_path = PathBuf::from(trimmed_line);
if input_path.extension().is_none() {
let _ = input_path.set_extension("tex");
}
if !input_path.is_file() {
input_path = [filepath.parent().unwrap(), &input_path].iter().collect();
}
let input_lines = merge_tex(&PathBuf::from(&input_path))?;
for input_line in input_lines {
lines.push(input_line)
}
i += 1;
}
Ok(lines)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_merge_tex() {
let testpath = PathBuf::from("tests/data/case1/main.tex");
let lines = merge_tex(&testpath).unwrap();
assert_eq!(lines.len(), 13);
}
}