blob: 5748a66f630678245d29aa58841f14e05e96c838 [file] [log] [blame]
part of mustache.impl;
List<Node> parse(String source,
bool lenient,
String templateName,
String delimiters) {
if (source == null) throw new ArgumentError.notNull('Template source');
var tokens =
new Scanner(source, templateName, delimiters, lenient: lenient).scan();
tokens = _removeStandaloneWhitespace(tokens);
tokens = _mergeAdjacentText(tokens);
var stack = new List<Node>()..add(new SectionNode('root', 0, 0, delimiters));
var delim;
for (var t in tokens) {
switch (t.type) {
case _TEXT:
var n = new TextNode(t.value, t.start, t.end);
stack.last.children.add(n);
break;
case _VARIABLE:
case _UNESC_VARIABLE:
var n = new VariableNode(
t.value, t.start, t.end, escape: t.type != _UNESC_VARIABLE);
stack.last.children.add(n);
break;
case _PARTIAL:
var n = new PartialNode(t.value, t.start, t.end, t.indent);
stack.last.children.add(n);
break;
case _OPEN_SECTION:
case _OPEN_INV_SECTION:
// Store the start, end of the inner string content not
// including the tag.
var child = new SectionNode(t.value, t.start, t.end, delim,
inverse: t.type == _OPEN_INV_SECTION)
..contentStart = t.end;
stack.last.children.add(child);
stack.add(child);
break;
case _CLOSE_SECTION:
if (stack.last.name != t.value) {
throw new TemplateException(
"Mismatched tag, expected: '${stack.last.name}', was: '${t.value}'",
templateName, source, t.start);
}
stack.last.contentEnd = t.start;
stack.removeLast();
break;
case _CHANGE_DELIMITER:
delim = t.value;
break;
case _COMMENT:
// Do nothing
break;
//FIXME change constants to enums, and then remove this default clause.
default:
throw new StateError('Unkown node type: $t');
}
}
if (stack.length != 1) {
throw new TemplateException(
"Unclosed tag: '${stack.last.name}'.",
templateName, source, stack.last.start);
}
return stack.last.children;
}
// Takes a list of tokens, and removes _NEWLINE, and _WHITESPACE tokens.
// This is used to implement mustache standalone lines.
// Where TAG is one of: OPEN_SECTION, INV_SECTION, CLOSE_SECTION
// LINE_END, [WHITESPACE], TAG, [WHITESPACE], LINE_END => LINE_END, TAG
// WHITESPACE => TEXT
// LINE_END => TEXT
// TODO could rewrite this to use a generator, rather than creating an inter-
// mediate list.
List<Token> _removeStandaloneWhitespace(List<Token> tokens) {
int i = 0;
Token read() { var ret = i < tokens.length ? tokens[i++] : null; return ret; }
Token peek([int n = 0]) => i + n < tokens.length ? tokens[i + n] : null;
bool isTag(token) => token != null
&& const [_OPEN_SECTION, _OPEN_INV_SECTION, _CLOSE_SECTION, _COMMENT,
_PARTIAL, _CHANGE_DELIMITER].contains(token.type);
bool isWhitespace(token) => token != null && token.type == _WHITESPACE;
bool isLineEnd(token) => token != null && token.type == _LINE_END;
var result = new List<Token>();
add(token) => result.add(token);
standaloneLineCheck() {
// Swallow leading whitespace
// Note, the scanner will only ever create a single whitespace token. There
// is no need to handle multiple whitespace tokens.
if (isWhitespace(peek())
&& isTag(peek(1))
&& (isLineEnd(peek(2)) || peek(2) == null)) { // null == EOF
read();
} else if (isWhitespace(peek())
&& isTag(peek(1))
&& isWhitespace(peek(2))
&& (isLineEnd(peek(3)) || peek(3) == null)) {
read();
}
if ((isTag(peek()) && isLineEnd(peek(1)))
|| (isTag(peek())
&& isWhitespace(peek(1))
&& (isLineEnd(peek(2)) || peek(2) == null))) {
// Add tag
add(read());
// Swallow trailing whitespace.
if (isWhitespace(peek()))
read();
// Swallow line end.
assert(isLineEnd(peek()));
read();
standaloneLineCheck(); //FIXME don't use recursion.
}
}
// Handle case where first line is a standalone tag.
standaloneLineCheck();
var t;
while ((t = read()) != null) {
if (t.type == _LINE_END) {
// Convert line end to text token
add(new Token(_TEXT, t.value, t.start, t.end));
standaloneLineCheck();
} else if (t.type == _WHITESPACE) {
// Convert whitespace to text token
add(new Token(_TEXT, t.value, t.start, t.end));
} else {
// Preserve token
add(t);
}
}
return result;
}
// Merging adjacent text nodes will improve the render speed, but slow down
// parsing. It will be beneficial where templates are parsed once and rendered
// a number of times.
List<Token> _mergeAdjacentText(List<Token> tokens) {
if (tokens.isEmpty) return <Token>[];
var result = new List<Token>();
int i = 0;
while(i < tokens.length) {
var t = tokens[i];
if (t.type != _TEXT
|| (i < tokens.length - 1 && tokens[i + 1].type != _TEXT)) {
result.add(tokens[i]);
i++;
} else {
var buffer = new StringBuffer();
while(i < tokens.length && tokens[i].type == _TEXT) {
buffer.write(tokens[i].value);
i++;
}
result.add(new Token(_TEXT, buffer.toString(), t.start, t.end));
}
}
return result;
}