Skip to content

Commit c0a34a9

Browse files
committed
Include helper and reduce the breach between Handlebars.js/Handlebars.java during the evaluation of partials
* See jknack/handlebars.java#140 * See handlebars-lang/handlebars.js#368 * See handlebars-lang/handlebars.js#182
1 parent 6025f5f commit c0a34a9

File tree

5 files changed

+149
-70
lines changed

5 files changed

+149
-70
lines changed

handlebars/src/main/antlr4/com/github/jknack/handlebars/internal/HbsLexer.g4

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ ELSE
185185
QID
186186
:
187187
'../' QID
188+
| '..'
188189
| '.'
189190
| '[' ID ']' ID_SEPARATOR QID
190191
| '[' ID ']'

handlebars/src/main/java/com/github/jknack/handlebars/Context.java

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import java.util.regex.Matcher;
2929
import java.util.regex.Pattern;
3030

31+
import com.github.jknack.handlebars.context.MapValueResolver;
32+
3133
/**
3234
* Mustache/Handlebars are contextual template engines. This class represent the
3335
* 'context stack' of a template.
@@ -53,9 +55,14 @@ public class Context {
5355
private static final String PATH_SEPARATOR = "./";
5456

5557
/**
56-
* Handlebars 'parent' reference.
58+
* Handlebars 'parent' attribute reference.
59+
*/
60+
private static final String PARENT_ATTR = "../";
61+
62+
/**
63+
* Handlebars 'parent' attribute reference.
5764
*/
58-
private static final String PARENT = "../";
65+
private static final String PARENT = "..";
5966

6067
/**
6168
* Handlebars 'this' reference.
@@ -342,6 +349,18 @@ public Context data(final String name, final Object value) {
342349
return this;
343350
}
344351

352+
/**
353+
* Store the map in the data storage.
354+
*
355+
* @param attributes The attributes to add. Required.
356+
* @return This context.
357+
*/
358+
public Context data(final Map<String, ?> attributes) {
359+
notNull(attributes, "The attributes are required.");
360+
data.putAll(attributes);
361+
return this;
362+
}
363+
345364
/**
346365
* Resolved as '.' or 'this' inside templates.
347366
*
@@ -351,6 +370,15 @@ public Object model() {
351370
return model;
352371
}
353372

373+
/**
374+
* The parent context or null.
375+
*
376+
* @return The parent context or null.
377+
*/
378+
public Context parent() {
379+
return parent;
380+
}
381+
354382
/**
355383
* List all the properties and values for the given object.
356384
*
@@ -394,26 +422,46 @@ public Set<Entry<String, Object>> propertySet() {
394422
* value is found.
395423
*/
396424
public Object get(final String key) {
425+
// '.' or 'this'
397426
if (MUSTACHE_THIS.equals(key) || THIS.equals(key)) {
398427
return model;
399428
}
400-
if (key.startsWith(PARENT)) {
401-
return parent == null ? null : parent.get(key.substring(PARENT.length()));
429+
// '..'
430+
if (key.equals(PARENT)) {
431+
return parent == null ? null : parent.model;
432+
}
433+
// '../'
434+
if (key.startsWith(PARENT_ATTR)) {
435+
return parent == null ? null : parent.get(key.substring(PARENT_ATTR.length()));
402436
}
403437
String[] path = toPath(key);
404438
Object value = get(path);
405439
if (value == null) {
406440
// No luck, check the extended context.
407441
value = get(extendedContext, key);
442+
// No luck, check the data context.
443+
if (value == null && data != null) {
444+
String dataKey = key.charAt(0) == '@' ? key.substring(1) : key;
445+
// simple data keys will be resolved immediately, complex keys need to go down and using a
446+
// new context.
447+
value = data.get(dataKey);
448+
if (value == null && path.length > 1) {
449+
// for complex keys, a new data context need to be created per invocation,
450+
// bc data might changes per execution.
451+
Context dataContext = Context.newBuilder(data).resolver(MapValueResolver.INSTANCE)
452+
.build();
453+
// don't extend the lookup further.
454+
dataContext.data = null;
455+
value = dataContext.get(dataKey);
456+
// destroy it!
457+
dataContext.destroy();
458+
}
459+
}
408460
// No luck, but before checking at the parent scope we need to check for
409461
// the 'this' qualifier. If present, no look up will be done.
410462
if (value == null && !path[0].equals(THIS)) {
411463
value = get(parent, key);
412464
}
413-
// See at the data context
414-
if (value == null && data != null) {
415-
value = data.get(key.startsWith("@") ? key.substring(1) : key);
416-
}
417465
}
418466
return value == NULL ? null : value;
419467
}

handlebars/src/main/java/com/github/jknack/handlebars/helper/EachHelper.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ private CharSequence hashContext(final Object context, final Options options)
8080
throws IOException {
8181
Set<Entry<String, Object>> propertySet = options.propertySet(context);
8282
StringBuilder buffer = new StringBuilder();
83-
Context parent = options.wrap(context);
83+
Context parent = options.context;
8484
for (Entry<String, Object> entry : propertySet) {
8585
Context current = Context.newContext(parent, entry.getValue())
8686
.data("key", entry.getKey());
@@ -105,8 +105,8 @@ private CharSequence iterableContext(final Iterable<Object> context, final Optio
105105
} else {
106106
Iterator<Object> iterator = context.iterator();
107107
int index = 0;
108+
Context parent = options.context;
108109
while (iterator.hasNext()) {
109-
Context parent = options.wrap(context);
110110
Object element = iterator.next();
111111
boolean first = index == 0;
112112
boolean even = index % 2 == 0;

handlebars/src/main/java/com/github/jknack/handlebars/helper/IncludeHelper.java

Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,46 +17,35 @@
1717
*/
1818
package com.github.jknack.handlebars.helper;
1919

20-
import com.github.jknack.handlebars.Context;
20+
import java.io.IOException;
21+
import java.net.URI;
22+
2123
import com.github.jknack.handlebars.Handlebars;
2224
import com.github.jknack.handlebars.Helper;
2325
import com.github.jknack.handlebars.Options;
2426
import com.github.jknack.handlebars.Template;
2527

26-
import java.io.IOException;
27-
import java.net.URI;
28-
import java.util.Map;
29-
3028
/**
3129
* Allows to include partials with custom context.
3230
* This is a port of https://github.com/wycats/handlebars.js/pull/368
3331
*/
3432
public class IncludeHelper implements Helper<String> {
35-
/**
36-
* A singleton instance of this helper.
37-
*/
38-
public static final Helper<String> INSTANCE = new IncludeHelper();
33+
/**
34+
* A singleton instance of this helper.
35+
*/
36+
public static final Helper<String> INSTANCE = new IncludeHelper();
3937

40-
/**
41-
* The helper's name.
42-
*/
43-
public static final String NAME = "include";
38+
/**
39+
* The helper's name.
40+
*/
41+
public static final String NAME = "include";
4442

45-
@Override
46-
public CharSequence apply(final String partial, final Options options) throws IOException {
47-
merge(options.context, options.hash);
48-
Template template = options.handlebars.compile(URI.create(partial));
49-
return new Handlebars.SafeString(template.apply(options.context));
50-
}
43+
@Override
44+
public CharSequence apply(final String partial, final Options options) throws IOException {
45+
// merge all the hashes into the context
46+
options.context.data(options.hash);
47+
Template template = options.handlebars.compile(URI.create(partial));
48+
return new Handlebars.SafeString(template.apply(options.context));
49+
}
5150

52-
/**
53-
* Merge everything from a hash into the given context.
54-
* @param context the context
55-
* @param hash the hash
56-
*/
57-
private void merge(final Context context, final Map<String, Object> hash) {
58-
for (Map.Entry<String, Object> a : hash.entrySet()) {
59-
context.data(a.getKey(), a.getValue());
60-
}
61-
}
6251
}
Lines changed: 72 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,80 @@
11
package handlebarsjs.spec;
22

3-
import com.github.jknack.handlebars.AbstractTest;
4-
import org.junit.Ignore;
3+
import java.io.IOException;
4+
55
import org.junit.Test;
66

7-
import java.io.IOException;
7+
import com.github.jknack.handlebars.AbstractTest;
88

99
public class IncludeTest extends AbstractTest {
1010

11-
private final Hash dudes = $("dudes",
12-
new Object[]{
13-
$("name", "Yehuda", "url", "http://yehuda"),
14-
$("name", "Alan", "url", "http://alan")
15-
});
16-
17-
@Test
18-
public void include() throws IOException {
19-
String template = "{{#each dudes}}{{include \"dude\" greeting=\"Hi\"}} {{/each}}";
20-
String partial = "{{greeting}}, {{name}}!";
21-
String expected = "Hi, Yehuda! Hi, Alan! ";
22-
shouldCompileToWithPartials(template, dudes, $("dude", partial), expected);
23-
}
24-
25-
/**
26-
* This is a port of the original test case from
27-
* https://github.com/wycats/handlebars.js/issues/182
28-
*/
29-
@Test
30-
@Ignore("Accessing the parent context fails to parse.")
31-
public void includeWithParentContext() throws IOException {
32-
String template = "{{#each dudes}}{{include \"dude\" greeting=..}} {{/each}}";
33-
String partial = "{{greeting.hello}}, {{name}}!";
34-
String expected = "Hi, Yehuda! Hi, Alan! ";
35-
Hash partials = $("dude", partial);
36-
Hash context = $("hello", "Hi", "dudes", dudes);
37-
shouldCompileToWithPartials(template, context, partials, expected);
38-
}
11+
@Test
12+
public void include() throws IOException {
13+
String template = "{{#each dudes}}{{include \"dude\" greeting=\"Hi\"}} {{/each}}";
14+
String partial = "{{greeting}}, {{name}}!";
15+
String expected = "Hi, Yehuda! Hi, Alan! ";
16+
Hash dudes = $("dudes",
17+
new Object[]{
18+
$("name", "Yehuda", "url", "http://yehuda"),
19+
$("name", "Alan", "url", "http://alan")
20+
});
21+
shouldCompileToWithPartials(template, dudes, $("dude", partial), expected);
22+
}
23+
24+
@Test
25+
public void accessToParentContext() throws IOException {
26+
String string = "{{#each hobbies}}{{../name}} has hobby {{hobbyname}} and lives in {{../town}} {{/each}}";
27+
Object hash = $("name", "Dennis", "town", "berlin", "hobbies",
28+
new Object[]{$("hobbyname", "swimming"), $("hobbyname", "dancing"),
29+
$("hobbyname", "movies") });
30+
shouldCompileTo(string, hash, "Dennis has hobby swimming and lives in berlin " +
31+
"Dennis has hobby dancing and lives in berlin " +
32+
"Dennis has hobby movies and lives in berlin ");
33+
}
34+
35+
@Test
36+
public void accessToParentContextFromPartialUsingInclude() throws IOException {
37+
String string = "{{#each hobbies}}{{include \"hobby\" parentcontext=.. town=../town}} {{/each}}";
38+
Object hash = $("name", "Dennis", "town", "berlin", "hobbies",
39+
new Object[]{$("hobbyname", "swimming"), $("hobbyname", "dancing"),
40+
$("hobbyname", "movies") });
41+
Hash partials = $("hobby",
42+
"{{parentcontext.name}} has hobby {{hobbyname}} and lives in {{town}}");
43+
44+
shouldCompileToWithPartials(string, hash, partials, "Dennis has hobby swimming and lives in berlin " +
45+
"Dennis has hobby dancing and lives in berlin " +
46+
"Dennis has hobby movies and lives in berlin ");
47+
}
48+
49+
@Test
50+
public void accessToParentContextFromPartialMustacheSpec() throws IOException {
51+
String string = "{{#each hobbies}}{{> hobby}} {{/each}}";
52+
53+
Object hash = $("name", "Dennis", "town", "berlin", "hobbies",
54+
new Object[]{$("hobbyname", "swimming"), $("hobbyname", "dancing"),
55+
$("hobbyname", "movies") });
56+
57+
Hash partials = $("hobby",
58+
"{{name}} has hobby {{hobbyname}} and lives in {{town}}");
59+
60+
shouldCompileToWithPartials(string, hash, partials, "Dennis has hobby swimming and lives in berlin " +
61+
"Dennis has hobby dancing and lives in berlin " +
62+
"Dennis has hobby movies and lives in berlin ");
63+
}
64+
65+
@Test
66+
public void explicitAccessToParentContextFromPartialMustacheSpec() throws IOException {
67+
String string = "{{#each hobbies}}{{> hobby}} {{/each}}";
68+
69+
Object hash = $("name", "Dennis", "town", "berlin", "hobbies",
70+
new Object[]{$("hobbyname", "swimming"), $("hobbyname", "dancing"),
71+
$("hobbyname", "movies") });
72+
73+
Hash partials = $("hobby",
74+
"{{../name}} has hobby {{hobbyname}} and lives in {{../town}}");
75+
76+
shouldCompileToWithPartials(string, hash, partials, "Dennis has hobby swimming and lives in berlin " +
77+
"Dennis has hobby dancing and lives in berlin " +
78+
"Dennis has hobby movies and lives in berlin ");
79+
}
3980
}

0 commit comments

Comments
 (0)