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}}",