Skip to main content

rumdl_lib/rules/
front_matter_utils.rs

1use regex::Regex;
2use std::collections::HashMap;
3use std::sync::LazyLock;
4
5// Standard front matter delimiter (three dashes)
6static 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
9// TOML front matter delimiter (three plus signs)
10static 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
13// JSON front matter delimiter (curly braces)
14static 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
17// Common malformed front matter (dash space dash dash)
18static 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
21// Alternate malformed front matter (dash dash space dash)
22static 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
25// Front matter field pattern
26static FRONT_MATTER_FIELD: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^([^:]+):\s*(.*)$").unwrap());
27
28// TOML field pattern
29static TOML_FIELD_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"^([^=]+)\s*=\s*"?([^"]*)"?$"#).unwrap());
30
31/// Represents the type of front matter found in a document
32#[derive(Debug, PartialEq, Eq, Clone, Copy)]
33pub enum FrontMatterType {
34    /// YAML front matter (---)
35    Yaml,
36    /// TOML front matter (+++)
37    Toml,
38    /// JSON front matter ({})
39    Json,
40    /// Malformed front matter
41    Malformed,
42    /// No front matter
43    None,
44}
45
46/// Utility functions for detecting and handling front matter in Markdown documents
47pub struct FrontMatterUtils;
48
49impl FrontMatterUtils {
50    /// Check if a line is inside front matter content
51    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            // Check if current line is a closing delimiter before updating state
69            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; // Closing delimiter is not part of front matter content
78            }
79
80            // Standard YAML front matter handling
81            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            // TOML front matter handling
87            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            // JSON front matter handling
93            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            // Malformed front matter type 1 (- --)
99            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            // Malformed front matter type 2 (-- -)
105            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        // Return true if we're in any type of front matter
113        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    /// Check if a content contains front matter with a specific field
121    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    /// Get the value of a specific front matter field
127    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                    // Handle TOML-style fields (key = value)
144                    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                    // Handle YAML/JSON-style fields (key: value)
154                    if let Some(captures) = FRONT_MATTER_FIELD.captures(line) {
155                        let mut key = captures.get(1).unwrap().as_str().trim();
156
157                        // Strip quotes from the key if present (for JSON-style fields in any format)
158                        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                            // Strip quotes if present
165                            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    /// Extract all front matter fields as a HashMap
179    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            // Handle indentation changes for nested fields
196            match line_indent.cmp(&indent_level) {
197                std::cmp::Ordering::Greater => {
198                    // Going deeper
199                    indent_level = line_indent;
200                }
201                std::cmp::Ordering::Less => {
202                    // Going back up
203                    indent_level = line_indent;
204                    // Remove last nested level from prefix
205                    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                    // Handle TOML-style fields
217                    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                    // Handle YAML/JSON-style fields
230                    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                        // Strip quotes from the key if present (for JSON-style fields in any format)
235                        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                            // This is a nested field marker
241                            if current_prefix.is_empty() {
242                                current_prefix = stripped.to_string();
243                            } else {
244                                current_prefix = format!("{current_prefix}.{stripped}");
245                            }
246                        } else {
247                            // This is a field with a value
248                            let full_key = if current_prefix.is_empty() {
249                                key.to_string()
250                            } else {
251                                format!("{current_prefix}.{key}")
252                            };
253                            // Strip quotes if present
254                            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    /// Extract the front matter content as a vector of lines
269    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    /// Detect the type of front matter in the content
335    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            // Check if there's a closing marker
345            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            // Check if there's a closing marker
352            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            // Check if there's a closing marker
359            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            // Check if there's a closing marker
368            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    /// Get the line number where front matter ends (or 0 if no front matter)
379    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    /// Fix malformed front matter
436    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            // Handle front matter start
448            if i == 0 {
449                if STANDARD_FRONT_MATTER_START.is_match(line) {
450                    // Standard front matter - keep as is
451                    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                    // Malformed front matter - fix it
455                    in_front_matter = true;
456                    is_malformed = true;
457                    result.push("---".to_string());
458                } else {
459                    // Regular line
460                    result.push(line.to_string());
461                }
462                continue;
463            }
464
465            // Handle front matter end
466            if in_front_matter {
467                if STANDARD_FRONT_MATTER_END.is_match(line) {
468                    // Standard front matter end - keep as is
469                    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                    // Malformed front matter end - fix it
475                    in_front_matter = false;
476                    result.push("---".to_string());
477                } else {
478                    // Content inside front matter
479                    result.push(line.to_string());
480                }
481                continue;
482            }
483
484            // Regular line
485            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        // YAML front matter
509        let yaml_content = "---\ntitle: Test\n---\nContent";
510        assert_eq!(
511            FrontMatterUtils::detect_front_matter_type(yaml_content),
512            FrontMatterType::Yaml
513        );
514
515        // TOML front matter
516        let toml_content = "+++\ntitle = \"Test\"\n+++\nContent";
517        assert_eq!(
518            FrontMatterUtils::detect_front_matter_type(toml_content),
519            FrontMatterType::Toml
520        );
521
522        // JSON front matter
523        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        // Malformed front matter
530        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        // No front matter
543        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        // Incomplete front matter (no closing marker)
550        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        // The implementation considers opening delimiter and content lines as inside front matter
561        // but the closing delimiter triggers the exit from front matter state
562        assert!(FrontMatterUtils::is_in_front_matter(content, 0)); // Opening ---
563        assert!(FrontMatterUtils::is_in_front_matter(content, 1)); // title line
564        assert!(FrontMatterUtils::is_in_front_matter(content, 2)); // author line
565        assert!(!FrontMatterUtils::is_in_front_matter(content, 3)); // Closing --- (not part of front matter)
566        assert!(!FrontMatterUtils::is_in_front_matter(content, 4)); // Content
567
568        // Out of bounds
569        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        // No front matter
582        let no_fm = FrontMatterUtils::extract_front_matter("Regular content");
583        assert!(no_fm.is_empty());
584
585        // Too short content
586        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        // No front matter
599        assert!(!FrontMatterUtils::has_front_matter_field("Regular content", "title"));
600
601        // Too short content
602        assert!(!FrontMatterUtils::has_front_matter_field("--", "title"));
603    }
604
605    #[test]
606    fn test_get_front_matter_field_value() {
607        // YAML front matter
608        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        // TOML front matter
623        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        // JSON-style fields in YAML front matter - keys should not include quotes
634        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        // Actual JSON front matter
641        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        // No front matter
648        assert_eq!(
649            FrontMatterUtils::get_front_matter_field_value("Regular content", "title"),
650            None
651        );
652
653        // Too short content
654        assert_eq!(FrontMatterUtils::get_front_matter_field_value("--", "title"), None);
655    }
656
657    #[test]
658    fn test_extract_front_matter_fields() {
659        // Simple YAML front matter
660        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        // TOML front matter
667        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        // No front matter
674        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        // TOML
684        let toml_content = "+++\ntitle = \"Test\"\n+++\nContent";
685        assert_eq!(FrontMatterUtils::get_front_matter_end_line(toml_content), 3);
686
687        // No front matter
688        assert_eq!(FrontMatterUtils::get_front_matter_end_line("Regular content"), 0);
689
690        // Too short
691        assert_eq!(FrontMatterUtils::get_front_matter_end_line("--"), 0);
692    }
693
694    #[test]
695    fn test_fix_malformed_front_matter() {
696        // Fix malformed type 1
697        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        // Fix malformed type 2
702        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        // Don't change valid front matter
707        let valid = "---\ntitle: Test\n---\nContent";
708        let unchanged = FrontMatterUtils::fix_malformed_front_matter(valid);
709        assert_eq!(unchanged, valid);
710
711        // No front matter
712        let no_fm = "# Regular content";
713        assert_eq!(FrontMatterUtils::fix_malformed_front_matter(no_fm), no_fm);
714
715        // Too short
716        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        // Note: The current implementation doesn't fully handle nested YAML
733        // This test documents the current behavior
734        assert!(fields.contains_key("title"));
735        // Nested fields handling would need enhancement
736    }
737
738    #[test]
739    fn test_edge_cases() {
740        // Empty content
741        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        // Only delimiters
746        let only_delim = "---\n---";
747        assert!(FrontMatterUtils::extract_front_matter(only_delim).is_empty());
748
749        // Multiple front matter sections (only first should be detected)
750        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}