Skip to content

Commit

Permalink
Merge pull request #103 from sunng87/feature/partial4.0
Browse files Browse the repository at this point in the history
Handlebars 4.0 Partials
  • Loading branch information
sunng87 committed Sep 24, 2016
2 parents 4373426 + c20a6b2 commit a1c8532
Show file tree
Hide file tree
Showing 14 changed files with 911 additions and 169 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ before_script:
script:
- |
cargo test &&
cargo test --features partial4 &&
travis-cargo --only nightly test -- --no-default-features
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ default = ["rustc_ser_type"]
rustc_ser_type = ["rustc-serialize"]
serde_type = ["serde", "serde_json"]
unstable = ["serde_type", "serde_macros"]
partial4 = []

[dev-dependencies]
env_logger = "^0.3.2"
Expand Down
80 changes: 80 additions & 0 deletions src/directives/inline.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use directives::DirectiveDef;
use registry::Registry;
use context::Context;
use render::{RenderError, RenderContext, Directive};

#[derive(Clone, Copy)]
pub struct InlineDirective;

#[cfg(all(feature = "rustc_ser_type", not(feature = "serde_type")))]
fn get_name<'a>(d: &'a Directive) -> Result<&'a str, RenderError> {
d.param(0)
.ok_or_else(|| RenderError::new("Param required for directive \"inline\""))
.and_then(|v| {
v.value()
.as_string()
.ok_or_else(|| RenderError::new("inline name must be string"))
})
}

#[cfg(feature = "serde_type")]
fn get_name<'a>(d: &'a Directive) -> Result<&'a str, RenderError> {
d.param(0)
.ok_or_else(|| RenderError::new("Param required for directive \"inline\""))
.and_then(|v| {
v.value()
.as_str()
.ok_or_else(|| RenderError::new("inline name must be string"))
})
}

impl DirectiveDef for InlineDirective {
fn call(&self,
_: &Context,
d: &Directive,
_: &Registry,
rc: &mut RenderContext)
-> Result<(), RenderError> {
let name = try!(get_name(d));

let template = try!(d.template()
.ok_or_else(|| RenderError::new("inline should have a block")));


rc.set_partial(name.to_owned(), template.clone());
Ok(())
}
}

pub static INLINE_DIRECTIVE: InlineDirective = InlineDirective;

#[cfg(test)]
mod test {
use template::Template;
use registry::Registry;
use context::Context;
use render::{RenderContext, Evalable};
use support::str::StringWriter;

#[cfg(all(feature = "rustc_ser_type", not(feature = "serde_type")))]
use serialize::json::Json;
#[cfg(feature = "serde_type")]
use serde_json::value::Value as Json;

#[test]
fn test_inline() {
let t0 =
Template::compile("{{#*inline \"hello\"}}the hello world inline partial.{{/inline}}"
.to_string())
.ok()
.unwrap();

let hbs = Registry::new();

let mut sw = StringWriter::new();
let mut rc = RenderContext::new(&mut sw);
t0.elements[0].eval(&Context::wraps(&Json::Null), &hbs, &mut rc).unwrap();

assert!(rc.get_partial(&"hello".to_owned()).is_some());
}
}
26 changes: 26 additions & 0 deletions src/directives/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use render::{RenderContext, RenderError, Directive};
use registry::Registry;
use context::Context;

pub use self::inline::INLINE_DIRECTIVE;

/// Directive Definition
///
/// Implement this trait to define your own decorators or directives
pub trait DirectiveDef: Send + Sync {
fn call(&self,
ctx: &Context,
d: &Directive,
r: &Registry,
rc: &mut RenderContext)
-> Result<(), RenderError>;
}

/// implement DirectiveDef for bare function so we can use function as directive
impl<F: Send + Sync + for<'a, 'b, 'c, 'd, 'e> Fn(&'a Context, &'b Directive, &'c Registry, &'d mut RenderContext) -> Result<(), RenderError>> DirectiveDef for F {
fn call(&self, ctx: &Context, d: &Directive, r: &Registry, rc: &mut RenderContext) -> Result<(), RenderError>{
(*self)(ctx, d, r, rc)
}
}

mod inline;
5 changes: 5 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ quick_error! {
open, closed, line_no, col_no)
description("wrong name of closing helper")
}
MismatchingClosedDirective(line_no: usize, col_no: usize, open: String, closed: String) {
display("directive {:?} was opened, but {:?} is closing at line {:?}, column {:?}",
open, closed, line_no, col_no)
description("wrong name of closing directive")
}
InvalidSyntax (line_no: usize, col_no: usize) {
display("invalid handlebars syntax at line {:?}, column {:?}", line_no, col_no)
description("invalid handlebars syntax")
Expand Down
189 changes: 186 additions & 3 deletions src/grammar.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use pest::prelude::*;

#[cfg(not(feature="partial4"))]
impl_rdp! {
grammar! {
whitespace = _{ [" "]|["\t"]|["\n"]|["\r"] }
Expand All @@ -25,7 +26,7 @@ impl_rdp! {
object_literal = { ["{"] ~ (string_literal ~ [":"] ~ literal)? ~ ([","] ~ string_literal ~ [":"] ~ literal)* ~ ["}"] }

// FIXME: a[0], a["b]
symbol_char = _{ ['a'..'z']|['A'..'Z']|['0'..'9']|["_"]|["."]|["@"]|["$"]|["<"]|[">"]|["-"] }
symbol_char = _{ ['a'..'z']|['A'..'Z']|['0'..'9']|["_"]|["."]|["@"]|["$"]|["-"]|["<"]|[">"] }
path_char = _{ ["/"] }

identifier = @{ symbol_char ~ ( symbol_char | path_char )* }
Expand All @@ -51,14 +52,150 @@ impl_rdp! {
helper_expression = { !invert_tag ~ ["{{"] ~ pre_whitespace_omitter? ~ exp_line ~
pro_whitespace_omitter? ~ ["}}"] }

directive_expression = { ["{{"] ~ pre_whitespace_omitter? ~ ["*"] ~ exp_line ~
pro_whitespace_omitter? ~ ["}}"] }
partial_expression = { ["{{"] ~ pre_whitespace_omitter? ~ [">"] ~ exp_line ~
pro_whitespace_omitter? ~ ["}}"] }

invert_tag = { ["{{else}}"]|["{{^}}"] }
helper_block_start = { ["{{"] ~ pre_whitespace_omitter? ~ ["#"] ~ exp_line ~
pro_whitespace_omitter? ~ ["}}"] }
helper_block_end = { ["{{"] ~ pre_whitespace_omitter? ~ ["/"] ~ name ~
pro_whitespace_omitter? ~ ["}}"] }
helper_block = _{ helper_block_start ~ template ~
(invert_tag ~ template)? ~
helper_block_end }

directive_block_start = { ["{{"] ~ pre_whitespace_omitter? ~ ["#"] ~ ["*"] ~ exp_line ~
pro_whitespace_omitter? ~ ["}}"] }
directive_block_end = { ["{{"] ~ pre_whitespace_omitter? ~ ["/"] ~ name ~
pro_whitespace_omitter? ~ ["}}"] }
directive_block = _{ directive_block_start ~ template ~
directive_block_end }

partial_block_start = { ["{{"] ~ pre_whitespace_omitter? ~ ["#"] ~ [">"] ~ exp_line ~
pro_whitespace_omitter? ~ ["}}"] }
partial_block_end = { ["{{"] ~ pre_whitespace_omitter? ~ ["/"] ~ name ~
pro_whitespace_omitter? ~ ["}}"] }
partial_block = _{ partial_block_start ~ template ~ partial_block_end }

raw_block_start = { ["{{{{"] ~ pre_whitespace_omitter? ~ exp_line ~
pro_whitespace_omitter? ~ ["}}}}"] }
raw_block_end = { ["{{{{"] ~ pre_whitespace_omitter? ~ ["/"] ~ name ~
pro_whitespace_omitter? ~ ["}}}}"] }
raw_block = _{ raw_block_start ~ raw_block_text ~ raw_block_end }

hbs_comment = { ["{{!"] ~ (!["}}"] ~ any)* ~ ["}}"] }

template = { (
raw_text |
expression |
html_expression |
helper_expression |
helper_block |
raw_block |
hbs_comment |
directive_expression |
directive_block )*
}

parameter = _{ param ~ eoi }
handlebars = _{ template ~ eoi }

// json path visitor
path_ident = _{ ['a'..'z']|['A'..'Z']|['0'..'9']|["_"]|["@"]|["$"]|["<"]|[">"]|["-"]}
path_id = { path_ident+ }
path_num_id = { ['0'..'9']+ }
path_raw_id = { path_ident* }
path_this = { ["this"] }
path_sep = _{ ["/"] | ["."] }
path_up = { [".."] }
path_var = { path_id }
path_key = { ["["] ~ (["\""]|["'"])? ~ path_raw_id ~ (["\""]|["'"])? ~ ["]"] }
path_idx = { ["["] ~ path_num_id ~ ["]"]}
path_item = _{ path_this|path_up|path_var }
path = _{ ["./"]? ~ path_item ~ ((path_sep ~ path_item) | (path_sep? ~ (path_key | path_idx)))* ~ eoi }
}
}

#[cfg(feature="partial4")]
impl_rdp! {
grammar! {
whitespace = _{ [" "]|["\t"]|["\n"]|["\r"] }

raw_text = @{ ( !["{{"] ~ any )+ }
raw_block_text = @{ ( !["{{{{"] ~ any )* }

// Note: this is not full and strict json literal definition, just for tokenize string,
// array and object types which may contains whitespace. We will use a real json parser
// for real json processing
literal = { string_literal |
array_literal |
object_literal |
number_literal |
null_literal |
boolean_literal }

null_literal = { ["null"] }
boolean_literal = { ["true"]|["false"] }
number_literal = @{ ["-"]? ~ ['0'..'9']+ ~ ["."]? ~ ['0'..'9']* ~ (["E"] ~ ["-"]? ~ ['0'..'9']+)? }
string_literal = @{ ["\""] ~ (!["\""] ~ (["\\\""] | any))* ~ ["\""] }
array_literal = { ["["] ~ literal? ~ ([","] ~ literal)* ~ ["]"] }
object_literal = { ["{"] ~ (string_literal ~ [":"] ~ literal)? ~ ([","] ~ string_literal ~ [":"] ~ literal)* ~ ["}"] }

// FIXME: a[0], a["b]
symbol_char = _{ ['a'..'z']|['A'..'Z']|['0'..'9']|["_"]|["."]|["@"]|["$"]|["-"] }
path_char = _{ ["/"] }

identifier = @{ symbol_char ~ ( symbol_char | path_char )* }
reference = @{ identifier ~ (["["] ~ (string_literal|['0'..'9']+) ~ ["]"])* ~ ["-"]* ~ reference* }
name = _{ subexpression | reference }

param = { !["as"] ~ (literal | reference | subexpression) }
hash = { identifier ~ ["="] ~ param }
block_param = { ["as"] ~ ["|"] ~ identifier ~ identifier? ~ ["|"]}
exp_line = _{ identifier ~ (hash|param)* ~ block_param?}

subexpression = { ["("] ~ name ~ (hash|param)* ~ [")"] }

pre_whitespace_omitter = { ["~"] }
pro_whitespace_omitter = { ["~"] }

expression = { !invert_tag ~ ["{{"] ~ pre_whitespace_omitter? ~ name ~
pro_whitespace_omitter? ~ ["}}"] }

html_expression = { ["{{{"] ~ pre_whitespace_omitter? ~ name ~
pro_whitespace_omitter? ~ ["}}}"] }

helper_expression = { !invert_tag ~ ["{{"] ~ pre_whitespace_omitter? ~ exp_line ~
pro_whitespace_omitter? ~ ["}}"] }

directive_expression = { ["{{"] ~ pre_whitespace_omitter? ~ ["*"] ~ exp_line ~
pro_whitespace_omitter? ~ ["}}"] }
partial_expression = { ["{{"] ~ pre_whitespace_omitter? ~ [">"] ~ exp_line ~
pro_whitespace_omitter? ~ ["}}"] }

invert_tag = { ["{{else}}"]|["{{^}}"] }
helper_block_start = { ["{{"] ~ pre_whitespace_omitter? ~ ["#"] ~ exp_line ~
pro_whitespace_omitter? ~ ["}}"] }
helper_block_end = { ["{{"] ~ pre_whitespace_omitter? ~ ["/"] ~ name ~
pro_whitespace_omitter? ~ ["}}"] }
helper_block = _{ helper_block_start ~ template ~
(invert_tag ~ template)? ~
helper_block_end }
helper_block_end }

directive_block_start = { ["{{"] ~ pre_whitespace_omitter? ~ ["#"] ~ ["*"] ~ exp_line ~
pro_whitespace_omitter? ~ ["}}"] }
directive_block_end = { ["{{"] ~ pre_whitespace_omitter? ~ ["/"] ~ name ~
pro_whitespace_omitter? ~ ["}}"] }
directive_block = _{ directive_block_start ~ template ~
directive_block_end }

partial_block_start = { ["{{"] ~ pre_whitespace_omitter? ~ ["#"] ~ [">"] ~ exp_line ~
pro_whitespace_omitter? ~ ["}}"] }
partial_block_end = { ["{{"] ~ pre_whitespace_omitter? ~ ["/"] ~ name ~
pro_whitespace_omitter? ~ ["}}"] }
partial_block = _{ partial_block_start ~ template ~ partial_block_end }

raw_block_start = { ["{{{{"] ~ pre_whitespace_omitter? ~ exp_line ~
pro_whitespace_omitter? ~ ["}}}}"] }
Expand All @@ -75,7 +212,11 @@ impl_rdp! {
helper_expression |
helper_block |
raw_block |
hbs_comment )*
hbs_comment |
directive_expression |
directive_block |
partial_expression |
partial_block )*
}

parameter = _{ param ~ eoi }
Expand Down Expand Up @@ -331,3 +472,45 @@ fn test_path() {
assert!(rdp.end());
}
}

#[test]
fn test_directive_expression() {
let s = vec!["{{* ssh}}", "{{~* ssh}}"];
for i in s.iter() {
let mut rdp = Rdp::new(StringInput::new(i));
assert!(rdp.directive_expression());
assert!(rdp.end());
}
}

#[test]
fn test_directive_block() {
let s = vec!["{{#* inline}}something{{/inline}}",
"{{~#* inline}}hello{{/inline}}",
"{{#* inline \"partialname\"}}something{{/inline}}"];
for i in s.iter() {
let mut rdp = Rdp::new(StringInput::new(i));
assert!(rdp.directive_block());
assert!(rdp.end());
}
}

#[test]
fn test_partial_expression() {
let s = vec!["{{> hello}}"];
for i in s.iter() {
let mut rdp = Rdp::new(StringInput::new(i));
assert!(rdp.partial_expression());
assert!(rdp.end());
}
}

#[test]
fn test_partial_block() {
let s = vec!["{{#> hello}}nice{{/hello}}"];
for i in s.iter() {
let mut rdp = Rdp::new(StringInput::new(i));
assert!(rdp.partial_block());
assert!(rdp.end());
}
}
Loading

0 comments on commit a1c8532

Please sign in to comment.