cobalt_config/
document.rs

1use std::fmt;
2
3use crate::Frontmatter;
4use crate::Result;
5use crate::Status;
6
7#[derive(Debug, Eq, PartialEq, Default, Clone)]
8pub struct Document {
9    front: Frontmatter,
10    content: liquid_core::model::KString,
11}
12
13impl Document {
14    pub fn new(front: Frontmatter, content: liquid_core::model::KString) -> Self {
15        Self { front, content }
16    }
17
18    pub fn parse(content: &str) -> Result<Self> {
19        let (front, content) = split_document(content);
20        let front = front
21            .map(parse_frontmatter)
22            .map(|r| r.map(Some))
23            .unwrap_or(Ok(None))?
24            .unwrap_or_default();
25        let content = liquid_core::model::KString::from_ref(content);
26        Ok(Self { front, content })
27    }
28
29    pub fn into_parts(self) -> (Frontmatter, liquid_core::model::KString) {
30        let Self { front, content } = self;
31        (front, content)
32    }
33}
34
35impl fmt::Display for Document {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        let front = self.front.to_string();
38        if front.is_empty() {
39            write!(f, "{}", self.content)
40        } else {
41            write!(f, "---\n{}\n---\n{}", front, self.content)
42        }
43    }
44}
45
46fn parse_frontmatter(front: &str) -> Result<Frontmatter> {
47    let front: Frontmatter = serde_yaml::from_str(front)
48        .map_err(|e| Status::new("Failed to parse frontmatter").with_source(e))?;
49    Ok(front)
50}
51
52#[cfg(feature = "preview_unstable")]
53static FRONT_MATTER: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
54    regex::RegexBuilder::new(r"\A---\s*\r?\n([\s\S]*\n)?---\s*\r?\n(.*)")
55        .dot_matches_new_line(true)
56        .build()
57        .unwrap()
58});
59
60#[cfg(feature = "preview_unstable")]
61fn split_document(content: &str) -> (Option<&str>, &str) {
62    if let Some(captures) = FRONT_MATTER.captures(content) {
63        let front_split = captures.get(1).map(|m| m.as_str()).unwrap_or_default();
64        let content_split = captures.get(2).unwrap().as_str();
65
66        if front_split.is_empty() {
67            (None, content_split)
68        } else {
69            (Some(front_split), content_split)
70        }
71    } else {
72        (None, content)
73    }
74}
75
76#[cfg(not(feature = "preview_unstable"))]
77fn split_document(content: &str) -> (Option<&str>, &str) {
78    static FRONT_MATTER_DIVIDE: std::sync::LazyLock<regex::Regex> =
79        std::sync::LazyLock::new(|| {
80            regex::RegexBuilder::new(r"---\s*\r?\n")
81                .dot_matches_new_line(true)
82                .build()
83                .unwrap()
84        });
85    static FRONT_MATTER: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
86        regex::RegexBuilder::new(r"\A---\s*\r?\n([\s\S]*\n)?---\s*\r?\n")
87            .dot_matches_new_line(true)
88            .build()
89            .unwrap()
90    });
91
92    if FRONT_MATTER.is_match(content) {
93        // skip first empty string
94        let mut splits = FRONT_MATTER_DIVIDE.splitn(content, 3).skip(1);
95
96        // split between dividers
97        let front_split = splits.next().unwrap_or("");
98
99        // split after second divider
100        let content_split = splits.next().unwrap_or("");
101
102        if front_split.is_empty() {
103            (None, content_split)
104        } else {
105            (Some(front_split), content_split)
106        }
107    } else {
108        deprecated_split_front_matter(content)
109    }
110}
111
112#[cfg(not(feature = "preview_unstable"))]
113fn deprecated_split_front_matter(content: &str) -> (Option<&str>, &str) {
114    static FRONT_MATTER_DIVIDE: std::sync::LazyLock<regex::Regex> =
115        std::sync::LazyLock::new(|| {
116            regex::RegexBuilder::new(r"(\A|\n)---\s*\r?\n")
117                .dot_matches_new_line(true)
118                .build()
119                .unwrap()
120        });
121    if FRONT_MATTER_DIVIDE.is_match(content) {
122        log::warn!(
123            "Trailing separators are deprecated. We recommend frontmatters be surrounded, above and below, with ---"
124        );
125
126        let mut splits = FRONT_MATTER_DIVIDE.splitn(content, 2);
127
128        // above the split are the attributes
129        let front_split = splits.next().unwrap_or("");
130
131        // everything below the split becomes the new content
132        let content_split = splits.next().unwrap_or("");
133
134        if front_split.is_empty() {
135            (None, content_split)
136        } else {
137            (Some(front_split), content_split)
138        }
139    } else {
140        (None, content)
141    }
142}
143
144#[cfg(test)]
145mod test {
146    use super::*;
147
148    #[test]
149    fn split_document_empty() {
150        let input = "";
151        let (cobalt_model, content) = split_document(input);
152        assert!(cobalt_model.is_none());
153        assert_eq!(content, "");
154    }
155
156    #[test]
157    fn split_document_no_front_matter() {
158        let input = "Body";
159        let (cobalt_model, content) = split_document(input);
160        assert!(cobalt_model.is_none());
161        assert_eq!(content, "Body");
162    }
163
164    #[test]
165    fn split_document_empty_front_matter() {
166        let input = "---\n---\nBody";
167        let (cobalt_model, content) = split_document(input);
168        assert!(cobalt_model.is_none());
169        assert_eq!(content, "Body");
170    }
171
172    #[test]
173    fn split_document_empty_body() {
174        let input = "---\ncobalt_model\n---\n";
175        let (cobalt_model, content) = split_document(input);
176        assert_eq!(cobalt_model.unwrap(), "cobalt_model\n");
177        assert_eq!(content, "");
178    }
179
180    #[test]
181    fn split_document_front_matter_and_body() {
182        let input = "---\ncobalt_model\n---\nbody";
183        let (cobalt_model, content) = split_document(input);
184        assert_eq!(cobalt_model.unwrap(), "cobalt_model\n");
185        assert_eq!(content, "body");
186    }
187
188    #[test]
189    fn split_document_no_new_line_after_front_matter() {
190        let input = "invalid_front_matter---\nbody";
191        let (cobalt_model, content) = split_document(input);
192        println!("{cobalt_model:?}");
193        assert!(cobalt_model.is_none());
194        assert_eq!(content, input);
195    }
196
197    #[test]
198    fn split_document_multiline_body() {
199        let input = "---\ncobalt_model\n---\nfirst\nsecond";
200        let (cobalt_model, content) = split_document(input);
201        println!("{cobalt_model:?}");
202        assert_eq!(cobalt_model.unwrap(), "cobalt_model\n");
203        assert_eq!(content, "first\nsecond");
204    }
205
206    #[test]
207    fn display_empty() {
208        let front = Frontmatter::empty();
209        let doc = Document::new(front, liquid_core::model::KString::new());
210        assert_eq!(&doc.to_string(), "");
211    }
212
213    #[test]
214    fn display_empty_front() {
215        let front = Frontmatter::empty();
216        let doc = Document::new(front, "body".into());
217        assert_eq!(&doc.to_string(), "body");
218    }
219
220    #[test]
221    fn display_empty_body() {
222        let front = Frontmatter {
223            slug: Some("foo".into()),
224            ..Default::default()
225        };
226        let doc = Document::new(front, liquid_core::model::KString::new());
227        assert_eq!(&doc.to_string(), "---\nslug: foo\n---\n");
228    }
229
230    #[test]
231    fn display_both() {
232        let front = Frontmatter {
233            slug: Some("foo".into()),
234            ..Default::default()
235        };
236        let doc = Document::new(front, "body".into());
237        assert_eq!(&doc.to_string(), "---\nslug: foo\n---\nbody");
238    }
239}