Rust library to generate static websites
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: rewrite assets system

+107 -52
+7
Cargo.lock
··· 164 164 checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 165 165 166 166 [[package]] 167 + name = "dyn-eq" 168 + version = "0.1.3" 169 + source = "registry+https://github.com/rust-lang/crates.io-index" 170 + checksum = "5c2d035d21af5cde1a6f5c7b444a5bf963520a9f142e5d06931178433d7d5388" 171 + 172 + [[package]] 167 173 name = "either" 168 174 version = "1.13.0" 169 175 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 289 295 dependencies = [ 290 296 "chrono", 291 297 "colored", 298 + "dyn-eq", 292 299 "env_logger", 293 300 "log", 294 301 "maud",
+1
crates/framework/Cargo.toml
··· 14 14 colored = "2.2.0" 15 15 rayon = "1.10.0" 16 16 rustc-hash = "2.1" 17 + dyn-eq = "0.1.3"
+43 -32
crates/framework/src/assets.rs
··· 1 - use std::fmt::Display; 2 - use std::fs::{self}; 3 - use std::path::PathBuf; 1 + use dyn_eq::DynEq; 2 + use std::hash::Hash; 3 + use std::{collections::HashSet, fs, path::PathBuf}; 4 + 5 + #[derive(Default)] 6 + pub struct PageAssets(pub(crate) HashSet<Box<dyn Asset>>); 7 + 8 + impl PageAssets { 9 + pub fn add_image(&mut self, image_path: PathBuf) -> Image { 10 + let image = Box::new(Image { path: image_path }); 11 + 12 + self.0.insert(image.clone()); 4 13 5 - pub struct Asset { 6 - file_path: PathBuf, 7 - final_url: String, 14 + *image 15 + } 16 + } 17 + pub trait Asset: DynEq { 18 + fn url(&self) -> Option<String>; 19 + fn path(&self) -> &PathBuf; 20 + 21 + fn process(&self); 22 + fn hash(&self) -> [u8; 8]; 8 23 } 9 24 10 - trait GenericAsset { 11 - fn load(file_path: PathBuf) -> Self; 25 + impl Hash for dyn Asset { 26 + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { 27 + self.hash().hash(state); 28 + } 12 29 } 13 30 14 - impl GenericAsset for Asset { 15 - fn load(file_path: PathBuf) -> Self { 16 - let canonicalized = file_path.canonicalize().unwrap(); 17 - let file_name = canonicalized.file_name().unwrap().to_string_lossy(); 31 + dyn_eq::eq_trait_object!(Asset); 18 32 19 - Asset { 20 - file_path: canonicalized.clone(), 21 - final_url: format!("/_assets/{}", file_name), 22 - } 23 - } 33 + #[derive(Clone, PartialEq, Eq, Hash)] 34 + #[non_exhaustive] 35 + pub struct Image { 36 + pub path: PathBuf, 24 37 } 25 38 26 - impl Asset { 27 - pub fn new(file_path: PathBuf) -> Self { 28 - let asset = Asset::load(file_path); 29 - asset.finalize(); 39 + impl Asset for Image { 40 + fn url(&self) -> Option<String> { 41 + let file_name = self.path.file_name().unwrap().to_str().unwrap(); 42 + 43 + format!("/_assets/{}", file_name).into() 44 + } 30 45 31 - asset 46 + fn path(&self) -> &PathBuf { 47 + &self.path 32 48 } 33 49 34 - fn finalize(&self) { 50 + fn process(&self) { 35 51 fs::copy( 36 - &self.file_path, 37 - format!( 38 - "dist/_assets/{}", 39 - self.file_path.file_name().unwrap().to_string_lossy() 40 - ), 52 + &self.path, 53 + "dist/_assets/".to_string() + self.path.file_name().unwrap().to_str().unwrap(), 41 54 ) 42 55 .unwrap(); 43 56 } 44 - } 45 57 46 - impl Display for Asset { 47 - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 48 - write!(f, "{}", self.final_url) 58 + fn hash(&self) -> [u8; 8] { 59 + [0; 8] 49 60 } 50 61 }
+34 -11
crates/framework/src/lib.rs
··· 6 6 mod logging; 7 7 8 8 use std::{ 9 - collections::HashMap, 9 + collections::{HashMap, HashSet}, 10 10 fs::{self, File}, 11 11 io::{self, Write}, 12 12 path::{Path, PathBuf}, ··· 14 14 time::SystemTime, 15 15 }; 16 16 17 + use assets::Asset; 17 18 use colored::{ColoredString, Colorize}; 18 19 use env_logger::{Builder, Env}; 19 20 ··· 76 77 ..Default::default() 77 78 }; 78 79 80 + let mut build_pages_assets: HashSet<Box<dyn Asset>> = HashSet::new(); 81 + 79 82 for route in &router.routes { 80 83 let routes = route.routes(); 81 84 match routes.is_empty() { 82 85 true => { 83 86 let route_start = SystemTime::now(); 84 - let ctx = RouteContext { 87 + let mut page_assets = assets::PageAssets::default(); 88 + let mut ctx = RouteContext { 85 89 params: page::RouteParams(HashMap::new()), 90 + assets: &mut page_assets, 86 91 }; 87 92 88 - let (file_path, file) = create_route_file(&**route, ctx.params.clone())?; 89 - render_route(file, &**route, &ctx)?; 93 + let (file_path, file) = create_route_file(&**route, &ctx.params)?; 94 + render_route(file, &**route, &mut ctx)?; 90 95 91 96 let formatted_elasped_time = 92 97 format_elapsed_time(route_start.elapsed(), &route_format_options)?; 93 - info!(target: "build", "{} -> {} {}", route.route(ctx.params), file_path.to_string_lossy().dimmed(), formatted_elasped_time); 98 + info!(target: "build", "{} -> {} {}", route.route(&ctx.params), file_path.to_string_lossy().dimmed(), formatted_elasped_time); 99 + 100 + build_pages_assets.extend(page_assets.0); 94 101 } 95 102 false => { 96 103 info!(target: "build", "{}", route.route_raw().to_string().bold()); 97 104 105 + // Reuse the same PageAssets HashSet for all the routes of a dynamic page 106 + let mut pages_assets = assets::PageAssets::default(); 107 + 98 108 routes.into_iter().for_each(|params| { 99 109 let route_start = SystemTime::now(); 100 - let ctx = RouteContext { params }; 110 + let mut ctx = RouteContext { params, assets: &mut pages_assets }; 101 111 102 - let (file_path, file) = create_route_file(&**route, ctx.params.clone()).unwrap(); 103 - render_route(file, &**route, &ctx).unwrap(); 112 + let (file_path, file) = create_route_file(&**route, &ctx.params).unwrap(); 113 + render_route(file, &**route, &mut ctx).unwrap(); 104 114 105 115 let formatted_elasped_time = format_elapsed_time(route_start.elapsed(), &route_format_options).unwrap(); 106 116 info!(target: "build", "├─ {} {}", file_path.to_string_lossy().dimmed(), formatted_elasped_time); 107 117 }); 118 + 119 + build_pages_assets.extend(pages_assets.0); 108 120 } 109 121 } 110 122 } ··· 113 125 format_elapsed_time(pages_start.elapsed(), &section_format_options)?; 114 126 info!(target: "build", "{}", format!("Pages generated in {}", formatted_elasped_time).bold()); 115 127 128 + let assets_start = SystemTime::now(); 129 + info!(target: "SKIP_FORMAT", "{}", format!(" generating {} assets ", build_pages_assets.len()).on_green().bold()); 130 + 131 + build_pages_assets.iter().for_each(|asset| { 132 + asset.process(); 133 + }); 134 + 135 + let formatted_elasped_time = 136 + format_elapsed_time(assets_start.elapsed(), &section_format_options)?; 137 + info!(target: "build", "{}", format!("Assets generated in {}", formatted_elasped_time).bold()); 138 + 116 139 // Check if static directory exists 117 140 if PathBuf::from_str("./static").unwrap().exists() { 118 141 let assets_start = SystemTime::now(); ··· 149 172 150 173 fn create_route_file( 151 174 route: &dyn page::FullPage, 152 - params: RouteParams, 175 + params: &RouteParams, 153 176 ) -> Result<(PathBuf, File), Box<dyn std::error::Error>> { 154 177 let file_path = PathBuf::from_str("./dist/") 155 178 .unwrap() 156 - .join(route.file_path(params.clone())); 179 + .join(route.file_path(params)); 157 180 158 181 // Create the parent directories if it doesn't exist 159 182 let parent_dir = Path::new(file_path.parent().unwrap()); ··· 168 191 fn render_route( 169 192 mut file: File, 170 193 route: &dyn page::FullPage, 171 - ctx: &RouteContext, 194 + ctx: &mut RouteContext, 172 195 ) -> Result<(), Box<dyn std::error::Error>> { 173 196 let rendered = route.render(ctx); 174 197 match rendered {
+7 -4
crates/framework/src/page.rs
··· 1 1 use std::{collections::HashMap, path::PathBuf}; 2 2 3 + use crate::assets::PageAssets; 4 + 3 5 pub enum RenderResult<T = maud::Markup> { 4 6 Html(T), 5 7 Text(String), 6 8 } 7 9 8 - pub struct RouteContext { 10 + pub struct RouteContext<'a> { 9 11 pub params: RouteParams, 12 + pub assets: &'a mut PageAssets, 10 13 } 11 14 12 15 pub trait Page { 13 - fn render(&self, ctx: &RouteContext) -> RenderResult; 16 + fn render(&self, ctx: &mut RouteContext) -> RenderResult; 14 17 } 15 18 16 19 #[derive(Clone, Default, Debug)] ··· 38 41 39 42 pub trait InternalPage { 40 43 fn route_raw(&self) -> String; 41 - fn route(&self, params: RouteParams) -> String; 42 - fn file_path(&self, params: RouteParams) -> PathBuf; 44 + fn route(&self, params: &RouteParams) -> String; 45 + fn file_path(&self, params: &RouteParams) -> PathBuf; 43 46 } 44 47 45 48 pub trait FullPage: Page + InternalPage + DynamicPage + Sync {}
+2 -2
crates/macros/src/lib.rs
··· 84 84 #path.to_string() 85 85 } 86 86 87 - fn route(&self, params: maudit::page::RouteParams) -> String { 87 + fn route(&self, params: &maudit::page::RouteParams) -> String { 88 88 #(#list_params;)* 89 89 return format!(#path_for_route); 90 90 } 91 91 92 - fn file_path(&self, params: maudit::page::RouteParams) -> std::path::PathBuf { 92 + fn file_path(&self, params: &maudit::page::RouteParams) -> std::path::PathBuf { 93 93 // List params in the shape of let id = ctx.params.get("id").unwrap().to_string(); 94 94 #(#list_params;)* 95 95 std::path::PathBuf::from(format!(#file_path_for_route))
+1
crates/user-example/data/logo.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="357.3" height="281" fill="none"><path fill="#0d0d0d" d="M303 267c-51-6-51-6-83-5h-32l-63 7a1419 1419 0 0 0-92 12L22 173 12 66l-3-1C3 63-1 55 0 46c0-5 1-6 6-9 5-4 5-4 11-3 5 0 7 1 9 3 3 3 3 3 4 12 0 8 0 10-2 12l-2 2 15 19c22 27 58 67 59 66l4-15a1924 1924 0 0 1 13-64l-4-8c-3-3-7-11-9-16l-3-10 6-7c6-6 6-7 13-8 9-3 13-1 18 5 5 8 5 11 1 23l-4 12 7 21c7 19 7 19 27 52 20 32 25 39 26 36l3-14 14-60 10-48c0-2-2-3-5-6l-5-7 1-6c1-6 2-7 7-11 4-2 6-3 10-2 6 0 8 1 12 7 4 7 5 15 3 19l-6 5-5 3c0 5 48 117 49 117 4 0 5-5 18-63l13-59-6-4-5-3-1-10-1-10 6-8 6-8 8-1c8 0 8 0 13 3s6 4 7 11c2 8 2 15 0 18l-8 6-6 4 8 34 8 34 8 81c7 80 7 81 5 81z"/></svg>
+9 -2
crates/user-example/src/pages/endpoint.rs
··· 5 5 pub struct Endpoint; 6 6 7 7 impl Page for Endpoint { 8 - fn render(&self, _context: &RouteContext) -> RenderResult { 8 + fn render(&self, ctx: &mut RouteContext) -> RenderResult { 9 + let image = ctx.assets.add_image("data/logo.svg".into()); 10 + 9 11 // Return some JSON 10 - RenderResult::Text(r#"{"message": "Hello, world!"}"#.to_string()) 12 + RenderResult::Text(format!( 13 + r#"{{ 14 + "image": "{}" 15 + }}"#, 16 + image.path.to_string_lossy() 17 + )) 11 18 } 12 19 }
+3 -1
crates/user-example/src/pages/index.rs
··· 23 23 } 24 24 25 25 impl Page for Index { 26 - fn render(&self, ctx: &RouteContext) -> RenderResult { 26 + fn render(&self, ctx: &mut RouteContext) -> RenderResult { 27 27 let params = ctx.params.parse_into::<Params>(); 28 + let image = ctx.assets.add_image("data/social-card.png".into()); 28 29 29 30 RenderResult::Html(html! { 30 31 h1 { "Hello, world!" } 32 + img src=(image.path.to_string_lossy()) {} 31 33 p { (params.page) } 32 34 }) 33 35 }