···99```sh
1010cargo bench
1111```
1212+1313+## Results
1414+1515+The following results were obtained on 2025-09-27 using a MacBook Pro (13-inch, M1, 2020) with 16 GB of RAM:
1616+1717+| Median Full Build Time |
1818+| ---------------------- |
1919+| 1.164s |
2020+2121+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
···99```sh
1010cargo bench
1111```
1212+1313+## Results
1414+1515+The following results were obtained on 2025-09-27 using a MacBook Pro (13-inch, M1, 2020) with 16 GB of RAM:
1616+1717+| Median Full Build Time |
1818+| ---------------------- |
1919+| 11.57ms |
2020+2121+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
···2424 *,
2525};
26262727+pub use highlight::{HighlightOptions, highlight_code};
2828+2729/// Helps implement a struct as a Markdown content entry.
2830///
2931/// ## Example
+61-44
crates/maudit/src/content/highlight.rs
···2020 THEME_SET.get_or_init(ThemeSet::load_defaults)
2121}
22222323+pub fn highlight_code(content: &str, options: &HighlightOptions) -> Result<String, Error> {
2424+ let ss = get_syntax_set();
2525+ let ts = get_theme_set();
2626+2727+ let syntax = ss
2828+ .find_syntax_by_token(&options.language)
2929+ // Maybe token is enough, looking around at other users of Syntect, it seems like they often just use by_token, not sure.
3030+ .or_else(|| ss.find_syntax_by_name(&options.language))
3131+ .or_else(|| ss.find_syntax_by_extension(&options.language))
3232+ .or_else(|| ss.find_syntax_by_first_line(content))
3333+ .unwrap_or_else(|| ss.find_syntax_plain_text());
3434+3535+ let theme = match ts.themes.get(&options.theme_path) {
3636+ Some(theme) => theme,
3737+ None => &match ThemeSet::get_theme(&options.theme_path) {
3838+ Ok(theme) => theme,
3939+ Err(_) => panic!(
4040+ "Theme '{}' not found in default themes and could not be loaded from file.",
4141+ options.theme_path
4242+ ),
4343+ },
4444+ };
4545+4646+ let mut h = HighlightLines::new(syntax, theme);
4747+4848+ let mut highlighted = String::new();
4949+ for line in LinesWithEndings::from(content) {
5050+ let regions = h.highlight_line(line, ss)?;
5151+ let html = styled_line_to_highlighted_html(®ions, IncludeBackground::No)?; // TODO: Handle the background coloring
5252+ highlighted.push_str(&html);
5353+ }
5454+5555+ Ok(highlighted)
5656+}
5757+2358fn opening_html(language: Option<&str>) -> String {
2459 let mut attrs = Vec::new();
2560···4782 format!("<pre{pre_attrs_str}><code{code_attrs_str}>")
4883}
49845050-pub struct CodeBlockMeta {
8585+pub struct HighlightOptions {
5186 pub language: String,
8787+ pub theme_path: String,
5288}
53895454-impl CodeBlockMeta {
5555- pub fn new_from_string(fence: &str) -> Self {
5656- // Parse the value after the opening of a fenced code block
5757- // e.g. for ```rs ins=0, you'd get lang: "rs", ins: "0"
5858-9090+impl HighlightOptions {
9191+ /// Parse the value after the opening of a fenced Markdown code block
9292+ /// e.g. for ```rs ins=0, you'd get lang: "rs", ins: "0"
9393+ pub fn new_from_fence(fence: &str, theme_path: impl Into<String>) -> Self {
5994 // TODO: Write the parser for this, lol
6095 let language = fence.to_string();
6161- Self { language }
9696+ Self {
9797+ language,
9898+ // TODO: We could somehow allow specifying the theme in the fence too, it'd be funny
9999+ theme_path: theme_path.into(),
100100+ }
101101+ }
102102+103103+ #[allow(dead_code)]
104104+ pub fn new(language: impl Into<String>, theme_path: impl Into<String>) -> Self {
105105+ Self {
106106+ language: language.into(),
107107+ theme_path: theme_path.into(),
108108+ }
62109 }
63110}
6411165112pub struct CodeBlock {
6666- pub meta: CodeBlockMeta,
113113+ pub highlight_options: HighlightOptions,
67114}
6811569116impl CodeBlock {
7070- pub fn new(fence: &str) -> (Self, String) {
7171- let meta = CodeBlockMeta::new_from_string(fence);
7272- let opening_html = opening_html(Some(&meta.language));
117117+ pub fn new(fence: &str, theme_path: &str) -> (Self, String) {
118118+ let highlight_options = HighlightOptions::new_from_fence(fence, theme_path);
119119+ let opening_html = opening_html(Some(&highlight_options.language));
731207474- (Self { meta }, opening_html)
121121+ (Self { highlight_options }, opening_html)
75122 }
761237777- pub fn highlight(&self, content: &str, theme_path: &str) -> Result<String, Error> {
7878- let ss = get_syntax_set();
7979- let ts = get_theme_set();
8080-8181- let syntax = ss
8282- .find_syntax_by_token(&self.meta.language)
8383- // Maybe token is enough, looking around at other users of Syntect, it seems like they often just use by_token, not sure.
8484- .or_else(|| ss.find_syntax_by_name(&self.meta.language))
8585- .or_else(|| ss.find_syntax_by_extension(&self.meta.language))
8686- .or_else(|| ss.find_syntax_by_first_line(content))
8787- .unwrap_or_else(|| ss.find_syntax_plain_text());
8888-8989- let theme = match ts.themes.get(theme_path) {
9090- Some(theme) => theme,
9191- None => &match ThemeSet::get_theme(theme_path) {
9292- Ok(theme) => theme,
9393- Err(_) => panic!(
9494- "Theme '{theme_path}' not found in default themes and could not be loaded from file."
9595- ),
9696- },
9797- };
9898-9999- let mut h = HighlightLines::new(syntax, theme);
100100-101101- let mut highlighted = String::new();
102102- for line in LinesWithEndings::from(content) {
103103- let regions = h.highlight_line(line, ss)?;
104104- let html = styled_line_to_highlighted_html(®ions, IncludeBackground::No)?; // TODO: Handle the background coloring
105105- highlighted.push_str(&html);
106106- }
107107-108108- Ok(highlighted)
124124+ pub fn highlight(&self, content: &str) -> Result<String, Error> {
125125+ highlight_code(content, &self.highlight_options)
109126 }
110127}
+7-7
crates/maudit/src/content/markdown.rs
···439439440440 // TODO: Handle this differently so it's compatible with the component system - erika, 2025-08-24
441441 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(fence))) => {
442442- let (block, begin) = CodeBlock::new(fence);
442442+ let (block, begin) = CodeBlock::new(
443443+ fence,
444444+ &options
445445+ .unwrap_or(&MarkdownOptions::default())
446446+ .highlight_theme,
447447+ );
443448 code_block = Some(block);
444449 events[i] = Event::Html(begin.into());
445450 }
446451447452 Event::End(TagEnd::CodeBlock) => {
448453 if let Some(ref mut code_block) = code_block {
449449- let html = code_block.highlight(
450450- &code_block_content,
451451- &options
452452- .unwrap_or(&MarkdownOptions::default())
453453- .highlight_theme,
454454- );
454454+ let html = code_block.highlight(&code_block_content);
455455 events[i] =
456456 Event::Html(format!("{}{}", html.unwrap(), "</code></pre>\n").into());
457457 }
+38-4
website/src/routes/index.rs
···11use maud::html;
22use maud::PreEscaped;
33+use maudit::content::highlight_code;
44+use maudit::content::HighlightOptions;
35use maudit::route::prelude::*;
4657use crate::layout::layout;
68use crate::layout::SeoMeta;
791010+const CODE_EXAMPLE: &str = r#"use maudit::prelude::*;
1111+1212+#[route("/")]
1313+pub struct Home;
1414+1515+impl Route for Home {
1616+ fn render(&self, _: &mut PageContext) -> impl Into<RenderResult> {
1717+ your_template_engine::render("home.html")
1818+ }
1919+}"#;
2020+821#[route("/")]
922pub struct Index;
1023···1629 ("Style your way", "Style with plain CSS, or opt for frameworks and preprocessors such as Tailwind and Sass."),
1730 ("Powerful routing", "Flexible and powerful routing system allows you to create complex sites with ease."),
1831 ("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."),
1919- ("Bring your templates", "Use your preferred templating engine to craft your website's pages. If it renders to HTML, Maudit supports it."),
3232+ ("Bring your templates", "Use your preferred templating engine to craft your website's pages. If it can return a String, Maudit supports it."),
2033 ].map(|(name, description)| {(name, PreEscaped(description))});
3434+3535+ let code_example = highlight_code(
3636+ CODE_EXAMPLE,
3737+ &HighlightOptions::new("rust", "base16-eighties.dark"),
3838+ )
3939+ .unwrap();
21402241 layout(
2342 html! {
···5877 }
59786079 section.features.py-14 {
6161- div."px-12"."lg:container".mx-auto {
8080+ div."px-6"."sm:px-12"."lg:container".mx-auto {
6281 div.grid."grid-cols-1"."md:grid-cols-2"."lg:grid-cols-3"."gap-8"."gap-y-12" {
6382 @for (name, description) in features {
6483 div.feature-card {
···72917392 div.h-12.bg-linear-to-b."from-darker-white".border-t.border-t-borders{}
74937575- h3.text-4xl.block.mb-12.mt-6.px-12.lg:container.mx-auto { "The court's library, not its king." }
7676-9494+ section."mb-12"."mt-6"."px-6"."sm:px-12".lg:container.mx-auto {
9595+ div.grid.grid-cols-1.lg:grid-cols-2.gap-8.items-center {
9696+ div {
9797+ h3.text-4xl.block.font-bold.mb-4 { "The court's library, not its king" }
9898+ p {
9999+ 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."
100100+ }
101101+ }
102102+ div {
103103+ pre.bg-gray-900.p-4.rounded-lg.overflow-x-auto.sm:text-base.text-sm {
104104+ code {
105105+ (PreEscaped(code_example))
106106+ }
107107+ }
108108+ }
109109+ }
110110+ }
77111 },
78112 true,
79113 true,