]>
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; |
9c376795 FG |
7 | use log::{debug, trace}; |
8 | use serde_json::json; | |
7cac9316 | 9 | |
2c00a5a8 | 10 | type StringMap = BTreeMap<String, String>; |
7cac9316 | 11 | |
2c00a5a8 XL |
12 | /// Target for `find_chapter`. |
13 | enum Target { | |
14 | Previous, | |
15 | Next, | |
16 | } | |
17 | ||
18 | impl Target { | |
19 | /// Returns target if found. | |
83c7162d XL |
20 | fn find( |
21 | &self, | |
9fa01778 XL |
22 | base_path: &str, |
23 | current_path: &str, | |
83c7162d XL |
24 | current_item: &StringMap, |
25 | previous_item: &StringMap, | |
26 | ) -> Result<Option<StringMap>, RenderError> { | |
9fa01778 XL |
27 | match *self { |
28 | Target::Next => { | |
83c7162d XL |
29 | let previous_path = previous_item |
30 | .get("path") | |
31 | .ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))?; | |
2c00a5a8 XL |
32 | |
33 | if previous_path == base_path { | |
34 | return Ok(Some(current_item.clone())); | |
35 | } | |
83c7162d | 36 | } |
2c00a5a8 | 37 | |
9fa01778 | 38 | Target::Previous => { |
2c00a5a8 XL |
39 | if current_path == base_path { |
40 | return Ok(Some(previous_item.clone())); | |
41 | } | |
42 | } | |
43 | } | |
44 | ||
45 | Ok(None) | |
46 | } | |
47 | } | |
7cac9316 | 48 | |
9fa01778 XL |
49 | fn find_chapter( |
50 | ctx: &Context, | |
f9f354fc | 51 | rc: &mut RenderContext<'_, '_>, |
9fa01778 XL |
52 | target: Target, |
53 | ) -> Result<Option<StringMap>, RenderError> { | |
2c00a5a8 | 54 | debug!("Get data from context"); |
ea8adc8c | 55 | |
416331ca XL |
56 | let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| { |
57 | serde_json::value::from_value::<Vec<StringMap>>(c.as_json().clone()) | |
2c00a5a8 XL |
58 | .map_err(|_| RenderError::new("Could not decode the JSON data")) |
59 | })?; | |
7cac9316 | 60 | |
9fa01778 | 61 | let base_path = rc |
416331ca XL |
62 | .evaluate(ctx, "@root/path")? |
63 | .as_json() | |
83c7162d XL |
64 | .as_str() |
65 | .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? | |
064997fb | 66 | .replace('\"', ""); |
7cac9316 | 67 | |
e74abb32 XL |
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 | }) | |
f035d41b | 80 | .nth(1) |
e74abb32 XL |
81 | { |
82 | Some(chapter) => return Ok(Some(chapter.clone())), | |
83 | None => return Ok(None), | |
84 | }, | |
85 | } | |
86 | } | |
87 | ||
2c00a5a8 | 88 | let mut previous: Option<StringMap> = None; |
7cac9316 | 89 | |
2c00a5a8 XL |
90 | debug!("Search for chapter"); |
91 | ||
92 | for item in chapters { | |
7cac9316 XL |
93 | match item.get("path") { |
94 | Some(path) if !path.is_empty() => { | |
2c00a5a8 | 95 | if let Some(previous) = previous { |
a2a8927a | 96 | if let Some(item) = target.find(&base_path, path, &item, &previous)? { |
2c00a5a8 | 97 | return Ok(Some(item)); |
7cac9316 | 98 | } |
7cac9316 | 99 | } |
7cac9316 | 100 | |
2c00a5a8 XL |
101 | previous = Some(item.clone()); |
102 | } | |
103 | _ => continue, | |
7cac9316 | 104 | } |
7cac9316 XL |
105 | } |
106 | ||
83c7162d | 107 | Ok(None) |
7cac9316 XL |
108 | } |
109 | ||
2c00a5a8 | 110 | fn render( |
dc9dc135 | 111 | _h: &Helper<'_, '_>, |
f9f354fc | 112 | r: &Handlebars<'_>, |
9fa01778 | 113 | ctx: &Context, |
f9f354fc | 114 | rc: &mut RenderContext<'_, '_>, |
dc9dc135 | 115 | out: &mut dyn Output, |
2c00a5a8 XL |
116 | chapter: &StringMap, |
117 | ) -> Result<(), RenderError> { | |
118 | trace!("Creating BTreeMap to inject in context"); | |
119 | ||
120 | let mut context = BTreeMap::new(); | |
9fa01778 | 121 | let base_path = rc |
416331ca XL |
122 | .evaluate(ctx, "@root/path")? |
123 | .as_json() | |
9fa01778 XL |
124 | .as_str() |
125 | .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? | |
064997fb | 126 | .replace('\"', ""); |
9fa01778 XL |
127 | |
128 | context.insert( | |
129 | "path_to_root".to_owned(), | |
130 | json!(utils::fs::path_to_root(&base_path)), | |
131 | ); | |
2c00a5a8 | 132 | |
83c7162d XL |
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")) | |
064997fb | 146 | .map(|p| context.insert("link".to_owned(), json!(p.replace('\\', "/")))) |
83c7162d | 147 | })?; |
2c00a5a8 XL |
148 | |
149 | trace!("Render template"); | |
150 | ||
9c376795 FG |
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) | |
2c00a5a8 | 157 | } |
7cac9316 | 158 | |
9fa01778 | 159 | pub fn previous( |
dc9dc135 | 160 | _h: &Helper<'_, '_>, |
f9f354fc | 161 | r: &Handlebars<'_>, |
9fa01778 | 162 | ctx: &Context, |
f9f354fc | 163 | rc: &mut RenderContext<'_, '_>, |
dc9dc135 | 164 | out: &mut dyn Output, |
9fa01778 | 165 | ) -> Result<(), RenderError> { |
2c00a5a8 | 166 | trace!("previous (handlebars helper)"); |
7cac9316 | 167 | |
9fa01778 XL |
168 | if let Some(previous) = find_chapter(ctx, rc, Target::Previous)? { |
169 | render(_h, r, ctx, rc, out, &previous)?; | |
2c00a5a8 | 170 | } |
7cac9316 | 171 | |
2c00a5a8 XL |
172 | Ok(()) |
173 | } | |
7cac9316 | 174 | |
9fa01778 | 175 | pub fn next( |
dc9dc135 | 176 | _h: &Helper<'_, '_>, |
f9f354fc | 177 | r: &Handlebars<'_>, |
9fa01778 | 178 | ctx: &Context, |
f9f354fc | 179 | rc: &mut RenderContext<'_, '_>, |
dc9dc135 | 180 | out: &mut dyn Output, |
9fa01778 | 181 | ) -> Result<(), RenderError> { |
2c00a5a8 | 182 | trace!("next (handlebars helper)"); |
7cac9316 | 183 | |
9fa01778 XL |
184 | if let Some(next) = find_chapter(ctx, rc, Target::Next)? { |
185 | render(_h, r, ctx, rc, out, &next)?; | |
2c00a5a8 | 186 | } |
7cac9316 | 187 | |
2c00a5a8 XL |
188 | Ok(()) |
189 | } | |
7cac9316 | 190 | |
2c00a5a8 XL |
191 | #[cfg(test)] |
192 | mod tests { | |
83c7162d | 193 | use super::*; |
2c00a5a8 | 194 | |
dc9dc135 | 195 | static TEMPLATE: &str = |
83c7162d | 196 | "{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}"; |
2c00a5a8 | 197 | |
83c7162d XL |
198 | #[test] |
199 | fn test_next_previous() { | |
200 | let data = json!({ | |
dc9dc135 XL |
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 | }); | |
2c00a5a8 | 218 | |
83c7162d XL |
219 | let mut h = Handlebars::new(); |
220 | h.register_helper("previous", Box::new(previous)); | |
221 | h.register_helper("next", Box::new(next)); | |
2c00a5a8 | 222 | |
83c7162d XL |
223 | assert_eq!( |
224 | h.render_template(TEMPLATE, &data).unwrap(), | |
225 | "one: one.html|three: three.html" | |
226 | ); | |
227 | } | |
2c00a5a8 | 228 | |
83c7162d XL |
229 | #[test] |
230 | fn test_first() { | |
231 | let data = json!({ | |
dc9dc135 XL |
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 | }); | |
2c00a5a8 | 249 | |
83c7162d XL |
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!({ | |
dc9dc135 XL |
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 | }); | |
2c00a5a8 | 279 | |
83c7162d XL |
280 | let mut h = Handlebars::new(); |
281 | h.register_helper("previous", Box::new(previous)); | |
282 | h.register_helper("next", Box::new(next)); | |
2c00a5a8 | 283 | |
83c7162d XL |
284 | assert_eq!( |
285 | h.render_template(TEMPLATE, &data).unwrap(), | |
286 | "two: two.html|" | |
287 | ); | |
288 | } | |
7cac9316 | 289 | } |