1use std::{collections::HashMap, path::PathBuf};
2
3use gray_matter::{Matter, engine::YAML};
4use serde::{Deserialize, Serialize};
5use toml::Value;
6
7#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone)]
8pub struct Heading {
9 pub depth: u8,
10 pub text: String,
11 pub slug: String,
12}
13
14#[derive(Debug, Serialize, Deserialize, Default, Clone)]
15pub struct Document {
16 pub at_path: String,
17 pub metadata: BaseMetaData,
18 pub markdown: String,
19 pub excerpt: Option<String>,
20 pub html: Option<String>,
21 pub toc: Vec<Heading>,
22}
23
24#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
25#[serde(default)]
26pub struct BaseMetaData {
27 pub title: String,
28 pub description: String,
29 pub tags: Vec<String>,
30 pub keywords: Vec<String>,
31 pub template: String,
32
33 #[serde(flatten)]
34 pub user: HashMap<String, Value>,
35}
36
37impl Default for BaseMetaData {
38 fn default() -> Self {
39 Self {
40 title: Default::default(),
41 tags: Default::default(),
42 description: Default::default(),
43 keywords: Default::default(),
44 template: "default".into(),
45 user: Default::default(),
46 }
47 }
48}
49
50impl Document {
51 pub fn new_from_path(path: PathBuf) -> Self {
52 let contents_result = std::fs::read_to_string(&path);
53
54 if contents_result.is_err() {
55 dbg!("error reading file: {}", contents_result.err());
56 panic!("failed to read '{}'", path.display());
57 }
58
59 let matter = Matter::<YAML>::new();
60 let parse_result = matter.parse(contents_result.as_ref().unwrap().as_str());
61 let base_metadata_opt = parse_result
62 .data
63 .as_ref()
64 .unwrap()
65 .deserialize::<BaseMetaData>();
66
67 if base_metadata_opt.is_err() {
68 dbg!("error parsing: {}", base_metadata_opt.err());
69 panic!("Failed to parse the frontmatter in {}", path.display());
70 }
71
72 let base_metadata = base_metadata_opt.unwrap();
73
74 if base_metadata.title.is_empty() {
75 panic!("title is required in your frontmatter!");
76 }
77
78 Self {
79 at_path: path.display().to_string(),
80 metadata: base_metadata,
81 markdown: parse_result.content,
82 excerpt: parse_result.excerpt,
83 ..Default::default()
84 }
85 }
86}
87
88#[cfg(test)]
89mod test {
90 use super::*;
91 use pretty_assertions::assert_eq;
92
93 #[test]
94 fn test_document_loading() {
95 let base_path_wd = std::env::current_dir()
96 .unwrap()
97 .as_os_str()
98 .to_os_string()
99 .to_str()
100 .unwrap()
101 .to_string();
102 let base_path = format!("{}/test_fixtures/markdown", base_path_wd);
103 let document = Document::new_from_path(format!("{}/full_frontmatter.md", base_path).into());
104
105 assert_eq!(
106 BaseMetaData {
107 tags: vec!["1".into()],
108 keywords: vec!["2".into()],
109 title: "test".into(),
110 description: "test".into(),
111 user: HashMap::new(),
112 template: "default".into(),
113 },
114 document.metadata
115 )
116 }
117
118 #[test]
119 #[should_panic]
120 fn test_bad_document_loading() {
121 let base_path_wd = std::env::current_dir()
122 .unwrap()
123 .as_os_str()
124 .to_os_string()
125 .to_str()
126 .unwrap()
127 .to_string();
128 let base_path = format!("{}/test_fixtures/markdown", base_path_wd);
129
130 Document::new_from_path(format!("{}/missing_frontmatter_keys.md", base_path).into());
131 }
132}