started working on compiling to latex

master
Kenton Hamaluik 5 years ago
parent e1dafa7f6f
commit 0dda753c01

15
Cargo.lock generated

@ -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"

@ -1,6 +1,6 @@
[package]
name = "mkbook"
version = "0.3.0"
version = "0.4.0"
authors = ["Kenton Hamaluik <kenton@hamaluik.ca>"]
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"

@ -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")

@ -0,0 +1,73 @@
pub fn create_katex_inline(src: &str) -> Result<String, Box<dyn std::error::Error>> {
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#"<figure class="math">{}</figure>"#, rendered))
}
pub fn create_plantuml_svg(src: &str) -> Result<String, Box<dyn std::error::Error>> {
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#"<?xml version="1.0" encoding="UTF-8" standalone="no"?>"#, "");
Ok(format!("<figure>{}</figure>", svg))
}

@ -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::<Vec<&str>>().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<FormatResponse, Box<dyn std::error::Error>> {
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<String, Box<dyn std::error::Error>> {
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#"<figure><img src="{}" alt="{}" title="{}"><figcaption>{}</figcaption></figure>"#, url, alt, title, title))
}
else {
Ok(format!(r#"<figure><img src="{}" alt="{}"></figure>"#, url, alt))
}
}
fn format_markdown(src: &str) -> Result<FormatResponse, Box<dyn std::error::Error>> {
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<dyn std::error::Error>>
where F : FnMut(&'a AstNode<'a>) -> Result<(), Box<dyn std::error::Error>> {
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<u8> = 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<u8> = Vec::from(figure.into_bytes());
*value = NodeValue::HtmlInline(figure);
}
}
}
},
_ => {}
}
Ok(())
})?;
let mut output: Vec<u8> = 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#"<figure><img src="https://placebear.com/g/512/256" alt="bear" title="A majestic bear"><figcaption>A majestic bear</figcaption></figure>"#);
}
}
#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate<'a, 'b, 'c> {
book: &'a FrontMatter,
chapters: &'b Vec<Chapter>,
book_description: &'c str,
include_katex_css: bool,
include_reload_script: bool,
}
fn generate_index<W: io::Write>(book: &FrontMatter, content: String, include_katex_css: bool, chapters: &Vec<Chapter>, mut output: W, include_reload_script: bool) -> Result<(), Box<dyn std::error::Error>> {
// 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<Chapter>,
prev_chapter: Option<&'d Chapter>,
next_chapter: Option<&'e Chapter>,
book: &'g FrontMatter,
include_katex_css: bool,
include_reload_script: bool,
}
fn format_page<W: io::Write>(book: &FrontMatter, chapter: &Chapter, chapters: &Vec<Chapter>, prev_chapter: Option<&Chapter>, next_chapter: Option<&Chapter>, content: &str, include_katex_css: bool, mut output: W, include_reload_script: bool) -> Result<(), Box<dyn std::error::Error>> {
// 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<PIn: AsRef<Path>, POut: AsRef<Path>>(src: PIn, dest: POut, include_reload_script: bool) -> Result<(), Box<dyn std::error::Error>> {
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<Chapter> = 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(&section.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, &section, &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::<PathBuf>());
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(())
}

@ -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<PIn: AsRef<Path>, POut: AsRef<Path>>(src: PIn, dest: POut) -> Result<(), Box<dyn std::error::Error>> {
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(())
}

@ -0,0 +1,5 @@
use chrono::prelude::*;
pub fn human_date(d: &DateTime<Utc>) -> askama::Result<String> {
Ok(d.format("%b %e, %Y").to_string())
}

@ -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::<Vec<&str>>().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<String, Box<dyn std::error::Error>> {
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#"<figure class="math">{}</figure>"#, rendered))
}
fn create_plantuml_svg(src: &str) -> Result<String, Box<dyn std::error::Error>> {
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#"<?xml version="1.0" encoding="UTF-8" standalone="no"?>"#, "");
Ok(format!("<figure>{}</figure>", svg))
}
struct FormatResponse {
output: String,
include_katex_css: bool,
}
fn format_code(lang: &str, src: &str) -> Result<FormatResponse, Box<dyn std::error::Error>> {
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<String, Box<dyn std::error::Error>> {
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#"<figure><img src="{}" alt="{}" title="{}"><figcaption>{}</figcaption></figure>"#, url, alt, title, title))
}
else {
Ok(format!(r#"<figure><img src="{}" alt="{}"></figure>"#, url, alt))
}
}
use models::frontmatter::{ParsedFrontMatter};
fn extract_frontmatter(src: &str) -> Result<(Option<ParsedFrontMatter>, String), Box<dyn std::error::Error>> {
if src.starts_with("---\n") {
@ -233,346 +48,6 @@ fn extract_frontmatter(src: &str) -> Result<(Option<ParsedFrontMatter>, String),
}
}
fn format_markdown(src: &str) -> Result<FormatResponse, Box<dyn std::error::Error>> {
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<dyn std::error::Error>>
where F : FnMut(&'a AstNode<'a>) -> Result<(), Box<dyn std::error::Error>> {
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<u8> = 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<u8> = Vec::from(figure.into_bytes());
*value = NodeValue::HtmlInline(figure);
}
}
}
},
_ => {}
}
Ok(())
})?;
let mut output: Vec<u8> = 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#"<figure><img src="https://placebear.com/g/512/256" alt="bear" title="A majestic bear"><figcaption>A majestic bear</figcaption></figure>"#);
}
}
#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate<'a, 'b, 'c> {
book: &'a FrontMatter,
chapters: &'b Vec<Chapter>,
book_description: &'c str,
include_katex_css: bool,
include_reload_script: bool,
}
fn generate_index<W: io::Write>(book: &FrontMatter, content: String, include_katex_css: bool, chapters: &Vec<Chapter>, mut output: W, include_reload_script: bool) -> Result<(), Box<dyn std::error::Error>> {
// 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<Chapter>,
prev_chapter: Option<&'d Chapter>,
next_chapter: Option<&'e Chapter>,
book: &'g FrontMatter,
include_katex_css: bool,
include_reload_script: bool,
}
fn format_page<W: io::Write>(book: &FrontMatter, chapter: &Chapter, chapters: &Vec<Chapter>, prev_chapter: Option<&Chapter>, next_chapter: Option<&Chapter>, content: &str, include_katex_css: bool, mut output: W, include_reload_script: bool) -> Result<(), Box<dyn std::error::Error>> {
// 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<PIn: AsRef<Path>, POut: AsRef<Path>>(src: PIn, dest: POut, include_reload_script: bool) -> Result<(), Box<dyn std::error::Error>> {
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<Chapter> = 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(&section.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, &section, &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::<PathBuf>());
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<ws::Sender>,
reload: std::sync::Arc<std::sync::atomic::AtomicBool>,
@ -680,7 +155,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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);

276
templates/.gitignore vendored

@ -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

@ -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}
Loading…
Cancel
Save