maud/docs/src/main.rs
Chris Wong 327480c442 Build table of contents in a separate step
- Add pretty logging to Makefile
2019-03-24 14:27:04 +13:00

208 lines
6.5 KiB
Rust

#![feature(crate_visibility_modifier)]
#![feature(proc_macro_hygiene)]
use comrak::{self, Arena, ComrakOptions};
use comrak::nodes::{AstNode, NodeCodeBlock, NodeHeading, NodeHtmlBlock, NodeLink, NodeValue};
use serde_json;
use std::error::Error;
use std::env;
use std::fs::{self, File};
use std::io::{self, BufReader};
use std::mem;
use std::path::Path;
use std::string::FromUtf8Error;
use syntect::parsing::SyntaxSet;
use syntect::highlighting::{Color, ThemeSet};
use syntect::html::highlighted_html_for_string;
mod string_writer;
mod views;
fn main() -> Result<(), Box<dyn Error>> {
let args = env::args().collect::<Vec<_>>();
if args.len() >= 3 && &args[1] == "build-nav" && args[3..].iter().all(|arg| arg.contains(":")) {
let entries = args[3..].iter().map(|arg| {
let mut splits = arg.splitn(2, ":");
let slug = splits.next().unwrap();
let input_path = splits.next().unwrap();
(slug, input_path)
}).collect::<Vec<_>>();
build_nav(&entries, &args[2])
} else if args.len() == 6 && &args[1] == "build-page" {
build_page(&args[2], &args[3], &args[4], &args[5])
} else {
Err("invalid arguments".into())
}
}
fn build_nav(entries: &[(&str, &str)], nav_path: &str) -> Result<(), Box<dyn Error>> {
let arena = Arena::new();
let options = comrak_options();
let nav = entries.iter().map(|&(slug, input_path)| {
let title = load_page_title(&arena, &options, input_path)?;
Ok((slug, title))
}).collect::<io::Result<Vec<_>>>()?;
// Only write if different to avoid spurious rebuilds
let old_string = fs::read_to_string(nav_path).unwrap_or(String::new());
let new_string = serde_json::to_string_pretty(&nav)?;
if old_string != new_string {
fs::create_dir_all(Path::new(nav_path).parent().unwrap())?;
fs::write(nav_path, new_string)?;
}
Ok(())
}
fn build_page(
output_path: &str,
slug: &str,
input_path: &str,
nav_path: &str,
) -> Result<(), Box<dyn Error>> {
let nav: Vec<(String, Option<String>)> = {
let file = File::open(nav_path)?;
let reader = BufReader::new(file);
serde_json::from_reader(reader)?
};
let arena = Arena::new();
let options = comrak_options();
let nav = nav.iter().filter_map(|(slug, title)| {
title.as_ref().map(|title| {
let title = comrak::parse_document(&arena, title, &options);
(slug.as_str(), title)
})
}).collect::<Vec<_>>();
let page = load_page(&arena, &options, input_path)?;
let markup = views::main(&options, slug, page, &nav);
fs::create_dir_all(Path::new(output_path).parent().unwrap())?;
fs::write(output_path, markup.into_string())?;
Ok(())
}
struct Page<'a> {
title: Option<&'a AstNode<'a>>,
content: &'a AstNode<'a>,
}
fn load_page<'a>(
arena: &'a Arena<AstNode<'a>>,
options: &ComrakOptions,
path: impl AsRef<Path>,
) -> io::Result<Page<'a>> {
let page = load_page_raw(arena, options, path)?;
lower_headings(page.content);
rewrite_md_links(page.content)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
highlight_code(page.content)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
Ok(page)
}
fn load_page_title<'a>(
arena: &'a Arena<AstNode<'a>>,
options: &ComrakOptions,
path: impl AsRef<Path>,
) -> io::Result<Option<String>> {
let page = load_page_raw(arena, options, path)?;
let title = page.title.map(|title| {
let mut buffer = String::new();
comrak::format_commonmark(
title,
options,
&mut string_writer::StringWriter(&mut buffer),
).unwrap();
buffer
});
Ok(title)
}
fn load_page_raw<'a>(
arena: &'a Arena<AstNode<'a>>,
options: &ComrakOptions,
path: impl AsRef<Path>,
) -> io::Result<Page<'a>> {
let buffer = fs::read_to_string(path)?;
let content = comrak::parse_document(arena, &buffer, options);
let title = content
.first_child()
.filter(|node| {
let mut data = node.data.borrow_mut();
if let NodeValue::Heading(NodeHeading { level: 1, .. }) = data.value {
node.detach();
data.value = NodeValue::Document;
true
} else {
false
}
});
Ok(Page { title, content })
}
fn lower_headings<'a>(root: &'a AstNode<'a>) {
for node in root.descendants() {
let mut data = node.data.borrow_mut();
if let NodeValue::Heading(NodeHeading { level, .. }) = &mut data.value {
*level += 1;
}
}
}
fn rewrite_md_links<'a>(root: &'a AstNode<'a>) -> Result<(), FromUtf8Error> {
for node in root.descendants() {
let mut data = node.data.borrow_mut();
if let NodeValue::Link(NodeLink { url, .. }) = &mut data.value {
let mut url_string = String::from_utf8(mem::replace(url, Vec::new()))?;
if url_string.ends_with(".md") {
url_string.truncate(url_string.len() - ".md".len());
url_string.push_str(".html");
}
*url = url_string.into_bytes();
}
}
Ok(())
}
fn highlight_code<'a>(root: &'a AstNode<'a>) -> Result<(), FromUtf8Error> {
let ss = SyntaxSet::load_defaults_newlines();
let ts = ThemeSet::load_defaults();
let mut theme = ts.themes["InspiredGitHub"].clone();
theme.settings.background = Some(Color { r: 0xee, g: 0xee, b: 0xee, a: 0xff });
for node in root.descendants() {
let mut data = node.data.borrow_mut();
if let NodeValue::CodeBlock(NodeCodeBlock { info, literal, .. }) = &mut data.value {
let info = String::from_utf8(mem::replace(info, Vec::new()))?;
let syntax = ss.find_syntax_by_token(&info)
.unwrap_or_else(|| ss.find_syntax_plain_text());
let mut literal = String::from_utf8(mem::replace(literal, Vec::new()))?;
if !literal.ends_with('\n') {
// Syntect expects a trailing newline
literal.push('\n');
}
let html = highlighted_html_for_string(&literal, &ss, syntax, &theme);
data.value = NodeValue::HtmlBlock(NodeHtmlBlock {
block_type: 0,
literal: html.into_bytes(),
});
}
}
Ok(())
}
fn comrak_options() -> ComrakOptions {
ComrakOptions {
ext_header_ids: Some("".to_string()),
unsafe_: true,
..ComrakOptions::default()
}
}