···11-use std::sync::Arc;
11+use std::{path::Path, sync::Arc};
2233use glob::glob as glob_fs;
44use log::warn;
···88pub mod components;
991010use components::{LinkType, ListType, MarkdownComponents, TableAlignment};
1111+1212+use crate::{assets::Asset, page::RouteContext};
11131214use super::{highlight::CodeBlock, slugger, ContentEntry};
1315···3436/// fn render(&self, ctx: &mut RouteContext) -> Markup {
3537/// let articles = ctx.content.get_source::<ArticleContent>("articles");
3638/// let article = articles.get_entry("my-article");
3737-/// let headings = article.data().get_headings(); // returns a Vec<MarkdownHeading>
3939+/// let headings = article.data(ctx).get_headings(); // returns a Vec<MarkdownHeading>
3840/// let toc = html! {
3941/// ul {
4042/// @for heading in headings {
···4648/// };
4749/// html! {
4850/// main {
4949-/// h1 { (article.data().title) }
5151+/// h1 { (article.data(ctx).title) }
5052/// nav { (toc) }
5153/// }
5254/// }
···195197196198 // Clone content for the closure
197199 let content_clone = content.clone();
198198- let data_loader = Box::new(move || {
200200+ let data_loader = Box::new(move |_: &mut RouteContext| {
199201 let mut slugger = slugger::Slugger::new();
200202201203 let mut options = Options::empty();
···249251 // Perhaps not ideal, but I don't know better. We're at the "get it working" stage - erika, 2025-08-24
250252 // Ideally, we'd at least avoid the allocation here whenever `options` is None, not sure how to do that ergonomically
251253 let opts = options.clone();
254254+ let path = entry.clone();
252255253256 entries.push(ContentEntry::new_lazy(
254257 id,
255255- Some(Box::new(move |content: &str| {
256256- render_markdown(content, opts.as_deref())
258258+ Some(Box::new(move |content: &str, route_ctx| {
259259+ render_markdown(content, opts.as_deref(), Some(&path), Some(route_ctx))
257260 })),
258261 Some(content),
259262 data_loader,
···335338/// };
336339/// let html = render_markdown(markdown, Some(&options));
337340/// ```
338338-pub fn render_markdown(content: &str, options: Option<&MarkdownOptions>) -> String {
341341+pub fn render_markdown(
342342+ content: &str,
343343+ options: Option<&MarkdownOptions>,
344344+ path: Option<&Path>,
345345+ mut route_ctx: Option<&mut RouteContext>,
346346+) -> String {
339347 let mut slugger = slugger::Slugger::new();
340348 let mut html_output = String::new();
341349 let parser_options = Options::ENABLE_YAML_STYLE_METADATA_BLOCKS
···367375 continue;
368376 }
369377378378+ // TODO: Write an integration test for assets resolution - erika, 2025-08-27
379379+ Event::Start(Tag::Image {
380380+ dest_url,
381381+ link_type,
382382+ id,
383383+ title,
384384+ }) => {
385385+ // TODO: Figure out a cleaner way to do this, it's a lot of if-lets and checks - erika, 2025-08-27
386386+ let new_event = if dest_url.starts_with("./") || dest_url.starts_with("../") {
387387+ path.and_then(|p| p.parent())
388388+ .and_then(|parent| {
389389+ let resolved = parent.join(dest_url.to_string());
390390+ route_ctx
391391+ .as_mut()
392392+ .and_then(|ctx| ctx.assets.add_image(resolved).url())
393393+ })
394394+ .map(|image_url| {
395395+ Event::Start(Tag::Image {
396396+ dest_url: image_url.into(),
397397+ title: title.clone(),
398398+ link_type: *link_type,
399399+ id: id.clone(),
400400+ })
401401+ })
402402+ } else {
403403+ None
404404+ };
405405+406406+ if let Some(event) = new_event {
407407+ events[i] = event;
408408+ }
409409+ }
410410+370411 // TODO: Handle this differently so it's compatible with the component system - erika, 2025-08-24
371412 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(ref fence))) => {
372413 let (block, begin) = CodeBlock::new(fence);
···764805765806More content here."#;
766807767767- let html = render_markdown(markdown, None);
808808+ let html = render_markdown(markdown, None, None, None);
768809769810 // Test basic markdown rendering
770811 assert!(html.contains("<h1"));
···783824 };
784825 let markdown = r#"# Hello, world!"#;
785826786786- let html = render_markdown(markdown, Some(&options));
787787- let default_html = render_markdown(markdown, None);
827827+ let html = render_markdown(markdown, Some(&options), None, None);
828828+ let default_html = render_markdown(markdown, None, None, None);
788829789830 // Should be the same as default rendering when no custom components are provided
790831 assert_eq!(html, default_html);
···799840### Another Level"#;
800841801842 // Render without any options
802802- let html_no_options = render_markdown(markdown, None);
843843+ let html_no_options = render_markdown(markdown, None, None, None);
803844804845 // Render with options but no custom heading component
805846 let options_no_heading = MarkdownOptions {
806847 components: MarkdownComponents::new(),
807848 };
808808- let html_with_empty_options = render_markdown(markdown, Some(&options_no_heading));
849849+ let html_with_empty_options =
850850+ render_markdown(markdown, Some(&options_no_heading), None, None);
809851810852 // Both should produce identical output
811853 assert_eq!(html_no_options, html_with_empty_options);
+24-13
crates/maudit/src/content/markdown/components.rs
···619619 components: MarkdownComponents::new().heading(TestCustomHeading),
620620 };
621621622622- let html = render_markdown("# Hello, world!", Some(&options));
622622+ let html = render_markdown("# Hello, world!", Some(&options), None, None);
623623 assert!(html.contains("🎯Hello, world!"));
624624 }
625625···629629 components: MarkdownComponents::new().paragraph(TestCustomParagraph),
630630 };
631631632632- let content = render_markdown("This is a paragraph.", Some(&options));
632632+ let content = render_markdown("This is a paragraph.", Some(&options), None, None);
633633 assert!(content.contains(
634634 "<p class=\"custom-paragraph\">This is a paragraph.</p><!-- end custom paragraph -->"
635635 ));
···641641 components: MarkdownComponents::new().link(TestCustomLink),
642642 };
643643644644- let content = render_markdown("[Example](https://example.com)", Some(&options));
644644+ let content = render_markdown("[Example](https://example.com)", Some(&options), None, None);
645645 assert!(
646646 content.contains("<a href=\"https://example.com\" class=\"custom-link\">🔗Example</a>")
647647 );
···653653 components: MarkdownComponents::new().image(TestCustomImage),
654654 };
655655656656- let content = render_markdown("", Some(&options));
656656+ let content = render_markdown("", Some(&options), None, None);
657657 assert!(
658658 content.contains("<img src=\"image.jpg\" alt=\"Alt text\" class=\"custom-image\" />📸")
659659 );
···665665 components: MarkdownComponents::new().strong(TestCustomStrong),
666666 };
667667668668- let content = render_markdown("**Bold text**", Some(&options));
668668+ let content = render_markdown("**Bold text**", Some(&options), None, None);
669669 assert!(content.contains("<strong class=\"custom-strong\">💪Bold text</strong>"));
670670 }
671671···675675 components: MarkdownComponents::new().emphasis(TestCustomEmphasis),
676676 };
677677678678- let content = render_markdown("*Italic text*", Some(&options));
678678+ let content = render_markdown("*Italic text*", Some(&options), None, None);
679679 assert!(content.contains("<em class=\"custom-emphasis\">✨Italic text</em>"));
680680 }
681681···685685 components: MarkdownComponents::new().code(TestCustomCode),
686686 };
687687688688- let content = render_markdown("`console.log('hello')`", Some(&options));
688688+ let content = render_markdown("`console.log('hello')`", Some(&options), None, None);
689689 assert!(content.contains("<code class=\"custom-code\">💻console.log('hello')</code>"));
690690 }
691691···695695 components: MarkdownComponents::new().blockquote(TestCustomBlockquote),
696696 };
697697698698- let content = render_markdown("> This is a quote", Some(&options));
698698+ let content = render_markdown("> This is a quote", Some(&options), None, None);
699699 assert!(content.contains("<blockquote class=\"custom-blockquote\">📝"));
700700 assert!(content.contains("</blockquote>"));
701701 assert!(content.contains("This is a quote"));
···714714 let content = render_markdown(
715715 "# Title\n\nThis is a **bold** [link](https://example.com).",
716716 Some(&options),
717717+ None,
718718+ None,
717719 );
718720719721 assert!(content.contains("🎯Title"));
···737739 let content = render_markdown(
738740 "> This is a **bold** and *italic* with `code`",
739741 Some(&options),
742742+ None,
743743+ None,
740744 );
741745 assert!(content.contains("<blockquote class=\"custom-blockquote\">📝"));
742746 assert!(content.contains("<strong class=\"custom-strong\">💪bold</strong>"));
···866870 let options = MarkdownOptions {
867871 components: MarkdownComponents::new().hard_break(TestHardBreak),
868872 };
869869- let content = render_markdown("Line 1 \nLine 2", Some(&options));
873873+ let content = render_markdown("Line 1 \nLine 2", Some(&options), None, None);
870874 assert!(content.contains("<br class=\"custom-break\" />"));
871875 }
872876···875879 let options = MarkdownOptions {
876880 components: MarkdownComponents::new().horizontal_rule(TestHorizontalRule),
877881 };
878878- let content = render_markdown("---", Some(&options));
882882+ let content = render_markdown("---", Some(&options), None, None);
879883 assert!(content.contains("<hr class=\"custom-rule\" />"));
880884 }
881885···886890 .list(TestList)
887891 .list_item(TestListItem),
888892 };
889889- let content = render_markdown("1. First\n2. Second\n\n- Bullet\n- Point", Some(&options));
893893+ let content = render_markdown(
894894+ "1. First\n2. Second\n\n- Bullet\n- Point",
895895+ Some(&options),
896896+ None,
897897+ None,
898898+ );
890899 assert!(content.contains("<ol class=\"custom-list\" start=\"1\">"));
891900 assert!(content.contains("<ul class=\"custom-list\">"));
892901 assert!(content.contains("<li class=\"custom-item\">"));
···897906 let options = MarkdownOptions {
898907 components: MarkdownComponents::new().strikethrough(TestStrikethrough),
899908 };
900900- let content = render_markdown("~~strikethrough~~", Some(&options));
909909+ let content = render_markdown("~~strikethrough~~", Some(&options), None, None);
901910 assert!(content.contains("<del class=\"custom-strike\">"));
902911 }
903912···906915 let options = MarkdownOptions {
907916 components: MarkdownComponents::new().task_list_marker(TestTaskListMarker),
908917 };
909909- let content = render_markdown("- [x] Done\n- [ ] Todo", Some(&options));
918918+ let content = render_markdown("- [x] Done\n- [ ] Todo", Some(&options), None, None);
910919 assert!(content.contains("<input type=\"checkbox\" checked class=\"custom-task\" />"));
911920 assert!(content.contains("<input type=\"checkbox\" class=\"custom-task\" />"));
912921 }
···923932 let content = render_markdown(
924933 "| Header | Header |\n|--------|--------|\n| Cell | Cell |",
925934 Some(&options),
935935+ None,
936936+ None,
926937 );
927938 assert!(content.contains("<table class=\"custom-table\">"));
928939 assert!(content.contains("<thead class=\"custom-thead\">"));
+1-1
crates/maudit/src/page.rs
···8585/// (logo)
8686/// ul {
8787/// @for entry in last_entries {
8888-/// li { (entry.data().title) }
8888+/// li { (entry.data(ctx).title) }
8989/// }
9090/// }
9191/// }
+4-4
crates/oubli/src/archetypes/blog.rs
···1919 main {
2020 @for entry in &blog_entries.entries {
2121 a href=(get_page_url(&route, BlogEntryParams { entry: entry.id.clone() })) {
2222- h2 { (entry.data().title) }
2323- p { (entry.data().description) }
2222+ h2 { (entry.data(ctx).title) }
2323+ p { (entry.data(ctx).description) }
2424 }
2525 }
2626 }
···5757 .get_source::<BlogEntryContent>(stringified_ident);
5858 let blog_entry = blog_entries.get_entry(¶ms.entry);
59596060- let headings = blog_entry.data().get_headings();
6060+ let headings = blog_entry.data(ctx).get_headings();
6161 println!("{:?}", headings);
62626363- layout(name, blog_entry.render())
6363+ layout(name, blog_entry.render(ctx))
6464}
+2-2
examples/blog/src/pages/article.rs
···2525 let articles = ctx.content.get_source::<ArticleContent>("articles");
2626 let article = articles.get_entry(¶ms.article);
27272828- let headings = article.data().get_headings();
2828+ let headings = article.data(ctx).get_headings();
2929 println!("{:?}", headings);
30303131- layout(article.render())
3131+ layout(article.render(ctx))
3232 }
3333}
+2-2
examples/blog/src/pages/index.rs
···1919 @for entry in &articles.entries {
2020 li {
2121 a href=(get_page_url(&Article, ArticleParams { article: entry.id.clone() })) {
2222- h2 { (entry.data().title) }
2222+ h2 { (entry.data(ctx).title) }
2323 }
2424- p { (entry.data().description) }
2424+ p { (entry.data(ctx).description) }
2525 }
2626 }
2727 }
+1-4
examples/markdown-components/src/pages.rs
···1313 let examples = ctx.content.get_source::<ComponentExample>("examples");
1414 let example = examples.get_entry("showcase");
15151616- // The content is already rendered with the custom components
1717- // when it was loaded via glob_markdown with options
1818- let content_html = example.render();
1919-1616+ let content_html = example.render(ctx);
2017 html! {
2118 (DOCTYPE)
2219 html lang="en" {
···50505151On a 2020 M1 MacBook Pro, [we've found that the final binary of a Maudit project can build a project with 4000 Markdown files in around 700ms](https://github.com/bruits/maudit/tree/main/benchmarks/md-benchmark), which we consider quite reasonable.
52525353-[](https://github.com/bruits/maudit/tree/main/benchmarks/md-benchmark)
5353+[](https://github.com/bruits/maudit/tree/main/benchmarks/md-benchmark)
54545555As we add more features, it's possible that Maudit will become slower, but we'll monitor performance and ensure that, yeah, it's reasonably fast.
5656
+2-3
website/src/layout/docs_sidebars.rs
···99 let mut sections = std::collections::HashMap::new();
10101111 for entry in content.entries.iter() {
1212- if let Some(section) = &entry.data().section {
1212+ if let Some(section) = &entry.data(ctx).section {
1313 sections.entry(section).or_insert_with(Vec::new).push(entry);
1414 }
1515 }
···4343 @let url = format!("/docs/{}", entry.id);
4444 @let is_current_page = url == ctx.current_url;
4545 li."border-l-2"."hover:border-brand-red"."pl-3"."py-1".(if is_current_page { "text-brand-red border-brand-red" } else { "border-borders" }) {
4646- a.block href=(format!("/docs/{}/", entry.id)) { (entry.data().title) } // TODO: Use type-safe routing
4646+ a.block href=(format!("/docs/{}/", entry.id)) { (entry.data(ctx).title) } // TODO: Use type-safe routing
4747 }
4848 }
4949 }
···8282 html!(
8383 h2.text-lg.font-bold { "On This Page" }
8484 nav.sticky.top-8 {
8585- // TODO: Implement this properly
8685 ul {
8786 @for heading in html_headings {
8887 (heading)