| Index: pkg/mustache/lib/template.dart
|
| diff --git a/pkg/mustache/lib/template.dart b/pkg/mustache/lib/template.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..3df8302a8541aa2a11b6b9bafdf42df1007009f4
|
| --- /dev/null
|
| +++ b/pkg/mustache/lib/template.dart
|
| @@ -0,0 +1,298 @@
|
| +part of mustache;
|
| +
|
| +final Object _NO_SUCH_PROPERTY = new Object();
|
| +
|
| +final RegExp _validTag = new RegExp(r'^[0-9a-zA-Z\_\-\.]+$');
|
| +final RegExp _integerTag = new RegExp(r'^[0-9]+$');
|
| +
|
| +Template _parse(String source, {bool lenient : false}) {
|
| + var tokens = _scan(source, lenient);
|
| + var ast = _parseTokens(tokens, lenient);
|
| + return new _Template(ast, lenient);
|
| +}
|
| +
|
| +_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 || t.type == _UNESC_VARIABLE) {
|
| + if (t.type == _VARIABLE || t.type == _UNESC_VARIABLE)
|
| + _checkTagChars(t, lenient);
|
| + stack.last.children.add(new _Node.fromToken(t));
|
| +
|
| + } else if (t.type == _OPEN_SECTION || t.type == _OPEN_INV_SECTION) {
|
| + _checkTagChars(t, lenient);
|
| + var child = new _Node.fromToken(t);
|
| + stack.last.children.add(child);
|
| + stack.add(child);
|
| +
|
| + } else if (t.type == _CLOSE_SECTION) {
|
| + _checkTagChars(t, lenient);
|
| +
|
| + if (stack.last.value != t.value) {
|
| + throw new MustacheFormatException('Mismatched tag, '
|
| + "expected: '${stack.last.value}', "
|
| + "was: '${t.value}', "
|
| + 'at: ${t.line}:${t.column}.', t.line, t.column);
|
| + }
|
| +
|
| + stack.removeLast();
|
| +
|
| + } else if (t.type == _COMMENT) {
|
| + // Do nothing
|
| +
|
| + } else {
|
| + throw new UnimplementedError();
|
| + }
|
| + }
|
| +
|
| + 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(this._root, this._lenient) {
|
| + _htmlEscapeMap[_AMP] = '&';
|
| + _htmlEscapeMap[_LT] = '<';
|
| + _htmlEscapeMap[_GT] = '>';
|
| + _htmlEscapeMap[_QUOTE] = '"';
|
| + _htmlEscapeMap[_APOS] = ''';
|
| + _htmlEscapeMap[_FORWARD_SLASH] = '/';
|
| + }
|
| +
|
| + final _Node _root;
|
| + final List _stack = new List();
|
| + final Map _htmlEscapeMap = new Map<int, String>();
|
| + final bool _lenient;
|
| +
|
| + bool _htmlEscapeValues;
|
| + StringSink _sink;
|
| +
|
| + String renderString(values, {bool lenient : false, bool htmlEscapeValues : true}) {
|
| + var buf = new StringBuffer();
|
| + render(values, buf, lenient: lenient, htmlEscapeValues: htmlEscapeValues);
|
| + return buf.toString();
|
| + }
|
| +
|
| + void render(values, StringSink sink, {bool lenient : false, bool htmlEscapeValues : true}) {
|
| + _sink = sink;
|
| + _htmlEscapeValues = htmlEscapeValues;
|
| + _stack.clear();
|
| + _stack.add(values);
|
| + _root.children.forEach(_renderNode);
|
| + _sink = null;
|
| + }
|
| +
|
| + _write(String output) => _sink.write(output);
|
| +
|
| + _renderNode(node) {
|
| + switch (node.type) {
|
| + case _TEXT:
|
| + _renderText(node);
|
| + break;
|
| + case _VARIABLE:
|
| + _renderVariable(node);
|
| + break;
|
| + case _UNESC_VARIABLE:
|
| + _renderVariable(node, escape: false);
|
| + break;
|
| + case _OPEN_SECTION:
|
| + _renderSection(node);
|
| + break;
|
| + case _OPEN_INV_SECTION:
|
| + _renderInvSection(node);
|
| + break;
|
| + case _COMMENT:
|
| + break; // Do nothing.
|
| + default:
|
| + throw new UnimplementedError();
|
| + }
|
| + }
|
| +
|
| + _renderText(node) {
|
| + _write(node.value);
|
| + }
|
| +
|
| + // Walks up the stack looking for the variable.
|
| + // Handles dotted names of the form "a.b.c".
|
| + _resolveValue(String name) {
|
| + if (name == '.') {
|
| + return _stack.last;
|
| + }
|
| + var parts = name.split('.');
|
| + var object = _NO_SUCH_PROPERTY;
|
| + for (var o in _stack.reversed) {
|
| + object = _getNamedProperty(o, parts[0]);
|
| + if (object != _NO_SUCH_PROPERTY) {
|
| + break;
|
| + }
|
| + }
|
| + for (int i = 1; i < parts.length; i++) {
|
| + if (object == null || object == _NO_SUCH_PROPERTY) {
|
| + return _NO_SUCH_PROPERTY;
|
| + }
|
| + object = _getNamedProperty(object, parts[i]);
|
| + }
|
| + return object;
|
| + }
|
| +
|
| + // Returns the property of the given object by name. For a map,
|
| + // which contains the key name, this is object[name]. For other
|
| + // objects, this is object.name or object.name(). If no property
|
| + // by the given name exists, this method returns _NO_SUCH_PROPERTY.
|
| + _getNamedProperty(object, name) {
|
| + var property = null;
|
| + if (object is Map && object.containsKey(name)) {
|
| + return object[name];
|
| + }
|
| + if (object is List && _integerTag.hasMatch(name)) {
|
| + return object[int.parse(name)];
|
| + }
|
| + if (_lenient && !_validTag.hasMatch(name)) {
|
| + return _NO_SUCH_PROPERTY;
|
| + }
|
| + var instance = reflect(object);
|
| + var field = instance.type.instanceMembers[new Symbol(name)];
|
| + if (field == null) {
|
| + return _NO_SUCH_PROPERTY;
|
| + }
|
| + var invocation = null;
|
| + if ((field is VariableMirror) || ((field is MethodMirror) && (field.isGetter))) {
|
| + invocation = instance.getField(field.simpleName);
|
| + } else if ((field is MethodMirror) && (field.parameters.length == 0)) {
|
| + invocation = instance.invoke(field.simpleName, []);
|
| + }
|
| + if (invocation == null) {
|
| + return _NO_SUCH_PROPERTY;
|
| + }
|
| + return invocation.reflectee;
|
| + }
|
| +
|
| + _renderVariable(node, {bool escape : true}) {
|
| + final value = _resolveValue(node.value);
|
| + if (value == _NO_SUCH_PROPERTY) {
|
| + if (!_lenient)
|
| + throw new MustacheFormatException(
|
| + 'Value was missing, '
|
| + 'variable: ${node.value}, '
|
| + 'at: ${node.line}:${node.column}.', node.line, node.column);
|
| + } else {
|
| + var valueString = (value == null) ? '' : value.toString();
|
| + var output = !escape || !_htmlEscapeValues
|
| + ? valueString
|
| + : _htmlEscape(valueString);
|
| + _write(output);
|
| + }
|
| + }
|
| +
|
| + _renderSectionWithValue(node, value) {
|
| + _stack.add(value);
|
| + node.children.forEach(_renderNode);
|
| + _stack.removeLast();
|
| + }
|
| +
|
| + _renderSection(node) {
|
| + final value = _resolveValue(node.value);
|
| + if (value == null) {
|
| + // Do nothing.
|
| + } else if (value is Iterable) {
|
| + value.forEach((v) => _renderSectionWithValue(node, v));
|
| + } else if (value is Map) {
|
| + _renderSectionWithValue(node, value);
|
| + } else if (value == true) {
|
| + _renderSectionWithValue(node, value);
|
| + } else if (value == false) {
|
| + // Do nothing.
|
| + } else if (value == _NO_SUCH_PROPERTY) {
|
| + if (!_lenient)
|
| + throw new MustacheFormatException(
|
| + 'Value was missing, '
|
| + 'section: ${node.value}, '
|
| + 'at: ${node.line}:${node.column}.', node.line, node.column);
|
| + } else {
|
| + 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 = _resolveValue(node.value);
|
| + if (value == null) {
|
| + _renderSectionWithValue(node, null);
|
| + } else if ((value is Iterable && value.isEmpty) || value == false) {
|
| + _renderSectionWithValue(node, value);
|
| + } else if (value == true || value is Map || value is Iterable) {
|
| + // Do nothing.
|
| + } else if (value == _NO_SUCH_PROPERTY) {
|
| + if (_lenient) {
|
| + _renderSectionWithValue(node, null);
|
| + } else {
|
| + throw new MustacheFormatException(
|
| + 'Value was missing, '
|
| + 'inverse-section: ${node.value}, '
|
| + 'at: ${node.line}:${node.column}.', node.line, node.column);
|
| + }
|
| + } else {
|
| + throw new MustacheFormatException(
|
| + 'Invalid value type for inverse section, '
|
| + 'section: ${node.value}, '
|
| + 'type: ${value.runtimeType}, '
|
| + 'at: ${node.line}:${node.column}.', node.line, node.column);
|
| + }
|
| + }
|
| +
|
| + String _htmlEscape(String s) {
|
| + var buffer = new StringBuffer();
|
| + int startIndex = 0;
|
| + int i = 0;
|
| + for (int c in s.runes) {
|
| + if (c == _AMP
|
| + || c == _LT
|
| + || c == _GT
|
| + || c == _QUOTE
|
| + || c == _APOS
|
| + || c == _FORWARD_SLASH) {
|
| + buffer.write(s.substring(startIndex, i));
|
| + buffer.write(_htmlEscapeMap[c]);
|
| + startIndex = i + 1;
|
| + }
|
| + i++;
|
| + }
|
| + buffer.write(s.substring(startIndex));
|
| + return buffer.toString();
|
| + }
|
| +}
|
| +
|
| +_visit(_Node root, visitor(_Node n)) {
|
| + var _stack = new List<_Node>()..add(root);
|
| + while (!_stack.isEmpty) {
|
| + var node = _stack.removeLast();
|
| + _stack.addAll(node.children);
|
| + 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)}';
|
| +}
|
|
|