content.rs
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
use crate::errors::{ Error, Result };
use regex::{ Regex, RegexBuilder };
use std::ffi::{ OsStr };
use std::fs;
use std::path::{ Path, PathBuf };
pub(crate) struct ContentProvider {
root: PathBuf,
title_regex: Regex,
}
impl ContentProvider {
pub(crate) fn new<P: AsRef<Path>>(root: P) -> Result<ContentProvider> {
let title_regex = RegexBuilder::new(r"^\s*#(?<title>[^#].*)$")
.crlf(true)
.multi_line(true)
.build()?;
Ok(ContentProvider {
root: root.as_ref().to_path_buf(),
title_regex,
})
}
pub(crate) fn find_page(&self, page_name: &str) -> Result<Page> {
for page_metadata in self.pages()? {
let page_metadata = page_metadata?;
if page_metadata.0 == page_name {
return self.load_page(page_metadata.1);
}
}
Err(Error::PageNotFound(page_name.to_string()))
}
pub(crate) fn pages(&self) -> Result<PageIterator> {
let directory_iterator = fs::read_dir(&self.root)?;
Ok(PageIterator(directory_iterator))
}
fn load_page<P: AsRef<Path>>(&self, path: P) -> Result<Page> {
let content = fs::read_to_string(path.as_ref())?;
// To keep things as simple as possible for authoring, we enforce exactly one top-level heading
// (`#`), issuing an error if it is either missing or if there is more than one. This single
// heading is used as the page title, which should be the only h1 on the page in order to make
// our document outline as accessible as possible.
let mut h1_matches = self.title_regex.captures_iter(&content);
let first_h1 = match h1_matches.next() {
Some(m) => m,
None => return Err(Error::MissingPageTitle(path.as_ref().to_path_buf()))
};
if h1_matches.count() > 0 {
return Err(Error::AmbiguousPageTitle(path.as_ref().to_path_buf()))
}
let title = first_h1["title"].trim().to_string();
let parser = pulldown_cmark::Parser::new(&content);
let mut content = String::new();
pulldown_cmark::html::push_html(&mut content, parser);
Ok(Page {
title,
content,
})
}
}
pub(crate) struct PageIterator(fs::ReadDir);
impl Iterator for PageIterator {
type Item = Result<(String, PathBuf)>;
fn next(&mut self) -> Option<Result<(String, PathBuf)>> {
let (page_name, page_path) = loop {
let path = match self.0.next()? {
Ok(entry) => entry.path(),
Err(e) => return Some(Err(e.into())),
};
// Pages have to be files
if !path.is_file() {
continue;
}
// Pages have to end in .md or .markdown
if path.extension() != Some(OsStr::new("md")) && path.extension() != Some(OsStr::new("markdown")) {
continue;
}
let Some(page_name) = path.file_stem() else {
return Some(Err(Error::CannotDeterminePageName(path.to_path_buf())));
};
let Some(page_name) = page_name.to_str() else {
return Some(Err(Error::NonUtf8PageName(path.to_path_buf())));
};
let page_path = path.to_path_buf();
break (page_name.to_string(), page_path);
};
Some(Ok((page_name, page_path)))
}
}
pub(crate) struct Page {
pub(crate) title: String,
pub(crate) content: String,
}