rumdl_lib/rules/
front_matter_utils.rs

1use lazy_static::lazy_static;
2use regex::Regex;
3use std::collections::HashMap;
4
5lazy_static! {
6    // Standard front matter delimiter (three dashes)
7    static ref STANDARD_FRONT_MATTER_START: Regex = Regex::new(r"^---\s*$").unwrap();
8    static ref STANDARD_FRONT_MATTER_END: Regex = Regex::new(r"^---\s*$").unwrap();
9
10    // TOML front matter delimiter (three plus signs)
11    static ref TOML_FRONT_MATTER_START: Regex = Regex::new(r"^\+\+\+\s*$").unwrap();
12    static ref TOML_FRONT_MATTER_END: Regex = Regex::new(r"^\+\+\+\s*$").unwrap();
13
14    // JSON front matter delimiter (curly braces)
15    static ref JSON_FRONT_MATTER_START: Regex = Regex::new(r"^\{\s*$").unwrap();
16    static ref JSON_FRONT_MATTER_END: Regex = Regex::new(r"^\}\s*$").unwrap();
17
18    // Common malformed front matter (dash space dash dash)
19    static ref MALFORMED_FRONT_MATTER_START1: Regex = Regex::new(r"^- --\s*$").unwrap();
20    static ref MALFORMED_FRONT_MATTER_END1: Regex = Regex::new(r"^- --\s*$").unwrap();
21
22    // Alternate malformed front matter (dash dash space dash)
23    static ref MALFORMED_FRONT_MATTER_START2: Regex = Regex::new(r"^-- -\s*$").unwrap();
24    static ref MALFORMED_FRONT_MATTER_END2: Regex = Regex::new(r"^-- -\s*$").unwrap();
25
26    // Front matter field pattern
27    static ref FRONT_MATTER_FIELD: Regex = Regex::new(r"^([^:]+):\s*(.*)$").unwrap();
28
29    // TOML field pattern
30    static ref TOML_FIELD_PATTERN: Regex = Regex::new(r#"^([^=]+)\s*=\s*"?([^"]*)"?$"#).unwrap();
31}
32
33/// Represents the type of front matter found in a document
34#[derive(Debug, PartialEq, Eq, Clone, Copy)]
35pub enum FrontMatterType {
36    /// YAML front matter (---)
37    Yaml,
38    /// TOML front matter (+++)
39    Toml,
40    /// JSON front matter ({})
41    Json,
42    /// Malformed front matter
43    Malformed,
44    /// No front matter
45    None,
46}
47
48/// Utility functions for detecting and handling front matter in Markdown documents
49pub struct FrontMatterUtils;
50
51impl FrontMatterUtils {
52    /// Check if a line is inside front matter content
53    pub fn is_in_front_matter(content: &str, line_num: usize) -> bool {
54        let lines: Vec<&str> = content.lines().collect();
55        if line_num >= lines.len() {
56            return false;
57        }
58
59        let mut in_standard_front_matter = false;
60        let mut in_toml_front_matter = false;
61        let mut in_json_front_matter = false;
62        let mut in_malformed_front_matter1 = false;
63        let mut in_malformed_front_matter2 = false;
64
65        for (i, line) in lines.iter().enumerate() {
66            if i > line_num {
67                break;
68            }
69
70            // Check if current line is a closing delimiter before updating state
71            if i == line_num
72                && i > 0
73                && ((in_standard_front_matter && STANDARD_FRONT_MATTER_END.is_match(line))
74                    || (in_toml_front_matter && TOML_FRONT_MATTER_END.is_match(line))
75                    || (in_json_front_matter && JSON_FRONT_MATTER_END.is_match(line))
76                    || (in_malformed_front_matter1 && MALFORMED_FRONT_MATTER_END1.is_match(line))
77                    || (in_malformed_front_matter2 && MALFORMED_FRONT_MATTER_END2.is_match(line)))
78            {
79                return false; // Closing delimiter is not part of front matter content
80            }
81
82            // Standard YAML front matter handling
83            if i == 0 && STANDARD_FRONT_MATTER_START.is_match(line) {
84                in_standard_front_matter = true;
85            } else if STANDARD_FRONT_MATTER_END.is_match(line) && in_standard_front_matter && i > 0 {
86                in_standard_front_matter = false;
87            }
88            // TOML front matter handling
89            else if i == 0 && TOML_FRONT_MATTER_START.is_match(line) {
90                in_toml_front_matter = true;
91            } else if TOML_FRONT_MATTER_END.is_match(line) && in_toml_front_matter && i > 0 {
92                in_toml_front_matter = false;
93            }
94            // JSON front matter handling
95            else if i == 0 && JSON_FRONT_MATTER_START.is_match(line) {
96                in_json_front_matter = true;
97            } else if JSON_FRONT_MATTER_END.is_match(line) && in_json_front_matter && i > 0 {
98                in_json_front_matter = false;
99            }
100            // Malformed front matter type 1 (- --)
101            else if i == 0 && MALFORMED_FRONT_MATTER_START1.is_match(line) {
102                in_malformed_front_matter1 = true;
103            } else if MALFORMED_FRONT_MATTER_END1.is_match(line) && in_malformed_front_matter1 && i > 0 {
104                in_malformed_front_matter1 = false;
105            }
106            // Malformed front matter type 2 (-- -)
107            else if i == 0 && MALFORMED_FRONT_MATTER_START2.is_match(line) {
108                in_malformed_front_matter2 = true;
109            } else if MALFORMED_FRONT_MATTER_END2.is_match(line) && in_malformed_front_matter2 && i > 0 {
110                in_malformed_front_matter2 = false;
111            }
112        }
113
114        // Return true if we're in any type of front matter
115        in_standard_front_matter
116            || in_toml_front_matter
117            || in_json_front_matter
118            || in_malformed_front_matter1
119            || in_malformed_front_matter2
120    }
121
122    /// Check if a content contains front matter with a specific field
123    pub fn has_front_matter_field(content: &str, field_prefix: &str) -> bool {
124        let lines: Vec<&str> = content.lines().collect();
125        if lines.len() < 3 {
126            return false;
127        }
128
129        let front_matter_type = Self::detect_front_matter_type(content);
130        if front_matter_type == FrontMatterType::None {
131            return false;
132        }
133
134        let front_matter = Self::extract_front_matter(content);
135        for line in front_matter {
136            if line.trim().starts_with(field_prefix) {
137                return true;
138            }
139        }
140
141        false
142    }
143
144    /// Get the value of a specific front matter field
145    pub fn get_front_matter_field_value<'a>(content: &'a str, field_name: &str) -> Option<&'a str> {
146        let lines: Vec<&'a str> = content.lines().collect();
147        if lines.len() < 3 {
148            return None;
149        }
150
151        let front_matter_type = Self::detect_front_matter_type(content);
152        if front_matter_type == FrontMatterType::None {
153            return None;
154        }
155
156        let front_matter = Self::extract_front_matter(content);
157        for line in front_matter {
158            let line = line.trim();
159            match front_matter_type {
160                FrontMatterType::Toml => {
161                    // Handle TOML-style fields (key = value)
162                    if let Some(captures) = TOML_FIELD_PATTERN.captures(line) {
163                        let key = captures.get(1).unwrap().as_str().trim();
164                        if key == field_name {
165                            let value = captures.get(2).unwrap().as_str();
166                            return Some(value);
167                        }
168                    }
169                }
170                _ => {
171                    // Handle YAML/JSON-style fields (key: value)
172                    if let Some(captures) = FRONT_MATTER_FIELD.captures(line) {
173                        let mut key = captures.get(1).unwrap().as_str().trim();
174
175                        // Strip quotes from the key if present (for JSON-style fields in any format)
176                        if key.starts_with('"') && key.ends_with('"') && key.len() >= 2 {
177                            key = &key[1..key.len() - 1];
178                        }
179
180                        if key == field_name {
181                            let value = captures.get(2).unwrap().as_str().trim();
182                            // Strip quotes if present
183                            if value.starts_with('"') && value.ends_with('"') && value.len() >= 2 {
184                                return Some(&value[1..value.len() - 1]);
185                            }
186                            return Some(value);
187                        }
188                    }
189                }
190            }
191        }
192
193        None
194    }
195
196    /// Extract all front matter fields as a HashMap
197    pub fn extract_front_matter_fields(content: &str) -> HashMap<String, String> {
198        let mut fields = HashMap::new();
199
200        let front_matter_type = Self::detect_front_matter_type(content);
201        if front_matter_type == FrontMatterType::None {
202            return fields;
203        }
204
205        let front_matter = Self::extract_front_matter(content);
206        let mut current_prefix = String::new();
207        let mut indent_level = 0;
208
209        for line in front_matter {
210            let line_indent = line.chars().take_while(|c| c.is_whitespace()).count();
211            let line = line.trim();
212
213            // Handle indentation changes for nested fields
214            match line_indent.cmp(&indent_level) {
215                std::cmp::Ordering::Greater => {
216                    // Going deeper
217                    indent_level = line_indent;
218                }
219                std::cmp::Ordering::Less => {
220                    // Going back up
221                    indent_level = line_indent;
222                    // Remove last nested level from prefix
223                    if let Some(last_dot) = current_prefix.rfind('.') {
224                        current_prefix.truncate(last_dot);
225                    } else {
226                        current_prefix.clear();
227                    }
228                }
229                std::cmp::Ordering::Equal => {}
230            }
231
232            match front_matter_type {
233                FrontMatterType::Toml => {
234                    // Handle TOML-style fields
235                    if let Some(captures) = TOML_FIELD_PATTERN.captures(line) {
236                        let key = captures.get(1).unwrap().as_str().trim();
237                        let value = captures.get(2).unwrap().as_str();
238                        let full_key = if current_prefix.is_empty() {
239                            key.to_string()
240                        } else {
241                            format!("{current_prefix}.{key}")
242                        };
243                        fields.insert(full_key, value.to_string());
244                    }
245                }
246                _ => {
247                    // Handle YAML/JSON-style fields
248                    if let Some(captures) = FRONT_MATTER_FIELD.captures(line) {
249                        let mut key = captures.get(1).unwrap().as_str().trim();
250                        let value = captures.get(2).unwrap().as_str().trim();
251
252                        // Strip quotes from the key if present (for JSON-style fields in any format)
253                        if key.starts_with('"') && key.ends_with('"') && key.len() >= 2 {
254                            key = &key[1..key.len() - 1];
255                        }
256
257                        if let Some(stripped) = key.strip_suffix(':') {
258                            // This is a nested field marker
259                            if current_prefix.is_empty() {
260                                current_prefix = stripped.to_string();
261                            } else {
262                                current_prefix = format!("{current_prefix}.{stripped}");
263                            }
264                        } else {
265                            // This is a field with a value
266                            let full_key = if current_prefix.is_empty() {
267                                key.to_string()
268                            } else {
269                                format!("{current_prefix}.{key}")
270                            };
271                            // Strip quotes if present
272                            let value = value
273                                .strip_prefix('"')
274                                .and_then(|v| v.strip_suffix('"'))
275                                .unwrap_or(value);
276                            fields.insert(full_key, value.to_string());
277                        }
278                    }
279                }
280            }
281        }
282
283        fields
284    }
285
286    /// Extract the front matter content as a vector of lines
287    pub fn extract_front_matter<'a>(content: &'a str) -> Vec<&'a str> {
288        let lines: Vec<&'a str> = content.lines().collect();
289        if lines.len() < 3 {
290            return Vec::new();
291        }
292
293        let front_matter_type = Self::detect_front_matter_type(content);
294        if front_matter_type == FrontMatterType::None {
295            return Vec::new();
296        }
297
298        let mut front_matter = Vec::new();
299        let mut in_front_matter = false;
300
301        for (i, line) in lines.iter().enumerate() {
302            match front_matter_type {
303                FrontMatterType::Yaml => {
304                    if i == 0 && STANDARD_FRONT_MATTER_START.is_match(line) {
305                        in_front_matter = true;
306                        continue;
307                    } else if STANDARD_FRONT_MATTER_END.is_match(line) && in_front_matter && i > 0 {
308                        break;
309                    }
310                }
311                FrontMatterType::Toml => {
312                    if i == 0 && TOML_FRONT_MATTER_START.is_match(line) {
313                        in_front_matter = true;
314                        continue;
315                    } else if TOML_FRONT_MATTER_END.is_match(line) && in_front_matter && i > 0 {
316                        break;
317                    }
318                }
319                FrontMatterType::Json => {
320                    if i == 0 && JSON_FRONT_MATTER_START.is_match(line) {
321                        in_front_matter = true;
322                        continue;
323                    } else if JSON_FRONT_MATTER_END.is_match(line) && in_front_matter && i > 0 {
324                        break;
325                    }
326                }
327                FrontMatterType::Malformed => {
328                    if i == 0
329                        && (MALFORMED_FRONT_MATTER_START1.is_match(line)
330                            || MALFORMED_FRONT_MATTER_START2.is_match(line))
331                    {
332                        in_front_matter = true;
333                        continue;
334                    } else if (MALFORMED_FRONT_MATTER_END1.is_match(line) || MALFORMED_FRONT_MATTER_END2.is_match(line))
335                        && in_front_matter
336                        && i > 0
337                    {
338                        break;
339                    }
340                }
341                FrontMatterType::None => break,
342            }
343
344            if in_front_matter {
345                front_matter.push(*line);
346            }
347        }
348
349        front_matter
350    }
351
352    /// Detect the type of front matter in the content
353    pub fn detect_front_matter_type(content: &str) -> FrontMatterType {
354        let lines: Vec<&str> = content.lines().collect();
355        if lines.is_empty() {
356            return FrontMatterType::None;
357        }
358
359        let first_line = lines[0];
360
361        if STANDARD_FRONT_MATTER_START.is_match(first_line) {
362            // Check if there's a closing marker
363            for line in lines.iter().skip(1) {
364                if STANDARD_FRONT_MATTER_END.is_match(line) {
365                    return FrontMatterType::Yaml;
366                }
367            }
368        } else if TOML_FRONT_MATTER_START.is_match(first_line) {
369            // Check if there's a closing marker
370            for line in lines.iter().skip(1) {
371                if TOML_FRONT_MATTER_END.is_match(line) {
372                    return FrontMatterType::Toml;
373                }
374            }
375        } else if JSON_FRONT_MATTER_START.is_match(first_line) {
376            // Check if there's a closing marker
377            for line in lines.iter().skip(1) {
378                if JSON_FRONT_MATTER_END.is_match(line) {
379                    return FrontMatterType::Json;
380                }
381            }
382        } else if MALFORMED_FRONT_MATTER_START1.is_match(first_line)
383            || MALFORMED_FRONT_MATTER_START2.is_match(first_line)
384        {
385            // Check if there's a closing marker
386            for line in lines.iter().skip(1) {
387                if MALFORMED_FRONT_MATTER_END1.is_match(line) || MALFORMED_FRONT_MATTER_END2.is_match(line) {
388                    return FrontMatterType::Malformed;
389                }
390            }
391        }
392
393        FrontMatterType::None
394    }
395
396    /// Get the line number where front matter ends (or 0 if no front matter)
397    pub fn get_front_matter_end_line(content: &str) -> usize {
398        let lines: Vec<&str> = content.lines().collect();
399        if lines.len() < 3 {
400            return 0;
401        }
402
403        let front_matter_type = Self::detect_front_matter_type(content);
404        if front_matter_type == FrontMatterType::None {
405            return 0;
406        }
407
408        let mut in_front_matter = false;
409
410        for (i, line) in lines.iter().enumerate() {
411            match front_matter_type {
412                FrontMatterType::Yaml => {
413                    if i == 0 && STANDARD_FRONT_MATTER_START.is_match(line) {
414                        in_front_matter = true;
415                    } else if STANDARD_FRONT_MATTER_END.is_match(line) && in_front_matter && i > 0 {
416                        return i + 1;
417                    }
418                }
419                FrontMatterType::Toml => {
420                    if i == 0 && TOML_FRONT_MATTER_START.is_match(line) {
421                        in_front_matter = true;
422                    } else if TOML_FRONT_MATTER_END.is_match(line) && in_front_matter && i > 0 {
423                        return i + 1;
424                    }
425                }
426                FrontMatterType::Json => {
427                    if i == 0 && JSON_FRONT_MATTER_START.is_match(line) {
428                        in_front_matter = true;
429                    } else if JSON_FRONT_MATTER_END.is_match(line) && in_front_matter && i > 0 {
430                        return i + 1;
431                    }
432                }
433                FrontMatterType::Malformed => {
434                    if i == 0
435                        && (MALFORMED_FRONT_MATTER_START1.is_match(line)
436                            || MALFORMED_FRONT_MATTER_START2.is_match(line))
437                    {
438                        in_front_matter = true;
439                    } else if (MALFORMED_FRONT_MATTER_END1.is_match(line) || MALFORMED_FRONT_MATTER_END2.is_match(line))
440                        && in_front_matter
441                        && i > 0
442                    {
443                        return i + 1;
444                    }
445                }
446                FrontMatterType::None => return 0,
447            }
448        }
449
450        0
451    }
452
453    /// Fix malformed front matter
454    pub fn fix_malformed_front_matter(content: &str) -> String {
455        let lines: Vec<&str> = content.lines().collect();
456        if lines.len() < 3 {
457            return content.to_string();
458        }
459
460        let mut result = Vec::new();
461        let mut in_front_matter = false;
462        let mut is_malformed = false;
463
464        for (i, line) in lines.iter().enumerate() {
465            // Handle front matter start
466            if i == 0 {
467                if STANDARD_FRONT_MATTER_START.is_match(line) {
468                    // Standard front matter - keep as is
469                    in_front_matter = true;
470                    result.push(line.to_string());
471                } else if MALFORMED_FRONT_MATTER_START1.is_match(line) || MALFORMED_FRONT_MATTER_START2.is_match(line) {
472                    // Malformed front matter - fix it
473                    in_front_matter = true;
474                    is_malformed = true;
475                    result.push("---".to_string());
476                } else {
477                    // Regular line
478                    result.push(line.to_string());
479                }
480                continue;
481            }
482
483            // Handle front matter end
484            if in_front_matter {
485                if STANDARD_FRONT_MATTER_END.is_match(line) {
486                    // Standard front matter end - keep as is
487                    in_front_matter = false;
488                    result.push(line.to_string());
489                } else if (MALFORMED_FRONT_MATTER_END1.is_match(line) || MALFORMED_FRONT_MATTER_END2.is_match(line))
490                    && is_malformed
491                {
492                    // Malformed front matter end - fix it
493                    in_front_matter = false;
494                    result.push("---".to_string());
495                } else {
496                    // Content inside front matter
497                    result.push(line.to_string());
498                }
499                continue;
500            }
501
502            // Regular line
503            result.push(line.to_string());
504        }
505
506        result.join("\n")
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513
514    #[test]
515    fn test_front_matter_type_enum() {
516        assert_eq!(FrontMatterType::Yaml, FrontMatterType::Yaml);
517        assert_eq!(FrontMatterType::Toml, FrontMatterType::Toml);
518        assert_eq!(FrontMatterType::Json, FrontMatterType::Json);
519        assert_eq!(FrontMatterType::Malformed, FrontMatterType::Malformed);
520        assert_eq!(FrontMatterType::None, FrontMatterType::None);
521        assert_ne!(FrontMatterType::Yaml, FrontMatterType::Toml);
522    }
523
524    #[test]
525    fn test_detect_front_matter_type() {
526        // YAML front matter
527        let yaml_content = "---\ntitle: Test\n---\nContent";
528        assert_eq!(
529            FrontMatterUtils::detect_front_matter_type(yaml_content),
530            FrontMatterType::Yaml
531        );
532
533        // TOML front matter
534        let toml_content = "+++\ntitle = \"Test\"\n+++\nContent";
535        assert_eq!(
536            FrontMatterUtils::detect_front_matter_type(toml_content),
537            FrontMatterType::Toml
538        );
539
540        // JSON front matter
541        let json_content = "{\n\"title\": \"Test\"\n}\nContent";
542        assert_eq!(
543            FrontMatterUtils::detect_front_matter_type(json_content),
544            FrontMatterType::Json
545        );
546
547        // Malformed front matter
548        let malformed1 = "- --\ntitle: Test\n- --\nContent";
549        assert_eq!(
550            FrontMatterUtils::detect_front_matter_type(malformed1),
551            FrontMatterType::Malformed
552        );
553
554        let malformed2 = "-- -\ntitle: Test\n-- -\nContent";
555        assert_eq!(
556            FrontMatterUtils::detect_front_matter_type(malformed2),
557            FrontMatterType::Malformed
558        );
559
560        // No front matter
561        assert_eq!(
562            FrontMatterUtils::detect_front_matter_type("# Regular content"),
563            FrontMatterType::None
564        );
565        assert_eq!(FrontMatterUtils::detect_front_matter_type(""), FrontMatterType::None);
566
567        // Incomplete front matter (no closing marker)
568        assert_eq!(
569            FrontMatterUtils::detect_front_matter_type("---\ntitle: Test"),
570            FrontMatterType::None
571        );
572    }
573
574    #[test]
575    fn test_is_in_front_matter() {
576        let content = "---\ntitle: Test\nauthor: Me\n---\nContent here";
577
578        // The implementation considers opening delimiter and content lines as inside front matter
579        // but the closing delimiter triggers the exit from front matter state
580        assert!(FrontMatterUtils::is_in_front_matter(content, 0)); // Opening ---
581        assert!(FrontMatterUtils::is_in_front_matter(content, 1)); // title line
582        assert!(FrontMatterUtils::is_in_front_matter(content, 2)); // author line
583        assert!(!FrontMatterUtils::is_in_front_matter(content, 3)); // Closing --- (not part of front matter)
584        assert!(!FrontMatterUtils::is_in_front_matter(content, 4)); // Content
585
586        // Out of bounds
587        assert!(!FrontMatterUtils::is_in_front_matter(content, 100));
588    }
589
590    #[test]
591    fn test_extract_front_matter() {
592        let content = "---\ntitle: Test\nauthor: Me\n---\nContent";
593        let front_matter = FrontMatterUtils::extract_front_matter(content);
594
595        assert_eq!(front_matter.len(), 2);
596        assert_eq!(front_matter[0], "title: Test");
597        assert_eq!(front_matter[1], "author: Me");
598
599        // No front matter
600        let no_fm = FrontMatterUtils::extract_front_matter("Regular content");
601        assert!(no_fm.is_empty());
602
603        // Too short content
604        let short = FrontMatterUtils::extract_front_matter("---\n---");
605        assert!(short.is_empty());
606    }
607
608    #[test]
609    fn test_has_front_matter_field() {
610        let content = "---\ntitle: Test\nauthor: Me\n---\nContent";
611
612        assert!(FrontMatterUtils::has_front_matter_field(content, "title"));
613        assert!(FrontMatterUtils::has_front_matter_field(content, "author"));
614        assert!(!FrontMatterUtils::has_front_matter_field(content, "date"));
615
616        // No front matter
617        assert!(!FrontMatterUtils::has_front_matter_field("Regular content", "title"));
618
619        // Too short content
620        assert!(!FrontMatterUtils::has_front_matter_field("--", "title"));
621    }
622
623    #[test]
624    fn test_get_front_matter_field_value() {
625        // YAML front matter
626        let yaml_content = "---\ntitle: Test Title\nauthor: \"John Doe\"\n---\nContent";
627        assert_eq!(
628            FrontMatterUtils::get_front_matter_field_value(yaml_content, "title"),
629            Some("Test Title")
630        );
631        assert_eq!(
632            FrontMatterUtils::get_front_matter_field_value(yaml_content, "author"),
633            Some("John Doe")
634        );
635        assert_eq!(
636            FrontMatterUtils::get_front_matter_field_value(yaml_content, "nonexistent"),
637            None
638        );
639
640        // TOML front matter
641        let toml_content = "+++\ntitle = \"Test Title\"\nauthor = \"John Doe\"\n+++\nContent";
642        assert_eq!(
643            FrontMatterUtils::get_front_matter_field_value(toml_content, "title"),
644            Some("Test Title")
645        );
646        assert_eq!(
647            FrontMatterUtils::get_front_matter_field_value(toml_content, "author"),
648            Some("John Doe")
649        );
650
651        // JSON-style fields in YAML front matter - keys should not include quotes
652        let json_style_yaml = "---\n\"title\": \"Test Title\"\n---\nContent";
653        assert_eq!(
654            FrontMatterUtils::get_front_matter_field_value(json_style_yaml, "title"),
655            Some("Test Title")
656        );
657
658        // Actual JSON front matter
659        let json_fm = "{\n\"title\": \"Test Title\"\n}\nContent";
660        assert_eq!(
661            FrontMatterUtils::get_front_matter_field_value(json_fm, "title"),
662            Some("Test Title")
663        );
664
665        // No front matter
666        assert_eq!(
667            FrontMatterUtils::get_front_matter_field_value("Regular content", "title"),
668            None
669        );
670
671        // Too short content
672        assert_eq!(FrontMatterUtils::get_front_matter_field_value("--", "title"), None);
673    }
674
675    #[test]
676    fn test_extract_front_matter_fields() {
677        // Simple YAML front matter
678        let yaml_content = "---\ntitle: Test\nauthor: Me\n---\nContent";
679        let fields = FrontMatterUtils::extract_front_matter_fields(yaml_content);
680
681        assert_eq!(fields.get("title"), Some(&"Test".to_string()));
682        assert_eq!(fields.get("author"), Some(&"Me".to_string()));
683
684        // TOML front matter
685        let toml_content = "+++\ntitle = \"Test\"\nauthor = \"Me\"\n+++\nContent";
686        let toml_fields = FrontMatterUtils::extract_front_matter_fields(toml_content);
687
688        assert_eq!(toml_fields.get("title"), Some(&"Test".to_string()));
689        assert_eq!(toml_fields.get("author"), Some(&"Me".to_string()));
690
691        // No front matter
692        let no_fields = FrontMatterUtils::extract_front_matter_fields("Regular content");
693        assert!(no_fields.is_empty());
694    }
695
696    #[test]
697    fn test_get_front_matter_end_line() {
698        let content = "---\ntitle: Test\n---\nContent";
699        assert_eq!(FrontMatterUtils::get_front_matter_end_line(content), 3);
700
701        // TOML
702        let toml_content = "+++\ntitle = \"Test\"\n+++\nContent";
703        assert_eq!(FrontMatterUtils::get_front_matter_end_line(toml_content), 3);
704
705        // No front matter
706        assert_eq!(FrontMatterUtils::get_front_matter_end_line("Regular content"), 0);
707
708        // Too short
709        assert_eq!(FrontMatterUtils::get_front_matter_end_line("--"), 0);
710    }
711
712    #[test]
713    fn test_fix_malformed_front_matter() {
714        // Fix malformed type 1
715        let malformed1 = "- --\ntitle: Test\n- --\nContent";
716        let fixed1 = FrontMatterUtils::fix_malformed_front_matter(malformed1);
717        assert!(fixed1.starts_with("---\ntitle: Test\n---"));
718
719        // Fix malformed type 2
720        let malformed2 = "-- -\ntitle: Test\n-- -\nContent";
721        let fixed2 = FrontMatterUtils::fix_malformed_front_matter(malformed2);
722        assert!(fixed2.starts_with("---\ntitle: Test\n---"));
723
724        // Don't change valid front matter
725        let valid = "---\ntitle: Test\n---\nContent";
726        let unchanged = FrontMatterUtils::fix_malformed_front_matter(valid);
727        assert_eq!(unchanged, valid);
728
729        // No front matter
730        let no_fm = "# Regular content";
731        assert_eq!(FrontMatterUtils::fix_malformed_front_matter(no_fm), no_fm);
732
733        // Too short
734        let short = "--";
735        assert_eq!(FrontMatterUtils::fix_malformed_front_matter(short), short);
736    }
737
738    #[test]
739    fn test_nested_yaml_fields() {
740        let content = "---
741title: Test
742author:
743  name: John Doe
744  email: john@example.com
745---
746Content";
747
748        let fields = FrontMatterUtils::extract_front_matter_fields(content);
749
750        // Note: The current implementation doesn't fully handle nested YAML
751        // This test documents the current behavior
752        assert!(fields.contains_key("title"));
753        // Nested fields handling would need enhancement
754    }
755
756    #[test]
757    fn test_edge_cases() {
758        // Empty content
759        assert_eq!(FrontMatterUtils::detect_front_matter_type(""), FrontMatterType::None);
760        assert!(FrontMatterUtils::extract_front_matter("").is_empty());
761        assert_eq!(FrontMatterUtils::get_front_matter_end_line(""), 0);
762
763        // Only delimiters
764        let only_delim = "---\n---";
765        assert!(FrontMatterUtils::extract_front_matter(only_delim).is_empty());
766
767        // Multiple front matter sections (only first should be detected)
768        let multiple = "---\ntitle: First\n---\n---\ntitle: Second\n---";
769        let fm_type = FrontMatterUtils::detect_front_matter_type(multiple);
770        assert_eq!(fm_type, FrontMatterType::Yaml);
771        let fields = FrontMatterUtils::extract_front_matter_fields(multiple);
772        assert_eq!(fields.get("title"), Some(&"First".to_string()));
773    }
774
775    #[test]
776    fn test_unicode_content() {
777        let content = "---\ntitle: 你好世界\nauthor: José\n---\nContent";
778
779        assert_eq!(
780            FrontMatterUtils::detect_front_matter_type(content),
781            FrontMatterType::Yaml
782        );
783        assert_eq!(
784            FrontMatterUtils::get_front_matter_field_value(content, "title"),
785            Some("你好世界")
786        );
787        assert_eq!(
788            FrontMatterUtils::get_front_matter_field_value(content, "author"),
789            Some("José")
790        );
791    }
792}