Skip to content

Commit 3d5eb48

Browse files
jacwahMichael-F-Bryan
authored andcommitted
Refactor navigation helpers (#465)
* Refactor navigation helpers * Target::find: take previous_item by reference This makes more sense for find as an interface, though it causes a second clone in some cases. Maybe rustc is smart here? * Test next and previous navigation helpers * Add more next/previous tests
1 parent d56ff94 commit 3d5eb48

File tree

1 file changed

+194
-129
lines changed

1 file changed

+194
-129
lines changed

src/renderer/html_handlebars/helpers/navigation.rs

Lines changed: 194 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -5,165 +5,230 @@ use serde_json;
55
use handlebars::{Context, Handlebars, Helper, RenderContext, RenderError, Renderable};
66

77

8-
// Handlebars helper for navigation
8+
type StringMap = BTreeMap<String, String>;
99

10-
pub fn previous(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
11-
debug!("[fn]: previous (handlebars helper)");
10+
/// Target for `find_chapter`.
11+
enum Target {
12+
Previous,
13+
Next,
14+
}
15+
16+
impl Target {
17+
/// Returns target if found.
18+
fn find(&self,
19+
base_path: &String,
20+
current_path: &String,
21+
current_item: &StringMap,
22+
previous_item: &StringMap,
23+
) -> Result<Option<StringMap>, RenderError> {
24+
match self {
25+
&Target::Next => {
26+
let previous_path = previous_item.get("path").ok_or_else(|| {
27+
RenderError::new("No path found for chapter in JSON data")
28+
})?;
29+
30+
if previous_path == base_path {
31+
return Ok(Some(current_item.clone()));
32+
}
33+
},
34+
35+
&Target::Previous => {
36+
if current_path == base_path {
37+
return Ok(Some(previous_item.clone()));
38+
}
39+
}
40+
}
41+
42+
Ok(None)
43+
}
44+
}
1245

46+
fn find_chapter(
47+
rc: &mut RenderContext,
48+
target: Target
49+
) -> Result<Option<StringMap>, RenderError> {
1350
debug!("[*]: Get data from context");
51+
1452
let chapters = rc.evaluate_absolute("chapters").and_then(|c| {
15-
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
53+
serde_json::value::from_value::<Vec<StringMap>>(c.clone())
1654
.map_err(|_| RenderError::new("Could not decode the JSON data"))
1755
})?;
1856

19-
let current = rc.evaluate_absolute("path")?
20-
.as_str()
21-
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
22-
.replace("\"", "");
57+
let base_path = rc.evaluate_absolute("path")?
58+
.as_str()
59+
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
60+
.replace("\"", "");
61+
62+
let mut previous: Option<StringMap> = None;
2363

24-
let mut previous: Option<BTreeMap<String, String>> = None;
64+
debug!("[*]: Search for chapter");
2565

26-
debug!("[*]: Search for current Chapter");
27-
// Search for current chapter and return previous entry
2866
for item in chapters {
2967
match item.get("path") {
3068
Some(path) if !path.is_empty() => {
31-
if path == &current {
32-
debug!("[*]: Found current chapter");
33-
if let Some(previous) = previous {
34-
debug!("[*]: Creating BTreeMap to inject in context");
35-
// Create new BTreeMap to extend the context: 'title' and 'link'
36-
let mut previous_chapter = BTreeMap::new();
37-
38-
// Chapter title
39-
previous.get("name")
40-
.ok_or_else(|| {
41-
RenderError::new("No title found for chapter in \
42-
JSON data")
43-
})
44-
.and_then(|n| {
45-
previous_chapter.insert("title".to_owned(), json!(n));
46-
Ok(())
47-
})?;
48-
49-
50-
// Chapter link
51-
previous.get("path")
52-
.ok_or_else(|| {
53-
RenderError::new("No path found for chapter in \
54-
JSON data")
55-
})
56-
.and_then(|p| {
57-
Path::new(p).with_extension("html")
58-
.to_str()
59-
.ok_or_else(|| {
60-
RenderError::new("Link could not be \
61-
converted to str")
62-
})
63-
.and_then(|p| {
64-
previous_chapter
65-
.insert("link".to_owned(), json!(p.replace("\\", "/")));
66-
Ok(())
67-
})
68-
})?;
69-
70-
71-
debug!("[*]: Render template");
72-
// Render template
73-
_h.template()
74-
.ok_or_else(|| RenderError::new("Error with the handlebars template"))
75-
.and_then(|t| {
76-
let mut local_rc = rc.with_context(Context::wraps(&previous_chapter)?);
77-
t.render(r, &mut local_rc)
78-
})?;
69+
if let Some(previous) = previous {
70+
if let Some(item) = target.find(&base_path, &path, &item, &previous)? {
71+
return Ok(Some(item));
7972
}
80-
break;
81-
} else {
82-
previous = Some(item.clone());
8373
}
74+
75+
previous = Some(item.clone());
8476
}
8577
_ => continue,
8678
}
8779
}
8880

81+
Ok(None)
82+
}
83+
84+
fn render(
85+
_h: &Helper,
86+
r: &Handlebars,
87+
rc: &mut RenderContext,
88+
chapter: &StringMap,
89+
) -> Result<(), RenderError> {
90+
debug!("[*]: Creating BTreeMap to inject in context");
91+
92+
let mut context = BTreeMap::new();
93+
94+
chapter.get("name")
95+
.ok_or_else(|| RenderError::new("No title found for chapter in JSON data"))
96+
.map(|name| context.insert("title".to_owned(), json!(name)))?;
97+
98+
chapter.get("path")
99+
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))
100+
.and_then(|p| {
101+
Path::new(p).with_extension("html")
102+
.to_str()
103+
.ok_or_else(|| RenderError::new("Link could not be converted to str"))
104+
.map(|p| context.insert("link".to_owned(), json!(p.replace("\\", "/"))))
105+
})?;
106+
107+
debug!("[*]: Render template");
108+
109+
_h.template()
110+
.ok_or_else(|| RenderError::new("Error with the handlebars template"))
111+
.and_then(|t| {
112+
let mut local_rc = rc.with_context(Context::wraps(&context)?);
113+
t.render(r, &mut local_rc)
114+
})?;
115+
89116
Ok(())
90117
}
91118

119+
pub fn previous(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
120+
debug!("[fn]: previous (handlebars helper)");
92121

122+
if let Some(previous) = find_chapter(rc, Target::Previous)? {
123+
render(_h, r, rc, &previous)?;
124+
}
93125

126+
Ok(())
127+
}
94128

95129
pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
96130
debug!("[fn]: next (handlebars helper)");
97131

98-
debug!("[*]: Get data from context");
99-
let chapters = rc.evaluate_absolute("chapters").and_then(|c| {
100-
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
101-
.map_err(|_| RenderError::new("Could not decode the JSON data"))
102-
})?;
103-
let current = rc.evaluate_absolute("path")?
104-
.as_str()
105-
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
106-
.replace("\"", "");
107-
108-
let mut previous: Option<BTreeMap<String, String>> = None;
132+
if let Some(next) = find_chapter(rc, Target::Next)? {
133+
render(_h, r, rc, &next)?;
134+
}
109135

110-
debug!("[*]: Search for current Chapter");
111-
// Search for current chapter and return previous entry
112-
for item in chapters {
113-
match item.get("path") {
114-
Some(path) if !path.is_empty() => {
115-
if let Some(previous) = previous {
116-
let previous_path = previous.get("path").ok_or_else(|| {
117-
RenderError::new("No path found for chapter in JSON data")
118-
})?;
119-
120-
if previous_path == &current {
121-
debug!("[*]: Found current chapter");
122-
debug!("[*]: Creating BTreeMap to inject in context");
123-
// Create new BTreeMap to extend the context: 'title' and 'link'
124-
let mut next_chapter = BTreeMap::new();
125-
126-
item.get("name")
127-
.ok_or_else(|| {
128-
RenderError::new("No title found for chapter in JSON \
129-
data")
130-
})
131-
.and_then(|n| {
132-
next_chapter.insert("title".to_owned(), json!(n));
133-
Ok(())
134-
})?;
135-
136-
Path::new(path).with_extension("html")
137-
.to_str()
138-
.ok_or_else(|| {
139-
RenderError::new("Link could not converted \
140-
to str")
141-
})
142-
.and_then(|l| {
143-
debug!("[*]: Inserting link: {:?}", l);
144-
// Hack for windows who tends to use `\` as separator instead of `/`
145-
next_chapter.insert("link".to_owned(), json!(l.replace("\\", "/")));
146-
Ok(())
147-
})?;
148-
149-
debug!("[*]: Render template");
150-
151-
// Render template
152-
_h.template()
153-
.ok_or_else(|| RenderError::new("Error with the handlebars template"))
154-
.and_then(|t| {
155-
let mut local_rc = rc.with_context(Context::wraps(&next_chapter)?);
156-
t.render(r, &mut local_rc)
157-
})?;
158-
break;
159-
}
160-
}
136+
Ok(())
137+
}
161138

162-
previous = Some(item.clone());
139+
#[cfg(test)]
140+
mod tests {
141+
use super::*;
142+
143+
static TEMPLATE: &'static str =
144+
"{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}";
145+
146+
#[test]
147+
fn test_next_previous() {
148+
let data = json!({
149+
"name": "two",
150+
"path": "two.path",
151+
"chapters": [
152+
{
153+
"name": "one",
154+
"path": "one.path"
155+
},
156+
{
157+
"name": "two",
158+
"path": "two.path",
159+
},
160+
{
161+
"name": "three",
162+
"path": "three.path"
163+
}
164+
]
165+
});
166+
167+
let mut h = Handlebars::new();
168+
h.register_helper("previous", Box::new(previous));
169+
h.register_helper("next", Box::new(next));
170+
171+
assert_eq!(
172+
h.template_render(TEMPLATE, &data).unwrap(),
173+
"one: one.html|three: three.html");
174+
}
175+
176+
#[test]
177+
fn test_first() {
178+
let data = json!({
179+
"name": "one",
180+
"path": "one.path",
181+
"chapters": [
182+
{
183+
"name": "one",
184+
"path": "one.path"
185+
},
186+
{
187+
"name": "two",
188+
"path": "two.path",
189+
},
190+
{
191+
"name": "three",
192+
"path": "three.path"
193+
}
194+
]
195+
});
196+
197+
let mut h = Handlebars::new();
198+
h.register_helper("previous", Box::new(previous));
199+
h.register_helper("next", Box::new(next));
200+
201+
assert_eq!(
202+
h.template_render(TEMPLATE, &data).unwrap(),
203+
"|two: two.html");
204+
}
205+
#[test]
206+
fn test_last() {
207+
let data = json!({
208+
"name": "three",
209+
"path": "three.path",
210+
"chapters": [
211+
{
212+
"name": "one",
213+
"path": "one.path"
214+
},
215+
{
216+
"name": "two",
217+
"path": "two.path",
218+
},
219+
{
220+
"name": "three",
221+
"path": "three.path"
163222
}
223+
]
224+
});
164225

165-
_ => continue,
166-
}
167-
}
168-
Ok(())
226+
let mut h = Handlebars::new();
227+
h.register_helper("previous", Box::new(previous));
228+
h.register_helper("next", Box::new(next));
229+
230+
assert_eq!(
231+
h.template_render(TEMPLATE, &data).unwrap(),
232+
"two: two.html|");
233+
}
169234
}

0 commit comments

Comments
 (0)