Match whitespace handing with python and node mustache implementations
diff --git a/lib/src/scanner.dart b/lib/src/scanner.dart
index 32b03dc..83be433 100644
--- a/lib/src/scanner.dart
+++ b/lib/src/scanner.dart
@@ -2,6 +2,7 @@
List<_Token> _scan(String source, bool lenient) => _trim(new _Scanner(source).scan());
+//FIXME use enums
const int _TEXT = 1;
const int _VARIABLE = 2;
const int _PARTIAL = 3;
@@ -13,7 +14,6 @@
const int _WHITESPACE = 9; // Should be filtered out, before returned by scan.
const int _LINE_END = 10; // Should be filtered out, before returned by scan.
-//FIXME make private
_tokenTypeString(int type) => [
'?',
'Text',
@@ -186,11 +186,6 @@
&& c != _EOF
&& c != _NEWLINE);
- // Actually excludes newlines.
- String _readWhitespace() => _r.readWhile(
- (c) => c == _SPACE
- || c == _TAB);
-
List<_Token> scan() {
while(true) {
switch(_peek()) {
@@ -231,7 +226,7 @@
break;
case _SPACE:
case _TAB:
- var value = _readWhitespace();
+ var value = _r.readWhile((c) => c == _SPACE || c == _TAB);
_tokens.add(new _Token(_WHITESPACE, value, _r.line, _r.column));
break;
default:
@@ -251,18 +246,24 @@
_expect(_OPEN_MUSTACHE);
+ // Escaped text {{{ ... }}}
+ if (_peek() == _OPEN_MUSTACHE) {
+ _read();
+ _addStringToken(_UNESC_VARIABLE);
+ _expect(_CLOSE_MUSTACHE);
+ _expect(_CLOSE_MUSTACHE);
+ _expect(_CLOSE_MUSTACHE);
+ return;
+ }
+
+ // Skip whitespace at start of tag. i.e. {{ # foo }} {{ / foo }}
+ _r.readWhile((c) => const [_SPACE, _TAB , _NEWLINE, _RETURN].contains(c));
+
switch(_peek()) {
case _EOF:
throw new TemplateException('Unexpected end of input',
_templateName, _r.line, _r.column);
-
- // Escaped text {{{ ... }}}
- case _OPEN_MUSTACHE:
- _read();
- _addStringToken(_UNESC_VARIABLE);
- _expect(_CLOSE_MUSTACHE);
- break;
-
+
// Escaped text {{& ... }}
case _AMP:
_read();
diff --git a/lib/src/template.dart b/lib/src/template.dart
index 2a67b3b..bec14a2 100644
--- a/lib/src/template.dart
+++ b/lib/src/template.dart
@@ -7,6 +7,7 @@
_Node _parseTokens(List<_Token> tokens, bool lenient, String templateName) {
var stack = new List<_Node>()..add(new _Node(_OPEN_SECTION, 'root', 0, 0));
+
for (var t in tokens) {
if (const [_TEXT, _VARIABLE, _UNESC_VARIABLE, _PARTIAL].contains(t.type)) {
if (t.type == _VARIABLE || t.type == _UNESC_VARIABLE)
@@ -346,7 +347,9 @@
_renderPartial(_Node node) {
var partialName = node.value;
- _Template template = _partialResolver(partialName);
+ _Template template = _partialResolver == null
+ ? null
+ : _partialResolver(partialName);
if (template != null) {
var renderer = new _Renderer.partial(this, template);
renderer.render();
diff --git a/test/download-spec.sh b/test/download-spec.sh
new file mode 100755
index 0000000..e36cb5b
--- /dev/null
+++ b/test/download-spec.sh
@@ -0,0 +1 @@
+git clone https://github.com/mustache/spec.git
diff --git a/test/mustache_specs.dart b/test/mustache_specs.dart
new file mode 100644
index 0000000..759072b
--- /dev/null
+++ b/test/mustache_specs.dart
@@ -0,0 +1,107 @@
+// Specification files can be downloaded here https://github.com/mustache/spec
+
+// Originally implemented by
+
+library mustache_specs;
+
+import 'dart:io';
+import 'dart:convert';
+import 'package:unittest/unittest.dart';
+import 'package:mustache/mustache.dart';
+
+String render(source, values, {partial}) {
+ var resolver = null;
+ resolver = (name) => new Template(partial(name), partialResolver: resolver, lenient: true);
+ var t = new Template(source, partialResolver: resolver, lenient: true);
+ return t.renderString(values);
+}
+
+main() {
+ defineTests();
+}
+
+defineTests () {
+ var specs_dir = new Directory('spec/specs');
+ specs_dir
+ .listSync()
+ .forEach((File f) {
+ var filename = f.path;
+ if (shouldRun(filename)) {
+ var text = f.readAsStringSync(encoding: UTF8);
+ _defineGroupFromFile(filename, text);
+ }
+ });
+}
+
+_defineGroupFromFile(filename, text) {
+ var json = JSON.decode(text);
+ var tests = json['tests'];
+ filename = filename.substring(filename.lastIndexOf('/') + 1);
+ group("Specs of $filename", () {
+
+ //Make sure that we reset the state of the Interpolation - Multiple Calls test
+ //as for some reason dart can run the group more than once causing the test
+ //to fail the second time it runs
+ tearDown (() =>lambdas['Interpolation - Multiple Calls'].reset());
+
+ tests.forEach( (t) {
+ var testDescription = new StringBuffer(t['name']);
+ testDescription.write(': ');
+ testDescription.write(t['desc']);
+ var template = t['template'];
+ var data = t['data'];
+ var templateOneline = template.replaceAll('\n', '\\n').replaceAll('\r', '\\r');
+ var reason = new StringBuffer("Could not render right '''$templateOneline'''");
+ var expected = t['expected'];
+ var partials = t['partials'];
+ var partial = (String name) {
+ if (partials == null) {
+ return null;
+ }
+ return partials[name];
+ };
+
+ //swap the data.lambda with a dart real function
+ if (data['lambda'] != null) {
+ data['lambda'] = lambdas[t['name']];
+ }
+ reason.write(" with '$data'");
+ if (partials != null) {
+ reason.write(" and partial: $partials");
+ }
+ test(testDescription.toString(), () => expect(render(template, data, partial: partial), expected, reason: reason.toString()));
+ });
+ });
+}
+
+bool shouldRun(String filename) {
+ // filter out only .json files
+ if (!filename.endsWith('.json')) {
+ return false;
+ }
+ return true;
+}
+
+//Until we'll find a way to load a piece of code dynamically,
+//we provide the lambdas at the test here
+class _DummyCallableWithState {
+ var _callCounter = 0;
+
+ call (arg) => "${++_callCounter}";
+
+ reset () => _callCounter = 0;
+}
+
+var lambdas = {
+ 'Interpolation' : (t) => 'world',
+ 'Interpolation - Expansion': (t) => '{{planet}}',
+ 'Interpolation - Alternate Delimiters': (t) => "|planet| => {{planet}}",
+ 'Interpolation - Multiple Calls': new _DummyCallableWithState(), //function() { return (g=(function(){return this})()).calls=(g.calls||0)+1 }
+ 'Escaping': (t) => '>',
+ 'Section': (txt) => txt == "{{x}}" ? "yes" : "no",
+ 'Section - Expansion': (txt) => "$txt{{planet}}$txt",
+ 'Section - Alternate Delimiters': (txt) => "$txt{{planet}} => |planet|$txt",
+ 'Section - Multiple Calls': (t) => "__${t}__",
+ 'Inverted Section': (txt) => false
+
+};
diff --git a/test/mustache_test.dart b/test/mustache_test.dart
index 67f8c92..4f25776 100644
--- a/test/mustache_test.dart
+++ b/test/mustache_test.dart
@@ -21,7 +21,7 @@
expect(output, equals('_bob_'));
});
test('Comment', () {
- var output = parse('_{{! i am a comment ! }}_').renderString({});
+ var output = parse('_{{! i am a\n comment ! }}_').renderString({});
expect(output, equals('__'));
});
});
@@ -87,6 +87,71 @@
expect(parse('{{foo.bar }}').renderString({'foo': {'bar': true}}), equals('true'));
expect(parse('{{ foo.bar }}').renderString({'foo': {'bar': true}}), equals('true'));
});
+
+
+
+ test('Odd whitespace in tags', () {
+
+ render(source, values, output)
+ => expect(parse(source, lenient: true)
+ .renderString(values), equals(output));
+
+ render(
+ "{{\t# foo}}oi{{\n/foo}}",
+ {'foo': true},
+ 'oi');
+
+ render(
+ "{{ # # foo }} {{ oi }} {{ / # foo }}",
+ {'# foo': [{'oi': 'OI!'}]},
+ ' OI! ');
+
+ render(
+ "{{ #foo }} {{ oi }} {{ /foo }}",
+ {'foo': [{'oi': 'OI!'}]},
+ ' OI! ');
+
+ render(
+ "{{\t#foo }} {{ oi }} {{ /foo }}",
+ {'foo': [{'oi': 'OI!'}]},
+ ' OI! ');
+
+ render(
+ "{{{ #foo }}} {{{ /foo }}}",
+ {'#foo': 1, '/foo': 2},
+ '1 2');
+
+// Invalid - I'm ok with that for now.
+// render(
+// "{{{ { }}}",
+// {'{': 1},
+// '1');
+
+ render(
+ "{{\nfoo}}",
+ {'foo': 'bar'},
+ 'bar');
+
+ render(
+ "{{\tfoo}}",
+ {'foo': 'bar'},
+ 'bar');
+
+ render(
+ "{{\t# foo}}oi{{\n/foo}}",
+ {'foo': true},
+ 'oi');
+
+ render(
+ "{{{\tfoo\t}}}",
+ {'foo': true},
+ 'true');
+
+ render(
+ "{{ > }}",
+ {'>': 'oi'},
+ '');
+ });
});
group('Inverse Section', () {
diff --git a/test/no_spec/whitespace.js b/test/no_spec/whitespace.js
index 71352c9..598ca1a 100644
--- a/test/no_spec/whitespace.js
+++ b/test/no_spec/whitespace.js
@@ -23,8 +23,8 @@
{'{': 1}); // 1
render(
- "{{ > }}}",
- {'>': 'oi'}); // "}" bug??
+ "{{ > }}",
+ {'>': 'oi'}); // ''
render(
"{{\nfoo}}",