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 field_name = field_prefix.trim_end_matches(':');
123 Self::get_front_matter_field_value(content, field_name).is_some()
124 }
125
126 pub fn get_front_matter_field_value<'a>(content: &'a str, field_name: &str) -> Option<&'a str> {
128 let lines: Vec<&'a str> = content.lines().collect();
129 if lines.len() < 3 {
130 return None;
131 }
132
133 let front_matter_type = Self::detect_front_matter_type(content);
134 if front_matter_type == FrontMatterType::None {
135 return None;
136 }
137
138 let front_matter = Self::extract_front_matter(content);
139 for line in front_matter {
140 let line = line.trim();
141 match front_matter_type {
142 FrontMatterType::Toml => {
143 if let Some(captures) = TOML_FIELD_PATTERN.captures(line) {
145 let key = captures.get(1).unwrap().as_str().trim();
146 if key == field_name {
147 let value = captures.get(2).unwrap().as_str();
148 return Some(value);
149 }
150 }
151 }
152 _ => {
153 if let Some(captures) = FRONT_MATTER_FIELD.captures(line) {
155 let mut key = captures.get(1).unwrap().as_str().trim();
156
157 if key.starts_with('"') && key.ends_with('"') && key.len() >= 2 {
159 key = &key[1..key.len() - 1];
160 }
161
162 if key == field_name {
163 let value = captures.get(2).unwrap().as_str().trim();
164 if value.starts_with('"') && value.ends_with('"') && value.len() >= 2 {
166 return Some(&value[1..value.len() - 1]);
167 }
168 return Some(value);
169 }
170 }
171 }
172 }
173 }
174
175 None
176 }
177
178 pub fn extract_front_matter_fields(content: &str) -> HashMap<String, String> {
180 let mut fields = HashMap::new();
181
182 let front_matter_type = Self::detect_front_matter_type(content);
183 if front_matter_type == FrontMatterType::None {
184 return fields;
185 }
186
187 let front_matter = Self::extract_front_matter(content);
188 let mut current_prefix = String::new();
189 let mut indent_level = 0;
190
191 for line in front_matter {
192 let line_indent = line.chars().take_while(|c| c.is_whitespace()).count();
193 let line = line.trim();
194
195 match line_indent.cmp(&indent_level) {
197 std::cmp::Ordering::Greater => {
198 indent_level = line_indent;
200 }
201 std::cmp::Ordering::Less => {
202 indent_level = line_indent;
204 if let Some(last_dot) = current_prefix.rfind('.') {
206 current_prefix.truncate(last_dot);
207 } else {
208 current_prefix.clear();
209 }
210 }
211 std::cmp::Ordering::Equal => {}
212 }
213
214 match front_matter_type {
215 FrontMatterType::Toml => {
216 if let Some(captures) = TOML_FIELD_PATTERN.captures(line) {
218 let key = captures.get(1).unwrap().as_str().trim();
219 let value = captures.get(2).unwrap().as_str();
220 let full_key = if current_prefix.is_empty() {
221 key.to_string()
222 } else {
223 format!("{current_prefix}.{key}")
224 };
225 fields.insert(full_key, value.to_string());
226 }
227 }
228 _ => {
229 if let Some(captures) = FRONT_MATTER_FIELD.captures(line) {
231 let mut key = captures.get(1).unwrap().as_str().trim();
232 let value = captures.get(2).unwrap().as_str().trim();
233
234 if key.starts_with('"') && key.ends_with('"') && key.len() >= 2 {
236 key = &key[1..key.len() - 1];
237 }
238
239 if let Some(stripped) = key.strip_suffix(':') {
240 if current_prefix.is_empty() {
242 current_prefix = stripped.to_string();
243 } else {
244 current_prefix = format!("{current_prefix}.{stripped}");
245 }
246 } else {
247 let full_key = if current_prefix.is_empty() {
249 key.to_string()
250 } else {
251 format!("{current_prefix}.{key}")
252 };
253 let value = value
255 .strip_prefix('"')
256 .and_then(|v| v.strip_suffix('"'))
257 .unwrap_or(value);
258 fields.insert(full_key, value.to_string());
259 }
260 }
261 }
262 }
263 }
264
265 fields
266 }
267
268 pub fn extract_front_matter<'a>(content: &'a str) -> Vec<&'a str> {
270 let lines: Vec<&'a str> = content.lines().collect();
271 if lines.len() < 3 {
272 return Vec::new();
273 }
274
275 let front_matter_type = Self::detect_front_matter_type(content);
276 if front_matter_type == FrontMatterType::None {
277 return Vec::new();
278 }
279
280 let mut front_matter = Vec::new();
281 let mut in_front_matter = false;
282
283 for (i, line) in lines.iter().enumerate() {
284 match front_matter_type {
285 FrontMatterType::Yaml => {
286 if i == 0 && STANDARD_FRONT_MATTER_START.is_match(line) {
287 in_front_matter = true;
288 continue;
289 } else if STANDARD_FRONT_MATTER_END.is_match(line) && in_front_matter && i > 0 {
290 break;
291 }
292 }
293 FrontMatterType::Toml => {
294 if i == 0 && TOML_FRONT_MATTER_START.is_match(line) {
295 in_front_matter = true;
296 continue;
297 } else if TOML_FRONT_MATTER_END.is_match(line) && in_front_matter && i > 0 {
298 break;
299 }
300 }
301 FrontMatterType::Json => {
302 if i == 0 && JSON_FRONT_MATTER_START.is_match(line) {
303 in_front_matter = true;
304 continue;
305 } else if JSON_FRONT_MATTER_END.is_match(line) && in_front_matter && i > 0 {
306 break;
307 }
308 }
309 FrontMatterType::Malformed => {
310 if i == 0
311 && (MALFORMED_FRONT_MATTER_START1.is_match(line)
312 || MALFORMED_FRONT_MATTER_START2.is_match(line))
313 {
314 in_front_matter = true;
315 continue;
316 } else if (MALFORMED_FRONT_MATTER_END1.is_match(line) || MALFORMED_FRONT_MATTER_END2.is_match(line))
317 && in_front_matter
318 && i > 0
319 {
320 break;
321 }
322 }
323 FrontMatterType::None => break,
324 }
325
326 if in_front_matter {
327 front_matter.push(*line);
328 }
329 }
330
331 front_matter
332 }
333
334 pub fn detect_front_matter_type(content: &str) -> FrontMatterType {
336 let lines: Vec<&str> = content.lines().collect();
337 if lines.is_empty() {
338 return FrontMatterType::None;
339 }
340
341 let first_line = lines[0];
342
343 if STANDARD_FRONT_MATTER_START.is_match(first_line) {
344 for line in lines.iter().skip(1) {
346 if STANDARD_FRONT_MATTER_END.is_match(line) {
347 return FrontMatterType::Yaml;
348 }
349 }
350 } else if TOML_FRONT_MATTER_START.is_match(first_line) {
351 for line in lines.iter().skip(1) {
353 if TOML_FRONT_MATTER_END.is_match(line) {
354 return FrontMatterType::Toml;
355 }
356 }
357 } else if JSON_FRONT_MATTER_START.is_match(first_line) {
358 for line in lines.iter().skip(1) {
360 if JSON_FRONT_MATTER_END.is_match(line) {
361 return FrontMatterType::Json;
362 }
363 }
364 } else if MALFORMED_FRONT_MATTER_START1.is_match(first_line)
365 || MALFORMED_FRONT_MATTER_START2.is_match(first_line)
366 {
367 for line in lines.iter().skip(1) {
369 if MALFORMED_FRONT_MATTER_END1.is_match(line) || MALFORMED_FRONT_MATTER_END2.is_match(line) {
370 return FrontMatterType::Malformed;
371 }
372 }
373 }
374
375 FrontMatterType::None
376 }
377
378 pub fn get_front_matter_end_line(content: &str) -> usize {
380 let lines: Vec<&str> = content.lines().collect();
381 if lines.len() < 3 {
382 return 0;
383 }
384
385 let front_matter_type = Self::detect_front_matter_type(content);
386 if front_matter_type == FrontMatterType::None {
387 return 0;
388 }
389
390 let mut in_front_matter = false;
391
392 for (i, line) in lines.iter().enumerate() {
393 match front_matter_type {
394 FrontMatterType::Yaml => {
395 if i == 0 && STANDARD_FRONT_MATTER_START.is_match(line) {
396 in_front_matter = true;
397 } else if STANDARD_FRONT_MATTER_END.is_match(line) && in_front_matter && i > 0 {
398 return i + 1;
399 }
400 }
401 FrontMatterType::Toml => {
402 if i == 0 && TOML_FRONT_MATTER_START.is_match(line) {
403 in_front_matter = true;
404 } else if TOML_FRONT_MATTER_END.is_match(line) && in_front_matter && i > 0 {
405 return i + 1;
406 }
407 }
408 FrontMatterType::Json => {
409 if i == 0 && JSON_FRONT_MATTER_START.is_match(line) {
410 in_front_matter = true;
411 } else if JSON_FRONT_MATTER_END.is_match(line) && in_front_matter && i > 0 {
412 return i + 1;
413 }
414 }
415 FrontMatterType::Malformed => {
416 if i == 0
417 && (MALFORMED_FRONT_MATTER_START1.is_match(line)
418 || MALFORMED_FRONT_MATTER_START2.is_match(line))
419 {
420 in_front_matter = true;
421 } else if (MALFORMED_FRONT_MATTER_END1.is_match(line) || MALFORMED_FRONT_MATTER_END2.is_match(line))
422 && in_front_matter
423 && i > 0
424 {
425 return i + 1;
426 }
427 }
428 FrontMatterType::None => return 0,
429 }
430 }
431
432 0
433 }
434
435 pub fn fix_malformed_front_matter(content: &str) -> String {
437 let lines: Vec<&str> = content.lines().collect();
438 if lines.len() < 3 {
439 return content.to_string();
440 }
441
442 let mut result = Vec::new();
443 let mut in_front_matter = false;
444 let mut is_malformed = false;
445
446 for (i, line) in lines.iter().enumerate() {
447 if i == 0 {
449 if STANDARD_FRONT_MATTER_START.is_match(line) {
450 in_front_matter = true;
452 result.push(line.to_string());
453 } else if MALFORMED_FRONT_MATTER_START1.is_match(line) || MALFORMED_FRONT_MATTER_START2.is_match(line) {
454 in_front_matter = true;
456 is_malformed = true;
457 result.push("---".to_string());
458 } else {
459 result.push(line.to_string());
461 }
462 continue;
463 }
464
465 if in_front_matter {
467 if STANDARD_FRONT_MATTER_END.is_match(line) {
468 in_front_matter = false;
470 result.push(line.to_string());
471 } else if (MALFORMED_FRONT_MATTER_END1.is_match(line) || MALFORMED_FRONT_MATTER_END2.is_match(line))
472 && is_malformed
473 {
474 in_front_matter = false;
476 result.push("---".to_string());
477 } else {
478 result.push(line.to_string());
480 }
481 continue;
482 }
483
484 result.push(line.to_string());
486 }
487
488 result.join("\n")
489 }
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495
496 #[test]
497 fn test_front_matter_type_enum() {
498 assert_eq!(FrontMatterType::Yaml, FrontMatterType::Yaml);
499 assert_eq!(FrontMatterType::Toml, FrontMatterType::Toml);
500 assert_eq!(FrontMatterType::Json, FrontMatterType::Json);
501 assert_eq!(FrontMatterType::Malformed, FrontMatterType::Malformed);
502 assert_eq!(FrontMatterType::None, FrontMatterType::None);
503 assert_ne!(FrontMatterType::Yaml, FrontMatterType::Toml);
504 }
505
506 #[test]
507 fn test_detect_front_matter_type() {
508 let yaml_content = "---\ntitle: Test\n---\nContent";
510 assert_eq!(
511 FrontMatterUtils::detect_front_matter_type(yaml_content),
512 FrontMatterType::Yaml
513 );
514
515 let toml_content = "+++\ntitle = \"Test\"\n+++\nContent";
517 assert_eq!(
518 FrontMatterUtils::detect_front_matter_type(toml_content),
519 FrontMatterType::Toml
520 );
521
522 let json_content = "{\n\"title\": \"Test\"\n}\nContent";
524 assert_eq!(
525 FrontMatterUtils::detect_front_matter_type(json_content),
526 FrontMatterType::Json
527 );
528
529 let malformed1 = "- --\ntitle: Test\n- --\nContent";
531 assert_eq!(
532 FrontMatterUtils::detect_front_matter_type(malformed1),
533 FrontMatterType::Malformed
534 );
535
536 let malformed2 = "-- -\ntitle: Test\n-- -\nContent";
537 assert_eq!(
538 FrontMatterUtils::detect_front_matter_type(malformed2),
539 FrontMatterType::Malformed
540 );
541
542 assert_eq!(
544 FrontMatterUtils::detect_front_matter_type("# Regular content"),
545 FrontMatterType::None
546 );
547 assert_eq!(FrontMatterUtils::detect_front_matter_type(""), FrontMatterType::None);
548
549 assert_eq!(
551 FrontMatterUtils::detect_front_matter_type("---\ntitle: Test"),
552 FrontMatterType::None
553 );
554 }
555
556 #[test]
557 fn test_is_in_front_matter() {
558 let content = "---\ntitle: Test\nauthor: Me\n---\nContent here";
559
560 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));
570 }
571
572 #[test]
573 fn test_extract_front_matter() {
574 let content = "---\ntitle: Test\nauthor: Me\n---\nContent";
575 let front_matter = FrontMatterUtils::extract_front_matter(content);
576
577 assert_eq!(front_matter.len(), 2);
578 assert_eq!(front_matter[0], "title: Test");
579 assert_eq!(front_matter[1], "author: Me");
580
581 let no_fm = FrontMatterUtils::extract_front_matter("Regular content");
583 assert!(no_fm.is_empty());
584
585 let short = FrontMatterUtils::extract_front_matter("---\n---");
587 assert!(short.is_empty());
588 }
589
590 #[test]
591 fn test_has_front_matter_field() {
592 let content = "---\ntitle: Test\nauthor: Me\n---\nContent";
593
594 assert!(FrontMatterUtils::has_front_matter_field(content, "title"));
595 assert!(FrontMatterUtils::has_front_matter_field(content, "author"));
596 assert!(!FrontMatterUtils::has_front_matter_field(content, "date"));
597
598 assert!(!FrontMatterUtils::has_front_matter_field("Regular content", "title"));
600
601 assert!(!FrontMatterUtils::has_front_matter_field("--", "title"));
603 }
604
605 #[test]
606 fn test_get_front_matter_field_value() {
607 let yaml_content = "---\ntitle: Test Title\nauthor: \"John Doe\"\n---\nContent";
609 assert_eq!(
610 FrontMatterUtils::get_front_matter_field_value(yaml_content, "title"),
611 Some("Test Title")
612 );
613 assert_eq!(
614 FrontMatterUtils::get_front_matter_field_value(yaml_content, "author"),
615 Some("John Doe")
616 );
617 assert_eq!(
618 FrontMatterUtils::get_front_matter_field_value(yaml_content, "nonexistent"),
619 None
620 );
621
622 let toml_content = "+++\ntitle = \"Test Title\"\nauthor = \"John Doe\"\n+++\nContent";
624 assert_eq!(
625 FrontMatterUtils::get_front_matter_field_value(toml_content, "title"),
626 Some("Test Title")
627 );
628 assert_eq!(
629 FrontMatterUtils::get_front_matter_field_value(toml_content, "author"),
630 Some("John Doe")
631 );
632
633 let json_style_yaml = "---\n\"title\": \"Test Title\"\n---\nContent";
635 assert_eq!(
636 FrontMatterUtils::get_front_matter_field_value(json_style_yaml, "title"),
637 Some("Test Title")
638 );
639
640 let json_fm = "{\n\"title\": \"Test Title\"\n}\nContent";
642 assert_eq!(
643 FrontMatterUtils::get_front_matter_field_value(json_fm, "title"),
644 Some("Test Title")
645 );
646
647 assert_eq!(
649 FrontMatterUtils::get_front_matter_field_value("Regular content", "title"),
650 None
651 );
652
653 assert_eq!(FrontMatterUtils::get_front_matter_field_value("--", "title"), None);
655 }
656
657 #[test]
658 fn test_extract_front_matter_fields() {
659 let yaml_content = "---\ntitle: Test\nauthor: Me\n---\nContent";
661 let fields = FrontMatterUtils::extract_front_matter_fields(yaml_content);
662
663 assert_eq!(fields.get("title"), Some(&"Test".to_string()));
664 assert_eq!(fields.get("author"), Some(&"Me".to_string()));
665
666 let toml_content = "+++\ntitle = \"Test\"\nauthor = \"Me\"\n+++\nContent";
668 let toml_fields = FrontMatterUtils::extract_front_matter_fields(toml_content);
669
670 assert_eq!(toml_fields.get("title"), Some(&"Test".to_string()));
671 assert_eq!(toml_fields.get("author"), Some(&"Me".to_string()));
672
673 let no_fields = FrontMatterUtils::extract_front_matter_fields("Regular content");
675 assert!(no_fields.is_empty());
676 }
677
678 #[test]
679 fn test_get_front_matter_end_line() {
680 let content = "---\ntitle: Test\n---\nContent";
681 assert_eq!(FrontMatterUtils::get_front_matter_end_line(content), 3);
682
683 let toml_content = "+++\ntitle = \"Test\"\n+++\nContent";
685 assert_eq!(FrontMatterUtils::get_front_matter_end_line(toml_content), 3);
686
687 assert_eq!(FrontMatterUtils::get_front_matter_end_line("Regular content"), 0);
689
690 assert_eq!(FrontMatterUtils::get_front_matter_end_line("--"), 0);
692 }
693
694 #[test]
695 fn test_fix_malformed_front_matter() {
696 let malformed1 = "- --\ntitle: Test\n- --\nContent";
698 let fixed1 = FrontMatterUtils::fix_malformed_front_matter(malformed1);
699 assert!(fixed1.starts_with("---\ntitle: Test\n---"));
700
701 let malformed2 = "-- -\ntitle: Test\n-- -\nContent";
703 let fixed2 = FrontMatterUtils::fix_malformed_front_matter(malformed2);
704 assert!(fixed2.starts_with("---\ntitle: Test\n---"));
705
706 let valid = "---\ntitle: Test\n---\nContent";
708 let unchanged = FrontMatterUtils::fix_malformed_front_matter(valid);
709 assert_eq!(unchanged, valid);
710
711 let no_fm = "# Regular content";
713 assert_eq!(FrontMatterUtils::fix_malformed_front_matter(no_fm), no_fm);
714
715 let short = "--";
717 assert_eq!(FrontMatterUtils::fix_malformed_front_matter(short), short);
718 }
719
720 #[test]
721 fn test_nested_yaml_fields() {
722 let content = "---
723title: Test
724author:
725 name: John Doe
726 email: john@example.com
727---
728Content";
729
730 let fields = FrontMatterUtils::extract_front_matter_fields(content);
731
732 assert!(fields.contains_key("title"));
735 }
737
738 #[test]
739 fn test_edge_cases() {
740 assert_eq!(FrontMatterUtils::detect_front_matter_type(""), FrontMatterType::None);
742 assert!(FrontMatterUtils::extract_front_matter("").is_empty());
743 assert_eq!(FrontMatterUtils::get_front_matter_end_line(""), 0);
744
745 let only_delim = "---\n---";
747 assert!(FrontMatterUtils::extract_front_matter(only_delim).is_empty());
748
749 let multiple = "---\ntitle: First\n---\n---\ntitle: Second\n---";
751 let fm_type = FrontMatterUtils::detect_front_matter_type(multiple);
752 assert_eq!(fm_type, FrontMatterType::Yaml);
753 let fields = FrontMatterUtils::extract_front_matter_fields(multiple);
754 assert_eq!(fields.get("title"), Some(&"First".to_string()));
755 }
756
757 #[test]
758 fn test_unicode_content() {
759 let content = "---\ntitle: 你好世界\nauthor: José\n---\nContent";
760
761 assert_eq!(
762 FrontMatterUtils::detect_front_matter_type(content),
763 FrontMatterType::Yaml
764 );
765 assert_eq!(
766 FrontMatterUtils::get_front_matter_field_value(content, "title"),
767 Some("你好世界")
768 );
769 assert_eq!(
770 FrontMatterUtils::get_front_matter_field_value(content, "author"),
771 Some("José")
772 );
773 }
774}