mdoc 0.3.0

Modern PDF creation through Markdown and LaTeX
Documentation
mod builder;
mod chapter;

pub use builder::*;
pub use chapter::*;

use crate::{
    bib,
    config::Config,
    error::Error,
    pandoc::{html_template, latex_template, lua, Pandoc, PandocFormat, PandocOption},
};
use anyhow::{anyhow, Context, Result};
use std::path::PathBuf;

pub struct Document {
    pub chapters: Vec<Chapter>,
    pub config: Config,
    pub root: Option<PathBuf>,
}

impl Document {
    pub fn from(content: impl Into<String>) -> Self {
        Document {
            chapters: vec![Chapter::new(content)],
            config: Config::default(),
            root: None,
        }
    }

    fn content(&self) -> Option<String> {
        let mut content = String::new();
        for ch in self.chapters.iter() {
            content.push_str(&ch.content);
            content.push_str("\n\n");
        }
        if content.trim().is_empty() {
            None
        } else {
            Some(content)
        }
    }

    fn setup_pandoc(&self) -> Result<Pandoc> {
        let mut pandoc = Pandoc::new();

        for filter in lua::get_filters()?.iter() {
            pandoc.push_opt(PandocOption::LuaFilter(filter.to_owned()))
        }

        for bib_file in bib::get_bib_files(self.root.as_ref()).iter() {
            pandoc.push_opt(PandocOption::Bibliography(bib_file.to_owned()));
        }

        for author in self.config.authors.iter() {
            pandoc.push_opt(PandocOption::Author(author.to_owned()));
        }

        if self.config.style.number_sections {
            pandoc.push_opt(PandocOption::NumberSections);
        }

        if let Some(class) = &self.config.style.document_class {
            pandoc.push_opt(PandocOption::DocumentClass(class.to_owned()));
        }

        pandoc.push_opt(PandocOption::Csl(bib::get_csl(&self.config.bib.csl)?));
        pandoc.push_opt(PandocOption::Title(self.config.title.to_owned()));
        pandoc.push_opt(PandocOption::Date(self.config.date()));
        pandoc.push_opt(PandocOption::Citeproc);
        pandoc.push_opt(PandocOption::LinkCitations);
        pandoc.push_opt(PandocOption::Standalone);
        pandoc.push_opt(PandocOption::From(PandocFormat::Markdown));

        Ok(pandoc)
    }

    pub fn latex(&self) -> Result<String> {
        Ok(String::from_utf8(self.latex_bytes()?)?)
    }

    pub fn latex_bytes(&self) -> Result<Vec<u8>> {
        if let Some(content) = self.content() {
            let mut pandoc = self.setup_pandoc()?;

            if let Some(header) = self.config.latex_header() {
                pandoc.include_in_header(&header)?;
            }

            if let Some(title_script) = &self.config.latex.title_script {
                pandoc.push_opt(PandocOption::TitleScript(title_script.to_owned()))
            }

            pandoc.push_opt(PandocOption::Template(latex_template()?));
            pandoc.push_opt(PandocOption::To(PandocFormat::Latex));
            pandoc.run(content.as_bytes()).context("Pandoc errored.")
        } else {
            Err(anyhow!("The document has no content.")).context("Cannot convert to LaTeX.")
        }
    }

    pub fn html_bytes(&self) -> Result<Vec<u8>> {
        if let Some(content) = self.content() {
            let mut pandoc = self.setup_pandoc()?;
            pandoc.push_opt(PandocOption::Katex);
            pandoc.push_opt(PandocOption::Template(html_template()?));
            pandoc.push_opt(PandocOption::To(PandocFormat::Html));
            pandoc.run(content.as_bytes()).context("Pandoc errored.")
        } else {
            Err(anyhow!("The document has no content.")).context("Cannot convert to LaTeX.")
        }
    }

    pub fn pdf_bytes(&self) -> Result<Vec<u8>> {
        let filename = &self.config.filename();

        let mut status = crate::log::MdocTectonicStatusBackend {
            tidy_logs: self.config.build.tidy_logs,
        };

        let config = tectonic::config::PersistentConfig::open(false)
            .map_err(Error::Tectonic)
            .context("Failed to open the default configuration file.")?;

        let only_cached = false;
        let bundle = config
            .default_bundle(only_cached, &mut status)
            .map_err(Error::Tectonic)
            .context("Failed to load the default resource bundle.")?;

        let format_cache_path = config
            .format_cache_path()
            .map_err(Error::Tectonic)
            .context("Failed to set up the format cache.")?;

        let mut files = {
            let mut sb = tectonic::driver::ProcessingSessionBuilder::default();
            sb.bundle(bundle)
                .primary_input_buffer(&self.latex_bytes()?)
                .tex_input_name(&format!("{}.tex", filename))
                .format_name("latex")
                .format_cache_path(format_cache_path)
                .output_format(tectonic::driver::OutputFormat::Pdf)
                .build_date(std::time::SystemTime::now())
                .do_not_write_output_files();

            let mut sess = sb
                .create(&mut status)
                .map_err(Error::Tectonic)
                .context("Failed to initialize the LaTeX processing session.")?;

            sess.run(&mut status)
                .map_err(Error::Tectonic)
                .context("The LaTeX engine failed.")?;

            sess.into_file_data()
        };

        match files.remove(&format!("{}.pdf", filename)) {
            Some(file) => Ok(file.data),
            None => Err(std::io::Error::new(
                std::io::ErrorKind::Other,
                "LaTeX didn't report failure, but no PDF was created.",
            )
            .into()),
        }
    }
}