Compare commits

...

10 Commits

Author SHA1 Message Date
Tom Alexander
586fd8a066
Getting rendered output from duster. 2023-10-22 18:31:56 -04:00
Tom Alexander
043cc5eda4
I think I have worked around the lifetime issue by keeping references to the intermediate str's. 2023-10-22 18:12:45 -04:00
Tom Alexander
58aba8efd5
Invoking the compile function.
I am going to have to address the lifetime issue of "compiled" duster templates borrowing the input str.
2023-10-22 17:37:27 -04:00
Tom Alexander
ce0819e85b
Feeding the templates into the renderer integration. 2023-10-22 17:31:12 -04:00
Tom Alexander
fc5342adce
Make the renderer a bit more generic. 2023-10-22 16:40:58 -04:00
Tom Alexander
aed88cf05a
Add include_dir.
This will let us embed the default versions of templates, stylesheets, javascript, etc into the binary. Naturally, we will eventually support overriding the defaults.
2023-10-22 16:28:54 -04:00
Tom Alexander
24bac982f1
Starting to create the renderer integrations.
These are the layer directly over dust which can be used by anything, not just blog posts.
2023-10-22 16:26:43 -04:00
Tom Alexander
5f34cb2dd5
Creating a SiteRenderer struct to handle the logic for invoking dust. 2023-10-22 16:10:41 -04:00
Tom Alexander
0b6900eeca
Serialize the RenderBlogPost to JSON.
This struct still does not contain anything, but I'm just setting up the skeleton for this code.
2023-10-22 16:01:42 -04:00
Tom Alexander
b72aec9d20
Starting a struct for what will be passed as the context into dust. 2023-10-22 15:31:45 -04:00
15 changed files with 243 additions and 15 deletions

20
Cargo.lock generated
View File

@ -216,6 +216,25 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
[[package]]
name = "include_dir"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e"
dependencies = [
"include_dir_macros",
]
[[package]]
name = "include_dir_macros"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f"
dependencies = [
"proc-macro2",
"quote",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.0.2" version = "2.0.2"
@ -624,6 +643,7 @@ version = "0.0.1"
dependencies = [ dependencies = [
"clap", "clap",
"duster", "duster",
"include_dir",
"organic", "organic",
"serde", "serde",
"serde_json", "serde_json",

View File

@ -6,14 +6,12 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
# error-context, suggestions, usage | env
clap = { version = "4.4.6", default-features = false, features = ["std", "color", "help", "derive"] } clap = { version = "4.4.6", default-features = false, features = ["std", "color", "help", "derive"] }
duster = { git = "https://code.fizz.buzz/talexander/duster.git", branch = "master" } duster = { git = "https://code.fizz.buzz/talexander/duster.git", branch = "master" }
include_dir = "0.7.3"
organic = "0.1.12" organic = "0.1.12"
# | alloc, rc, serde_derive, unstable
serde = { version = "1.0.189", default-features = false, features = ["std", "derive"] } serde = { version = "1.0.189", default-features = false, features = ["std", "derive"] }
serde_json = "1.0.107" serde_json = "1.0.107"
tokio = { version = "1.30.0", default-features = false, features = ["rt", "rt-multi-thread", "fs", "io-util"] } tokio = { version = "1.30.0", default-features = false, features = ["rt", "rt-multi-thread", "fs", "io-util"] }
# display, parse | indexmap, preserve_order
toml = "0.8.2" toml = "0.8.2"
walkdir = "2.4.0" walkdir = "2.4.0"

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<p>Hello world!</p>
</body>
</html>

8
src/blog_post/convert.rs Normal file
View File

@ -0,0 +1,8 @@
use super::render_context::RenderBlogPost;
use super::BlogPost;
pub(crate) fn convert_blog_post_to_render_context(post: &BlogPost) -> RenderBlogPost {
RenderBlogPost {
id: post.id.clone(),
}
}

View File

@ -8,7 +8,7 @@ use crate::error::CustomError;
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct BlogPost { pub(crate) struct BlogPost {
id: String, pub(crate) id: String,
} }
impl BlogPost { impl BlogPost {

View File

@ -1,2 +1,5 @@
mod convert;
mod definition; mod definition;
mod render_context;
pub(crate) use convert::convert_blog_post_to_render_context;
pub(crate) use definition::BlogPost; pub(crate) use definition::BlogPost;

View File

@ -0,0 +1,8 @@
use serde::Serialize;
#[derive(Debug, Serialize)]
#[serde(tag = "type")]
#[serde(rename = "blog_post")]
pub(crate) struct RenderBlogPost {
pub(crate) id: String,
}

View File

@ -1,3 +1,4 @@
mod render;
mod runner; mod runner;
pub(crate) use runner::build_site; pub(crate) use runner::build_site;

View File

@ -0,0 +1,78 @@
use std::ffi::OsStr;
use std::path::PathBuf;
use include_dir::include_dir;
use include_dir::Dir;
use crate::blog_post::convert_blog_post_to_render_context;
use crate::blog_post::BlogPost;
use crate::error::CustomError;
use crate::render::DusterRenderer;
use crate::render::RendererIntegration;
static MAIN_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/default_environment/templates/html");
pub(crate) struct SiteRenderer {
output_directory: PathBuf,
blog_posts: Vec<BlogPost>,
}
impl SiteRenderer {
pub(crate) fn new<P: Into<PathBuf>>(
output_directory: P,
blog_posts: Vec<BlogPost>,
) -> SiteRenderer {
SiteRenderer {
output_directory: output_directory.into(),
blog_posts,
}
}
pub(crate) async fn render_blog_posts(&self) -> Result<(), CustomError> {
let mut renderer_integration = DusterRenderer::new();
let (main_template, other_templates): (Vec<_>, Vec<_>) = MAIN_TEMPLATES
.files()
.filter(|f| f.path().extension() == Some(OsStr::new("dust")))
.partition(|f| f.path().file_stem() == Some(OsStr::new("main")));
if main_template.len() != 1 {
return Err("Expect exactly 1 main.dust template file.".into());
}
let decoded_templates = {
let mut decoded_templates =
Vec::with_capacity(main_template.len() + other_templates.len());
for entry in main_template {
decoded_templates.push(build_name_contents_pairs(entry)?);
}
for entry in other_templates {
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 {
let render_context = convert_blog_post_to_render_context(blog_post);
let rendered_output = renderer_integration.render(render_context)?;
println!("Rendered: {}", rendered_output);
}
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))
}

View File

@ -2,22 +2,16 @@ use std::path::PathBuf;
use crate::blog_post::BlogPost; use crate::blog_post::BlogPost;
use crate::cli::parameters::BuildArgs; use crate::cli::parameters::BuildArgs;
use crate::command::build::render::SiteRenderer;
use crate::config::Config; use crate::config::Config;
use crate::error::CustomError; use crate::error::CustomError;
pub(crate) async fn build_site(args: BuildArgs) -> Result<(), CustomError> { pub(crate) async fn build_site(args: BuildArgs) -> Result<(), CustomError> {
let config = Config::load_from_file(args.config).await?; let config = Config::load_from_file(args.config).await?;
let root_directory = config.get_root_directory().to_owned(); let blog_posts = load_blog_posts(&config).await?;
let output_directory = get_output_directory(&config).await?; let renderer = SiteRenderer::new(get_output_directory(&config).await?, blog_posts);
let post_directories = get_post_directories(&config).await?; renderer.render_blog_posts().await?;
let load_jobs = post_directories
.into_iter()
.map(|path| tokio::spawn(BlogPost::load_blog_post(root_directory.clone(), path)));
let mut blog_posts = Vec::new();
for job in load_jobs {
blog_posts.push(job.await??);
}
println!("{:?}", blog_posts);
Ok(()) Ok(())
} }
@ -51,3 +45,16 @@ async fn get_post_directories(config: &Config) -> Result<Vec<PathBuf>, CustomErr
} }
Ok(ret) Ok(ret)
} }
async fn load_blog_posts(config: &Config) -> Result<Vec<BlogPost>, CustomError> {
let root_directory = config.get_root_directory().to_owned();
let post_directories = get_post_directories(&config).await?;
let load_jobs = post_directories
.into_iter()
.map(|path| tokio::spawn(BlogPost::load_blog_post(root_directory.clone(), path)));
let mut blog_posts = Vec::new();
for job in load_jobs {
blog_posts.push(job.await??);
}
Ok(blog_posts)
}

View File

@ -1,3 +1,6 @@
use std::str::Utf8Error;
use std::string::FromUtf8Error;
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum CustomError { pub(crate) enum CustomError {
Static(&'static str), Static(&'static str),
@ -6,6 +9,11 @@ pub(crate) enum CustomError {
TomlDeserialize(toml::de::Error), TomlDeserialize(toml::de::Error),
WalkDir(walkdir::Error), WalkDir(walkdir::Error),
Tokio(tokio::task::JoinError), Tokio(tokio::task::JoinError),
Serde(serde_json::Error),
Utf8(Utf8Error),
FromUtf8(FromUtf8Error),
DusterCompile(duster::renderer::CompileError),
DusterRender(duster::renderer::RenderError),
} }
impl From<std::io::Error> for CustomError { impl From<std::io::Error> for CustomError {
@ -43,3 +51,33 @@ impl From<tokio::task::JoinError> for CustomError {
CustomError::Tokio(value) CustomError::Tokio(value)
} }
} }
impl From<serde_json::Error> for CustomError {
fn from(value: serde_json::Error) -> Self {
CustomError::Serde(value)
}
}
impl From<Utf8Error> for CustomError {
fn from(value: Utf8Error) -> Self {
CustomError::Utf8(value)
}
}
impl From<FromUtf8Error> for CustomError {
fn from(value: FromUtf8Error) -> Self {
CustomError::FromUtf8(value)
}
}
impl From<duster::renderer::CompileError> for CustomError {
fn from(value: duster::renderer::CompileError) -> Self {
CustomError::DusterCompile(value)
}
}
impl From<duster::renderer::RenderError> for CustomError {
fn from(value: duster::renderer::RenderError) -> Self {
CustomError::DusterRender(value)
}
}

View File

@ -12,6 +12,7 @@ mod cli;
mod command; mod command;
mod config; mod config;
mod error; mod error;
mod render;
fn main() -> Result<ExitCode, CustomError> { fn main() -> Result<ExitCode, CustomError> {
let rt = tokio::runtime::Runtime::new()?; let rt = tokio::runtime::Runtime::new()?;

View File

@ -0,0 +1,42 @@
use std::collections::HashMap;
use crate::error::CustomError;
use super::renderer_integration::RendererIntegration;
use serde::Serialize;
pub(crate) struct DusterRenderer<'a> {
templates: HashMap<&'a str, duster::parser::Template<'a>>,
}
impl<'a> DusterRenderer<'a> {
pub(crate) fn new() -> DusterRenderer<'a> {
DusterRenderer {
templates: HashMap::new(),
}
}
}
impl<'a> RendererIntegration<'a> for DusterRenderer<'a> {
fn load_template(&mut self, name: &'a str, contents: &'a str) -> Result<(), CustomError> {
let compiled_template = duster::renderer::compile_template(contents.as_ref())?;
self.templates.insert(name, compiled_template);
Ok(())
}
fn render<C>(&self, context: C) -> Result<String, CustomError>
where
C: Serialize,
{
let mut dust_renderer = duster::renderer::DustRenderer::new();
for (name, compiled_template) in self.templates.iter() {
dust_renderer.load_source(compiled_template, (*name).to_owned());
}
// TODO: This is horribly inefficient. I am converting from a serialize type to json and back again so I can use the existing implementation of IntoContextElement. Honestly, I probably need to rework a lot of duster now that I've improved in rust over the years.
let json_context = serde_json::to_string(&context)?;
println!("Context: {}", json_context);
let parsed_context: serde_json::Value = serde_json::from_str(json_context.as_str())?;
let rendered_output = dust_renderer.render("main", Some(&parsed_context))?;
Ok(rendered_output)
}
}

4
src/render/mod.rs Normal file
View File

@ -0,0 +1,4 @@
mod duster_renderer;
mod renderer_integration;
pub(crate) use duster_renderer::DusterRenderer;
pub(crate) use renderer_integration::RendererIntegration;

View File

@ -0,0 +1,11 @@
use serde::Serialize;
use crate::error::CustomError;
pub(crate) trait RendererIntegration<'a> {
fn load_template(&mut self, name: &'a str, contents: &'a str) -> Result<(), CustomError>;
fn render<C>(&self, context: C) -> Result<String, CustomError>
where
C: Serialize;
}