#[macro_use] extern crate lazy_static; use std::path::PathBuf; use std::{fs, io}; use comrak::ComrakOptions; use syntect::{parsing::SyntaxSet, highlighting::{ThemeSet, Theme}}; use askama::Template; 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")); lazy_static! { static ref HIGHLIGHT_SYNTAX_SETS: SyntaxSet = SyntaxSet::load_defaults_newlines(); 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::book::{ParsedBook, Book}; fn format_code(lang: &str, src: &str) -> Result> { use syntect::parsing::SyntaxReference; use syntect::html::highlighted_html_for_string; 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(html) } fn extract_frontmatter(src: &str) -> Result<(Option, String), Box> { if src.starts_with("---\n") { let slice = &src[4..]; let end = slice.find("---\n"); if end.is_none() { return Ok((None, src.to_owned())); } let end = end.unwrap(); let front = &slice[..end]; let contents = &slice[end+4..]; let front: ParsedFrontMatter = toml::from_str(front)?; Ok((Some(front), contents.to_owned())) } else if src.starts_with("---\r\n") { let slice = &src[5..]; let end = slice.find("---\r\n"); if end.is_none() { return Ok((None, src.to_owned())); } let end = end.unwrap(); let front = &slice[..end]; let contents = &slice[end+5..]; let front: ParsedFrontMatter = toml::from_str(front)?; Ok((Some(front), contents.to_owned())) } else { Ok((None, src.to_owned())) } } 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: &F) -> Result<(), Box> where F : Fn(&'a AstNode<'a>) -> Result<(), Box> { f(node)?; for c in node.children() { iter_nodes(c, f)?; } Ok(()) } iter_nodes(root, &|node| { let value = &mut node.data.borrow_mut().value; if let NodeValue::CodeBlock(ref block) = value { let lang = String::from_utf8(block.info.clone()).expect("code lang is utf-8"); let source = String::from_utf8(block.literal.clone()).expect("source code is utf-8"); let highlighted: String = format_code(&lang, &source)?; let highlighted: Vec = Vec::from(highlighted.into_bytes()); *value = NodeValue::HtmlBlock(comrak::nodes::NodeHtmlBlock { literal: highlighted, block_type: 0, }); } 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(output) } #[derive(Template)] #[template(path = "index.html")] struct IndexTemplate<'a, 'b> { book: &'a Book, chapters: &'b Vec, } fn generate_index(book: &Book, chapters: &Vec, mut output: W) -> Result<(), Box> { // fill out our template let template = IndexTemplate { book, chapters, }; // 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, 'f, 'g> { title: &'a str, content: &'b str, url: &'f str, chapters: &'c Vec, prev_chapter: Option<&'d models::chapter::Chapter>, next_chapter: Option<&'e models::chapter::Chapter>, book: &'g Book, } fn format_page(book: &Book, frontmatter: FrontMatter, chapters: &Vec, url: &str, content: &str, mut output: W) -> Result<(), Box> { let this_index = chapters.iter().enumerate().find(|(_, chap)| chap.url == url).map(|(i, _)| i).expect("chapter exists"); let prev_chapter = if this_index > 0 { Some(chapters.iter().nth(this_index - 1).expect("chapter n-1 exists")) } else { None }; let next_chapter = if this_index < chapters.len() - 1 { Some(chapters.iter().nth(this_index + 1).expect("chapter n+1 exists")) } else { None }; // fill out our template let template = PageTemplate { title: &frontmatter.title, content, url, chapters, prev_chapter, next_chapter, book, }; // and render! let s = template.render()?; output.write_all(s.as_bytes())?; Ok(()) } fn main() -> Result<(), Box> { let matches = cli::build_cli().get_matches(); if let Some(_submatches) = matches.subcommand_matches("init") { unimplemented!() } 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"); let src = PathBuf::from(src); let dest = PathBuf::from(dest); std::fs::create_dir_all(&dest)?; // load our book let book_toml_path = src.join("mkbook.toml"); let parsed_book: Option = if book_toml_path.exists() { let contents = fs::read_to_string(&book_toml_path)?; let contents: ParsedBook = toml::from_str(&contents)?; Some(contents) } else { None }; let parsed_book = parsed_book.unwrap_or_default(); let book: Book = parsed_book.into(); // load all our chapters let mut chapters: Vec = Vec::default(); for entry in src.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(); let contents = fs::read_to_string(&path)?; let (front, _) = extract_frontmatter(&contents)?; let front = front.unwrap_or_default().into_front(name); chapters.push(models::chapter::Chapter { url: format!("{}.html", name), title: front.title, }); } } chapters.sort_by(|a, b| a.url.cmp(&b.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, &chapters, index_out)?; println!("Rendered index into `{}`", index_out_path.display()); // compile markdown for entry in src.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(); let out = dest.join(format!("{}.html", name)); let outfile = fs::File::create(&out)?; let outfile = io::BufWriter::new(outfile); let contents = fs::read_to_string(&path)?; let (front, contents) = extract_frontmatter(&contents)?; let front = front.unwrap_or_default().into_front(name); let contents = format_markdown(&contents)?; format_page(&book, front, &chapters, &format!("{}.html", name), &contents, outfile)?; println!("Rendered `{}` into `{}`", path.display(), out.display()); } } // save the assets fs::write(dest.join("style.css"), STYLESHEET)?; println!("Wrote {}", dest.join("style.css").display()); fs::write(dest.join("favicon.ico"), ASSET_FAVICON)?; println!("Wrote {}", dest.join("favicon.ico").display()); fs::write(dest.join("icons.svg"), ASSET_ICONS)?; println!("Wrote {}", dest.join("icons.svg").display()); println!("Done!"); Ok(()) } else { cli::build_cli().print_long_help()?; Ok(()) } }