Skip to content

Commit

Permalink
Refactor book builder (lambda-fairy#288)
Browse files Browse the repository at this point in the history
Move page and nav builders to separate modules.
  • Loading branch information
lambda-fairy authored Sep 4, 2021
1 parent eb9cd82 commit 3ceb70d
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 131 deletions.
18 changes: 10 additions & 8 deletions docs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ slugs := index getting-started text-escaping elements-attributes splices-toggles
slug_to_md = content/$(1).md
slug_to_html = site/$(1).html

builder := target/debug/docs
build_nav := target/debug/build_nav
build_page := target/debug/build_page

nav_json := site/nav.json

version := $(shell git describe)
Expand All @@ -18,17 +20,17 @@ print_status = @ printf ' \e[1;35m♦ %s\e[0m\n' '$(1)'
.PHONY: all
all: $(html_files) site/styles.css

$(builder): $(wildcard src/*)
$(call print_status,Cargo)
@ cargo build --locked
$(build_nav) $(build_page): target/debug/%: $(shell find src)
$(call print_status,Cargo $@)
@ cargo build --bin $(@F) --locked

$(nav_json): $(md_files) $(builder)
$(nav_json): $(md_files) $(build_nav)
$(call print_status,Table of contents)
@ $(builder) build-nav $@ $(slugs_and_md_files)
@ $(build_nav) $@ $(slugs_and_md_files)

site/%.html: content/%.md $(nav_json) $(builder)
site/%.html: content/%.md $(nav_json) $(build_page)
$(call print_status,Render $(*F))
@ $(builder) build-page $@ $(*F) $< $(nav_json) $(version) $(hash)
@ $(build_page) $@ $(*F) $< $(nav_json) $(version) $(hash)

site/styles.css: styles.css
$(call print_status,Copy stylesheet)
Expand Down
63 changes: 63 additions & 0 deletions docs/src/bin/build_nav.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use comrak::nodes::AstNode;
use comrak::{self, Arena};
use docs::page::{Page, COMRAK_OPTIONS};
use docs::string_writer::StringWriter;
use serde_json;
use std::env;
use std::error::Error;
use std::fs;
use std::io;
use std::path::Path;
use std::str;

fn main() -> Result<(), Box<dyn Error>> {
let args = env::args().collect::<Vec<_>>();
if args.len() < 2 || !args[2..].iter().all(|arg| arg.contains(":")) {
return Err("invalid arguments".into());
}
let entries = args[2..]
.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[1])
}

fn build_nav(entries: &[(&str, &str)], nav_path: &str) -> Result<(), Box<dyn Error>> {
let arena = Arena::new();

let nav = entries
.iter()
.map(|&(slug, input_path)| {
let title = load_page_title(&arena, 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 load_page_title<'a>(
arena: &'a Arena<AstNode<'a>>,
path: impl AsRef<Path>,
) -> io::Result<Option<String>> {
let page = Page::load(arena, path)?;
let title = page.title.map(|title| {
let mut buffer = String::new();
comrak::format_commonmark(title, &COMRAK_OPTIONS, &mut StringWriter(&mut buffer)).unwrap();
buffer
});
Ok(title)
}
125 changes: 18 additions & 107 deletions docs/src/main.rs → docs/src/bin/build_page.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use comrak::nodes::{AstNode, NodeCodeBlock, NodeHeading, NodeHtmlBlock, NodeLink, NodeValue};
use comrak::{self, Arena, ComrakOptions};
use comrak::{self, Arena};
use docs::page::{Page, COMRAK_OPTIONS};
use docs::views;
use serde_json;
use std::env;
use std::error::Error;
use std::fs::{self, File};
use std::io::{self, BufReader};
use std::io::BufReader;
use std::mem;
use std::path::Path;
use std::str::{self, Utf8Error};
Expand All @@ -13,54 +15,12 @@ use syntect::highlighting::{Color, ThemeSet};
use syntect::html::highlighted_html_for_string;
use syntect::parsing::SyntaxSet;

use crate::page::Page;
use crate::string_writer::StringWriter;

mod page;
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() == 8 && &args[1] == "build-page" {
build_page(&args[2], &args[3], &args[4], &args[5], &args[6], &args[7])
} else {
Err("invalid arguments".into())
if args.len() != 7 {
return 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(())
build_page(&args[1], &args[2], &args[3], &args[4], &args[5], &args[6])
}

fn build_page(
Expand All @@ -78,76 +38,34 @@ fn build_page(
};

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);
let title = comrak::parse_document(&arena, title, &COMRAK_OPTIONS);
(slug.as_str(), title)
})
})
.collect::<Vec<_>>();

let page = load_page(&arena, &options, input_path)?;
let markup = views::main(&options, slug, page, &nav, version, hash);
let page = Page::load(&arena, input_path)?;
postprocess(page.content)?;

let markup = views::main(slug, page, &nav, version, hash);

fs::create_dir_all(Path::new(output_path).parent().unwrap())?;
fs::write(output_path, markup.into_string())?;

Ok(())
}

fn load_page<'a>(
arena: &'a Arena<AstNode<'a>>,
options: &ComrakOptions,
path: impl AsRef<Path>,
) -> Result<Page<'a>, Box<dyn Error>> {
let page = load_page_raw(arena, options, path)?;

lower_headings(page.content);
rewrite_md_links(page.content)?;
strip_hidden_code(page.content)?;
highlight_code(page.content)?;

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 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 postprocess<'a>(content: &'a AstNode<'a>) -> Result<(), Box<dyn Error>> {
lower_headings(content);
rewrite_md_links(content)?;
strip_hidden_code(content)?;
highlight_code(content)?;
Ok(())
}

fn lower_headings<'a>(root: &'a AstNode<'a>) {
Expand Down Expand Up @@ -235,10 +153,3 @@ fn highlight_code<'a>(root: &'a AstNode<'a>) -> Result<(), Box<dyn Error>> {
fn parse_code_block_info(info: &[u8]) -> Result<Vec<&str>, Utf8Error> {
str::from_utf8(info).map(|info| info.split(",").map(str::trim).collect())
}

fn comrak_options() -> ComrakOptions {
let mut options = ComrakOptions::default();
options.extension.header_ids = Some("".to_string());
options.render.unsafe_ = true;
options
}
5 changes: 5 additions & 0 deletions docs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#![feature(once_cell)]

pub mod page;
pub mod string_writer;
pub mod views;
34 changes: 33 additions & 1 deletion docs/src/page.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
use comrak::nodes::AstNode;
use comrak::nodes::{AstNode, NodeHeading, NodeValue};
use comrak::{Arena, ComrakOptions};
use std::fs;
use std::io;
use std::lazy::SyncLazy;
use std::path::Path;

pub struct Page<'a> {
pub title: Option<&'a AstNode<'a>>,
pub content: &'a AstNode<'a>,
}

impl<'a> Page<'a> {
pub fn load(arena: &'a Arena<AstNode<'a>>, path: impl AsRef<Path>) -> io::Result<Self> {
let buffer = fs::read_to_string(path)?;
let content = comrak::parse_document(arena, &buffer, &COMRAK_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(Self { title, content })
}
}

pub static COMRAK_OPTIONS: SyncLazy<ComrakOptions> = SyncLazy::new(|| {
let mut options = ComrakOptions::default();
options.extension.header_ids = Some("".to_string());
options.render.unsafe_ = true;
options
});
Loading

0 comments on commit 3ceb70d

Please sign in to comment.