1use indexmap::IndexMap;
15use serde::{Deserialize, Serialize};
16
17use super::prescan::NestedComment;
18use crate::value::QuillValue;
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22#[serde(tag = "kind", rename_all = "lowercase")]
23pub enum FrontmatterItem {
24 Field {
26 key: String,
27 value: QuillValue,
28 #[serde(default)]
31 fill: bool,
32 },
33 Comment { text: String },
36}
37
38impl FrontmatterItem {
39 pub fn field(key: impl Into<String>, value: QuillValue) -> Self {
41 FrontmatterItem::Field {
42 key: key.into(),
43 value,
44 fill: false,
45 }
46 }
47
48 pub fn comment(text: impl Into<String>) -> Self {
50 FrontmatterItem::Comment { text: text.into() }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Default)]
61pub struct Frontmatter {
62 items: Vec<FrontmatterItem>,
63 nested_comments: Vec<NestedComment>,
64}
65
66impl Frontmatter {
67 pub fn new() -> Self {
69 Self {
70 items: Vec::new(),
71 nested_comments: Vec::new(),
72 }
73 }
74
75 pub fn from_index_map(map: IndexMap<String, QuillValue>) -> Self {
77 let items = map
78 .into_iter()
79 .map(|(key, value)| FrontmatterItem::Field {
80 key,
81 value,
82 fill: false,
83 })
84 .collect();
85 Self {
86 items,
87 nested_comments: Vec::new(),
88 }
89 }
90
91 pub fn from_items(items: Vec<FrontmatterItem>) -> Self {
93 Self {
94 items,
95 nested_comments: Vec::new(),
96 }
97 }
98
99 pub fn from_items_with_nested(
101 items: Vec<FrontmatterItem>,
102 nested_comments: Vec<NestedComment>,
103 ) -> Self {
104 Self {
105 items,
106 nested_comments,
107 }
108 }
109
110 pub fn nested_comments(&self) -> &[NestedComment] {
114 &self.nested_comments
115 }
116
117 pub fn items(&self) -> &[FrontmatterItem] {
119 &self.items
120 }
121
122 pub fn iter(&self) -> impl Iterator<Item = (&String, &QuillValue)> + '_ {
124 self.items.iter().filter_map(|item| match item {
125 FrontmatterItem::Field { key, value, .. } => Some((key, value)),
126 FrontmatterItem::Comment { .. } => None,
127 })
128 }
129
130 pub fn keys(&self) -> impl Iterator<Item = &String> + '_ {
132 self.items.iter().filter_map(|item| match item {
133 FrontmatterItem::Field { key, .. } => Some(key),
134 FrontmatterItem::Comment { .. } => None,
135 })
136 }
137
138 pub fn len(&self) -> usize {
140 self.items
141 .iter()
142 .filter(|item| matches!(item, FrontmatterItem::Field { .. }))
143 .count()
144 }
145
146 pub fn is_empty(&self) -> bool {
148 self.len() == 0
149 }
150
151 pub fn get(&self, key: &str) -> Option<&QuillValue> {
153 self.items.iter().find_map(|item| match item {
154 FrontmatterItem::Field { key: k, value, .. } if k == key => Some(value),
155 _ => None,
156 })
157 }
158
159 pub fn contains_key(&self, key: &str) -> bool {
161 self.get(key).is_some()
162 }
163
164 pub fn insert(&mut self, key: impl Into<String>, value: QuillValue) -> Option<QuillValue> {
168 let key = key.into();
169 for item in self.items.iter_mut() {
170 if let FrontmatterItem::Field {
171 key: k,
172 value: v,
173 fill,
174 } = item
175 {
176 if k == &key {
177 let old = std::mem::replace(v, value);
178 *fill = false;
179 return Some(old);
180 }
181 }
182 }
183 self.items.push(FrontmatterItem::Field {
184 key,
185 value,
186 fill: false,
187 });
188 None
189 }
190
191 pub fn insert_fill(&mut self, key: impl Into<String>, value: QuillValue) -> Option<QuillValue> {
194 let key = key.into();
195 for item in self.items.iter_mut() {
196 if let FrontmatterItem::Field {
197 key: k,
198 value: v,
199 fill,
200 } = item
201 {
202 if k == &key {
203 let old = std::mem::replace(v, value);
204 *fill = true;
205 return Some(old);
206 }
207 }
208 }
209 self.items.push(FrontmatterItem::Field {
210 key,
211 value,
212 fill: true,
213 });
214 None
215 }
216
217 pub fn remove(&mut self, key: &str) -> Option<QuillValue> {
220 let pos = self
221 .items
222 .iter()
223 .position(|item| matches!(item, FrontmatterItem::Field { key: k, .. } if k == key))?;
224 match self.items.remove(pos) {
225 FrontmatterItem::Field { value, .. } => Some(value),
226 FrontmatterItem::Comment { .. } => unreachable!(),
227 }
228 }
229
230 pub fn is_fill(&self, key: &str) -> bool {
232 self.items.iter().any(|item| match item {
233 FrontmatterItem::Field { key: k, fill, .. } => k == key && *fill,
234 _ => false,
235 })
236 }
237
238 pub fn to_index_map(&self) -> IndexMap<String, QuillValue> {
241 let mut map = IndexMap::new();
242 for item in &self.items {
243 if let FrontmatterItem::Field { key, value, .. } = item {
244 map.insert(key.clone(), value.clone());
245 }
246 }
247 map
248 }
249}
250
251impl<'a> IntoIterator for &'a Frontmatter {
252 type Item = (&'a String, &'a QuillValue);
253 type IntoIter = std::iter::FilterMap<
254 std::slice::Iter<'a, FrontmatterItem>,
255 fn(&'a FrontmatterItem) -> Option<(&'a String, &'a QuillValue)>,
256 >;
257
258 fn into_iter(self) -> Self::IntoIter {
259 fn filter<'a>(item: &'a FrontmatterItem) -> Option<(&'a String, &'a QuillValue)> {
260 match item {
261 FrontmatterItem::Field { key, value, .. } => Some((key, value)),
262 FrontmatterItem::Comment { .. } => None,
263 }
264 }
265 self.items.iter().filter_map(filter)
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 fn qv(s: &str) -> QuillValue {
274 QuillValue::from_json(serde_json::json!(s))
275 }
276
277 #[test]
278 fn insert_new_appends() {
279 let mut fm = Frontmatter::new();
280 fm.insert("title", qv("Hello"));
281 fm.insert("author", qv("Alice"));
282 assert_eq!(fm.len(), 2);
283 let keys: Vec<&String> = fm.keys().collect();
284 assert_eq!(keys, vec!["title", "author"]);
285 }
286
287 #[test]
288 fn insert_existing_preserves_position() {
289 let mut fm = Frontmatter::new();
290 fm.insert("a", qv("1"));
291 fm.insert("b", qv("2"));
292 fm.insert("a", qv("updated"));
293 let keys: Vec<&String> = fm.keys().collect();
294 assert_eq!(keys, vec!["a", "b"]);
295 assert_eq!(fm.get("a").unwrap().as_str(), Some("updated"));
296 }
297
298 #[test]
299 fn insert_clears_fill() {
300 let mut fm = Frontmatter::new();
301 fm.insert_fill("k", qv("placeholder"));
302 assert!(fm.is_fill("k"));
303 fm.insert("k", qv("user value"));
304 assert!(!fm.is_fill("k"));
305 }
306
307 #[test]
308 fn insert_fill_preserves_position_and_sets_flag() {
309 let mut fm = Frontmatter::new();
310 fm.insert("k", qv("v"));
311 fm.insert_fill("k", qv("placeholder"));
312 assert!(fm.is_fill("k"));
313 assert_eq!(fm.get("k").unwrap().as_str(), Some("placeholder"));
314 }
315
316 #[test]
317 fn remove_leaves_comments_alone() {
318 let items = vec![
319 FrontmatterItem::comment("header"),
320 FrontmatterItem::field("a", qv("1")),
321 FrontmatterItem::comment("mid"),
322 FrontmatterItem::field("b", qv("2")),
323 ];
324 let mut fm = Frontmatter::from_items(items);
325 let removed = fm.remove("a").unwrap();
326 assert_eq!(removed.as_str(), Some("1"));
327 let comments: Vec<&str> = fm
328 .items()
329 .iter()
330 .filter_map(|item| match item {
331 FrontmatterItem::Comment { text } => Some(text.as_str()),
332 FrontmatterItem::Field { .. } => None,
333 })
334 .collect();
335 assert_eq!(comments, vec!["header", "mid"]);
336 }
337
338 #[test]
339 fn map_style_iter_skips_comments() {
340 let items = vec![
341 FrontmatterItem::comment("c"),
342 FrontmatterItem::field("a", qv("1")),
343 FrontmatterItem::field("b", qv("2")),
344 ];
345 let fm = Frontmatter::from_items(items);
346 let pairs: Vec<(String, String)> = fm
347 .iter()
348 .map(|(k, v)| (k.clone(), v.as_str().unwrap_or_default().to_string()))
349 .collect();
350 assert_eq!(
351 pairs,
352 vec![
353 ("a".to_string(), "1".to_string()),
354 ("b".to_string(), "2".to_string())
355 ]
356 );
357 }
358}