1use std::collections::BTreeMap;
2use std::path::Path;
3
4use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable};
5
6use crate::utils;
7use log::{debug, trace};
8use serde_json::json;
9
10type StringMap = BTreeMap<String, String>;
11
12/// Target for `find_chapter`.
13enum Target {
14 Previous,
15 Next,
16}
17
18impl Target {
19 /// Returns target if found.
20 fn find(
21 &self,
22 base_path: &str,
23 current_path: &str,
24 current_item: &StringMap,
25 previous_item: &StringMap,
26 ) -> Result<Option<StringMap>, RenderError> {
27 match *self {
28 Target::Next => {
29 let previous_path = previous_item
30 .get("path")
31 .ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))?;
32
33 if previous_path == base_path {
34 return Ok(Some(current_item.clone()));
35 }
36 }
37
38 Target::Previous => {
39 if current_path == base_path {
40 return Ok(Some(previous_item.clone()));
41 }
42 }
43 }
44
45 Ok(None)
46 }
47}
48
49fn find_chapter(
50 ctx: &Context,
51 rc: &mut RenderContext<'_, '_>,
52 target: Target,
53) -> Result<Option<StringMap>, RenderError> {
54 debug!("Get data from context");
55
56 let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| {
57 serde_json::value::from_value::<Vec<StringMap>>(c.as_json().clone())
58 .map_err(|_| RenderError::new("Could not decode the JSON data"))
59 })?;
60
61 let base_path = rc
62 .evaluate(ctx, "@root/path")?
63 .as_json()
64 .as_str()
65 .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
66 .replace('\"', "");
67
68 if !rc.evaluate(ctx, "@root/is_index")?.is_missing() {
69 // Special case for index.md which may be a synthetic page.
70 // Target::find won't match because there is no page with the path
71 // "index.md" (unless there really is an index.md in SUMMARY.md).
72 match target {
73 Target::Previous => return Ok(None),
74 Target::Next => match chapters
75 .iter()
76 .filter(|chapter| {
77 // Skip things like "spacer"
78 chapter.contains_key("path")
79 })
80 .nth(1)
81 {
82 Some(chapter) => return Ok(Some(chapter.clone())),
83 None => return Ok(None),
84 },
85 }
86 }
87
88 let mut previous: Option<StringMap> = None;
89
90 debug!("Search for chapter");
91
92 for item in chapters {
93 match item.get("path") {
94 Some(path) if !path.is_empty() => {
95 if let Some(previous) = previous {
96 if let Some(item) = target.find(&base_path, path, &item, &previous)? {
97 return Ok(Some(item));
98 }
99 }
100
101 previous = Some(item.clone());
102 }
103 _ => continue,
104 }
105 }
106
107 Ok(None)
108}
109
110fn render(
111 _h: &Helper<'_, '_>,
112 r: &Handlebars<'_>,
113 ctx: &Context,
114 rc: &mut RenderContext<'_, '_>,
115 out: &mut dyn Output,
116 chapter: &StringMap,
117) -> Result<(), RenderError> {
118 trace!("Creating BTreeMap to inject in context");
119
120 let mut context = BTreeMap::new();
121 let base_path = rc
122 .evaluate(ctx, "@root/path")?
123 .as_json()
124 .as_str()
125 .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
126 .replace('\"', "");
127
128 context.insert(
129 "path_to_root".to_owned(),
130 json!(utils::fs::path_to_root(base_path)),
131 );
132
133 chapter
134 .get("name")
135 .ok_or_else(|| RenderError::new("No title found for chapter in JSON data"))
136 .map(|name| context.insert("title".to_owned(), json!(name)))?;
137
138 chapter
139 .get("path")
140 .ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))
141 .and_then(|p| {
142 Path::new(p)
143 .with_extension("html")
144 .to_str()
145 .ok_or_else(|| RenderError::new("Link could not be converted to str"))
146 .map(|p| context.insert("link".to_owned(), json!(p.replace('\\', "/"))))
147 })?;
148
149 trace!("Render template");
150
151 let t = _h
152 .template()
153 .ok_or_else(|| RenderError::new("Error with the handlebars template"))?;
154 let local_ctx = Context::wraps(&context)?;
155 let mut local_rc = rc.clone();
156 t.render(r, &local_ctx, &mut local_rc, out)
157}
158
159pub fn previous(
160 _h: &Helper<'_, '_>,
161 r: &Handlebars<'_>,
162 ctx: &Context,
163 rc: &mut RenderContext<'_, '_>,
164 out: &mut dyn Output,
165) -> Result<(), RenderError> {
166 trace!("previous (handlebars helper)");
167
168 if let Some(previous: BTreeMap) = find_chapter(ctx, rc, Target::Previous)? {
169 render(_h:_h, r, ctx, rc, out, &previous)?;
170 }
171
172 Ok(())
173}
174
175pub fn next(
176 _h: &Helper<'_, '_>,
177 r: &Handlebars<'_>,
178 ctx: &Context,
179 rc: &mut RenderContext<'_, '_>,
180 out: &mut dyn Output,
181) -> Result<(), RenderError> {
182 trace!("next (handlebars helper)");
183
184 if let Some(next: BTreeMap) = find_chapter(ctx, rc, Target::Next)? {
185 render(_h:_h, r, ctx, rc, out, &next)?;
186 }
187
188 Ok(())
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 static TEMPLATE: &str =
196 "{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}";
197
198 #[test]
199 fn test_next_previous() {
200 let data = json!({
201 "name": "two",
202 "path": "two.path",
203 "chapters": [
204 {
205 "name": "one",
206 "path": "one.path"
207 },
208 {
209 "name": "two",
210 "path": "two.path",
211 },
212 {
213 "name": "three",
214 "path": "three.path"
215 }
216 ]
217 });
218
219 let mut h = Handlebars::new();
220 h.register_helper("previous", Box::new(previous));
221 h.register_helper("next", Box::new(next));
222
223 assert_eq!(
224 h.render_template(TEMPLATE, &data).unwrap(),
225 "one: one.html|three: three.html"
226 );
227 }
228
229 #[test]
230 fn test_first() {
231 let data = json!({
232 "name": "one",
233 "path": "one.path",
234 "chapters": [
235 {
236 "name": "one",
237 "path": "one.path"
238 },
239 {
240 "name": "two",
241 "path": "two.path",
242 },
243 {
244 "name": "three",
245 "path": "three.path"
246 }
247 ]
248 });
249
250 let mut h = Handlebars::new();
251 h.register_helper("previous", Box::new(previous));
252 h.register_helper("next", Box::new(next));
253
254 assert_eq!(
255 h.render_template(TEMPLATE, &data).unwrap(),
256 "|two: two.html"
257 );
258 }
259 #[test]
260 fn test_last() {
261 let data = json!({
262 "name": "three",
263 "path": "three.path",
264 "chapters": [
265 {
266 "name": "one",
267 "path": "one.path"
268 },
269 {
270 "name": "two",
271 "path": "two.path",
272 },
273 {
274 "name": "three",
275 "path": "three.path"
276 }
277 ]
278 });
279
280 let mut h = Handlebars::new();
281 h.register_helper("previous", Box::new(previous));
282 h.register_helper("next", Box::new(next));
283
284 assert_eq!(
285 h.render_template(TEMPLATE, &data).unwrap(),
286 "two: two.html|"
287 );
288 }
289}
290