Quick hacky helpers impl - lenient mode only
diff --git a/lib/mustache.dart b/lib/mustache.dart
index 742370c..e66e447 100644
--- a/lib/mustache.dart
+++ b/lib/mustache.dart
@@ -23,26 +23,27 @@
abstract class Template {
/// The constructor parses the template source and throws [TemplateException]
- /// if the syntax of the source is invalid.
- /// Tag names may only contain characters a-z, A-Z, 0-9, underscore, and minus,
- /// unless lenient mode is specified.
+ /// if the syntax of the source is invalid. Tag names may only contain the
+ /// characters a-z, A-Z, 0-9, underscore, minus, and period unless lenient
+ /// mode is specified.
factory Template(String source,
- {bool lenient,
- bool htmlEscapeValues,
- String name,
- PartialResolver partialResolver}) = _Template.fromSource;
+ {String name,
+ bool lenient,
+ bool htmlEscapeValues,
+ PartialResolver partialResolver,
+ Map<String,LambdaFunction> helpers}) = _Template.fromSource;
String get name;
String get source;
/// [values] can be a combination of Map, List, String. Any non-String object
- /// will be converted using toString(). Null values will cause a
+ /// will be converted using toString(). Missing values will cause a
/// [TemplateException], unless lenient module is enabled.
String renderString(values);
/// [values] can be a combination of Map, List, String. Any non-String object
- /// will be converted using toString(). Null values will cause a
- /// FormatException, unless lenient module is enabled.
+ /// will be converted using toString(). Missing values will cause a
+ /// [TemplateException], unless lenient module is enabled.
void render(values, StringSink sink);
}
@@ -82,6 +83,10 @@
/// Lookup the value of a variable in the current context.
Object lookup(String variableName);
+
+ bool get isHelper;
+
+ List<String> get arguments;
}
diff --git a/lib/src/lambda_context.dart b/lib/src/lambda_context.dart
index 158101c..5c3fdf0 100644
--- a/lib/src/lambda_context.dart
+++ b/lib/src/lambda_context.dart
@@ -6,10 +6,19 @@
final _Node _node;
final _Renderer _renderer;
final bool _isSection;
+ final bool _isHelper;
+ final List<String> _arguments;
bool _closed = false;
- _LambdaContext(this._node, this._renderer, {bool isSection: true})
- : _isSection = isSection;
+ //FIXME remove isSection parameter, can just check node.type instead.
+ // Perhaps for isHelper too, once implemented.
+ _LambdaContext(this._node, this._renderer,
+ {bool isSection: true,
+ bool isHelper: false,
+ List<String> arguments: const <String>[]})
+ : _isSection = isSection,
+ _isHelper = isHelper,
+ _arguments = arguments;
void close() {
_closed = true;
@@ -21,6 +30,12 @@
_renderer._templateName, _renderer._source, _node.start);
}
+ bool get isSection => _isSection;
+
+ bool get isHelper => _isHelper;
+
+ List<String> get arguments => _arguments;
+
//TODO is allowing adding value to the stack a good idea?? Not sure.
String renderString({Object value}) {
_checkClosed();
@@ -65,7 +80,8 @@
var node = _parse(source,
_renderer._lenient,
_renderer._templateName,
- delimiters);
+ delimiters,
+ _renderer._helpers);
var renderer = new _Renderer.lambda(
_renderer,
node,
diff --git a/lib/src/parse.dart b/lib/src/parse.dart
index dbe4bfc..f7fb50f 100644
--- a/lib/src/parse.dart
+++ b/lib/src/parse.dart
@@ -3,7 +3,8 @@
_Node _parse(String source,
bool lenient,
String templateName,
- String delimiters) {
+ String delimiters,
+ Map<String,LambdaFunction> helpers) {
if (source == null) throw new ArgumentError('Template source is null');
@@ -34,14 +35,18 @@
break;
case _CLOSE_SECTION:
+
if (stack.last.value != t.value) {
- throw new _TemplateException(
- "Mismatched tag, expected: '${stack.last.value}', was: '${t.value}'",
- templateName, source, t.start);
+ //FIXME quick hack for experimental helpers support.
+ var helperName = t.value.split(new RegExp('[ \n\r\t]+')).first;
+ if (helpers == null || !helpers.containsKey(helperName)) {
+ throw new _TemplateException(
+ "Mismatched tag, expected: '${stack.last.value}', was: '${t.value}'",
+ templateName, source, t.start);
+ }
}
stack.last.contentEnd = t.start;
-
stack.removeLast();
break;
diff --git a/lib/src/renderer.dart b/lib/src/renderer.dart
index 3c4c054..93cd113 100644
--- a/lib/src/renderer.dart
+++ b/lib/src/renderer.dart
@@ -17,7 +17,8 @@
this._templateName,
this._indent,
this._source,
- this._delimiters)
+ this._delimiters,
+ this._helpers)
: _stack = new List.from(stack);
_Renderer.partial(_Renderer renderer, _Template partial, String indent)
@@ -31,7 +32,8 @@
renderer._templateName,
renderer._indent + indent,
partial.source,
- '{{ }}');
+ '{{ }}',
+ renderer._helpers);
_Renderer.subtree(_Renderer renderer, _Node node, StringSink sink)
: this(node,
@@ -44,7 +46,8 @@
renderer._templateName,
renderer._indent,
renderer._source,
- '{{ }}');
+ '{{ }}',
+ renderer._helpers);
_Renderer.lambda(
_Renderer renderer,
@@ -63,7 +66,8 @@
renderer._templateName,
renderer._indent + indent,
source,
- delimiters);
+ delimiters,
+ renderer._helpers);
final _Node _root;
final StringSink _sink;
@@ -75,6 +79,7 @@
final String _templateName;
final String _indent;
final String _source;
+ final Map<String,LambdaFunction> _helpers;
String _delimiters;
@@ -107,7 +112,20 @@
_write(Object output) => _sink.write(output.toString());
- _renderNode(_Node node) {
+ void _renderNode(_Node node) {
+
+ //FIXME just an ugly hack for now.
+ // Perhaps make a separate helper node type.
+ // Or just a flag on node.
+ //TODO handle more types of tags - i.e. unesc_var
+ if (node.type == _VARIABLE || node.type == _OPEN_SECTION) {
+ var arguments = node.value.split(new RegExp('[ \n\t\r]+'));
+ if (_helpers != null && _helpers.containsKey(arguments[0])) {
+ _renderHelper(node);
+ return;
+ }
+ }
+
switch (node.type) {
case _TEXT:
_renderText(node);
@@ -137,7 +155,7 @@
}
}
- _renderText(_Node node, {bool lastNode: false}) {
+ void _renderText(_Node node, {bool lastNode: false}) {
var s = node.value;
if (s == '') return;
if (_indent == null || _indent == '') {
@@ -153,7 +171,7 @@
// Walks up the stack looking for the variable.
// Handles dotted names of the form "a.b.c".
- _resolveValue(String name) {
+ Object _resolveValue(String name) {
if (name == '.') {
return _stack.last;
}
@@ -178,7 +196,7 @@
// 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) {
+ Object _getNamedProperty(object, name) {
if (object is Map && object.containsKey(name))
return object[name];
@@ -205,7 +223,7 @@
return invocation.reflectee;
}
- _renderVariable(_Node node, {bool escape : true}) {
+ void _renderVariable(_Node node, {bool escape : true}) {
var value = _resolveValue(node.value);
if (value is Function) {
@@ -226,12 +244,39 @@
}
}
- _renderSectionWithValue(node, value) {
+ void _renderSectionWithValue(node, value) {
_stack.add(value);
node.children.forEach(_renderNode);
_stack.removeLast();
}
+ void _renderHelper(_Node node, {escape: true}) {
+ //FIXME just an ugly hack for now.
+ //Will only work in lenient mode. Would also be nice not to have to any
+ //argument parsing during rendering.
+
+ var arguments = node.value.split(new RegExp('[ \n\t\r]+'));
+ var helper = _helpers[arguments[0]];
+
+ var argValues = arguments
+ .skip(1)
+ .map((arg) => _resolveValue(arg)).toList(growable: false);
+
+ var context = new _LambdaContext(node, this,
+ isSection: false,
+ isHelper: true,
+ arguments: argValues);
+ var value = helper(context);
+ context.close();
+
+ var valueString = (value == null) ? '' : value.toString();
+ var output = !escape && _htmlEscapeValues
+ ? _htmlEscape(valueString)
+ : valueString;
+
+ _write(output);
+ }
+
void _renderSubtree(node, StringSink sink, {Object value}) {
var renderer = new _Renderer.subtree(this, node, sink);
if (value != null) renderer._stack.add(value);
diff --git a/lib/src/template.dart b/lib/src/template.dart
index 6dbe3b1..f1972c2 100644
--- a/lib/src/template.dart
+++ b/lib/src/template.dart
@@ -3,16 +3,18 @@
class _Template implements Template {
_Template.fromSource(String source,
- {bool lenient: false,
- bool htmlEscapeValues : true,
- String name,
- PartialResolver partialResolver})
+ {String name,
+ bool lenient: false,
+ bool htmlEscapeValues : true,
+ PartialResolver partialResolver,
+ Map<String,LambdaFunction> helpers})
: source = source,
- _root = _parse(source, lenient, name, '{{ }}'),
+ _root = _parse(source, lenient, name, '{{ }}', helpers),
_lenient = lenient,
_htmlEscapeValues = htmlEscapeValues,
_name = name,
- _partialResolver = partialResolver;
+ _partialResolver = partialResolver,
+ _helpers = helpers;
final String source;
final _Node _root;
@@ -20,6 +22,7 @@
final bool _htmlEscapeValues;
final String _name;
final PartialResolver _partialResolver;
+ final Map<String,LambdaFunction> _helpers;
String get name => _name;
@@ -32,7 +35,7 @@
void render(values, StringSink sink) {
var renderer = new _Renderer(_root, sink, values, [values],
_lenient, _htmlEscapeValues, _partialResolver, _name, '', source,
- '{{ }}');
+ '{{ }}', _helpers);
renderer.render();
}
}
diff --git a/test/mustache_test.dart b/test/mustache_test.dart
index 6d6cfb5..a7142e3 100644
--- a/test/mustache_test.dart
+++ b/test/mustache_test.dart
@@ -599,6 +599,36 @@
});
});
+
+ group('Handlerbars helpers', () {
+
+ test("Variable", () {
+ var source = '<{{helper foo}}>';
+ var helpers = {'helper': (LambdaContext ctx) {
+ return ctx.arguments[0];
+ }};
+ var values = {'foo': 'bar'};
+ var output = '<bar>';
+
+ var t = new Template(source, lenient: true, helpers: helpers);
+ expect(t.renderString(values), equals(output));
+ });
+
+ test("Section", () {
+ var source = '<{{#helper foo}} oi {{/helper}}>';
+ var helpers = {'helper': (LambdaContext ctx) {
+ ctx.write(ctx.arguments[0]);
+ ctx.render();
+ ctx.write(ctx.arguments[0]);
+ }};
+ var values = {'foo': '-'};
+ var output = '<- oi ->';
+
+ var t = new Template(source, lenient: true, helpers: helpers);
+ expect(t.renderString(values), equals(output));
+ });
+
+ });
group('Other', () {
test('Standalone line', () {