Combine RenderContext and Renderer
diff --git a/lib/src/lambda_context.dart b/lib/src/lambda_context.dart
index 261e64c..130e445 100644
--- a/lib/src/lambda_context.dart
+++ b/lib/src/lambda_context.dart
@@ -4,7 +4,6 @@
import 'node.dart';
import 'parser.dart' as parser;
-import 'render_context.dart';
import 'renderer.dart';
import 'template_exception.dart';
@@ -12,11 +11,11 @@
class LambdaContext implements m.LambdaContext {
final Node _node;
- final RenderContext _context;
+ final Renderer _renderer;
final bool _isSection;
bool _closed = false;
- LambdaContext(this._node, this._context, {bool isSection: true})
+ LambdaContext(this._node, this._renderer, {bool isSection: true})
: _isSection = isSection;
void close() {
@@ -28,7 +27,7 @@
}
TemplateException _error(String msg) {
- return new TemplateException(msg, _context.templateName, _context.source,
+ return new TemplateException(msg, _renderer.templateName, _renderer.source,
_node.start);
}
@@ -44,10 +43,9 @@
}
void _renderSubtree(StringSink sink, Object value) {
- var ctx = new RenderContext.subtree(_context, sink);
- var renderer = new Renderer(ctx);
+ var renderer = new Renderer.subtree(_renderer, sink);
SectionNode section = _node;
- if (value != null) ctx.push(value);
+ if (value != null) renderer.push(value);
renderer.render(section.children);
}
@@ -55,12 +53,12 @@
_checkClosed();
if (_node is! SectionNode) _error(
'LambdaContext.render() can only be called on section tags.');
- _renderSubtree(_context.sink, value);
+ _renderSubtree(_renderer.sink, value);
}
void write(Object object) {
_checkClosed();
- _context.write(object);
+ _renderer.write(object);
}
/// Get the unevaluated template source for the current section tag.
@@ -77,7 +75,7 @@
if (nodes.length == 1 && nodes.first is TextNode) return nodes.first.text;
- return _context.source.substring(node.contentStart, node.contentEnd);
+ return _renderer.source.substring(node.contentStart, node.contentEnd);
}
/// Evaluate the string as a mustache template using the current context.
@@ -93,20 +91,18 @@
}
var nodes = parser.parse(source,
- _context.lenient,
- _context.templateName,
+ _renderer.lenient,
+ _renderer.templateName,
delimiters);
- var ctx = new RenderContext.lambda(
- _context,
+ var renderer = new Renderer.lambda(
+ _renderer,
source,
- _context.indent,
+ _renderer.indent,
sink,
delimiters);
- var renderer = new Renderer(ctx);
-
- if (value != null) ctx.push(value);
+ if (value != null) renderer.push(value);
renderer.render(nodes);
return sink.toString();
@@ -115,7 +111,7 @@
/// Lookup the value of a variable in the current context.
Object lookup(String variableName) {
_checkClosed();
- return _context.resolveValue(variableName);
+ return _renderer.resolveValue(variableName);
}
}
\ No newline at end of file
diff --git a/lib/src/renderer.dart b/lib/src/renderer.dart
index d3fc0e8..dcc0a96 100644
--- a/lib/src/renderer.dart
+++ b/lib/src/renderer.dart
@@ -1,31 +1,91 @@
library mustache.renderer;
+@MirrorsUsed(metaTargets: const [m.mustache])
+import 'dart:mirrors';
+import 'package:mustache/mustache.dart' as m;
import 'lambda_context.dart';
import 'node.dart';
-import 'render_context.dart';
import 'template.dart';
+import 'template_exception.dart';
+
+final RegExp _validTag = new RegExp(r'^[0-9a-zA-Z\_\-\.]+$');
+final RegExp _integerTag = new RegExp(r'^[0-9]+$');
+
+const Object noSuchProperty = const Object();
class Renderer extends Visitor {
+
+ Renderer(this.sink,
+ List stack,
+ this.lenient,
+ this.htmlEscapeValues,
+ this.partialResolver,
+ this.templateName,
+ this.indent,
+ this.source)
+ : _stack = new List.from(stack);
- Renderer(this.ctx);
-
- //TODO merge classes together.
- RenderContext ctx;
+ Renderer.partial(Renderer ctx, Template partial, String indent)
+ : this(ctx.sink,
+ ctx._stack,
+ ctx.lenient,
+ ctx.htmlEscapeValues,
+ ctx.partialResolver,
+ ctx.templateName,
+ ctx.indent + indent,
+ partial.source);
+ Renderer.subtree(Renderer ctx, StringSink sink)
+ : this(sink,
+ ctx._stack,
+ ctx.lenient,
+ ctx.htmlEscapeValues,
+ ctx.partialResolver,
+ ctx.templateName,
+ ctx.indent,
+ ctx.source);
+
+ Renderer.lambda(
+ Renderer ctx,
+ String source,
+ String indent,
+ StringSink sink,
+ String delimiters)
+ : this(sink,
+ ctx._stack,
+ ctx.lenient,
+ ctx.htmlEscapeValues,
+ ctx.partialResolver,
+ ctx.templateName,
+ ctx.indent + indent,
+ source);
+
+ final StringSink sink;
+ final List _stack;
+ final bool lenient;
+ final bool htmlEscapeValues;
+ final m.PartialResolver partialResolver;
+ final String templateName;
+ final String indent;
+ final String source;
+
+ void push(value) => _stack.add(value);
+
+ Object pop() => _stack.removeLast();
+
+ write(Object output) => sink.write(output.toString());
+
void render(List<Node> nodes) {
- if (ctx.indent == null || ctx.indent == '') {
+ if (indent == null || indent == '') {
nodes.forEach((n) => n.accept(this));
} else if (nodes.isNotEmpty) {
// Special case to make sure there is not an extra indent after the last
- // line in the partial file.
+ // line in the partial file.
+ write(indent);
- ctx.write(ctx.indent);
-
- for (var n in nodes.take(nodes.length - 1)) {
- n.accept(this);
- }
+ nodes.take(nodes.length - 1).forEach((n) => n.accept(this));
var node = nodes.last;
if (node is TextNode) {
@@ -39,37 +99,37 @@
void visitText(TextNode node, {bool lastNode: false}) {
if (node.text == '') return;
- if (ctx.indent == null || ctx.indent == '') {
- ctx.write(node.text);
+ if (indent == null || indent == '') {
+ write(node.text);
} else if (lastNode && node.text.runes.last == _NEWLINE) {
// Don't indent after the last line in a template.
var s = node.text.substring(0, node.text.length - 1);
- ctx.write(s.replaceAll('\n', '\n${ctx.indent}'));
- ctx.write('\n');
+ write(s.replaceAll('\n', '\n${indent}'));
+ write('\n');
} else {
- ctx.write(node.text.replaceAll('\n', '\n${ctx.indent}'));
+ write(node.text.replaceAll('\n', '\n${indent}'));
}
}
void visitVariable(VariableNode node) {
- var value = ctx.resolveValue(node.name);
+ var value = resolveValue(node.name);
if (value is Function) {
- var context = new LambdaContext(node, ctx, isSection: false);
+ var context = new LambdaContext(node, this, isSection: false);
value = value(context);
context.close();
}
if (value == noSuchProperty) {
- if (!ctx.lenient)
- throw ctx.error('Value was missing for variable tag: ${node.name}.',
+ if (!lenient)
+ throw error('Value was missing for variable tag: ${node.name}.',
node);
} else {
var valueString = (value == null) ? '' : value.toString();
- var output = !node.escape || !ctx.htmlEscapeValues
+ var output = !node.escape || !htmlEscapeValues
? valueString
: _htmlEscape(valueString);
- if (output != null) ctx.write(output);
+ if (output != null) write(output);
}
}
@@ -79,7 +139,7 @@
//TODO can probably combine Inv and Normal to shorten.
void _renderSection(SectionNode node) {
- var value = ctx.resolveValue(node.name);
+ var value = resolveValue(node.name);
if (value == null) {
// Do nothing.
@@ -97,25 +157,24 @@
// Do nothing.
} else if (value == noSuchProperty) {
- if (!ctx.lenient)
- throw ctx.error('Value was missing for section tag: ${node.name}.',
- node);
+ if (!lenient)
+ throw error('Value was missing for section tag: ${node.name}.', node);
} else if (value is Function) {
- var context = new LambdaContext(node, ctx, isSection: true);
+ var context = new LambdaContext(node, this, isSection: true);
var output = value(context);
context.close();
- if (output != null) ctx.write(output);
+ if (output != null) write(output);
} else {
- throw ctx.error('Invalid value type for section, '
+ throw error('Invalid value type for section, '
'section: ${node.name}, '
'type: ${value.runtimeType}.', node);
}
}
void _renderInvSection(SectionNode node) {
- var value = ctx.resolveValue(node.name);
+ var value = resolveValue(node.name);
if (value == null) {
_renderWithValue(node, null);
@@ -127,10 +186,10 @@
// Do nothing.
} else if (value == noSuchProperty) {
- if (ctx.lenient) {
+ if (lenient) {
_renderWithValue(node, null);
} else {
- throw ctx.error('Value was missing for inverse section: ${node.name}.', node);
+ throw error('Value was missing for inverse section: ${node.name}.', node);
}
} else if (value is Function) {
@@ -138,7 +197,7 @@
//TODO in strict mode should this be an error?
} else {
- throw ctx.error(
+ throw error(
'Invalid value type for inverse section, '
'section: ${node.name}, '
'type: ${value.runtimeType}.', node);
@@ -146,28 +205,84 @@
}
void _renderWithValue(SectionNode node, value) {
- ctx.push(value);
+ push(value);
node.visitChildren(this);
- ctx.pop();
+ pop();
}
void visitPartial(PartialNode node) {
var partialName = node.name;
- Template template = ctx.partialResolver == null
+ Template template = partialResolver == null
? null
- : ctx.partialResolver(partialName);
+ : partialResolver(partialName);
if (template != null) {
- var partialCtx = new RenderContext.partial(ctx, template, node.indent);
- var renderer = new Renderer(partialCtx);
+ var renderer = new Renderer.partial(this, template, node.indent);
var nodes = getTemplateNodes(template);
renderer.render(nodes);
- } else if (ctx.lenient) {
+ } else if (lenient) {
// do nothing
} else {
- throw ctx.error('Partial not found: $partialName.', node);
+ throw error('Partial not found: $partialName.', node);
}
}
+ // Walks up the stack looking for the variable.
+ // Handles dotted names of the form "a.b.c".
+ Object resolveValue(String name) {
+ if (name == '.') {
+ return _stack.last;
+ }
+ var parts = name.split('.');
+ var object = noSuchProperty;
+ for (var o in _stack.reversed) {
+ object = _getNamedProperty(o, parts[0]);
+ if (object != noSuchProperty) {
+ break;
+ }
+ }
+ for (int i = 1; i < parts.length; i++) {
+ if (object == null || object == noSuchProperty) {
+ return noSuchProperty;
+ }
+ 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 noSuchProperty.
+ _getNamedProperty(object, name) {
+
+ 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 noSuchProperty;
+
+ var instance = reflect(object);
+ var field = instance.type.instanceMembers[new Symbol(name)];
+ if (field == null) return noSuchProperty;
+
+ 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 noSuchProperty;
+ }
+ return invocation.reflectee;
+ }
+
+ m.TemplateException error(String message, Node node)
+ => new TemplateException(message, templateName, source, node.start);
+
static const Map<String,String> _htmlEscapeMap = const {
_AMP: '&',
_LT: '<',
@@ -198,6 +313,7 @@
buffer.write(s.substring(startIndex));
return buffer.toString();
}
+
}
const int _AMP = 38;
diff --git a/lib/src/template.dart b/lib/src/template.dart
index 70ec776..8d85f18 100644
--- a/lib/src/template.dart
+++ b/lib/src/template.dart
@@ -37,9 +37,8 @@
}
void render(values, StringSink sink) {
- var ctx = new RenderContext(sink, [values], _lenient, _htmlEscapeValues,
+ var renderer = new Renderer(sink, [values], _lenient, _htmlEscapeValues,
_partialResolver, _name, '', source);
- var renderer = new Renderer(ctx);
renderer.render(_nodes);
}
}