Skip to content

Commit 20857ee

Browse files
committed
#210 First implementation of TemplateParser
using recursive descent parser. Pending: parse IfTag and ListTag.
1 parent 3c38491 commit 20857ee

File tree

5 files changed

+281
-0
lines changed

5 files changed

+281
-0
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.javalite.templator;
2+
3+
import java.util.Map;
4+
5+
/**
6+
* This terminal node represents a static chunk of text in template.
7+
*
8+
* @author Igor Polevoy
9+
* @author Eric Nielsen
10+
*/
11+
class ConstNode extends TemplateNode {
12+
private final String value;
13+
14+
public ConstNode(String value) {
15+
this.value = value;
16+
}
17+
18+
@Override
19+
public void process(Map values, Appendable appendable) throws Exception {
20+
appendable.append(value);
21+
}
22+
23+
@Override
24+
public String toString(){
25+
return value;
26+
}
27+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.javalite.templator;
2+
3+
import java.util.Map;
4+
5+
/**
6+
* @author Igor Polevoy
7+
* @author Eric Nielsen
8+
*/
9+
abstract class TemplateNode {
10+
abstract void process(Map values, Appendable appendable) throws Exception;
11+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package org.javalite.templator;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
7+
/**
8+
* BNF rules:
9+
*
10+
* <pre>
11+
* WHITESPACE = Character.isWhitespace()+
12+
* IDENTIFIER = (Character.isJavaIdentifierStart() excluding '$') Character.isJavaIdentifierPart()*
13+
* IDENTIFIER_OR_FUNCTION = IDENTIFIER ('(' ')')?
14+
* CHAINED_IDENTIFIERS = IDENTIFIER ('.' IDENTIFIER_OR_FUNCTION)*
15+
* VAR = '$' '{' CHAINED_IDENTIFIER (WHITESPACE IDENTIFIER)? '}'
16+
* CONST = .*
17+
*
18+
* TAG_START = '&lt;' '#'
19+
* TAG_END = '&lt;' '/' '#'
20+
* LIST_TAG = TAG_START "list" WHITESPACE IDENTIFIER WHITESPACE "as" WHITESPACE IDENTIFIER '&gt;' LIST_TAG_BODY TAG_END "list" '>'
21+
* </pre>
22+
*
23+
* @author Eric Nielsen
24+
*/
25+
final class TemplateParser {
26+
27+
private final List<TemplateNode> nodes = new ArrayList<TemplateNode>();
28+
private final String source;
29+
private int index = -1;
30+
private int currentCodePoint = 0;
31+
private int constStartIndex;
32+
33+
TemplateParser(String source) {
34+
this.source = source;
35+
}
36+
37+
private boolean next() {
38+
if (++index < source.length()) {
39+
currentCodePoint = source.codePointAt(index);
40+
return true;
41+
} else {
42+
return false;
43+
}
44+
}
45+
46+
@SuppressWarnings("empty-statement")
47+
private boolean _whitespace() {
48+
if (!Character.isWhitespace(currentCodePoint)) { return false; }
49+
while (next() && Character.isWhitespace(currentCodePoint));
50+
return true;
51+
}
52+
53+
@SuppressWarnings("empty-statement")
54+
private boolean _identifier() {
55+
// Do not accept $ at identifier start. Will someone ever complain about this?
56+
if (currentCodePoint == '$' || !Character.isJavaIdentifierStart(currentCodePoint)) { return false; }
57+
while (next() && Character.isJavaIdentifierPart(currentCodePoint));
58+
return true;
59+
}
60+
61+
private boolean _identifierOrFunction() {
62+
if (!_identifier()) { return false; }
63+
if (currentCodePoint == '(') {
64+
if (!(next() && currentCodePoint == ')')) { return false; }
65+
next();
66+
}
67+
return true;
68+
}
69+
70+
private boolean _chainedIdentifiers(List<String> identifiers) {
71+
int startIndex = index;
72+
if (!_identifier()) { return false; }
73+
identifiers.add(source.substring(startIndex, index));
74+
while (currentCodePoint == '.') {
75+
if (!next()) { return false; }
76+
startIndex = index;
77+
if (!_identifierOrFunction()) { return false; }
78+
identifiers.add(source.substring(startIndex, index));
79+
}
80+
return true;
81+
}
82+
83+
private boolean _var() {
84+
int startIndex = index;
85+
if (currentCodePoint != '$') { return false; }
86+
if (!(next() && currentCodePoint == '{')) { return false; }
87+
List<String> identifiers = new ArrayList<String>();
88+
if (!(next() && _chainedIdentifiers(identifiers))) { return false; }
89+
BuiltIn builtIn = null;
90+
if (_whitespace()) {
91+
int builtInStartIndex = index;
92+
if (!_identifier()) { return false; }
93+
builtIn = TemplatorConfig.instance().getBuiltIn(source.substring(builtInStartIndex, index));
94+
}
95+
if (currentCodePoint != '}') { return false; }
96+
next();
97+
//TODO: refator this
98+
addConstEndingAt(startIndex);
99+
constStartIndex = index;
100+
nodes.add(new VarNode(identifiers, builtIn));
101+
return true;
102+
}
103+
104+
@SuppressWarnings("empty-statement")
105+
List<TemplateNode> parse() {
106+
//TODO: refactor this
107+
if (next()) {
108+
for (;;) {
109+
int startIndex = index;
110+
if (!_var()) {
111+
if (startIndex == index) {
112+
if (!next()) { break; }
113+
}
114+
}
115+
}
116+
}
117+
addConstEndingAt(source.length());
118+
return nodes;
119+
}
120+
121+
private void addConstEndingAt(int endIndex) {
122+
if (endIndex - 1 > constStartIndex) {
123+
nodes.add(new ConstNode(source.substring(constStartIndex, endIndex)));
124+
}
125+
}
126+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package org.javalite.templator;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
import static org.javalite.common.Inflector.capitalize;
6+
7+
/**
8+
* This terminal node represents a variable chunk of text in template.
9+
*
10+
* @author Igor Polevoy
11+
* @author Eric Nielsen
12+
*/
13+
class VarNode extends TemplateNode {
14+
private final List<String> identifiers;
15+
private final BuiltIn builtIn;
16+
17+
VarNode(List<String> identifiers, BuiltIn builtIn) {
18+
this.identifiers = identifiers;
19+
this.builtIn = builtIn;
20+
}
21+
22+
private Object valueOf(Object obj, String propertyName) throws Exception {
23+
if (propertyName.endsWith("()")) {
24+
return obj.getClass().getMethod(propertyName.substring(0, propertyName.length() - 2)).invoke(obj);
25+
}
26+
if (obj instanceof Map) {
27+
return ((Map) obj).get(propertyName);
28+
}
29+
try {
30+
// try generic get method
31+
return obj.getClass().getMethod("get", String.class).invoke(obj, propertyName);
32+
} catch (Exception e) {
33+
try {
34+
// try javabean property
35+
return obj.getClass().getMethod("get" + capitalize(propertyName)).invoke(obj);
36+
} catch (Exception e1) {
37+
// try public field
38+
try {
39+
return obj.getClass().getDeclaredField(propertyName).get(obj);
40+
} catch (Exception e2) {
41+
}
42+
}
43+
}
44+
return null;
45+
}
46+
47+
@Override
48+
void process(Map values, Appendable appendable) throws Exception {
49+
Object obj = values.get(identifiers.get(0));
50+
for (int i = 1 ; i < identifiers.size(); i++) {
51+
obj = valueOf(obj, identifiers.get(i));
52+
}
53+
if (obj != null) {
54+
if (builtIn != null) {
55+
obj = builtIn.process(obj.toString());
56+
}
57+
appendable.append(obj.toString());
58+
}
59+
}
60+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package org.javalite.templator;
2+
3+
import java.io.StringWriter;
4+
import org.junit.Test;
5+
6+
import java.util.List;
7+
import java.util.Map;
8+
import static org.javalite.common.Collections.*;
9+
import static org.javalite.test.jspec.JSpec.*;
10+
11+
/**
12+
* @author Eric Nielsen
13+
*/
14+
public class TemplateParserSpec {
15+
16+
@Test
17+
public void shouldParseTextTemplate() throws Exception {
18+
String source = "this is just plain text, no strings attached";
19+
List<TemplateNode> tokens = new TemplateParser(source).parse();
20+
the(tokens.size()).shouldBeEqual(1);
21+
StringWriter sw = new StringWriter();
22+
tokens.get(0).process(map(), sw);
23+
the(sw.toString()).shouldBeEqual(source);
24+
}
25+
26+
@Test
27+
public void shouldTokenizeTemplate() throws Exception {
28+
List<TemplateNode> tokens = new TemplateParser(
29+
"Hello ${first_name}, your code is <b>${${foo.size()}${invalid()}}</b> ${one.two.three.four}")
30+
.parse();
31+
a(tokens.size()).shouldBeEqual(6);
32+
Map values = map(
33+
"first_name", "John",
34+
"foo", map(),
35+
"one", map("two", map("three", map("four", "five"))));
36+
StringWriter sw = new StringWriter();
37+
tokens.get(0).process(values, sw);
38+
tokens.get(1).process(values, sw);
39+
tokens.get(2).process(values, sw);
40+
tokens.get(3).process(values, sw);
41+
tokens.get(4).process(values, sw);
42+
tokens.get(5).process(values, sw);
43+
the(sw.toString()).shouldBeEqual("Hello John, your code is <b>${0${invalid()}}</b> five");
44+
}
45+
46+
@Test
47+
public void shouldParseBuiltIn() throws Exception {
48+
List<TemplateNode> tokens = new TemplateParser("<b>${article.content esc}</b>").parse();
49+
a(tokens.size()).shouldBeEqual(3);
50+
Map values = map("article", map("content", "R&B"));
51+
StringWriter sw = new StringWriter();
52+
tokens.get(0).process(values, sw);
53+
tokens.get(1).process(values, sw);
54+
tokens.get(2).process(values, sw);
55+
the(sw.toString()).shouldBeEqual("<b>R&amp;B</b>");
56+
}
57+
}

0 commit comments

Comments
 (0)