use std::ffi::OsStr; use std::path::PathBuf; use include_dir::include_dir; use include_dir::Dir; use crate::config::Config; use crate::error::CustomError; use crate::intermediate::convert_blog_post_page_to_render_context; use crate::intermediate::BlogPost; use crate::render::DusterRenderer; use crate::render::RendererIntegration; use super::stylesheet::Stylesheet; static MAIN_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/default_environment/templates/html"); pub(crate) struct SiteRenderer { output_directory: PathBuf, blog_posts: Vec, stylesheets: Vec, } impl SiteRenderer { pub(crate) fn new>( output_directory: P, blog_posts: Vec, stylesheets: Vec, ) -> SiteRenderer { SiteRenderer { output_directory: output_directory.into(), blog_posts, stylesheets, } } pub(crate) async fn render_blog_posts(&self, config: &Config) -> Result<(), CustomError> { let mut renderer_integration = DusterRenderer::new(); let sources: Vec<_> = MAIN_TEMPLATES .files() .filter(|f| f.path().extension() == Some(OsStr::new("dust"))) .collect(); if sources .iter() .filter(|f| f.path().file_stem() == Some(OsStr::new("main"))) .count() != 1 { return Err("Expect exactly 1 main.dust template file.".into()); } let decoded_templates = { let mut decoded_templates = Vec::with_capacity(sources.len()); for entry in sources { decoded_templates.push(build_name_contents_pairs(entry)?); } decoded_templates }; for (name, contents) in decoded_templates { renderer_integration.load_template(name, contents)?; } for blog_post in &self.blog_posts { for blog_post_page in &blog_post.pages { let output_path = self .output_directory .join("posts") .join(&blog_post.id) .join(blog_post_page.get_output_path()); let render_context = convert_blog_post_page_to_render_context( config, &self.output_directory, &output_path, blog_post, blog_post_page, )?; 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?; } } Ok(()) } pub(crate) async fn render_blog_stream(&self, config: &Config) -> Result<(), CustomError> { // TODO: Actually render a blog stream to index.html // Steps: sort blog posts by date, newest first // // Steps: group blog posts based on # of posts per page // // Steps: for each group, create a RenderBlogStream // // Steps: pass each RenderBlogStream to dust as the context to render index.html and any additional stream pages. Ok(()) } 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(()) } } fn build_name_contents_pairs<'a>( entry: &'a include_dir::File<'_>, ) -> Result<(&'a str, &'a str), CustomError> { 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())?; Ok((name, contents)) }