1use chrono::{DateTime, Local};
2use gray_matter::{Matter, engine::YAML};
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap as Map;
5use std::path::PathBuf;
6use toml::Value;
7
8use crate::{document_toc::toc_from_document, normalize_line_endings};
9
10#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone)]
11pub struct Heading {
12 pub depth: u8,
13 pub text: String,
14 pub slug: String,
15}
16
17#[derive(Debug, Serialize, Deserialize, Default, Clone)]
18pub struct Document {
19 pub at_path: String,
20 pub metadata: BaseMetaData,
21 pub markdown: String,
22 pub html: Option<String>,
23 pub toc: Vec<Heading>,
24 pub emit: bool,
25 pub content_root: PathBuf,
26}
27
28#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
29#[serde(default)]
30pub struct BaseMetaData {
31 pub title: String,
32 pub description: String,
33 pub tags: Vec<String>,
34 pub keywords: Vec<String>,
35 pub template: String,
36 pub emit: bool,
37 pub published: Option<String>,
38 pub last_updated: Option<String>,
39 pub excerpt: Option<String>,
40
41 #[serde(flatten)]
42 pub user: Map<String, Value>,
43}
44
45impl Default for BaseMetaData {
46 fn default() -> Self {
47 Self {
48 title: Default::default(),
49 tags: Default::default(),
50 description: Default::default(),
51 keywords: Default::default(),
52 template: "default".into(),
53 published: None,
54 last_updated: None,
55 emit: true,
56 user: Map::new(),
57 excerpt: None,
58 }
59 }
60}
61
62impl Document {
63 pub fn new_from_path(content_root: PathBuf, path: PathBuf) -> Self {
64 let contents_result = std::fs::read_to_string(&path);
65 let file_meta = std::fs::metadata(&path).unwrap();
66
67 if contents_result.is_err() {
68 dbg!("error reading file: {}", contents_result.err());
69 panic!("failed to read '{}'", path.display());
70 }
71
72 let matter = Matter::<YAML>::new();
73 let parseable = normalize_line_endings(contents_result.as_ref().unwrap().as_bytes());
74 let parse_result = matter.parse(&parseable);
75 let base_metadata_opt = match parse_result.data {
76 Some(data) => data.deserialize::<BaseMetaData>(),
77 None => Ok(BaseMetaData::default()),
78 };
79
80 if base_metadata_opt.is_err() {
81 eprintln!(
82 "error parsing '{}': {:?}",
83 &path.display(),
84 base_metadata_opt.err()
85 );
86 return Self::default();
87 }
88
89 let mut base_metadata = base_metadata_opt.unwrap();
90
91 if base_metadata.published.is_some() {
93 match dateparser::parse(&base_metadata.published.clone().unwrap()) {
94 Ok(parsed) => {
95 base_metadata.published = Some(DateTime::<Local>::from(parsed).to_string());
97 base_metadata.last_updated = base_metadata.published.clone();
98 }
99 Err(e) => {
100 eprintln!(
101 "Failed to parse the published date in {}\n{}",
102 &path.display(),
103 e
104 );
105 }
106 }
107 } else {
108 base_metadata.published =
109 Some(DateTime::<Local>::from(file_meta.created().unwrap()).to_string());
110 base_metadata.last_updated =
111 Some(DateTime::<Local>::from(file_meta.modified().unwrap()).to_string());
112 }
113
114 let should_emit = base_metadata.clone().emit;
115
116 Self {
117 content_root,
118 at_path: path.display().to_string(),
119 metadata: base_metadata,
120 markdown: parse_result.content.clone(),
121 emit: should_emit,
122 toc: toc_from_document(parse_result.content.as_str()),
123
124 ..Default::default()
125 }
126 }
127}
128
129#[cfg(test)]
130mod test {
131 use super::*;
132 use pretty_assertions::assert_eq;
133
134 #[test]
135 fn test_document_loading() {
136 let base_path_wd = std::env::current_dir()
137 .unwrap()
138 .as_os_str()
139 .to_os_string()
140 .to_str()
141 .unwrap()
142 .to_string();
143 let base_path = format!("{}/test_fixtures/markdown", base_path_wd);
144 let document = Document::new_from_path(
145 base_path.clone().into(),
146 format!("{}/full_frontmatter.md", &base_path).into(),
147 );
148 let time: DateTime<Local> = Local::now();
149 let expected = BaseMetaData {
150 tags: vec!["1".into()],
151 keywords: vec!["2".into()],
152 title: "test".into(),
153 description: "test".into(),
154 published: Some(time.to_string()),
155 last_updated: Some(time.to_string()),
156 emit: true,
157 excerpt: Some("testing".into()),
158 ..Default::default()
159 };
160
161 assert_eq!(expected.tags, document.metadata.tags);
162 assert_eq!(expected.keywords, document.metadata.keywords);
163 assert_eq!(expected.title, document.metadata.title);
164 assert_eq!(expected.description, document.metadata.description);
165 assert_eq!(expected.excerpt, document.metadata.excerpt);
166 assert_eq!(expected.user, document.metadata.user);
167 assert_eq!(expected.emit, document.metadata.emit);
168 assert_eq!(expected.template, document.metadata.template);
169 assert!(document.metadata.published.is_some());
170 assert!(document.metadata.last_updated.is_some());
171 }
172
173 #[test]
174 fn test_document_loading_with_user_metadata() {
175 let base_path_wd = std::env::current_dir()
176 .unwrap()
177 .as_os_str()
178 .to_os_string()
179 .to_str()
180 .unwrap()
181 .to_string();
182 let base_path = format!("{}/test_fixtures/markdown", base_path_wd);
183 let document = Document::new_from_path(
184 base_path.clone().into(),
185 format!("{}/user_metadata.md", &base_path).into(),
186 );
187 let time: DateTime<Local> = Local::now();
188 let expected = BaseMetaData {
189 tags: vec!["1".into()],
190 keywords: vec!["2".into()],
191 title: "test".into(),
192 description: "test".into(),
193 published: Some(time.to_string()),
194 last_updated: Some(time.to_string()),
195 emit: true,
196 excerpt: Some("testing".into()),
197 user: Map::from([
198 (
199 "author".into(),
200 toml::Value::from("Dave Mackintosh".to_string()),
201 ),
202 ("custom_property".into(), toml::Value::from(123)),
203 ]),
204 ..Default::default()
205 };
206
207 assert_eq!(expected.tags, document.metadata.tags);
208 assert_eq!(expected.keywords, document.metadata.keywords);
209 assert_eq!(expected.title, document.metadata.title);
210 assert_eq!(expected.description, document.metadata.description);
211 assert_eq!(expected.excerpt, document.metadata.excerpt);
212 assert_eq!(expected.user, document.metadata.user);
213 assert_eq!(expected.emit, document.metadata.emit);
214 assert_eq!(expected.template, document.metadata.template);
215 assert!(document.metadata.published.is_some());
216 assert!(document.metadata.last_updated.is_some());
217 }
218}