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