moved book parsing back to root lib, started fleshing out latex generation

master
Kenton Hamaluik 5 years ago
parent 015550f275
commit c8684bacdb

@ -4,7 +4,7 @@ use comrak::ComrakOptions;
use syntect::{parsing::SyntaxSet, highlighting::{ThemeSet, Theme}};
use askama::Template;
use super::models::frontmatter::{ParsedFrontMatter, FrontMatter};
use super::models::frontmatter::{FrontMatter};
use super::models::chapter::{Chapter};
use super::extensions::{create_plantuml_svg, create_katex_inline};
@ -270,109 +270,20 @@ pub fn build<PIn: AsRef<Path>, POut: AsRef<Path>>(src: PIn, dest: POut, include_
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 = super::load_book(&src)?;
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)?;
generate_index(&book.front, book_description, include_katex_css, &book.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() {
for (chapter_index, chapter) in book.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");
@ -388,14 +299,14 @@ pub fn build<PIn: AsRef<Path>, POut: AsRef<Path>>(src: PIn, dest: POut, include_
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 if chapter_index < book.chapters.len() - 1 {
Some(book.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)?;
format_page(&book.front, &chapter, &book.chapters, prev_chapter, next_chapter, &output, include_katex_css, outfile, include_reload_script)?;
prev_chapter = Some(chapter);
// now the sections
@ -412,14 +323,14 @@ pub fn build<PIn: AsRef<Path>, POut: AsRef<Path>>(src: PIn, dest: POut, include_
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 if chapter_index < book.chapters.len() - 1 {
Some(book.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)?;
format_page(&book.front, &section, &book.chapters, prev_chapter, next_chapter, &output, include_katex_css, outfile, include_reload_script)?;
prev_chapter = Some(section);
}

@ -1,15 +1,209 @@
use std::path::{Path, PathBuf};
use askama::Template;
use std::fs;
use comrak::ComrakOptions;
mod filters;
use super::models::chapter::Chapter;
use super::models::frontmatter::FrontMatter;
lazy_static! {
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()
};
}
#[derive(Template)]
#[template(path = "book.tex", escape = "none")]
struct BookTemplate<'a> {
book: &'a FrontMatter,
struct BookTemplate<'a, 'b, 'c> {
front: &'a FrontMatter,
description: &'b str,
chapters: Vec<&'c Chapter>,
}
fn format_text<'a>(node: &'a comrak::nodes::AstNode<'a>, output: &mut String) {
use comrak::nodes::NodeValue;
match &node.data.borrow().value {
NodeValue::Text(text) => {
if let Ok(text) = std::str::from_utf8(text) {
output.push_str(text);
}
},
NodeValue::Code(text) => {
if let Ok(text) = std::str::from_utf8(text) {
output.push_str("\\texttt{");
output.push_str(text);
output.push_str("}");
}
},
_ => for child in node.children() { format_text(child, output); },
}
}
fn format_node<'a>(node: &'a comrak::nodes::AstNode<'a>, output: &mut String) {
use comrak::nodes::NodeValue;
match &node.data.borrow().value {
NodeValue::Document => for child in node.children() { format_node(child, output); },
NodeValue::BlockQuote => {
output.push_str("\\begin{quote}\n");
for child in node.children() { format_node(child, output); }
output.push_str("\\end{quote}\n");
},
NodeValue::List(node_list) => {
match node_list.list_type {
comrak::nodes::ListType::Bullet => output.push_str("\\begin{itemize}\n"),
comrak::nodes::ListType::Ordered => output.push_str("\\begin{enumerate}\n"),
}
for child in node.children() { format_node(child, output); }
match node_list.list_type {
comrak::nodes::ListType::Bullet => output.push_str("\\end{itemize}\n"),
comrak::nodes::ListType::Ordered => output.push_str("\\end{enumerate}\n"),
}
},
NodeValue::Item(_) => {
output.push_str("\\item ");
for child in node.children() { format_node(child, output); }
output.push_str("\n");
},
NodeValue::DescriptionList => {
output.push_str("\\begin{description}\n");
for child in node.children() { format_node(child, output); }
output.push_str("\\end{description}\n");
},
NodeValue::DescriptionItem(_) => for child in node.children() { format_node(child, output); },
NodeValue::DescriptionTerm => {
output.push_str("\\item [");
let mut term: String = String::default();
for child in node.children() { format_text(child, &mut term); }
output.push_str(term.trim());
output.push_str("] ");
},
NodeValue::DescriptionDetails => {
for child in node.children() { format_node(child, output); }
output.push_str("\n");
},
NodeValue::CodeBlock(node_code_block) => {
},
NodeValue::HtmlBlock(node_html_block) => {
},
NodeValue::Paragraph => {
for child in node.children() {
format_node(child, output);
}
output.push_str("\n\n");
},
NodeValue::Heading(node_heading) => {
match node_heading.level {
1 => output.push_str("\\section{"),
2 => output.push_str("\\subsection{"),
3 => output.push_str("\\subsubsection{"),
4 => output.push_str("\\paragraph{"),
5 => output.push_str("\\subparagraph{"),
6 => output.push_str("\\textbf{"),
_ => unreachable!()
}
for child in node.children() { format_text(child, output); }
output.push_str("}");
},
NodeValue::ThematicBreak => {
output.push_str("\\hline\n");
},
NodeValue::FootnoteDefinition(text) => {
},
NodeValue::Table(table_alignments) => {
},
NodeValue::TableRow(bool) => {
},
NodeValue::TableCell => {
},
NodeValue::Text(text) => {
if let Ok(text) = std::str::from_utf8(text) {
output.push_str(text);
}
},
NodeValue::TaskItem(bool) => {
},
NodeValue::SoftBreak => {
output.push_str("\n");
},
NodeValue::LineBreak => {
output.push_str("\\newline");
},
NodeValue::Code(text) => {
if let Ok(text) = std::str::from_utf8(text) {
output.push_str("\\verb|");
output.push_str(text);
output.push_str("|");
}
},
NodeValue::HtmlInline(text) => {
},
NodeValue::Emph => {
output.push_str("\\emph{");
for child in node.children() { format_node(child, output); }
output.push_str("}");
},
NodeValue::Strong => {
output.push_str("\\textbf{");
for child in node.children() { format_node(child, output); }
output.push_str("}");
},
NodeValue::Strikethrough => {
output.push_str("\\sout{");
for child in node.children() { format_node(child, output); }
output.push_str("}");
},
NodeValue::Superscript => {
output.push_str("\\textsuperscript{");
for child in node.children() { format_node(child, output); }
output.push_str("}");
},
NodeValue::Link(node_link) => {
},
NodeValue::Image(node_link) => {
},
NodeValue::FootnoteReference(text) => {
},
}
}
fn format_markdown(src: &str) -> Result<String, Box<dyn std::error::Error>> {
let arena = comrak::Arena::new();
let root = comrak::parse_document(
&arena,
src,
&COMRAK_OPTIONS);
let mut latex: String = String::with_capacity(src.len());
format_node(&root, &mut latex);
Ok(latex)
}
pub fn build<PIn: AsRef<Path>, POut: AsRef<Path>>(src: PIn, dest: POut) -> Result<(), Box<dyn std::error::Error>> {
@ -22,25 +216,23 @@ pub fn build<PIn: AsRef<Path>, POut: AsRef<Path>>(src: PIn, dest: POut) -> Resul
}
}
// 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)
// load the book
let mut book = super::load_book(&src)?;
// then convert all the markdown
book.description = format_markdown(&book.description)?;
for chapter in book.chapters.iter_mut() {
chapter.contents = format_markdown(&chapter.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,
// and render to a template
let latexbook = BookTemplate {
front: &book.front,
description: &book.description,
chapters: book.chapters.iter().collect(),
};
let rendered = book.render()?;
std::fs::write(dest, rendered)?;
let rendered = latexbook.render()?;
fs::write(dest, rendered)?;
Ok(())
}

@ -1,7 +1,7 @@
#[macro_use]
extern crate lazy_static;
use std::path::{PathBuf};
use std::path::{Path, PathBuf};
use std::{fs};
pub const ASSET_DEFAULT_README: &'static [u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/README.default.md"));
@ -14,7 +14,9 @@ mod html;
mod latex;
mod extensions;
use models::frontmatter::{ParsedFrontMatter};
use models::book::Book;
use models::chapter::Chapter;
use models::frontmatter::{ParsedFrontMatter, FrontMatter};
fn extract_frontmatter(src: &str) -> Result<(Option<ParsedFrontMatter>, String), Box<dyn std::error::Error>> {
if src.starts_with("---\n") {
@ -48,6 +50,108 @@ fn extract_frontmatter(src: &str) -> Result<(Option<ParsedFrontMatter>, String),
}
}
/// load the entire book at once
/// maybe a bad idea with large books but ¯\_(ツ)_/¯
fn load_book<P: AsRef<Path>>(src: P) -> Result<Book, Box<dyn std::error::Error>> {
// load our book
let src = PathBuf::from(src.as_ref());
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());
// 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));
}
Ok(Book {
front: book_front,
description: book_description,
chapters,
})
}
struct ReloadClient {
sender: std::sync::Arc<ws::Sender>,
reload: std::sync::Arc<std::sync::atomic::AtomicBool>,

@ -1,2 +1,3 @@
pub mod book;
pub mod chapter;
pub mod frontmatter;

@ -0,0 +1,8 @@
use super::frontmatter::FrontMatter;
use super::chapter::Chapter;
pub struct Book {
pub front: FrontMatter,
pub description: String,
pub chapters: Vec<Chapter>,
}

@ -1,14 +1,44 @@
\documentclass{book}
\usepackage{color}
\title{ {{ book.title }} }
\author{ {{ book.author }} }
\date{ {{ book.pubdate|human_date }} }
% booklet-sized pages
\usepackage[papersize={5.5in, 8.5in}]{geometry}
% for strike-outs
\usepackage{ulem}
% use prettier fonts
\usepackage{fontspec}
\defaultfontfeatures{Mapping=tex-text,Scale=MatchLowercase}
\setmainfont{Crimson}
\setmonofont{Fira Mono}
\makeatletter
\renewcommand{\frontmatter}{\cleardoublepage\@mainmatterfalse}
\renewcommand{\mainmatter}{\cleardoublepage\@mainmattertrue}
\makeatother
\title{ {{ front.title }} }
\author{ {{ front.author }} }
\date{ {{ front.pubdate|human_date }} }
\begin{document}
\frontmatter
\maketitle
\chapter{First chapter}
Herp derp
\tableofcontents
\chapter{Preface}
{{ description }}
\mainmatter
{% for chapter in chapters %}
\chapter{ {{ chapter.front.title }} }
{{ chapter.contents }}
{% endfor %}
\end{document}

Loading…
Cancel
Save