···11+use ::image::image_dimensions;
12use dyn_eq::DynEq;
33+use log::debug;
24use rustc_hash::FxHashSet;
35use std::hash::Hash;
46use std::sync::OnceLock;
77+use std::time::Instant;
58use std::{fs, path::PathBuf};
99+use xxhash_rust::xxh3::xxh3_64;
610711mod image;
812pub mod image_cache;
···4650 return image.clone();
4751 }
48525353+ let (width, height) = image_dimensions(&image_path).unwrap_or((0, 0));
5454+4955 let image = Image {
5056 path: image_path.clone(),
5757+ width,
5858+ height,
5159 assets_dir: self.assets_dir.clone(),
6060+ // TODO: This is gonna re-read the file, even though we already had to in order to get dimensions, perhaps we can re-use the same data?
5261 hash: calculate_hash(&image_path, Some(HashConfig::Image(&options))),
5362 options: if options == ImageOptions::default() {
5463 None
···232241}
233242234243fn calculate_hash(path: &PathBuf, options: Option<HashConfig>) -> String {
235235- let content = fs::read(path).unwrap();
244244+ let start_time = Instant::now();
245245+ let content =
246246+ fs::read(path).unwrap_or_else(|_| panic!("Failed to read asset file: {:?}", path));
247247+248248+ // Pre-allocate a single buffer to hash at once
249249+ let mut buf = Vec::with_capacity(content.len() + 256);
250250+ buf.extend_from_slice(&content);
251251+ buf.extend_from_slice(path.to_string_lossy().as_bytes());
236252237237- // TODO: Consider using xxhash for both performance and to match Rolldown's hashing
238238- let mut hasher = blake3::Hasher::new();
239239- hasher.update(&content);
240240- hasher.update(path.to_string_lossy().as_bytes());
241253 if let Some(options) = options {
242254 match options {
243255 HashConfig::Image(opts) => {
244244- let mut buf = Vec::new();
245256 if let Some(width) = opts.width {
246257 buf.extend_from_slice(&width.to_le_bytes());
247258 }
···251262 if let Some(format) = &opts.format {
252263 buf.extend_from_slice(&format.to_hash_value().to_le_bytes());
253264 }
254254-255255- hasher.update(&buf);
256265 }
257266 HashConfig::Style(opts) => {
258258- let mut buf = Vec::new();
259259- buf.extend_from_slice(&[opts.tailwind as u8]);
260260- hasher.update(&buf);
267267+ buf.push(opts.tailwind as u8);
261268 }
262269 }
263270 }
264264- let hash = hasher.finalize();
271271+272272+ let hash = xxh3_64(&buf); // one-shot, much faster than streaming
273273+274274+ debug!(
275275+ "Calculated hash for asset {:?} in {:?}",
276276+ path,
277277+ start_time.elapsed()
278278+ );
265279266266- // Take the first 5 characters of the hex string for a short hash like "al3hx"
267267- hash.to_hex()[..5].to_string()
280280+ // TODO: This works, but perhaps we can generate prettier hashes, see https://github.com/rolldown/rolldown/blob/abf62c45d7a69b42dab4bff92095e320b418e9b8/crates/rolldown_utils/src/xxhash.rs
281281+ let hex = format!("{:016x}", hash);
282282+ hex[..5].to_string()
268283}
269284270285trait InternalAsset {
···8989 content: &str,
9090 shortcodes: &MarkdownShortcodes,
9191 ) -> Result<String, String> {
9292- preprocess_shortcodes(content, shortcodes, None)
9292+ preprocess_shortcodes(content, shortcodes, None, None)
9393 }
94949595 // Helper function that automatically wraps RouteContext in Some() for existing tests
···9898 shortcodes: &MarkdownShortcodes,
9999 route_ctx: &mut RouteContext,
100100 ) -> Result<String, String> {
101101- preprocess_shortcodes(content, shortcodes, Some(route_ctx))
101101+ preprocess_shortcodes(content, shortcodes, Some(route_ctx), None)
102102 }
103103104104 #[test]
···114114 #[test]
115115 fn test_simple_self_closing_shortcode() {
116116 let shortcodes = create_test_shortcodes();
117117- let content = "Before {{ simple }} after";
117117+ let content = "Before {{ simple /}} after";
118118 let result = with_test_route_context(|route_ctx| {
119119 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx).unwrap()
120120 });
···124124 #[test]
125125 fn test_shortcode_with_arguments() {
126126 let shortcodes = create_test_shortcodes();
127127- let content = "{{ greet name=Alice }}";
127127+ let content = "{{ greet name=Alice /}}";
128128 let result = with_test_route_context(|route_ctx| {
129129 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx).unwrap()
130130 });
···134134 #[test]
135135 fn test_multiple_arguments() {
136136 let shortcodes = create_test_shortcodes();
137137- let content = "{{ date format=iso year=2023 }}";
137137+ let content = "{{ date day=Monday month=January year=2024 /}}";
138138 let result = with_test_route_context(|route_ctx| {
139139 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx).unwrap()
140140 });
141141- assert_eq!(result, "DATE[iso]");
141141+ assert_eq!(result, "DATE[default]");
142142 }
143143144144 #[test]
145145 fn test_frontmatter_shortcodes() {
146146 let shortcodes = create_test_shortcodes();
147147 let content = r#"---
148148-title: {{ greet name=Blog }}
149149-date: {{ date format=iso }}
148148+title: {{ greet name=Blog /}}
149149+date: {{ date format=iso /}}
150150---
151151152152# Content here"#;
···165165 #[test]
166166 fn test_shortcodes_in_headings() {
167167 let shortcodes = create_test_shortcodes();
168168- let content = "# {{ greet name=Header }}\n\n## Section {{ date format=short }}";
168168+ let content = "# {{ greet name=Header /}}\n\n## Section {{ date format=short /}}";
169169 let result = with_test_route_context(|route_ctx| {
170170 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx).unwrap()
171171 });
···175175 #[test]
176176 fn test_shortcodes_in_links() {
177177 let shortcodes = create_test_shortcodes();
178178- let content = "[{{ greet name=Link }}](https://example.com)";
178178+ let content = "[{{ greet name=Link /}}](https://example.com)";
179179 let result = with_test_route_context(|route_ctx| {
180180 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx).unwrap()
181181 });
···185185 #[test]
186186 fn test_shortcodes_in_code_blocks() {
187187 let shortcodes = create_test_shortcodes();
188188- let content = "```\nSome code with {{ simple }}\n```";
188188+ let content = "```\nSome code with {{ simple /}}\n```";
189189 let result = with_test_route_context(|route_ctx| {
190190 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx).unwrap()
191191 });
···205205 #[test]
206206 fn test_nested_shortcodes_in_block() {
207207 let shortcodes = create_test_shortcodes();
208208- let content = "{{ section title=Main }}\nHello {{ greet name=World }}!\n{{ /section }}";
208208+ let content = "{{ section title=Main }}\nHello {{ greet name=World /}}!\n{{ /section }}";
209209 let result = with_test_route_context(|route_ctx| {
210210 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
211211 })
···222222 let content = r#"{{ section title=Outer }}
223223{{ alert type=warning }}
224224{{ highlight lang=javascript }}
225225-console.log("{{ greet name=Nested }}");
225225+console.log("{{ greet name=Nested /}}");
226226{{ /highlight }}
227227{{ /alert }}
228228{{ /section }}"#;
···243243 #[test]
244244 fn test_multiple_shortcodes_same_line() {
245245 let shortcodes = create_test_shortcodes();
246246- let content = "{{ greet name=Alice }} and {{ greet name=Bob }}";
246246+ let content = "{{ greet name=Alice /}} and {{ greet name=Bob /}}";
247247 let result = with_test_route_context(|route_ctx| {
248248 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
249249 })
···254254 #[test]
255255 fn test_shortcodes_in_lists() {
256256 let shortcodes = create_test_shortcodes();
257257- let content = r#"- Item 1: {{ greet name=First }}
258258-- Item 2: {{ date format=short }}
259259-- Item 3: {{ simple }}"#;
257257+ let content = r#"- Item 1: {{ greet name=First /}}
258258+- Item 2: {{ date format=short /}}
259259+- Item 3: {{ simple /}}"#;
260260 let result = with_test_route_context(|route_ctx| {
261261 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
262262 })
···272272 let shortcodes = create_test_shortcodes();
273273 let content = r#"| Name | Greeting |
274274|------|----------|
275275-| Alice | {{ greet name=Alice }} |
276276-| Bob | {{ greet name=Bob }} |"#;
275275+| Alice | {{ greet name=Alice /}} |
276276+| Bob | {{ greet name=Bob /}} |"#;
277277 let result = with_test_route_context(|route_ctx| {
278278 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
279279 })
···288288 #[test]
289289 fn test_shortcodes_with_special_characters() {
290290 let shortcodes = create_test_shortcodes();
291291- let content = "Before\n{{ simple }}\nAfter\n\n{{ greet name=Test }}";
291291+ let content = "Before\n{{ simple /}}\nAfter\n\n{{ greet name=Test /}}";
292292 let result = with_test_route_context(|route_ctx| {
293293 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
294294 })
···299299 #[test]
300300 fn test_error_unknown_shortcode() {
301301 let shortcodes = create_test_shortcodes();
302302- let content = "{{ unknown_shortcode }}";
302302+ let content = "{{ unknown_shortcode /}}";
303303 let result = with_test_route_context(|route_ctx| {
304304 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
305305 });
···326326 #[test]
327327 fn test_unclosed_shortcode_with_valid_shortcode_after() {
328328 let shortcodes = create_test_shortcodes();
329329- let content = "Before {{ unclosed. Then {{ simple }} after.";
329329+ let content = "Before {{ unclosed. Then {{ simple /}} after.";
330330 let result = with_test_route_context(|route_ctx| {
331331 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
332332 })
···361361 #[test]
362362 fn test_error_invalid_argument_format() {
363363 let shortcodes = create_test_shortcodes();
364364- let content = "{{ greet name Alice }}";
364364+ let content = "{{ greet name Alice /}}";
365365 let result = with_test_route_context(|route_ctx| {
366366 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
367367 });
···383383 #[test]
384384 fn test_whitespace_handling() {
385385 let shortcodes = create_test_shortcodes();
386386- let content = "{{ simple }}";
386386+ let content = "{{ simple /}}";
387387 let result = with_test_route_context(|route_ctx| {
388388 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
389389 })
···394394 #[test]
395395 fn test_whitespace_in_arguments() {
396396 let shortcodes = create_test_shortcodes();
397397- let content = "{{ greet name=Alice }}";
397397+ let content = "{{ greet name=Alice /}}";
398398 let result = with_test_route_context(|route_ctx| {
399399 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
400400 })
···406406 fn test_complex_markdown_document() {
407407 let shortcodes = create_test_shortcodes();
408408 let content = r#"---
409409-title: {{ greet name=Blog }}
410410-author: {{ greet name=Author }}
409409+title: {{ greet name=Blog /}}
410410+author: {{ greet name=Author /}}
411411---
412412413413-# {{ greet name=Reader }}
413413+# {{ greet name=Reader /}}
414414415415-Welcome to my blog! Today is {{ date format=full }}.
415415+Welcome to my blog! Today is {{ date format=full /}}.
416416417417## Code Example
418418419419{{ highlight lang=rust }}
420420fn main() {
421421- println!("{{ greet name=Rust }}");
421421+ println!("{{ greet name=Rust /}}");
422422}
423423{{ /highlight }}
424424425425## Alert Section
426426427427{{ alert type=info }}
428428-This is an important message with {{ simple }} content.
428428+This is an important message with {{ simple /}} content.
429429{{ /alert }}
430430431431-- List item with {{ greet name=Item }}
432432-- Another item: {{ date format=short }}
431431+- List item with {{ greet name=Item /}}
432432+- Another item: {{ date format=short /}}
433433434434-> Quote with {{ simple }} shortcode
434434+> Quote with {{ simple /}} shortcode
435435436436-[Link with {{ greet name=Link }}](http://example.com)"#;
436436+[Link with {{ greet name=Link /}}](http://example.com)"#;
437437438438 let result = with_test_route_context(|route_ctx| {
439439 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
···475475 #[test]
476476 fn test_markdown_integration_headings_with_shortcodes() {
477477 let shortcodes = create_test_shortcodes();
478478- let content = "# {{ greet name=Title }}\n\n## Section {{ date format=short }}";
478478+ let content = "# {{ greet name=Title /}}\n\n## Section {{ date format=short /}}";
479479480480 // Test shortcode preprocessing first
481481 let processed = with_test_route_context(|route_ctx| {
···488488 #[test]
489489 fn test_markdown_integration_emphasis_with_shortcodes() {
490490 let shortcodes = create_test_shortcodes();
491491- let content = "*{{ greet name=Italic }}* and **{{ greet name=Bold }}**";
491491+ let content = "*{{ greet name=Italic /}}* and **{{ greet name=Bold /}}**";
492492 let processed = with_test_route_context(|route_ctx| {
493493 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
494494 })
···499499 #[test]
500500 fn test_markdown_integration_code_spans_with_shortcodes() {
501501 let shortcodes = create_test_shortcodes();
502502- let content = "Use `{{ simple }}` in your code";
502502+ let content = "Use `{{ simple /}}` in your code";
503503 let processed = with_test_route_context(|route_ctx| {
504504 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
505505 })
···510510 #[test]
511511 fn test_markdown_integration_blockquotes_with_shortcodes() {
512512 let shortcodes = create_test_shortcodes();
513513- let content = "> {{ greet name=Quote }}\n> \n> {{ simple }}";
513513+ let content = "> {{ greet name=Quote /}}\n> \n> {{ simple /}}";
514514 let processed = with_test_route_context(|route_ctx| {
515515 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
516516 })
···521521 #[test]
522522 fn test_markdown_integration_nested_lists_with_shortcodes() {
523523 let shortcodes = create_test_shortcodes();
524524- let content = r#"1. {{ greet name=First }}
525525- - Nested {{ simple }}
526526- - {{ date format=iso }}
527527-2. {{ greet name=Second }}
528528- 1. Numbered {{ simple }}
529529- 2. {{ greet name=Nested }}"#;
524524+ let content = r#"1. {{ greet name=First /}}
525525+ - Nested {{ simple /}}
526526+ - {{ date format=iso /}}
527527+2. {{ greet name=Second /}}
528528+ 1. Numbered {{ simple /}}
529529+ 2. {{ greet name=Nested /}}"#;
530530 let processed = with_test_route_context(|route_ctx| {
531531 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
532532 })
···543543 #[test]
544544 fn test_markdown_integration_complex_tables_with_shortcodes() {
545545 let shortcodes = create_test_shortcodes();
546546- let content = r#"| **{{ greet name=Header }}** | _{{ date format=long }}_ |
546546+ let content = r#"| **{{ greet name=Header /}}** | _{{ date format=long /}}_ |
547547|:---------------------------|-------------------------:|
548548-| {{ simple }} | {{ greet name=Cell }} |
549549-| `{{ greet name=Code }}` | > {{ simple }} |"#;
548548+| {{ simple /}} | {{ greet name=Cell /}} |
549549+| `{{ greet name=Code /}}` | > {{ simple /}} |"#;
550550 let processed = with_test_route_context(|route_ctx| {
551551 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
552552 })
···561561 #[test]
562562 fn test_markdown_integration_task_lists_with_shortcodes() {
563563 let shortcodes = create_test_shortcodes();
564564- let content = r#"- [x] {{ greet name=Done }}
565565-- [ ] {{ simple }}
566566-- [ ] {{ date format=todo }}"#;
564564+ let content = r#"- [x] {{ greet name=Done /}}
565565+- [ ] {{ simple /}}
566566+- [ ] {{ date format=todo /}}"#;
567567 let processed = with_test_route_context(|route_ctx| {
568568 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
569569 })
···577577 #[test]
578578 fn test_markdown_integration_strikethrough_with_shortcodes() {
579579 let shortcodes = create_test_shortcodes();
580580- let content = "~~{{ greet name=Deleted }}~~ and {{ simple }}";
580580+ let content = "~~{{ greet name=Deleted /}}~~ and {{ simple /}}";
581581 let processed = with_test_route_context(|route_ctx| {
582582 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
583583 })
···588588 #[test]
589589 fn test_markdown_integration_horizontal_rules_with_shortcodes() {
590590 let shortcodes = create_test_shortcodes();
591591- let content = "{{ greet name=Before }}\n\n---\n\n{{ simple }}";
591591+ let content = "{{ greet name=Before /}}\n\n---\n\n{{ simple /}}";
592592 let processed = with_test_route_context(|route_ctx| {
593593 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
594594 })
···599599 #[test]
600600 fn test_markdown_integration_footnotes_with_shortcodes() {
601601 let shortcodes = create_test_shortcodes();
602602- let content = "{{ greet name=Text }}[^1]\n\n[^1]: {{ simple }}";
602602+ let content = "{{ greet name=Text /}}[^1]\n\n[^1]: {{ simple /}}";
603603 let processed = with_test_route_context(|route_ctx| {
604604 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
605605 })
···610610 #[test]
611611 fn test_markdown_integration_complex_links_with_shortcodes() {
612612 let shortcodes = create_test_shortcodes();
613613- let content = r#"[{{ greet name=Link }}](https://example.com "{{ simple }}")
613613+ let content = r#"[{{ greet name=Link /}}](https://example.com "{{ simple /}}")
614614615615-
615615+
616616617617-[Reference {{ simple }}][ref]
617617+[Reference {{ simple /}}][ref]
618618619619-[ref]: https://example.com "{{ greet name=RefTitle }}""#;
619619+[ref]: https://example.com "{{ greet name=RefTitle /}}""#;
620620 let processed = with_test_route_context(|route_ctx| {
621621 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
622622 })
···636636 let shortcodes = create_test_shortcodes();
637637 let content = r#"```rust
638638fn main() {
639639- println!("{{ greet name=Rust }}");
640640- // {{ simple }}
639639+ println!("{{ greet name=Rust /}}");
640640+ // {{ simple /}}
641641}
642642```
643643644644-```{{ greet name=Language }}
645645-{{ simple }}
644644+```{{ greet name=Language /}}
645645+{{ simple /}}
646646```"#;
647647 let processed = with_test_route_context(|route_ctx| {
648648 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
···665665 fn test_markdown_integration_html_blocks_with_shortcodes() {
666666 let shortcodes = create_test_shortcodes();
667667 let content = r#"<div class="custom">
668668- <h2>{{ greet name=HTML }}</h2>
669669- <p>{{ simple }}</p>
668668+ <h2>{{ greet name=HTML /}}</h2>
669669+ <p>{{ simple /}}</p>
670670</div>
671671672672-<img src="test.jpg" alt="{{ greet name=Alt }}" title="{{ date format=attr }}">"#;
672672+<img src="test.jpg" alt="{{ greet name=Alt /}}" title="{{ date format=attr /}}">"#;
673673 let processed = with_test_route_context(|route_ctx| {
674674 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
675675 })
···686686 #[test]
687687 fn test_markdown_integration_math_blocks_with_shortcodes() {
688688 let shortcodes = create_test_shortcodes();
689689- let content = r#"Inline math: ${{ simple }}$
689689+ let content = r#"Inline math: ${{ simple /}}$
690690691691Block math:
692692$$
693693-{{ greet name=Math }}
693693+{{ greet name=Math /}}
694694$$
695695696696-{{ greet name=Text }} with $x = {{ simple }}$ inline."#;
696696+{{ greet name=Text /}} with $x = {{ simple /}}$ inline."#;
697697 let processed = with_test_route_context(|route_ctx| {
698698 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
699699 })
···713713 fn test_markdown_integration_frontmatter_yaml_with_shortcodes() {
714714 let shortcodes = create_test_shortcodes();
715715 let content = r#"---
716716-title: "{{ greet name=Blog }}"
717717-description: {{ simple }}
716716+title: "{{ greet name=Blog /}}"
717717+description: {{ simple /}}
718718tags:
719719- - {{ greet name=Tag1 }}
720720- - {{ simple }}
719719+ - {{ greet name=Tag1 /}}
720720+ - {{ simple /}}
721721metadata:
722722- created: {{ date format=iso }}
723723- author: {{ greet name=Author }}
722722+ created: {{ date format=iso /}}
723723+ author: {{ greet name=Author /}}
724724---
725725726726-# {{ greet name=Content }}
726726+# {{ greet name=Content /}}
727727728728-{{ simple }}"#;
728728+{{ simple /}}"#;
729729 let processed = with_test_route_context(|route_ctx| {
730730 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
731731 })
···751751 fn test_markdown_integration_block_shortcodes_with_markdown() {
752752 let shortcodes = create_test_shortcodes();
753753 let content = r#"{{ section title=Main }}
754754-# {{ greet name=Header }}
754754+# {{ greet name=Header /}}
755755756756-**{{ greet name=Bold }}** and *{{ simple }}*
756756+**{{ greet name=Bold /}}** and *{{ simple /}}*
757757758758-- {{ greet name=Item1 }}
759759-- {{ simple }}
758758+- {{ greet name=Item1 /}}
759759+- {{ simple /}}
760760761761-> {{ greet name=Quote }}
761761+> {{ greet name=Quote /}}
762762763763{{ /section }}"#;
764764 let processed = with_test_route_context(|route_ctx| {
···782782 #[test]
783783 fn test_markdown_integration_edge_cases() {
784784 let shortcodes = create_test_shortcodes();
785785- let content = r#"{{ greet name=Start }}
785785+ let content = r#"{{ greet name=Start /}}
786786787787-<!-- {{ simple }} in comment -->
787787+<!-- {{ simple /}} in comment -->
788788789789{{ highlight lang=markdown }}
790790-# {{ greet name=NestedMD }}
791791-{{ simple }}
790790+# {{ greet name=NestedMD /}}
791791+{{ simple /}}
792792{{ /highlight }}
793793794794-`{{ greet name=BacktickCode }}`
794794+`{{ greet name=BacktickCode /}}`
795795796796-{{ greet name=End }}"#;
796796+{{ greet name=End /}}"#;
797797 let processed = with_test_route_context(|route_ctx| {
798798 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
799799 })
···817817 fn test_markdown_integration_real_world_blog_post() {
818818 let shortcodes = create_test_shortcodes();
819819 let content = r#"---
820820-title: {{ greet name=BlogPost }}
821821-date: {{ date format=iso }}
822822-author: {{ greet name=Writer }}
823823-tags: [{{ simple }}, {{ greet name=Tutorial }}]
820820+title: {{ greet name=BlogPost /}}
821821+date: {{ date format=iso /}}
822822+author: {{ greet name=Writer /}}
823823+tags: [{{ simple /}}, {{ greet name=Tutorial /}}]
824824---
825825826826-# {{ greet name=Reader }}!
826826+# {{ greet name=Reader /}}!
827827828828-Welcome to my blog post about {{ simple }}.
828828+Welcome to my blog post about {{ simple /}}.
829829830830## What we'll cover
831831832832-1. **{{ greet name=Introduction }}** - Getting started
833833-2. **{{ simple }}** basics
834834-3. Advanced {{ greet name=Techniques }}
832832+1. **{{ greet name=Introduction /}}** - Getting started
833833+2. **{{ simple /}}** basics
834834+3. Advanced {{ greet name=Techniques /}}
835835836836{{ alert type=info }}
837837-💡 **Tip**: {{ greet name=Remember }} to {{ simple }}!
837837+💡 **Tip**: {{ greet name=Remember /}} to {{ simple /}}!
838838{{ /alert }}
839839840840## Code Example
841841842842{{ highlight lang=rust }}
843843fn main() {
844844- println!("{{ greet name=World }}!");
845845- // {{ simple }}
844844+ println!("{{ greet name=World /}}!");
845845+ // {{ simple /}}
846846}
847847{{ /highlight }}
848848849849### Task List
850850851851-- [x] {{ greet name=Setup }}
852852-- [ ] {{ simple }}
853853-- [ ] {{ greet name=Publish }}
851851+- [x] {{ greet name=Setup /}}
852852+- [ ] {{ simple /}}
853853+- [ ] {{ greet name=Publish /}}
854854855855---
856856857857-> "{{ greet name=Quote }}" - {{ simple }}
857857+> "{{ greet name=Quote /}}" - {{ simple /}}
858858859859{{ section title=Resources }}
860860-- [Documentation](https://docs.rs) - {{ simple }}
861861-- [GitHub](https://github.com) - {{ greet name=Source }}
860860+- [Documentation](https://docs.rs) - {{ simple /}}
861861+- [GitHub](https://github.com) - {{ greet name=Source /}}
862862{{ /section }}
863863864864-*Published on {{ date format=long }}*"#;
864864+*Published on {{ date format=long /}}*"#;
865865866866 let processed = with_test_route_context(|route_ctx| {
867867 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
···922922923923 // Test invalid names that should be treated as literal text
924924 let test_cases = vec![
925925- ("{{ 123invalid }}", "{{ 123invalid }}"), // starts with number
926926- ("{{ -invalid }}", "{{ -invalid }}"), // starts with dash
925925+ ("{{ 123invalid /}}", "{{ 123invalid /}}"), // starts with number
926926+ ("{{ -invalid }}", "{{ -invalid }}"), // starts with dash
927927 ("{{ invalid-name }}", "{{ invalid-name }}"), // contains dash
928928 ("{{ invalid.name }}", "{{ invalid.name }}"), // contains dot
929929 ("{{ invalid@name }}", "{{ invalid@name }}"), // contains special char
···946946 shortcodes.register("name123", |_, _| "NAME123".to_string());
947947948948 let test_cases = vec![
949949- ("{{ valid_name }}", "VALID_NAME"),
950950- ("{{ ValidName }}", "VALID_NAME_CAMEL"),
951951- ("{{ _underscore }}", "UNDERSCORE"),
952952- ("{{ name123 }}", "NAME123"),
953953- ("{{ a }}", "{{ a }}"), // single char is invalid (too short for pattern)
949949+ ("{{ valid_name /}}", "VALID_NAME"),
950950+ ("{{ ValidName /}}", "VALID_NAME_CAMEL"),
951951+ ("{{ _underscore /}}", "UNDERSCORE"),
952952+ ("{{ name123 /}}", "NAME123"),
953953+ ("{{ a /}}", "{{ a /}}"), // single char is invalid (too short for pattern)
954954 ];
955955956956 for (input, expected) in test_cases {
···968968 (r#"\{{"hello"}}"#, r#"\{{"hello"}}"#),
969969 (r#"Before \{{test}} after"#, r#"Before \{{test}} after"#),
970970 (
971971- r#"\{{invalid}} and {{ simple }}"#,
971971+ r#"\{{invalid}} and {{ simple /}}"#,
972972 r#"\{{invalid}} and SIMPLE_OUTPUT"#,
973973 ),
974974 ];
···985985986986 // Test various edge cases - invalid names should be treated as literal text
987987 let test_cases = vec![
988988- ("{{ 1 }}", "{{ 1 }}"), // single digit (invalid)
989989- ("{{ _ }}", "{{ _ }}"), // single underscore (invalid - too short)
990990- ("{{ 1A }}", "{{ 1A }}"), // invalid: digit + letter
991991- ("{{ café }}", "{{ café }}"), // invalid: non-ASCII
988988+ ("{{ 1 /}}", "{{ 1 /}}"), // single digit (invalid)
989989+ ("{{ _ /}}", "{{ _ /}}"), // single underscore (invalid - too short)
990990+ ("{{ 1A /}}", "{{ 1A /}}"), // invalid: digit + letter
991991+ ("{{ café /}}", "{{ café /}}"), // invalid: non-ASCII
992992 ];
993993994994 for (input, expected) in test_cases {
···997997 }
998998999999 // Test valid names that aren't registered - should get "Unknown shortcode" error
10001000- let valid_but_unregistered =
10011001- vec!["{{ _a }}", "{{ a_ }}", "{{ A1 }}", "{{ valid_name123 }}"];
10001000+ let valid_but_unregistered = vec![
10011001+ "{{ _a /}}",
10021002+ "{{ a_ /}}",
10031003+ "{{ A1 /}}",
10041004+ "{{ valid_name123 /}}",
10051005+ ];
1002100610031007 for input in valid_but_unregistered {
10041008 let result = preprocess_shortcodes_simple(input, &shortcodes);
···10301034 let shortcodes = create_test_shortcodes();
1031103510321036 let content = r#"
10331033-Valid: {{ simple }}
10341034-Invalid number start: {{ 123invalid }}
10351035-Valid underscore: {{ greet name=Test }}
10371037+Valid: {{ simple /}}
10381038+Invalid number start: {{ 123invalid /}}
10391039+Valid underscore: {{ greet name=Test /}}
10361040Invalid dash: {{ invalid-name }}
10371037-Another valid: {{ date format=iso }}
10411041+Another valid: {{ date format=iso /}}
10381042"#;
1039104310401044 let result = with_test_route_context(|route_ctx| {
···10431047 .unwrap();
10441048 let expected = r#"
10451049Valid: SIMPLE_OUTPUT
10461046-Invalid number start: {{ 123invalid }}
10501050+Invalid number start: {{ 123invalid /}}
10471051Valid underscore: Hello, Test!
10481052Invalid dash: {{ invalid-name }}
10491053Another valid: DATE[iso]
···10571061 let shortcodes = create_test_shortcodes();
1058106210591063 // Test double quotes
10601060- let content = r#"{{ greet name="Hello World" }}"#;
10641064+ let content = r#"{{ greet name="Hello World" /}}"#;
10611065 let result = with_test_route_context(|route_ctx| {
10621066 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
10631067 })
···10651069 assert_eq!(result, "Hello, Hello World!");
1066107010671071 // Test single quotes
10681068- let content = r#"{{ greet name='Hello World' }}"#;
10721072+ let content = r#"{{ greet name='Hello World' /}}"#;
10691073 let result = with_test_route_context(|route_ctx| {
10701074 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
10711075 })
···10821086 format!("{} - {}", text, author)
10831087 });
1084108810851085- let content = r#"{{ message text="Hello World" author=John }}"#;
10891089+ let content = r#"{{ message text="Hello World" author=John /}}"#;
10861090 let result = with_test_route_context(|route_ctx| {
10871091 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
10881092 })
···10971101 args.get_str("value").unwrap_or("").to_string()
10981102 });
1099110311001100- let content = r#"{{ special value="Hello, World! How are you?" }}"#;
11041104+ let content = r#"{{ special value="Hello, World! How are you?" /}}"#;
11011105 let result = with_test_route_context(|route_ctx| {
11021106 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
11031107 })
···11091113 fn test_error_unclosed_quotes() {
11101114 let shortcodes = create_test_shortcodes();
1111111511121112- let content = r#"{{ greet name="Hello World }}"#;
11161116+ let content = r#"{{ greet name="Hello World /}}"#;
11131117 let result = with_test_route_context(|route_ctx| {
11141118 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
11151119 });
···11251129 format!("'{}'", value)
11261130 });
1127113111281128- let content = r#"{{ empty value="" }}"#;
11321132+ let content = r#"{{ empty value="" /}}"#;
11291133 let result = with_test_route_context(|route_ctx| {
11301134 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
11311135 })
11321136 .unwrap();
11331137 assert_eq!(result, "''");
11381138+ }
11391139+11401140+ #[test]
11411141+ fn test_escaped_quotes_in_arguments() {
11421142+ let mut shortcodes = create_test_shortcodes();
11431143+ shortcodes.register("quote", |args, _| {
11441144+ let message = args.get_str("message").unwrap_or("");
11451145+ format!("Quote: {}", message)
11461146+ });
11471147+11481148+ // Test escaped double quotes
11491149+ let content = r#"{{ quote message="Me answering \"thanks man glad you like it\" to someone saying a feature is stupid" /}}"#;
11501150+ let result = with_test_route_context(|route_ctx| {
11511151+ preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
11521152+ })
11531153+ .unwrap();
11541154+ assert_eq!(
11551155+ result,
11561156+ r#"Quote: Me answering "thanks man glad you like it" to someone saying a feature is stupid"#
11571157+ );
11581158+11591159+ // Test escaped single quotes
11601160+ let content = r#"{{ quote message='It\'s a "great" day!' /}}"#;
11611161+ let result = with_test_route_context(|route_ctx| {
11621162+ preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
11631163+ })
11641164+ .unwrap();
11651165+ assert_eq!(result, r#"Quote: It's a "great" day!"#);
11661166+11671167+ // Test escaped backslashes
11681168+ let content = r#"{{ quote message="Path: C:\\Users\\name\\file.txt" /}}"#;
11691169+ let result = with_test_route_context(|route_ctx| {
11701170+ preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
11711171+ })
11721172+ .unwrap();
11731173+ assert_eq!(result, r#"Quote: Path: C:\Users\name\file.txt"#);
11741174+11751175+ // Test escaped newlines and tabs
11761176+ let content = r#"{{ quote message="Line 1\nLine 2\tTabbed" /}}"#;
11771177+ let result = with_test_route_context(|route_ctx| {
11781178+ preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
11791179+ })
11801180+ .unwrap();
11811181+ assert_eq!(result, "Quote: Line 1\nLine 2\tTabbed");
11821182+11831183+ // Test mixed escape sequences
11841184+ let content = r#"{{ quote message="Say \"Hello\", then press \\n for newline\nDone!" /}}"#;
11851185+ let result = with_test_route_context(|route_ctx| {
11861186+ preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
11871187+ })
11881188+ .unwrap();
11891189+ assert_eq!(
11901190+ result,
11911191+ "Quote: Say \"Hello\", then press \\n for newline\nDone!"
11921192+ );
11931193+ }
11941194+11951195+ #[test]
11961196+ fn test_self_closing_shortcode_syntax() {
11971197+ let mut shortcodes = create_test_shortcodes();
11981198+ shortcodes.register("current_date", |_args, _| "2024-01-01".to_string());
11991199+ shortcodes.register("user", |args, _| {
12001200+ let name = args.get_str("name").unwrap_or("Anonymous");
12011201+ format!("User: {}", name)
12021202+ });
12031203+12041204+ // Test basic self-closing shortcode
12051205+ let content = r#"Today is {{ current_date /}}"#;
12061206+ let result = with_test_route_context(|route_ctx| {
12071207+ preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
12081208+ })
12091209+ .unwrap();
12101210+ assert_eq!(result, "Today is 2024-01-01");
12111211+12121212+ // Test self-closing shortcode with arguments
12131213+ let content = r#"{{ user name="Alice" /}}"#;
12141214+ let result = with_test_route_context(|route_ctx| {
12151215+ preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
12161216+ })
12171217+ .unwrap();
12181218+ assert_eq!(result, "User: Alice");
12191219+12201220+ // Test self-closing shortcode with spaces before /
12211221+ let content = r#"{{ user name="Bob" /}}"#;
12221222+ let result = with_test_route_context(|route_ctx| {
12231223+ preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
12241224+ })
12251225+ .unwrap();
12261226+ assert_eq!(result, "User: Bob");
12271227+12281228+ // Test multiple self-closing shortcodes
12291229+ let content = r#"{{ user name="Alice" /}} and {{ user name="Bob" /}}"#;
12301230+ let result = with_test_route_context(|route_ctx| {
12311231+ preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
12321232+ })
12331233+ .unwrap();
12341234+ assert_eq!(result, "User: Alice and User: Bob");
12351235+ }
12361236+12371237+ #[test]
12381238+ fn test_block_shortcode_requires_closing_tag() {
12391239+ let shortcodes = create_test_shortcodes();
12401240+12411241+ // This should now be an error because it's not self-closing and has no closing tag
12421242+ let content = r#"{{ highlight lang="rust" }}"#;
12431243+ let result = with_test_route_context(|route_ctx| {
12441244+ preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
12451245+ });
12461246+ assert!(result.is_err());
12471247+ let error_msg = result.unwrap_err();
12481248+ assert!(error_msg.contains("missing its closing tag"));
12491249+ assert!(error_msg.contains("Use '{{ highlight /}}' for self-closing"));
12501250+ }
12511251+12521252+ #[test]
12531253+ fn test_ambiguous_shortcode_resolution() {
12541254+ let mut shortcodes = create_test_shortcodes();
12551255+ shortcodes.register("img", |args, _| {
12561256+ let src = args.get_str("src").unwrap_or("");
12571257+ format!("<img src=\"{}\">", src)
12581258+ });
12591259+12601260+ // This scenario would have been ambiguous in the old syntax:
12611261+ // Two shortcodes where the first could mistakenly consume the second as body
12621262+12631263+ // Using new self-closing syntax - should work correctly
12641264+ let content = r#"{{ img src="photo.jpg" /}} {{ img src="photo2.jpg" /}}"#;
12651265+ let result = with_test_route_context(|route_ctx| {
12661266+ preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
12671267+ })
12681268+ .unwrap();
12691269+ assert_eq!(result, r#"<img src="photo.jpg"> <img src="photo2.jpg">"#);
12701270+12711271+ // Block shortcode with proper closing tags should still work
12721272+ let content = r#"{{ highlight lang="rust" }}
12731273+let x = 5;
12741274+{{ /highlight }}
12751275+12761276+{{ highlight lang="js" }}
12771277+const y = 10;
12781278+{{ /highlight }}"#;
12791279+ let result = with_test_route_context(|route_ctx| {
12801280+ preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx)
12811281+ })
12821282+ .unwrap();
12831283+ assert!(result.contains(r#"<code lang="rust">"#));
12841284+ assert!(result.contains("let x = 5;"));
12851285+ assert!(result.contains(r#"<code lang="js">"#));
12861286+ assert!(result.contains("const y = 10;"));
11341287 }
11351288}
+5-3
crates/maudit/src/lib.rs
···2626#[cfg(feature = "maud")]
2727#[cfg_attr(docsrs, doc(cfg(feature = "maud")))]
2828pub mod maud {
2929- //! Allows to use [Maud](https://maud.lambda.xyz), a macro for writing HTML templates in Rust.
3030- //!
3131- //! Maudit supports Maud by default, but you can use your own templating engine.
2929+ //! Allows to use [Maud](https://maud.lambda.xyz), a macro for writing HTML templates, ergonomically in your Maudit pages.
3230 //!
3331 //! ## Example
3432 //! ```rs
···5957use logging::init_logging;
6058use page::FullPage;
61596060+/// Returns whether Maudit is running in development mode (through `maudit dev`).
6161+///
6262+/// This can be useful to conditionally enable features or logging that should only be active during development.
6363+/// Oftentimes, this is used to disable some expensive operations that would slow down build times during development.
6264pub fn is_dev() -> bool {
6365 if option_env!("MAUDIT_DEV") == Some("true") {
6466 return true;
+1-4
crates/maudit/src/page.rs
···367367#[doc(hidden)]
368368/// Used internally by Maudit and should not be implemented by the user.
369369/// We expose it because [`maudit_macros::route`] implements it for the user behind the scenes.
370370-pub trait FullPage: InternalPage + Sync {
370370+pub trait FullPage: InternalPage + Sync + Send {
371371 fn render_internal(&self, ctx: &mut RouteContext) -> RenderResult;
372372 fn routes_internal(&self, context: &mut DynamicRouteContext) -> RoutesInternalResult;
373373}
···399399 DynamicRouteContext, Page, PaginationMeta, RenderResult, Route, RouteContext, RouteParams,
400400 Routes, get_page_slice, get_page_url, paginate_content,
401401 };
402402- // TODO: Remove this internal re-export when possible
403403- #[doc(hidden)]
404404- pub use super::{FullPage, InternalPage};
405402 pub use crate::assets::{Asset, Image, Style, StyleOptions};
406403 pub use crate::content::MarkdownContent;
407404 pub use maudit_macros::{Params, route};