-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Refactor navigation helpers #465
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,165 +5,230 @@ use serde_json; | |
use handlebars::{Context, Handlebars, Helper, RenderContext, RenderError, Renderable}; | ||
|
||
|
||
// Handlebars helper for navigation | ||
type StringMap = BTreeMap<String, String>; | ||
|
||
pub fn previous(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> { | ||
debug!("[fn]: previous (handlebars helper)"); | ||
/// Target for `find_chapter`. | ||
enum Target { | ||
Previous, | ||
Next, | ||
} | ||
|
||
impl Target { | ||
/// Returns target if found. | ||
fn find(&self, | ||
base_path: &String, | ||
current_path: &String, | ||
current_item: &StringMap, | ||
previous_item: &StringMap, | ||
) -> Result<Option<StringMap>, RenderError> { | ||
match self { | ||
&Target::Next => { | ||
let previous_path = previous_item.get("path").ok_or_else(|| { | ||
RenderError::new("No path found for chapter in JSON data") | ||
})?; | ||
|
||
if previous_path == base_path { | ||
return Ok(Some(current_item.clone())); | ||
} | ||
}, | ||
|
||
&Target::Previous => { | ||
if current_path == base_path { | ||
return Ok(Some(previous_item.clone())); | ||
} | ||
} | ||
} | ||
|
||
Ok(None) | ||
} | ||
} | ||
|
||
fn find_chapter( | ||
rc: &mut RenderContext, | ||
target: Target | ||
) -> Result<Option<StringMap>, RenderError> { | ||
debug!("[*]: Get data from context"); | ||
|
||
let chapters = rc.evaluate_absolute("chapters").and_then(|c| { | ||
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone()) | ||
serde_json::value::from_value::<Vec<StringMap>>(c.clone()) | ||
.map_err(|_| RenderError::new("Could not decode the JSON data")) | ||
})?; | ||
|
||
let current = rc.evaluate_absolute("path")? | ||
.as_str() | ||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? | ||
.replace("\"", ""); | ||
let base_path = rc.evaluate_absolute("path")? | ||
.as_str() | ||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? | ||
.replace("\"", ""); | ||
|
||
let mut previous: Option<StringMap> = None; | ||
|
||
let mut previous: Option<BTreeMap<String, String>> = None; | ||
debug!("[*]: Search for chapter"); | ||
|
||
debug!("[*]: Search for current Chapter"); | ||
// Search for current chapter and return previous entry | ||
for item in chapters { | ||
match item.get("path") { | ||
Some(path) if !path.is_empty() => { | ||
if path == ¤t { | ||
debug!("[*]: Found current chapter"); | ||
if let Some(previous) = previous { | ||
debug!("[*]: Creating BTreeMap to inject in context"); | ||
// Create new BTreeMap to extend the context: 'title' and 'link' | ||
let mut previous_chapter = BTreeMap::new(); | ||
|
||
// Chapter title | ||
previous.get("name") | ||
.ok_or_else(|| { | ||
RenderError::new("No title found for chapter in \ | ||
JSON data") | ||
}) | ||
.and_then(|n| { | ||
previous_chapter.insert("title".to_owned(), json!(n)); | ||
Ok(()) | ||
})?; | ||
|
||
|
||
// Chapter link | ||
previous.get("path") | ||
.ok_or_else(|| { | ||
RenderError::new("No path found for chapter in \ | ||
JSON data") | ||
}) | ||
.and_then(|p| { | ||
Path::new(p).with_extension("html") | ||
.to_str() | ||
.ok_or_else(|| { | ||
RenderError::new("Link could not be \ | ||
converted to str") | ||
}) | ||
.and_then(|p| { | ||
previous_chapter | ||
.insert("link".to_owned(), json!(p.replace("\\", "/"))); | ||
Ok(()) | ||
}) | ||
})?; | ||
|
||
|
||
debug!("[*]: Render template"); | ||
// Render template | ||
_h.template() | ||
.ok_or_else(|| RenderError::new("Error with the handlebars template")) | ||
.and_then(|t| { | ||
let mut local_rc = rc.with_context(Context::wraps(&previous_chapter)?); | ||
t.render(r, &mut local_rc) | ||
})?; | ||
if let Some(previous) = previous { | ||
if let Some(item) = target.find(&base_path, &path, &item, &previous)? { | ||
return Ok(Some(item)); | ||
} | ||
break; | ||
} else { | ||
previous = Some(item.clone()); | ||
} | ||
|
||
previous = Some(item.clone()); | ||
} | ||
_ => continue, | ||
} | ||
} | ||
|
||
Ok(None) | ||
} | ||
|
||
fn render( | ||
_h: &Helper, | ||
r: &Handlebars, | ||
rc: &mut RenderContext, | ||
chapter: &StringMap, | ||
) -> Result<(), RenderError> { | ||
debug!("[*]: Creating BTreeMap to inject in context"); | ||
|
||
let mut context = BTreeMap::new(); | ||
|
||
chapter.get("name") | ||
.ok_or_else(|| RenderError::new("No title found for chapter in JSON data")) | ||
.map(|name| context.insert("title".to_owned(), json!(name)))?; | ||
|
||
chapter.get("path") | ||
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data")) | ||
.and_then(|p| { | ||
Path::new(p).with_extension("html") | ||
.to_str() | ||
.ok_or_else(|| RenderError::new("Link could not be converted to str")) | ||
.map(|p| context.insert("link".to_owned(), json!(p.replace("\\", "/")))) | ||
})?; | ||
|
||
debug!("[*]: Render template"); | ||
|
||
_h.template() | ||
.ok_or_else(|| RenderError::new("Error with the handlebars template")) | ||
.and_then(|t| { | ||
let mut local_rc = rc.with_context(Context::wraps(&context)?); | ||
t.render(r, &mut local_rc) | ||
})?; | ||
|
||
Ok(()) | ||
} | ||
|
||
pub fn previous(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> { | ||
debug!("[fn]: previous (handlebars helper)"); | ||
|
||
if let Some(previous) = find_chapter(rc, Target::Previous)? { | ||
render(_h, r, rc, &previous)?; | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> { | ||
debug!("[fn]: next (handlebars helper)"); | ||
|
||
debug!("[*]: Get data from context"); | ||
let chapters = rc.evaluate_absolute("chapters").and_then(|c| { | ||
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone()) | ||
.map_err(|_| RenderError::new("Could not decode the JSON data")) | ||
})?; | ||
let current = rc.evaluate_absolute("path")? | ||
.as_str() | ||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? | ||
.replace("\"", ""); | ||
|
||
let mut previous: Option<BTreeMap<String, String>> = None; | ||
if let Some(next) = find_chapter(rc, Target::Next)? { | ||
render(_h, r, rc, &next)?; | ||
} | ||
|
||
debug!("[*]: Search for current Chapter"); | ||
// Search for current chapter and return previous entry | ||
for item in chapters { | ||
match item.get("path") { | ||
Some(path) if !path.is_empty() => { | ||
if let Some(previous) = previous { | ||
let previous_path = previous.get("path").ok_or_else(|| { | ||
RenderError::new("No path found for chapter in JSON data") | ||
})?; | ||
|
||
if previous_path == ¤t { | ||
debug!("[*]: Found current chapter"); | ||
debug!("[*]: Creating BTreeMap to inject in context"); | ||
// Create new BTreeMap to extend the context: 'title' and 'link' | ||
let mut next_chapter = BTreeMap::new(); | ||
|
||
item.get("name") | ||
.ok_or_else(|| { | ||
RenderError::new("No title found for chapter in JSON \ | ||
data") | ||
}) | ||
.and_then(|n| { | ||
next_chapter.insert("title".to_owned(), json!(n)); | ||
Ok(()) | ||
})?; | ||
|
||
Path::new(path).with_extension("html") | ||
.to_str() | ||
.ok_or_else(|| { | ||
RenderError::new("Link could not converted \ | ||
to str") | ||
}) | ||
.and_then(|l| { | ||
debug!("[*]: Inserting link: {:?}", l); | ||
// Hack for windows who tends to use `\` as separator instead of `/` | ||
next_chapter.insert("link".to_owned(), json!(l.replace("\\", "/"))); | ||
Ok(()) | ||
})?; | ||
|
||
debug!("[*]: Render template"); | ||
|
||
// Render template | ||
_h.template() | ||
.ok_or_else(|| RenderError::new("Error with the handlebars template")) | ||
.and_then(|t| { | ||
let mut local_rc = rc.with_context(Context::wraps(&next_chapter)?); | ||
t.render(r, &mut local_rc) | ||
})?; | ||
break; | ||
} | ||
} | ||
Ok(()) | ||
} | ||
|
||
previous = Some(item.clone()); | ||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
static TEMPLATE: &'static str = | ||
"{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}"; | ||
|
||
#[test] | ||
fn test_next_previous() { | ||
let data = json!({ | ||
"name": "two", | ||
"path": "two.path", | ||
"chapters": [ | ||
{ | ||
"name": "one", | ||
"path": "one.path" | ||
}, | ||
{ | ||
"name": "two", | ||
"path": "two.path", | ||
}, | ||
{ | ||
"name": "three", | ||
"path": "three.path" | ||
} | ||
] | ||
}); | ||
|
||
let mut h = Handlebars::new(); | ||
h.register_helper("previous", Box::new(previous)); | ||
h.register_helper("next", Box::new(next)); | ||
|
||
assert_eq!( | ||
h.template_render(TEMPLATE, &data).unwrap(), | ||
"one: one.html|three: three.html"); | ||
} | ||
|
||
#[test] | ||
fn test_first() { | ||
let data = json!({ | ||
"name": "one", | ||
"path": "one.path", | ||
"chapters": [ | ||
{ | ||
"name": "one", | ||
"path": "one.path" | ||
}, | ||
{ | ||
"name": "two", | ||
"path": "two.path", | ||
}, | ||
{ | ||
"name": "three", | ||
"path": "three.path" | ||
} | ||
] | ||
}); | ||
|
||
let mut h = Handlebars::new(); | ||
h.register_helper("previous", Box::new(previous)); | ||
h.register_helper("next", Box::new(next)); | ||
|
||
assert_eq!( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I usually break this out into a couple lines. So at the top of the test I'll assign what I expect to a It's definitely a personal taste thing, but it's nice when you can look at the top and figure out what the expected output should be at a glance without needing to look for all |
||
h.template_render(TEMPLATE, &data).unwrap(), | ||
"|two: two.html"); | ||
} | ||
#[test] | ||
fn test_last() { | ||
let data = json!({ | ||
"name": "three", | ||
"path": "three.path", | ||
"chapters": [ | ||
{ | ||
"name": "one", | ||
"path": "one.path" | ||
}, | ||
{ | ||
"name": "two", | ||
"path": "two.path", | ||
}, | ||
{ | ||
"name": "three", | ||
"path": "three.path" | ||
} | ||
] | ||
}); | ||
|
||
_ => continue, | ||
} | ||
} | ||
Ok(()) | ||
let mut h = Handlebars::new(); | ||
h.register_helper("previous", Box::new(previous)); | ||
h.register_helper("next", Box::new(next)); | ||
|
||
assert_eq!( | ||
h.template_render(TEMPLATE, &data).unwrap(), | ||
"two: two.html|"); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How hard would it be to write a test which checks this handlebars helper? Would it be possible to mock up a quick template then run the handlebars helper and assert that the rendered output looks like we expect it to?