]>
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"))? | |
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 | }) | |
78 | .skip(1) | |
79 | .next() | |
80 | { | |
81 | Some(chapter) => return Ok(Some(chapter.clone())), | |
82 | None => return Ok(None), | |
83 | }, | |
84 | } | |
85 | } | |
86 | ||
2c00a5a8 | 87 | let mut previous: Option<StringMap> = None; |
7cac9316 | 88 | |
2c00a5a8 XL |
89 | debug!("Search for chapter"); |
90 | ||
91 | for item in chapters { | |
7cac9316 XL |
92 | match item.get("path") { |
93 | Some(path) if !path.is_empty() => { | |
2c00a5a8 XL |
94 | if let Some(previous) = previous { |
95 | if let Some(item) = target.find(&base_path, &path, &item, &previous)? { | |
96 | return Ok(Some(item)); | |
7cac9316 | 97 | } |
7cac9316 | 98 | } |
7cac9316 | 99 | |
2c00a5a8 XL |
100 | previous = Some(item.clone()); |
101 | } | |
102 | _ => continue, | |
7cac9316 | 103 | } |
7cac9316 XL |
104 | } |
105 | ||
83c7162d | 106 | Ok(None) |
7cac9316 XL |
107 | } |
108 | ||
2c00a5a8 | 109 | fn render( |
dc9dc135 | 110 | _h: &Helper<'_, '_>, |
f9f354fc | 111 | r: &Handlebars<'_>, |
9fa01778 | 112 | ctx: &Context, |
f9f354fc | 113 | rc: &mut RenderContext<'_, '_>, |
dc9dc135 | 114 | out: &mut dyn Output, |
2c00a5a8 XL |
115 | chapter: &StringMap, |
116 | ) -> Result<(), RenderError> { | |
117 | trace!("Creating BTreeMap to inject in context"); | |
118 | ||
119 | let mut context = BTreeMap::new(); | |
9fa01778 | 120 | let base_path = rc |
416331ca XL |
121 | .evaluate(ctx, "@root/path")? |
122 | .as_json() | |
9fa01778 XL |
123 | .as_str() |
124 | .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? | |
125 | .replace("\"", ""); | |
126 | ||
127 | context.insert( | |
128 | "path_to_root".to_owned(), | |
129 | json!(utils::fs::path_to_root(&base_path)), | |
130 | ); | |
2c00a5a8 | 131 | |
83c7162d XL |
132 | chapter |
133 | .get("name") | |
134 | .ok_or_else(|| RenderError::new("No title found for chapter in JSON data")) | |
135 | .map(|name| context.insert("title".to_owned(), json!(name)))?; | |
136 | ||
137 | chapter | |
138 | .get("path") | |
139 | .ok_or_else(|| RenderError::new("No path found for chapter in JSON data")) | |
140 | .and_then(|p| { | |
141 | Path::new(p) | |
142 | .with_extension("html") | |
143 | .to_str() | |
144 | .ok_or_else(|| RenderError::new("Link could not be converted to str")) | |
145 | .map(|p| context.insert("link".to_owned(), json!(p.replace("\\", "/")))) | |
146 | })?; | |
2c00a5a8 XL |
147 | |
148 | trace!("Render template"); | |
149 | ||
150 | _h.template() | |
151 | .ok_or_else(|| RenderError::new("Error with the handlebars template")) | |
152 | .and_then(|t| { | |
f9f354fc | 153 | let mut local_rc = rc.clone(); |
9fa01778 XL |
154 | let local_ctx = Context::wraps(&context)?; |
155 | t.render(r, &local_ctx, &mut local_rc, out) | |
2c00a5a8 | 156 | })?; |
7cac9316 | 157 | |
2c00a5a8 XL |
158 | Ok(()) |
159 | } | |
7cac9316 | 160 | |
9fa01778 | 161 | pub fn previous( |
dc9dc135 | 162 | _h: &Helper<'_, '_>, |
f9f354fc | 163 | r: &Handlebars<'_>, |
9fa01778 | 164 | ctx: &Context, |
f9f354fc | 165 | rc: &mut RenderContext<'_, '_>, |
dc9dc135 | 166 | out: &mut dyn Output, |
9fa01778 | 167 | ) -> Result<(), RenderError> { |
2c00a5a8 | 168 | trace!("previous (handlebars helper)"); |
7cac9316 | 169 | |
9fa01778 XL |
170 | if let Some(previous) = find_chapter(ctx, rc, Target::Previous)? { |
171 | render(_h, r, ctx, rc, out, &previous)?; | |
2c00a5a8 | 172 | } |
7cac9316 | 173 | |
2c00a5a8 XL |
174 | Ok(()) |
175 | } | |
7cac9316 | 176 | |
9fa01778 | 177 | pub fn next( |
dc9dc135 | 178 | _h: &Helper<'_, '_>, |
f9f354fc | 179 | r: &Handlebars<'_>, |
9fa01778 | 180 | ctx: &Context, |
f9f354fc | 181 | rc: &mut RenderContext<'_, '_>, |
dc9dc135 | 182 | out: &mut dyn Output, |
9fa01778 | 183 | ) -> Result<(), RenderError> { |
2c00a5a8 | 184 | trace!("next (handlebars helper)"); |
7cac9316 | 185 | |
9fa01778 XL |
186 | if let Some(next) = find_chapter(ctx, rc, Target::Next)? { |
187 | render(_h, r, ctx, rc, out, &next)?; | |
2c00a5a8 | 188 | } |
7cac9316 | 189 | |
2c00a5a8 XL |
190 | Ok(()) |
191 | } | |
7cac9316 | 192 | |
2c00a5a8 XL |
193 | #[cfg(test)] |
194 | mod tests { | |
83c7162d | 195 | use super::*; |
2c00a5a8 | 196 | |
dc9dc135 | 197 | static TEMPLATE: &str = |
83c7162d | 198 | "{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}"; |
2c00a5a8 | 199 | |
83c7162d XL |
200 | #[test] |
201 | fn test_next_previous() { | |
202 | let data = json!({ | |
dc9dc135 XL |
203 | "name": "two", |
204 | "path": "two.path", | |
205 | "chapters": [ | |
206 | { | |
207 | "name": "one", | |
208 | "path": "one.path" | |
209 | }, | |
210 | { | |
211 | "name": "two", | |
212 | "path": "two.path", | |
213 | }, | |
214 | { | |
215 | "name": "three", | |
216 | "path": "three.path" | |
217 | } | |
218 | ] | |
219 | }); | |
2c00a5a8 | 220 | |
83c7162d XL |
221 | let mut h = Handlebars::new(); |
222 | h.register_helper("previous", Box::new(previous)); | |
223 | h.register_helper("next", Box::new(next)); | |
2c00a5a8 | 224 | |
83c7162d XL |
225 | assert_eq!( |
226 | h.render_template(TEMPLATE, &data).unwrap(), | |
227 | "one: one.html|three: three.html" | |
228 | ); | |
229 | } | |
2c00a5a8 | 230 | |
83c7162d XL |
231 | #[test] |
232 | fn test_first() { | |
233 | let data = json!({ | |
dc9dc135 XL |
234 | "name": "one", |
235 | "path": "one.path", | |
236 | "chapters": [ | |
237 | { | |
238 | "name": "one", | |
239 | "path": "one.path" | |
240 | }, | |
241 | { | |
242 | "name": "two", | |
243 | "path": "two.path", | |
244 | }, | |
245 | { | |
246 | "name": "three", | |
247 | "path": "three.path" | |
248 | } | |
249 | ] | |
250 | }); | |
2c00a5a8 | 251 | |
83c7162d XL |
252 | let mut h = Handlebars::new(); |
253 | h.register_helper("previous", Box::new(previous)); | |
254 | h.register_helper("next", Box::new(next)); | |
255 | ||
256 | assert_eq!( | |
257 | h.render_template(TEMPLATE, &data).unwrap(), | |
258 | "|two: two.html" | |
259 | ); | |
260 | } | |
261 | #[test] | |
262 | fn test_last() { | |
263 | let data = json!({ | |
dc9dc135 XL |
264 | "name": "three", |
265 | "path": "three.path", | |
266 | "chapters": [ | |
267 | { | |
268 | "name": "one", | |
269 | "path": "one.path" | |
270 | }, | |
271 | { | |
272 | "name": "two", | |
273 | "path": "two.path", | |
274 | }, | |
275 | { | |
276 | "name": "three", | |
277 | "path": "three.path" | |
278 | } | |
279 | ] | |
280 | }); | |
2c00a5a8 | 281 | |
83c7162d XL |
282 | let mut h = Handlebars::new(); |
283 | h.register_helper("previous", Box::new(previous)); | |
284 | h.register_helper("next", Box::new(next)); | |
2c00a5a8 | 285 | |
83c7162d XL |
286 | assert_eq!( |
287 | h.render_template(TEMPLATE, &data).unwrap(), | |
288 | "two: two.html|" | |
289 | ); | |
290 | } | |
7cac9316 | 291 | } |