Echo Writes Code

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
use crate::errors::{ Error, Result };

use regex::{ Regex, RegexBuilder };

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_path: &str) -> Result<Page> {
		let mut load_path = self.root.clone();
		load_path.push(format!("{}.md", page_path));

		let load_path = load_path.canonicalize()
			.or_else(|e| {
				tracing::error!("Failed to canonicalize path (query: {}, filesystem: {})", page_path, load_path.display());
				Err(e)
			})?;

		if !load_path.starts_with(&self.root) {
			return Err(Error::PageNotFound(page_path.to_string()));
		}

		self.load_page(load_path)
	}

	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 Page {
	pub(crate) title: String,
	pub(crate) content: String,
}