use std::{
collections::HashMap,
env, fs,
io::{Read, Write},
path::{Component, Path, PathBuf},
};
use tectonic_errors::prelude::*;
use crate::syntax;
use crate::workspace::WorkspaceCreator;
pub const DEFAULT_PREAMBLE_FILE: &str = "_preamble.tex";
pub const DEFAULT_INDEX_FILE: &str = "index.tex";
pub const DEFAULT_POSTAMBLE_FILE: &str = "_postamble.tex";
pub const DEFAULT_INPUTS: &[&str] = &["_preamble.tex", "index.tex", "_postamble.tex"];
#[derive(Debug)]
pub struct Document {
src_dir: PathBuf,
build_dir: PathBuf,
pub metadata: Option<toml::Value>,
pub name: String,
pub bundle_loc: String,
pub extra_paths: Vec<PathBuf>,
pub outputs: HashMap<String, OutputProfile>,
}
impl Document {
pub fn new_from_toml<P1: Into<PathBuf>, P2: Into<PathBuf>, R: Read>(
src_dir: P1,
build_dir: P2,
toml_data: &mut R,
) -> Result<Self> {
let mut toml_text = String::new();
toml_data.read_to_string(&mut toml_text)?;
let doc: syntax::TomlDocument = toml::from_str(&toml_text)?;
let mut outputs = HashMap::new();
for toml_output in &doc.outputs {
let output: OutputProfile = toml_output.into();
if outputs.insert(output.name.clone(), output).is_some() {
bail!(
"duplicated output name `{}` in TOML specification",
&toml_output.name
);
}
}
if outputs.is_empty() {
bail!("TOML specification must define at least one output");
}
Ok(Document {
src_dir: src_dir.into(),
build_dir: build_dir.into(),
name: doc.doc.name,
bundle_loc: doc.doc.bundle,
extra_paths: doc.doc.extra_paths.unwrap_or_default(),
metadata: doc.doc.metadata,
outputs,
})
}
pub fn create_toml(&self) -> Result<()> {
let outputs = self
.outputs
.values()
.map(syntax::TomlOutputProfile::from)
.collect();
let extra_paths = if self.extra_paths.is_empty() {
None
} else {
Some(self.extra_paths.clone())
};
let doc = syntax::TomlDocument {
doc: syntax::TomlDocSection {
name: self.name.clone(),
bundle: self.bundle_loc.clone(),
extra_paths,
metadata: None,
},
outputs,
};
let toml_text = toml::to_string_pretty(&doc)?;
let mut toml_path = self.src_dir.clone();
toml_path.push("Tectonic.toml");
let mut toml_file = atry!(fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&toml_path);
["couldn\'t create `{}`", toml_path.display()]
);
toml_file.write_all(toml_text.as_bytes())?;
Ok(())
}
pub fn src_dir(&self) -> &Path {
&self.src_dir
}
pub fn build_dir(&self) -> &Path {
&self.build_dir
}
pub fn output_names(&self) -> impl Iterator<Item = &str> {
self.outputs.keys().map(|k| k.as_ref())
}
pub fn output_main_file(&self, profile_name: &str) -> PathBuf {
let profile = self.outputs.get(profile_name).unwrap();
let mut p = self.build_dir.clone();
p.push(&profile.name);
match profile.target_type {
BuildTargetType::Pdf => {
p.push(&profile.name);
p.set_extension("pdf");
}
BuildTargetType::Html => {
p.push("index.html");
}
}
p
}
}
#[derive(Clone, Debug)]
pub struct OutputProfile {
pub name: String,
pub target_type: BuildTargetType,
pub tex_format: String,
pub inputs: Vec<InputFile>,
pub shell_escape: bool,
pub shell_escape_cwd: Option<String>,
pub synctex: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BuildTargetType {
Html,
Pdf,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum InputFile {
File(String),
Inline(String),
}
impl Document {
pub(crate) fn create_for(
wc: &WorkspaceCreator,
bundle_loc: String,
extra_paths: Vec<PathBuf>,
) -> Result<Self> {
let src_dir = wc.root_dir.clone();
let mut build_dir = src_dir.clone();
build_dir.push("build");
let name = {
let mut name = "document".to_owned();
let mut tried_src_path = false;
if let Some(Component::Normal(t)) = src_dir.components().next_back() {
tried_src_path = true;
if let Some(s) = t.to_str() {
s.clone_into(&mut name);
}
}
if !tried_src_path {
if let Ok(cwd) = env::current_dir() {
let full_path = cwd.join(&src_dir);
if let Some(Component::Normal(t)) = full_path.components().next_back() {
if let Some(s) = t.to_str() {
s.clone_into(&mut name);
}
}
}
}
name
};
Ok(Document {
src_dir,
build_dir,
name,
bundle_loc,
extra_paths,
outputs: crate::document::default_outputs(),
metadata: None,
})
}
}
pub(crate) fn default_outputs() -> HashMap<String, OutputProfile> {
let mut outputs = HashMap::new();
outputs.insert(
"default".to_owned(),
OutputProfile {
name: "default".to_owned(),
target_type: BuildTargetType::Pdf,
tex_format: "latex".to_owned(),
inputs: DEFAULT_INPUTS
.iter()
.map(|x| InputFile::File(x.to_string()))
.collect(),
shell_escape: false,
shell_escape_cwd: None,
synctex: false,
},
);
outputs
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use super::*;
#[test]
fn default_inputs() {
const TOML: &str = r#"
[doc]
name = "test"
bundle = "na"
[[output]]
name = "o"
type = "pdf"
"#;
let mut c = Cursor::new(TOML.as_bytes());
let doc = Document::new_from_toml(".", ".", &mut c).unwrap();
assert_eq!(
doc.outputs.get("o").unwrap().inputs,
DEFAULT_INPUTS
.iter()
.map(|x| InputFile::File(x.to_string()))
.collect::<Vec<InputFile>>()
);
}
#[test]
fn shell_escape_default_false() {
const TOML: &str = r#"
[doc]
name = "test"
bundle = "na"
[[output]]
name = "o"
type = "pdf"
"#;
let mut c = Cursor::new(TOML.as_bytes());
let doc = Document::new_from_toml(".", ".", &mut c).unwrap();
assert!(!doc.outputs.get("o").unwrap().shell_escape);
}
#[test]
fn shell_escape_cwd_implies_shell_escape() {
const TOML: &str = r#"
[doc]
name = "test"
bundle = "na"
[[output]]
name = "o"
type = "pdf"
shell_escape_cwd = "."
"#;
let mut c = Cursor::new(TOML.as_bytes());
let doc = Document::new_from_toml(".", ".", &mut c).unwrap();
assert!(doc.outputs.get("o").unwrap().shell_escape);
}
#[test]
fn synctex_default_false() {
const TOML: &str = r#"
[doc]
name = "test"
bundle = "na"
[[output]]
name = "o"
type = "pdf"
"#;
let mut c = Cursor::new(TOML.as_bytes());
let doc = Document::new_from_toml(".", ".", &mut c).unwrap();
assert!(!doc.outputs.get("o").unwrap().synctex);
}
#[test]
fn synctex_set_true() {
const TOML: &str = r#"
[doc]
name = "test"
bundle = "na"
[[output]]
name = "o"
type = "pdf"
synctex = true
"#;
let mut c = Cursor::new(TOML.as_bytes());
let doc = Document::new_from_toml(".", ".", &mut c).unwrap();
assert!(doc.outputs.get("o").unwrap().synctex);
}
}