2023-10-22 17:31:12 -04:00
|
|
|
use std::ffi::OsStr;
|
2023-12-23 15:45:23 -05:00
|
|
|
use std::path::Path;
|
2023-10-22 16:10:41 -04:00
|
|
|
use std::path::PathBuf;
|
|
|
|
|
2023-10-22 17:31:12 -04:00
|
|
|
use include_dir::include_dir;
|
|
|
|
use include_dir::Dir;
|
2023-12-23 15:45:23 -05:00
|
|
|
use tokio::task::JoinHandle;
|
|
|
|
use walkdir::WalkDir;
|
2023-10-22 17:31:12 -04:00
|
|
|
|
2023-10-23 21:51:15 -04:00
|
|
|
use crate::config::Config;
|
2023-12-19 15:42:37 -05:00
|
|
|
use crate::context::RenderBlogPostPage;
|
|
|
|
use crate::context::RenderBlogPostPageInput;
|
2023-12-17 17:26:15 -05:00
|
|
|
use crate::context::RenderBlogStream;
|
2023-12-19 15:42:37 -05:00
|
|
|
use crate::context::RenderBlogStreamInput;
|
|
|
|
use crate::context::RenderContext;
|
2023-10-22 16:10:41 -04:00
|
|
|
use crate::error::CustomError;
|
2023-12-17 17:16:26 -05:00
|
|
|
use crate::intermediate::get_web_path;
|
2023-10-27 13:05:34 -04:00
|
|
|
use crate::intermediate::BlogPost;
|
2023-10-22 16:26:43 -04:00
|
|
|
use crate::render::DusterRenderer;
|
2023-10-22 16:40:58 -04:00
|
|
|
use crate::render::RendererIntegration;
|
2023-10-22 16:10:41 -04:00
|
|
|
|
2023-12-17 13:46:47 -05:00
|
|
|
use super::stylesheet::Stylesheet;
|
|
|
|
|
2023-10-22 17:31:12 -04:00
|
|
|
static MAIN_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/default_environment/templates/html");
|
|
|
|
|
2023-10-22 16:10:41 -04:00
|
|
|
pub(crate) struct SiteRenderer {
|
2023-10-22 17:31:12 -04:00
|
|
|
output_directory: PathBuf,
|
|
|
|
blog_posts: Vec<BlogPost>,
|
2023-12-17 13:46:47 -05:00
|
|
|
stylesheets: Vec<Stylesheet>,
|
2023-10-22 16:10:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
impl SiteRenderer {
|
2023-10-22 17:31:12 -04:00
|
|
|
pub(crate) fn new<P: Into<PathBuf>>(
|
|
|
|
output_directory: P,
|
|
|
|
blog_posts: Vec<BlogPost>,
|
2023-12-17 13:46:47 -05:00
|
|
|
stylesheets: Vec<Stylesheet>,
|
2023-10-22 17:31:12 -04:00
|
|
|
) -> SiteRenderer {
|
|
|
|
SiteRenderer {
|
|
|
|
output_directory: output_directory.into(),
|
|
|
|
blog_posts,
|
2023-12-17 13:46:47 -05:00
|
|
|
stylesheets,
|
2023-10-22 17:31:12 -04:00
|
|
|
}
|
|
|
|
}
|
2023-12-17 12:43:47 -05:00
|
|
|
|
2023-12-17 17:26:15 -05:00
|
|
|
fn init_renderer_integration(&self) -> Result<DusterRenderer<'_>, CustomError> {
|
2023-10-22 17:31:12 -04:00
|
|
|
let mut renderer_integration = DusterRenderer::new();
|
|
|
|
|
2023-10-22 18:39:05 -04:00
|
|
|
let sources: Vec<_> = MAIN_TEMPLATES
|
2023-10-22 17:31:12 -04:00
|
|
|
.files()
|
|
|
|
.filter(|f| f.path().extension() == Some(OsStr::new("dust")))
|
2023-10-22 18:39:05 -04:00
|
|
|
.collect();
|
|
|
|
if sources
|
|
|
|
.iter()
|
|
|
|
.filter(|f| f.path().file_stem() == Some(OsStr::new("main")))
|
|
|
|
.count()
|
|
|
|
!= 1
|
|
|
|
{
|
2023-10-22 17:31:12 -04:00
|
|
|
return Err("Expect exactly 1 main.dust template file.".into());
|
|
|
|
}
|
|
|
|
|
2023-10-22 17:43:30 -04:00
|
|
|
let decoded_templates = {
|
2023-10-22 18:39:05 -04:00
|
|
|
let mut decoded_templates = Vec::with_capacity(sources.len());
|
|
|
|
for entry in sources {
|
2023-10-22 17:43:30 -04:00
|
|
|
decoded_templates.push(build_name_contents_pairs(entry)?);
|
|
|
|
}
|
|
|
|
decoded_templates
|
|
|
|
};
|
2023-10-22 17:31:12 -04:00
|
|
|
|
2023-10-22 17:43:30 -04:00
|
|
|
for (name, contents) in decoded_templates {
|
|
|
|
renderer_integration.load_template(name, contents)?;
|
2023-10-22 17:31:12 -04:00
|
|
|
}
|
|
|
|
|
2023-12-17 17:26:15 -05:00
|
|
|
Ok(renderer_integration)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) async fn render_blog_posts(&self, config: &Config) -> Result<(), CustomError> {
|
|
|
|
let renderer_integration = self.init_renderer_integration()?;
|
|
|
|
|
2023-10-22 16:10:41 -04:00
|
|
|
for blog_post in &self.blog_posts {
|
2023-10-23 20:30:43 -04:00
|
|
|
for blog_post_page in &blog_post.pages {
|
|
|
|
let output_path = self
|
|
|
|
.output_directory
|
2023-12-19 16:46:32 -05:00
|
|
|
.join(config.get_relative_path_to_post(&blog_post.id))
|
2023-10-23 20:30:43 -04:00
|
|
|
.join(blog_post_page.get_output_path());
|
2023-10-23 22:10:26 -04:00
|
|
|
|
2023-12-19 15:42:37 -05:00
|
|
|
let convert_input = RenderBlogPostPageInput::new(blog_post, blog_post_page);
|
|
|
|
let render_context = RenderContext::new(
|
2023-10-23 21:51:15 -04:00
|
|
|
config,
|
2023-12-19 15:42:37 -05:00
|
|
|
self.output_directory.as_path(),
|
|
|
|
output_path.as_path(),
|
2023-12-19 17:51:35 -05:00
|
|
|
None,
|
2023-10-23 22:10:26 -04:00
|
|
|
)?;
|
2023-12-19 15:42:37 -05:00
|
|
|
let render_context = RenderBlogPostPage::new(render_context, &convert_input)?;
|
2023-10-23 20:30:43 -04:00
|
|
|
let rendered_output = renderer_integration.render(render_context)?;
|
|
|
|
let parent_directory = output_path
|
|
|
|
.parent()
|
|
|
|
.ok_or("Output file should have a containing directory.")?;
|
|
|
|
tokio::fs::create_dir_all(parent_directory).await?;
|
|
|
|
tokio::fs::write(output_path, rendered_output).await?;
|
|
|
|
}
|
2023-10-22 16:10:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2023-12-17 12:43:47 -05:00
|
|
|
|
2023-12-17 15:23:40 -05:00
|
|
|
pub(crate) async fn render_blog_stream(&self, config: &Config) -> Result<(), CustomError> {
|
2023-12-17 17:26:15 -05:00
|
|
|
let renderer_integration = self.init_renderer_integration()?;
|
2023-12-17 15:45:50 -05:00
|
|
|
|
2023-12-17 16:57:37 -05:00
|
|
|
// Sort blog posts by date, newest first.
|
|
|
|
let sorted_blog_posts = {
|
|
|
|
let mut sorted_blog_posts: Vec<_> = self.blog_posts.iter().collect();
|
|
|
|
sorted_blog_posts
|
|
|
|
.sort_by_key(|blog_post| (blog_post.get_date(), blog_post.id.as_str()));
|
|
|
|
sorted_blog_posts.reverse();
|
|
|
|
sorted_blog_posts
|
|
|
|
};
|
|
|
|
|
|
|
|
for blog_post in &sorted_blog_posts {
|
|
|
|
if blog_post.get_date().is_none() {
|
|
|
|
return Err(format!("Blog post {} does not have a date.", blog_post.id).into());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Group blog posts based on # of posts per page.
|
|
|
|
let stream_chunks: Vec<_> = sorted_blog_posts
|
|
|
|
.chunks(config.get_stream_entries_per_page())
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
// For each group, create a RenderBlogStream.
|
|
|
|
let num_stream_pages = stream_chunks.len();
|
2023-12-17 17:16:26 -05:00
|
|
|
for (page_num, chunk) in stream_chunks.into_iter().enumerate() {
|
|
|
|
let output_file = if page_num == 0 {
|
|
|
|
self.output_directory.join("index.html")
|
|
|
|
} else {
|
|
|
|
self.output_directory
|
|
|
|
.join("stream")
|
|
|
|
.join(format!("{}.html", page_num))
|
|
|
|
};
|
|
|
|
let newer_link = if page_num == 0 {
|
|
|
|
None
|
|
|
|
} else if page_num == 1 {
|
|
|
|
Some(get_web_path(
|
|
|
|
config,
|
|
|
|
&self.output_directory,
|
|
|
|
&output_file,
|
|
|
|
"index.html",
|
|
|
|
)?)
|
|
|
|
} else {
|
|
|
|
Some(get_web_path(
|
|
|
|
config,
|
|
|
|
&self.output_directory,
|
|
|
|
&output_file,
|
|
|
|
format!("stream/{}.html", page_num - 1),
|
|
|
|
)?)
|
|
|
|
};
|
|
|
|
let older_link = if page_num == (num_stream_pages - 1) {
|
|
|
|
None
|
|
|
|
} else {
|
|
|
|
Some(get_web_path(
|
|
|
|
config,
|
|
|
|
&self.output_directory,
|
|
|
|
&output_file,
|
|
|
|
format!("stream/{}.html", page_num + 1),
|
|
|
|
)?)
|
|
|
|
};
|
2023-12-17 17:26:15 -05:00
|
|
|
|
2023-12-19 15:42:37 -05:00
|
|
|
let convert_input = RenderBlogStreamInput::new(chunk, older_link, newer_link);
|
|
|
|
let render_context = RenderContext::new(
|
2023-12-17 17:26:15 -05:00
|
|
|
config,
|
|
|
|
self.output_directory.as_path(),
|
|
|
|
output_file.as_path(),
|
2023-12-19 17:51:35 -05:00
|
|
|
None,
|
2023-12-17 17:26:15 -05:00
|
|
|
)?;
|
2023-12-19 15:42:37 -05:00
|
|
|
let blog_stream = RenderBlogStream::new(render_context, &convert_input)?;
|
2023-12-17 17:26:15 -05:00
|
|
|
|
|
|
|
// Pass each RenderBlogStream to dust as the context to render index.html and any additional stream pages.
|
|
|
|
let rendered_output = renderer_integration.render(blog_stream)?;
|
|
|
|
let parent_directory = output_file
|
|
|
|
.parent()
|
|
|
|
.ok_or("Output file should have a containing directory.")?;
|
|
|
|
tokio::fs::create_dir_all(parent_directory).await?;
|
|
|
|
tokio::fs::write(output_file, rendered_output).await?;
|
2023-12-17 15:45:50 -05:00
|
|
|
}
|
2023-12-17 15:23:40 -05:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2023-12-17 13:46:47 -05:00
|
|
|
pub(crate) async fn render_stylesheets(&self) -> Result<(), CustomError> {
|
|
|
|
let stylesheet_output_directory = self.output_directory.join("stylesheet");
|
|
|
|
if !stylesheet_output_directory.exists() {
|
|
|
|
tokio::fs::create_dir(&stylesheet_output_directory).await?;
|
|
|
|
}
|
|
|
|
for stylesheet in &self.stylesheets {
|
|
|
|
let file_output_path = stylesheet_output_directory.join(&stylesheet.path);
|
|
|
|
let parent_directory = file_output_path
|
|
|
|
.parent()
|
|
|
|
.ok_or("Output file should have a containing directory.")?;
|
|
|
|
tokio::fs::create_dir_all(parent_directory).await?;
|
|
|
|
tokio::fs::write(file_output_path, stylesheet.contents.as_bytes()).await?;
|
|
|
|
}
|
|
|
|
Ok(())
|
2023-12-17 12:43:47 -05:00
|
|
|
}
|
2023-12-23 15:45:23 -05:00
|
|
|
|
|
|
|
pub(crate) async fn copy_static_files(&self, config: &Config) -> Result<(), CustomError> {
|
|
|
|
let static_files_directory = config
|
|
|
|
.get_root_directory()
|
|
|
|
.join(config.get_relative_path_to_static_files());
|
|
|
|
if !static_files_directory.exists() {
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
let static_files = get_all_files(&static_files_directory)?;
|
|
|
|
for entry in static_files {
|
|
|
|
let (path, contents) = entry.await??;
|
|
|
|
let relative_path = path.strip_prefix(&static_files_directory)?;
|
|
|
|
let output_path = self.output_directory.join(relative_path);
|
|
|
|
let parent_directory = output_path
|
|
|
|
.parent()
|
|
|
|
.ok_or("Output file should have a containing directory.")?;
|
|
|
|
tokio::fs::create_dir_all(parent_directory).await?;
|
|
|
|
tokio::fs::write(output_path, contents).await?;
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
2023-10-22 16:10:41 -04:00
|
|
|
}
|
2023-10-22 17:31:12 -04:00
|
|
|
|
2023-10-22 17:43:30 -04:00
|
|
|
fn build_name_contents_pairs<'a>(
|
|
|
|
entry: &'a include_dir::File<'_>,
|
|
|
|
) -> Result<(&'a str, &'a str), CustomError> {
|
2023-10-22 17:31:12 -04:00
|
|
|
let path = entry.path();
|
|
|
|
let name = path
|
|
|
|
.file_stem()
|
|
|
|
.ok_or("All templates should have a stem.")?
|
|
|
|
.to_str()
|
|
|
|
.ok_or("All template filenames should be valid utf-8.")?;
|
|
|
|
let contents = std::str::from_utf8(entry.contents())?;
|
2023-10-22 17:43:30 -04:00
|
|
|
Ok((name, contents))
|
2023-10-22 17:31:12 -04:00
|
|
|
}
|
2023-12-23 15:45:23 -05:00
|
|
|
|
|
|
|
fn get_all_files<P: AsRef<Path>>(
|
|
|
|
root_dir: P,
|
|
|
|
) -> Result<impl Iterator<Item = JoinHandle<std::io::Result<(PathBuf, Vec<u8>)>>>, walkdir::Error> {
|
|
|
|
let files = WalkDir::new(root_dir)
|
|
|
|
.into_iter()
|
|
|
|
.filter(|e| match e {
|
|
|
|
Ok(dir_entry) => dir_entry.file_type().is_file(),
|
|
|
|
Err(_) => true,
|
|
|
|
})
|
|
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
|
|
let org_files = files
|
|
|
|
.into_iter()
|
|
|
|
.map(walkdir::DirEntry::into_path)
|
|
|
|
.map(|path| tokio::spawn(read_file(path)));
|
|
|
|
Ok(org_files)
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn read_file(path: PathBuf) -> std::io::Result<(PathBuf, Vec<u8>)> {
|
|
|
|
let contents = tokio::fs::read(&path).await?;
|
|
|
|
Ok((path, contents))
|
|
|
|
}
|