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