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 pub emit: bool,
25}
26
27#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
28#[serde(default)]
29pub struct BaseMetaData {
30 pub title: String,
31 pub description: String,
32 pub tags: Vec<String>,
33 pub keywords: Vec<String>,
34 pub template: String,
35 pub emit: bool,
36
37 #[serde(flatten)]
38 pub user: HashMap<String, Value>,
39}
40
41impl Default for BaseMetaData {
42 fn default() -> Self {
43 Self {
44 title: Default::default(),
45 tags: Default::default(),
46 description: Default::default(),
47 keywords: Default::default(),
48 template: "default".into(),
49 emit: true,
50 user: Default::default(),
51 }
52 }
53}
54
55impl Document {
56 pub fn new_from_path(path: PathBuf) -> Self {
57 let contents_result = std::fs::read_to_string(&path);
58
59 if contents_result.is_err() {
60 dbg!("error reading file: {}", contents_result.err());
61 panic!("failed to read '{}'", path.display());
62 }
63
64 let matter = Matter::<YAML>::new();
65 let parseable = normalize_line_endings(contents_result.as_ref().unwrap().as_bytes());
66 let parse_result = matter.parse(&parseable);
67 let base_metadata_opt = match parse_result.data {
68 Some(data) => data.deserialize::<BaseMetaData>(),
69 None => Ok(BaseMetaData::default()),
70 };
71
72 if base_metadata_opt.is_err() {
73 eprintln!(
74 "error parsing '{}': {:?}",
75 &path.display(),
76 base_metadata_opt.err()
77 );
78 return Self::default();
79 }
80
81 let base_metadata = base_metadata_opt.unwrap();
82 let should_emit = base_metadata.clone().emit;
83
84 Self {
85 at_path: path.display().to_string(),
86 metadata: base_metadata,
87 markdown: parse_result.content,
88 excerpt: parse_result.excerpt,
89 emit: should_emit,
90 ..Default::default()
91 }
92 }
93}
94
95#[cfg(test)]
96mod test {
97 use super::*;
98 use pretty_assertions::assert_eq;
99
100 #[test]
101 fn test_document_loading() {
102 let base_path_wd = std::env::current_dir()
103 .unwrap()
104 .as_os_str()
105 .to_os_string()
106 .to_str()
107 .unwrap()
108 .to_string();
109 let base_path = format!("{}/test_fixtures/markdown", base_path_wd);
110 let document = Document::new_from_path(format!("{}/full_frontmatter.md", base_path).into());
111
112 assert_eq!(
113 BaseMetaData {
114 tags: vec!["1".into()],
115 keywords: vec!["2".into()],
116 title: "test".into(),
117 description: "test".into(),
118 user: HashMap::new(),
119 emit: true,
120 template: "default".into(),
121 },
122 document.metadata
123 )
124 }
125}