Rust library to generate static websites
5
fork

Configure Feed

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

fix: markdown things (#34)

* fix: markdown things

* feat(assets): hashing performance and dimensions

* chore: random things

* chore: changeset

authored by

Erika and committed by
GitHub
4496b9bc da488ee6

+503 -244
+5
.sampo/changesets/ancient-baroness-vipunen.md
··· 1 + --- 2 + maudit: patch 3 + --- 4 + 5 + Changed syntax for self-closing shortcodes to require an explicit closing slash, ex: `{{ image /}}`
+5
.sampo/changesets/doughty-warden-erika.md
··· 1 + --- 2 + maudit: patch 3 + --- 4 + 5 + Adds width and height properties to images and generated html
+5
.sampo/changesets/majestic-duchess-kalma.md
··· 1 + --- 2 + maudit: patch 3 + --- 4 + 5 + Improve hashing performance for assets
+1 -1
Cargo.lock
··· 2794 2794 version = "0.5.1" 2795 2795 dependencies = [ 2796 2796 "base64", 2797 - "blake3", 2798 2797 "brk_rolldown", 2799 2798 "chrono", 2800 2799 "colored", ··· 2819 2818 "thumbhash", 2820 2819 "tokio", 2821 2820 "webp", 2821 + "xxhash-rust", 2822 2822 ] 2823 2823 2824 2824 [[package]]
+1
crates/maudit-cli/src/dev/404.html
··· 1 + <!-- TODO: Make this prettier --> 1 2 <!DOCTYPE html> 2 3 <html lang="en"> 3 4 <head>
+2 -2
crates/maudit/Cargo.toml
··· 44 44 rustc-hash = "2.1" 45 45 dyn-eq = "0.1.3" 46 46 thiserror = "2.0.9" 47 - blake3 = "1.8.2" 48 47 oxc_sourcemap = "4.1.0" 49 - rayon = "1.11.0" 48 + rayon = "1.11.0" 49 + xxhash-rust = "0.8.15"
+29 -14
crates/maudit/src/assets.rs
··· 1 + use ::image::image_dimensions; 1 2 use dyn_eq::DynEq; 3 + use log::debug; 2 4 use rustc_hash::FxHashSet; 3 5 use std::hash::Hash; 4 6 use std::sync::OnceLock; 7 + use std::time::Instant; 5 8 use std::{fs, path::PathBuf}; 9 + use xxhash_rust::xxh3::xxh3_64; 6 10 7 11 mod image; 8 12 pub mod image_cache; ··· 46 50 return image.clone(); 47 51 } 48 52 53 + let (width, height) = image_dimensions(&image_path).unwrap_or((0, 0)); 54 + 49 55 let image = Image { 50 56 path: image_path.clone(), 57 + width, 58 + height, 51 59 assets_dir: self.assets_dir.clone(), 60 + // 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? 52 61 hash: calculate_hash(&image_path, Some(HashConfig::Image(&options))), 53 62 options: if options == ImageOptions::default() { 54 63 None ··· 232 241 } 233 242 234 243 fn calculate_hash(path: &PathBuf, options: Option<HashConfig>) -> String { 235 - let content = fs::read(path).unwrap(); 244 + let start_time = Instant::now(); 245 + let content = 246 + fs::read(path).unwrap_or_else(|_| panic!("Failed to read asset file: {:?}", path)); 247 + 248 + // Pre-allocate a single buffer to hash at once 249 + let mut buf = Vec::with_capacity(content.len() + 256); 250 + buf.extend_from_slice(&content); 251 + buf.extend_from_slice(path.to_string_lossy().as_bytes()); 236 252 237 - // TODO: Consider using xxhash for both performance and to match Rolldown's hashing 238 - let mut hasher = blake3::Hasher::new(); 239 - hasher.update(&content); 240 - hasher.update(path.to_string_lossy().as_bytes()); 241 253 if let Some(options) = options { 242 254 match options { 243 255 HashConfig::Image(opts) => { 244 - let mut buf = Vec::new(); 245 256 if let Some(width) = opts.width { 246 257 buf.extend_from_slice(&width.to_le_bytes()); 247 258 } ··· 251 262 if let Some(format) = &opts.format { 252 263 buf.extend_from_slice(&format.to_hash_value().to_le_bytes()); 253 264 } 254 - 255 - hasher.update(&buf); 256 265 } 257 266 HashConfig::Style(opts) => { 258 - let mut buf = Vec::new(); 259 - buf.extend_from_slice(&[opts.tailwind as u8]); 260 - hasher.update(&buf); 267 + buf.push(opts.tailwind as u8); 261 268 } 262 269 } 263 270 } 264 - let hash = hasher.finalize(); 271 + 272 + let hash = xxh3_64(&buf); // one-shot, much faster than streaming 273 + 274 + debug!( 275 + "Calculated hash for asset {:?} in {:?}", 276 + path, 277 + start_time.elapsed() 278 + ); 265 279 266 - // Take the first 5 characters of the hex string for a short hash like "al3hx" 267 - hash.to_hex()[..5].to_string() 280 + // TODO: This works, but perhaps we can generate prettier hashes, see https://github.com/rolldown/rolldown/blob/abf62c45d7a69b42dab4bff92095e320b418e9b8/crates/rolldown_utils/src/xxhash.rs 281 + let hex = format!("{:016x}", hash); 282 + hex[..5].to_string() 268 283 } 269 284 270 285 trait InternalAsset {
+2
crates/maudit/src/assets/image.rs
··· 64 64 #[non_exhaustive] 65 65 pub struct Image { 66 66 pub path: PathBuf, 67 + pub width: u32, 68 + pub height: u32, 67 69 pub(crate) assets_dir: PathBuf, 68 70 pub(crate) hash: String, 69 71 pub(crate) options: Option<ImageOptions>,
+53 -48
crates/maudit/src/content/markdown.rs
··· 311 311 let content = if let Some(shortcodes) = options.map(|o| &o.shortcodes) 312 312 && !shortcodes.is_empty() 313 313 { 314 - preprocess_shortcodes(content, shortcodes, route_ctx.as_deref_mut()) 315 - .unwrap_or_else(|e| panic!("Failed to preprocess shortcodes: {}", e)) 314 + preprocess_shortcodes( 315 + content, 316 + shortcodes, 317 + route_ctx.as_deref_mut(), 318 + path.and_then(|p| p.to_str()), 319 + ) 320 + .unwrap_or_else(|e| panic!("Failed to preprocess shortcodes for {:?}: {}", path, e)) 316 321 } else { 317 322 content.to_string() 318 323 }; ··· 965 970 ..Default::default() 966 971 }; 967 972 968 - let markdown = "# {{ greet name=Title }}\n\nHello {{ simple }}!"; 973 + let markdown = "# {{ greet name=Title /}}\n\nHello {{ simple /}}!"; 969 974 let html = render_markdown(markdown, Some(&options), None, None); 970 975 971 976 assert!(html.contains("<h1")); ··· 981 986 ..Default::default() 982 987 }; 983 988 984 - let markdown = r#"# {{ greet name=Main }} 989 + let markdown = r#"# {{ greet name=Main /}} 985 990 986 - ## Section {{ date format=short }} 991 + ## Section {{ date format=short /}} 987 992 988 - ### {{ simple }} Chapter"#; 993 + ### {{ simple /}} Chapter"#; 989 994 let html = render_markdown(markdown, Some(&options), None, None); 990 995 991 996 assert!(html.contains("<h1")); ··· 1004 1009 ..Default::default() 1005 1010 }; 1006 1011 1007 - let markdown = "*{{ greet name=Italic }}* and **{{ simple }}**"; 1012 + let markdown = "*{{ greet name=Italic /}}* and **{{ simple /}}**"; 1008 1013 let html = render_markdown(markdown, Some(&options), None, None); 1009 1014 1010 1015 assert!(html.contains("<em>Hello, Italic!</em>")); ··· 1019 1024 ..Default::default() 1020 1025 }; 1021 1026 1022 - let markdown = r#"1. {{ greet name=First }} 1023 - 2. {{ simple }} 1024 - 3. {{ date format=iso }}"#; 1027 + let markdown = r#"1. {{ greet name=First /}} 1028 + 2. {{ simple /}} 1029 + 3. {{ date format=iso /}}"#; 1025 1030 let html = render_markdown(markdown, Some(&options), None, None); 1026 1031 1027 1032 assert!(html.contains("<ol>")); ··· 1040 1045 1041 1046 let markdown = r#"| Name | Greeting | 1042 1047 |------|----------| 1043 - | Alice | {{ greet name=Alice }} | 1044 - | Bob | {{ simple }} |"#; 1048 + | Alice | {{ greet name=Alice /}} | 1049 + | Bob | {{ simple /}} |"#; 1045 1050 let html = render_markdown(markdown, Some(&options), None, None); 1046 1051 1047 1052 assert!(html.contains("<table>")); ··· 1061 1066 ..Default::default() 1062 1067 }; 1063 1068 1064 - let markdown = r#"> {{ greet name=Quote }} 1069 + let markdown = r#"> {{ greet name=Quote /}} 1065 1070 > 1066 - > {{ simple }}"#; 1071 + > {{ simple /}}"#; 1067 1072 let html = render_markdown(markdown, Some(&options), None, None); 1068 1073 1069 1074 assert!(html.contains("<blockquote>")); ··· 1081 1086 1082 1087 let markdown = r#"```rust 1083 1088 fn main() { 1084 - println!("{{ greet name=Rust }}"); 1085 - // {{ simple }} 1089 + println!("{{ greet name=Rust /}}"); 1090 + // {{ simple /}} 1086 1091 } 1087 1092 ```"#; 1088 1093 let html = render_markdown(markdown, Some(&options), None, None); ··· 1101 1106 ..Default::default() 1102 1107 }; 1103 1108 1104 - let markdown = r#"[{{ greet name=Link }}](https://example.com "{{ simple }}") 1109 + let markdown = r#"[{{ greet name=Link /}}](https://example.com "{{ simple /}}") 1105 1110 1106 - ![{{ greet name=Alt }}](image.jpg "{{ date format=title }}")"#; 1111 + ![{{ greet name=Alt /}}](image.jpg "{{ date format=title /}}")"#; 1107 1112 let html = render_markdown(markdown, Some(&options), None, None); 1108 1113 1109 1114 assert!(html.contains("<a href=\"https://example.com\"")); ··· 1124 1129 1125 1130 let markdown = r#"{{ highlight lang=rust }} 1126 1131 fn main() { 1127 - println!("{{ greet name=World }}"); 1132 + println!("{{ greet name=World /}}"); 1128 1133 } 1129 1134 {{ /highlight }}"#; 1130 1135 let html = render_markdown(markdown, Some(&options), None, None); ··· 1143 1148 }; 1144 1149 1145 1150 let markdown = r#"{{ alert type=warning }} 1146 - ## {{ greet name=Alert }} 1151 + ## {{ greet name=Alert /}} 1147 1152 1148 - {{ simple }} content here. 1153 + {{ simple /}} content here. 1149 1154 {{ /alert }}"#; 1150 1155 let html = render_markdown(markdown, Some(&options), None, None); 1151 1156 ··· 1165 1170 }; 1166 1171 1167 1172 let markdown = r#"{{ section title=Main }} 1168 - # {{ greet name=Header }} 1173 + # {{ greet name=Header /}} 1169 1174 1170 1175 {{ alert type=info }} 1171 - **{{ greet name=Bold }}** and *{{ simple }}* 1176 + **{{ greet name=Bold /}}** and *{{ simple /}}* 1172 1177 {{ /alert }} 1173 1178 {{ /section }}"#; 1174 1179 let html = render_markdown(markdown, Some(&options), None, None); ··· 1191 1196 }; 1192 1197 1193 1198 let markdown = r#"--- 1194 - title: {{ greet name=Blog }} 1195 - date: {{ date format=iso }} 1196 - tags: [{{ simple }}, {{ greet name=Tutorial }}] 1199 + title: {{ greet name=Blog /}} 1200 + date: {{ date format=iso /}} 1201 + tags: [{{ simple /}}, {{ greet name=Tutorial /}}] 1197 1202 --- 1198 1203 1199 - # {{ greet name=Content }} 1204 + # {{ greet name=Content /}} 1200 1205 1201 - Welcome to {{ simple }}!"#; 1206 + Welcome to {{ simple /}}!"#; 1202 1207 let html = render_markdown(markdown, Some(&options), None, None); 1203 1208 1204 1209 // The HTML shouldn't contain the frontmatter, but shortcodes in content should be processed ··· 1217 1222 ..Default::default() 1218 1223 }; 1219 1224 1220 - let markdown = r#"- [x] {{ greet name=Done }} 1221 - - [ ] {{ simple }} 1222 - - [ ] {{ date format=todo }}"#; 1225 + let markdown = r#"- [x] {{ greet name=Done /}} 1226 + - [ ] {{ simple /}} 1227 + - [ ] {{ date format=todo /}}"#; 1223 1228 let html = render_markdown(markdown, Some(&options), None, None); 1224 1229 1225 1230 assert!(html.contains("<ul>")); ··· 1238 1243 ..Default::default() 1239 1244 }; 1240 1245 1241 - let markdown = "~~{{ greet name=Deleted }}~~ and {{ simple }}"; 1246 + let markdown = "~~{{ greet name=Deleted /}}~~ and {{ simple /}}"; 1242 1247 let html = render_markdown(markdown, Some(&options), None, None); 1243 1248 1244 1249 assert!(html.contains("<del>Hello, Deleted!</del>")); ··· 1254 1259 }; 1255 1260 1256 1261 let markdown = r#"--- 1257 - title: {{ greet name=BlogPost }} 1258 - date: {{ date format=iso }} 1262 + title: {{ greet name=BlogPost /}} 1263 + date: {{ date format=iso /}} 1259 1264 --- 1260 1265 1261 - # {{ greet name=Reader }}! 1266 + # {{ greet name=Reader /}}! 1262 1267 1263 - Welcome to my blog about **{{ simple }}**. 1268 + Welcome to my blog about **{{ simple /}}**. 1264 1269 1265 1270 ## What we'll cover 1266 1271 1267 - 1. {{ greet name=Introduction }} 1268 - 2. {{ simple }} basics 1269 - 3. Advanced {{ greet name=Techniques }} 1272 + 1. {{ greet name=Introduction /}} 1273 + 2. {{ simple /}} basics 1274 + 3. Advanced {{ greet name=Techniques /}} 1270 1275 1271 1276 {{ alert type=info }} 1272 - 💡 **Tip**: Remember {{ greet name=This }}! 1277 + 💡 **Tip**: Remember {{ greet name=This /}}! 1273 1278 {{ /alert }} 1274 1279 1275 1280 ### Code Example 1276 1281 1277 1282 {{ highlight lang=rust }} 1278 1283 fn main() { 1279 - println!("{{ greet name=World }}!"); 1284 + println!("{{ greet name=World /}}!"); 1280 1285 } 1281 1286 {{ /highlight }} 1282 1287 1283 1288 ### Task List 1284 1289 1285 - - [x] {{ greet name=Setup }} 1286 - - [ ] {{ simple }} 1287 - - [ ] {{ greet name=Deploy }} 1290 + - [x] {{ greet name=Setup /}} 1291 + - [ ] {{ simple /}} 1292 + - [ ] {{ greet name=Deploy /}} 1288 1293 1289 - > "{{ greet name=Quote }}" - *{{ simple }}* 1294 + > "{{ greet name=Quote /}}" - *{{ simple /}}* 1290 1295 1291 - Check out [this link](https://example.com "{{ greet name=Title }}")!"#; 1296 + Check out [this link](https://example.com "{{ greet name=Title /}}")!"#; 1292 1297 1293 1298 let html = render_markdown(markdown, Some(&options), None, None); 1294 1299 ··· 1320 1325 ..Default::default() 1321 1326 }; 1322 1327 1323 - let markdown = r#"Inline math with {{ simple }}: $x = {{ greet name=Variable }}$ 1328 + let markdown = r#"Inline math with {{ simple /}}: $x = {{ greet name=Variable /}}$ 1324 1329 1325 1330 Block math: 1326 1331 $$ 1327 - {{ greet name=Equation }} 1332 + {{ greet name=Equation /}} 1328 1333 $$"#; 1329 1334 let html = render_markdown(markdown, Some(&options), None, None); 1330 1335
+100 -34
crates/maudit/src/content/markdown/shortcodes.rs
··· 59 59 content: &str, 60 60 shortcodes: &MarkdownShortcodes, 61 61 mut route_ctx: Option<&mut RouteContext>, 62 + markdown_path: Option<&str>, 62 63 ) -> Result<String, String> { 63 64 let mut output = String::new(); 64 65 let mut rest = content; 65 66 67 + // TODO: Rewrite all of this or at least review it carefully, it's a mess and it was generated by AI 66 68 while let Some(start) = rest.find("{{") { 67 69 // Check for escaped shortcode syntax like `\{{` - if found, skip this occurrence 68 70 if start > 0 && rest.chars().nth(start - 1) == Some('\\') { ··· 85 87 }; 86 88 87 89 let shortcode_content = remaining[..tag_end].trim(); 90 + 91 + // Check if this is a self-closing shortcode (ends with /) 92 + let is_self_closing = shortcode_content.ends_with('/'); 93 + let shortcode_content = if is_self_closing { 94 + shortcode_content.trim_end_matches('/').trim() 95 + } else { 96 + shortcode_content 97 + }; 88 98 89 99 // Parse shortcode name and arguments 90 100 let mut parts = shortcode_content.split_whitespace(); ··· 130 140 chars.next(); // consume the quote 131 141 } 132 142 } 143 + '\\' if !in_key && in_quotes => { 144 + // Handle escaped characters 145 + if let Some(escaped_ch) = chars.next() { 146 + match escaped_ch { 147 + '"' | '\'' => { 148 + // Escaped quote - add literal quote character 149 + current_value.push(escaped_ch); 150 + } 151 + '\\' => { 152 + // Escaped backslash - add literal backslash 153 + current_value.push('\\'); 154 + } 155 + 'n' => { 156 + // Escaped newline 157 + current_value.push('\n'); 158 + } 159 + 't' => { 160 + // Escaped tab 161 + current_value.push('\t'); 162 + } 163 + 'r' => { 164 + // Escaped carriage return 165 + current_value.push('\r'); 166 + } 167 + _ => { 168 + // For any other escaped character, keep the backslash and the character 169 + current_value.push('\\'); 170 + current_value.push(escaped_ch); 171 + } 172 + } 173 + } else { 174 + // Trailing backslash - add it literally 175 + current_value.push('\\'); 176 + } 177 + } 133 178 '"' | '\'' if !in_key && in_quotes && ch == quote_char => { 134 179 // End of quoted value 135 180 in_quotes = false; ··· 202 247 // Move past the opening tag 203 248 let after_opening_tag = &remaining[tag_end + 2..]; 204 249 205 - // Look for closing tag - handle both {{/name}} and {{ /name }} formats 206 - let closing_tag_compact = format!("{{{{/{}}}}}", name); 207 - let closing_tag_spaced = format!("{{{{ /{} }}}}", name); 208 - 209 - let close_pos = after_opening_tag 210 - .find(&closing_tag_compact) 211 - .or_else(|| after_opening_tag.find(&closing_tag_spaced)); 212 - 213 - if let Some(close_pos) = close_pos { 214 - // Determine which closing tag format was found to calculate the correct length 215 - let closing_tag_len = 216 - if after_opening_tag[close_pos..].starts_with(&closing_tag_compact) { 217 - closing_tag_compact.len() 218 - } else { 219 - closing_tag_spaced.len() 220 - }; 221 - 222 - // Block shortcode - extract body and recursively process it 223 - let body = &after_opening_tag[..close_pos]; 224 - let processed_body = preprocess_shortcodes(body, shortcodes, route_ctx.as_deref_mut())?; 225 - 226 - // Execute shortcode with processed body 250 + if is_self_closing { 251 + // Self-closing shortcode - execute immediately 227 252 if let Some(func) = shortcodes.get(name) { 228 253 let mut shortcode_args = ShortcodeArgs::new(args); 229 - shortcode_args.0.insert("body".to_string(), processed_body); 254 + shortcode_args.0.insert( 255 + "markdown_path".to_string(), 256 + markdown_path.unwrap_or("").to_string(), 257 + ); 230 258 let result = func(&shortcode_args, route_ctx.as_deref_mut()); 231 259 output.push_str(&result); 232 260 } else { 233 261 return Err(format!("Unknown shortcode: '{}'", name)); 234 262 } 235 263 236 - // Continue after the closing tag 237 - rest = &after_opening_tag[close_pos + closing_tag_len..]; 264 + // Continue after the opening tag 265 + rest = after_opening_tag; 238 266 } else { 239 - // Self-closing shortcode 240 - if let Some(func) = shortcodes.get(name) { 241 - let shortcode_args = ShortcodeArgs::new(args); 242 - let result = func(&shortcode_args, route_ctx.as_deref_mut()); 243 - output.push_str(&result); 267 + // Block shortcode - look for closing tag 268 + let closing_tag_compact = format!("{{{{/{}}}}}", name); 269 + let closing_tag_spaced = format!("{{{{ /{} }}}}", name); 270 + 271 + let close_pos = after_opening_tag 272 + .find(&closing_tag_compact) 273 + .or_else(|| after_opening_tag.find(&closing_tag_spaced)); 274 + 275 + if let Some(close_pos) = close_pos { 276 + // Determine which closing tag format was found to calculate the correct length 277 + let closing_tag_len = 278 + if after_opening_tag[close_pos..].starts_with(&closing_tag_compact) { 279 + closing_tag_compact.len() 280 + } else { 281 + closing_tag_spaced.len() 282 + }; 283 + 284 + // Block shortcode - extract body and recursively process it 285 + let body = &after_opening_tag[..close_pos]; 286 + let processed_body = preprocess_shortcodes( 287 + body, 288 + shortcodes, 289 + route_ctx.as_deref_mut(), 290 + markdown_path, 291 + )?; 292 + 293 + // Execute shortcode with processed body 294 + if let Some(func) = shortcodes.get(name) { 295 + let mut shortcode_args = ShortcodeArgs::new(args); 296 + shortcode_args.0.insert("body".to_string(), processed_body); 297 + shortcode_args.0.insert( 298 + "markdown_path".to_string(), 299 + markdown_path.unwrap_or("").to_string(), 300 + ); 301 + let result = func(&shortcode_args, route_ctx.as_deref_mut()); 302 + output.push_str(&result); 303 + } else { 304 + return Err(format!("Unknown shortcode: '{}'", name)); 305 + } 306 + 307 + // Continue after the closing tag 308 + rest = &after_opening_tag[close_pos + closing_tag_len..]; 244 309 } else { 245 - return Err(format!("Unknown shortcode: '{}'", name)); 310 + // No closing tag found for block shortcode - this is an error 311 + return Err(format!( 312 + "Block shortcode '{}' is missing its closing tag. Use '{{{{ {} /}}}}' for self-closing shortcodes or add '{{{{/{}}}}}'", 313 + name, name, name 314 + )); 246 315 } 247 - 248 - // Continue after the opening tag 249 - rest = after_opening_tag; 250 316 } 251 317 } 252 318
+284 -131
crates/maudit/src/content/markdown/shortcodes_tests.rs
··· 89 89 content: &str, 90 90 shortcodes: &MarkdownShortcodes, 91 91 ) -> Result<String, String> { 92 - preprocess_shortcodes(content, shortcodes, None) 92 + preprocess_shortcodes(content, shortcodes, None, None) 93 93 } 94 94 95 95 // Helper function that automatically wraps RouteContext in Some() for existing tests ··· 98 98 shortcodes: &MarkdownShortcodes, 99 99 route_ctx: &mut RouteContext, 100 100 ) -> Result<String, String> { 101 - preprocess_shortcodes(content, shortcodes, Some(route_ctx)) 101 + preprocess_shortcodes(content, shortcodes, Some(route_ctx), None) 102 102 } 103 103 104 104 #[test] ··· 114 114 #[test] 115 115 fn test_simple_self_closing_shortcode() { 116 116 let shortcodes = create_test_shortcodes(); 117 - let content = "Before {{ simple }} after"; 117 + let content = "Before {{ simple /}} after"; 118 118 let result = with_test_route_context(|route_ctx| { 119 119 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx).unwrap() 120 120 }); ··· 124 124 #[test] 125 125 fn test_shortcode_with_arguments() { 126 126 let shortcodes = create_test_shortcodes(); 127 - let content = "{{ greet name=Alice }}"; 127 + let content = "{{ greet name=Alice /}}"; 128 128 let result = with_test_route_context(|route_ctx| { 129 129 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx).unwrap() 130 130 }); ··· 134 134 #[test] 135 135 fn test_multiple_arguments() { 136 136 let shortcodes = create_test_shortcodes(); 137 - let content = "{{ date format=iso year=2023 }}"; 137 + let content = "{{ date day=Monday month=January year=2024 /}}"; 138 138 let result = with_test_route_context(|route_ctx| { 139 139 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx).unwrap() 140 140 }); 141 - assert_eq!(result, "DATE[iso]"); 141 + assert_eq!(result, "DATE[default]"); 142 142 } 143 143 144 144 #[test] 145 145 fn test_frontmatter_shortcodes() { 146 146 let shortcodes = create_test_shortcodes(); 147 147 let content = r#"--- 148 - title: {{ greet name=Blog }} 149 - date: {{ date format=iso }} 148 + title: {{ greet name=Blog /}} 149 + date: {{ date format=iso /}} 150 150 --- 151 151 152 152 # Content here"#; ··· 165 165 #[test] 166 166 fn test_shortcodes_in_headings() { 167 167 let shortcodes = create_test_shortcodes(); 168 - let content = "# {{ greet name=Header }}\n\n## Section {{ date format=short }}"; 168 + let content = "# {{ greet name=Header /}}\n\n## Section {{ date format=short /}}"; 169 169 let result = with_test_route_context(|route_ctx| { 170 170 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx).unwrap() 171 171 }); ··· 175 175 #[test] 176 176 fn test_shortcodes_in_links() { 177 177 let shortcodes = create_test_shortcodes(); 178 - let content = "[{{ greet name=Link }}](https://example.com)"; 178 + let content = "[{{ greet name=Link /}}](https://example.com)"; 179 179 let result = with_test_route_context(|route_ctx| { 180 180 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx).unwrap() 181 181 }); ··· 185 185 #[test] 186 186 fn test_shortcodes_in_code_blocks() { 187 187 let shortcodes = create_test_shortcodes(); 188 - let content = "```\nSome code with {{ simple }}\n```"; 188 + let content = "```\nSome code with {{ simple /}}\n```"; 189 189 let result = with_test_route_context(|route_ctx| { 190 190 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx).unwrap() 191 191 }); ··· 205 205 #[test] 206 206 fn test_nested_shortcodes_in_block() { 207 207 let shortcodes = create_test_shortcodes(); 208 - let content = "{{ section title=Main }}\nHello {{ greet name=World }}!\n{{ /section }}"; 208 + let content = "{{ section title=Main }}\nHello {{ greet name=World /}}!\n{{ /section }}"; 209 209 let result = with_test_route_context(|route_ctx| { 210 210 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 211 211 }) ··· 222 222 let content = r#"{{ section title=Outer }} 223 223 {{ alert type=warning }} 224 224 {{ highlight lang=javascript }} 225 - console.log("{{ greet name=Nested }}"); 225 + console.log("{{ greet name=Nested /}}"); 226 226 {{ /highlight }} 227 227 {{ /alert }} 228 228 {{ /section }}"#; ··· 243 243 #[test] 244 244 fn test_multiple_shortcodes_same_line() { 245 245 let shortcodes = create_test_shortcodes(); 246 - let content = "{{ greet name=Alice }} and {{ greet name=Bob }}"; 246 + let content = "{{ greet name=Alice /}} and {{ greet name=Bob /}}"; 247 247 let result = with_test_route_context(|route_ctx| { 248 248 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 249 249 }) ··· 254 254 #[test] 255 255 fn test_shortcodes_in_lists() { 256 256 let shortcodes = create_test_shortcodes(); 257 - let content = r#"- Item 1: {{ greet name=First }} 258 - - Item 2: {{ date format=short }} 259 - - Item 3: {{ simple }}"#; 257 + let content = r#"- Item 1: {{ greet name=First /}} 258 + - Item 2: {{ date format=short /}} 259 + - Item 3: {{ simple /}}"#; 260 260 let result = with_test_route_context(|route_ctx| { 261 261 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 262 262 }) ··· 272 272 let shortcodes = create_test_shortcodes(); 273 273 let content = r#"| Name | Greeting | 274 274 |------|----------| 275 - | Alice | {{ greet name=Alice }} | 276 - | Bob | {{ greet name=Bob }} |"#; 275 + | Alice | {{ greet name=Alice /}} | 276 + | Bob | {{ greet name=Bob /}} |"#; 277 277 let result = with_test_route_context(|route_ctx| { 278 278 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 279 279 }) ··· 288 288 #[test] 289 289 fn test_shortcodes_with_special_characters() { 290 290 let shortcodes = create_test_shortcodes(); 291 - let content = "Before\n{{ simple }}\nAfter\n\n{{ greet name=Test }}"; 291 + let content = "Before\n{{ simple /}}\nAfter\n\n{{ greet name=Test /}}"; 292 292 let result = with_test_route_context(|route_ctx| { 293 293 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 294 294 }) ··· 299 299 #[test] 300 300 fn test_error_unknown_shortcode() { 301 301 let shortcodes = create_test_shortcodes(); 302 - let content = "{{ unknown_shortcode }}"; 302 + let content = "{{ unknown_shortcode /}}"; 303 303 let result = with_test_route_context(|route_ctx| { 304 304 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 305 305 }); ··· 326 326 #[test] 327 327 fn test_unclosed_shortcode_with_valid_shortcode_after() { 328 328 let shortcodes = create_test_shortcodes(); 329 - let content = "Before {{ unclosed. Then {{ simple }} after."; 329 + let content = "Before {{ unclosed. Then {{ simple /}} after."; 330 330 let result = with_test_route_context(|route_ctx| { 331 331 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 332 332 }) ··· 361 361 #[test] 362 362 fn test_error_invalid_argument_format() { 363 363 let shortcodes = create_test_shortcodes(); 364 - let content = "{{ greet name Alice }}"; 364 + let content = "{{ greet name Alice /}}"; 365 365 let result = with_test_route_context(|route_ctx| { 366 366 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 367 367 }); ··· 383 383 #[test] 384 384 fn test_whitespace_handling() { 385 385 let shortcodes = create_test_shortcodes(); 386 - let content = "{{ simple }}"; 386 + let content = "{{ simple /}}"; 387 387 let result = with_test_route_context(|route_ctx| { 388 388 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 389 389 }) ··· 394 394 #[test] 395 395 fn test_whitespace_in_arguments() { 396 396 let shortcodes = create_test_shortcodes(); 397 - let content = "{{ greet name=Alice }}"; 397 + let content = "{{ greet name=Alice /}}"; 398 398 let result = with_test_route_context(|route_ctx| { 399 399 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 400 400 }) ··· 406 406 fn test_complex_markdown_document() { 407 407 let shortcodes = create_test_shortcodes(); 408 408 let content = r#"--- 409 - title: {{ greet name=Blog }} 410 - author: {{ greet name=Author }} 409 + title: {{ greet name=Blog /}} 410 + author: {{ greet name=Author /}} 411 411 --- 412 412 413 - # {{ greet name=Reader }} 413 + # {{ greet name=Reader /}} 414 414 415 - Welcome to my blog! Today is {{ date format=full }}. 415 + Welcome to my blog! Today is {{ date format=full /}}. 416 416 417 417 ## Code Example 418 418 419 419 {{ highlight lang=rust }} 420 420 fn main() { 421 - println!("{{ greet name=Rust }}"); 421 + println!("{{ greet name=Rust /}}"); 422 422 } 423 423 {{ /highlight }} 424 424 425 425 ## Alert Section 426 426 427 427 {{ alert type=info }} 428 - This is an important message with {{ simple }} content. 428 + This is an important message with {{ simple /}} content. 429 429 {{ /alert }} 430 430 431 - - List item with {{ greet name=Item }} 432 - - Another item: {{ date format=short }} 431 + - List item with {{ greet name=Item /}} 432 + - Another item: {{ date format=short /}} 433 433 434 - > Quote with {{ simple }} shortcode 434 + > Quote with {{ simple /}} shortcode 435 435 436 - [Link with {{ greet name=Link }}](http://example.com)"#; 436 + [Link with {{ greet name=Link /}}](http://example.com)"#; 437 437 438 438 let result = with_test_route_context(|route_ctx| { 439 439 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) ··· 475 475 #[test] 476 476 fn test_markdown_integration_headings_with_shortcodes() { 477 477 let shortcodes = create_test_shortcodes(); 478 - let content = "# {{ greet name=Title }}\n\n## Section {{ date format=short }}"; 478 + let content = "# {{ greet name=Title /}}\n\n## Section {{ date format=short /}}"; 479 479 480 480 // Test shortcode preprocessing first 481 481 let processed = with_test_route_context(|route_ctx| { ··· 488 488 #[test] 489 489 fn test_markdown_integration_emphasis_with_shortcodes() { 490 490 let shortcodes = create_test_shortcodes(); 491 - let content = "*{{ greet name=Italic }}* and **{{ greet name=Bold }}**"; 491 + let content = "*{{ greet name=Italic /}}* and **{{ greet name=Bold /}}**"; 492 492 let processed = with_test_route_context(|route_ctx| { 493 493 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 494 494 }) ··· 499 499 #[test] 500 500 fn test_markdown_integration_code_spans_with_shortcodes() { 501 501 let shortcodes = create_test_shortcodes(); 502 - let content = "Use `{{ simple }}` in your code"; 502 + let content = "Use `{{ simple /}}` in your code"; 503 503 let processed = with_test_route_context(|route_ctx| { 504 504 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 505 505 }) ··· 510 510 #[test] 511 511 fn test_markdown_integration_blockquotes_with_shortcodes() { 512 512 let shortcodes = create_test_shortcodes(); 513 - let content = "> {{ greet name=Quote }}\n> \n> {{ simple }}"; 513 + let content = "> {{ greet name=Quote /}}\n> \n> {{ simple /}}"; 514 514 let processed = with_test_route_context(|route_ctx| { 515 515 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 516 516 }) ··· 521 521 #[test] 522 522 fn test_markdown_integration_nested_lists_with_shortcodes() { 523 523 let shortcodes = create_test_shortcodes(); 524 - let content = r#"1. {{ greet name=First }} 525 - - Nested {{ simple }} 526 - - {{ date format=iso }} 527 - 2. {{ greet name=Second }} 528 - 1. Numbered {{ simple }} 529 - 2. {{ greet name=Nested }}"#; 524 + let content = r#"1. {{ greet name=First /}} 525 + - Nested {{ simple /}} 526 + - {{ date format=iso /}} 527 + 2. {{ greet name=Second /}} 528 + 1. Numbered {{ simple /}} 529 + 2. {{ greet name=Nested /}}"#; 530 530 let processed = with_test_route_context(|route_ctx| { 531 531 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 532 532 }) ··· 543 543 #[test] 544 544 fn test_markdown_integration_complex_tables_with_shortcodes() { 545 545 let shortcodes = create_test_shortcodes(); 546 - let content = r#"| **{{ greet name=Header }}** | _{{ date format=long }}_ | 546 + let content = r#"| **{{ greet name=Header /}}** | _{{ date format=long /}}_ | 547 547 |:---------------------------|-------------------------:| 548 - | {{ simple }} | {{ greet name=Cell }} | 549 - | `{{ greet name=Code }}` | > {{ simple }} |"#; 548 + | {{ simple /}} | {{ greet name=Cell /}} | 549 + | `{{ greet name=Code /}}` | > {{ simple /}} |"#; 550 550 let processed = with_test_route_context(|route_ctx| { 551 551 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 552 552 }) ··· 561 561 #[test] 562 562 fn test_markdown_integration_task_lists_with_shortcodes() { 563 563 let shortcodes = create_test_shortcodes(); 564 - let content = r#"- [x] {{ greet name=Done }} 565 - - [ ] {{ simple }} 566 - - [ ] {{ date format=todo }}"#; 564 + let content = r#"- [x] {{ greet name=Done /}} 565 + - [ ] {{ simple /}} 566 + - [ ] {{ date format=todo /}}"#; 567 567 let processed = with_test_route_context(|route_ctx| { 568 568 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 569 569 }) ··· 577 577 #[test] 578 578 fn test_markdown_integration_strikethrough_with_shortcodes() { 579 579 let shortcodes = create_test_shortcodes(); 580 - let content = "~~{{ greet name=Deleted }}~~ and {{ simple }}"; 580 + let content = "~~{{ greet name=Deleted /}}~~ and {{ simple /}}"; 581 581 let processed = with_test_route_context(|route_ctx| { 582 582 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 583 583 }) ··· 588 588 #[test] 589 589 fn test_markdown_integration_horizontal_rules_with_shortcodes() { 590 590 let shortcodes = create_test_shortcodes(); 591 - let content = "{{ greet name=Before }}\n\n---\n\n{{ simple }}"; 591 + let content = "{{ greet name=Before /}}\n\n---\n\n{{ simple /}}"; 592 592 let processed = with_test_route_context(|route_ctx| { 593 593 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 594 594 }) ··· 599 599 #[test] 600 600 fn test_markdown_integration_footnotes_with_shortcodes() { 601 601 let shortcodes = create_test_shortcodes(); 602 - let content = "{{ greet name=Text }}[^1]\n\n[^1]: {{ simple }}"; 602 + let content = "{{ greet name=Text /}}[^1]\n\n[^1]: {{ simple /}}"; 603 603 let processed = with_test_route_context(|route_ctx| { 604 604 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 605 605 }) ··· 610 610 #[test] 611 611 fn test_markdown_integration_complex_links_with_shortcodes() { 612 612 let shortcodes = create_test_shortcodes(); 613 - let content = r#"[{{ greet name=Link }}](https://example.com "{{ simple }}") 613 + let content = r#"[{{ greet name=Link /}}](https://example.com "{{ simple /}}") 614 614 615 - ![{{ greet name=Alt }}](image.jpg "{{ date format=title }}") 615 + ![{{ greet name=Alt /}}](image.jpg "{{ date format=title /}}") 616 616 617 - [Reference {{ simple }}][ref] 617 + [Reference {{ simple /}}][ref] 618 618 619 - [ref]: https://example.com "{{ greet name=RefTitle }}""#; 619 + [ref]: https://example.com "{{ greet name=RefTitle /}}""#; 620 620 let processed = with_test_route_context(|route_ctx| { 621 621 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 622 622 }) ··· 636 636 let shortcodes = create_test_shortcodes(); 637 637 let content = r#"```rust 638 638 fn main() { 639 - println!("{{ greet name=Rust }}"); 640 - // {{ simple }} 639 + println!("{{ greet name=Rust /}}"); 640 + // {{ simple /}} 641 641 } 642 642 ``` 643 643 644 - ```{{ greet name=Language }} 645 - {{ simple }} 644 + ```{{ greet name=Language /}} 645 + {{ simple /}} 646 646 ```"#; 647 647 let processed = with_test_route_context(|route_ctx| { 648 648 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) ··· 665 665 fn test_markdown_integration_html_blocks_with_shortcodes() { 666 666 let shortcodes = create_test_shortcodes(); 667 667 let content = r#"<div class="custom"> 668 - <h2>{{ greet name=HTML }}</h2> 669 - <p>{{ simple }}</p> 668 + <h2>{{ greet name=HTML /}}</h2> 669 + <p>{{ simple /}}</p> 670 670 </div> 671 671 672 - <img src="test.jpg" alt="{{ greet name=Alt }}" title="{{ date format=attr }}">"#; 672 + <img src="test.jpg" alt="{{ greet name=Alt /}}" title="{{ date format=attr /}}">"#; 673 673 let processed = with_test_route_context(|route_ctx| { 674 674 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 675 675 }) ··· 686 686 #[test] 687 687 fn test_markdown_integration_math_blocks_with_shortcodes() { 688 688 let shortcodes = create_test_shortcodes(); 689 - let content = r#"Inline math: ${{ simple }}$ 689 + let content = r#"Inline math: ${{ simple /}}$ 690 690 691 691 Block math: 692 692 $$ 693 - {{ greet name=Math }} 693 + {{ greet name=Math /}} 694 694 $$ 695 695 696 - {{ greet name=Text }} with $x = {{ simple }}$ inline."#; 696 + {{ greet name=Text /}} with $x = {{ simple /}}$ inline."#; 697 697 let processed = with_test_route_context(|route_ctx| { 698 698 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 699 699 }) ··· 713 713 fn test_markdown_integration_frontmatter_yaml_with_shortcodes() { 714 714 let shortcodes = create_test_shortcodes(); 715 715 let content = r#"--- 716 - title: "{{ greet name=Blog }}" 717 - description: {{ simple }} 716 + title: "{{ greet name=Blog /}}" 717 + description: {{ simple /}} 718 718 tags: 719 - - {{ greet name=Tag1 }} 720 - - {{ simple }} 719 + - {{ greet name=Tag1 /}} 720 + - {{ simple /}} 721 721 metadata: 722 - created: {{ date format=iso }} 723 - author: {{ greet name=Author }} 722 + created: {{ date format=iso /}} 723 + author: {{ greet name=Author /}} 724 724 --- 725 725 726 - # {{ greet name=Content }} 726 + # {{ greet name=Content /}} 727 727 728 - {{ simple }}"#; 728 + {{ simple /}}"#; 729 729 let processed = with_test_route_context(|route_ctx| { 730 730 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 731 731 }) ··· 751 751 fn test_markdown_integration_block_shortcodes_with_markdown() { 752 752 let shortcodes = create_test_shortcodes(); 753 753 let content = r#"{{ section title=Main }} 754 - # {{ greet name=Header }} 754 + # {{ greet name=Header /}} 755 755 756 - **{{ greet name=Bold }}** and *{{ simple }}* 756 + **{{ greet name=Bold /}}** and *{{ simple /}}* 757 757 758 - - {{ greet name=Item1 }} 759 - - {{ simple }} 758 + - {{ greet name=Item1 /}} 759 + - {{ simple /}} 760 760 761 - > {{ greet name=Quote }} 761 + > {{ greet name=Quote /}} 762 762 763 763 {{ /section }}"#; 764 764 let processed = with_test_route_context(|route_ctx| { ··· 782 782 #[test] 783 783 fn test_markdown_integration_edge_cases() { 784 784 let shortcodes = create_test_shortcodes(); 785 - let content = r#"{{ greet name=Start }} 785 + let content = r#"{{ greet name=Start /}} 786 786 787 - <!-- {{ simple }} in comment --> 787 + <!-- {{ simple /}} in comment --> 788 788 789 789 {{ highlight lang=markdown }} 790 - # {{ greet name=NestedMD }} 791 - {{ simple }} 790 + # {{ greet name=NestedMD /}} 791 + {{ simple /}} 792 792 {{ /highlight }} 793 793 794 - `{{ greet name=BacktickCode }}` 794 + `{{ greet name=BacktickCode /}}` 795 795 796 - {{ greet name=End }}"#; 796 + {{ greet name=End /}}"#; 797 797 let processed = with_test_route_context(|route_ctx| { 798 798 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 799 799 }) ··· 817 817 fn test_markdown_integration_real_world_blog_post() { 818 818 let shortcodes = create_test_shortcodes(); 819 819 let content = r#"--- 820 - title: {{ greet name=BlogPost }} 821 - date: {{ date format=iso }} 822 - author: {{ greet name=Writer }} 823 - tags: [{{ simple }}, {{ greet name=Tutorial }}] 820 + title: {{ greet name=BlogPost /}} 821 + date: {{ date format=iso /}} 822 + author: {{ greet name=Writer /}} 823 + tags: [{{ simple /}}, {{ greet name=Tutorial /}}] 824 824 --- 825 825 826 - # {{ greet name=Reader }}! 826 + # {{ greet name=Reader /}}! 827 827 828 - Welcome to my blog post about {{ simple }}. 828 + Welcome to my blog post about {{ simple /}}. 829 829 830 830 ## What we'll cover 831 831 832 - 1. **{{ greet name=Introduction }}** - Getting started 833 - 2. **{{ simple }}** basics 834 - 3. Advanced {{ greet name=Techniques }} 832 + 1. **{{ greet name=Introduction /}}** - Getting started 833 + 2. **{{ simple /}}** basics 834 + 3. Advanced {{ greet name=Techniques /}} 835 835 836 836 {{ alert type=info }} 837 - 💡 **Tip**: {{ greet name=Remember }} to {{ simple }}! 837 + 💡 **Tip**: {{ greet name=Remember /}} to {{ simple /}}! 838 838 {{ /alert }} 839 839 840 840 ## Code Example 841 841 842 842 {{ highlight lang=rust }} 843 843 fn main() { 844 - println!("{{ greet name=World }}!"); 845 - // {{ simple }} 844 + println!("{{ greet name=World /}}!"); 845 + // {{ simple /}} 846 846 } 847 847 {{ /highlight }} 848 848 849 849 ### Task List 850 850 851 - - [x] {{ greet name=Setup }} 852 - - [ ] {{ simple }} 853 - - [ ] {{ greet name=Publish }} 851 + - [x] {{ greet name=Setup /}} 852 + - [ ] {{ simple /}} 853 + - [ ] {{ greet name=Publish /}} 854 854 855 855 --- 856 856 857 - > "{{ greet name=Quote }}" - {{ simple }} 857 + > "{{ greet name=Quote /}}" - {{ simple /}} 858 858 859 859 {{ section title=Resources }} 860 - - [Documentation](https://docs.rs) - {{ simple }} 861 - - [GitHub](https://github.com) - {{ greet name=Source }} 860 + - [Documentation](https://docs.rs) - {{ simple /}} 861 + - [GitHub](https://github.com) - {{ greet name=Source /}} 862 862 {{ /section }} 863 863 864 - *Published on {{ date format=long }}*"#; 864 + *Published on {{ date format=long /}}*"#; 865 865 866 866 let processed = with_test_route_context(|route_ctx| { 867 867 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) ··· 922 922 923 923 // Test invalid names that should be treated as literal text 924 924 let test_cases = vec![ 925 - ("{{ 123invalid }}", "{{ 123invalid }}"), // starts with number 926 - ("{{ -invalid }}", "{{ -invalid }}"), // starts with dash 925 + ("{{ 123invalid /}}", "{{ 123invalid /}}"), // starts with number 926 + ("{{ -invalid }}", "{{ -invalid }}"), // starts with dash 927 927 ("{{ invalid-name }}", "{{ invalid-name }}"), // contains dash 928 928 ("{{ invalid.name }}", "{{ invalid.name }}"), // contains dot 929 929 ("{{ invalid@name }}", "{{ invalid@name }}"), // contains special char ··· 946 946 shortcodes.register("name123", |_, _| "NAME123".to_string()); 947 947 948 948 let test_cases = vec![ 949 - ("{{ valid_name }}", "VALID_NAME"), 950 - ("{{ ValidName }}", "VALID_NAME_CAMEL"), 951 - ("{{ _underscore }}", "UNDERSCORE"), 952 - ("{{ name123 }}", "NAME123"), 953 - ("{{ a }}", "{{ a }}"), // single char is invalid (too short for pattern) 949 + ("{{ valid_name /}}", "VALID_NAME"), 950 + ("{{ ValidName /}}", "VALID_NAME_CAMEL"), 951 + ("{{ _underscore /}}", "UNDERSCORE"), 952 + ("{{ name123 /}}", "NAME123"), 953 + ("{{ a /}}", "{{ a /}}"), // single char is invalid (too short for pattern) 954 954 ]; 955 955 956 956 for (input, expected) in test_cases { ··· 968 968 (r#"\{{"hello"}}"#, r#"\{{"hello"}}"#), 969 969 (r#"Before \{{test}} after"#, r#"Before \{{test}} after"#), 970 970 ( 971 - r#"\{{invalid}} and {{ simple }}"#, 971 + r#"\{{invalid}} and {{ simple /}}"#, 972 972 r#"\{{invalid}} and SIMPLE_OUTPUT"#, 973 973 ), 974 974 ]; ··· 985 985 986 986 // Test various edge cases - invalid names should be treated as literal text 987 987 let test_cases = vec![ 988 - ("{{ 1 }}", "{{ 1 }}"), // single digit (invalid) 989 - ("{{ _ }}", "{{ _ }}"), // single underscore (invalid - too short) 990 - ("{{ 1A }}", "{{ 1A }}"), // invalid: digit + letter 991 - ("{{ café }}", "{{ café }}"), // invalid: non-ASCII 988 + ("{{ 1 /}}", "{{ 1 /}}"), // single digit (invalid) 989 + ("{{ _ /}}", "{{ _ /}}"), // single underscore (invalid - too short) 990 + ("{{ 1A /}}", "{{ 1A /}}"), // invalid: digit + letter 991 + ("{{ café /}}", "{{ café /}}"), // invalid: non-ASCII 992 992 ]; 993 993 994 994 for (input, expected) in test_cases { ··· 997 997 } 998 998 999 999 // Test valid names that aren't registered - should get "Unknown shortcode" error 1000 - let valid_but_unregistered = 1001 - vec!["{{ _a }}", "{{ a_ }}", "{{ A1 }}", "{{ valid_name123 }}"]; 1000 + let valid_but_unregistered = vec![ 1001 + "{{ _a /}}", 1002 + "{{ a_ /}}", 1003 + "{{ A1 /}}", 1004 + "{{ valid_name123 /}}", 1005 + ]; 1002 1006 1003 1007 for input in valid_but_unregistered { 1004 1008 let result = preprocess_shortcodes_simple(input, &shortcodes); ··· 1030 1034 let shortcodes = create_test_shortcodes(); 1031 1035 1032 1036 let content = r#" 1033 - Valid: {{ simple }} 1034 - Invalid number start: {{ 123invalid }} 1035 - Valid underscore: {{ greet name=Test }} 1037 + Valid: {{ simple /}} 1038 + Invalid number start: {{ 123invalid /}} 1039 + Valid underscore: {{ greet name=Test /}} 1036 1040 Invalid dash: {{ invalid-name }} 1037 - Another valid: {{ date format=iso }} 1041 + Another valid: {{ date format=iso /}} 1038 1042 "#; 1039 1043 1040 1044 let result = with_test_route_context(|route_ctx| { ··· 1043 1047 .unwrap(); 1044 1048 let expected = r#" 1045 1049 Valid: SIMPLE_OUTPUT 1046 - Invalid number start: {{ 123invalid }} 1050 + Invalid number start: {{ 123invalid /}} 1047 1051 Valid underscore: Hello, Test! 1048 1052 Invalid dash: {{ invalid-name }} 1049 1053 Another valid: DATE[iso] ··· 1057 1061 let shortcodes = create_test_shortcodes(); 1058 1062 1059 1063 // Test double quotes 1060 - let content = r#"{{ greet name="Hello World" }}"#; 1064 + let content = r#"{{ greet name="Hello World" /}}"#; 1061 1065 let result = with_test_route_context(|route_ctx| { 1062 1066 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 1063 1067 }) ··· 1065 1069 assert_eq!(result, "Hello, Hello World!"); 1066 1070 1067 1071 // Test single quotes 1068 - let content = r#"{{ greet name='Hello World' }}"#; 1072 + let content = r#"{{ greet name='Hello World' /}}"#; 1069 1073 let result = with_test_route_context(|route_ctx| { 1070 1074 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 1071 1075 }) ··· 1082 1086 format!("{} - {}", text, author) 1083 1087 }); 1084 1088 1085 - let content = r#"{{ message text="Hello World" author=John }}"#; 1089 + let content = r#"{{ message text="Hello World" author=John /}}"#; 1086 1090 let result = with_test_route_context(|route_ctx| { 1087 1091 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 1088 1092 }) ··· 1097 1101 args.get_str("value").unwrap_or("").to_string() 1098 1102 }); 1099 1103 1100 - let content = r#"{{ special value="Hello, World! How are you?" }}"#; 1104 + let content = r#"{{ special value="Hello, World! How are you?" /}}"#; 1101 1105 let result = with_test_route_context(|route_ctx| { 1102 1106 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 1103 1107 }) ··· 1109 1113 fn test_error_unclosed_quotes() { 1110 1114 let shortcodes = create_test_shortcodes(); 1111 1115 1112 - let content = r#"{{ greet name="Hello World }}"#; 1116 + let content = r#"{{ greet name="Hello World /}}"#; 1113 1117 let result = with_test_route_context(|route_ctx| { 1114 1118 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 1115 1119 }); ··· 1125 1129 format!("'{}'", value) 1126 1130 }); 1127 1131 1128 - let content = r#"{{ empty value="" }}"#; 1132 + let content = r#"{{ empty value="" /}}"#; 1129 1133 let result = with_test_route_context(|route_ctx| { 1130 1134 preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 1131 1135 }) 1132 1136 .unwrap(); 1133 1137 assert_eq!(result, "''"); 1138 + } 1139 + 1140 + #[test] 1141 + fn test_escaped_quotes_in_arguments() { 1142 + let mut shortcodes = create_test_shortcodes(); 1143 + shortcodes.register("quote", |args, _| { 1144 + let message = args.get_str("message").unwrap_or(""); 1145 + format!("Quote: {}", message) 1146 + }); 1147 + 1148 + // Test escaped double quotes 1149 + let content = r#"{{ quote message="Me answering \"thanks man glad you like it\" to someone saying a feature is stupid" /}}"#; 1150 + let result = with_test_route_context(|route_ctx| { 1151 + preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 1152 + }) 1153 + .unwrap(); 1154 + assert_eq!( 1155 + result, 1156 + r#"Quote: Me answering "thanks man glad you like it" to someone saying a feature is stupid"# 1157 + ); 1158 + 1159 + // Test escaped single quotes 1160 + let content = r#"{{ quote message='It\'s a "great" day!' /}}"#; 1161 + let result = with_test_route_context(|route_ctx| { 1162 + preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 1163 + }) 1164 + .unwrap(); 1165 + assert_eq!(result, r#"Quote: It's a "great" day!"#); 1166 + 1167 + // Test escaped backslashes 1168 + let content = r#"{{ quote message="Path: C:\\Users\\name\\file.txt" /}}"#; 1169 + let result = with_test_route_context(|route_ctx| { 1170 + preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 1171 + }) 1172 + .unwrap(); 1173 + assert_eq!(result, r#"Quote: Path: C:\Users\name\file.txt"#); 1174 + 1175 + // Test escaped newlines and tabs 1176 + let content = r#"{{ quote message="Line 1\nLine 2\tTabbed" /}}"#; 1177 + let result = with_test_route_context(|route_ctx| { 1178 + preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 1179 + }) 1180 + .unwrap(); 1181 + assert_eq!(result, "Quote: Line 1\nLine 2\tTabbed"); 1182 + 1183 + // Test mixed escape sequences 1184 + let content = r#"{{ quote message="Say \"Hello\", then press \\n for newline\nDone!" /}}"#; 1185 + let result = with_test_route_context(|route_ctx| { 1186 + preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 1187 + }) 1188 + .unwrap(); 1189 + assert_eq!( 1190 + result, 1191 + "Quote: Say \"Hello\", then press \\n for newline\nDone!" 1192 + ); 1193 + } 1194 + 1195 + #[test] 1196 + fn test_self_closing_shortcode_syntax() { 1197 + let mut shortcodes = create_test_shortcodes(); 1198 + shortcodes.register("current_date", |_args, _| "2024-01-01".to_string()); 1199 + shortcodes.register("user", |args, _| { 1200 + let name = args.get_str("name").unwrap_or("Anonymous"); 1201 + format!("User: {}", name) 1202 + }); 1203 + 1204 + // Test basic self-closing shortcode 1205 + let content = r#"Today is {{ current_date /}}"#; 1206 + let result = with_test_route_context(|route_ctx| { 1207 + preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 1208 + }) 1209 + .unwrap(); 1210 + assert_eq!(result, "Today is 2024-01-01"); 1211 + 1212 + // Test self-closing shortcode with arguments 1213 + let content = r#"{{ user name="Alice" /}}"#; 1214 + let result = with_test_route_context(|route_ctx| { 1215 + preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 1216 + }) 1217 + .unwrap(); 1218 + assert_eq!(result, "User: Alice"); 1219 + 1220 + // Test self-closing shortcode with spaces before / 1221 + let content = r#"{{ user name="Bob" /}}"#; 1222 + let result = with_test_route_context(|route_ctx| { 1223 + preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 1224 + }) 1225 + .unwrap(); 1226 + assert_eq!(result, "User: Bob"); 1227 + 1228 + // Test multiple self-closing shortcodes 1229 + let content = r#"{{ user name="Alice" /}} and {{ user name="Bob" /}}"#; 1230 + let result = with_test_route_context(|route_ctx| { 1231 + preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 1232 + }) 1233 + .unwrap(); 1234 + assert_eq!(result, "User: Alice and User: Bob"); 1235 + } 1236 + 1237 + #[test] 1238 + fn test_block_shortcode_requires_closing_tag() { 1239 + let shortcodes = create_test_shortcodes(); 1240 + 1241 + // This should now be an error because it's not self-closing and has no closing tag 1242 + let content = r#"{{ highlight lang="rust" }}"#; 1243 + let result = with_test_route_context(|route_ctx| { 1244 + preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 1245 + }); 1246 + assert!(result.is_err()); 1247 + let error_msg = result.unwrap_err(); 1248 + assert!(error_msg.contains("missing its closing tag")); 1249 + assert!(error_msg.contains("Use '{{ highlight /}}' for self-closing")); 1250 + } 1251 + 1252 + #[test] 1253 + fn test_ambiguous_shortcode_resolution() { 1254 + let mut shortcodes = create_test_shortcodes(); 1255 + shortcodes.register("img", |args, _| { 1256 + let src = args.get_str("src").unwrap_or(""); 1257 + format!("<img src=\"{}\">", src) 1258 + }); 1259 + 1260 + // This scenario would have been ambiguous in the old syntax: 1261 + // Two shortcodes where the first could mistakenly consume the second as body 1262 + 1263 + // Using new self-closing syntax - should work correctly 1264 + let content = r#"{{ img src="photo.jpg" /}} {{ img src="photo2.jpg" /}}"#; 1265 + let result = with_test_route_context(|route_ctx| { 1266 + preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 1267 + }) 1268 + .unwrap(); 1269 + assert_eq!(result, r#"<img src="photo.jpg"> <img src="photo2.jpg">"#); 1270 + 1271 + // Block shortcode with proper closing tags should still work 1272 + let content = r#"{{ highlight lang="rust" }} 1273 + let x = 5; 1274 + {{ /highlight }} 1275 + 1276 + {{ highlight lang="js" }} 1277 + const y = 10; 1278 + {{ /highlight }}"#; 1279 + let result = with_test_route_context(|route_ctx| { 1280 + preprocess_shortcodes_with_ctx(content, &shortcodes, route_ctx) 1281 + }) 1282 + .unwrap(); 1283 + assert!(result.contains(r#"<code lang="rust">"#)); 1284 + assert!(result.contains("let x = 5;")); 1285 + assert!(result.contains(r#"<code lang="js">"#)); 1286 + assert!(result.contains("const y = 10;")); 1134 1287 } 1135 1288 }
+5 -3
crates/maudit/src/lib.rs
··· 26 26 #[cfg(feature = "maud")] 27 27 #[cfg_attr(docsrs, doc(cfg(feature = "maud")))] 28 28 pub mod maud { 29 - //! Allows to use [Maud](https://maud.lambda.xyz), a macro for writing HTML templates in Rust. 30 - //! 31 - //! Maudit supports Maud by default, but you can use your own templating engine. 29 + //! Allows to use [Maud](https://maud.lambda.xyz), a macro for writing HTML templates, ergonomically in your Maudit pages. 32 30 //! 33 31 //! ## Example 34 32 //! ```rs ··· 59 57 use logging::init_logging; 60 58 use page::FullPage; 61 59 60 + /// Returns whether Maudit is running in development mode (through `maudit dev`). 61 + /// 62 + /// This can be useful to conditionally enable features or logging that should only be active during development. 63 + /// Oftentimes, this is used to disable some expensive operations that would slow down build times during development. 62 64 pub fn is_dev() -> bool { 63 65 if option_env!("MAUDIT_DEV") == Some("true") { 64 66 return true;
+1 -4
crates/maudit/src/page.rs
··· 367 367 #[doc(hidden)] 368 368 /// Used internally by Maudit and should not be implemented by the user. 369 369 /// We expose it because [`maudit_macros::route`] implements it for the user behind the scenes. 370 - pub trait FullPage: InternalPage + Sync { 370 + pub trait FullPage: InternalPage + Sync + Send { 371 371 fn render_internal(&self, ctx: &mut RouteContext) -> RenderResult; 372 372 fn routes_internal(&self, context: &mut DynamicRouteContext) -> RoutesInternalResult; 373 373 } ··· 399 399 DynamicRouteContext, Page, PaginationMeta, RenderResult, Route, RouteContext, RouteParams, 400 400 Routes, get_page_slice, get_page_url, paginate_content, 401 401 }; 402 - // TODO: Remove this internal re-export when possible 403 - #[doc(hidden)] 404 - pub use super::{FullPage, InternalPage}; 405 402 pub use crate::assets::{Asset, Image, Style, StyleOptions}; 406 403 pub use crate::content::MarkdownContent; 407 404 pub use maudit_macros::{Params, route};
+3 -3
crates/maudit/src/templating/maud_ext.rs
··· 1 - use maud::{html, Markup, Render}; 1 + use maud::{Markup, Render, html}; 2 2 3 3 use crate::{ 4 - assets::{Asset, Image, Script, Style}, 5 4 GENERATOR, 5 + assets::{Asset, Image, Script, Style}, 6 6 }; 7 7 8 8 impl Render for Style { ··· 24 24 impl Render for Image { 25 25 fn render(&self) -> Markup { 26 26 html! { 27 - img src=(self.url().unwrap()) loading="lazy" decoding="async"; 27 + img src=(self.url().unwrap()) width=(self.width) height=(self.height) loading="lazy" decoding="async"; 28 28 } 29 29 } 30 30 }
+6 -3
crates/oubli/src/archetypes/blog.rs
··· 4 4 use maud::{html, Markup}; 5 5 use maudit::content::markdown_entry; 6 6 use maudit::page::prelude::*; 7 + use maudit::page::FullPage; 7 8 8 9 pub fn blog_index_content<T: FullPage>( 9 10 route: impl FullPage, ··· 45 46 pub fn blog_entry_routes(ctx: &mut DynamicRouteContext, name: &str) -> Vec<Route<BlogEntryParams>> { 46 47 let blog_entries = ctx.content.get_source::<BlogEntryContent>(name); 47 48 48 - blog_entries.into_routes(|entry| Route::from_params(BlogEntryParams { 49 - entry: entry.id.clone(), 50 - })) 49 + blog_entries.into_routes(|entry| { 50 + Route::from_params(BlogEntryParams { 51 + entry: entry.id.clone(), 52 + }) 53 + }) 51 54 } 52 55 53 56 pub fn blog_entry_render(ctx: &mut RouteContext, name: &str, stringified_ident: &str) -> Markup {
+1 -1
website/content/docs/templating.md
··· 42 42 let logo = ctx.add_image("./logo.png"); 43 43 44 44 html! { 45 - (logo) // Will generate <img src="IMAGE_PATH" loading="lazy" decoding="async" /> 45 + (logo) // Will generate <img src="IMAGE_PATH" width="IMAGE_WIDTH" height="IMAGE_HEIGHT" loading="lazy" decoding="async" /> 46 46 } 47 47 } 48 48 }