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 {
44 text: String,
45 #[serde(default)]
46 inline: bool,
47 },
48}
49
50impl FrontmatterItem {
51 pub fn field(key: impl Into<String>, value: QuillValue) -> Self {
53 FrontmatterItem::Field {
54 key: key.into(),
55 value,
56 fill: false,
57 }
58 }
59
60 pub fn comment(text: impl Into<String>) -> Self {
62 FrontmatterItem::Comment {
63 text: text.into(),
64 inline: false,
65 }
66 }
67
68 pub fn comment_inline(text: impl Into<String>) -> Self {
71 FrontmatterItem::Comment {
72 text: text.into(),
73 inline: true,
74 }
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Default)]
85pub struct Frontmatter {
86 items: Vec<FrontmatterItem>,
87 nested_comments: Vec<NestedComment>,
88}
89
90impl Frontmatter {
91 pub fn new() -> Self {
93 Self {
94 items: Vec::new(),
95 nested_comments: Vec::new(),
96 }
97 }
98
99 pub fn from_index_map(map: IndexMap<String, QuillValue>) -> Self {
101 let items = map
102 .into_iter()
103 .map(|(key, value)| FrontmatterItem::Field {
104 key,
105 value,
106 fill: false,
107 })
108 .collect();
109 Self {
110 items,
111 nested_comments: Vec::new(),
112 }
113 }
114
115 pub fn from_items(items: Vec<FrontmatterItem>) -> Self {
117 Self {
118 items,
119 nested_comments: Vec::new(),
120 }
121 }
122
123 pub fn from_items_with_nested(
125 items: Vec<FrontmatterItem>,
126 nested_comments: Vec<NestedComment>,
127 ) -> Self {
128 Self {
129 items,
130 nested_comments,
131 }
132 }
133
134 pub fn nested_comments(&self) -> &[NestedComment] {
138 &self.nested_comments
139 }
140
141 pub fn items(&self) -> &[FrontmatterItem] {
143 &self.items
144 }
145
146 pub fn iter(&self) -> impl Iterator<Item = (&String, &QuillValue)> + '_ {
148 self.items.iter().filter_map(|item| match item {
149 FrontmatterItem::Field { key, value, .. } => Some((key, value)),
150 FrontmatterItem::Comment { .. } => None,
151 })
152 }
153
154 pub fn keys(&self) -> impl Iterator<Item = &String> + '_ {
156 self.items.iter().filter_map(|item| match item {
157 FrontmatterItem::Field { key, .. } => Some(key),
158 FrontmatterItem::Comment { .. } => None,
159 })
160 }
161
162 pub fn len(&self) -> usize {
164 self.items
165 .iter()
166 .filter(|item| matches!(item, FrontmatterItem::Field { .. }))
167 .count()
168 }
169
170 pub fn is_empty(&self) -> bool {
172 self.len() == 0
173 }
174
175 pub fn get(&self, key: &str) -> Option<&QuillValue> {
177 self.items.iter().find_map(|item| match item {
178 FrontmatterItem::Field { key: k, value, .. } if k == key => Some(value),
179 _ => None,
180 })
181 }
182
183 pub fn contains_key(&self, key: &str) -> bool {
185 self.get(key).is_some()
186 }
187
188 pub fn insert(&mut self, key: impl Into<String>, value: QuillValue) -> Option<QuillValue> {
192 let key = key.into();
193 for item in self.items.iter_mut() {
194 if let FrontmatterItem::Field {
195 key: k,
196 value: v,
197 fill,
198 } = item
199 {
200 if k == &key {
201 let old = std::mem::replace(v, value);
202 *fill = false;
203 return Some(old);
204 }
205 }
206 }
207 self.items.push(FrontmatterItem::Field {
208 key,
209 value,
210 fill: false,
211 });
212 None
213 }
214
215 pub fn insert_fill(&mut self, key: impl Into<String>, value: QuillValue) -> Option<QuillValue> {
218 let key = key.into();
219 for item in self.items.iter_mut() {
220 if let FrontmatterItem::Field {
221 key: k,
222 value: v,
223 fill,
224 } = item
225 {
226 if k == &key {
227 let old = std::mem::replace(v, value);
228 *fill = true;
229 return Some(old);
230 }
231 }
232 }
233 self.items.push(FrontmatterItem::Field {
234 key,
235 value,
236 fill: true,
237 });
238 None
239 }
240
241 pub fn remove(&mut self, key: &str) -> Option<QuillValue> {
244 let pos = self
245 .items
246 .iter()
247 .position(|item| matches!(item, FrontmatterItem::Field { key: k, .. } if k == key))?;
248 match self.items.remove(pos) {
249 FrontmatterItem::Field { value, .. } => Some(value),
250 FrontmatterItem::Comment { .. } => unreachable!(),
251 }
252 }
253
254 pub fn is_fill(&self, key: &str) -> bool {
256 self.items.iter().any(|item| match item {
257 FrontmatterItem::Field { key: k, fill, .. } => k == key && *fill,
258 _ => false,
259 })
260 }
261
262 pub fn to_index_map(&self) -> IndexMap<String, QuillValue> {
265 let mut map = IndexMap::new();
266 for item in &self.items {
267 if let FrontmatterItem::Field { key, value, .. } = item {
268 map.insert(key.clone(), value.clone());
269 }
270 }
271 map
272 }
273}
274
275impl<'a> IntoIterator for &'a Frontmatter {
276 type Item = (&'a String, &'a QuillValue);
277 type IntoIter = std::iter::FilterMap<
278 std::slice::Iter<'a, FrontmatterItem>,
279 fn(&'a FrontmatterItem) -> Option<(&'a String, &'a QuillValue)>,
280 >;
281
282 fn into_iter(self) -> Self::IntoIter {
283 fn filter(item: &FrontmatterItem) -> Option<(&String, &QuillValue)> {
284 match item {
285 FrontmatterItem::Field { key, value, .. } => Some((key, value)),
286 FrontmatterItem::Comment { .. } => None,
287 }
288 }
289 self.items.iter().filter_map(filter)
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 fn qv(s: &str) -> QuillValue {
298 QuillValue::from_json(serde_json::json!(s))
299 }
300
301 #[test]
302 fn insert_new_appends() {
303 let mut fm = Frontmatter::new();
304 fm.insert("title", qv("Hello"));
305 fm.insert("author", qv("Alice"));
306 assert_eq!(fm.len(), 2);
307 let keys: Vec<&String> = fm.keys().collect();
308 assert_eq!(keys, vec!["title", "author"]);
309 }
310
311 #[test]
312 fn insert_existing_preserves_position() {
313 let mut fm = Frontmatter::new();
314 fm.insert("a", qv("1"));
315 fm.insert("b", qv("2"));
316 fm.insert("a", qv("updated"));
317 let keys: Vec<&String> = fm.keys().collect();
318 assert_eq!(keys, vec!["a", "b"]);
319 assert_eq!(fm.get("a").unwrap().as_str(), Some("updated"));
320 }
321
322 #[test]
323 fn insert_clears_fill() {
324 let mut fm = Frontmatter::new();
325 fm.insert_fill("k", qv("placeholder"));
326 assert!(fm.is_fill("k"));
327 fm.insert("k", qv("user value"));
328 assert!(!fm.is_fill("k"));
329 }
330
331 #[test]
332 fn insert_fill_preserves_position_and_sets_flag() {
333 let mut fm = Frontmatter::new();
334 fm.insert("k", qv("v"));
335 fm.insert_fill("k", qv("placeholder"));
336 assert!(fm.is_fill("k"));
337 assert_eq!(fm.get("k").unwrap().as_str(), Some("placeholder"));
338 }
339
340 #[test]
341 fn remove_leaves_comments_alone() {
342 let items = vec![
343 FrontmatterItem::comment("header"),
344 FrontmatterItem::field("a", qv("1")),
345 FrontmatterItem::comment("mid"),
346 FrontmatterItem::field("b", qv("2")),
347 ];
348 let mut fm = Frontmatter::from_items(items);
349 let removed = fm.remove("a").unwrap();
350 assert_eq!(removed.as_str(), Some("1"));
351 let comments: Vec<&str> = fm
352 .items()
353 .iter()
354 .filter_map(|item| match item {
355 FrontmatterItem::Comment { text, .. } => Some(text.as_str()),
356 FrontmatterItem::Field { .. } => None,
357 })
358 .collect();
359 assert_eq!(comments, vec!["header", "mid"]);
360 }
361
362 #[test]
363 fn map_style_iter_skips_comments() {
364 let items = vec![
365 FrontmatterItem::comment("c"),
366 FrontmatterItem::field("a", qv("1")),
367 FrontmatterItem::field("b", qv("2")),
368 ];
369 let fm = Frontmatter::from_items(items);
370 let pairs: Vec<(String, String)> = fm
371 .iter()
372 .map(|(k, v)| (k.clone(), v.as_str().unwrap_or_default().to_string()))
373 .collect();
374 assert_eq!(
375 pairs,
376 vec![
377 ("a".to_string(), "1".to_string()),
378 ("b".to_string(), "2".to_string())
379 ]
380 );
381 }
382}