From 0dda753c01e7fa5932fc3621cf34b4a47a64cefc Mon Sep 17 00:00:00 2001 From: Kenton Hamaluik Date: Tue, 3 Dec 2019 16:32:29 -0700 Subject: [PATCH] started working on compiling to latex --- Cargo.lock | 15 +- Cargo.toml | 4 +- src/cli.rs | 12 + src/extensions.rs | 73 +++++ src/html.rs | 462 ++++++++++++++++++++++++++++++++ src/{ => html}/filters.rs | 0 src/latex.rs | 46 ++++ src/latex/filters.rs | 5 + src/main.rs | 551 ++------------------------------------ templates/.gitignore | 276 +++++++++++++++++++ templates/book.tex | 14 + 11 files changed, 921 insertions(+), 537 deletions(-) create mode 100644 src/extensions.rs create mode 100644 src/html.rs rename src/{ => html}/filters.rs (100%) create mode 100644 src/latex.rs create mode 100644 src/latex/filters.rs create mode 100644 templates/.gitignore create mode 100644 templates/book.tex diff --git a/Cargo.lock b/Cargo.lock index 845e356..cb4e9ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -241,6 +241,7 @@ dependencies = [ "atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -650,7 +651,7 @@ dependencies = [ [[package]] name = "mkbook" -version = "0.3.0" +version = "0.4.0" dependencies = [ "askama 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1067,6 +1068,16 @@ dependencies = [ "yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "term_size" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "termcolor" version = "1.0.5" @@ -1080,6 +1091,7 @@ name = "textwrap" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ + "term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1435,6 +1447,7 @@ dependencies = [ "checksum syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)" = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" "checksum syn 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "661641ea2aa15845cddeb97dad000d22070bb5c1fb456b96c1cba883ec691e92" "checksum syntect 3.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "955e9da2455eea5635f7032fc3a229908e6af18c39600313866095e07db0d8b8" +"checksum term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9e5b9a66db815dcfd2da92db471106457082577c3c278d4138ab3e3b4e189327" "checksum termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "96d6098003bde162e4277c70665bd87c326f5a0c3f3fbfb285787fa482d54e6e" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" diff --git a/Cargo.toml b/Cargo.toml index 38c50e6..402dc98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mkbook" -version = "0.3.0" +version = "0.4.0" authors = ["Kenton Hamaluik "] edition = "2018" build = "build.rs" @@ -22,7 +22,7 @@ maintenance = { status = "actively-developed" } [dependencies] syntect = "3.3" comrak = "0.6" -clap = "2.33" +clap = { version = "2.33", features = ["wrap_help"] } askama = "0.8" serde = { version = "1.0", features = ["derive"] } toml = "0.5" diff --git a/src/cli.rs b/src/cli.rs index f077b17..f9270ed 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -28,6 +28,12 @@ pub fn build_cli() -> App<'static, 'static> { .default_value("book") .help("an optional directory to render the contents into") ) + .arg(Arg::with_name("latex") + .short("l") + .long("latex") + .takes_value(true) + .help("build a `.tex` file instead of a website") + ) ) .subcommand(SubCommand::with_name("watch") .about("build the book and continually rebuild whenever the source changes") @@ -43,6 +49,12 @@ pub fn build_cli() -> App<'static, 'static> { .default_value("book") .help("an optional directory to render the contents into") ) + .arg(Arg::with_name("latex") + .short("l") + .long("latex") + .takes_value(true) + .help("build a `.tex` file instead of a website") + ) .arg(Arg::with_name("reload") .short("r") .long("reload") diff --git a/src/extensions.rs b/src/extensions.rs new file mode 100644 index 0000000..9ec03e8 --- /dev/null +++ b/src/extensions.rs @@ -0,0 +1,73 @@ +pub fn create_katex_inline(src: &str) -> Result> { + use std::process::{Command, Stdio}; + use std::io::Write; + + let mut child = match Command::new("katex") + .arg("-d") + .arg("-t") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() { + Ok(c) => c, + Err(e) => { + log::warn!("failed to launch katex, not rendering math block: {:?}", e); + return Err(Box::from(e)); + } + }; + + let stdin = child.stdin.as_mut().expect("valid katex stdin"); + stdin.write_all(src.as_ref())?; + + let output = child.wait_with_output()?; + if !output.status.success() { + log::error!("failed to generate katex, exit code: {:?}", output.status.code()); + log::error!("katex STDOUT:"); + log::error!("{}", String::from_utf8_lossy(output.stdout.as_ref())); + log::error!("katex STDERR:"); + log::error!("{}", String::from_utf8_lossy(output.stdout.as_ref())); + log::error!("/katex output"); + return Err(Box::from("katex failed")); + } + let rendered: String = String::from_utf8(output.stdout)?; + + Ok(format!(r#"
{}
"#, rendered)) +} + +pub fn create_plantuml_svg(src: &str) -> Result> { + use std::process::{Command, Stdio}; + use std::io::Write; + + let mut child = match Command::new("plantuml") + .arg("-tsvg") + .arg("-nometadata") + .arg("-pipe") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() { + Ok(c) => c, + Err(e) => { + log::warn!("failed to launch plantuml, not rendering plantuml block: {:?}", e); + return Err(Box::from(e)) + } + }; + + let stdin = child.stdin.as_mut().expect("valid plantuml stdin"); + stdin.write_all(src.as_ref())?; + + let output = child.wait_with_output()?; + if !output.status.success() { + log::error!("failed to generate plantuml, exit code: {:?}", output.status.code()); + log::error!("plantuml STDOUT:"); + log::error!("{}", String::from_utf8_lossy(output.stdout.as_ref())); + log::error!("plantuml STDERR:"); + log::error!("{}", String::from_utf8_lossy(output.stdout.as_ref())); + log::error!("/plantuml output"); + return Err(Box::from("plantuml failed")); + } + let svg: String = String::from_utf8(output.stdout)?; + let svg = svg.replace(r#""#, ""); + + Ok(format!("
{}
", svg)) +} \ No newline at end of file diff --git a/src/html.rs b/src/html.rs new file mode 100644 index 0000000..00e1d13 --- /dev/null +++ b/src/html.rs @@ -0,0 +1,462 @@ +use std::path::{PathBuf, Path}; +use std::{fs, io}; +use comrak::ComrakOptions; +use syntect::{parsing::SyntaxSet, highlighting::{ThemeSet, Theme}}; +use askama::Template; + +use super::models::frontmatter::{ParsedFrontMatter, FrontMatter}; +use super::models::chapter::{Chapter}; +use super::extensions::{create_plantuml_svg, create_katex_inline}; + +pub const STYLESHEET: &'static str = include_str!(concat!(env!("OUT_DIR"), "/style.css")); +pub const ASSET_FAVICON: &'static [u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/favicon.ico")); +pub const ASSET_ICONS: &'static [u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/icons.svg")); + +pub const SYNTAX_TOML: &'static str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/syntaxes/TOML.sublime-syntax")); +pub const SYNTAX_HAXE: &'static str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/syntaxes/haxe.sublime-syntax")); +pub const SYNTAX_HXML: &'static str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/syntaxes/hxml.sublime-syntax")); +pub const SYNTAX_SASS: &'static str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/syntaxes/Sass.sublime-syntax")); +pub const SYNTAX_SCSS: &'static str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/syntaxes/SCSS.sublime-syntax")); + +lazy_static! { + static ref HIGHLIGHT_SYNTAX_SETS: SyntaxSet = { + use syntect::parsing::SyntaxDefinition; + + let ss = SyntaxSet::load_defaults_newlines(); + let mut ssb = ss.into_builder(); + ssb.add(SyntaxDefinition::load_from_str(SYNTAX_TOML, true, None).expect("valid TOML syntax definition")); + ssb.add(SyntaxDefinition::load_from_str(SYNTAX_HAXE, true, None).expect("valid haxe syntax definition")); + ssb.add(SyntaxDefinition::load_from_str(SYNTAX_HXML, true, None).expect("valid hxml syntax definition")); + ssb.add(SyntaxDefinition::load_from_str(SYNTAX_SASS, true, None).expect("valid sass syntax definition")); + ssb.add(SyntaxDefinition::load_from_str(SYNTAX_SCSS, true, None).expect("valid scss syntax definition")); + let ss = ssb.build(); + + //if cfg!(debug_assertions) { + // let mut syntaxes: Vec<(String, String)> = ss.syntaxes().iter() + // .map(|s| (s.name.clone(), s.file_extensions.iter().map(|s| &**s).collect::>().join("`, `"))) + // .collect(); + // syntaxes.sort_by(|a, b| a.0.cmp(&b.0)); + // for syntax in syntaxes { + // println!("{}\n\n: `{}`\n\n", syntax.0, syntax.1); + // } + //} + + ss + }; + static ref HIGHLIGHT_THEME_SETS: ThemeSet = ThemeSet::load_defaults(); + static ref HIGHLIGHT_THEME: &'static Theme = &HIGHLIGHT_THEME_SETS.themes["base16-eighties.dark"]; + static ref COMRAK_OPTIONS: ComrakOptions = ComrakOptions { + hardbreaks: false, + smart: true, + github_pre_lang: false, + default_info_string: None, + unsafe_: true, + ext_strikethrough: true, + ext_tagfilter: false, + ext_table: true, + ext_autolink: true, + ext_tasklist: true, + ext_superscript: true, + ext_header_ids: Some("header".to_owned()), + ext_footnotes: true, + ext_description_lists: true, + ..ComrakOptions::default() + }; +} + +mod filters; + +struct FormatResponse { + output: String, + include_katex_css: bool, +} + +fn format_code(lang: &str, src: &str) -> Result> { + use syntect::parsing::SyntaxReference; + use syntect::html::highlighted_html_for_string; + + // render plantuml code blocks into an inline svg + if lang == "plantuml" { + return Ok(FormatResponse { + output: create_plantuml_svg(src)?, + include_katex_css: false, + }); + } + // render katex code blocks into an inline math + if lang == "katex" { + return Ok(FormatResponse { + output: create_katex_inline(src)?, + include_katex_css: true, + }); + } + + let syntax: Option<&SyntaxReference> = if lang.len() > 0 { + let syntax = HIGHLIGHT_SYNTAX_SETS.find_syntax_by_token(lang); + if syntax.is_none() { + eprintln!("warning: language `{}` not recognized, formatting code block as plain text!", lang); + } + syntax + } + else { + None + }; + let syntax = syntax.unwrap_or(HIGHLIGHT_SYNTAX_SETS.find_syntax_plain_text()); + + let html = highlighted_html_for_string(src, &HIGHLIGHT_SYNTAX_SETS, &syntax, &HIGHLIGHT_THEME); + + Ok(FormatResponse { + output: html, + include_katex_css: false, + }) +} + +fn wrap_image_in_figure(link: &comrak::nodes::NodeLink, alt: &str) -> Result> { + let title = String::from_utf8_lossy(link.title.as_ref()); + let url = String::from_utf8_lossy(link.url.as_ref()); + if title.len() > 0 { + Ok(format!(r#"
{}
{}
"#, url, alt, title, title)) + } + else { + Ok(format!(r#"
{}
"#, url, alt)) + } +} + +fn format_markdown(src: &str) -> Result> { + use comrak::{Arena, parse_document, format_html}; + use comrak::nodes::{AstNode, NodeValue}; + + let arena = Arena::new(); + + let root = parse_document( + &arena, + src, + &COMRAK_OPTIONS); + + fn iter_nodes<'a, F>(node: &'a AstNode<'a>, f: &mut F) -> Result<(), Box> + where F : FnMut(&'a AstNode<'a>) -> Result<(), Box> { + f(node)?; + for c in node.children() { + iter_nodes(c, f)?; + } + Ok(()) + } + + let mut use_katex_css = false; + iter_nodes(root, &mut |node| { + let value = &mut node.data.borrow_mut().value; + match value { + NodeValue::CodeBlock(ref block) => { + let lang = String::from_utf8_lossy(block.info.as_ref()); + let source = String::from_utf8_lossy(block.literal.as_ref()); + let FormatResponse { output, include_katex_css } = format_code(&lang, &source)?; + if include_katex_css { + use_katex_css = true; + } + let highlighted: Vec = Vec::from(output.into_bytes()); + *value = NodeValue::HtmlInline(highlighted); + }, + NodeValue::Paragraph => { + if node.children().count() == 1 { + let first_child = &node.first_child().unwrap(); + let first_value = &first_child.data.borrow().value; + if let NodeValue::Image(link) = first_value { + if first_child.children().count() > 0 { + let mut alt: String = String::default(); + for child in first_child.children() { + if let NodeValue::Text(t) = &child.data.borrow().value { + alt.push_str(&String::from_utf8_lossy(&t)); + } + child.detach(); + } + first_child.detach(); + let figure = wrap_image_in_figure(&link, &alt)?; + let figure: Vec = Vec::from(figure.into_bytes()); + *value = NodeValue::HtmlInline(figure); + } + } + } + }, + _ => {} + } + Ok(()) + })?; + + let mut output: Vec = Vec::with_capacity((src.len() as f64 * 1.2) as usize); + format_html(root, &COMRAK_OPTIONS, &mut output).expect("can format HTML"); + let output = String::from_utf8(output).expect("valid utf-8 generated HTML"); + Ok(FormatResponse { + output, + include_katex_css: use_katex_css, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_generate_figures() { + let src = r#"![bear](https://placebear.com/g/512/256 "A majestic bear")"#; + let result = format_markdown(src).expect("can format"); + assert_eq!(result.output, r#"
bear
A majestic bear
"#); + } +} + +#[derive(Template)] +#[template(path = "index.html")] +struct IndexTemplate<'a, 'b, 'c> { + book: &'a FrontMatter, + chapters: &'b Vec, + book_description: &'c str, + include_katex_css: bool, + include_reload_script: bool, +} + +fn generate_index(book: &FrontMatter, content: String, include_katex_css: bool, chapters: &Vec, mut output: W, include_reload_script: bool) -> Result<(), Box> { + // fill out our template + let template = IndexTemplate { + book, + chapters, + book_description: &content, + include_katex_css, + include_reload_script, + }; + + // and render! + let s = template.render()?; + output.write_all(s.as_bytes())?; + + Ok(()) +} + +#[derive(Template)] +#[template(path = "page.html")] +struct PageTemplate<'a, 'b, 'c, 'd, 'e, 'g> { + chapter: &'a Chapter, + content: &'b str, + chapters: &'c Vec, + prev_chapter: Option<&'d Chapter>, + next_chapter: Option<&'e Chapter>, + book: &'g FrontMatter, + include_katex_css: bool, + include_reload_script: bool, +} + +fn format_page(book: &FrontMatter, chapter: &Chapter, chapters: &Vec, prev_chapter: Option<&Chapter>, next_chapter: Option<&Chapter>, content: &str, include_katex_css: bool, mut output: W, include_reload_script: bool) -> Result<(), Box> { + // fill out our template + let template = PageTemplate { + chapter, + content, + chapters, + prev_chapter, + next_chapter, + book, + include_katex_css, + include_reload_script, + }; + + // and render! + let s = template.render()?; + output.write_all(s.as_bytes())?; + + Ok(()) +} + +pub fn build, POut: AsRef>(src: PIn, dest: POut, include_reload_script: bool) -> Result<(), Box> { + let src = PathBuf::from(src.as_ref()); + let dest = PathBuf::from(dest.as_ref()); + if !dest.exists() { + std::fs::create_dir_all(&dest)?; + log::info!("created directory `{}`...", dest.display()); + } + + // load our book + let book_readme_path = src.join("README.md"); + let (book_front, book_description) = if book_readme_path.exists() { + let contents = fs::read_to_string(&book_readme_path)?; + let (front, contents) = super::extract_frontmatter(&contents)?; + (front, contents) + } + else { + let content = String::new(); + (None, content) + }; + let book_front = FrontMatter::from_root(book_front.unwrap_or_default()); + let FormatResponse { output, include_katex_css } = format_markdown(&book_description)?; + let book_description = output; + + // load all our chapters + let mut chapters: Vec = Vec::default(); + for entry in src.read_dir()? { + let entry = entry?; + let path = entry.path(); + if entry.file_type()?.is_dir() { + // try to find a `README.md` file and parse it to get the chapter's title, fall back to the directory + // name if we can't do that + let chapter_name = path.file_name().map(std::ffi::OsStr::to_str).flatten().unwrap_or_default(); + let index_path = path.join("README.md"); + let (front, contents) = if index_path.exists() { + let contents = fs::read_to_string(&index_path)?; + let (front, contents) = super::extract_frontmatter(&contents)?; + let front = front.unwrap_or_default().into_front(&book_front, chapter_name, &format!("{}/index.html", chapter_name)); + (front, contents) + } + else { + (ParsedFrontMatter::default().into_front(&book_front, chapter_name, &format!("{}/index.html", chapter_name)), String::new()) + }; + + let mut chapter: Chapter = Chapter { + front, + sections: Vec::default(), + source: path.clone(), + contents, + }; + + for entry in path.read_dir()? { + let entry = entry?; + let path = entry.path(); + if let Some("md") = path.extension().map(std::ffi::OsStr::to_str).flatten() { + let name = path.file_stem().map(std::ffi::OsStr::to_str).flatten(); + if name.is_none() { continue; } + let name = name.unwrap(); + if name == "README" { + continue; + } + + let contents = fs::read_to_string(&path)?; + let (front, contents) = super::extract_frontmatter(&contents)?; + let front = front.unwrap_or_default().into_front(&book_front, name, &format!("{}/{}.html", chapter_name, name)); + chapter.sections.push(Chapter { + front, + sections: Vec::new(), + source: path, + contents, + }); + } + } + + chapters.push(chapter); + } + else if let Some("md") = path.extension().map(std::ffi::OsStr::to_str).flatten() { + let name = path.file_stem().map(std::ffi::OsStr::to_str).flatten(); + if name.is_none() { continue; } + let name = name.unwrap(); + if name == "README" { + continue; + } + + let contents = fs::read_to_string(&path)?; + let (front, contents) = super::extract_frontmatter(&contents)?; + let front = front.unwrap_or_default().into_front(&book_front, name, &format!("{}/index.html", name)); + chapters.push(Chapter { + front, + sections: Vec::new(), + source: path, + contents, + }); + } + } + + // sort all the chapters + chapters.sort_by(|a, b| a.front.url.cmp(&b.front.url)); + for chapter in chapters.iter_mut() { + chapter.sections.sort_by(|a, b| a.front.url.cmp(&b.front.url)); + } + + // generate our index + let index_out_path = dest.join("index.html"); + let index_out = fs::File::create(&index_out_path)?; + let index_out = io::BufWriter::new(index_out); + generate_index(&book_front, book_description, include_katex_css, &chapters, index_out, include_reload_script)?; + log::info!("Rendered index into `{}`", index_out_path.display()); + + // compile markdown and write the actual pages + let mut prev_chapter = None; + for (chapter_index, chapter) in chapters.iter().enumerate() { + // render the index + let chapter_root = dest.join(chapter.source.file_stem().map(std::ffi::OsStr::to_str).flatten().unwrap()); + let out = chapter_root.join("index.html"); + log::info!("Rendering `{}` into `{}`...", chapter.source.display(), out.display()); + fs::create_dir_all(&chapter_root)?; + + let outfile = fs::File::create(&out)?; + let outfile = io::BufWriter::new(outfile); + + let FormatResponse { output, include_katex_css } = format_markdown(&chapter.contents)?; + + let next_chapter = + if chapter.sections.len() > 0 { + Some(chapter.sections.iter().nth(0).expect("section 0 exists")) + } + else if chapter_index < chapters.len() - 1 { + Some(chapters.iter().nth(chapter_index + 1).expect("chapter n+1 exists")) + } + else { + None + }; + + format_page(&book_front, &chapter, &chapters, prev_chapter, next_chapter, &output, include_katex_css, outfile, include_reload_script)?; + prev_chapter = Some(chapter); + + // now the sections + for (section_index, section) in chapter.sections.iter().enumerate() { + let name = section.source.file_stem().map(std::ffi::OsStr::to_str).flatten().unwrap(); + let out = chapter_root.join(&format!("{}.html", name)); + log::info!("Rendering `{}` into `{}`...", section.source.display(), out.display()); + + let outfile = fs::File::create(&out)?; + let outfile = io::BufWriter::new(outfile); + + let FormatResponse { output, include_katex_css } = format_markdown(§ion.contents)?; + + let next_chapter = if section_index < chapter.sections.len() - 1 { + Some(chapter.sections.iter().nth(section_index + 1).expect("chapter n+1 exists")) + } + else if chapter_index < chapters.len() - 1 { + Some(chapters.iter().nth(chapter_index + 1).expect("chapter n+1 exists")) + } + else { + None + }; + + format_page(&book_front, §ion, &chapters, prev_chapter, next_chapter, &output, include_katex_css, outfile, include_reload_script)?; + prev_chapter = Some(section); + + } + } + + // copy the assets + for entry in ignore::Walk::new(&src) { + let entry = entry?; + if let Some(t) = entry.file_type() { + if t.is_file() { + if let Some("md") = entry.path().extension().map(std::ffi::OsStr::to_str).flatten() { + // ignore markdown files + } + else { + // we found an asset to copy! + let dest_path: PathBuf = dest.join(entry.path().iter().skip(1).map(PathBuf::from).collect::()); + if let Some(parent) = dest_path.parent() { + if !parent.exists() { + fs::create_dir_all(parent)?; + log::info!("created directory `{}`...", parent.display()); + } + } + fs::copy(entry.path(), &dest_path)?; + log::info!("Copied `{}` to `{}`...", entry.path().display(), dest_path.display()); + } + } + } + } + + // save the built-in assets + fs::write(dest.join("style.css"), STYLESHEET)?; + log::info!("Wrote {}", dest.join("style.css").display()); + fs::write(dest.join("favicon.ico"), ASSET_FAVICON)?; + log::info!("Wrote {}", dest.join("favicon.ico").display()); + fs::write(dest.join("icons.svg"), ASSET_ICONS)?; + log::info!("Wrote {}", dest.join("icons.svg").display()); + + log::info!("Done!"); + Ok(()) +} \ No newline at end of file diff --git a/src/filters.rs b/src/html/filters.rs similarity index 100% rename from src/filters.rs rename to src/html/filters.rs diff --git a/src/latex.rs b/src/latex.rs new file mode 100644 index 0000000..7b99e8d --- /dev/null +++ b/src/latex.rs @@ -0,0 +1,46 @@ +use std::path::{Path, PathBuf}; +use askama::Template; +use std::fs; + +mod filters; + +use super::models::frontmatter::FrontMatter; + +#[derive(Template)] +#[template(path = "book.tex", escape = "none")] +struct BookTemplate<'a> { + book: &'a FrontMatter, +} + +pub fn build, POut: AsRef>(src: PIn, dest: POut) -> Result<(), Box> { + let src = PathBuf::from(src.as_ref()); + let dest = PathBuf::from(dest.as_ref()); + if let Some(parent) = dest.parent() { + if !parent.exists() { + fs::create_dir_all(&parent)?; + log::info!("created directory `{}`...", parent.display()); + } + } + + // load our book + let book_readme_path = src.join("README.md"); + let (book_front, book_description) = if book_readme_path.exists() { + let contents = fs::read_to_string(&book_readme_path)?; + let (front, contents) = super::extract_frontmatter(&contents)?; + (front, contents) + } + else { + let content = String::new(); + (None, content) + }; + let book_front = FrontMatter::from_root(book_front.unwrap_or_default()); + + let book: BookTemplate = BookTemplate { + book: &book_front, + }; + + let rendered = book.render()?; + std::fs::write(dest, rendered)?; + + Ok(()) +} \ No newline at end of file diff --git a/src/latex/filters.rs b/src/latex/filters.rs new file mode 100644 index 0000000..0293805 --- /dev/null +++ b/src/latex/filters.rs @@ -0,0 +1,5 @@ +use chrono::prelude::*; + +pub fn human_date(d: &DateTime) -> askama::Result { + Ok(d.format("%b %e, %Y").to_string()) +} diff --git a/src/main.rs b/src/main.rs index e7f25e3..658032e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,205 +1,20 @@ #[macro_use] extern crate lazy_static; -use std::path::{PathBuf, Path}; -use std::{fs, io}; -use comrak::ComrakOptions; -use syntect::{parsing::SyntaxSet, highlighting::{ThemeSet, Theme}}; -use askama::Template; +use std::path::{PathBuf}; +use std::{fs}; -pub const STYLESHEET: &'static str = include_str!(concat!(env!("OUT_DIR"), "/style.css")); -pub const ASSET_FAVICON: &'static [u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/favicon.ico")); -pub const ASSET_ICONS: &'static [u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/icons.svg")); pub const ASSET_DEFAULT_README: &'static [u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/README.default.md")); pub const ASSET_DEFAULT_INTRODUCTION: &'static [u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/01-introduction.default.md")); -pub const SYNTAX_TOML: &'static str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/syntaxes/TOML.sublime-syntax")); -pub const SYNTAX_HAXE: &'static str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/syntaxes/haxe.sublime-syntax")); -pub const SYNTAX_HXML: &'static str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/syntaxes/hxml.sublime-syntax")); -pub const SYNTAX_SASS: &'static str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/syntaxes/Sass.sublime-syntax")); -pub const SYNTAX_SCSS: &'static str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/syntaxes/SCSS.sublime-syntax")); - -lazy_static! { - static ref HIGHLIGHT_SYNTAX_SETS: SyntaxSet = { - use syntect::parsing::SyntaxDefinition; - - let ss = SyntaxSet::load_defaults_newlines(); - let mut ssb = ss.into_builder(); - ssb.add(SyntaxDefinition::load_from_str(SYNTAX_TOML, true, None).expect("valid TOML syntax definition")); - ssb.add(SyntaxDefinition::load_from_str(SYNTAX_HAXE, true, None).expect("valid haxe syntax definition")); - ssb.add(SyntaxDefinition::load_from_str(SYNTAX_HXML, true, None).expect("valid hxml syntax definition")); - ssb.add(SyntaxDefinition::load_from_str(SYNTAX_SASS, true, None).expect("valid sass syntax definition")); - ssb.add(SyntaxDefinition::load_from_str(SYNTAX_SCSS, true, None).expect("valid scss syntax definition")); - let ss = ssb.build(); - - //if cfg!(debug_assertions) { - // let mut syntaxes: Vec<(String, String)> = ss.syntaxes().iter() - // .map(|s| (s.name.clone(), s.file_extensions.iter().map(|s| &**s).collect::>().join("`, `"))) - // .collect(); - // syntaxes.sort_by(|a, b| a.0.cmp(&b.0)); - // for syntax in syntaxes { - // println!("{}\n\n: `{}`\n\n", syntax.0, syntax.1); - // } - //} - - ss - }; - static ref HIGHLIGHT_THEME_SETS: ThemeSet = ThemeSet::load_defaults(); - static ref HIGHLIGHT_THEME: &'static Theme = &HIGHLIGHT_THEME_SETS.themes["base16-eighties.dark"]; - static ref COMRAK_OPTIONS: ComrakOptions = ComrakOptions { - hardbreaks: false, - smart: true, - github_pre_lang: false, - default_info_string: None, - unsafe_: true, - ext_strikethrough: true, - ext_tagfilter: false, - ext_table: true, - ext_autolink: true, - ext_tasklist: true, - ext_superscript: true, - ext_header_ids: Some("header".to_owned()), - ext_footnotes: true, - ext_description_lists: true, - ..ComrakOptions::default() - }; -} mod cli; mod models; -mod filters; - -use models::frontmatter::{ParsedFrontMatter, FrontMatter}; -use models::chapter::{Chapter}; - -fn create_katex_inline(src: &str) -> Result> { - use std::process::{Command, Stdio}; - use io::Write; - - let mut child = match Command::new("katex") - .arg("-d") - .arg("-t") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() { - Ok(c) => c, - Err(e) => { - log::warn!("failed to launch katex, not rendering math block: {:?}", e); - return Ok(format_code("", src)?.output); - } - }; - - let stdin = child.stdin.as_mut().expect("valid katex stdin"); - stdin.write_all(src.as_ref())?; - - let output = child.wait_with_output()?; - if !output.status.success() { - log::error!("failed to generate katex, exit code: {:?}", output.status.code()); - log::error!("katex STDOUT:"); - log::error!("{}", String::from_utf8_lossy(output.stdout.as_ref())); - log::error!("katex STDERR:"); - log::error!("{}", String::from_utf8_lossy(output.stdout.as_ref())); - log::error!("/katex output"); - return Ok(format_code("", src)?.output); - } - let rendered: String = String::from_utf8(output.stdout)?; - - Ok(format!(r#"
{}
"#, rendered)) -} - -fn create_plantuml_svg(src: &str) -> Result> { - use std::process::{Command, Stdio}; - use io::Write; - - let mut child = match Command::new("plantuml") - .arg("-tsvg") - .arg("-nometadata") - .arg("-pipe") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() { - Ok(c) => c, - Err(e) => { - log::warn!("failed to launch plantuml, not rendering plantuml block: {:?}", e); - return Ok(format_code("", src)?.output); - } - }; +mod html; +mod latex; +mod extensions; - let stdin = child.stdin.as_mut().expect("valid plantuml stdin"); - stdin.write_all(src.as_ref())?; - - let output = child.wait_with_output()?; - if !output.status.success() { - log::error!("failed to generate plantuml, exit code: {:?}", output.status.code()); - log::error!("plantuml STDOUT:"); - log::error!("{}", String::from_utf8_lossy(output.stdout.as_ref())); - log::error!("plantuml STDERR:"); - log::error!("{}", String::from_utf8_lossy(output.stdout.as_ref())); - log::error!("/plantuml output"); - return Ok(format_code("", src)?.output); - } - let svg: String = String::from_utf8(output.stdout)?; - let svg = svg.replace(r#""#, ""); - - Ok(format!("
{}
", svg)) -} - -struct FormatResponse { - output: String, - include_katex_css: bool, -} - -fn format_code(lang: &str, src: &str) -> Result> { - use syntect::parsing::SyntaxReference; - use syntect::html::highlighted_html_for_string; - - // render plantuml code blocks into an inline svg - if lang == "plantuml" { - return Ok(FormatResponse { - output: create_plantuml_svg(src)?, - include_katex_css: false, - }); - } - // render katex code blocks into an inline math - if lang == "katex" { - return Ok(FormatResponse { - output: create_katex_inline(src)?, - include_katex_css: true, - }); - } - - let syntax: Option<&SyntaxReference> = if lang.len() > 0 { - let syntax = HIGHLIGHT_SYNTAX_SETS.find_syntax_by_token(lang); - if syntax.is_none() { - eprintln!("warning: language `{}` not recognized, formatting code block as plain text!", lang); - } - syntax - } - else { - None - }; - let syntax = syntax.unwrap_or(HIGHLIGHT_SYNTAX_SETS.find_syntax_plain_text()); - - let html = highlighted_html_for_string(src, &HIGHLIGHT_SYNTAX_SETS, &syntax, &HIGHLIGHT_THEME); - - Ok(FormatResponse { - output: html, - include_katex_css: false, - }) -} - -fn wrap_image_in_figure(link: &comrak::nodes::NodeLink, alt: &str) -> Result> { - let title = String::from_utf8_lossy(link.title.as_ref()); - let url = String::from_utf8_lossy(link.url.as_ref()); - if title.len() > 0 { - Ok(format!(r#"
{}
{}
"#, url, alt, title, title)) - } - else { - Ok(format!(r#"
{}
"#, url, alt)) - } -} +use models::frontmatter::{ParsedFrontMatter}; fn extract_frontmatter(src: &str) -> Result<(Option, String), Box> { if src.starts_with("---\n") { @@ -233,346 +48,6 @@ fn extract_frontmatter(src: &str) -> Result<(Option, String), } } -fn format_markdown(src: &str) -> Result> { - use comrak::{Arena, parse_document, format_html}; - use comrak::nodes::{AstNode, NodeValue}; - - let arena = Arena::new(); - - let root = parse_document( - &arena, - src, - &COMRAK_OPTIONS); - - fn iter_nodes<'a, F>(node: &'a AstNode<'a>, f: &mut F) -> Result<(), Box> - where F : FnMut(&'a AstNode<'a>) -> Result<(), Box> { - f(node)?; - for c in node.children() { - iter_nodes(c, f)?; - } - Ok(()) - } - - let mut use_katex_css = false; - iter_nodes(root, &mut |node| { - let value = &mut node.data.borrow_mut().value; - match value { - NodeValue::CodeBlock(ref block) => { - let lang = String::from_utf8_lossy(block.info.as_ref()); - let source = String::from_utf8_lossy(block.literal.as_ref()); - let FormatResponse { output, include_katex_css } = format_code(&lang, &source)?; - if include_katex_css { - use_katex_css = true; - } - let highlighted: Vec = Vec::from(output.into_bytes()); - *value = NodeValue::HtmlInline(highlighted); - }, - NodeValue::Paragraph => { - if node.children().count() == 1 { - let first_child = &node.first_child().unwrap(); - let first_value = &first_child.data.borrow().value; - if let NodeValue::Image(link) = first_value { - if first_child.children().count() > 0 { - let mut alt: String = String::default(); - for child in first_child.children() { - if let NodeValue::Text(t) = &child.data.borrow().value { - alt.push_str(&String::from_utf8_lossy(&t)); - } - child.detach(); - } - first_child.detach(); - let figure = wrap_image_in_figure(&link, &alt)?; - let figure: Vec = Vec::from(figure.into_bytes()); - *value = NodeValue::HtmlInline(figure); - } - } - } - }, - _ => {} - } - Ok(()) - })?; - - let mut output: Vec = Vec::with_capacity((src.len() as f64 * 1.2) as usize); - format_html(root, &COMRAK_OPTIONS, &mut output).expect("can format HTML"); - let output = String::from_utf8(output).expect("valid utf-8 generated HTML"); - Ok(FormatResponse { - output, - include_katex_css: use_katex_css, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn can_generate_figures() { - let src = r#"![bear](https://placebear.com/g/512/256 "A majestic bear")"#; - let result = format_markdown(src).expect("can format"); - assert_eq!(result.output, r#"
bear
A majestic bear
"#); - } -} - -#[derive(Template)] -#[template(path = "index.html")] -struct IndexTemplate<'a, 'b, 'c> { - book: &'a FrontMatter, - chapters: &'b Vec, - book_description: &'c str, - include_katex_css: bool, - include_reload_script: bool, -} - -fn generate_index(book: &FrontMatter, content: String, include_katex_css: bool, chapters: &Vec, mut output: W, include_reload_script: bool) -> Result<(), Box> { - // fill out our template - let template = IndexTemplate { - book, - chapters, - book_description: &content, - include_katex_css, - include_reload_script, - }; - - // and render! - let s = template.render()?; - output.write_all(s.as_bytes())?; - - Ok(()) -} - -#[derive(Template)] -#[template(path = "page.html")] -struct PageTemplate<'a, 'b, 'c, 'd, 'e, 'g> { - chapter: &'a Chapter, - content: &'b str, - chapters: &'c Vec, - prev_chapter: Option<&'d Chapter>, - next_chapter: Option<&'e Chapter>, - book: &'g FrontMatter, - include_katex_css: bool, - include_reload_script: bool, -} - -fn format_page(book: &FrontMatter, chapter: &Chapter, chapters: &Vec, prev_chapter: Option<&Chapter>, next_chapter: Option<&Chapter>, content: &str, include_katex_css: bool, mut output: W, include_reload_script: bool) -> Result<(), Box> { - // fill out our template - let template = PageTemplate { - chapter, - content, - chapters, - prev_chapter, - next_chapter, - book, - include_katex_css, - include_reload_script, - }; - - // and render! - let s = template.render()?; - output.write_all(s.as_bytes())?; - - Ok(()) -} - -fn build, POut: AsRef>(src: PIn, dest: POut, include_reload_script: bool) -> Result<(), Box> { - let src = PathBuf::from(src.as_ref()); - let dest = PathBuf::from(dest.as_ref()); - if !dest.exists() { - std::fs::create_dir_all(&dest)?; - log::info!("created directory `{}`...", dest.display()); - } - - // load our book - let book_readme_path = src.join("README.md"); - let (book_front, book_description) = if book_readme_path.exists() { - let contents = fs::read_to_string(&book_readme_path)?; - let (front, contents) = extract_frontmatter(&contents)?; - (front, contents) - } - else { - let content = String::new(); - (None, content) - }; - let book_front = FrontMatter::from_root(book_front.unwrap_or_default()); - let FormatResponse { output, include_katex_css } = format_markdown(&book_description)?; - let book_description = output; - - // load all our chapters - let mut chapters: Vec = Vec::default(); - for entry in src.read_dir()? { - let entry = entry?; - let path = entry.path(); - if entry.file_type()?.is_dir() { - // try to find a `README.md` file and parse it to get the chapter's title, fall back to the directory - // name if we can't do that - let chapter_name = path.file_name().map(std::ffi::OsStr::to_str).flatten().unwrap_or_default(); - let index_path = path.join("README.md"); - let (front, contents) = if index_path.exists() { - let contents = fs::read_to_string(&index_path)?; - let (front, contents) = extract_frontmatter(&contents)?; - let front = front.unwrap_or_default().into_front(&book_front, chapter_name, &format!("{}/index.html", chapter_name)); - (front, contents) - } - else { - (ParsedFrontMatter::default().into_front(&book_front, chapter_name, &format!("{}/index.html", chapter_name)), String::new()) - }; - - let mut chapter: Chapter = Chapter { - front, - sections: Vec::default(), - source: path.clone(), - contents, - }; - - for entry in path.read_dir()? { - let entry = entry?; - let path = entry.path(); - if let Some("md") = path.extension().map(std::ffi::OsStr::to_str).flatten() { - let name = path.file_stem().map(std::ffi::OsStr::to_str).flatten(); - if name.is_none() { continue; } - let name = name.unwrap(); - if name == "README" { - continue; - } - - let contents = fs::read_to_string(&path)?; - let (front, contents) = extract_frontmatter(&contents)?; - let front = front.unwrap_or_default().into_front(&book_front, name, &format!("{}/{}.html", chapter_name, name)); - chapter.sections.push(Chapter { - front, - sections: Vec::new(), - source: path, - contents, - }); - } - } - - chapters.push(chapter); - } - else if let Some("md") = path.extension().map(std::ffi::OsStr::to_str).flatten() { - let name = path.file_stem().map(std::ffi::OsStr::to_str).flatten(); - if name.is_none() { continue; } - let name = name.unwrap(); - if name == "README" { - continue; - } - - let contents = fs::read_to_string(&path)?; - let (front, contents) = extract_frontmatter(&contents)?; - let front = front.unwrap_or_default().into_front(&book_front, name, &format!("{}/index.html", name)); - chapters.push(Chapter { - front, - sections: Vec::new(), - source: path, - contents, - }); - } - } - - // sort all the chapters - chapters.sort_by(|a, b| a.front.url.cmp(&b.front.url)); - for chapter in chapters.iter_mut() { - chapter.sections.sort_by(|a, b| a.front.url.cmp(&b.front.url)); - } - - // generate our index - let index_out_path = dest.join("index.html"); - let index_out = fs::File::create(&index_out_path)?; - let index_out = io::BufWriter::new(index_out); - generate_index(&book_front, book_description, include_katex_css, &chapters, index_out, include_reload_script)?; - log::info!("Rendered index into `{}`", index_out_path.display()); - - // compile markdown and write the actual pages - let mut prev_chapter = None; - for (chapter_index, chapter) in chapters.iter().enumerate() { - // render the index - let chapter_root = dest.join(chapter.source.file_stem().map(std::ffi::OsStr::to_str).flatten().unwrap()); - let out = chapter_root.join("index.html"); - log::info!("Rendering `{}` into `{}`...", chapter.source.display(), out.display()); - fs::create_dir_all(&chapter_root)?; - - let outfile = fs::File::create(&out)?; - let outfile = io::BufWriter::new(outfile); - - let FormatResponse { output, include_katex_css } = format_markdown(&chapter.contents)?; - - let next_chapter = - if chapter.sections.len() > 0 { - Some(chapter.sections.iter().nth(0).expect("section 0 exists")) - } - else if chapter_index < chapters.len() - 1 { - Some(chapters.iter().nth(chapter_index + 1).expect("chapter n+1 exists")) - } - else { - None - }; - - format_page(&book_front, &chapter, &chapters, prev_chapter, next_chapter, &output, include_katex_css, outfile, include_reload_script)?; - prev_chapter = Some(chapter); - - // now the sections - for (section_index, section) in chapter.sections.iter().enumerate() { - let name = section.source.file_stem().map(std::ffi::OsStr::to_str).flatten().unwrap(); - let out = chapter_root.join(&format!("{}.html", name)); - log::info!("Rendering `{}` into `{}`...", section.source.display(), out.display()); - - let outfile = fs::File::create(&out)?; - let outfile = io::BufWriter::new(outfile); - - let FormatResponse { output, include_katex_css } = format_markdown(§ion.contents)?; - - let next_chapter = if section_index < chapter.sections.len() - 1 { - Some(chapter.sections.iter().nth(section_index + 1).expect("chapter n+1 exists")) - } - else if chapter_index < chapters.len() - 1 { - Some(chapters.iter().nth(chapter_index + 1).expect("chapter n+1 exists")) - } - else { - None - }; - - format_page(&book_front, §ion, &chapters, prev_chapter, next_chapter, &output, include_katex_css, outfile, include_reload_script)?; - prev_chapter = Some(section); - - } - } - - // copy the assets - for entry in ignore::Walk::new(&src) { - let entry = entry?; - if let Some(t) = entry.file_type() { - if t.is_file() { - if let Some("md") = entry.path().extension().map(std::ffi::OsStr::to_str).flatten() { - // ignore markdown files - } - else { - // we found an asset to copy! - let dest_path: PathBuf = dest.join(entry.path().iter().skip(1).map(PathBuf::from).collect::()); - if let Some(parent) = dest_path.parent() { - if !parent.exists() { - fs::create_dir_all(parent)?; - log::info!("created directory `{}`...", parent.display()); - } - } - fs::copy(entry.path(), &dest_path)?; - log::info!("Copied `{}` to `{}`...", entry.path().display(), dest_path.display()); - } - } - } - } - - // save the built-in assets - fs::write(dest.join("style.css"), STYLESHEET)?; - log::info!("Wrote {}", dest.join("style.css").display()); - fs::write(dest.join("favicon.ico"), ASSET_FAVICON)?; - log::info!("Wrote {}", dest.join("favicon.ico").display()); - fs::write(dest.join("icons.svg"), ASSET_ICONS)?; - log::info!("Wrote {}", dest.join("icons.svg").display()); - - log::info!("Done!"); - Ok(()) -} - struct ReloadClient { sender: std::sync::Arc, reload: std::sync::Arc, @@ -680,7 +155,15 @@ fn main() -> Result<(), Box> { else if let Some(submatches) = matches.subcommand_matches("build") { let src = submatches.value_of("in").expect("in value"); let dest = submatches.value_of("out").expect("out value"); - build(src, dest, false) + + if submatches.is_present("latex") { + let latex_file = submatches.value_of("latex").unwrap(); + let latex_file = PathBuf::from(latex_file); + latex::build(src, latex_file) + } + else { + html::build(src, dest, false) + } } else if let Some(submatches) = matches.subcommand_matches("watch") { let reload_trigger = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); @@ -698,7 +181,7 @@ fn main() -> Result<(), Box> { let src = submatches.value_of("in").expect("in value"); let dest = submatches.value_of("out").expect("out value"); - build(src, dest, do_reload)?; + html::build(src, dest, do_reload)?; let (tx, rx) = std::sync::mpsc::channel(); let mut watcher: RecommendedWatcher = Watcher::new(tx, std::time::Duration::from_secs(1))?; @@ -708,7 +191,7 @@ fn main() -> Result<(), Box> { match rx.recv() { Ok(notify::DebouncedEvent::NoticeWrite(_)) | Ok(notify::DebouncedEvent::NoticeRemove(_)) => {}, Ok(_) => { - build(src, dest, do_reload)?; + html::build(src, dest, do_reload)?; reload_trigger.store(true, std::sync::atomic::Ordering::SeqCst); std::thread::sleep(std::time::Duration::from_millis(150)); reload_trigger.store(false, std::sync::atomic::Ordering::SeqCst); diff --git a/templates/.gitignore b/templates/.gitignore new file mode 100644 index 0000000..2e6c2a0 --- /dev/null +++ b/templates/.gitignore @@ -0,0 +1,276 @@ +*.pdf + +# Created by https://www.gitignore.io/api/latex +# Edit at https://www.gitignore.io/?templates=latex + +### LaTeX ### +## Core latex/pdflatex auxiliary files: +*.aux +*.lof +*.log +*.lot +*.fls +*.out +*.toc +*.fmt +*.fot +*.cb +*.cb2 +.*.lb + +## Intermediate documents: +*.dvi +*.xdv +*-converted-to.* +# these rules might exclude image files for figures etc. +# *.ps +# *.eps +# *.pdf + +## Generated if empty string is given at "Please type another file name for output:" +.pdf + +## Bibliography auxiliary files (bibtex/biblatex/biber): +*.bbl +*.bcf +*.blg +*-blx.aux +*-blx.bib +*.run.xml + +## Build tool auxiliary files: +*.fdb_latexmk +*.synctex +*.synctex(busy) +*.synctex.gz +*.synctex.gz(busy) +*.pdfsync + +## Build tool directories for auxiliary files +# latexrun +latex.out/ + +## Auxiliary and intermediate files from other packages: +# algorithms +*.alg +*.loa + +# achemso +acs-*.bib + +# amsthm +*.thm + +# beamer +*.nav +*.pre +*.snm +*.vrb + +# changes +*.soc + +# comment +*.cut + +# cprotect +*.cpt + +# elsarticle (documentclass of Elsevier journals) +*.spl + +# endnotes +*.ent + +# fixme +*.lox + +# feynmf/feynmp +*.mf +*.mp +*.t[1-9] +*.t[1-9][0-9] +*.tfm + +#(r)(e)ledmac/(r)(e)ledpar +*.end +*.?end +*.[1-9] +*.[1-9][0-9] +*.[1-9][0-9][0-9] +*.[1-9]R +*.[1-9][0-9]R +*.[1-9][0-9][0-9]R +*.eledsec[1-9] +*.eledsec[1-9]R +*.eledsec[1-9][0-9] +*.eledsec[1-9][0-9]R +*.eledsec[1-9][0-9][0-9] +*.eledsec[1-9][0-9][0-9]R + +# glossaries +*.acn +*.acr +*.glg +*.glo +*.gls +*.glsdefs + +# uncomment this for glossaries-extra (will ignore makeindex's style files!) +# *.ist + +# gnuplottex +*-gnuplottex-* + +# gregoriotex +*.gaux +*.gtex + +# htlatex +*.4ct +*.4tc +*.idv +*.lg +*.trc +*.xref + +# hyperref +*.brf + +# knitr +*-concordance.tex +# TODO Comment the next line if you want to keep your tikz graphics files +*.tikz +*-tikzDictionary + +# listings +*.lol + +# luatexja-ruby +*.ltjruby + +# makeidx +*.idx +*.ilg +*.ind + +# minitoc +*.maf +*.mlf +*.mlt +*.mtc[0-9]* +*.slf[0-9]* +*.slt[0-9]* +*.stc[0-9]* + +# minted +_minted* +*.pyg + +# morewrites +*.mw + +# nomencl +*.nlg +*.nlo +*.nls + +# pax +*.pax + +# pdfpcnotes +*.pdfpc + +# sagetex +*.sagetex.sage +*.sagetex.py +*.sagetex.scmd + +# scrwfile +*.wrt + +# sympy +*.sout +*.sympy +sympy-plots-for-*.tex/ + +# pdfcomment +*.upa +*.upb + +# pythontex +*.pytxcode +pythontex-files-*/ + +# tcolorbox +*.listing + +# thmtools +*.loe + +# TikZ & PGF +*.dpth +*.md5 +*.auxlock + +# todonotes +*.tdo + +# vhistory +*.hst +*.ver + +# easy-todo +*.lod + +# xcolor +*.xcp + +# xmpincl +*.xmpi + +# xindy +*.xdy + +# xypic precompiled matrices +*.xyc + +# endfloat +*.ttt +*.fff + +# Latexian +TSWLatexianTemp* + +## Editors: +# WinEdt +*.bak +*.sav + +# Texpad +.texpadtmp + +# LyX +*.lyx~ + +# Kile +*.backup + +# KBibTeX +*~[0-9]* + +# auto folder when using emacs and auctex +./auto/* +*.el + +# expex forward references with \gathertags +*-tags.tex + +# standalone packages +*.sta + +### LaTeX Patch ### +# glossaries +*.glstex + +# End of https://www.gitignore.io/api/latex diff --git a/templates/book.tex b/templates/book.tex new file mode 100644 index 0000000..9f0659f --- /dev/null +++ b/templates/book.tex @@ -0,0 +1,14 @@ +\documentclass{book} +\usepackage{color} + +\title{ {{ book.title }} } +\author{ {{ book.author }} } +\date{ {{ book.pubdate|human_date }} } + +\begin{document} +\maketitle + +\chapter{First chapter} +Herp derp + +\end{document}