#![deny(missing_docs)]
use crate::asset::Asset;
use crate::index::Index;
use crate::page::Page;
use crate::renderer::Renderer;
use crate::search;
use crate::util::read_file_to_string;
use crate::Result;
use log::trace;
use pulldown_cmark::{html, Parser};
use std::path::{Component, Path, PathBuf};
#[derive(Debug)]
enum DocketError {
PathNotDirectory { path: PathBuf },
}
impl std::fmt::Display for DocketError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DocketError::PathNotDirectory { path } => {
write!(f, "Not a directory {:?}", path)?;
Ok(())
}
}
}
}
impl std::error::Error for DocketError {}
#[derive(Debug)]
pub struct Docket {
title: String,
index: Option<PathBuf>,
footer: Option<PathBuf>,
pages: Vec<PathBuf>,
assets: Vec<Asset>,
}
static STYLE: &'static str = include_str!("../assets/style.css");
static SEARCH_JS: &'static str = include_str!("../assets/search.js");
impl Docket {
#[allow(clippy::new_ret_no_self)]
pub fn new(doc_path: &Path) -> Result<Self> {
trace!("Searching for docs in {:?}", doc_path);
if !doc_path.is_dir() {
return Err(DocketError::PathNotDirectory {
path: doc_path.to_owned(),
}
.into());
}
let title_file = doc_path.join("title");
let title = if title_file.is_file() {
read_file_to_string(&title_file)
} else {
Path::canonicalize(doc_path)
.unwrap()
.components()
.filter_map(|c| match c {
Component::Normal(path) => path.to_owned().into_string().ok(),
_ => None,
})
.filter(|s| s != "docs")
.last()
.unwrap()
};
let mut index = None;
let mut footer = None;
let mut pages = Vec::new();
let mut assets = vec![
Asset::internal("style.css", &STYLE),
Asset::internal("search.js", &SEARCH_JS),
];
for entry in doc_path.read_dir()? {
let entry = entry?;
let path = entry.path();
if !path.is_file() {
continue;
}
let name = path.file_name().unwrap().to_string_lossy();
if name == "titlle" {
continue;
}
if let Some(ext) = path.extension() {
let extension = ext.to_string_lossy();
match extension.as_ref() {
"md" | "markdown" | "mdown" => match path {
ref p if has_stem(&p, "footer") => footer = Some(p.to_owned()),
ref p if has_stem(&p, "index") => index = Some(p.to_owned()),
_ => pages.push(path.clone()),
},
_ => assets.push(Asset::path(path.clone())),
}
} else {
assets.push(Asset::path(path.clone()));
}
}
pages.sort();
Ok(Docket {
title: title.to_string(),
index,
footer,
pages,
assets,
})
}
pub fn render(self, output: &Path) -> Result<()> {
trace!("Rendering docs to {:?} ({:?})", output, self);
let footer = self.rendered_footer();
let renderer = Renderer::new(self.title.clone(), footer);
let rendered_pages = map_maybe_par(self.pages, |p| {
let page = Page::new(&p);
renderer.render(&page, &output)
})?;
search::write_search_indices(&output, rendered_pages.iter())?;
let index = Index::new(self.title, self.index, rendered_pages);
renderer.render(&index, &output)?;
for asset in self.assets {
asset.copy_to(&output)?;
}
Ok(())
}
fn rendered_footer(&self) -> String {
let mut footer = String::new();
if let Some(ref footer_path) = self.footer {
let contents = read_file_to_string(&footer_path);
let parsed = Parser::new(&contents);
html::push_html(&mut footer, parsed);
}
footer
}
}
#[cfg(feature = "par_render")]
fn map_maybe_par<T, F, U>(input: Vec<T>, f: F) -> Result<Vec<U>>
where
T: Sync,
F: Fn(&T) -> Result<U> + Send + Sync,
U: Send,
{
use rayon::prelude::*;
input.into_par_iter().map(f).collect()
}
#[cfg(not(feature = "par_render"))]
fn map_maybe_par<T, F, U>(input: Vec<T>, f: F) -> Result<Vec<U>>
where
F: Fn(T) -> Result<U>,
{
input.into_iter().map(f).collect()
}
fn has_stem(path: &Path, name: &str) -> bool {
path.file_stem().map(|p| p == name).unwrap_or(false)
}