1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
|
use std::str::FromStr;
/// The content part of a file that fills in a template.
pub struct Templated {
pub frontmatter: Frontmatter,
pub content: String,
}
impl FromStr for Templated {
type Err = TemplateError;
fn from_str(raw: &str) -> Result<Self, Self::Err> {
tracing::trace!("parsing Templated");
let (front, content) = match raw.strip_prefix("---\n") {
None => return Err(TemplateError::MissingFrontmatter),
Some(no_start) => match no_start.split_once("\n---\n") {
None => return Err(TemplateError::MissingFrontmatter),
Some((front, content)) => (front, content),
},
};
tracing::trace!("isolated frontmatter");
Ok(Self {
frontmatter: front.parse()?,
content: content.to_owned(),
})
}
}
pub struct Frontmatter {
entries: Vec<(String, String)>,
}
impl FromStr for Frontmatter {
type Err = TemplateError;
fn from_str(raw: &str) -> Result<Self, Self::Err> {
let mut entries = vec![];
for line in raw.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
// Skip over blank lines and comments
continue;
}
let (key, value) = match trimmed.split_once('=') {
None => {
return Err(TemplateError::MalformedFrontmatterEntry {
line: trimmed.to_owned(),
});
}
Some(tup) => tup,
};
entries.push((key.trim().to_owned(), value.trim().to_owned()));
tracing::trace!("pushed frontmatter entry {} successfully", key.trim());
}
Ok(Self { entries })
}
}
impl Frontmatter {
pub fn get(&self, key: &str) -> Option<&str> {
self.entries
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
}
pub fn get_many(&self, key: &str) -> Vec<&str> {
// this could probably be a filter_map() but the explicit filter and
// then map seems cleaner
self.entries
.iter()
.filter(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
.collect()
}
}
#[derive(Debug, snafu::Snafu)]
pub enum TemplateError {
#[snafu(display("file is missing the frontmatter"))]
MissingFrontmatter,
#[snafu(display("the frontmatter entry is malformed: {line}"))]
MalformedFrontmatterEntry { line: String },
}
#[cfg(test)]
mod test {
use std::str::FromStr;
use crate::templated::{Frontmatter, Templated};
#[test]
fn frontmatter_parse() {
let frntmtr = "key=value\nkey2=value2";
assert!(Frontmatter::from_str(frntmtr).is_ok())
}
#[test]
fn frontmatter_fails_on_invalid_line() {
let front = "key1=value1\nincorrect line";
assert!(Frontmatter::from_str(front).is_err())
}
#[test]
fn templated_parse() {
let tempalted = "---\ntitle=Title!\n---\nContent line!";
assert!(Templated::from_str(tempalted).is_ok())
}
#[test]
fn templated_doesnt_eat_frontmatter_line() {
let templated = "---\ntitle=Title---\nContent";
assert!(Templated::from_str(templated).is_err());
}
#[test]
fn templated_parses_correct() {
let raw = "---\ntitle=Title!\n---\n<p>Paragraph!</p>";
let tmpl = Templated::from_str(raw).unwrap();
assert_eq!(tmpl.frontmatter.get("title").unwrap(), "Title!");
assert_eq!(tmpl.content, "<p>Paragraph!</p>")
}
}
|