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