1 | use std::collections::BTreeMap; |
2 | use std::path::Path; |
3 | |
4 | use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable}; |
5 | |
6 | use crate::utils; |
7 | use log::{debug, trace}; |
8 | use serde_json::json; |
9 | |
10 | type StringMap = BTreeMap<String, String>; |
11 | |
12 | /// Target for `find_chapter`. |
13 | enum Target { |
14 | Previous, |
15 | Next, |
16 | } |
17 | |
18 | impl 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 | |
49 | fn 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 | |
110 | fn 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 | |
159 | pub 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 | |
175 | pub 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)] |
192 | mod 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 | |