blob: 2e92d0d7733e8b7705b40e3faf3761b9e0d3a78e [file] [log] [blame]
library mustache_test;
import 'package:mustache/mustache.dart';
import 'package:test/test.dart';
const MISMATCHED_TAG = 'Mismatched tag';
const UNEXPECTED_EOF = 'Unexpected end of input';
const BAD_VALUE_SECTION = 'Invalid value type for section';
const BAD_VALUE_INV_SECTION = 'Invalid value type for inverse section';
const BAD_TAG_NAME = 'Unless in lenient mode, tags may only contain';
const VALUE_NULL = 'Value was null or missing';
const VALUE_MISSING = 'Value was missing';
const UNCLOSED_TAG = 'Unclosed tag';
Template parse(String source, {bool lenient: false}) =>
new Template(source, lenient: lenient);
main() {
group('Basic', () {
test('Variable', () {
var output = parse('_{{var}}_').renderString({"var": "bob"});
expect(output, equals('_bob_'));
});
test('Comment', () {
var output = parse('_{{! i am a\n comment ! }}_').renderString({});
expect(output, equals('__'));
});
});
group('Section', () {
test('Map', () {
var output = parse('{{#section}}_{{var}}_{{/section}}').renderString({
"section": {"var": "bob"}
});
expect(output, equals('_bob_'));
});
test('List', () {
var output = parse('{{#section}}_{{var}}_{{/section}}').renderString({
"section": [
{"var": "bob"},
{"var": "jim"}
]
});
expect(output, equals('_bob__jim_'));
});
test('Empty List', () {
var output = parse('{{#section}}_{{var}}_{{/section}}')
.renderString({"section": []});
expect(output, equals(''));
});
test('False', () {
var output = parse('{{#section}}_{{var}}_{{/section}}')
.renderString({"section": false});
expect(output, equals(''));
});
test('Invalid value', () {
var ex = renderFail('{{#section}}_{{var}}_{{/section}}', {"section": 42});
expect(ex is TemplateException, isTrue);
expect(ex.message, startsWith(BAD_VALUE_SECTION));
});
test('Invalid value - lenient mode', () {
var output = parse('{{#var}}_{{var}}_{{/var}}', lenient: true)
.renderString({'var': 42});
expect(output, equals('_42_'));
});
test('True', () {
var output =
parse('{{#section}}_ok_{{/section}}').renderString({"section": true});
expect(output, equals('_ok_'));
});
test('Nested', () {
var output = parse(
'{{#section}}.{{var}}.{{#nested}}_{{nestedvar}}_{{/nested}}.{{/section}}')
.renderString({
"section": {
"var": "bob",
"nested": [
{"nestedvar": "jim"},
{"nestedvar": "sally"}
]
}
});
expect(output, equals('.bob._jim__sally_.'));
});
test('isNotEmpty', () {
var t = new Template('''{{^ section }}
Empty.
{{/ section }}
{{# section.isNotEmpty }}
<ul>
{{# section }}
<li>{{ . }}</li>
{{/ section }}
</ul>
{{/ section.isNotEmpty }}
''');
expect(
t.renderString({
"section": [1, 2, 3]
}),
equals(''' <ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
'''));
expect(t.renderString({"section": []}), equals('Empty.\n'));
});
test('Whitespace in section tags', () {
expect(
parse('{{#foo.bar}}oi{{/foo.bar}}').renderString({
'foo': {'bar': true}
}),
equals('oi'));
expect(
parse('{{# foo.bar}}oi{{/foo.bar}}').renderString({
'foo': {'bar': true}
}),
equals('oi'));
expect(
parse('{{#foo.bar }}oi{{/foo.bar}}').renderString({
'foo': {'bar': true}
}),
equals('oi'));
expect(
parse('{{# foo.bar }}oi{{/foo.bar}}').renderString({
'foo': {'bar': true}
}),
equals('oi'));
expect(
parse('{{#foo.bar}}oi{{/ foo.bar}}').renderString({
'foo': {'bar': true}
}),
equals('oi'));
expect(
parse('{{#foo.bar}}oi{{/foo.bar }}').renderString({
'foo': {'bar': true}
}),
equals('oi'));
expect(
parse('{{#foo.bar}}oi{{/ foo.bar }}').renderString({
'foo': {'bar': true}
}),
equals('oi'));
expect(
parse('{{# foo.bar }}oi{{/ foo.bar }}').renderString({
'foo': {'bar': true}
}),
equals('oi'));
});
test('Whitespace in variable tags', () {
expect(
parse('{{foo.bar}}').renderString({
'foo': {'bar': true}
}),
equals('true'));
expect(
parse('{{ foo.bar}}').renderString({
'foo': {'bar': true}
}),
equals('true'));
expect(
parse('{{foo.bar }}').renderString({
'foo': {'bar': true}
}),
equals('true'));
expect(
parse('{{ foo.bar }}').renderString({
'foo': {'bar': true}
}),
equals('true'));
});
test('Odd whitespace in tags', () {
render(source, values, output) => expect(
parse(source, lenient: true).renderString(values), equals(output));
render("{{\t# foo}}oi{{\n/foo}}", {'foo': true}, 'oi');
render(
"{{ # # foo }} {{ oi }} {{ / # foo }}",
{
'# foo': [
{'oi': 'OI!'}
]
},
' OI! ');
render(
"{{ #foo }} {{ oi }} {{ /foo }}",
{
'foo': [
{'oi': 'OI!'}
]
},
' OI! ');
render(
"{{\t#foo }} {{ oi }} {{ /foo }}",
{
'foo': [
{'oi': 'OI!'}
]
},
' OI! ');
render("{{{ #foo }}} {{{ /foo }}}", {'#foo': 1, '/foo': 2}, '1 2');
// Invalid - I'm ok with that for now.
// render(
// "{{{ { }}}",
// {'{': 1},
// '1');
render("{{\nfoo}}", {'foo': 'bar'}, 'bar');
render("{{\tfoo}}", {'foo': 'bar'}, 'bar');
render("{{\t# foo}}oi{{\n/foo}}", {'foo': true}, 'oi');
render("{{{\tfoo\t}}}", {'foo': true}, 'true');
//FIXME empty, or error in strict mode.
// render(
// "{{ > }}",
// {'>': 'oi'},
// '');
});
test('Empty source', () {
var t = new Template('');
expect(t.renderString({}), equals(''));
});
test('Template name', () {
var t = new Template('', name: 'foo');
expect(t.name, equals('foo'));
});
test('Bad tag', () {
expect(() => new Template('{{{ foo }|'), throwsException);
});
});
group('Inverse Section', () {
test('Map', () {
var output = parse('{{^section}}_{{var}}_{{/section}}').renderString({
"section": {"var": "bob"}
});
expect(output, equals(''));
});
test('List', () {
var output = parse('{{^section}}_{{var}}_{{/section}}').renderString({
"section": [
{"var": "bob"},
{"var": "jim"}
]
});
expect(output, equals(''));
});
test('Empty List', () {
var output =
parse('{{^section}}_ok_{{/section}}').renderString({"section": []});
expect(output, equals('_ok_'));
});
test('False', () {
var output = parse('{{^section}}_ok_{{/section}}')
.renderString({"section": false});
expect(output, equals('_ok_'));
});
test('Invalid value', () {
var ex = renderFail('{{^section}}_{{var}}_{{/section}}', {"section": 42});
expect(ex is TemplateException, isTrue);
expect(ex.message, startsWith(BAD_VALUE_INV_SECTION));
});
test('Invalid value - lenient mode', () {
var output = parse('{{^var}}_ok_{{/var}}', lenient: true)
.renderString({'var': 42});
expect(output, equals(''));
});
test('True', () {
var output =
parse('{{^section}}_ok_{{/section}}').renderString({"section": true});
expect(output, equals(''));
});
});
group('Html escape', () {
test('Escape at start', () {
var output = parse('_{{var}}_').renderString({"var": "&."});
expect(output, equals('_&amp;._'));
});
test('Escape at end', () {
var output = parse('_{{var}}_').renderString({"var": ".&"});
expect(output, equals('_.&amp;_'));
});
test('&', () {
var output = parse('_{{var}}_').renderString({"var": "&"});
expect(output, equals('_&amp;_'));
});
test('<', () {
var output = parse('_{{var}}_').renderString({"var": "<"});
expect(output, equals('_&lt;_'));
});
test('>', () {
var output = parse('_{{var}}_').renderString({"var": ">"});
expect(output, equals('_&gt;_'));
});
test('"', () {
var output = parse('_{{var}}_').renderString({"var": '"'});
expect(output, equals('_&quot;_'));
});
test("'", () {
var output = parse('_{{var}}_').renderString({"var": "'"});
expect(output, equals('_&#x27;_'));
});
test("/", () {
var output = parse('_{{var}}_').renderString({"var": "/"});
expect(output, equals('_&#x2F;_'));
});
});
group('Invalid format', () {
test('Mismatched tag', () {
var source = '{{#section}}_{{var}}_{{/notsection}}';
var ex = renderFail(source, {
"section": {"var": "bob"}
});
expectFail(ex, 1, 22, 'Mismatched tag');
});
test('Unexpected EOF', () {
var source = '{{#section}}_{{var}}_{{/section';
var ex = renderFail(source, {
"section": {"var": "bob"}
});
expectFail(ex, 1, 31, UNEXPECTED_EOF);
});
test('Bad tag name, open section', () {
var source = r'{{#section$%$^%}}_{{var}}_{{/section}}';
var ex = renderFail(source, {
"section": {"var": "bob"}
});
expectFail(ex, null, null, BAD_TAG_NAME);
});
test('Bad tag name, close section', () {
var source = r'{{#section}}_{{var}}_{{/section$%$^%}}';
var ex = renderFail(source, {
"section": {"var": "bob"}
});
expectFail(ex, null, null, BAD_TAG_NAME);
});
test('Bad tag name, variable', () {
var source = r'{{#section}}_{{var$%$^%}}_{{/section}}';
var ex = renderFail(source, {
"section": {"var": "bob"}
});
expectFail(ex, null, null, BAD_TAG_NAME);
});
test('Missing variable', () {
var source = r'{{#section}}_{{var}}_{{/section}}';
var ex = renderFail(source, {"section": {}});
expectFail(ex, null, null, VALUE_MISSING);
});
// Null variables shouldn't be a problem.
test('Null variable', () {
var t = new Template('{{#section}}_{{var}}_{{/section}}');
var output = t.renderString({
"section": {'var': null}
});
expect(output, equals('__'));
});
test('Unclosed section', () {
var source = r'{{#section}}foo';
var ex = renderFail(source, {"section": {}});
expectFail(ex, null, null, UNCLOSED_TAG);
});
});
group('Lenient', () {
test('Odd section name', () {
var output =
parse(r'{{#section$%$^%}}_{{var}}_{{/section$%$^%}}', lenient: true)
.renderString({
r'section$%$^%': {'var': 'bob'}
});
expect(output, equals('_bob_'));
});
test('Odd variable name', () {
var output =
parse(r'{{#section}}_{{var$%$^%}}_{{/section}}', lenient: true)
.renderString({
'section': {r'var$%$^%': 'bob'}
});
expect(output, equals('_bob_'));
});
test('Null variable', () {
var output = parse(r'{{#section}}_{{var}}_{{/section}}', lenient: true)
.renderString({
'section': {'var': null}
});
expect(output, equals('__'));
});
test('Null section', () {
var output = parse('{{#section}}_{{var}}_{{/section}}', lenient: true)
.renderString({"section": null});
expect(output, equals(''));
});
// Known failure
// test('Null inverse section', () {
// var output = parse('{{^section}}_{{var}}_{{/section}}', lenient: true)
// .renderString({"section": null}, lenient: true);
// expect(output, equals(''));
// });
});
group('Escape tags', () {
test('{{{ ... }}}', () {
var output = parse('{{{blah}}}').renderString({'blah': '&'});
expect(output, equals('&'));
});
test('{{& ... }}', () {
var output = parse('{{{blah}}}').renderString({'blah': '&'});
expect(output, equals('&'));
});
});
group('Partial tag', () {
String _partialTest(Map values, Map sources, String renderTemplate,
{bool lenient: false}) {
var templates = new Map<String, Template>();
var resolver = (String name) => templates[name];
for (var k in sources.keys) {
templates[k] = new Template(sources[k],
name: k, lenient: lenient, partialResolver: resolver);
}
var t = resolver(renderTemplate);
return t.renderString(values);
}
test('basic', () {
var output = _partialTest({'foo': 'bar'},
{'root': '{{>partial}}', 'partial': '{{foo}}'}, 'root');
expect(output, 'bar');
});
test('missing partial strict', () {
var threw = false;
try {
_partialTest({'foo': 'bar'}, {'root': '{{>partial}}'}, 'root',
lenient: false);
} catch (e) {
expect(e is TemplateException, isTrue);
threw = true;
}
expect(threw, isTrue);
});
test('missing partial lenient', () {
var output = _partialTest(
{'foo': 'bar'}, {'root': '{{>partial}}'}, 'root',
lenient: true);
expect(output, equals(''));
});
test('context', () {
var output = _partialTest({
'text': 'content'
}, {
'root': '"{{>partial}}"',
'partial': '*{{text}}*'
}, 'root', lenient: true);
expect(output, equals('"*content*"'));
});
test('recursion', () {
var output = _partialTest({
'content': "X",
'nodes': [
{'content': "Y", 'nodes': []}
]
}, {
'root': '{{>node}}',
'node': '{{content}}<{{#nodes}}{{>node}}{{/nodes}}>'
}, 'root', lenient: true);
expect(output, equals('X<Y<>>'));
});
test('standalone without previous', () {
var output = _partialTest(
{}, {'root': ' {{>partial}}\n>', 'partial': '>\n>'}, 'root',
lenient: true);
expect(output, equals(' >\n >>'));
});
test('standalone indentation', () {
var output = _partialTest({
'content': "<\n->"
}, {
'root': "\\\n {{>partial}}\n\/\n",
'partial': "|\n{{{content}}}\n|\n"
}, 'root', lenient: true);
expect(output, equals("\\\n |\n <\n->\n |\n\/\n"));
});
});
group('Lambdas', () {
_lambdaTest({template, lambda, output}) => expect(
parse(template).renderString({'lambda': lambda}), equals(output));
test('basic', () {
_lambdaTest(
template: 'Hello, {{lambda}}!',
lambda: (_) => 'world',
output: 'Hello, world!');
});
test('escaping', () {
_lambdaTest(
template: '<{{lambda}}{{{lambda}}}',
lambda: (_) => '>',
output: '<&gt;>');
});
test('sections', () {
_lambdaTest(
template: '{{#lambda}}FILE{{/lambda}} != {{#lambda}}LINE{{/lambda}}',
lambda: (LambdaContext ctx) => '__${ctx.renderString()}__',
output: '__FILE__ != __LINE__');
});
//FIXME
test('inverted sections truthy', () {
var template = '<{{^lambda}}{{static}}{{/lambda}}>';
var values = {'lambda': (_) => false, 'static': 'static'};
var output = '<>';
expect(parse(template).renderString(values), equals(output));
}, skip: "skip test");
test("seth's use case", () {
var template = '<{{#markdown}}{{content}}{{/markdown}}>';
var values = {
'markdown': (ctx) => ctx.renderString().toLowerCase(),
'content': 'OI YOU!'
};
var output = '<oi you!>';
expect(parse(template).renderString(values), equals(output));
});
test("Lambda v2", () {
var template = '<{{#markdown}}{{content}}{{/markdown}}>';
var values = {'markdown': (ctx) => ctx.source, 'content': 'OI YOU!'};
var output = '<{{content}}>';
expect(parse(template).renderString(values), equals(output));
});
test("Lambda v2...", () {
var template = '<{{#markdown}}dsfsf dsfsdf dfsdfsd{{/markdown}}>';
var values = {'markdown': (ctx) => ctx.source};
var output = '<dsfsf dsfsdf dfsdfsd>';
expect(parse(template).renderString(values), equals(output));
});
test('Alternate Delimiters', () {
// A lambda's return value should parse with the default delimiters.
var template = '{{= | | =}}\nHello, (|&lambda|)!';
//function() { return "|planet| => {{planet}}" }
var values = {
'planet': 'world',
'lambda': (LambdaContext ctx) =>
ctx.renderSource('|planet| => {{planet}}')
};
var output = 'Hello, (|planet| => world)!';
expect(parse(template).renderString(values), equals(output));
});
test('Alternate Delimiters 2', () {
// Lambdas used for sections should parse with the current delimiters.
var template = '{{= | | =}}<|#lambda|-|/lambda|>';
//function() { return "|planet| => {{planet}}" }
var values = {
'planet': 'Earth',
'lambda': (LambdaContext ctx) {
var txt = ctx.source;
return ctx.renderSource('$txt{{planet}} => |planet|$txt');
}
};
var output = '<-{{planet}} => Earth->';
expect(parse(template).renderString(values), equals(output));
});
test('LambdaContext.lookup', () {
var t = new Template('{{ foo }}');
var s = t.renderString({'foo': (lc) => lc.lookup('bar'), 'bar': 'jim'});
expect(s, equals('jim'));
});
test('LambdaContext.lookup closed', () {
var t = new Template('{{ foo }}');
var lc2;
t.renderString({'foo': (lc) => lc2 = lc, 'bar': 'jim'});
expect(() => lc2.lookup('foo'), throwsException);
});
});
group('Other', () {
test('Standalone line', () {
var val = parse('|\n{{#bob}}\n{{/bob}}\n|').renderString({'bob': []});
expect(val, equals('|\n|'));
});
});
group('Array indexing', () {
test('Basic', () {
var val = parse('{{array.1}}').renderString({
'array': [1, 2, 3]
});
expect(val, equals('2'));
});
test('RangeError', () {
var error = renderFail('{{array.5}}', {
'array': [1, 2, 3]
});
expect(error, isRangeError);
});
});
group('Mirrors', () {
test('Simple field', () {
var output = parse('_{{bar}}_').renderString(new Foo()..bar = 'bob');
expect(output, equals('_bob_'));
});
test('Simple field', () {
var output = parse('_{{jim}}_').renderString(new Foo());
expect(output, equals('_bob_'));
});
test('Lambda', () {
var output =
parse('_{{lambda}}_').renderString(new Foo()..lambda = (_) => 'yo');
expect(output, equals('_yo_'));
});
});
group('Delimiters', () {
test('Basic', () {
var val = parse('{{=<% %>=}}(<%text%>)').renderString({'text': 'Hey!'});
expect(val, equals('(Hey!)'));
});
test('Single delimiters', () {
var val = parse('({{=[ ]=}}[text])').renderString({'text': 'It worked!'});
expect(val, equals('(It worked!)'));
});
});
group('Template with custom delimiters', () {
test('Basic', () {
var t = new Template('(<%text%>)', delimiters: '<% %>');
var val = t.renderString({'text': 'Hey!'});
expect(val, equals('(Hey!)'));
});
});
group('Lambda context', () {
test("LambdaContext write", () {
var template = '<{{#markdown}}{{content}}{{/markdown}}>';
var values = {
'markdown': (ctx) {
ctx.write('foo');
}
};
var output = '<foo>';
expect(parse(template).renderString(values), equals(output));
});
test("LambdaContext render", () {
var template = '<{{#markdown}}{{content}}{{/markdown}}>';
var values = {
'content': 'bar',
'markdown': (ctx) {
ctx.render();
}
};
var output = '<bar>';
expect(parse(template).renderString(values), equals(output));
});
test("LambdaContext render with value", () {
var template = '<{{#markdown}}{{content}}{{/markdown}}>';
var values = {
'markdown': (LambdaContext ctx) {
ctx.render(value: {'content': 'oi!'});
}
};
var output = '<oi!>';
expect(parse(template).renderString(values), equals(output));
});
test("LambdaContext renderString with value", () {
var template = '<{{#markdown}}{{content}}{{/markdown}}>';
var values = {
'markdown': (LambdaContext ctx) {
return ctx.renderString(value: {'content': 'oi!'});
}
};
var output = '<oi!>';
expect(parse(template).renderString(values), equals(output));
});
test("LambdaContext write and return", () {
var template = '<{{#markdown}}{{content}}{{/markdown}}>';
var values = {
'markdown': (LambdaContext ctx) {
ctx.write('foo');
return 'bar';
}
};
var output = '<foobar>';
expect(parse(template).renderString(values), equals(output));
});
test("LambdaContext renderSource with value", () {
var template = '<{{#markdown}}{{content}}{{/markdown}}>';
var values = {
'markdown': (LambdaContext ctx) {
return ctx.renderSource(ctx.source, value: {'content': 'oi!'});
}
};
var output = '<oi!>';
expect(parse(template).renderString(values), equals(output));
});
});
}
renderFail(source, values) {
try {
parse(source).renderString(values);
return null;
} catch (e) {
return e;
}
}
expectFail(ex, int line, int column, [String msgStartsWith]) {
expect(ex is TemplateException, isTrue);
if (line != null) expect(ex.line, equals(line));
if (column != null) expect(ex.column, equals(column));
if (msgStartsWith != null) expect(ex.message, startsWith(msgStartsWith));
}
class Foo {
String bar;
Function lambda;
jim() => 'bob';
}