Rust library to generate static websites
5
fork

Configure Feed

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

perf: try out caching more things (#47)

* perf: try out caching more things

* fix: trying some things

* fix: make it simpler

* fix: comment

authored by

Erika and committed by
GitHub
19e4629d 44f74f01

+291 -225
-7
Cargo.lock
··· 1310 1310 checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" 1311 1311 1312 1312 [[package]] 1313 - name = "dyn-eq" 1314 - version = "0.1.3" 1315 - source = "registry+https://github.com/rust-lang/crates.io-index" 1316 - checksum = "5c2d035d21af5cde1a6f5c7b444a5bf963520a9f142e5d06931178433d7d5388" 1317 - 1318 - [[package]] 1319 1313 name = "either" 1320 1314 version = "1.15.0" 1321 1315 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2406 2400 "brk_rolldown", 2407 2401 "chrono", 2408 2402 "colored", 2409 - "dyn-eq", 2410 2403 "env_logger", 2411 2404 "glob", 2412 2405 "image",
-1
crates/maudit/Cargo.toml
··· 42 42 chrono = "0.4.39" 43 43 colored = "2.2.0" 44 44 rustc-hash = "2.1" 45 - dyn-eq = "0.1.3" 46 45 thiserror = "2.0.9" 47 46 oxc_sourcemap = "4.1.0" 48 47 rayon = "1.11.0"
+70 -46
crates/maudit/src/assets.rs
··· 1 - use dyn_eq::DynEq; 2 1 use log::debug; 3 2 use rustc_hash::FxHashSet; 4 - use std::hash::Hash; 5 3 use std::path::Path; 6 4 use std::time::Instant; 7 5 use std::{fs, path::PathBuf}; ··· 77 75 /// Add an image to the page assets, causing the file to be created in the output directory. The image is resolved relative to the current working directory. 78 76 /// 79 77 /// The image will not automatically be included in the page, but can be included through the `.url()` method on the returned `Image` object. 80 - /// 81 - /// Subsequent calls to this function using the same path will return the same image, as such, the value returned by this function can be cloned and used multiple times without issue. 82 78 pub fn add_image_with_options<P>(&mut self, image_path: P, options: ImageOptions) -> Image 83 79 where 84 80 P: Into<PathBuf>, 85 81 { 86 82 let image_path = image_path.into(); 87 83 88 - // Check if the image already exists in the assets, if so, return it 89 - if let Some(image) = self.images.iter().find_map(|asset| { 90 - asset.as_any().downcast_ref::<Image>().filter(|image| { 91 - image.path == image_path 92 - && options == *image.options.as_ref().unwrap_or(&ImageOptions::default()) 93 - }) 94 - }) { 95 - return image.clone(); 96 - } 84 + let image_options = if options == ImageOptions::default() { 85 + None 86 + } else { 87 + Some(options) 88 + }; 97 89 98 - let image = Image::new( 99 - image_path, 100 - if options == ImageOptions::default() { 101 - None 102 - } else { 103 - Some(options) 104 - }, 105 - &self.options, 90 + let hash = calculate_hash( 91 + &image_path, 92 + Some(&HashConfig { 93 + asset_type: HashAssetType::Image( 94 + image_options.as_ref().unwrap_or(&ImageOptions::default()), 95 + ), 96 + hashing_strategy: &self.options.hashing_strategy, 97 + }), 106 98 ); 99 + 100 + let image = Image::new(image_path, image_options, hash, &self.options); 107 101 108 102 self.images.insert(image.clone()); 109 103 ··· 121 115 /// 122 116 /// The script will not automatically be included in the page, but can be included through the `.url()` method on the returned `Script` object. 123 117 /// Alternatively, a script can be included automatically using the [RouteAssets::include_script] method instead. 124 - /// 125 - /// Subsequent calls to this function using the same path will return the same script, as such, the value returned by this function can be cloned and used multiple times without issue. 126 118 pub fn add_script<P>(&mut self, script_path: P) -> Script 127 119 where 128 120 P: Into<PathBuf>, 129 121 { 130 - let script = Script::new(script_path.into(), false, &self.options); 122 + let script_path = script_path.into(); 123 + 124 + let hash = calculate_hash( 125 + &script_path, 126 + Some(&HashConfig { 127 + asset_type: HashAssetType::Script, 128 + hashing_strategy: &self.options.hashing_strategy, 129 + }), 130 + ); 131 + 132 + let script = Script::new(script_path, false, hash, &self.options); 131 133 132 134 self.scripts.insert(script.clone()); 133 135 ··· 143 145 where 144 146 P: Into<PathBuf>, 145 147 { 146 - let script = Script::new(script_path.into(), true, &self.options); 148 + let script_path = script_path.into(); 149 + 150 + let hash = calculate_hash( 151 + &script_path, 152 + Some(&HashConfig { 153 + asset_type: HashAssetType::Script, 154 + hashing_strategy: &self.options.hashing_strategy, 155 + }), 156 + ); 157 + 158 + let script = Script::new(script_path, true, hash, &self.options); 147 159 148 160 self.scripts.insert(script); 149 161 } ··· 153 165 /// The style will not automatically be included in the page, but can be included through the `.url()` method on the returned `Style` object. 154 166 /// Alternatively, a style can be included automatically using the [RouteAssets::include_style] method instead. 155 167 /// 156 - /// Subsequent calls to this method using the same path will return the same style, as such, the value returned by this method can be cloned and used multiple times without issue. this method is equivalent to calling `add_style_with_options` with the default `StyleOptions` and is purely provided for convenience. 168 + /// Subsequent calls to this method using the same path will return the same style, as such, the value returned by this method can be cloned and used multiple times without issue. This method is equivalent to calling `add_style_with_options` with the default `StyleOptions` and is purely provided for convenience. 157 169 pub fn add_style<P>(&mut self, style_path: P) -> Style 158 170 where 159 171 P: Into<PathBuf>, ··· 170 182 where 171 183 P: Into<PathBuf>, 172 184 { 173 - let style = Style::new(style_path.into(), false, &options, &self.options); 185 + let style_path = style_path.into(); 186 + 187 + let hash = calculate_hash( 188 + &style_path, 189 + Some(&HashConfig { 190 + asset_type: HashAssetType::Style(&options), 191 + hashing_strategy: &self.options.hashing_strategy, 192 + }), 193 + ); 194 + 195 + let style = Style::new(style_path, false, &options, hash, &self.options); 174 196 175 197 self.styles.insert(style.clone()); 176 198 ··· 198 220 where 199 221 P: Into<PathBuf>, 200 222 { 201 - let style = Style::new(style_path.into(), true, &options, &self.options); 223 + let style_path = style_path.into(); 224 + 225 + let hash = calculate_hash( 226 + &style_path, 227 + Some(&HashConfig { 228 + asset_type: HashAssetType::Style(&options), 229 + hashing_strategy: &self.options.hashing_strategy, 230 + }), 231 + ); 232 + 233 + let style = Style::new(style_path, true, &options, hash, &self.options); 202 234 203 235 self.styles.insert(style); 204 236 } 205 237 } 206 238 207 - pub trait Asset: DynEq + Sync + Send { 239 + pub trait Asset: Sync + Send { 208 240 fn build_path(&self) -> &PathBuf; 209 - fn url(&self) -> Option<&String>; 241 + fn url(&self) -> &String; 210 242 fn path(&self) -> &PathBuf; 211 243 fn filename(&self) -> &PathBuf; 212 244 } ··· 226 258 &self.build_path 227 259 } 228 260 229 - fn url(&self) -> Option<&String> { 230 - Some(&self.url) 261 + fn url(&self) -> &String { 262 + &self.url 231 263 } 232 264 } 233 265 }; ··· 332 364 hex[..5].to_string() 333 365 } 334 366 335 - impl Hash for dyn Asset { 336 - fn hash<H: std::hash::Hasher>(&self, state: &mut H) { 337 - self.path().hash(state); 338 - } 339 - } 340 - 341 - dyn_eq::eq_trait_object!(Asset); 342 - 343 367 #[cfg(test)] 344 368 mod tests { 345 369 use super::*; ··· 411 435 let mut page_assets = RouteAssets::default(); 412 436 413 437 let image = page_assets.add_image(temp_dir.join("image.png")); 414 - assert_eq!(image.url().unwrap().chars().next(), Some('/')); 438 + assert_eq!(image.url().chars().next(), Some('/')); 415 439 416 440 let script = page_assets.add_script(temp_dir.join("script.js")); 417 - assert_eq!(script.url().unwrap().chars().next(), Some('/')); 441 + assert_eq!(script.url().chars().next(), Some('/')); 418 442 419 443 let style = page_assets.add_style(temp_dir.join("style.css")); 420 - assert_eq!(style.url().unwrap().chars().next(), Some('/')); 444 + assert_eq!(style.url().chars().next(), Some('/')); 421 445 } 422 446 423 447 #[test] ··· 426 450 let mut page_assets = RouteAssets::default(); 427 451 428 452 let image = page_assets.add_image(temp_dir.join("image.png")); 429 - assert!(image.url().unwrap().contains(&image.hash)); 453 + assert!(image.url().contains(&image.hash)); 430 454 431 455 let script = page_assets.add_script(temp_dir.join("script.js")); 432 - assert!(script.url().unwrap().contains(&script.hash)); 456 + assert!(script.url().contains(&script.hash)); 433 457 434 458 let style = page_assets.add_style(temp_dir.join("style.css")); 435 - assert!(style.url().unwrap().contains(&style.hash)); 459 + assert!(style.url().contains(&style.hash)); 436 460 } 437 461 438 462 #[test]
+2 -14
crates/maudit/src/assets/image.rs
··· 7 7 use thumbhash::{rgba_to_thumb_hash, thumb_hash_to_average_rgba, thumb_hash_to_rgba}; 8 8 9 9 use super::image_cache::ImageCache; 10 - use crate::assets::{ 11 - HashAssetType, HashConfig, RouteAssetsOptions, calculate_hash, make_filename, make_final_path, 12 - make_final_url, 13 - }; 10 + use crate::assets::{RouteAssetsOptions, make_filename, make_final_path, make_final_url}; 14 11 use crate::is_dev; 15 12 16 13 #[derive(Clone, Debug, PartialEq, Eq, Hash)] ··· 78 75 pub fn new( 79 76 path: PathBuf, 80 77 image_options: Option<ImageOptions>, 78 + hash: String, 81 79 route_assets_options: &RouteAssetsOptions, 82 80 ) -> Self { 83 - let hash = calculate_hash( 84 - &path, 85 - Some(&HashConfig { 86 - asset_type: HashAssetType::Image( 87 - image_options.as_ref().unwrap_or(&ImageOptions::default()), 88 - ), 89 - hashing_strategy: &route_assets_options.hashing_strategy, 90 - }), 91 - ); 92 - 93 81 let filename = make_filename( 94 82 &path, 95 83 &hash,
+7 -13
crates/maudit/src/assets/script.rs
··· 1 1 use std::path::PathBuf; 2 2 3 - use crate::assets::{ 4 - HashAssetType, HashConfig, RouteAssetsOptions, calculate_hash, make_filename, make_final_path, 5 - make_final_url, 6 - }; 3 + use crate::assets::{RouteAssetsOptions, make_filename, make_final_path, make_final_url}; 7 4 8 5 #[derive(Clone, PartialEq, Eq, Hash)] 9 6 pub struct Script { ··· 17 14 } 18 15 19 16 impl Script { 20 - pub fn new(path: PathBuf, included: bool, route_assets_options: &RouteAssetsOptions) -> Self { 21 - let hash = calculate_hash( 22 - &path, 23 - Some(&HashConfig { 24 - asset_type: HashAssetType::Script, 25 - hashing_strategy: &route_assets_options.hashing_strategy, 26 - }), 27 - ); 28 - 17 + pub fn new( 18 + path: PathBuf, 19 + included: bool, 20 + hash: String, 21 + route_assets_options: &RouteAssetsOptions, 22 + ) -> Self { 29 23 let filename = make_filename(&path, &hash, Some("js")); 30 24 let build_path = make_final_path(&route_assets_options.output_assets_dir, &filename); 31 25 let url = make_final_url(&route_assets_options.assets_dir, &filename);
+2 -12
crates/maudit/src/assets/style.rs
··· 1 1 use std::path::PathBuf; 2 2 3 - use crate::assets::{ 4 - HashAssetType, HashConfig, RouteAssetsOptions, calculate_hash, make_filename, make_final_path, 5 - make_final_url, 6 - }; 3 + use crate::assets::{RouteAssetsOptions, make_filename, make_final_path, make_final_url}; 7 4 8 5 #[derive(Clone, PartialEq, Eq, Hash, Default)] 9 6 pub struct StyleOptions { ··· 27 24 path: PathBuf, 28 25 included: bool, 29 26 style_options: &StyleOptions, 27 + hash: String, 30 28 route_assets_options: &RouteAssetsOptions, 31 29 ) -> Self { 32 - let hash = calculate_hash( 33 - &path, 34 - Some(&HashConfig { 35 - asset_type: HashAssetType::Style(style_options), 36 - hashing_strategy: &route_assets_options.hashing_strategy, 37 - }), 38 - ); 39 - 40 30 let filename = make_filename(&path, &hash, Some("css")); 41 31 let build_path = make_final_path(&route_assets_options.output_assets_dir, &filename); 42 32 let url = make_final_url(&route_assets_options.assets_dir, &filename);
+27 -23
crates/maudit/src/build.rs
··· 18 18 content::{ContentSources, RouteContent}, 19 19 is_dev, 20 20 logging::print_title, 21 - route::{DynamicRouteContext, FullRoute, PageContext, PageParams, RouteType}, 21 + route::{ 22 + CachedRoute, DynamicRouteContext, FullRoute, InternalRoute, PageContext, PageParams, 23 + RouteType, 24 + }, 22 25 }; 23 26 use colored::{ColoredString, Colorize}; 24 27 use log::{debug, info, trace, warn}; ··· 53 56 // Create a directory for the output 54 57 trace!(target: "build", "Setting up required directories..."); 55 58 56 - let old_dist_tmp_dir = if options.clean_output_dir { 57 - let duration = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); 58 - let num = (duration.as_secs() + duration.subsec_nanos() as u64) % 100000; 59 - let new_dir_for_old_dist = env::temp_dir().join(format!("maudit_old_dist_{}", num)); 60 - let _ = fs::rename(&options.output_dir, &new_dir_for_old_dist); 61 - Some(new_dir_for_old_dist) 59 + let clean_up_handle = if options.clean_output_dir { 60 + let old_dist_tmp_dir = { 61 + let duration = SystemTime::now().duration_since(UNIX_EPOCH)?; 62 + let num = (duration.as_secs() + duration.subsec_nanos() as u64) % 100000; 63 + let new_dir_for_old_dist = env::temp_dir().join(format!("maudit_old_dist_{}", num)); 64 + let _ = fs::rename(&options.output_dir, &new_dir_for_old_dist); 65 + new_dir_for_old_dist 66 + }; 67 + 68 + Some(tokio::spawn(async { 69 + let _ = fs::remove_dir_all(old_dist_tmp_dir); 70 + })) 62 71 } else { 63 72 None 64 73 }; 65 74 66 - let should_clear_dist = options.clean_output_dir; 67 - let clean_up_handle = tokio::spawn(async move { 68 - if should_clear_dist { 69 - let _ = fs::remove_dir_all(old_dist_tmp_dir.unwrap()); 70 - } 71 - }); 72 - 73 75 let route_assets_options = options.route_assets_options(); 74 76 75 77 info!(target: "build", "Output directory: {}", options.output_dir.display()); ··· 118 120 let mut page_count = 0; 119 121 120 122 // This is fully serial. It is somewhat trivial to make it parallel, but it currently isn't because every time I've tried to 121 - // (uncommited, #25 and #41) it either made no difference or was slower. The overhead of parallelism is just too high for 123 + // (uncommited, #25, #41, #46) it either made no difference or was slower. The overhead of parallelism is just too high for 122 124 // how fast most sites build. Ideally, it'd be configurable and default to serial, but I haven't found an ergonomic way to do that yet. 123 125 // If you manage to make it parallel and it actually improves performance, please open a PR! 124 126 for route in routes { 125 - match route.route_type() { 127 + let cached_route = CachedRoute::new(*route); 128 + 129 + match cached_route.route_type() { 126 130 RouteType::Static => { 127 131 let route_start = Instant::now(); 128 132 ··· 130 134 let mut page_assets = RouteAssets::new(&route_assets_options); 131 135 132 136 let params = PageParams::default(); 133 - let url = route.url(&params); 137 + let url = cached_route.url(&params); 134 138 135 139 let result = route.build(&mut PageContext::from_static_route( 136 140 &content, ··· 139 143 &options.base_url, 140 144 ))?; 141 145 142 - let file_path = route.file_path(&params, &options.output_dir); 146 + let file_path = cached_route.file_path(&params, &options.output_dir); 143 147 144 148 write_route_file(&result, &file_path)?; 145 149 ··· 176 180 for page in pages { 177 181 let route_start = Instant::now(); 178 182 179 - let url = route.url(&page.0); 183 + let url = cached_route.url(&page.0); 180 184 181 185 let content = route.build(&mut PageContext::from_dynamic_route( 182 186 &page, ··· 186 190 &options.base_url, 187 191 ))?; 188 192 189 - let file_path = route.file_path(&page.0, &options.output_dir); 193 + let file_path = cached_route.file_path(&page.0, &options.output_dir); 190 194 191 195 write_route_file(&content, &file_path)?; 192 196 ··· 327 331 debug!("Failed to copy from cache {} to dest {}", cache_path.display(), dest_path.display()); 328 332 } 329 333 } else if !dest_path.exists() { 330 - // TODO: Check if copying should be done in this parallel iterator, I/O doesn't benefit from parallelism so having those tasks here might just be slowing processing 331 334 fs::copy(image.path(), dest_path).unwrap_or_else(|e| { 332 335 panic!( 333 336 "Failed to copy image from {} to {}: {}", ··· 348 351 let assets_start = Instant::now(); 349 352 print_title("copying assets"); 350 353 351 - // Copy the static directory to the dist directory 352 354 copy_recursively( 353 355 &options.static_dir, 354 356 &options.output_dir, ··· 361 363 info!(target: "SKIP_FORMAT", "{}", ""); 362 364 info!(target: "build", "{}", format!("Build completed in {}", format_elapsed_time(build_start.elapsed(), &section_format_options)).bold()); 363 365 364 - clean_up_handle.await.unwrap(); 366 + if let Some(clean_up_handle) = clean_up_handle { 367 + clean_up_handle.await?; 368 + } 365 369 366 370 Ok(build_metadata) 367 371 }
+1 -1
crates/maudit/src/content/markdown.rs
··· 418 418 let resolved = parent.join(dest_url.to_string()); 419 419 route_ctx 420 420 .as_mut() 421 - .and_then(|ctx| ctx.assets.add_image(resolved).url().cloned()) 421 + .map(|ctx| ctx.assets.add_image(resolved).url().clone()) 422 422 }) 423 423 .map(|image_url| { 424 424 Event::Start(Tag::Image {
+1 -2
crates/maudit/src/lib.rs
··· 210 210 211 211 let async_runtime = tokio::runtime::Builder::new_multi_thread() 212 212 .enable_all() 213 - .build() 214 - .unwrap(); 213 + .build()?; 215 214 216 215 execute_build(routes, &mut content_sources, &options, &async_runtime) 217 216 }
+135 -94
crates/maudit/src/route.rs
··· 473 473 } 474 474 475 475 fn url(&self, params: &PageParams) -> String { 476 - let mut params_def = extract_params_from_raw_route(&self.route_raw()); 477 - 478 - // Replace every param_def with the value from the params hashmap for said key 479 - // So, ex: "/articles/[article]" (params: Hashmap {article: "truc"}) -> "/articles/truc" 480 - let mut route = self.route_raw(); 481 - 482 - // Sort params by index in reverse order to avoid index shifting issues 483 - params_def.sort_by(|a, b| b.index.cmp(&a.index)); 484 - 485 - for param_def in params_def { 486 - let value = params.0.get(&param_def.key); 487 - 488 - match value { 489 - Some(value) => match value { 490 - Some(value) => { 491 - route.replace_range( 492 - param_def.index..param_def.index + param_def.length, 493 - value, 494 - ); 495 - } 496 - None => { 497 - route 498 - .replace_range(param_def.index..param_def.index + param_def.length, ""); 499 - } 500 - }, 501 - None => { 502 - panic!( 503 - "Route {:?} is missing parameter {:?}", 504 - self.route_raw(), 505 - param_def.key 506 - ); 507 - } 508 - } 509 - } 510 - 511 - // Collapse multiple slashes into single slashes 512 - route.replace("//", "/") 476 + let params_def = extract_params_from_raw_route(&self.route_raw()); 477 + build_url_with_params(&self.route_raw(), &params_def, params) 513 478 } 514 479 515 480 fn file_path(&self, params: &PageParams, output_dir: &Path) -> PathBuf { 516 481 let params_def = extract_params_from_raw_route(&self.route_raw()); 517 - let route_template = self.route_raw(); 518 - 519 - let mut sorted_params = params_def; 520 - sorted_params.sort_by_key(|p| p.index); 521 - 522 - let mut path = PathBuf::from(output_dir); 523 - let mut last_index = 0; 524 - let mut current_component = String::new(); 525 - 526 - for param_def in sorted_params.iter() { 527 - // Push everything before this param into current_component 528 - current_component.push_str(&route_template[last_index..param_def.index]); 529 - 530 - // Append param value if present 531 - let value = params.0.get(&param_def.key).unwrap_or_else(|| { 532 - panic!( 533 - "Route {:?} is missing parameter {:?}", 534 - route_template, param_def.key 535 - ) 536 - }); 537 - if let Some(v) = value { 538 - current_component.push_str(v); 539 - } 540 - 541 - last_index = param_def.index + param_def.length; 542 - } 543 - 544 - // Append remainder of the route 545 - current_component.push_str(&route_template[last_index..]); 546 - 547 - // Split by '/' and push non-empty components into the PathBuf 548 - for part in current_component.split('/').filter(|s| !s.is_empty()) { 549 - path.push(part); 550 - } 551 - 552 - // Handle endpoint vs. page 553 - if !self.is_endpoint() { 554 - path.push("index.html"); 555 - } 556 - 557 - path 482 + build_file_path_with_params( 483 + &self.route_raw(), 484 + &params_def, 485 + params, 486 + output_dir, 487 + self.is_endpoint(), 488 + ) 558 489 } 559 490 } 560 491 ··· 604 535 } 605 536 } 606 537 538 + use crate::routing::ParameterDef; 539 + use std::sync::OnceLock; 540 + 541 + // This function and the one below are extremely performance-sensitive, as they are called for every single page during the build. 542 + // It'd be great to optimize them as much as possible, make them allocation-free, etc. But, I'm not smart enough right now to do that! 543 + fn build_url_with_params( 544 + route_template: &str, 545 + params_def: &[ParameterDef], 546 + params: &PageParams, 547 + ) -> String { 548 + if params_def.is_empty() { 549 + return route_template.to_string(); 550 + } 551 + 552 + let mut result = route_template.to_string(); 553 + 554 + for param_def in params_def { 555 + let value = params.0.get(&param_def.key).unwrap_or_else(|| { 556 + panic!( 557 + "Route {:?} is missing parameter {:?}", 558 + route_template, param_def.key 559 + ) 560 + }); 561 + 562 + let replacement = value.as_deref().unwrap_or(""); 563 + result.replace_range( 564 + param_def.index..param_def.index + param_def.length, 565 + replacement, 566 + ); 567 + } 568 + 569 + result.replace("//", "/") 570 + } 571 + 572 + fn build_file_path_with_params( 573 + route_template: &str, 574 + params_def: &[ParameterDef], 575 + params: &PageParams, 576 + output_dir: &Path, 577 + is_endpoint: bool, 578 + ) -> PathBuf { 579 + // Build route string with parameters 580 + let mut route = route_template.to_string(); 581 + 582 + for param_def in params_def { 583 + let value = params.0.get(&param_def.key).unwrap_or_else(|| { 584 + panic!( 585 + "Route {:?} is missing parameter {:?}", 586 + route_template, param_def.key 587 + ) 588 + }); 589 + 590 + let replacement = value.as_deref().unwrap_or(""); 591 + route.replace_range( 592 + param_def.index..param_def.index + param_def.length, 593 + replacement, 594 + ); 595 + } 596 + 597 + // Build path from route string 598 + let mut path = PathBuf::from(output_dir); 599 + path.extend(route.split('/').filter(|s| !s.is_empty())); 600 + 601 + if !is_endpoint { 602 + path.push("index.html"); 603 + } 604 + 605 + path 606 + } 607 + 608 + /// Wrapper around a route that caches its parameter extraction and endpoint status to avoid redundant computations. 609 + pub struct CachedRoute<'a> { 610 + inner: &'a dyn FullRoute, 611 + params_cache: OnceLock<Vec<ParameterDef>>, 612 + is_endpoint: OnceLock<bool>, 613 + } 614 + 615 + impl<'a> CachedRoute<'a> { 616 + pub fn new(route: &'a dyn FullRoute) -> Self { 617 + Self { 618 + inner: route, 619 + params_cache: OnceLock::new(), 620 + is_endpoint: OnceLock::new(), 621 + } 622 + } 623 + 624 + fn get_cached_params(&self) -> &Vec<ParameterDef> { 625 + self.params_cache 626 + .get_or_init(|| extract_params_from_raw_route(&self.inner.route_raw())) 627 + } 628 + 629 + fn is_endpoint(&self) -> bool { 630 + *self 631 + .is_endpoint 632 + .get_or_init(|| guess_if_route_is_endpoint(&self.inner.route_raw())) 633 + } 634 + } 635 + 636 + impl<'a> InternalRoute for CachedRoute<'a> { 637 + fn route_raw(&self) -> String { 638 + self.inner.route_raw() 639 + } 640 + 641 + fn route_type(&self) -> RouteType { 642 + get_route_type_from_route_params(self.get_cached_params()) 643 + } 644 + 645 + fn url(&self, params: &PageParams) -> String { 646 + build_url_with_params(&self.route_raw(), self.get_cached_params(), params) 647 + } 648 + 649 + fn file_path(&self, params: &PageParams, output_dir: &Path) -> PathBuf { 650 + build_file_path_with_params( 651 + &self.route_raw(), 652 + self.get_cached_params(), 653 + params, 654 + output_dir, 655 + self.is_endpoint(), 656 + ) 657 + } 658 + } 659 + 607 660 pub fn finish_route( 608 661 render_result: RenderResult, 609 662 page_assets: &RouteAssets, ··· 625 678 element!("head", |el| { 626 679 for style in &included_styles { 627 680 el.append( 628 - &format!( 629 - "<link rel=\"stylesheet\" href=\"{}\">", 630 - style.url().unwrap_or_else(|| panic!( 631 - "Failed to get URL for style: {:?}. This should not happen, please report this issue", 632 - style.path() 633 - )) 634 - ), 681 + &format!("<link rel=\"stylesheet\" href=\"{}\">", style.url()), 635 682 lol_html::html_content::ContentType::Html, 636 683 ); 637 684 } 638 685 639 686 for script in &included_scripts { 640 687 el.append( 641 - &format!( 642 - "<script src=\"{}\" type=\"module\"></script>", 643 - script.url().unwrap_or_else(|| panic!( 644 - "Failed to get URL for script: {:?}. This should not happen, please report this issue.", 645 - script.path() 646 - )) 647 - ), 688 + &format!("<script src=\"{}\" type=\"module\"></script>", script.url()), 648 689 lol_html::html_content::ContentType::Html, 649 690 ); 650 691 } ··· 692 733 //! use maudit::route::prelude::*; 693 734 //! ``` 694 735 pub use super::{ 695 - DynamicRouteContext, FullRoute, Page, PageContext, PageParams, Pages, PaginatedContentPage, 696 - PaginationPage, RenderResult, Route, RouteExt, paginate, 736 + CachedRoute, DynamicRouteContext, FullRoute, Page, PageContext, PageParams, Pages, 737 + PaginatedContentPage, PaginationPage, RenderResult, Route, RouteExt, paginate, 697 738 }; 698 739 pub use crate::assets::{Asset, Image, ImageFormat, ImageOptions, Script, Style, StyleOptions}; 699 740 pub use crate::content::{
+9 -6
crates/maudit/src/routing.rs
··· 2 2 3 3 use crate::route::RouteType; 4 4 5 - #[derive(Debug, PartialEq)] 5 + #[derive(Debug, PartialEq, Clone)] 6 6 pub struct ParameterDef { 7 7 pub(crate) key: String, 8 8 pub(crate) index: usize, ··· 44 44 } 45 45 } 46 46 47 + // Sort by index in reverse order to avoid index shifting issues during replacement 48 + params.sort_by(|a, b| b.index.cmp(&a.index)); 49 + 47 50 params 48 51 } 49 52 ··· 85 88 let input = "/articles/[article]/[id]"; 86 89 let expected = vec![ 87 90 ParameterDef { 91 + key: "id".to_string(), 92 + index: 20, 93 + length: 4, 94 + }, 95 + ParameterDef { 88 96 key: "article".to_string(), 89 97 index: 10, 90 98 length: 9, 91 - }, 92 - ParameterDef { 93 - key: "id".to_string(), 94 - index: 20, 95 - length: 4, 96 99 }, 97 100 ]; 98 101
+3 -3
crates/maudit/src/templating/maud_ext.rs
··· 9 9 impl Render for Style { 10 10 fn render(&self) -> Markup { 11 11 html! { 12 - link rel="stylesheet" type="text/css" href=(self.url().unwrap()); 12 + link rel="stylesheet" type="text/css" href=(self.url()); 13 13 } 14 14 } 15 15 } ··· 17 17 impl Render for Script { 18 18 fn render(&self) -> Markup { 19 19 html! { 20 - script src=(self.url().unwrap()) type="module" {} 20 + script src=(self.url()) type="module" {} 21 21 } 22 22 } 23 23 } ··· 26 26 fn render(&self) -> Markup { 27 27 let (width, height) = self.dimensions(); 28 28 html! { 29 - img src=(self.url().unwrap()) width=(width) height=(height) loading="lazy" decoding="async"; 29 + img src=(self.url()) width=(width) height=(height) loading="lazy" decoding="async"; 30 30 } 31 31 } 32 32 }
+3 -3
examples/kitchen-sink/src/routes/index.rs
··· 20 20 html! { 21 21 head { 22 22 title { "Index" } 23 - link rel="stylesheet" href=(style.url().unwrap()) {} 23 + link rel="stylesheet" href=(style.url()) {} 24 24 } 25 25 h1 { "Index" } 26 - img src=(image.url().unwrap()) {} 27 - script src=(script.url().unwrap()) {} 26 + img src=(image.url()) {} 27 + script src=(script.url()) {} 28 28 a."text-red-500" href=(link_to_first_dynamic) { "Go to first dynamic page" } 29 29 } 30 30 }
+31
website/content/docs/content.md
··· 176 176 177 177 Either through loaders or by using the [`render_markdown`](https://docs.rs/maudit/latest/maudit/content/markdown/fn.render_markdown.html) function directly, Maudit supports rendering local and remote Markdown and enriching it with shortcodes and custom components. 178 178 179 + ### Syntax Highlighting 180 + 181 + Maudit uses [Syntect](https://github.com/trishume/syntect) to provide syntax highlighting for code blocks in Markdown at build time. No client-side JavaScript is used to provide syntax highlighting. Syntax highlighting can also be used outside of Markdown by using the `maudit::content::highlight_code` function. 182 + 183 + By default, the theme `base16-ocean.dark` is used, but you can customize this by providing a different theme name in the `highlight_theme` field of `MarkdownOptions`. Maudit includes all of Syntect's built-in themes: 184 + 185 + - `base16-ocean.dark`,`base16-eighties.dark`,`base16-mocha.dark`,`base16-ocean.light` 186 + - `InspiredGitHub` from [here](https://github.com/sethlopezme/InspiredGitHub.tmtheme) 187 + - `Solarized (dark)` and `Solarized (light)` 188 + 189 + ```rust 190 + use maudit::content::markdown::MarkdownOptions; 191 + 192 + fn main() { 193 + coronate( 194 + routes![ 195 + // ... 196 + ], 197 + content_sources![ 198 + "blog" => glob_markdown_with_options::<BlogPost>("content/blog/**/*.md", MarkdownOptions { 199 + highlight_theme: "InspiredGitHub".into(), 200 + ..Default::default() 201 + }), 202 + ], 203 + ..Default::default(), 204 + ); 205 + } 206 + ``` 207 + 208 + You may also provide your own custom theme by passing a path to a `.tmTheme` file in the `highlight_theme` field of `MarkdownOptions`. This path is relative to the current working directory when building the site. 209 + 179 210 ### Shortcodes 180 211 181 212 Shortcodes provide a way to extend Markdown with custom functionality. They serve a similar role to [components in MDX](https://mdxjs.com) or [tags in Markdoc](https://markdoc.dev/docs/tags), allowing authors to define and reuse snippets throughout their content. Shortcodes can accept attributes and content, and can be self-closing or not.