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