use super::latex::{PreparedFile, escape, prepare_file};
use super::walk::DirNode;
use std::error::Error;
use std::path::Path;
pub struct PdfOptions {
pub title: String,
pub author: Option<String>,
}
const PREAMBLE: &str = r#"\documentclass[11pt]{article}
\usepackage[margin=1in]{geometry}
\usepackage{hyperref}
\IfFileExists{parskip.sty}{\usepackage{parskip}}{}
\providecommand{\sout}[1]{#1}
\IfFileExists{ulem.sty}{\usepackage{ulem}}{}
\setcounter{secnumdepth}{0}
\setcounter{tocdepth}{2}
\hypersetup{colorlinks=true, linkcolor=black, urlcolor=blue}
"#;
pub fn build_latex(tree: &[DirNode], opts: &PdfOptions) -> Result<String, Box<dyn Error>> {
let mut s = String::with_capacity(8 * 1024);
s.push_str(PREAMBLE);
s.push_str(&format!("\\title{{{}}}\n", escape(&opts.title)));
if let Some(a) = &opts.author {
s.push_str(&format!("\\author{{{}}}\n", escape(a)));
} else {
s.push_str("\\date{}\n");
}
s.push_str("\\begin{document}\n\\maketitle\n\\tableofcontents\n");
for (i, dir) in tree.iter().enumerate() {
if i > 0 {
s.push_str("\\clearpage\n");
}
emit_directory(&mut s, dir)?;
}
s.push_str("\\end{document}\n");
Ok(s)
}
fn emit_directory(out: &mut String, dir: &DirNode) -> Result<(), Box<dyn Error>> {
let readme_prepared = match &dir.readme {
Some(p) => Some(prepare_file(p, 1)?),
None => None,
};
let section_title = section_title(dir, readme_prepared.as_ref());
out.push_str(&format!("\\section{{{}}}\n", escape(§ion_title)));
if let Some(prep) = &readme_prepared {
out.push_str(&prep.body_latex);
if !prep.body_latex.ends_with('\n') {
out.push('\n');
}
}
for md in &dir.other_md {
let prep = prepare_file(md, 2)?;
let sub_title = subsection_title(md, &prep);
out.push_str(&format!("\\subsection{{{}}}\n", escape(&sub_title)));
out.push_str(&prep.body_latex);
if !prep.body_latex.ends_with('\n') {
out.push('\n');
}
}
Ok(())
}
fn section_title(dir: &DirNode, readme: Option<&PreparedFile>) -> String {
if let Some(prep) = readme
&& let Some(t) = &prep.first_h1
&& !t.is_empty()
{
return t.clone();
}
if let Some(prep) = readme
&& let Some(t) = &prep.frontmatter_title
&& !t.is_empty()
{
return t.clone();
}
if dir.is_root() {
"Overview".to_string()
} else {
dir.rel_path.to_string_lossy().into_owned()
}
}
fn subsection_title(path: &Path, prep: &PreparedFile) -> String {
if let Some(t) = &prep.frontmatter_title
&& !t.is_empty()
{
return t.clone();
}
if let Some(t) = &prep.first_h1
&& !t.is_empty()
{
return t.clone();
}
path.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "Untitled".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
fn touch(p: &PathBuf, body: &str) {
if let Some(parent) = p.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(p, body).unwrap();
}
#[test]
fn root_has_no_clearpage_subdirs_do() {
let tmp = TempDir::new().unwrap();
let root_readme = tmp.path().join("README.md");
touch(&root_readme, "# Top\n\nintro\n");
let mixes_readme = tmp.path().join("mixes/README.md");
touch(&mixes_readme, "# Mixes\n\nconcepts\n");
let track = tmp.path().join("mixes/track.md");
touch(&track, "---\ntitle: Track One\n---\n\nbody\n");
let tree =
super::super::walk::walk(tmp.path(), &crate::zimignore::ZimIgnore::new()).unwrap();
let tex = build_latex(
&tree,
&PdfOptions {
title: "Demo".into(),
author: None,
},
)
.unwrap();
let root_pos = tex.find("\\section{Top}").expect("root section present");
let pre_root = &tex[..root_pos];
assert!(!pre_root.contains("\\clearpage"));
let mixes_pos = tex.find("\\section{Mixes}").expect("mixes section present");
let between = &tex[root_pos..mixes_pos];
assert!(between.contains("\\clearpage"));
assert!(tex.contains("\\subsection{Track One}"));
}
#[test]
fn fallback_section_title_uses_dir_name() {
let tmp = TempDir::new().unwrap();
touch(&tmp.path().join("only/track.md"), "body\n");
let tree =
super::super::walk::walk(tmp.path(), &crate::zimignore::ZimIgnore::new()).unwrap();
let tex = build_latex(
&tree,
&PdfOptions {
title: "Demo".into(),
author: None,
},
)
.unwrap();
assert!(tex.contains("\\section{only}"));
assert!(tex.contains("\\subsection{track}"));
}
}