1use regex::Regex;
2use std::collections::HashMap;
3use std::sync::LazyLock;
4
5static STANDARD_FRONT_MATTER_START: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^---\s*$").unwrap());
7static STANDARD_FRONT_MATTER_END: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^---\s*$").unwrap());
8
9static TOML_FRONT_MATTER_START: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\+\+\+\s*$").unwrap());
11static TOML_FRONT_MATTER_END: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\+\+\+\s*$").unwrap());
12
13static JSON_FRONT_MATTER_START: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\{\s*$").unwrap());
15static JSON_FRONT_MATTER_END: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\}\s*$").unwrap());
16
17static MALFORMED_FRONT_MATTER_START1: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^- --\s*$").unwrap());
19static MALFORMED_FRONT_MATTER_END1: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^- --\s*$").unwrap());
20
21static MALFORMED_FRONT_MATTER_START2: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^-- -\s*$").unwrap());
23static MALFORMED_FRONT_MATTER_END2: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^-- -\s*$").unwrap());
24
25static FRONT_MATTER_FIELD: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^([^:]+):\s*(.*)$").unwrap());
27
28static TOML_FIELD_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"^([^=]+)\s*=\s*"?([^"]*)"?$"#).unwrap());
30
31#[derive(Debug, PartialEq, Eq, Clone, Copy)]
33pub enum FrontMatterType {
34 Yaml,
36 Toml,
38 Json,
40 Malformed,
42 None,
44}
45
46pub struct FrontMatterUtils;
48
49impl FrontMatterUtils {
50 pub fn is_in_front_matter(content: &str, line_num: usize) -> bool {
52 let lines: Vec<&str> = content.lines().collect();
53 if line_num >= lines.len() {
54 return false;
55 }
56
57 let mut in_standard_front_matter = false;
58 let mut in_toml_front_matter = false;
59 let mut in_json_front_matter = false;
60 let mut in_malformed_front_matter1 = false;
61 let mut in_malformed_front_matter2 = false;
62
63 for (i, line) in lines.iter().enumerate() {
64 if i > line_num {
65 break;
66 }
67
68 if i == line_num
70 && i > 0
71 && ((in_standard_front_matter && STANDARD_FRONT_MATTER_END.is_match(line))
72 || (in_toml_front_matter && TOML_FRONT_MATTER_END.is_match(line))
73 || (in_json_front_matter && JSON_FRONT_MATTER_END.is_match(line))
74 || (in_malformed_front_matter1 && MALFORMED_FRONT_MATTER_END1.is_match(line))
75 || (in_malformed_front_matter2 && MALFORMED_FRONT_MATTER_END2.is_match(line)))
76 {
77 return false; }
79
80 if i == 0 && STANDARD_FRONT_MATTER_START.is_match(line) {
82 in_standard_front_matter = true;
83 } else if STANDARD_FRONT_MATTER_END.is_match(line) && in_standard_front_matter && i > 0 {
84 in_standard_front_matter = false;
85 }
86 else if i == 0 && TOML_FRONT_MATTER_START.is_match(line) {
88 in_toml_front_matter = true;
89 } else if TOML_FRONT_MATTER_END.is_match(line) && in_toml_front_matter && i > 0 {
90 in_toml_front_matter = false;
91 }
92 else if i == 0 && JSON_FRONT_MATTER_START.is_match(line) {
94 in_json_front_matter = true;
95 } else if JSON_FRONT_MATTER_END.is_match(line) && in_json_front_matter && i > 0 {
96 in_json_front_matter = false;
97 }
98 else if i == 0 && MALFORMED_FRONT_MATTER_START1.is_match(line) {
100 in_malformed_front_matter1 = true;
101 } else if MALFORMED_FRONT_MATTER_END1.is_match(line) && in_malformed_front_matter1 && i > 0 {
102 in_malformed_front_matter1 = false;
103 }
104 else if i == 0 && MALFORMED_FRONT_MATTER_START2.is_match(line) {
106 in_malformed_front_matter2 = true;
107 } else if MALFORMED_FRONT_MATTER_END2.is_match(line) && in_malformed_front_matter2 && i > 0 {
108 in_malformed_front_matter2 = false;
109 }
110 }
111
112 in_standard_front_matter
114 || in_toml_front_matter
115 || in_json_front_matter
116 || in_malformed_front_matter1
117 || in_malformed_front_matter2
118 }
119
120 pub fn has_front_matter_field(content: &str, field_prefix: &str) -> bool {
122 let lines: Vec<&str> = content.lines().collect();
123 if lines.len() < 3 {
124 return false;
125 }
126
127 let front_matter_type = Self::detect_front_matter_type(content);
128 if front_matter_type == FrontMatterType::None {
129 return false;
130 }
131
132 let front_matter = Self::extract_front_matter(content);
133 for line in front_matter {
134 if line.trim().starts_with(field_prefix) {
135 return true;
136 }
137 }
138
139 false
140 }
141
142 pub fn get_front_matter_field_value<'a>(content: &'a str, field_name: &str) -> Option<&'a str> {
144 let lines: Vec<&'a str> = content.lines().collect();
145 if lines.len() < 3 {
146 return None;
147 }
148
149 let front_matter_type = Self::detect_front_matter_type(content);
150 if front_matter_type == FrontMatterType::None {
151 return None;
152 }
153
154 let front_matter = Self::extract_front_matter(content);
155 for line in front_matter {
156 let line = line.trim();
157 match front_matter_type {
158 FrontMatterType::Toml => {
159 if let Some(captures) = TOML_FIELD_PATTERN.captures(line) {
161 let key = captures.get(1).unwrap().as_str().trim();
162 if key == field_name {
163 let value = captures.get(2).unwrap().as_str();
164 return Some(value);
165 }
166 }
167 }
168 _ => {
169 if let Some(captures) = FRONT_MATTER_FIELD.captures(line) {
171 let mut key = captures.get(1).unwrap().as_str().trim();
172
173 if key.starts_with('"') && key.ends_with('"') && key.len() >= 2 {
175 key = &key[1..key.len() - 1];
176 }
177
178 if key == field_name {
179 let value = captures.get(2).unwrap().as_str().trim();
180 if value.starts_with('"') && value.ends_with('"') && value.len() >= 2 {
182 return Some(&value[1..value.len() - 1]);
183 }
184 return Some(value);
185 }
186 }
187 }
188 }
189 }
190
191 None
192 }
193
194 pub fn extract_front_matter_fields(content: &str) -> HashMap<String, String> {
196 let mut fields = HashMap::new();
197
198 let front_matter_type = Self::detect_front_matter_type(content);
199 if front_matter_type == FrontMatterType::None {
200 return fields;
201 }
202
203 let front_matter = Self::extract_front_matter(content);
204 let mut current_prefix = String::new();
205 let mut indent_level = 0;
206
207 for line in front_matter {
208 let line_indent = line.chars().take_while(|c| c.is_whitespace()).count();
209 let line = line.trim();
210
211 match line_indent.cmp(&indent_level) {
213 std::cmp::Ordering::Greater => {
214 indent_level = line_indent;
216 }
217 std::cmp::Ordering::Less => {
218 indent_level = line_indent;
220 if let Some(last_dot) = current_prefix.rfind('.') {
222 current_prefix.truncate(last_dot);
223 } else {
224 current_prefix.clear();
225 }
226 }
227 std::cmp::Ordering::Equal => {}
228 }
229
230 match front_matter_type {
231 FrontMatterType::Toml => {
232 if let Some(captures) = TOML_FIELD_PATTERN.captures(line) {
234 let key = captures.get(1).unwrap().as_str().trim();
235 let value = captures.get(2).unwrap().as_str();
236 let full_key = if current_prefix.is_empty() {
237 key.to_string()
238 } else {
239 format!("{current_prefix}.{key}")
240 };
241 fields.insert(full_key, value.to_string());
242 }
243 }
244 _ => {
245 if let Some(captures) = FRONT_MATTER_FIELD.captures(line) {
247 let mut key = captures.get(1).unwrap().as_str().trim();
248 let value = captures.get(2).unwrap().as_str().trim();
249
250 if key.starts_with('"') && key.ends_with('"') && key.len() >= 2 {
252 key = &key[1..key.len() - 1];
253 }
254
255 if let Some(stripped) = key.strip_suffix(':') {
256 if current_prefix.is_empty() {
258 current_prefix = stripped.to_string();
259 } else {
260 current_prefix = format!("{current_prefix}.{stripped}");
261 }
262 } else {
263 let full_key = if current_prefix.is_empty() {
265 key.to_string()
266 } else {
267 format!("{current_prefix}.{key}")
268 };
269 let value = value
271 .strip_prefix('"')
272 .and_then(|v| v.strip_suffix('"'))
273 .unwrap_or(value);
274 fields.insert(full_key, value.to_string());
275 }
276 }
277 }
278 }
279 }
280
281 fields
282 }
283
284 pub fn extract_front_matter<'a>(content: &'a str) -> Vec<&'a str> {
286 let lines: Vec<&'a str> = content.lines().collect();
287 if lines.len() < 3 {
288 return Vec::new();
289 }
290
291 let front_matter_type = Self::detect_front_matter_type(content);
292 if front_matter_type == FrontMatterType::None {
293 return Vec::new();
294 }
295
296 let mut front_matter = Vec::new();
297 let mut in_front_matter = false;
298
299 for (i, line) in lines.iter().enumerate() {
300 match front_matter_type {
301 FrontMatterType::Yaml => {
302 if i == 0 && STANDARD_FRONT_MATTER_START.is_match(line) {
303 in_front_matter = true;
304 continue;
305 } else if STANDARD_FRONT_MATTER_END.is_match(line) && in_front_matter && i > 0 {
306 break;
307 }
308 }
309 FrontMatterType::Toml => {
310 if i == 0 && TOML_FRONT_MATTER_START.is_match(line) {
311 in_front_matter = true;
312 continue;
313 } else if TOML_FRONT_MATTER_END.is_match(line) && in_front_matter && i > 0 {
314 break;
315 }
316 }
317 FrontMatterType::Json => {
318 if i == 0 && JSON_FRONT_MATTER_START.is_match(line) {
319 in_front_matter = true;
320 continue;
321 } else if JSON_FRONT_MATTER_END.is_match(line) && in_front_matter && i > 0 {
322 break;
323 }
324 }
325 FrontMatterType::Malformed => {
326 if i == 0
327 && (MALFORMED_FRONT_MATTER_START1.is_match(line)
328 || MALFORMED_FRONT_MATTER_START2.is_match(line))
329 {
330 in_front_matter = true;
331 continue;
332 } else if (MALFORMED_FRONT_MATTER_END1.is_match(line) || MALFORMED_FRONT_MATTER_END2.is_match(line))
333 && in_front_matter
334 && i > 0
335 {
336 break;
337 }
338 }
339 FrontMatterType::None => break,
340 }
341
342 if in_front_matter {
343 front_matter.push(*line);
344 }
345 }
346
347 front_matter
348 }
349
350 pub fn detect_front_matter_type(content: &str) -> FrontMatterType {
352 let lines: Vec<&str> = content.lines().collect();
353 if lines.is_empty() {
354 return FrontMatterType::None;
355 }
356
357 let first_line = lines[0];
358
359 if STANDARD_FRONT_MATTER_START.is_match(first_line) {
360 for line in lines.iter().skip(1) {
362 if STANDARD_FRONT_MATTER_END.is_match(line) {
363 return FrontMatterType::Yaml;
364 }
365 }
366 } else if TOML_FRONT_MATTER_START.is_match(first_line) {
367 for line in lines.iter().skip(1) {
369 if TOML_FRONT_MATTER_END.is_match(line) {
370 return FrontMatterType::Toml;
371 }
372 }
373 } else if JSON_FRONT_MATTER_START.is_match(first_line) {
374 for line in lines.iter().skip(1) {
376 if JSON_FRONT_MATTER_END.is_match(line) {
377 return FrontMatterType::Json;
378 }
379 }
380 } else if MALFORMED_FRONT_MATTER_START1.is_match(first_line)
381 || MALFORMED_FRONT_MATTER_START2.is_match(first_line)
382 {
383 for line in lines.iter().skip(1) {
385 if MALFORMED_FRONT_MATTER_END1.is_match(line) || MALFORMED_FRONT_MATTER_END2.is_match(line) {
386 return FrontMatterType::Malformed;
387 }
388 }
389 }
390
391 FrontMatterType::None
392 }
393
394 pub fn get_front_matter_end_line(content: &str) -> usize {
396 let lines: Vec<&str> = content.lines().collect();
397 if lines.len() < 3 {
398 return 0;
399 }
400
401 let front_matter_type = Self::detect_front_matter_type(content);
402 if front_matter_type == FrontMatterType::None {
403 return 0;
404 }
405
406 let mut in_front_matter = false;
407
408 for (i, line) in lines.iter().enumerate() {
409 match front_matter_type {
410 FrontMatterType::Yaml => {
411 if i == 0 && STANDARD_FRONT_MATTER_START.is_match(line) {
412 in_front_matter = true;
413 } else if STANDARD_FRONT_MATTER_END.is_match(line) && in_front_matter && i > 0 {
414 return i + 1;
415 }
416 }
417 FrontMatterType::Toml => {
418 if i == 0 && TOML_FRONT_MATTER_START.is_match(line) {
419 in_front_matter = true;
420 } else if TOML_FRONT_MATTER_END.is_match(line) && in_front_matter && i > 0 {
421 return i + 1;
422 }
423 }
424 FrontMatterType::Json => {
425 if i == 0 && JSON_FRONT_MATTER_START.is_match(line) {
426 in_front_matter = true;
427 } else if JSON_FRONT_MATTER_END.is_match(line) && in_front_matter && i > 0 {
428 return i + 1;
429 }
430 }
431 FrontMatterType::Malformed => {
432 if i == 0
433 && (MALFORMED_FRONT_MATTER_START1.is_match(line)
434 || MALFORMED_FRONT_MATTER_START2.is_match(line))
435 {
436 in_front_matter = true;
437 } else if (MALFORMED_FRONT_MATTER_END1.is_match(line) || MALFORMED_FRONT_MATTER_END2.is_match(line))
438 && in_front_matter
439 && i > 0
440 {
441 return i + 1;
442 }
443 }
444 FrontMatterType::None => return 0,
445 }
446 }
447
448 0
449 }
450
451 pub fn fix_malformed_front_matter(content: &str) -> String {
453 let lines: Vec<&str> = content.lines().collect();
454 if lines.len() < 3 {
455 return content.to_string();
456 }
457
458 let mut result = Vec::new();
459 let mut in_front_matter = false;
460 let mut is_malformed = false;
461
462 for (i, line) in lines.iter().enumerate() {
463 if i == 0 {
465 if STANDARD_FRONT_MATTER_START.is_match(line) {
466 in_front_matter = true;
468 result.push(line.to_string());
469 } else if MALFORMED_FRONT_MATTER_START1.is_match(line) || MALFORMED_FRONT_MATTER_START2.is_match(line) {
470 in_front_matter = true;
472 is_malformed = true;
473 result.push("---".to_string());
474 } else {
475 result.push(line.to_string());
477 }
478 continue;
479 }
480
481 if in_front_matter {
483 if STANDARD_FRONT_MATTER_END.is_match(line) {
484 in_front_matter = false;
486 result.push(line.to_string());
487 } else if (MALFORMED_FRONT_MATTER_END1.is_match(line) || MALFORMED_FRONT_MATTER_END2.is_match(line))
488 && is_malformed
489 {
490 in_front_matter = false;
492 result.push("---".to_string());
493 } else {
494 result.push(line.to_string());
496 }
497 continue;
498 }
499
500 result.push(line.to_string());
502 }
503
504 result.join("\n")
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511
512 #[test]
513 fn test_front_matter_type_enum() {
514 assert_eq!(FrontMatterType::Yaml, FrontMatterType::Yaml);
515 assert_eq!(FrontMatterType::Toml, FrontMatterType::Toml);
516 assert_eq!(FrontMatterType::Json, FrontMatterType::Json);
517 assert_eq!(FrontMatterType::Malformed, FrontMatterType::Malformed);
518 assert_eq!(FrontMatterType::None, FrontMatterType::None);
519 assert_ne!(FrontMatterType::Yaml, FrontMatterType::Toml);
520 }
521
522 #[test]
523 fn test_detect_front_matter_type() {
524 let yaml_content = "---\ntitle: Test\n---\nContent";
526 assert_eq!(
527 FrontMatterUtils::detect_front_matter_type(yaml_content),
528 FrontMatterType::Yaml
529 );
530
531 let toml_content = "+++\ntitle = \"Test\"\n+++\nContent";
533 assert_eq!(
534 FrontMatterUtils::detect_front_matter_type(toml_content),
535 FrontMatterType::Toml
536 );
537
538 let json_content = "{\n\"title\": \"Test\"\n}\nContent";
540 assert_eq!(
541 FrontMatterUtils::detect_front_matter_type(json_content),
542 FrontMatterType::Json
543 );
544
545 let malformed1 = "- --\ntitle: Test\n- --\nContent";
547 assert_eq!(
548 FrontMatterUtils::detect_front_matter_type(malformed1),
549 FrontMatterType::Malformed
550 );
551
552 let malformed2 = "-- -\ntitle: Test\n-- -\nContent";
553 assert_eq!(
554 FrontMatterUtils::detect_front_matter_type(malformed2),
555 FrontMatterType::Malformed
556 );
557
558 assert_eq!(
560 FrontMatterUtils::detect_front_matter_type("# Regular content"),
561 FrontMatterType::None
562 );
563 assert_eq!(FrontMatterUtils::detect_front_matter_type(""), FrontMatterType::None);
564
565 assert_eq!(
567 FrontMatterUtils::detect_front_matter_type("---\ntitle: Test"),
568 FrontMatterType::None
569 );
570 }
571
572 #[test]
573 fn test_is_in_front_matter() {
574 let content = "---\ntitle: Test\nauthor: Me\n---\nContent here";
575
576 assert!(FrontMatterUtils::is_in_front_matter(content, 0)); assert!(FrontMatterUtils::is_in_front_matter(content, 1)); assert!(FrontMatterUtils::is_in_front_matter(content, 2)); assert!(!FrontMatterUtils::is_in_front_matter(content, 3)); assert!(!FrontMatterUtils::is_in_front_matter(content, 4)); assert!(!FrontMatterUtils::is_in_front_matter(content, 100));
586 }
587
588 #[test]
589 fn test_extract_front_matter() {
590 let content = "---\ntitle: Test\nauthor: Me\n---\nContent";
591 let front_matter = FrontMatterUtils::extract_front_matter(content);
592
593 assert_eq!(front_matter.len(), 2);
594 assert_eq!(front_matter[0], "title: Test");
595 assert_eq!(front_matter[1], "author: Me");
596
597 let no_fm = FrontMatterUtils::extract_front_matter("Regular content");
599 assert!(no_fm.is_empty());
600
601 let short = FrontMatterUtils::extract_front_matter("---\n---");
603 assert!(short.is_empty());
604 }
605
606 #[test]
607 fn test_has_front_matter_field() {
608 let content = "---\ntitle: Test\nauthor: Me\n---\nContent";
609
610 assert!(FrontMatterUtils::has_front_matter_field(content, "title"));
611 assert!(FrontMatterUtils::has_front_matter_field(content, "author"));
612 assert!(!FrontMatterUtils::has_front_matter_field(content, "date"));
613
614 assert!(!FrontMatterUtils::has_front_matter_field("Regular content", "title"));
616
617 assert!(!FrontMatterUtils::has_front_matter_field("--", "title"));
619 }
620
621 #[test]
622 fn test_get_front_matter_field_value() {
623 let yaml_content = "---\ntitle: Test Title\nauthor: \"John Doe\"\n---\nContent";
625 assert_eq!(
626 FrontMatterUtils::get_front_matter_field_value(yaml_content, "title"),
627 Some("Test Title")
628 );
629 assert_eq!(
630 FrontMatterUtils::get_front_matter_field_value(yaml_content, "author"),
631 Some("John Doe")
632 );
633 assert_eq!(
634 FrontMatterUtils::get_front_matter_field_value(yaml_content, "nonexistent"),
635 None
636 );
637
638 let toml_content = "+++\ntitle = \"Test Title\"\nauthor = \"John Doe\"\n+++\nContent";
640 assert_eq!(
641 FrontMatterUtils::get_front_matter_field_value(toml_content, "title"),
642 Some("Test Title")
643 );
644 assert_eq!(
645 FrontMatterUtils::get_front_matter_field_value(toml_content, "author"),
646 Some("John Doe")
647 );
648
649 let json_style_yaml = "---\n\"title\": \"Test Title\"\n---\nContent";
651 assert_eq!(
652 FrontMatterUtils::get_front_matter_field_value(json_style_yaml, "title"),
653 Some("Test Title")
654 );
655
656 let json_fm = "{\n\"title\": \"Test Title\"\n}\nContent";
658 assert_eq!(
659 FrontMatterUtils::get_front_matter_field_value(json_fm, "title"),
660 Some("Test Title")
661 );
662
663 assert_eq!(
665 FrontMatterUtils::get_front_matter_field_value("Regular content", "title"),
666 None
667 );
668
669 assert_eq!(FrontMatterUtils::get_front_matter_field_value("--", "title"), None);
671 }
672
673 #[test]
674 fn test_extract_front_matter_fields() {
675 let yaml_content = "---\ntitle: Test\nauthor: Me\n---\nContent";
677 let fields = FrontMatterUtils::extract_front_matter_fields(yaml_content);
678
679 assert_eq!(fields.get("title"), Some(&"Test".to_string()));
680 assert_eq!(fields.get("author"), Some(&"Me".to_string()));
681
682 let toml_content = "+++\ntitle = \"Test\"\nauthor = \"Me\"\n+++\nContent";
684 let toml_fields = FrontMatterUtils::extract_front_matter_fields(toml_content);
685
686 assert_eq!(toml_fields.get("title"), Some(&"Test".to_string()));
687 assert_eq!(toml_fields.get("author"), Some(&"Me".to_string()));
688
689 let no_fields = FrontMatterUtils::extract_front_matter_fields("Regular content");
691 assert!(no_fields.is_empty());
692 }
693
694 #[test]
695 fn test_get_front_matter_end_line() {
696 let content = "---\ntitle: Test\n---\nContent";
697 assert_eq!(FrontMatterUtils::get_front_matter_end_line(content), 3);
698
699 let toml_content = "+++\ntitle = \"Test\"\n+++\nContent";
701 assert_eq!(FrontMatterUtils::get_front_matter_end_line(toml_content), 3);
702
703 assert_eq!(FrontMatterUtils::get_front_matter_end_line("Regular content"), 0);
705
706 assert_eq!(FrontMatterUtils::get_front_matter_end_line("--"), 0);
708 }
709
710 #[test]
711 fn test_fix_malformed_front_matter() {
712 let malformed1 = "- --\ntitle: Test\n- --\nContent";
714 let fixed1 = FrontMatterUtils::fix_malformed_front_matter(malformed1);
715 assert!(fixed1.starts_with("---\ntitle: Test\n---"));
716
717 let malformed2 = "-- -\ntitle: Test\n-- -\nContent";
719 let fixed2 = FrontMatterUtils::fix_malformed_front_matter(malformed2);
720 assert!(fixed2.starts_with("---\ntitle: Test\n---"));
721
722 let valid = "---\ntitle: Test\n---\nContent";
724 let unchanged = FrontMatterUtils::fix_malformed_front_matter(valid);
725 assert_eq!(unchanged, valid);
726
727 let no_fm = "# Regular content";
729 assert_eq!(FrontMatterUtils::fix_malformed_front_matter(no_fm), no_fm);
730
731 let short = "--";
733 assert_eq!(FrontMatterUtils::fix_malformed_front_matter(short), short);
734 }
735
736 #[test]
737 fn test_nested_yaml_fields() {
738 let content = "---
739title: Test
740author:
741 name: John Doe
742 email: john@example.com
743---
744Content";
745
746 let fields = FrontMatterUtils::extract_front_matter_fields(content);
747
748 assert!(fields.contains_key("title"));
751 }
753
754 #[test]
755 fn test_edge_cases() {
756 assert_eq!(FrontMatterUtils::detect_front_matter_type(""), FrontMatterType::None);
758 assert!(FrontMatterUtils::extract_front_matter("").is_empty());
759 assert_eq!(FrontMatterUtils::get_front_matter_end_line(""), 0);
760
761 let only_delim = "---\n---";
763 assert!(FrontMatterUtils::extract_front_matter(only_delim).is_empty());
764
765 let multiple = "---\ntitle: First\n---\n---\ntitle: Second\n---";
767 let fm_type = FrontMatterUtils::detect_front_matter_type(multiple);
768 assert_eq!(fm_type, FrontMatterType::Yaml);
769 let fields = FrontMatterUtils::extract_front_matter_fields(multiple);
770 assert_eq!(fields.get("title"), Some(&"First".to_string()));
771 }
772
773 #[test]
774 fn test_unicode_content() {
775 let content = "---\ntitle: 你好世界\nauthor: José\n---\nContent";
776
777 assert_eq!(
778 FrontMatterUtils::detect_front_matter_type(content),
779 FrontMatterType::Yaml
780 );
781 assert_eq!(
782 FrontMatterUtils::get_front_matter_field_value(content, "title"),
783 Some("你好世界")
784 );
785 assert_eq!(
786 FrontMatterUtils::get_front_matter_field_value(content, "author"),
787 Some("José")
788 );
789 }
790}