Add strict/lenient mode
diff --git a/check.sh b/check.sh
index 04d5741..0bdc6cf 100755
--- a/check.sh
+++ b/check.sh
@@ -1,4 +1,10 @@
+#!/bin/bash
+
+# Abort if non-zero code returned.
+set -e
+
 dart_analyzer --type-checks-for-inferred-types lib/mustache.dart
+dart_analyzer --type-checks-for-inferred-types test/mustache_test.dart
 
 dart --checked test/mustache_test.dart
 
diff --git a/lib/mustache.dart b/lib/mustache.dart
index d1b93f5..cdd604b 100644
--- a/lib/mustache.dart
+++ b/lib/mustache.dart
@@ -9,13 +9,14 @@
 /// Returns a [Template] which can be used to render the template with substituted
 /// values.
 /// Throws [FormatException] if the syntax of the source is invalid.
-Template parse(String source) => new _Template(source);
+Template parse(String source,
+	             {bool lenient : false}) => _parse(source, lenient: lenient);
 
 /// A Template can be used multiple times to [render] content with different values.
 abstract class Template {
 	/// [values] can be a combination of Map, List, String. Any non-String object
 	/// will be converted using toString().
-	String render(values);
+	String render(values, {bool lenient : false});
 }
 
 /// MustacheFormatException can be used to obtain the line and column numbers
diff --git a/lib/scanner.dart b/lib/scanner.dart
index 3ab2fac..5bd1c6a 100644
--- a/lib/scanner.dart
+++ b/lib/scanner.dart
@@ -1,6 +1,6 @@
 part of mustache;

 

-List<_Token> _scan(String source) => new _Scanner(source).scan();

+List<_Token> _scan(String source, bool lenient) => new _Scanner(source).scan();

 

 const int _TEXT = 1;

 const int _VARIABLE = 2;

diff --git a/lib/template.dart b/lib/template.dart
index f2927ad..31102ee 100644
--- a/lib/template.dart
+++ b/lib/template.dart
@@ -1,33 +1,30 @@
 part of mustache;

 

-class _Node {

-	_Node(this.type, this.value, this.line, this.column);

-	_Node.fromToken(_Token token)

-		: type = token.type,

-		  value = token.value,

-		  line = token.line,

-		  column = token.column;

-	final int type;

-	final String value;

-	final int line;

-	final int column;

-	final List<_Node> children = new List<_Node>();

-	String toString() => '_Node: ${tokenTypeString(type)}';

+final RegExp _validTag = new RegExp(r'^[0-9a-zA-Z\_\-]+$');

+

+Template _parse(String source, {bool lenient : false}) {

+	var tokens = _scan(source, lenient);

+	var ast = _parseTokens(tokens, lenient);

+	return new _Template(ast, lenient);

 }

 

-_Node _parse(List<_Token> tokens) {

+_Node _parseTokens(List<_Token> tokens, bool lenient) {

 	var stack = new List<_Node>()..add(new _Node(_OPEN_SECTION, 'root', 0, 0));

 	for (var t in tokens) {

 		if (t.type == _TEXT || t.type == _VARIABLE) {

+			if (t.type == _VARIABLE)

+				_checkTagChars(t, lenient);

 			stack.last.children.add(new _Node.fromToken(t));

+		

 		} else if (t.type == _OPEN_SECTION || t.type == _OPEN_INV_SECTION) {

-			//TODO in strict mode limit characters allowed in tag names.

+			_checkTagChars(t, lenient);

 			var child = new _Node.fromToken(t);

 			stack.last.children.add(child);

 			stack.add(child);

 

 		} else if (t.type == _CLOSE_SECTION) {

-			//TODO in strict mode limit characters allowed in tag names.

+			_checkTagChars(t, lenient);

+

 			if (stack.last.value != t.value) {

 				throw new MustacheFormatException('Mismatched tag, '

 					"expected: '${stack.last.value}', "

@@ -36,6 +33,7 @@
 			}

 

 			stack.removeLast();

+		

 		} else {

 			throw new UnimplementedError();

 		}

@@ -44,9 +42,17 @@
 	return stack.last;

 }

 

+_checkTagChars(_Token t, bool lenient) {

+		if (!lenient && !_validTag.hasMatch(t.value)) {

+			throw new MustacheFormatException(

+				'Tag contained invalid characters in name, '

+				'allowed: 0-9, a-z, A-Z, underscore, and minus, '

+				'at: ${t.line}:${t.column}.', t.line, t.column);

+		}	

+}

+

 class _Template implements Template {

-	_Template(String source) 

-			: _root = _parse(_scan(source)) {

+	_Template(this._root, this._lenient) {

 		_htmlEscapeMap[_AMP] = '&amp;';

 		_htmlEscapeMap[_LT] = '&lt;';

 		_htmlEscapeMap[_GT] = '&gt;';

@@ -56,11 +62,12 @@
 	}

 

 	final _Node _root;

-	final _buffer = new StringBuffer();

-	final _stack = new List();

-	final _htmlEscapeMap = new Map<int, String>();

+	final StringBuffer _buffer = new StringBuffer();

+	final List _stack = new List();

+	final Map _htmlEscapeMap = new Map<int, String>();

+	final bool _lenient;

 

-	render(values) {

+	render(values, {bool lenient : false}) {

 		_buffer.clear();

 		_stack.clear();

 		_stack.add(values);	

@@ -97,9 +104,12 @@
 

 	_renderVariable(node) {

 		final value = _stack.last[node.value];

-

 		if (value == null) {

-			//FIXME in strict mode throw an error.

+			if (!_lenient)

+				throw new MustacheFormatException(

+					'Value was null or missing, '

+					'variable: ${node.value}, '

+					'at: ${node.line}:${node.column}.', node.line, node.column);

 		} else {

 			_write(_htmlEscape(value.toString()));

 		}

@@ -122,24 +132,38 @@
 		} else if (value == false) {

 			// Do nothing.

 		} else if (value == null) {

-			// Do nothing.

-			// FIXME in strict mode, log an error.

+			if (!_lenient)

+				throw new MustacheFormatException(

+					'Value was null or missing, '

+					'section: ${node.value}, '

+					'at: ${node.line}:${node.column}.', node.line, node.column);

 		} else {

-			throw new FormatException("Invalid value type for section: '${node.value}', type: ${value.runtimeType}.");

+			throw new MustacheFormatException(

+				'Invalid value type for section, '

+				'section: ${node.value}, '

+				'type: ${value.runtimeType}, '

+				'at: ${node.line}:${node.column}.', node.line, node.column);

 		}

 	}

 

 	_renderInvSection(node) {

 		final value = _stack.last[node.value];

-		if ((value is List && value.isEmpty)

-				|| value == null

-				|| value == false) {

-			// FIXME in strict mode, log an error if value is null.

+		if ((value is List && value.isEmpty) || value == false) {

 			_renderSectionWithValue(node, {});

 		} else if (value == true || value is Map || value is List) {

 			// Do nothing.

+		} else if (value == null) {

+			if (!_lenient)

+				throw new MustacheFormatException(

+					'Value was null or missing, '

+					'inverse-section: ${node.value}, '

+					'at: ${node.line}:${node.column}.', node.line, node.column);

 		} else {

-			throw new FormatException("Invalid value type for inverse section: '${node.value}', type: ${value.runtimeType}.");	

+			throw new MustacheFormatException(

+				'Invalid value type for inverse section, '

+				'section: ${node.value}, '

+				'type: ${value.runtimeType}, '

+				'at: ${node.line}:${node.column}.', node.line, node.column);

 		}

 	}

 

@@ -173,3 +197,18 @@
 		visitor(node);

 	}

 }

+

+class _Node {

+	_Node(this.type, this.value, this.line, this.column);

+	_Node.fromToken(_Token token)

+		: type = token.type,

+		  value = token.value,

+		  line = token.line,

+		  column = token.column;

+	final int type;

+	final String value;

+	final int line;

+	final int column;

+	final List<_Node> children = new List<_Node>();

+	String toString() => '_Node: ${tokenTypeString(type)}';

+}
\ No newline at end of file
diff --git a/test/mustache_test.dart b/test/mustache_test.dart
index 5ab27fa..cde7bb8 100644
--- a/test/mustache_test.dart
+++ b/test/mustache_test.dart
@@ -5,8 +5,10 @@
 
 const MISMATCHED_TAG = 'Mismatched tag';
 const UNEXPECTED_EOF = 'Unexpected end of input';
-const INVALID_VALUE_SECTION = 'Invalid value type for section';
-const INVALID_VALUE_INV_SECTION = 'Invalid value type for inverse section';
+const BAD_VALUE_SECTION = 'Invalid value type for section';
+const BAD_VALUE_INV_SECTION = 'Invalid value type for inverse section';
+const BAD_TAG_NAME = 'Tag contained invalid characters in name';
+const VALUE_NULL = 'Value was null or missing';
 
 main() {
 	group('Section', () {
@@ -30,17 +32,12 @@
 				.render({"section": false});
 			expect(output, equals(''));
 		});
-		test('Null', () {
-			var output = parse('{{#section}}_{{var}}_{{/section}}')
-				.render({"section": null});
-			expect(output, equals(''));
-		});
 		test('Invalid value', () {
 			var ex = renderFail(
 				'{{#section}}_{{var}}_{{/section}}',
 				{"section": 42});
 			expect(ex is FormatException, isTrue);
-			expect(ex.message, startsWith(INVALID_VALUE_SECTION));
+			expect(ex.message, startsWith(BAD_VALUE_SECTION));
 		});
 		test('True', () {
 			var output = parse('{{#section}}_ok_{{/section}}')
@@ -48,12 +45,12 @@
 			expect(output, equals('_ok_'));
 		});
 		test('Nested', () {
-			var output = parse('{{#section}}.{{var}}.{{#nested}}_{{nested_var}}_{{/nested}}.{{/section}}')
+			var output = parse('{{#section}}.{{var}}.{{#nested}}_{{nestedvar}}_{{/nested}}.{{/section}}')
 				.render({"section": {
 					"var": "bob",
 					"nested": [
-						{"nested_var": "jim"},
-						{"nested_var": "sally"}
+						{"nestedvar": "jim"},
+						{"nestedvar": "sally"}
 					]
 				}});
 			expect(output, equals('.bob._jim__sally_.'));
@@ -81,17 +78,12 @@
 				.render({"section": false});
 			expect(output, equals('_ok_'));
 		});
-		test('Null', () {
-			var output = parse('{{^section}}_ok_{{/section}}')
-				.render({"section": null});
-			expect(output, equals('_ok_'));
-		});
 		test('Invalid value', () {
 			var ex = renderFail(
 				'{{^section}}_{{var}}_{{/section}}',
 				{"section": 42});
 			expect(ex is FormatException, isTrue);
-			expect(ex.message, startsWith(INVALID_VALUE_INV_SECTION));
+			expect(ex.message, startsWith(BAD_VALUE_INV_SECTION));
 		});
 		test('True', () {
 			var output = parse('{{^section}}_ok_{{/section}}')
@@ -154,15 +146,84 @@
 
 	group('Invalid format', () {
 		test('Mismatched tag', () {
-			var source = '{{#section}}_{{var}}_{{/not_section}}';
+			var source = '{{#section}}_{{var}}_{{/notsection}}';
 			var ex = renderFail(source, {"section": {"var": "bob"}});
 			expectFail(ex, 1, 25, 'Mismatched tag');
 		});
+
 		test('Unexpected EOF', () {
-			var source = '{{#section}}_{{var}}_{{/not_section';
+			var source = '{{#section}}_{{var}}_{{/section';
 			var ex = renderFail(source, {"section": {"var": "bob"}});
 			expectFail(ex, 1, source.length + 2, UNEXPECTED_EOF);
 		});
+
+		test('Bad tag name, open section', () {
+			var source = r'{{#section$%$^%}}_{{var}}_{{/section}}';
+			var ex = renderFail(source, {"section": {"var": "bob"}});
+			expectFail(ex, null, null, BAD_TAG_NAME);
+		});
+
+		test('Bad tag name, close section', () {
+			var source = r'{{#section}}_{{var}}_{{/section$%$^%}}';
+			var ex = renderFail(source, {"section": {"var": "bob"}});
+			expectFail(ex, null, null, BAD_TAG_NAME);
+		});
+
+		test('Bad tag name, variable', () {
+			var source = r'{{#section}}_{{var$%$^%}}_{{/section}}';
+			var ex = renderFail(source, {"section": {"var": "bob"}});
+			expectFail(ex, null, null, BAD_TAG_NAME);
+		});
+
+		test('Null section', () {
+			var source = '{{#section}}_{{var}}_{{/section}}';
+			var ex = renderFail(source, {'section': null});
+			expectFail(ex, null, null, VALUE_NULL);
+		});
+
+		test('Null inverse section', () {
+			var source = '{{^section}}_{{var}}_{{/section}}';
+			var ex = renderFail(source, {'section': null});
+			expectFail(ex, null, null, VALUE_NULL);
+		});
+
+		test('Null variable', () {
+			var source = '{{#section}}_{{var}}_{{/section}}';
+			var ex = renderFail(source, {'section': {'var': null}});
+			expectFail(ex, null, null, VALUE_NULL);
+		});		
+	});
+
+	group('Lenient', () {
+		test('Odd section name', () {
+			var output = parse(r'{{#section$%$^%}}_{{var}}_{{/section$%$^%}}', lenient: true)
+				.render({r'section$%$^%': {'var': 'bob'}}, lenient: true);
+			expect(output, equals('_bob_'));
+		});
+
+		test('Odd variable name', () {
+			var output = parse(r'{{#section}}_{{var$%$^%}}_{{/section}}', lenient: true)
+				.render({'section': {r'var$%$^%': 'bob'}}, lenient: true);
+		});
+
+		test('Null variable', () {
+			var output = parse(r'{{#section}}_{{var}}_{{/section}}', lenient: true)
+				.render({'section': {'var': null}}, lenient: true);
+			expect(output, equals('__'));
+		});
+
+		test('Null section', () {
+			var output = parse('{{#section}}_{{var}}_{{/section}}', lenient: true)
+				.render({"section": null}, lenient: true);
+			expect(output, equals(''));
+		});
+
+		test('Null inverse section', () {
+			var output = parse('{{^section}}_{{var}}_{{/section}}', lenient: true)
+				.render({"section": null}, lenient: true);
+			expect(output, equals(''));
+		});
+
 	});
 
 }
@@ -179,8 +240,10 @@
 expectFail(ex, int line, int column, [String msgStartsWith]) {
 		expect(ex, isFormatException);
 		expect(ex is MustacheFormatException, isTrue);
-		expect(ex.line, equals(line));
-		expect(ex.column, equals(column));
+		if (line != null)
+			expect(ex.line, equals(line));
+		if (column != null)
+			expect(ex.column, equals(column));
 		if (msgStartsWith != null)
 			expect(ex.message, startsWith(msgStartsWith));
 }
\ No newline at end of file