use log::trace;
mod layout;
use crate::{
asset::Asset,
doctree::{self, DoctreeItem, Frontispiece, Page},
error::Result,
search::{self, SearchableDocument},
};
use std::{
fs::{self, File},
io::BufWriter,
path::{Path, PathBuf},
};
use self::layout::Layout;
pub struct RenderContext {
path: PathBuf,
site_name: String,
layout: Option<Box<dyn Layout>>,
}
impl RenderContext {
pub fn new(path: PathBuf, site_name: String) -> Self {
RenderContext {
path,
site_name,
layout: None,
}
}
fn layout(&self) -> &dyn Layout {
self.layout
.as_deref()
.unwrap_or_else(layout::get_default_layout)
}
}
enum RenderStateKind<'s, 'b> {
Root(&'s RenderContext),
Nested(&'s RenderState<'s, 'b>, PathBuf),
}
impl<'s, 'b> RenderStateKind<'s, 'b> {
fn new_root(context: &'s RenderContext) -> Self {
RenderStateKind::Root(context)
}
fn with_parent(state: &'s RenderState<'s, 'b>, slug: &str) -> Self {
let mut new_path = PathBuf::from(state.output_path());
new_path.push(slug);
RenderStateKind::Nested(state, new_path)
}
}
pub struct RenderState<'s, 'b> {
kind: RenderStateKind<'s, 'b>,
bale: &'b Frontispiece,
navs: Vec<NavInfo>,
}
impl<'s, 'b> RenderState<'s, 'b> {
fn new(kind: RenderStateKind<'s, 'b>, bale: &'b Frontispiece, navs: Vec<NavInfo>) -> Self {
RenderState { kind, bale, navs }
}
fn output_path(&self) -> &Path {
match &self.kind {
RenderStateKind::Root(ctx) => &ctx.path,
RenderStateKind::Nested(_, path) => path,
}
}
fn current_bale(&self) -> &Frontispiece {
&self.bale
}
fn ctx(&self) -> &RenderContext {
match self.kind {
RenderStateKind::Root(ctx) => ctx,
RenderStateKind::Nested(parent, _) => parent.ctx(),
}
}
fn parent(&self) -> Option<&RenderState> {
if let RenderStateKind::Nested(parent, _) = &self.kind {
Some(parent)
} else {
None
}
}
fn path_to_root(&self, page: &PageKind) -> String {
let mut current = self;
let mut path = String::new();
while let Some(parent) = current.parent() {
path.push_str("../");
current = parent
}
if let PageKind::Nested(_) = page {
path.push_str("../");
}
path
}
}
struct NavInfo {
pub title: String,
pub slug: String,
}
impl NavInfo {
fn new(slug: &str, title: &str) -> Self {
NavInfo {
title: title.to_owned(),
slug: slug.to_owned(),
}
}
}
pub(crate) enum PageKind {
Index,
Nested(String),
}
impl PageKind {
fn path_to_bale(&self) -> &'static str {
match self {
PageKind::Index => "./",
PageKind::Nested(_) => "../",
}
}
}
enum RenderedItem {
Page(Page),
Nested(String, Box<RenderedItem>),
}
impl SearchableDocument for RenderedItem {
fn title(&self) -> &str {
match self {
RenderedItem::Page(p) => p.title(),
RenderedItem::Nested(_, n) => n.title(),
}
}
fn slug(&self) -> &str {
match self {
RenderedItem::Page(p) => p.slug(),
RenderedItem::Nested(s, _) => &s,
}
}
fn search_index(&self) -> Option<&search::TermFrequenciesIndex> {
match self {
RenderedItem::Page(p) => p.search_index(),
RenderedItem::Nested(_, n) => n.search_index(),
}
}
}
impl RenderedItem {
fn page(page: Page) -> Self {
Self::Page(page)
}
fn nested(slug: &str, inner: RenderedItem) -> Self {
let slug = format!("{}/{}", slug, inner.slug());
Self::Nested(slug, Box::new(inner))
}
}
fn render_bale_contents(
state: &RenderState,
assets: Vec<Asset>,
items: Vec<DoctreeItem>,
) -> Result<Vec<RenderedItem>> {
trace!(
"rendering bale contents {:?} to {:?}",
state.bale,
state.output_path()
);
fs::create_dir_all(&state.output_path())?;
let mut rendered_items = Vec::new();
if let Some(page) = state.current_bale().index_page() {
trace!("Bale has an index. Rendering.");
render_page(&state, PageKind::Index, page)?;
}
for asset in assets {
asset.copy_to(&state.output_path())?;
}
for item in items {
match item {
DoctreeItem::Bale(bale) => {
let (bale, assets, items) = bale.break_open()?;
let navs = navs_for_items(&items);
let state = RenderState::new(
RenderStateKind::with_parent(&state, bale.slug()),
&bale,
navs,
);
rendered_items.extend(
render_bale_contents(&state, assets, items)?
.into_iter()
.map(|item| RenderedItem::nested(bale.slug(), item)),
);
}
DoctreeItem::Page(page) => {
render_page(&state, PageKind::Nested(page.slug().to_owned()), &page)?;
rendered_items.push(RenderedItem::page(page))
}
}
}
Ok(rendered_items)
}
fn navs_for_items(items: &[DoctreeItem]) -> Vec<NavInfo> {
items
.iter()
.map(|item| match item {
DoctreeItem::Page(page) => NavInfo::new(page.slug(), page.title()),
DoctreeItem::Bale(bale) => {
NavInfo::new(bale.frontispiece().slug(), bale.frontispiece().title())
}
})
.collect()
}
fn render_page(state: &RenderState, kind: PageKind, page: &doctree::Page) -> Result<()> {
let mut path = PathBuf::from(state.output_path());
if let PageKind::Nested(slug) = &kind {
path.push(slug);
fs::create_dir_all(&path)?;
};
trace!("rendering page {} at {:?}", page.title(), path);
let output_path = path.join("index.html");
let file = File::create(&output_path)?;
let mut writer = BufWriter::new(file);
let layout = state.ctx().layout();
layout.render(&mut writer, state, kind, page)?;
Ok(())
}
fn copy_global_assets(ctx: &RenderContext) -> Result<()> {
fs::create_dir_all(&ctx.path)?;
for asset in ctx.layout().assets() {
asset.copy_to(&ctx.path)?;
}
Ok(())
}
pub(crate) fn render<P: AsRef<Path>>(
target: P,
title: String,
doctree_root: doctree::Bale,
) -> Result<()> {
let ctx = RenderContext::new(target.as_ref().to_owned(), title);
let (frontispiece, assets, items) = doctree_root.break_open()?;
let navs = navs_for_items(&items);
let state = RenderState::new(RenderStateKind::new_root(&ctx), &frontispiece, navs);
copy_global_assets(&ctx)?;
let docs = render_bale_contents(&state, assets, items)?;
search::write_search_indices(&ctx.path, docs.iter())?;
Ok(())
}