Change TemplateException
diff --git a/lib/mustache.dart b/lib/mustache.dart
index a187b7a..94758e9 100644
--- a/lib/mustache.dart
+++ b/lib/mustache.dart
@@ -52,45 +52,6 @@
 abstract class MustacheFormatException implements FormatException {  
 }
 
-
-/// [TemplateException] is used to obtain the line and column numbers
-/// of the token which caused parse or render to fail.
-class TemplateException implements MustacheFormatException, Exception {
-  
-  factory TemplateException(
-      String message, String template, int line, int column) {
-    
-    var at = template == null
-        ? '$line:$column'
-        : '$template:$line:$column';
-    
-    return new TemplateException._private(
-      '$message, at: $at.', template, line, column);
-  }  
-  
-  TemplateException._private(
-      this.message, this.templateName, this.line, this.column);
-  
-	final String message;
-
-	final String templateName;
-	
-	/// The 1-based line number of the token where formatting error was found.
-	final int line;
-
-	/// The 1-based column number of the token where formatting error was found.
-	final int column;	
-	
-	String toString() => message;
-	
-	@deprecated
-	get source => '';
-	
-	@deprecated
-	get offset => 1;
-}
-
-
 typedef Template PartialResolver(String templateName);
 
 typedef Object LambdaFunction(LambdaContext context);
@@ -128,3 +89,34 @@
 class MustacheMirrorsUsedAnnotation {
   const MustacheMirrorsUsedAnnotation();
 }
+
+
+/// [TemplateException] is used to obtain the line and column numbers
+/// of the token which caused parse or render to fail.
+abstract class TemplateException implements MustacheFormatException, Exception {
+  
+  /// A message describing the problem parsing or rendering the template.
+  String get message;
+
+  /// The name used to identify the template, as passed to the Template
+  /// constructor.
+  String get templateName;
+  
+  /// The 1-based line number of the token where formatting error was found.
+  int get line;
+
+  /// The 1-based column number of the token where formatting error was found.
+  int get column; 
+  
+  /// The character offset within the template source.
+  int get offset;
+
+  /// The template source.
+  String get source;
+  
+  /// A short source substring of the source at the point the problem occurred
+  /// with parsing or rendering.
+  String get context;
+  
+  String toString();
+}
diff --git a/lib/src/char_reader.dart b/lib/src/char_reader.dart
index 42cbce8..66740f9 100644
--- a/lib/src/char_reader.dart
+++ b/lib/src/char_reader.dart
@@ -54,8 +54,8 @@
     //FIXME provide template name. Or perhaps this is a programmer error
     // and this shouldn't actually happen.
     if (peek() == _EOF)
-      throw new TemplateException(
-          'Unexpected end of input', null, line, column);
+      throw new _TemplateException(
+          'Unexpected end of input', null, null, 0);
     
     int start = _i;
     
diff --git a/lib/src/lambda_context.dart b/lib/src/lambda_context.dart
index c52a2a7..80c89df 100644
--- a/lib/src/lambda_context.dart
+++ b/lib/src/lambda_context.dart
@@ -16,9 +16,9 @@
   }
   
   _checkClosed() {
-    if (_closed) throw new TemplateException(
+    if (_closed) throw new _TemplateException(
         'LambdaContext accessed outside of callback.', 
-        _renderer._templateName, _node.line, _node.column);
+        _renderer._templateName, _renderer._source, _node.start);
   }
   
   /// Render the current section tag in the current context and return the
diff --git a/lib/src/parse.dart b/lib/src/parse.dart
index 96d6033..40ecd0d 100644
--- a/lib/src/parse.dart
+++ b/lib/src/parse.dart
@@ -15,9 +15,19 @@
   
   tokens = _removeStandaloneWhitespace(tokens);
   tokens = _mergeAdjacentText(tokens);
+
+  checkTagChars(_Token t) {
+      if (!lenient && !_validTag.hasMatch(t.value)) {
+        throw new _TemplateException(
+          'Tag contained invalid characters in name, '
+          'allowed: 0-9, a-z, A-Z, underscore, and minus',
+          templateName, source, t.offset);
+      }
+  }
+
   
   var stack = new List<_Node>()..add(new _Node(_OPEN_SECTION, 'root', 0, 0));
-  
+
   for (var t in tokens) {
     switch (t.type) {
       case _TEXT:
@@ -25,13 +35,13 @@
       case _UNESC_VARIABLE:
       case _PARTIAL:
         if (t.type == _VARIABLE || t.type == _UNESC_VARIABLE)
-          _checkTagChars(t, lenient, templateName);
+          checkTagChars(t);
         stack.last.children.add(new _Node.fromToken(t));
         break;
 
       case _OPEN_SECTION:
       case _OPEN_INV_SECTION:
-        _checkTagChars(t, lenient, templateName);
+        checkTagChars(t);
         var child = new _Node.fromToken(t);
         child.start = t.offset;
         stack.last.children.add(child);
@@ -39,12 +49,12 @@
         break;
 
       case _CLOSE_SECTION:
-        _checkTagChars(t, lenient, templateName);
+        checkTagChars(t);
 
         if (stack.last.value != t.value) {
-          throw new TemplateException(
+          throw new _TemplateException(
             "Mismatched tag, expected: '${stack.last.value}', was: '${t.value}'",
-            templateName, t.line, t.column);
+            templateName, source, t.offset);
         }
   
         stack.last.end = t.offset;
@@ -69,15 +79,6 @@
   return stack.last;
 }
 
-_checkTagChars(_Token t, bool lenient, String templateName) {
-    if (!lenient && !_validTag.hasMatch(t.value)) {
-      throw new TemplateException(
-        'Tag contained invalid characters in name, '
-        'allowed: 0-9, a-z, A-Z, underscore, and minus',
-        templateName, t.line, t.column);
-    }
-}
-
 // 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
diff --git a/lib/src/renderer.dart b/lib/src/renderer.dart
index 0f00b78..198b15b 100644
--- a/lib/src/renderer.dart
+++ b/lib/src/renderer.dart
@@ -213,10 +213,8 @@
     }
     
     if (value == _noSuchProperty) {
-      if (!_lenient)
-        throw new TemplateException(
-          'Value was missing, variable: ${node.value}',
-          _templateName, node.line, node.column);
+      if (!_lenient) 
+        throw _error('Value was missing, variable: ${node.value}', node);
     } else {
       var valueString = (value == null) ? '' : value.toString();
       var output = !escape || !_htmlEscapeValues
@@ -259,9 +257,7 @@
     
     } else if (value == _noSuchProperty) {
       if (!_lenient)
-        throw new TemplateException(
-          'Value was missing, section: ${node.value}',
-          _templateName, node.line, node.column);
+        throw _error('Value was missing, section: ${node.value}', node);
     
     } else if (value is Function) {
       var context = new _LambdaContext(node, this, isSection: true);
@@ -270,11 +266,9 @@
       _write(output);
       
     } else {
-      throw new TemplateException(
-        'Invalid value type for section, '
+      throw _error('Invalid value type for section, '
         'section: ${node.value}, '
-        'type: ${value.runtimeType}',
-        _templateName, node.line, node.column);
+        'type: ${value.runtimeType}', node);
     }
   }
 
@@ -294,20 +288,18 @@
       if (_lenient) {
         _renderSectionWithValue(node, null);
       } else {
-        throw new TemplateException(
-            'Value was missing, inverse-section: ${node.value}',
-            _templateName, node.line, node.column);
+        throw _error('Value was missing, inverse-section: ${node.value}', node);
       }
 
      } else if (value is Function) {       
       // Do nothing.
+       //TODO in strict mode should this be an error?
 
     } else {
-      throw new TemplateException(
+      throw _error(
         'Invalid value type for inverse section, '
         'section: ${node.value}, '
-        'type: ${value.runtimeType}, ',
-        _templateName, node.line, node.column);
+        'type: ${value.runtimeType}, ', node);
     }
   }
 
@@ -322,9 +314,7 @@
     } else if (_lenient) {
       // do nothing
     } else {
-      throw new TemplateException(
-          'Partial not found: $partialName',
-          _templateName, node.line, node.column);
+      throw _error('Partial not found: $partialName.', node);
     }
   }
 
@@ -358,4 +348,7 @@
     buffer.write(s.substring(startIndex));
     return buffer.toString();
   }
+  
+  TemplateException _error(String message, _Node node)
+    => new _TemplateException(message, _templateName, _source, node.start);
 }
diff --git a/lib/src/scanner.dart b/lib/src/scanner.dart
index bf1dacf..1a1c01c 100644
--- a/lib/src/scanner.dart
+++ b/lib/src/scanner.dart
@@ -24,6 +24,7 @@
   

 	_Scanner(String source, this._templateName, String delimiters, {bool lenient: true})

 	 : _r = new _CharReader(source),

+	   _source = source,

 	   _lenient = lenient {

 	  

 	  var delims = _parseDelimiterString(delimiters);

@@ -34,6 +35,7 @@
 	}

 

 	final String _templateName;

+	final String _source;

 	

 	//FIXME not used yet.

 	final bool _lenient;

@@ -97,14 +99,14 @@
 		int c = _read();

 

 		if (c == _EOF) {

-			throw new TemplateException('Unexpected end of input',

-			    _templateName, _r.line, _r.column);

+			throw new _TemplateException('Unexpected end of input',

+			    _templateName, _source, _r.offset);

 

 		} else if (c != expectedCharCode) {

-			throw new TemplateException('Unexpected character, '

+			throw new _TemplateException('Unexpected character, '

 				'expected: ${new String.fromCharCode(expectedCharCode)} ($expectedCharCode), '

 				'was: ${new String.fromCharCode(c)} ($c)', 

-				_templateName, _r.line, _r.column);

+				_templateName, _source, _r.offset);

 		}

 	}

 

@@ -258,8 +260,8 @@
 		

 		switch(_peek()) {

 			case _EOF:

-				throw new TemplateException('Unexpected end of input',

-				    _templateName, _r.line, _r.column);

+				throw new _TemplateException('Unexpected end of input',

+				    _templateName, _source,  _r.offset);

   			

 			// Escaped text {{& ... }}

 			case _AMP:

diff --git a/lib/src/template.dart b/lib/src/template.dart
index 3574981..921dc9d 100644
--- a/lib/src/template.dart
+++ b/lib/src/template.dart
@@ -36,3 +36,112 @@
     renderer.render();

   }

 }

+

+class _TemplateException implements 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