Skip to main content

mdlint/markdown/
front_matter.rs

1#[derive(Debug, Clone, PartialEq, Eq)]
2pub enum FrontMatterType {
3    Yaml,
4    Toml,
5}
6
7const YAML_FRONT_MATTER: &str = "---";
8const TOML_FRONT_MATTER: &str = "+++";
9
10#[derive(Debug, Clone)]
11pub struct FrontMatter {
12    pub matter_type: FrontMatterType,
13    pub content: String,
14    pub end_line: usize,
15}
16
17pub fn detect_front_matter(content: &str) -> Option<FrontMatter> {
18    let lines: Vec<&str> = content.lines().collect();
19    if lines.is_empty() {
20        return None;
21    }
22
23    detect_filetype_front_matter(&lines, FrontMatterType::Yaml)
24        .or_else(|| detect_filetype_front_matter(&lines, FrontMatterType::Toml))
25}
26
27fn detect_filetype_front_matter(
28    lines: &[&str],
29    matter_type: FrontMatterType,
30) -> Option<FrontMatter> {
31    if lines.is_empty() {
32        return None;
33    }
34
35    let front_matter = match matter_type {
36        FrontMatterType::Yaml => YAML_FRONT_MATTER,
37        FrontMatterType::Toml => TOML_FRONT_MATTER,
38    };
39    if lines[0] != front_matter {
40        return None;
41    }
42
43    for (i, line) in lines.iter().enumerate().skip(1) {
44        if *line != front_matter {
45            continue;
46        }
47        let content = lines[1..i].join("\n");
48        return Some(FrontMatter {
49            matter_type,
50            content,
51            end_line: i + 1,
52        });
53    }
54
55    None
56}
57
58#[allow(dead_code)]
59pub fn strip_front_matter(content: &str) -> String {
60    if let Some(front_matter) = detect_front_matter(content) {
61        let lines: Vec<&str> = content.lines().collect();
62        lines[front_matter.end_line..].join("\n")
63    } else {
64        content.to_string()
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn test_detect_yaml_front_matter() {
74        let content = "---\ntitle: Test\nauthor: John\n---\n# Heading";
75        let fm = detect_front_matter(content).unwrap();
76
77        assert_eq!(fm.matter_type, FrontMatterType::Yaml);
78        assert_eq!(fm.content, "title: Test\nauthor: John");
79        assert_eq!(fm.end_line, 4);
80    }
81
82    #[test]
83    fn test_detect_toml_front_matter() {
84        let content = "+++\ntitle = \"Test\"\nauthor = \"John\"\n+++\n# Heading";
85        let fm = detect_front_matter(content).unwrap();
86
87        assert_eq!(fm.matter_type, FrontMatterType::Toml);
88        assert_eq!(fm.content, "title = \"Test\"\nauthor = \"John\"");
89        assert_eq!(fm.end_line, 4);
90    }
91
92    #[test]
93    fn test_no_front_matter() {
94        let content = "# Heading\nSome content";
95        assert!(detect_front_matter(content).is_none());
96    }
97
98    #[test]
99    fn test_incomplete_front_matter() {
100        let content = "---\ntitle: Test\n# Heading";
101        assert!(detect_front_matter(content).is_none());
102    }
103
104    #[test]
105    fn test_strip_front_matter() {
106        let content = "---\ntitle: Test\n---\n# Heading\nContent";
107        let stripped = strip_front_matter(content);
108
109        assert_eq!(stripped, "# Heading\nContent");
110    }
111
112    #[test]
113    fn test_strip_no_front_matter() {
114        let content = "# Heading\nContent";
115        let stripped = strip_front_matter(content);
116
117        assert_eq!(stripped, content);
118    }
119}