cobalt_config/
document.rs1use 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 let mut splits = FRONT_MATTER_DIVIDE.splitn(content, 3).skip(1);
95
96 let front_split = splits.next().unwrap_or("");
98
99 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 let front_split = splits.next().unwrap_or("");
130
131 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}