mdpdf_core/
frontmatter.rs1use std::collections::HashMap;
2
3#[derive(Debug, Default)]
5pub struct Frontmatter {
6 pub title: Option<String>,
7 pub subtitle: Option<String>,
8 pub date: Option<String>,
9 pub mode: Option<String>,
10 pub toc: Option<String>,
11 pub extra: HashMap<String, String>,
12}
13
14impl Frontmatter {
15 pub fn is_dense(&self) -> bool {
16 self.mode.as_deref() == Some("dense")
17 }
18
19 pub fn skip_toc(&self) -> bool {
20 self.toc.as_deref() == Some("false")
21 }
22}
23
24pub fn parse_frontmatter(src: &str) -> (Frontmatter, &str) {
29 let trimmed = src.trim_start_matches('\u{feff}'); if !trimmed.starts_with("---\n") && !trimmed.starts_with("---\r\n") {
31 return (Frontmatter::default(), src);
32 }
33
34 let after_fence = &trimmed[4..];
35 let end = after_fence.find("\n---\n")
36 .or_else(|| after_fence.find("\n---\r\n"))
37 .or_else(|| {
38 if after_fence.ends_with("\n---") {
39 Some(after_fence.len() - 4)
40 } else {
41 None
42 }
43 });
44
45 let end = match end {
46 Some(e) => e,
47 None => return (Frontmatter::default(), src),
48 };
49
50 let yaml_block = &after_fence[..end];
51 let body_start = 4 + end + 5; let body = if body_start <= trimmed.len() {
53 &trimmed[body_start..]
54 } else {
55 ""
56 };
57
58 let mut fm = Frontmatter::default();
59 for line in yaml_block.lines() {
60 let line = line.trim();
61 if line.is_empty() || line.starts_with('#') {
62 continue;
63 }
64 if let Some((key, val)) = parse_kv(line) {
65 match key {
66 "title" => fm.title = Some(val),
67 "subtitle" => fm.subtitle = Some(val),
68 "date" => fm.date = Some(val),
69 "mode" => fm.mode = Some(val),
70 "toc" => fm.toc = Some(val),
71 _ => { fm.extra.insert(key.to_owned(), val); }
72 }
73 }
74 }
75
76 (fm, body)
77}
78
79fn parse_kv(line: &str) -> Option<(&str, String)> {
80 let colon = line.find(':')?;
81 let key = line[..colon].trim();
82 let val = line[colon + 1..].trim();
83
84 let val = if (val.starts_with('"') && val.ends_with('"'))
86 || (val.starts_with('\'') && val.ends_with('\''))
87 {
88 val[1..val.len() - 1].to_owned()
89 } else {
90 val.to_owned()
91 };
92
93 Some((key, val))
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn parse_basic_frontmatter() {
102 let src = "---\ntitle: \"Hello World\"\nmode: dense\ntoc: false\n---\n# Body";
103 let (fm, body) = parse_frontmatter(src);
104 assert_eq!(fm.title.as_deref(), Some("Hello World"));
105 assert!(fm.is_dense());
106 assert!(fm.skip_toc());
107 assert_eq!(body, "# Body");
108 }
109
110 #[test]
111 fn no_frontmatter() {
112 let src = "# Just a heading\nSome text";
113 let (fm, body) = parse_frontmatter(src);
114 assert!(fm.title.is_none());
115 assert_eq!(body, src);
116 }
117}