package com.github.mustachejava;
import java.io.
BufferedReader;
import java.io.
IOException;
import java.io.
Reader;
import java.util.concurrent.atomic.
AtomicInteger;
/**
* The parser generates callbacks into the MustacheFactory to build them. Do not use these
* directly as you must manage the Mustache object lifecycle as well.
* <p>
* User: sam
* Date: 5/14/11
* Time: 3:52 PM
*/
public class
MustacheParser {
public static final
String DEFAULT_SM = "{{";
public static final
String DEFAULT_EM = "}}";
private
MustacheFactory mf;
protected
MustacheParser(
MustacheFactory mf) {
this.
mf =
mf;
}
public
Mustache compile(
String file) {
Reader reader =
mf.
getReader(
file);
if (
reader == null) {
throw new
MustacheNotFoundException(
file);
}
return
compile(
reader,
file);
}
public
Mustache compile(
Reader reader,
String file) {
return
compile(
reader,
file,
DEFAULT_SM,
DEFAULT_EM);
}
public
Mustache compile(
Reader reader,
String file,
String sm,
String em) {
return
compile(
reader, null, new
AtomicInteger(0),
file,
sm,
em, true);
}
public
Mustache compile(
Reader reader,
String file,
String sm,
String em, boolean
startOfLine) {
return
compile(
reader, null, new
AtomicInteger(0),
file,
sm,
em,
startOfLine);
}
@
SuppressWarnings("ConstantConditions") // this method is too complex
protected
Mustache compile(final
Reader reader,
String tag, final
AtomicInteger currentLine,
String file,
String sm,
String em, boolean
startOfLine) throws
MustacheException {
if (
reader == null) {
throw new
MustacheException("Reader is null");
}
Reader br;
if (
reader.
markSupported()) {
br =
reader;
} else {
br = new
BufferedReader(
reader);
}
try {
boolean
sawCR = false;
int
startLine =
currentLine.
get();
MustacheVisitor mv =
mf.
createMustacheVisitor();
// Now we grab the mustache template
boolean
onlywhitespace = true;
// Starting a new line
boolean
iterable =
currentLine.
get() != 0;
currentLine.
compareAndSet(0, 1);
StringBuilder out = new
StringBuilder();
try {
int
c;
while ((
c =
br.
read()) != -1) {
// We remember that we saw a carriage return so we can put it back in later
if (
c == '\r') {
sawCR = true;
continue;
}
// Increment the line
if (
c == '\n') {
currentLine.
incrementAndGet();
if (!
iterable || (
iterable && !
onlywhitespace)) {
if (
sawCR)
out.
append("\r");
out.
append("\n");
}
out =
write(
mv,
out,
file,
currentLine.
intValue(),
startOfLine);
iterable = false;
onlywhitespace = true;
startOfLine = true;
continue;
}
sawCR = false;
// Check for a mustache start
if (
c ==
sm.
charAt(0)) {
br.
mark(1);
if (
sm.
length() == 1 ||
br.
read() ==
sm.
charAt(1)) {
// Two mustaches, now capture command
StringBuilder sb = new
StringBuilder();
while ((
c =
br.
read()) != -1) {
br.
mark(1);
if (
c ==
em.
charAt(0)) {
if (
em.
length() > 1) {
if (
br.
read() ==
em.
charAt(1)) {
// Matched end
break;
} else {
// Only one
br.
reset();
}
} else break;
}
sb.
append((char)
c);
}
final
String command =
mf.
translate(
sb.
toString());
if (
command.
length() == 0) {
throw new
MustacheException("Empty mustache in " +
file + ":" +
currentLine);
}
final char
ch =
command.
charAt(0);
final
String variable =
command.
substring(1).
trim();
switch (
ch) {
case '#':
case '^':
case '<':
case '$': {
boolean
oldStartOfLine =
startOfLine;
startOfLine =
startOfLine &
onlywhitespace;
int
line =
currentLine.
get();
final
Mustache mustache =
compile(
br,
variable,
currentLine,
file,
sm,
em,
startOfLine);
int
lines =
currentLine.
get() -
line;
if (!
onlywhitespace ||
lines == 0) {
write(
mv,
out,
file,
currentLine.
intValue(),
oldStartOfLine);
}
out = new
StringBuilder();
switch (
ch) {
case '#':
mv.
iterable(new
TemplateContext(
sm,
em,
file,
line,
startOfLine),
variable,
mustache);
break;
case '^':
mv.
notIterable(new
TemplateContext(
sm,
em,
file,
line,
startOfLine),
variable,
mustache);
break;
case '<':
mv.
extend(new
TemplateContext(
sm,
em,
file,
line,
startOfLine),
variable,
mustache);
break;
case '$':
mv.
name(new
TemplateContext(
sm,
em,
file,
line,
startOfLine),
variable,
mustache);
break;
}
iterable =
lines != 0;
break;
}
case '/': {
// Tag end
if (!
startOfLine || !
onlywhitespace) {
write(
mv,
out,
file,
currentLine.
intValue(),
startOfLine);
}
if (!
variable.
equals(
tag)) {
throw new
MustacheException(
"Mismatched start/end tags: " +
tag + " != " +
variable + " in " +
file + ":" +
currentLine);
}
return
mv.
mustache(new
TemplateContext(
sm,
em,
file, 0,
startOfLine));
}
case '>': {
out =
write(
mv,
out,
file,
currentLine.
intValue(),
startOfLine);
startOfLine =
startOfLine &
onlywhitespace;
mv.
partial(new
TemplateContext(
sm,
em,
file,
currentLine.
get(),
startOfLine),
variable);
break;
}
case '{': {
out =
write(
mv,
out,
file,
currentLine.
intValue(),
startOfLine);
// Not escaped
String name =
variable;
if (
em.
charAt(1) != '}') {
name =
variable.
substring(0,
variable.
length() - 1);
} else {
if (
br.
read() != '}') {
throw new
MustacheException(
"Improperly closed variable in " +
file + ":" +
currentLine);
}
}
mv.
value(new
TemplateContext(
sm,
em,
file,
currentLine.
get(), false),
name, false);
break;
}
case '&': {
// Not escaped
out =
write(
mv,
out,
file,
currentLine.
intValue(),
startOfLine);
mv.
value(new
TemplateContext(
sm,
em,
file,
currentLine.
get(), false),
variable, false);
break;
}
case '%':
// Pragmas
out =
write(
mv,
out,
file,
currentLine.
intValue(),
startOfLine);
int
index =
variable.
indexOf(" ");
String pragma;
String args;
if (
index == -1) {
pragma =
variable;
args = null;
} else {
pragma =
variable.
substring(0,
index);
args =
variable.
substring(
index + 1);
}
mv.
pragma(new
TemplateContext(
sm,
em,
file,
currentLine.
get(),
startOfLine),
pragma,
args);
break;
case '!':
// Comment
mv.
comment(new
TemplateContext(
sm,
em,
file,
currentLine.
get(),
startOfLine),
variable);
out =
write(
mv,
out,
file,
currentLine.
intValue(),
startOfLine);
break;
case '=':
// Change delimiters
out =
write(
mv,
out,
file,
currentLine.
intValue(),
startOfLine);
String delimiters =
command.
replaceAll("\\s+", "");
int
length =
delimiters.
length();
if (
length > 6 ||
length / 2 * 2 !=
length) {
throw new
MustacheException("Invalid delimiter string");
}
sm =
delimiters.
substring(1,
length / 2);
em =
delimiters.
substring(
length / 2,
length - 1);
break;
default: {
if (
c == -1) {
throw new
MustacheException("Improperly closed variable in " +
file + ":" +
currentLine);
}
// Reference
out =
write(
mv,
out,
file,
currentLine.
intValue(),
startOfLine);
mv.
value(new
TemplateContext(
sm,
em,
file,
currentLine.
get(), false),
command.
trim(), true);
break;
}
}
// Additional text is no longer at the start of the line
startOfLine = false;
continue;
} else {
// Only one
br.
reset();
}
}
onlywhitespace =
onlywhitespace && (
c == ' ' ||
c == '\t' ||
c == '\r');
out.
append((char)
c);
}
write(
mv,
out,
file,
currentLine.
intValue(),
startOfLine);
if (
tag == null) {
br.
close();
} else {
throw new
MustacheException("Failed to close '" +
tag + "' tag at line " +
startLine);
}
} catch (
IOException e) {
throw new
MustacheException("Failed to read",
e);
}
mv.
eof(new
TemplateContext(
sm,
em,
file,
currentLine.
get(),
startOfLine));
return
mv.
mustache(new
TemplateContext(
sm,
em,
file, 0,
startOfLine));
} catch (
MustacheException me) {
try {
// We're going to blow the whole stack of compilation and
// close the readers on the way up.
br.
close();
} catch (
IOException e) {
// Ignore IOExceptions on close
}
throw
me;
}
}
/**
* Ignore empty strings and append to the previous code if it was also a write.
*/
private
StringBuilder write(
MustacheVisitor mv,
StringBuilder out,
String file, int
line, boolean
startOfLine) {
String text =
out.
toString();
mv.
write(new
TemplateContext(null, null,
file,
line,
startOfLine),
text);
return new
StringBuilder();
}
}