weaver_lib/
document.rs

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 there's no published in the base_metadata, we will use the file's created at meta.
92        if base_metadata.published.is_some() {
93            match dateparser::parse(&base_metadata.published.clone().unwrap()) {
94                Ok(parsed) => {
95                    // TODO: Fix the unwraps here.
96                    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}