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 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    /// Get the value of a specific front matter field
143    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                    // Handle TOML-style fields (key = value)
160                    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                    // Handle YAML/JSON-style fields (key: value)
170                    if let Some(captures) = FRONT_MATTER_FIELD.captures(line) {
171                        let mut key = captures.get(1).unwrap().as_str().trim();
172
173                        // Strip quotes from the key if present (for JSON-style fields in any format)
174                        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                            // Strip quotes if present
181                            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    /// Extract all front matter fields as a HashMap
195    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            // Handle indentation changes for nested fields
212            match line_indent.cmp(&indent_level) {
213                std::cmp::Ordering::Greater => {
214                    // Going deeper
215                    indent_level = line_indent;
216                }
217                std::cmp::Ordering::Less => {
218                    // Going back up
219                    indent_level = line_indent;
220                    // Remove last nested level from prefix
221                    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                    // Handle TOML-style fields
233                    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                    // Handle YAML/JSON-style fields
246                    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                        // Strip quotes from the key if present (for JSON-style fields in any format)
251                        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                            // This is a nested field marker
257                            if current_prefix.is_empty() {
258                                current_prefix = stripped.to_string();
259                            } else {
260                                current_prefix = format!("{current_prefix}.{stripped}");
261                            }
262                        } else {
263                            // This is a field with a value
264                            let full_key = if current_prefix.is_empty() {
265                                key.to_string()
266                            } else {
267                                format!("{current_prefix}.{key}")
268                            };
269                            // Strip quotes if present
270                            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    /// Extract the front matter content as a vector of lines
285    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    /// Detect the type of front matter in the content
351    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            // Check if there's a closing marker
361            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            // Check if there's a closing marker
368            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            // Check if there's a closing marker
375            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            // Check if there's a closing marker
384            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    /// Get the line number where front matter ends (or 0 if no front matter)
395    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    /// Fix malformed front matter
452    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            // Handle front matter start
464            if i == 0 {
465                if STANDARD_FRONT_MATTER_START.is_match(line) {
466                    // Standard front matter - keep as is
467                    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                    // Malformed front matter - fix it
471                    in_front_matter = true;
472                    is_malformed = true;
473                    result.push("---".to_string());
474                } else {
475                    // Regular line
476                    result.push(line.to_string());
477                }
478                continue;
479            }
480
481            // Handle front matter end
482            if in_front_matter {
483                if STANDARD_FRONT_MATTER_END.is_match(line) {
484                    // Standard front matter end - keep as is
485                    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                    // Malformed front matter end - fix it
491                    in_front_matter = false;
492                    result.push("---".to_string());
493                } else {
494                    // Content inside front matter
495                    result.push(line.to_string());
496                }
497                continue;
498            }
499
500            // Regular line
501            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        // YAML front matter
525        let yaml_content = "---\ntitle: Test\n---\nContent";
526        assert_eq!(
527            FrontMatterUtils::detect_front_matter_type(yaml_content),
528            FrontMatterType::Yaml
529        );
530
531        // TOML front matter
532        let toml_content = "+++\ntitle = \"Test\"\n+++\nContent";
533        assert_eq!(
534            FrontMatterUtils::detect_front_matter_type(toml_content),
535            FrontMatterType::Toml
536        );
537
538        // JSON front matter
539        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        // Malformed front matter
546        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        // No front matter
559        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        // Incomplete front matter (no closing marker)
566        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        // The implementation considers opening delimiter and content lines as inside front matter
577        // but the closing delimiter triggers the exit from front matter state
578        assert!(FrontMatterUtils::is_in_front_matter(content, 0)); // Opening ---
579        assert!(FrontMatterUtils::is_in_front_matter(content, 1)); // title line
580        assert!(FrontMatterUtils::is_in_front_matter(content, 2)); // author line
581        assert!(!FrontMatterUtils::is_in_front_matter(content, 3)); // Closing --- (not part of front matter)
582        assert!(!FrontMatterUtils::is_in_front_matter(content, 4)); // Content
583
584        // Out of bounds
585        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        // No front matter
598        let no_fm = FrontMatterUtils::extract_front_matter("Regular content");
599        assert!(no_fm.is_empty());
600
601        // Too short content
602        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        // No front matter
615        assert!(!FrontMatterUtils::has_front_matter_field("Regular content", "title"));
616
617        // Too short content
618        assert!(!FrontMatterUtils::has_front_matter_field("--", "title"));
619    }
620
621    #[test]
622    fn test_get_front_matter_field_value() {
623        // YAML front matter
624        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        // TOML front matter
639        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        // JSON-style fields in YAML front matter - keys should not include quotes
650        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        // Actual JSON front matter
657        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        // No front matter
664        assert_eq!(
665            FrontMatterUtils::get_front_matter_field_value("Regular content", "title"),
666            None
667        );
668
669        // Too short content
670        assert_eq!(FrontMatterUtils::get_front_matter_field_value("--", "title"), None);
671    }
672
673    #[test]
674    fn test_extract_front_matter_fields() {
675        // Simple YAML front matter
676        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        // TOML front matter
683        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        // No front matter
690        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        // TOML
700        let toml_content = "+++\ntitle = \"Test\"\n+++\nContent";
701        assert_eq!(FrontMatterUtils::get_front_matter_end_line(toml_content), 3);
702
703        // No front matter
704        assert_eq!(FrontMatterUtils::get_front_matter_end_line("Regular content"), 0);
705
706        // Too short
707        assert_eq!(FrontMatterUtils::get_front_matter_end_line("--"), 0);
708    }
709
710    #[test]
711    fn test_fix_malformed_front_matter() {
712        // Fix malformed type 1
713        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        // Fix malformed type 2
718        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        // Don't change valid front matter
723        let valid = "---\ntitle: Test\n---\nContent";
724        let unchanged = FrontMatterUtils::fix_malformed_front_matter(valid);
725        assert_eq!(unchanged, valid);
726
727        // No front matter
728        let no_fm = "# Regular content";
729        assert_eq!(FrontMatterUtils::fix_malformed_front_matter(no_fm), no_fm);
730
731        // Too short
732        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        // Note: The current implementation doesn't fully handle nested YAML
749        // This test documents the current behavior
750        assert!(fields.contains_key("title"));
751        // Nested fields handling would need enhancement
752    }
753
754    #[test]
755    fn test_edge_cases() {
756        // Empty content
757        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        // Only delimiters
762        let only_delim = "---\n---";
763        assert!(FrontMatterUtils::extract_front_matter(only_delim).is_empty());
764
765        // Multiple front matter sections (only first should be detected)
766        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}