Refactor renderering
diff --git a/lib/mustache.dart b/lib/mustache.dart
index eab0f6c..197b89a 100644
--- a/lib/mustache.dart
+++ b/lib/mustache.dart
@@ -6,7 +6,7 @@
part 'src/lambda_context.dart';
part 'src/node.dart';
part 'src/parse.dart';
-part 'src/renderer.dart';
+part 'src/render_context.dart';
part 'src/scanner.dart';
part 'src/template.dart';
part 'src/token.dart';
diff --git a/lib/src/lambda_context.dart b/lib/src/lambda_context.dart
index ee5f2f8..2ed1e47 100644
--- a/lib/src/lambda_context.dart
+++ b/lib/src/lambda_context.dart
@@ -4,28 +4,37 @@
class _LambdaContext implements LambdaContext {
final _Node _node;
- final _Renderer _renderer;
+ final _RenderContext _context;
final bool _isSection;
bool _closed = false;
- _LambdaContext(this._node, this._renderer, {bool isSection: true})
+ _LambdaContext(this._node, this._context, {bool isSection: true})
: _isSection = isSection;
void close() {
_closed = true;
}
- _checkClosed() {
- if (_closed) throw new _TemplateException(
- 'LambdaContext accessed outside of callback.',
- _renderer._templateName, _renderer._source, _node.start);
+ void _checkClosed() {
+ if (_closed) throw _error('LambdaContext accessed outside of callback.');
+ }
+
+ _TemplateException _error(String msg) {
+ return new _TemplateException(msg, _context.templateName, _context.source,
+ _node.start);
}
/// Render the current section tag in the current context and return the
/// result as a string.
String renderString() {
+ if (_node is! _SectionNode) _error(
+ 'LambdaContext.renderString() can only be called on section tags.');
_checkClosed();
- return _renderer._renderSubtree(_node);
+ var sink = new StringBuffer();
+ var ctx = new _RenderContext.subtree(_context, sink);
+ _SectionNode section = _node;
+ _renderWithContext(ctx, section.children);
+ return sink.toString();
}
//FIXME Currently only return values are supported.
@@ -53,7 +62,7 @@
if (nodes.length == 1 && nodes.first is _TextNode)
return nodes.first.text;
- var source = _renderer._source.substring(
+ var source = _context.source.substring(
_node.contentStart, _node.contentEnd);
return source;
@@ -63,27 +72,35 @@
String renderSource(String source) {
_checkClosed();
var sink = new StringBuffer();
+
// Lambdas used for sections should parse with the current delimiters.
- var delimiters = _isSection ? _renderer._delimiters : '{{ }}';
- var node = _parse(source,
- _renderer._lenient,
- _renderer._templateName,
+ var delimiters = '{{ }}';
+ if (_node is _SectionNode) {
+ _SectionNode node = _node;
+ delimiters = node.delimiters;
+ }
+
+ var nodes = _parse(source,
+ _context.lenient,
+ _context.templateName,
delimiters);
- var renderer = new _Renderer.lambda(
- _renderer,
- node,
+
+ var ctx = new _RenderContext.lambda(
+ _context,
source,
- _renderer._indent,
+ _context.indent,
sink,
- _renderer._delimiters);
- renderer.render();
+ delimiters);
+
+ _renderWithContext(ctx, nodes);
+
return sink.toString();
}
/// Lookup the value of a variable in the current context.
Object lookup(String variableName) {
_checkClosed();
- return _renderer._resolveValue(variableName);
+ return _context.resolveValue(variableName);
}
}
\ No newline at end of file
diff --git a/lib/src/node.dart b/lib/src/node.dart
index 1366fd0..edd7b4e 100644
--- a/lib/src/node.dart
+++ b/lib/src/node.dart
@@ -1,10 +1,33 @@
part of mustache;
+void _renderWithContext(_RenderContext ctx, List<_Node> nodes) {
+ if (ctx.indent == null || ctx.indent == '') {
+ nodes.forEach((n) => n.render(ctx));
+
+ } else if (nodes.isNotEmpty) {
+ // Special case to make sure there is not an extra indent after the last
+ // line in the partial file.
+
+ ctx.write(ctx.indent);
+
+ for (var n in nodes.take(nodes.length - 1)) {
+ n.render(ctx);
+ }
+
+ var node = nodes.last;
+ if (node is _TextNode) {
+ node.render(ctx, lastNode: true);
+ } else {
+ node.render(ctx);
+ }
+ }
+}
+
abstract class _Node {
_Node(this.start, this.end);
- void render(_Renderer renderer);
+ void render(_RenderContext renderer);
// The offset of the start of the token in the file. Unless this is a section
// or inverse section, then this stores the start of the content of the
@@ -24,16 +47,17 @@
final String text;
- render(_Renderer renderer, {lastNode: false}) {
+ void render(_RenderContext ctx, {lastNode: false}) {
if (text == '') return;
- if (renderer._indent == null || renderer._indent == '') {
- renderer._write(text);
+ if (ctx.indent == null || ctx.indent == '') {
+ ctx.write(text);
} else if (lastNode && text.runes.last == _NEWLINE) {
+ // Don't indent after the last line in a template.
var s = text.substring(0, text.length - 1);
- renderer._write(s.replaceAll('\n', '\n${renderer._indent}'));
- renderer._write('\n');
+ ctx.write(s.replaceAll('\n', '\n${ctx.indent}'));
+ ctx.write('\n');
} else {
- renderer._write(text.replaceAll('\n', '\n${renderer._indent}'));
+ ctx.write(text.replaceAll('\n', '\n${ctx.indent}'));
}
}
}
@@ -46,27 +70,58 @@
final String name;
final bool escape;
- render(_Renderer renderer) {
+ void render(_RenderContext ctx) {
- var value = renderer._resolveValue(name);
+ var value = ctx.resolveValue(name);
if (value is Function) {
- var context = new _LambdaContext(this, renderer, isSection: false);
+ var context = new _LambdaContext(this, ctx, isSection: false);
value = value(context);
context.close();
}
if (value == _noSuchProperty) {
- if (!renderer._lenient)
- throw renderer._error('Value was missing for variable tag: ${name}.', this);
+ if (!ctx.lenient)
+ throw ctx.error('Value was missing for variable tag: ${name}.', this);
} else {
var valueString = (value == null) ? '' : value.toString();
- var output = !escape || !renderer._htmlEscapeValues
+ var output = !escape || !ctx.htmlEscapeValues
? valueString
- : renderer._htmlEscape(valueString);
- renderer._write(output);
+ : _htmlEscape(valueString);
+ ctx.write(output);
}
}
+
+ static const Map<String,String> _htmlEscapeMap = const {
+ _AMP: '&',
+ _LT: '<',
+ _GT: '>',
+ _QUOTE: '"',
+ _APOS: ''',
+ _FORWARD_SLASH: '/'
+ };
+
+ 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();
+ }
}
@@ -84,64 +139,62 @@
final List<_Node> children = <_Node>[];
//TODO can probably combine Inv and Normal to shorten.
- void render(_Renderer renderer) {
- // Keep track of delimiters for use in LambdaContext.renderSource().
- renderer._delimiters = delimiters;
- return inverse ? renderInv(renderer) : renderNormal(renderer);
- }
+ void render(_RenderContext ctx) => inverse
+ ? renderInv(ctx)
+ : renderNormal(ctx);
- void renderNormal(_Renderer renderer) {
- var value = renderer._resolveValue(name);
+ void renderNormal(_RenderContext renderer) {
+ var value = renderer.resolveValue(name);
if (value == null) {
// Do nothing.
} else if (value is Iterable) {
- value.forEach((v) => renderer._renderSectionWithValue(this, v)); //FIXME probably pull this code into the node?
+ value.forEach((v) => _renderWithValue(renderer, v));
} else if (value is Map) {
- renderer._renderSectionWithValue(this, value);
+ _renderWithValue(renderer, value);
} else if (value == true) {
- renderer._renderSectionWithValue(this, value);
+ _renderWithValue(renderer, value);
} else if (value == false) {
// Do nothing.
} else if (value == _noSuchProperty) {
- if (!renderer._lenient)
- throw renderer._error('Value was missing for section tag: ${name}.', this);
+ if (!renderer.lenient)
+ throw renderer.error('Value was missing for section tag: ${name}.', this);
} else if (value is Function) {
var context = new _LambdaContext(this, renderer, isSection: true);
var output = value(context);
context.close();
- renderer._write(output);
+ renderer.write(output);
} else {
- throw renderer._error('Invalid value type for section, '
+ throw renderer.error('Invalid value type for section, '
'section: ${name}, '
'type: ${value.runtimeType}.', this);
}
}
- void renderInv(_Renderer renderer) {
- var value = renderer._resolveValue(name);
+ void renderInv(_RenderContext ctx) {
+ var value = ctx.resolveValue(name);
if (value == null) {
- renderer._renderSectionWithValue(this, null);
+ _renderWithValue(ctx, null);
} else if ((value is Iterable && value.isEmpty) || value == false) {
- renderer._renderSectionWithValue(this, name);
+ _renderWithValue(ctx, name);
} else if (value == true || value is Map || value is Iterable) {
// Do nothing.
} else if (value == _noSuchProperty) {
- if (renderer._lenient) {
- renderer._renderSectionWithValue(this, null);
+ if (ctx.lenient) {
+ _renderWithValue(ctx, null);
} else {
- throw renderer._error('Value was missing for inverse section: ${name}.', this);
+ throw ctx.error('Value was missing for inverse section: ${name}.', this);
}
} else if (value is Function) {
@@ -149,12 +202,18 @@
//TODO in strict mode should this be an error?
} else {
- throw renderer._error(
+ throw ctx.error(
'Invalid value type for inverse section, '
'section: $name, '
'type: ${value.runtimeType}.', this);
}
}
+
+ void _renderWithValue(_RenderContext ctx, value) {
+ ctx.pushValue(value);
+ children.forEach((n) => n.render(ctx));
+ ctx.popValue();
+ }
}
class _PartialNode extends _Node {
@@ -168,19 +227,18 @@
// it's content can be correctly indented.
final String indent;
- void render(_Renderer renderer) {
+ void render(_RenderContext ctx) {
var partialName = name;
- _Template template = renderer._partialResolver == null
+ _Template template = ctx.partialResolver == null
? null
- : renderer._partialResolver(partialName);
+ : ctx.partialResolver(partialName);
if (template != null) {
- var r = new _Renderer.partial(renderer, template, this.indent);
- r.render();
- } else if (renderer._lenient) {
+ var partialCtx = new _RenderContext.partial(ctx, template, this.indent);
+ _renderWithContext(partialCtx, template._nodes);
+ } else if (ctx.lenient) {
// do nothing
} else {
- throw renderer._error('Partial not found: $partialName.', this);
+ throw ctx.error('Partial not found: $partialName.', this);
}
}
}
-
diff --git a/lib/src/parse.dart b/lib/src/parse.dart
index cbaec3e..fe7f870 100644
--- a/lib/src/parse.dart
+++ b/lib/src/parse.dart
@@ -1,6 +1,6 @@
part of mustache;
-_Node _parse(String source,
+List<_Node> _parse(String source,
bool lenient,
String templateName,
String delimiters) {
@@ -73,7 +73,10 @@
}
}
- return stack.last;
+ //FIXME assert stack has only one item and error message if not.
+ // Add test for this.
+
+ return stack.last.children;
}
// Takes a list of tokens, and removes _NEWLINE, and _WHITESPACE tokens.
diff --git a/lib/src/render_context.dart b/lib/src/render_context.dart
new file mode 100644
index 0000000..4f1c8f7
--- /dev/null
+++ b/lib/src/render_context.dart
@@ -0,0 +1,126 @@
+part of mustache;
+
+final RegExp _validTag = new RegExp(r'^[0-9a-zA-Z\_\-\.]+$');
+final RegExp _integerTag = new RegExp(r'^[0-9]+$');
+
+const Object _noSuchProperty = const Object();
+
+class _RenderContext {
+
+ _RenderContext(this._sink,
+ List stack,
+ this.lenient,
+ this.htmlEscapeValues,
+ this.partialResolver,
+ this.templateName,
+ this.indent,
+ this.source)
+ : _stack = new List.from(stack);
+
+ _RenderContext.partial(_RenderContext ctx, _Template partial, String indent)
+ : this(ctx._sink,
+ ctx._stack,
+ ctx.lenient,
+ ctx.htmlEscapeValues,
+ ctx.partialResolver,
+ ctx.templateName,
+ ctx.indent + indent,
+ partial.source);
+
+ _RenderContext.subtree(_RenderContext ctx, StringSink sink)
+ : this(sink,
+ ctx._stack,
+ ctx.lenient,
+ ctx.htmlEscapeValues,
+ ctx.partialResolver,
+ ctx.templateName,
+ ctx.indent,
+ ctx.source);
+
+ _RenderContext.lambda(
+ _RenderContext 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 PartialResolver partialResolver;
+ final String templateName;
+ final String indent;
+ final String source;
+
+ void pushValue(value) => _stack.add(value);
+
+ Object popValue() => _stack.removeLast();
+
+ write(Object output) => _sink.write(output.toString());
+
+ // 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;
+ }
+
+ TemplateException error(String message, _Node node)
+ => new _TemplateException(message, templateName, source, node.start);
+}
diff --git a/lib/src/renderer.dart b/lib/src/renderer.dart
deleted file mode 100644
index f429bf7..0000000
--- a/lib/src/renderer.dart
+++ /dev/null
@@ -1,213 +0,0 @@
-part of mustache;
-
-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 {
-
- _Renderer(this._root,
- this._sink,
- this._values,
- List stack,
- this._lenient,
- this._htmlEscapeValues,
- this._partialResolver,
- this._templateName,
- this._indent,
- this._source,
- this._delimiters)
- : _stack = new List.from(stack);
-
- _Renderer.partial(_Renderer renderer, _Template partial, String indent)
- : this(partial._root,
- renderer._sink,
- renderer._values,
- renderer._stack,
- renderer._lenient,
- renderer._htmlEscapeValues,
- renderer._partialResolver,
- renderer._templateName,
- renderer._indent + indent,
- partial.source,
- '{{ }}');
-
- _Renderer.subtree(_Renderer renderer, _Node node, StringSink sink)
- : this(node,
- sink,
- renderer._values,
- renderer._stack,
- renderer._lenient,
- renderer._htmlEscapeValues,
- renderer._partialResolver,
- renderer._templateName,
- renderer._indent,
- renderer._source,
- '{{ }}');
-
- _Renderer.lambda(
- _Renderer renderer,
- _Node node,
- String source,
- String indent,
- StringSink sink,
- String delimiters)
- : this(node,
- sink,
- renderer._values,
- renderer._stack,
- renderer._lenient,
- renderer._htmlEscapeValues,
- renderer._partialResolver,
- renderer._templateName,
- renderer._indent + indent,
- source,
- delimiters);
-
- final _SectionNode _root;
- final StringSink _sink;
- final _values;
- final List _stack;
- final bool _lenient;
- final bool _htmlEscapeValues;
- final PartialResolver _partialResolver;
- final String _templateName;
- final String _indent;
- final String _source;
-
- // Need to keep track of the current delimiters during rendering.
- // These are used in LambdaContext.renderSource().
- String _delimiters;
-
- void render() {
- if (_indent == null || _indent == '') {
- _root.children.forEach((n) => n.render(this));
- } else {
- _renderWithIndent();
- }
- }
-
- void _renderWithIndent() {
- // Special case to make sure there is not an extra indent after the last
- // line in the partial file.
- var nodes = _root.children;
- if (nodes.isEmpty) return;
-
- _write(_indent);
-
- for (var n in nodes.take(nodes.length - 1)) {
- n.render(this);
- }
-
- var node = _root.children.last;
- if (node is _TextNode) {
- node.render(this, lastNode: true);
- } else {
- node.render(this);
- }
- }
-
- _write(Object output) => _sink.write(output.toString());
-
- // 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 = _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;
- }
-
- void _renderSectionWithValue(node, value) {
- _stack.add(value);
- node.children.forEach((n) => n.render(this));
- _stack.removeLast();
- }
-
- String _renderSubtree(node) {
- var sink = new StringBuffer();
- var renderer = new _Renderer.subtree(this, node, sink);
- renderer.render();
- return sink.toString();
- }
-
- static const Map<String,String> _htmlEscapeMap = const {
- _AMP: '&',
- _LT: '<',
- _GT: '>',
- _QUOTE: '"',
- _APOS: ''',
- _FORWARD_SLASH: '/'
- };
-
- 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();
- }
-
- TemplateException _error(String message, _Node node)
- => new _TemplateException(message, _templateName, _source, node.start);
-}
diff --git a/lib/src/template.dart b/lib/src/template.dart
index 6dbe3b1..1e5643b 100644
--- a/lib/src/template.dart
+++ b/lib/src/template.dart
@@ -8,14 +8,14 @@
String name,
PartialResolver partialResolver})
: source = source,
- _root = _parse(source, lenient, name, '{{ }}'),
+ _nodes = _parse(source, lenient, name, '{{ }}'),
_lenient = lenient,
_htmlEscapeValues = htmlEscapeValues,
_name = name,
_partialResolver = partialResolver;
final String source;
- final _Node _root;
+ final List<_Node> _nodes;
final bool _lenient;
final bool _htmlEscapeValues;
final String _name;
@@ -30,10 +30,9 @@
}
void render(values, StringSink sink) {
- var renderer = new _Renderer(_root, sink, values, [values],
- _lenient, _htmlEscapeValues, _partialResolver, _name, '', source,
- '{{ }}');
- renderer.render();
+ var ctx = new _RenderContext(sink, [values], _lenient, _htmlEscapeValues,
+ _partialResolver, _name, '', source);
+ _renderWithContext(ctx, _nodes);
}
}