···11-use dyn_eq::DynEq;
21use log::debug;
32use rustc_hash::FxHashSet;
44-use std::hash::Hash;
53use std::path::Path;
64use std::time::Instant;
75use std::{fs, path::PathBuf};
···7775 /// 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.
7876 ///
7977 /// The image will not automatically be included in the page, but can be included through the `.url()` method on the returned `Image` object.
8080- ///
8181- /// 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.
8278 pub fn add_image_with_options<P>(&mut self, image_path: P, options: ImageOptions) -> Image
8379 where
8480 P: Into<PathBuf>,
8581 {
8682 let image_path = image_path.into();
87838888- // Check if the image already exists in the assets, if so, return it
8989- if let Some(image) = self.images.iter().find_map(|asset| {
9090- asset.as_any().downcast_ref::<Image>().filter(|image| {
9191- image.path == image_path
9292- && options == *image.options.as_ref().unwrap_or(&ImageOptions::default())
9393- })
9494- }) {
9595- return image.clone();
9696- }
8484+ let image_options = if options == ImageOptions::default() {
8585+ None
8686+ } else {
8787+ Some(options)
8888+ };
97899898- let image = Image::new(
9999- image_path,
100100- if options == ImageOptions::default() {
101101- None
102102- } else {
103103- Some(options)
104104- },
105105- &self.options,
9090+ let hash = calculate_hash(
9191+ &image_path,
9292+ Some(&HashConfig {
9393+ asset_type: HashAssetType::Image(
9494+ image_options.as_ref().unwrap_or(&ImageOptions::default()),
9595+ ),
9696+ hashing_strategy: &self.options.hashing_strategy,
9797+ }),
10698 );
9999+100100+ let image = Image::new(image_path, image_options, hash, &self.options);
107101108102 self.images.insert(image.clone());
109103···121115 ///
122116 /// The script will not automatically be included in the page, but can be included through the `.url()` method on the returned `Script` object.
123117 /// Alternatively, a script can be included automatically using the [RouteAssets::include_script] method instead.
124124- ///
125125- /// 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.
126118 pub fn add_script<P>(&mut self, script_path: P) -> Script
127119 where
128120 P: Into<PathBuf>,
129121 {
130130- let script = Script::new(script_path.into(), false, &self.options);
122122+ let script_path = script_path.into();
123123+124124+ let hash = calculate_hash(
125125+ &script_path,
126126+ Some(&HashConfig {
127127+ asset_type: HashAssetType::Script,
128128+ hashing_strategy: &self.options.hashing_strategy,
129129+ }),
130130+ );
131131+132132+ let script = Script::new(script_path, false, hash, &self.options);
131133132134 self.scripts.insert(script.clone());
133135···143145 where
144146 P: Into<PathBuf>,
145147 {
146146- let script = Script::new(script_path.into(), true, &self.options);
148148+ let script_path = script_path.into();
149149+150150+ let hash = calculate_hash(
151151+ &script_path,
152152+ Some(&HashConfig {
153153+ asset_type: HashAssetType::Script,
154154+ hashing_strategy: &self.options.hashing_strategy,
155155+ }),
156156+ );
157157+158158+ let script = Script::new(script_path, true, hash, &self.options);
147159148160 self.scripts.insert(script);
149161 }
···153165 /// The style will not automatically be included in the page, but can be included through the `.url()` method on the returned `Style` object.
154166 /// Alternatively, a style can be included automatically using the [RouteAssets::include_style] method instead.
155167 ///
156156- /// 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.
168168+ /// 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.
157169 pub fn add_style<P>(&mut self, style_path: P) -> Style
158170 where
159171 P: Into<PathBuf>,
···170182 where
171183 P: Into<PathBuf>,
172184 {
173173- let style = Style::new(style_path.into(), false, &options, &self.options);
185185+ let style_path = style_path.into();
186186+187187+ let hash = calculate_hash(
188188+ &style_path,
189189+ Some(&HashConfig {
190190+ asset_type: HashAssetType::Style(&options),
191191+ hashing_strategy: &self.options.hashing_strategy,
192192+ }),
193193+ );
194194+195195+ let style = Style::new(style_path, false, &options, hash, &self.options);
174196175197 self.styles.insert(style.clone());
176198···198220 where
199221 P: Into<PathBuf>,
200222 {
201201- let style = Style::new(style_path.into(), true, &options, &self.options);
223223+ let style_path = style_path.into();
224224+225225+ let hash = calculate_hash(
226226+ &style_path,
227227+ Some(&HashConfig {
228228+ asset_type: HashAssetType::Style(&options),
229229+ hashing_strategy: &self.options.hashing_strategy,
230230+ }),
231231+ );
232232+233233+ let style = Style::new(style_path, true, &options, hash, &self.options);
202234203235 self.styles.insert(style);
204236 }
205237}
206238207207-pub trait Asset: DynEq + Sync + Send {
239239+pub trait Asset: Sync + Send {
208240 fn build_path(&self) -> &PathBuf;
209209- fn url(&self) -> Option<&String>;
241241+ fn url(&self) -> &String;
210242 fn path(&self) -> &PathBuf;
211243 fn filename(&self) -> &PathBuf;
212244}
···226258 &self.build_path
227259 }
228260229229- fn url(&self) -> Option<&String> {
230230- Some(&self.url)
261261+ fn url(&self) -> &String {
262262+ &self.url
231263 }
232264 }
233265 };
···332364 hex[..5].to_string()
333365}
334366335335-impl Hash for dyn Asset {
336336- fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
337337- self.path().hash(state);
338338- }
339339-}
340340-341341-dyn_eq::eq_trait_object!(Asset);
342342-343367#[cfg(test)]
344368mod tests {
345369 use super::*;
···411435 let mut page_assets = RouteAssets::default();
412436413437 let image = page_assets.add_image(temp_dir.join("image.png"));
414414- assert_eq!(image.url().unwrap().chars().next(), Some('/'));
438438+ assert_eq!(image.url().chars().next(), Some('/'));
415439416440 let script = page_assets.add_script(temp_dir.join("script.js"));
417417- assert_eq!(script.url().unwrap().chars().next(), Some('/'));
441441+ assert_eq!(script.url().chars().next(), Some('/'));
418442419443 let style = page_assets.add_style(temp_dir.join("style.css"));
420420- assert_eq!(style.url().unwrap().chars().next(), Some('/'));
444444+ assert_eq!(style.url().chars().next(), Some('/'));
421445 }
422446423447 #[test]
···426450 let mut page_assets = RouteAssets::default();
427451428452 let image = page_assets.add_image(temp_dir.join("image.png"));
429429- assert!(image.url().unwrap().contains(&image.hash));
453453+ assert!(image.url().contains(&image.hash));
430454431455 let script = page_assets.add_script(temp_dir.join("script.js"));
432432- assert!(script.url().unwrap().contains(&script.hash));
456456+ assert!(script.url().contains(&script.hash));
433457434458 let style = page_assets.add_style(temp_dir.join("style.css"));
435435- assert!(style.url().unwrap().contains(&style.hash));
459459+ assert!(style.url().contains(&style.hash));
436460 }
437461438462 #[test]
···1818 content::{ContentSources, RouteContent},
1919 is_dev,
2020 logging::print_title,
2121- route::{DynamicRouteContext, FullRoute, PageContext, PageParams, RouteType},
2121+ route::{
2222+ CachedRoute, DynamicRouteContext, FullRoute, InternalRoute, PageContext, PageParams,
2323+ RouteType,
2424+ },
2225};
2326use colored::{ColoredString, Colorize};
2427use log::{debug, info, trace, warn};
···5356 // Create a directory for the output
5457 trace!(target: "build", "Setting up required directories...");
55585656- let old_dist_tmp_dir = if options.clean_output_dir {
5757- let duration = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
5858- let num = (duration.as_secs() + duration.subsec_nanos() as u64) % 100000;
5959- let new_dir_for_old_dist = env::temp_dir().join(format!("maudit_old_dist_{}", num));
6060- let _ = fs::rename(&options.output_dir, &new_dir_for_old_dist);
6161- Some(new_dir_for_old_dist)
5959+ let clean_up_handle = if options.clean_output_dir {
6060+ let old_dist_tmp_dir = {
6161+ let duration = SystemTime::now().duration_since(UNIX_EPOCH)?;
6262+ let num = (duration.as_secs() + duration.subsec_nanos() as u64) % 100000;
6363+ let new_dir_for_old_dist = env::temp_dir().join(format!("maudit_old_dist_{}", num));
6464+ let _ = fs::rename(&options.output_dir, &new_dir_for_old_dist);
6565+ new_dir_for_old_dist
6666+ };
6767+6868+ Some(tokio::spawn(async {
6969+ let _ = fs::remove_dir_all(old_dist_tmp_dir);
7070+ }))
6271 } else {
6372 None
6473 };
65746666- let should_clear_dist = options.clean_output_dir;
6767- let clean_up_handle = tokio::spawn(async move {
6868- if should_clear_dist {
6969- let _ = fs::remove_dir_all(old_dist_tmp_dir.unwrap());
7070- }
7171- });
7272-7375 let route_assets_options = options.route_assets_options();
74767577 info!(target: "build", "Output directory: {}", options.output_dir.display());
···118120 let mut page_count = 0;
119121120122 // This is fully serial. It is somewhat trivial to make it parallel, but it currently isn't because every time I've tried to
121121- // (uncommited, #25 and #41) it either made no difference or was slower. The overhead of parallelism is just too high for
123123+ // (uncommited, #25, #41, #46) it either made no difference or was slower. The overhead of parallelism is just too high for
122124 // 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.
123125 // If you manage to make it parallel and it actually improves performance, please open a PR!
124126 for route in routes {
125125- match route.route_type() {
127127+ let cached_route = CachedRoute::new(*route);
128128+129129+ match cached_route.route_type() {
126130 RouteType::Static => {
127131 let route_start = Instant::now();
128132···130134 let mut page_assets = RouteAssets::new(&route_assets_options);
131135132136 let params = PageParams::default();
133133- let url = route.url(¶ms);
137137+ let url = cached_route.url(¶ms);
134138135139 let result = route.build(&mut PageContext::from_static_route(
136140 &content,
···139143 &options.base_url,
140144 ))?;
141145142142- let file_path = route.file_path(¶ms, &options.output_dir);
146146+ let file_path = cached_route.file_path(¶ms, &options.output_dir);
143147144148 write_route_file(&result, &file_path)?;
145149···176180 for page in pages {
177181 let route_start = Instant::now();
178182179179- let url = route.url(&page.0);
183183+ let url = cached_route.url(&page.0);
180184181185 let content = route.build(&mut PageContext::from_dynamic_route(
182186 &page,
···186190 &options.base_url,
187191 ))?;
188192189189- let file_path = route.file_path(&page.0, &options.output_dir);
193193+ let file_path = cached_route.file_path(&page.0, &options.output_dir);
190194191195 write_route_file(&content, &file_path)?;
192196···327331 debug!("Failed to copy from cache {} to dest {}", cache_path.display(), dest_path.display());
328332 }
329333 } else if !dest_path.exists() {
330330- // 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
331334 fs::copy(image.path(), dest_path).unwrap_or_else(|e| {
332335 panic!(
333336 "Failed to copy image from {} to {}: {}",
···348351 let assets_start = Instant::now();
349352 print_title("copying assets");
350353351351- // Copy the static directory to the dist directory
352354 copy_recursively(
353355 &options.static_dir,
354356 &options.output_dir,
···361363 info!(target: "SKIP_FORMAT", "{}", "");
362364 info!(target: "build", "{}", format!("Build completed in {}", format_elapsed_time(build_start.elapsed(), §ion_format_options)).bold());
363365364364- clean_up_handle.await.unwrap();
366366+ if let Some(clean_up_handle) = clean_up_handle {
367367+ clean_up_handle.await?;
368368+ }
365369366370 Ok(build_metadata)
367371}
···2020 html! {
2121 head {
2222 title { "Index" }
2323- link rel="stylesheet" href=(style.url().unwrap()) {}
2323+ link rel="stylesheet" href=(style.url()) {}
2424 }
2525 h1 { "Index" }
2626- img src=(image.url().unwrap()) {}
2727- script src=(script.url().unwrap()) {}
2626+ img src=(image.url()) {}
2727+ script src=(script.url()) {}
2828 a."text-red-500" href=(link_to_first_dynamic) { "Go to first dynamic page" }
2929 }
3030 }
+31
website/content/docs/content.md
···176176177177Either 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.
178178179179+### Syntax Highlighting
180180+181181+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.
182182+183183+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:
184184+185185+- `base16-ocean.dark`,`base16-eighties.dark`,`base16-mocha.dark`,`base16-ocean.light`
186186+- `InspiredGitHub` from [here](https://github.com/sethlopezme/InspiredGitHub.tmtheme)
187187+- `Solarized (dark)` and `Solarized (light)`
188188+189189+```rust
190190+use maudit::content::markdown::MarkdownOptions;
191191+192192+fn main() {
193193+ coronate(
194194+ routes![
195195+ // ...
196196+ ],
197197+ content_sources![
198198+ "blog" => glob_markdown_with_options::<BlogPost>("content/blog/**/*.md", MarkdownOptions {
199199+ highlight_theme: "InspiredGitHub".into(),
200200+ ..Default::default()
201201+ }),
202202+ ],
203203+ ..Default::default(),
204204+ );
205205+}
206206+```
207207+208208+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.
209209+179210### Shortcodes
180211181212Shortcodes 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.