Rust library to generate static websites
5
fork

Configure Feed

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

feat: make it easier to highlight code outside of markdown rendering

+131 -58
+1 -1
benchmarks/md-benchmark/benches/build.rs
··· 13 13 } 14 14 15 15 #[divan::bench(args = [250, 500, 1000, 2000, 4000], sample_count = 3)] 16 - fn markdown(bencher: Bencher, markdown_count: u32) { 16 + fn markdown(bencher: Bencher, markdown_count: usize) { 17 17 bencher 18 18 .with_inputs(|| { 19 19 // Clear dist directory before each sample, otherwise later samples will either be very quick if we don't clean
+1 -1
benchmarks/md-benchmark/src/lib.rs
··· 4 4 }; 5 5 mod page; 6 6 7 - pub fn build_website(markdown_count: u32) { 7 + pub fn build_website(markdown_count: usize) { 8 8 let _ = coronate( 9 9 routes![page::Article], 10 10 content_sources!["articles" => glob_markdown::<UntypedMarkdownContent>(&format!("content/{}/*.md", markdown_count))],
+1 -1
benchmarks/md-benchmark/src/main.rs
··· 7 7 .unwrap(); 8 8 9 9 println!("Building with {} markdown files", markdown_count); 10 - build_website(markdown_count.try_into().unwrap()); 10 + build_website(markdown_count); 11 11 }
+10
benchmarks/overhead/README.md
··· 9 9 ```sh 10 10 cargo bench 11 11 ``` 12 + 13 + ## Results 14 + 15 + The following results were obtained on 2025-09-27 using a MacBook Pro (13-inch, M1, 2020) with 16 GB of RAM: 16 + 17 + | Median Full Build Time | 18 + | ---------------------- | 19 + | 1.164s | 20 + 21 + These numbers are not scientific and only serve as a rough estimate of the performance of Maudit. Your mileage may vary.
+10
benchmarks/realistic-blog/README.md
··· 9 9 ```sh 10 10 cargo bench 11 11 ``` 12 + 13 + ## Results 14 + 15 + The following results were obtained on 2025-09-27 using a MacBook Pro (13-inch, M1, 2020) with 16 GB of RAM: 16 + 17 + | Median Full Build Time | 18 + | ---------------------- | 19 + | 11.57ms | 20 + 21 + These numbers are not scientific and only serve as a rough estimate of the performance of Maudit. Your mileage may vary.
+2
crates/maudit/src/content.rs
··· 24 24 *, 25 25 }; 26 26 27 + pub use highlight::{HighlightOptions, highlight_code}; 28 + 27 29 /// Helps implement a struct as a Markdown content entry. 28 30 /// 29 31 /// ## Example
+61 -44
crates/maudit/src/content/highlight.rs
··· 20 20 THEME_SET.get_or_init(ThemeSet::load_defaults) 21 21 } 22 22 23 + pub fn highlight_code(content: &str, options: &HighlightOptions) -> Result<String, Error> { 24 + let ss = get_syntax_set(); 25 + let ts = get_theme_set(); 26 + 27 + let syntax = ss 28 + .find_syntax_by_token(&options.language) 29 + // Maybe token is enough, looking around at other users of Syntect, it seems like they often just use by_token, not sure. 30 + .or_else(|| ss.find_syntax_by_name(&options.language)) 31 + .or_else(|| ss.find_syntax_by_extension(&options.language)) 32 + .or_else(|| ss.find_syntax_by_first_line(content)) 33 + .unwrap_or_else(|| ss.find_syntax_plain_text()); 34 + 35 + let theme = match ts.themes.get(&options.theme_path) { 36 + Some(theme) => theme, 37 + None => &match ThemeSet::get_theme(&options.theme_path) { 38 + Ok(theme) => theme, 39 + Err(_) => panic!( 40 + "Theme '{}' not found in default themes and could not be loaded from file.", 41 + options.theme_path 42 + ), 43 + }, 44 + }; 45 + 46 + let mut h = HighlightLines::new(syntax, theme); 47 + 48 + let mut highlighted = String::new(); 49 + for line in LinesWithEndings::from(content) { 50 + let regions = h.highlight_line(line, ss)?; 51 + let html = styled_line_to_highlighted_html(&regions, IncludeBackground::No)?; // TODO: Handle the background coloring 52 + highlighted.push_str(&html); 53 + } 54 + 55 + Ok(highlighted) 56 + } 57 + 23 58 fn opening_html(language: Option<&str>) -> String { 24 59 let mut attrs = Vec::new(); 25 60 ··· 47 82 format!("<pre{pre_attrs_str}><code{code_attrs_str}>") 48 83 } 49 84 50 - pub struct CodeBlockMeta { 85 + pub struct HighlightOptions { 51 86 pub language: String, 87 + pub theme_path: String, 52 88 } 53 89 54 - impl CodeBlockMeta { 55 - pub fn new_from_string(fence: &str) -> Self { 56 - // Parse the value after the opening of a fenced code block 57 - // e.g. for ```rs ins=0, you'd get lang: "rs", ins: "0" 58 - 90 + impl HighlightOptions { 91 + /// Parse the value after the opening of a fenced Markdown code block 92 + /// e.g. for ```rs ins=0, you'd get lang: "rs", ins: "0" 93 + pub fn new_from_fence(fence: &str, theme_path: impl Into<String>) -> Self { 59 94 // TODO: Write the parser for this, lol 60 95 let language = fence.to_string(); 61 - Self { language } 96 + Self { 97 + language, 98 + // TODO: We could somehow allow specifying the theme in the fence too, it'd be funny 99 + theme_path: theme_path.into(), 100 + } 101 + } 102 + 103 + #[allow(dead_code)] 104 + pub fn new(language: impl Into<String>, theme_path: impl Into<String>) -> Self { 105 + Self { 106 + language: language.into(), 107 + theme_path: theme_path.into(), 108 + } 62 109 } 63 110 } 64 111 65 112 pub struct CodeBlock { 66 - pub meta: CodeBlockMeta, 113 + pub highlight_options: HighlightOptions, 67 114 } 68 115 69 116 impl CodeBlock { 70 - pub fn new(fence: &str) -> (Self, String) { 71 - let meta = CodeBlockMeta::new_from_string(fence); 72 - let opening_html = opening_html(Some(&meta.language)); 117 + pub fn new(fence: &str, theme_path: &str) -> (Self, String) { 118 + let highlight_options = HighlightOptions::new_from_fence(fence, theme_path); 119 + let opening_html = opening_html(Some(&highlight_options.language)); 73 120 74 - (Self { meta }, opening_html) 121 + (Self { highlight_options }, opening_html) 75 122 } 76 123 77 - pub fn highlight(&self, content: &str, theme_path: &str) -> Result<String, Error> { 78 - let ss = get_syntax_set(); 79 - let ts = get_theme_set(); 80 - 81 - let syntax = ss 82 - .find_syntax_by_token(&self.meta.language) 83 - // Maybe token is enough, looking around at other users of Syntect, it seems like they often just use by_token, not sure. 84 - .or_else(|| ss.find_syntax_by_name(&self.meta.language)) 85 - .or_else(|| ss.find_syntax_by_extension(&self.meta.language)) 86 - .or_else(|| ss.find_syntax_by_first_line(content)) 87 - .unwrap_or_else(|| ss.find_syntax_plain_text()); 88 - 89 - let theme = match ts.themes.get(theme_path) { 90 - Some(theme) => theme, 91 - None => &match ThemeSet::get_theme(theme_path) { 92 - Ok(theme) => theme, 93 - Err(_) => panic!( 94 - "Theme '{theme_path}' not found in default themes and could not be loaded from file." 95 - ), 96 - }, 97 - }; 98 - 99 - let mut h = HighlightLines::new(syntax, theme); 100 - 101 - let mut highlighted = String::new(); 102 - for line in LinesWithEndings::from(content) { 103 - let regions = h.highlight_line(line, ss)?; 104 - let html = styled_line_to_highlighted_html(&regions, IncludeBackground::No)?; // TODO: Handle the background coloring 105 - highlighted.push_str(&html); 106 - } 107 - 108 - Ok(highlighted) 124 + pub fn highlight(&self, content: &str) -> Result<String, Error> { 125 + highlight_code(content, &self.highlight_options) 109 126 } 110 127 }
+7 -7
crates/maudit/src/content/markdown.rs
··· 439 439 440 440 // TODO: Handle this differently so it's compatible with the component system - erika, 2025-08-24 441 441 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(fence))) => { 442 - let (block, begin) = CodeBlock::new(fence); 442 + let (block, begin) = CodeBlock::new( 443 + fence, 444 + &options 445 + .unwrap_or(&MarkdownOptions::default()) 446 + .highlight_theme, 447 + ); 443 448 code_block = Some(block); 444 449 events[i] = Event::Html(begin.into()); 445 450 } 446 451 447 452 Event::End(TagEnd::CodeBlock) => { 448 453 if let Some(ref mut code_block) = code_block { 449 - let html = code_block.highlight( 450 - &code_block_content, 451 - &options 452 - .unwrap_or(&MarkdownOptions::default()) 453 - .highlight_theme, 454 - ); 454 + let html = code_block.highlight(&code_block_content); 455 455 events[i] = 456 456 Event::Html(format!("{}{}", html.unwrap(), "</code></pre>\n").into()); 457 457 }
+38 -4
website/src/routes/index.rs
··· 1 1 use maud::html; 2 2 use maud::PreEscaped; 3 + use maudit::content::highlight_code; 4 + use maudit::content::HighlightOptions; 3 5 use maudit::route::prelude::*; 4 6 5 7 use crate::layout::layout; 6 8 use crate::layout::SeoMeta; 7 9 10 + const CODE_EXAMPLE: &str = r#"use maudit::prelude::*; 11 + 12 + #[route("/")] 13 + pub struct Home; 14 + 15 + impl Route for Home { 16 + fn render(&self, _: &mut PageContext) -> impl Into<RenderResult> { 17 + your_template_engine::render("home.html") 18 + } 19 + }"#; 20 + 8 21 #[route("/")] 9 22 pub struct Index; 10 23 ··· 16 29 ("Style your way", "Style with plain CSS, or opt for frameworks and preprocessors such as Tailwind and Sass."), 17 30 ("Powerful routing", "Flexible and powerful routing system allows you to create complex sites with ease."), 18 31 ("Ecosystem-ready", "Maudit utilize <a class=\"underline\" href=\"https://rolldown.rs\">Rolldown</a>, a fast bundler for JavaScript and CSS, enabling the usage of TypeScript and the npm ecosystem."), 19 - ("Bring your templates", "Use your preferred templating engine to craft your website's pages. If it renders to HTML, Maudit supports it."), 32 + ("Bring your templates", "Use your preferred templating engine to craft your website's pages. If it can return a String, Maudit supports it."), 20 33 ].map(|(name, description)| {(name, PreEscaped(description))}); 34 + 35 + let code_example = highlight_code( 36 + CODE_EXAMPLE, 37 + &HighlightOptions::new("rust", "base16-eighties.dark"), 38 + ) 39 + .unwrap(); 21 40 22 41 layout( 23 42 html! { ··· 58 77 } 59 78 60 79 section.features.py-14 { 61 - div."px-12"."lg:container".mx-auto { 80 + div."px-6"."sm:px-12"."lg:container".mx-auto { 62 81 div.grid."grid-cols-1"."md:grid-cols-2"."lg:grid-cols-3"."gap-8"."gap-y-12" { 63 82 @for (name, description) in features { 64 83 div.feature-card { ··· 72 91 73 92 div.h-12.bg-linear-to-b."from-darker-white".border-t.border-t-borders{} 74 93 75 - h3.text-4xl.block.mb-12.mt-6.px-12.lg:container.mx-auto { "The court's library, not its king." } 76 - 94 + section."mb-12"."mt-6"."px-6"."sm:px-12".lg:container.mx-auto { 95 + div.grid.grid-cols-1.lg:grid-cols-2.gap-8.items-center { 96 + div { 97 + h3.text-4xl.block.font-bold.mb-4 { "The court's library, not its king" } 98 + p { 99 + a.underline href="/docs/philosophy/#maudit-is-a-library-not-a-framework" { "Maudit is a library, not a framework." } " A Maudit site is a normal Rust program that you have full control over. Hook into the build process, customize the output, and use any libraries you want." 100 + } 101 + } 102 + div { 103 + pre.bg-gray-900.p-4.rounded-lg.overflow-x-auto.sm:text-base.text-sm { 104 + code { 105 + (PreEscaped(code_example)) 106 + } 107 + } 108 + } 109 + } 110 + } 77 111 }, 78 112 true, 79 113 true,