blob: 9b346eeaa860e39615112f0bdfb782f9fc9e3623 [file] [log] [blame]
part of mustache;
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 _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] = '&amp;';
_htmlEscapeMap[_LT] = '&lt;';
_htmlEscapeMap[_GT] = '&gt;';
_htmlEscapeMap[_QUOTE] = '&quot;';
_htmlEscapeMap[_APOS] = '&#x27;';
_htmlEscapeMap[_FORWARD_SLASH] = '&#x2F;';
}
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) {
var parts = name.split('.');
var map = _stack
.reversed
.firstWhere(
(v) => v is Map && v.containsKey(parts[0]),
orElse: () => null);
if (map == null)
return null;
var v;
for (int i = 0; i < parts.length; i++) {
v = map[parts[i]];
if (v == null) {
return null;
} else if (v is Map) {
map = v;
} else if (i < parts.length - 1) {
return null;
}
}
return v;
}
_renderVariable(node, {bool escape : true}) {
final value = _resolveValue(node.value);
if (value == null) {
if (!_lenient)
throw new MustacheFormatException(
'Value was null or missing, '
'variable: ${node.value}, '
'at: ${node.line}:${node.column}.', node.line, node.column);
} else {
var output = !escape || !_htmlEscapeValues
? value.toString()
: _htmlEscape(value.toString());
_write(output);
}
}
_renderSectionWithValue(node, value) {
_stack.add(value);
node.children.forEach(_renderNode);
_stack.removeLast();
}
_renderSection(node) {
final value = _resolveValue(node.value);
if (value is List) {
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 == null) {
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 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 is List && value.isEmpty) || value == false) {
_renderSectionWithValue(node, value);
} else if (value == true || value is Map || value is List) {
// Do nothing.
} else if (value == null) {
if (_lenient) {
_renderSectionWithValue(node, value);
} else {
throw new MustacheFormatException(
'Value was null or 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)}';
}