docgen_core/
frontmatter.rs1use serde_yml::Value;
2
3#[derive(Debug, Clone, PartialEq)]
5pub struct Parsed {
6 pub frontmatter: Value,
7 pub body: String,
8}
9
10pub fn parse_frontmatter(raw: &str) -> Parsed {
15 parse_frontmatter_checked(raw).0
16}
17
18pub fn parse_frontmatter_checked(raw: &str) -> (Parsed, Option<String>) {
23 let input = raw.strip_prefix('\u{feff}').unwrap_or(raw);
24
25 let after_open = input
27 .strip_prefix("---\n")
28 .or_else(|| input.strip_prefix("---\r\n"));
29
30 if let Some(rest) = after_open {
31 let mut offset = 0usize;
33 for line in rest.split_inclusive('\n') {
34 let trimmed = line.trim_end_matches('\n').trim_end_matches('\r');
35 if trimmed.trim_end() == "---" {
36 let yaml = &rest[..offset];
37 let after = &rest[offset + line.len()..];
38 let (frontmatter, err) = parse_yaml(yaml);
39 return (
40 Parsed {
41 frontmatter,
42 body: after.to_string(),
43 },
44 err,
45 );
46 }
47 offset += line.len();
48 }
49 let last = &rest[offset..];
51 if last.trim_end_matches('\r').trim_end() == "---" {
52 let yaml = &rest[..offset];
53 let (frontmatter, err) = parse_yaml(yaml);
54 return (
55 Parsed {
56 frontmatter,
57 body: String::new(),
58 },
59 err,
60 );
61 }
62 }
63
64 (
65 Parsed {
66 frontmatter: Value::Null,
67 body: input.to_string(),
68 },
69 None,
70 )
71}
72
73fn parse_yaml(yaml: &str) -> (Value, Option<String>) {
76 if yaml.trim().is_empty() {
77 return (Value::Null, None);
78 }
79 match serde_yml::from_str(yaml) {
80 Ok(v) => (v, None),
81 Err(e) => (Value::Null, Some(e.to_string())),
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn parses_yaml_frontmatter_and_body() {
91 let raw = "---\ntitle: Hello\n---\n# Body\n";
92 let parsed = parse_frontmatter(raw);
93 assert_eq!(parsed.frontmatter["title"].as_str(), Some("Hello"));
94 assert_eq!(parsed.body, "# Body\n");
95 }
96
97 #[test]
98 fn no_frontmatter_returns_null_and_full_body() {
99 let raw = "# Just body\n";
100 let parsed = parse_frontmatter(raw);
101 assert!(parsed.frontmatter.is_null());
102 assert_eq!(parsed.body, "# Just body\n");
103 }
104
105 #[test]
106 fn parses_crlf_frontmatter() {
107 let raw = "---\r\ntitle: X\r\n---\r\nbody\r\n";
108 let parsed = parse_frontmatter(raw);
109 assert_eq!(parsed.frontmatter["title"].as_str(), Some("X"));
110 assert_eq!(parsed.body, "body\r\n");
111 }
112
113 #[test]
114 fn parses_empty_frontmatter_block() {
115 let raw = "---\n---\nbody\n";
116 let parsed = parse_frontmatter(raw);
117 assert!(parsed.frontmatter.is_null());
118 assert_eq!(parsed.body, "body\n");
119 }
120
121 #[test]
122 fn longer_dash_run_is_not_a_closing_fence() {
123 let raw = "---\ntitle: X\n----\nbody\n";
126 let parsed = parse_frontmatter(raw);
127 assert!(parsed.frontmatter.is_null());
129 assert_eq!(parsed.body, raw);
130 }
131
132 #[test]
133 fn malformed_yaml_falls_back_to_null_with_body() {
134 let raw = "---\n: not: valid: yaml\n---\nbody\n";
135 let parsed = parse_frontmatter(raw);
136 assert!(parsed.frontmatter.is_null());
137 assert_eq!(parsed.body, "body\n");
138 }
139
140 #[test]
141 fn unterminated_block_returns_full_input_as_body() {
142 let raw = "---\ntitle: X\n";
143 let parsed = parse_frontmatter(raw);
144 assert!(parsed.frontmatter.is_null());
145 assert_eq!(parsed.body, raw);
146 }
147
148 #[test]
149 fn checked_reports_no_error_for_valid_frontmatter() {
150 let (parsed, err) = parse_frontmatter_checked("---\ntitle: Hello\n---\nbody\n");
151 assert_eq!(parsed.frontmatter["title"].as_str(), Some("Hello"));
152 assert!(err.is_none());
153 }
154
155 #[test]
156 fn checked_reports_yaml_error_for_malformed_frontmatter() {
157 let (parsed, err) = parse_frontmatter_checked("---\n: not: valid: yaml\n---\nbody\n");
158 assert!(parsed.frontmatter.is_null());
159 assert_eq!(parsed.body, "body\n");
160 assert!(
161 err.is_some(),
162 "malformed YAML must surface an error message"
163 );
164 assert!(!err.unwrap().is_empty());
165 }
166
167 #[test]
168 fn checked_reports_no_error_when_frontmatter_absent_or_empty() {
169 let (parsed, err) = parse_frontmatter_checked("# Just body\n");
170 assert!(parsed.frontmatter.is_null());
171 assert!(err.is_none());
172
173 let (parsed, err) = parse_frontmatter_checked("---\n---\nbody\n");
174 assert!(parsed.frontmatter.is_null());
175 assert!(err.is_none());
176 }
177
178 #[test]
179 fn strips_leading_bom() {
180 let raw = "\u{feff}---\ntitle: X\n---\nbody\n";
181 let parsed = parse_frontmatter(raw);
182 assert_eq!(parsed.frontmatter["title"].as_str(), Some("X"));
183 assert_eq!(parsed.body, "body\n");
184 }
185}