mod status;
use std::{
error::Error,
fmt::{self, Display, Formatter},
fs,
io::{self, Write},
path::PathBuf,
time::SystemTime,
};
use jotdown::{Parser, Render};
use rayon::prelude::*;
use super::Builder;
use crate::{latex, Document};
impl Builder {
pub fn write_pdf<W: Write>(&self, document: &Document, mut w: W) -> Result<(), PdfError> {
let with_name = |e| PdfError::from(e).document_name(&document.title);
let filename = document.filename();
let mut status = status::LoggingStatusBackend;
let config = tectonic::config::PersistentConfig::default();
let bundle = config
.default_bundle(false, &mut status)
.map_err(with_name)?;
let format_cache_path = config.format_cache_path().map_err(with_name)?;
let mut bytes = Vec::new();
self.write_latex(document, &mut bytes)?;
let files = {
let mut sb = tectonic::driver::ProcessingSessionBuilder::default();
sb.bundle(bundle)
.primary_input_buffer(&bytes)
.keep_intermediates(true)
.keep_logs(true)
.tex_input_name(&format!("{filename}.tex"))
.format_name("latex")
.format_cache_path(format_cache_path)
.output_format(tectonic::driver::OutputFormat::Pdf)
.build_date(SystemTime::now());
if let Some(ref build_dir) = self.build_dir {
let build_dir = build_dir.join(&filename);
sb.filesystem_root(&build_dir).output_dir(&build_dir);
fs::create_dir_all(&build_dir).map_err(|e| PdfError {
document_name: Some(document.title.clone()),
kind: PdfErrorKind::CreateDir {
path: build_dir,
source: e,
},
})?;
}
let mut sess = sb.create(&mut status).map_err(with_name)?;
sess.run(&mut status).map_err(with_name)?;
sess.into_file_data()
};
match files.get(&format!("{filename}.pdf")) {
Some(file) => w.write_all(&file.data)?,
None => {
return Err(PdfError {
document_name: Some(document.title.clone()),
kind: PdfErrorKind::NoPdfCreated,
})
}
}
Ok(())
}
pub fn write_latex<W: Write>(&self, document: &Document, mut w: W) -> Result<(), PdfError> {
let mut inner = || -> Result<(), PdfError> {
writeln!(w, r"\documentclass{{{}}}", document.document_type.as_ref())?;
DEFAULT_PACKAGES
.iter()
.try_for_each(|package| writeln!(w, r"\usepackage{{{package}}}"))?;
w.write_all(DEFAULT_PREAMBLE)?;
let locale = document
.locale
.split_once('_')
.map_or(document.locale.as_str(), |(s, _)| s);
writeln!(w, r"\setdefaultlanguage{{{locale}}}")?;
write!(w, r"\title{{")?;
latex::Renderer::default().write(Parser::new(&document.title), &mut w)?;
writeln!(w, "}}")?;
match document.date.format_with_locale(&document.locale) {
Some(date) => writeln!(w, r"\date{{{date}}}")?,
None => writeln!(w, r"\predate{{}}\date{{}}\postdate{{}}")?,
}
if document.authors.is_empty() {
writeln!(w, r"\preauthor{{}}\author{{}}\postauthor{{}}")?;
}
for author in &document.authors {
write!(w, r"\author{{{}", author.name)?;
if let Some(ref email) = author.email {
write!(w, r" \thanks{{\href{{mailto:{email}}}{{{email}}}}}")?;
}
writeln!(w, "}}")?;
}
writeln!(w, r"\begin{{document}}")?;
document
.texts
.par_iter()
.try_fold_with(Vec::new(), |mut buf, text| {
latex::Renderer::default()
.number_sections(self.number_sections)
.write(Parser::new(text), &mut buf)?;
Ok(buf)
})
.collect::<Result<Vec<Vec<u8>>, PdfError>>()?
.into_iter()
.try_for_each(|s| w.write_all(&s))?;
writeln!(w, r"\end{{document}}")?;
Ok(())
};
inner().map_err(|e| e.document_name(&document.title))
}
}
#[non_exhaustive]
#[derive(Debug)]
pub struct PdfError {
pub document_name: Option<String>,
pub kind: PdfErrorKind,
}
impl PdfError {
#[must_use]
pub fn document_name(self, document_name: &str) -> Self {
Self {
document_name: Some(document_name.to_string()),
..self
}
}
}
impl From<io::Error> for PdfError {
fn from(e: io::Error) -> Self {
Self {
document_name: None,
kind: PdfErrorKind::Io(e),
}
}
}
impl From<tectonic::Error> for PdfError {
fn from(e: tectonic::Error) -> Self {
Self {
document_name: None,
kind: PdfErrorKind::Tectonic(e),
}
}
}
impl Display for PdfError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if let Some(name) = &self.document_name {
write!(f, "{name} - ")?;
}
match &self.kind {
PdfErrorKind::Tectonic(_) => write!(f, "tectonic errored during pdf build"),
PdfErrorKind::Io(e) => write!(f, "io error: {e}"),
PdfErrorKind::CreateDir { path, .. } => {
write!(f, "failed to create directory {path:?}")
}
PdfErrorKind::NoPdfCreated => write!(f, "engine finished, but no pdf was created"),
}
}
}
impl Error for PdfError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match &self.kind {
PdfErrorKind::Tectonic(source) => Some(source),
PdfErrorKind::Io(source) => Some(source),
PdfErrorKind::CreateDir { source, .. } => Some(source),
PdfErrorKind::NoPdfCreated => None,
}
}
}
unsafe impl Sync for PdfError {}
#[non_exhaustive]
#[derive(Debug)]
pub enum PdfErrorKind {
Tectonic(tectonic::Error),
Io(io::Error),
CreateDir { path: PathBuf, source: io::Error },
NoPdfCreated,
}
const DEFAULT_PACKAGES: [&str; 17] = [
"amsmath",
"authblk",
"bookmark",
"graphicx",
"hyperref",
"microtype",
"parskip",
"soul",
"titling",
"upquote",
"xurl",
"xcolor",
"lmodern",
"unicode-math",
"polyglossia",
"pifont",
"enumitem",
];
const DEFAULT_PREAMBLE: &[u8] = br#"
\defaultfontfeatures{Scale=MatchLowercase}
\defaultfontfeatures[\rmfamily]{Ligatures=TeX,Scale=1}
% Task lists
\newcommand{\checkbox}{\text{\fboxsep=-.15pt\fbox{\rule{0pt}{1.5ex}\rule{1.5ex}{0pt}}}}
\newcommand{\done}{\rlap{\checkbox}{\raisebox{2pt}{\large\hspace{1pt}\ding{51}}}\hspace{-2.5pt}}
\newlist{tasklist}{itemize}{2}
\setlist[tasklist]{label=\checkbox}
% Other settings
\UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts
\setlength{\emergencystretch}{3em} % prevent overfull lines
\providecommand{\tightlist}{%
\setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
\urlstyle{same} % disable monospaced font for URLs
\hypersetup{
colorlinks=true,
allcolors=.,
urlcolor=blue,
linktocpage,
pdfcreator={djoc}}
"#;