Add initial parser re-implementation
diff --git a/lib/src/mustache_impl.dart b/lib/src/mustache_impl.dart
index 1531e9d..0c11e5a 100644
--- a/lib/src/mustache_impl.dart
+++ b/lib/src/mustache_impl.dart
@@ -5,6 +5,8 @@
import 'package:mustache/mustache.dart' as m;
+import 'parser.dart' as parser;
+
part 'lambda_context.dart';
part 'node.dart';
part 'parse.dart';
diff --git a/lib/src/node.dart b/lib/src/node.dart
index 20b9ece..0d90176 100644
--- a/lib/src/node.dart
+++ b/lib/src/node.dart
@@ -47,6 +47,16 @@
final String text;
+ String toString() => '(TextNode "$text" $start $end)';
+
+ // Only used for testing.
+ bool operator ==(o) => o is TextNode
+ && text == o.text
+ && start == o.start
+ && end == o.end;
+
+ // TODO hashcode. import quiver.
+
void render(RenderContext ctx, {lastNode: false}) {
if (text == '') return;
if (ctx.indent == null || ctx.indent == '') {
@@ -64,12 +74,24 @@
class VariableNode extends Node {
- VariableNode(this.name, int start, int end, {this.escape: false})
+ VariableNode(this.name, int start, int end, {this.escape: true})
: super(start, end);
final String name;
final bool escape;
+ String toString() => '(VariableNode "$name" escape: $escape $start $end)';
+
+ // Only used for testing.
+ bool operator ==(o) => o is VariableNode
+ && name == o.name
+ && escape == o.escape
+ && start == o.start
+ && end == o.end;
+
+ // TODO hashcode. import quiver.
+
+
void render(RenderContext ctx) {
var value = ctx.resolveValue(name);
@@ -137,6 +159,20 @@
int contentStart;
int contentEnd;
final List<Node> children = <Node>[];
+
+ toString() => '(SectionNode $name inverse: $inverse)';
+
+ // TODO Only used for testing.
+ //FIXME use deepequals in test for comparing children.
+ //Perhaps shift all of this == code into test.
+ bool operator ==(o) => o is SectionNode
+ && name == o.name
+ && delimiters == o.delimiters
+ && inverse == o.inverse
+ && contentStart == o.contentEnd;
+
+ // TODO hashcode. import quiver.
+
//TODO can probably combine Inv and Normal to shorten.
void render(RenderContext ctx) => inverse
@@ -226,6 +262,14 @@
// Used to store the preceding whitespace before a partial tag, so that
// it's content can be correctly indented.
final String indent;
+
+ //TODO move to test.
+ bool operator ==(o) => o is PartialNode
+ && name == o.name
+ && indent == o.indent;
+
+ // TODO hashcode. import quiver.
+
void render(RenderContext ctx) {
var partialName = name;
diff --git a/lib/src/parser.dart b/lib/src/parser.dart
new file mode 100644
index 0000000..484e7b1
--- /dev/null
+++ b/lib/src/parser.dart
@@ -0,0 +1,275 @@
+library parser;
+
+//TODO just import nodes.
+import 'mustache_impl.dart' show Node, SectionNode, TextNode, PartialNode, VariableNode;
+import 'scanner2.dart';
+import 'template_exception.dart';
+import 'token2.dart';
+
+List<Node> parse(String source,
+ bool lenient,
+ String templateName,
+ String delimiters) {
+ var parser = new Parser(source, templateName, delimiters, lenient: lenient);
+ return parser.parse();
+}
+
+class Parser {
+
+ Parser(this._source, this._templateName, this._delimiters, {lenient: false})
+ : _lenient = lenient {
+ // _scanner = new Scanner(_source, _templateName, _delimiters, _lenient);
+ }
+
+ //TODO do I need to keep all of these variables around?
+ final String _source;
+ final bool _lenient;
+ final String _templateName;
+ final String _delimiters;
+ Scanner _scanner; //TODO make final
+ List<Token> _tokens;
+ final List<SectionNode> _stack = <SectionNode>[];
+ String _currentDelimiters;
+
+ int _i = 0;
+
+ //TODO EOF??
+ Token _peek() => _i < _tokens.length ? _tokens[_i] : null;
+
+ // TODO EOF?? return null on EOF?
+ Token _read() {
+ var t = null;
+ if (_i < _tokens.length) {
+ t = _tokens[_i];
+ _i++;
+ }
+ return t;
+ }
+
+ //TODO use a sync* generator once landed in Dart 1.10.
+ Iterable<Token> _readWhile(bool predicate(Token t)) {
+ var list = <Token>[];
+ for (var t = _peek(); t != null && predicate(t); t = _peek()) {
+ _read();
+ list.add(t);
+ }
+ return list;
+ }
+
+ List<Node> parse() {
+ _scanner = new Scanner(_source, _templateName, _delimiters,
+ lenient: _lenient);
+
+ _tokens = _scanner.scan();
+ _tokens = _removeStandaloneWhitespace(_tokens);
+ _tokens = _mergeAdjacentText(_tokens);
+
+ _currentDelimiters = _delimiters;
+
+ _stack.add(new SectionNode('root', 0, 0, _delimiters));
+
+ for (var token = _peek(); token != null; token = _peek()) {
+
+ if (token.type == TokenType.text) {
+ _read();
+ _stack.last.children.add(
+ new TextNode(token.value, token.start, token.end));
+
+ } else if (token.type == TokenType.openDelimiter) {
+ if (token.value == '{{{') {
+ _parseTripleMustacheTag();
+ } else {
+ _parseTag();
+ }
+ } else if (token.type == TokenType.changeDelimiter) {
+ _read();
+ _currentDelimiters = token.value;
+ } else {
+ throw 'boom!';
+ }
+ }
+
+ //TODO proper error message.
+ assert(_stack.length == 1);
+
+ return _stack.last.children;
+ }
+
+ void _parseTripleMustacheTag() {
+ var open = _read();
+ var name = _parseIdentifier();
+ var close = _read();
+ _stack.last.children.add(
+ new VariableNode(name, open.start, open.end, escape: false));
+ }
+
+ void _parseTag() {
+ var open = _read();
+
+ if (_peek().type == TokenType.whitespace) _read();
+
+ // sigil character, or null. A sigil is the character which identifies which
+ // sort of tag it is, i.e. '#', '/', or '>'.
+ var sigil = _peek().type == TokenType.sigil ? _read().value : null;
+
+ if (_peek().type == TokenType.whitespace) _read();
+
+ // TODO split up names here instead of during render.
+ // Also check that they are valid token types.
+ var name = _parseIdentifier();
+
+ var close = _read();
+
+ if (sigil == '#' || sigil == '^') {
+ // Section and inverser section.
+ bool inverse = sigil == '^';
+ var node = new SectionNode(name, open.start, close.end,
+ _currentDelimiters, inverse: inverse);
+ _stack.last.children.add(node);
+ _stack.add(node);
+
+ } else if (sigil == '/') {
+ // Close section tag
+ if (name != _stack.last.name) throw 'boom!';
+ _stack.removeLast();
+
+ } else if (sigil == '&' || sigil == null) {
+ // Variable and unescaped variable tag
+ bool escape = sigil == null;
+ _stack.last.children.add(
+ new VariableNode(name, open.start, close.end, escape: escape));
+
+ } else if (sigil == '>') {
+ // Partial tag
+ //TODO find precending whitespace.
+ var indent = '';
+ _stack.last.children.add(
+ new PartialNode(name, open.start, close.end, indent));
+
+ } else if (sigil == '!') {
+ // Ignore comments
+
+ } else {
+ assert(false); //TODO
+ }
+ }
+
+ //TODO shouldn't just return a string.
+ String _parseIdentifier() {
+ // TODO split up names here instead of during render.
+ // Also check that they are valid token types.
+ var name = _readWhile((t) => t.type != TokenType.closeDelimiter)
+ .map((t) => t.value)
+ .join()
+ .trim();
+
+ return name;
+ }
+
+ // 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 [TokenType.openDelimiter, TokenType.changeDelimiter].contains(token.type);
+
+ bool isWhitespace(token) => token != null && token.type == TokenType.whitespace;
+ bool isLineEnd(token) => token != null && token.type == TokenType.lineEnd;
+
+ 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 == TokenType.lineEnd) {
+ // Convert line end to text token
+ add(new Token(TokenType.text, t.value, t.start, t.end));
+ standaloneLineCheck();
+ } else if (t.type == TokenType.whitespace) {
+ // Convert whitespace to text token
+ add(new Token(TokenType.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 != TokenType.text
+ || (i < tokens.length - 1 && tokens[i + 1].type != TokenType.text)) {
+ result.add(tokens[i]);
+ i++;
+ } else {
+ var buffer = new StringBuffer();
+ while(i < tokens.length && tokens[i].type == TokenType.text) {
+ buffer.write(tokens[i].value);
+ i++;
+ }
+ result.add(new Token(TokenType.text, buffer.toString(), t.start, t.end));
+ }
+ }
+ return result;
+ }
+
+}
+
diff --git a/lib/src/scanner.dart b/lib/src/scanner.dart
index 4120ce7..e370f45 100644
--- a/lib/src/scanner.dart
+++ b/lib/src/scanner.dart
@@ -7,6 +7,8 @@
_lenient = lenient,
_itr = source.runes.iterator {
+ if (source == null) throw new ArgumentError.notNull('Template source');
+
var delims = _parseDelimiterString(delimiters);
_openDelimiter = delims[0];
_openDelimiterInner = delims[1];
diff --git a/lib/src/scanner2.dart b/lib/src/scanner2.dart
index 37fe05f..2900c5b 100644
--- a/lib/src/scanner2.dart
+++ b/lib/src/scanner2.dart
@@ -1,4 +1,7 @@
-library scanner;
+library mustache.scanner;
+
+import 'token2.dart';
+import 'template_exception.dart';
class Scanner {
@@ -68,7 +71,7 @@
_peek() == _OPEN_MUSTACHE) {
_read();
- _push(TokenType.openTripleMustache, '{{{', start, _offset);
+ _push(TokenType.openDelimiter, '{{{', start, _offset);
_scanTagContent();
_scanCloseTripleMustache();
@@ -132,6 +135,7 @@
}
}
+ // TODO rename this.
_push(TokenType type, String value, int start, int end) =>
_tokens.add(new Token(type, value, start, end));
@@ -257,7 +261,6 @@
// Scan close triple mustache delimiter token.
void _scanCloseTripleMustache() {
-
if (_peek() != _EOF) {
int start = _offset;
@@ -265,9 +268,8 @@
_expect(_CLOSE_MUSTACHE);
_expect(_CLOSE_MUSTACHE);
- _push(TokenType.closeTripleMustache, '}}}', start, _offset);
- }
-
+ _push(TokenType.closeDelimiter, '}}}', start, _offset);
+ }
}
// Open delimiter characters and = have already been read.
@@ -388,168 +390,3 @@
const int _MINUS = 45;
-
-
-class TokenType {
-
- const TokenType(this.name);
-
- final String name;
-
- String toString() => '(TokenType $name)';
-
- static const TokenType text = const TokenType('text');
- static const TokenType comment = const TokenType('comment');
- static const TokenType openDelimiter = const TokenType('openDelimiter');
- static const TokenType closeDelimiter = const TokenType('closeDelimiter');
-
- // A sigil is the word commonly used to describe the special character at the
- // start of mustache tag i.e. #, ^ or /.
- static const TokenType sigil = const TokenType('sigil');
- static const TokenType identifier = const TokenType('identifier');
- static const TokenType dot = const TokenType('dot');
-
- static const TokenType changeDelimiter = const TokenType('changeDelimiter');
- //TODO consider just using normal delimiter and checking the value to see if it is a triple
- static const TokenType openTripleMustache = const TokenType('openTripleMustache');
- static const TokenType closeTripleMustache = const TokenType('closeTripleMustache');
- static const TokenType whitespace = const TokenType('whitespace');
- static const TokenType lineEnd = const TokenType('lineEnd');
-
-}
-
-
-class Token {
-
- Token(this.type, this.value, this.start, this.end);
-
- final TokenType type;
- final String value;
-
- final int start;
- final int end;
-
- String toString() => "(Token ${type.name} \"$value\" $start $end)";
-
- // Only used for testing.
- bool operator ==(o) => o is Token
- && type == o.type
- && value == o.value
- && start == o.start
- && end == o.end;
-
- // TODO hashcode. import quiver.
-}
-
-
-
-
-
-class TemplateException { //implements m.TemplateException {
-
- TemplateException(this.message, this.templateName, this.source, this.offset);
-
- final String message;
- final String templateName;
- final String source;
- final int offset;
-
- bool _isUpdated = false;
- int _line;
- int _column;
- String _context;
-
- int get line {
- _update();
- return _line;
- }
-
- int get column {
- _update();
- return _column;
- }
-
- String get context {
- _update();
- return _context;
- }
-
- String toString() {
- var list = [];
- if (templateName != null) list.add(templateName);
- if (line != null) list.add(line);
- if (column != null) list.add(column);
- var location = list.isEmpty ? '' : ' (${list.join(':')})';
- return '$message$location\n$context';
- }
-
- // This source code is a modified version of FormatException.toString().
- void _update() {
- if (_isUpdated) return;
- _isUpdated = true;
-
- if (source == null
- || offset == null
- || (offset < 0 || offset > source.length))
- return;
-
- // Find line and character column.
- int lineNum = 1;
- int lineStart = 0;
- bool lastWasCR;
- for (int i = 0; i < offset; i++) {
- int char = source.codeUnitAt(i);
- if (char == 0x0a) {
- if (lineStart != i || !lastWasCR) {
- lineNum++;
- }
- lineStart = i + 1;
- lastWasCR = false;
- } else if (char == 0x0d) {
- lineNum++;
- lineStart = i + 1;
- lastWasCR = true;
- }
- }
-
- _line = lineNum;
- _column = offset - lineStart + 1;
-
- // Find context.
- int lineEnd = source.length;
- for (int i = offset; i < source.length; i++) {
- int char = source.codeUnitAt(i);
- if (char == 0x0a || char == 0x0d) {
- lineEnd = i;
- break;
- }
- }
- int length = lineEnd - lineStart;
- int start = lineStart;
- int end = lineEnd;
- String prefix = "";
- String postfix = "";
- if (length > 78) {
- // Can't show entire line. Try to anchor at the nearest end, if
- // one is within reach.
- int index = offset - lineStart;
- if (index < 75) {
- end = start + 75;
- postfix = "...";
- } else if (end - offset < 75) {
- start = end - 75;
- prefix = "...";
- } else {
- // Neither end is near, just pick an area around the offset.
- start = offset - 36;
- end = offset + 36;
- prefix = postfix = "...";
- }
- }
- String slice = source.substring(start, end);
- int markOffset = offset - start + prefix.length;
-
- _context = "$prefix$slice$postfix\n${" " * markOffset}^\n";
- }
-
-}
\ No newline at end of file
diff --git a/lib/src/template.dart b/lib/src/template.dart
index db4cf3a..8ea5ed3 100644
--- a/lib/src/template.dart
+++ b/lib/src/template.dart
@@ -8,7 +8,7 @@
String name,
m.PartialResolver partialResolver})
: source = source,
- _nodes = parse(source, lenient, name, '{{ }}'),
+ _nodes = parser.parse(source, lenient, name, '{{ }}'),
_lenient = lenient,
_htmlEscapeValues = htmlEscapeValues,
_name = name,
diff --git a/lib/src/template_exception.dart b/lib/src/template_exception.dart
new file mode 100644
index 0000000..d4db9c0
--- /dev/null
+++ b/lib/src/template_exception.dart
@@ -0,0 +1,113 @@
+library mustache.template_exception;
+
+import 'package:mustache/mustache.dart' as m;
+
+class TemplateException implements m.TemplateException {
+
+ TemplateException(this.message, this.templateName, this.source, this.offset);
+
+ final String message;
+ final String templateName;
+ final String source;
+ final int offset;
+
+ bool _isUpdated = false;
+ int _line;
+ int _column;
+ String _context;
+
+ int get line {
+ _update();
+ return _line;
+ }
+
+ int get column {
+ _update();
+ return _column;
+ }
+
+ String get context {
+ _update();
+ return _context;
+ }
+
+ String toString() {
+ var list = [];
+ if (templateName != null) list.add(templateName);
+ if (line != null) list.add(line);
+ if (column != null) list.add(column);
+ var location = list.isEmpty ? '' : ' (${list.join(':')})';
+ return '$message$location\n$context';
+ }
+
+ // This source code is a modified version of FormatException.toString().
+ void _update() {
+ if (_isUpdated) return;
+ _isUpdated = true;
+
+ if (source == null
+ || offset == null
+ || (offset < 0 || offset > source.length))
+ return;
+
+ // Find line and character column.
+ int lineNum = 1;
+ int lineStart = 0;
+ bool lastWasCR;
+ for (int i = 0; i < offset; i++) {
+ int char = source.codeUnitAt(i);
+ if (char == 0x0a) {
+ if (lineStart != i || !lastWasCR) {
+ lineNum++;
+ }
+ lineStart = i + 1;
+ lastWasCR = false;
+ } else if (char == 0x0d) {
+ lineNum++;
+ lineStart = i + 1;
+ lastWasCR = true;
+ }
+ }
+
+ _line = lineNum;
+ _column = offset - lineStart + 1;
+
+ // Find context.
+ int lineEnd = source.length;
+ for (int i = offset; i < source.length; i++) {
+ int char = source.codeUnitAt(i);
+ if (char == 0x0a || char == 0x0d) {
+ lineEnd = i;
+ break;
+ }
+ }
+ int length = lineEnd - lineStart;
+ int start = lineStart;
+ int end = lineEnd;
+ String prefix = "";
+ String postfix = "";
+ if (length > 78) {
+ // Can't show entire line. Try to anchor at the nearest end, if
+ // one is within reach.
+ int index = offset - lineStart;
+ if (index < 75) {
+ end = start + 75;
+ postfix = "...";
+ } else if (end - offset < 75) {
+ start = end - 75;
+ prefix = "...";
+ } else {
+ // Neither end is near, just pick an area around the offset.
+ start = offset - 36;
+ end = offset + 36;
+ prefix = postfix = "...";
+ }
+ }
+ String slice = source.substring(start, end);
+ int markOffset = offset - start + prefix.length;
+
+ _context = "$prefix$slice$postfix\n${" " * markOffset}^\n";
+ }
+
+}
+
diff --git a/lib/src/token2.dart b/lib/src/token2.dart
new file mode 100644
index 0000000..682bc89
--- /dev/null
+++ b/lib/src/token2.dart
@@ -0,0 +1,49 @@
+library mustache.token;
+
+
+class TokenType {
+
+ const TokenType(this.name);
+
+ final String name;
+
+ String toString() => '(TokenType $name)';
+
+ static const TokenType text = const TokenType('text');
+ static const TokenType openDelimiter = const TokenType('openDelimiter');
+ static const TokenType closeDelimiter = const TokenType('closeDelimiter');
+
+ // A sigil is the word commonly used to describe the special character at the
+ // start of mustache tag i.e. #, ^ or /.
+ static const TokenType sigil = const TokenType('sigil');
+ static const TokenType identifier = const TokenType('identifier');
+ static const TokenType dot = const TokenType('dot');
+
+ static const TokenType changeDelimiter = const TokenType('changeDelimiter');
+ static const TokenType whitespace = const TokenType('whitespace');
+ static const TokenType lineEnd = const TokenType('lineEnd');
+
+}
+
+
+class Token {
+
+ Token(this.type, this.value, this.start, this.end);
+
+ final TokenType type;
+ final String value;
+
+ final int start;
+ final int end;
+
+ String toString() => "(Token ${type.name} \"$value\" $start $end)";
+
+ // Only used for testing.
+ bool operator ==(o) => o is Token
+ && type == o.type
+ && value == o.value
+ && start == o.start
+ && end == o.end;
+
+ // TODO hashcode. import quiver.
+}
diff --git a/test/parser_test.dart b/test/parser_test.dart
index 25f9aa7..59d6036 100644
--- a/test/parser_test.dart
+++ b/test/parser_test.dart
@@ -1,6 +1,9 @@
import 'package:unittest/unittest.dart';
+import 'package:mustache/src/mustache_impl.dart' show TextNode, VariableNode, SectionNode;
+import 'package:mustache/src/parser.dart';
import 'package:mustache/src/scanner2.dart';
+import 'package:mustache/src/token2.dart';
main() {
@@ -83,28 +86,66 @@
var tokens = scanner.scan();
expect(tokens, orderedEquals([
new Token(TokenType.text, 'abc', 0, 3),
- new Token(TokenType.openTripleMustache, '{{{', 3, 6),
+ new Token(TokenType.openDelimiter, '{{{', 3, 6),
new Token(TokenType.identifier, 'foo', 6, 9),
- new Token(TokenType.closeTripleMustache, '}}}', 9, 12),
+ new Token(TokenType.closeDelimiter, '}}}', 9, 12),
new Token(TokenType.text, 'def', 12, 15)
]));
});
test('scan triple mustache whitespace', () {
- var source = 'abc{{{ foo }}}def';
+ var source = 'abc{{{ foo }}}def';
var scanner = new Scanner(source, 'foo', '{{ }}', lenient: false);
var tokens = scanner.scan();
expect(tokens, orderedEquals([
new Token(TokenType.text, 'abc', 0, 3),
- new Token(TokenType.openTripleMustache, '{{{', 3, 6),
+ new Token(TokenType.openDelimiter, '{{{', 3, 6),
new Token(TokenType.whitespace, ' ', 6, 7),
new Token(TokenType.identifier, 'foo', 7, 10),
new Token(TokenType.whitespace, ' ', 10, 11),
- new Token(TokenType.closeTripleMustache, '}}}', 11, 14),
+ new Token(TokenType.closeDelimiter, '}}}', 11, 14),
new Token(TokenType.text, 'def', 14, 17)
]));
});
+ });
+
+ group('Parser', () {
+
+ test('parse variable', () {
+ var source = 'abc{{foo}}def';
+ var parser = new Parser(source, 'foo', '{{ }}', lenient: false);
+ var nodes = parser.parse();
+ expect(nodes, orderedEquals([
+ new TextNode('abc', 0, 3),
+ new VariableNode('foo', 3, 10, escape: true),
+ new TextNode('def', 10, 13)
+ ]));
+ });
+
+ test('parse variable whitespace', () {
+ var source = 'abc{{ foo }}def';
+ var parser = new Parser(source, 'foo', '{{ }}', lenient: false);
+ var nodes = parser.parse();
+ expect(nodes, orderedEquals([
+ new TextNode('abc', 0, 3),
+ new VariableNode('foo', 3, 12, escape: true),
+ new TextNode('def', 12, 15)
+ ]));
+ });
+
+ test('parse section', () {
+ var source = 'abc{{#foo}}def{{/foo}}ghi';
+ var parser = new Parser(source, 'foo', '{{ }}', lenient: false);
+ var nodes = parser.parse();
+ expect(nodes, orderedEquals([
+ new TextNode('abc', 0, 3),
+ new SectionNode('foo', 3, 11, '{{ }}'),
+ new TextNode('ghi', 22, 25)
+ ]));
+ expect(nodes[1].children, orderedEquals([new TextNode('def', 11, 14)]));
+ });
});
+
}
\ No newline at end of file