rustbrain_core/obsidian/
frontmatter.rs1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Serialize, Deserialize, Default)]
8pub struct Frontmatter {
9 #[serde(default)]
11 pub tags: Vec<String>,
12 #[serde(default)]
14 pub node_type: Option<String>,
15 #[serde(default)]
17 pub aliases: Vec<String>,
18 #[serde(flatten)]
20 pub extra: HashMap<String, serde_yaml_ng::Value>,
21}
22
23pub fn parse_frontmatter(markdown: &str) -> (Option<Frontmatter>, &str) {
27 let trimmed = markdown.trim_start();
28 if !trimmed.starts_with("---") {
29 return (None, markdown);
30 }
31
32 let rest = &trimmed[3..];
33 let rest = rest.strip_prefix('\n').unwrap_or(rest);
35
36 if let Some(end_idx) = rest.find("\n---") {
37 let yaml_str = &rest[..end_idx];
38 let body = rest[end_idx + 4..].trim_start_matches('\n');
39 match serde_yaml_ng::from_str::<Frontmatter>(yaml_str) {
40 Ok(fm) => return (Some(fm), body),
41 Err(_) => return (None, markdown),
42 }
43 }
44
45 (None, markdown)
46}
47
48#[cfg(test)]
49mod tests {
50 use super::*;
51
52 #[test]
53 fn parse_frontmatter_basic() {
54 let content =
55 "---\ntags: [consensus, raft]\nnode_type: concept\naliases: [Raft]\n---\n# Raft Consensus\nNotes here.";
56 let (fm, body) = parse_frontmatter(content);
57 assert!(fm.is_some());
58 let fm = fm.unwrap();
59 assert_eq!(fm.tags, vec!["consensus", "raft"]);
60 assert_eq!(fm.node_type.as_deref(), Some("concept"));
61 assert_eq!(fm.aliases, vec!["Raft"]);
62 assert!(body.starts_with("# Raft Consensus"));
63 }
64
65 #[test]
66 fn no_frontmatter() {
67 let (fm, body) = parse_frontmatter("# Hello");
68 assert!(fm.is_none());
69 assert_eq!(body, "# Hello");
70 }
71}