1use gray_matter::{Matter, engine::YAML};
2use serde::{Deserialize, Serialize};
3use std::{collections::HashMap, path::PathBuf};
4use toml::Value;
5
6use crate::{document_toc::toc_from_document, normalize_line_endings};
7
8#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone)]
9pub struct Heading {
10 pub depth: u8,
11 pub text: String,
12 pub slug: String,
13}
14
15#[derive(Debug, Serialize, Deserialize, Default, Clone)]
16pub struct Document {
17 pub at_path: String,
18 pub metadata: BaseMetaData,
19 pub markdown: String,
20 pub excerpt: Option<String>,
21 pub html: Option<String>,
22 pub toc: Vec<Heading>,
23 pub emit: bool,
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 pub emit: bool,
35
36 #[serde(flatten)]
37 pub user: HashMap<String, Value>,
38}
39
40impl Default for BaseMetaData {
41 fn default() -> Self {
42 Self {
43 title: Default::default(),
44 tags: Default::default(),
45 description: Default::default(),
46 keywords: Default::default(),
47 template: "default".into(),
48 emit: true,
49 user: Default::default(),
50 }
51 }
52}
53
54impl Document {
55 pub fn new_from_path(path: PathBuf) -> Self {
56 let contents_result = std::fs::read_to_string(&path);
57
58 if contents_result.is_err() {
59 dbg!("error reading file: {}", contents_result.err());
60 panic!("failed to read '{}'", path.display());
61 }
62
63 let matter = Matter::<YAML>::new();
64 let parseable = normalize_line_endings(contents_result.as_ref().unwrap().as_bytes());
65 let parse_result = matter.parse(&parseable);
66 let base_metadata_opt = match parse_result.data {
67 Some(data) => data.deserialize::<BaseMetaData>(),
68 None => Ok(BaseMetaData::default()),
69 };
70
71 if base_metadata_opt.is_err() {
72 eprintln!(
73 "error parsing '{}': {:?}",
74 &path.display(),
75 base_metadata_opt.err()
76 );
77 return Self::default();
78 }
79
80 let base_metadata = base_metadata_opt.unwrap();
81 let should_emit = base_metadata.clone().emit;
82
83 Self {
84 at_path: path.display().to_string(),
85 metadata: base_metadata,
86 markdown: parse_result.content.clone(),
87 excerpt: parse_result.excerpt,
88 emit: should_emit,
89 toc: toc_from_document(parse_result.content.as_str()),
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}