blob: 086247bab56b3cfa55af26c8d414ad16aadef911 [file] [log] [blame]
import 'package:mustache_template/mustache.dart';
import 'package:test/test.dart';
const String UNEXPECTED_EOF = 'Unexpected end of input';
const String BAD_VALUE_INV_SECTION = 'Invalid value type for inverse section';
const String BAD_TAG_NAME = 'Unless in lenient mode, tags may only contain';
const String VALUE_MISSING = 'Value was missing';
const String UNCLOSED_TAG = 'Unclosed tag';
Template parse(String source, {bool lenient = false}) =>
Template(source, lenient: lenient);
void main() {
group('Basic', () {
test('Variable', () {
final String output = parse(
'_{{var}}_',
).renderString(<String, String>{'var': 'bob'});
expect(output, equals('_bob_'));
});
test('Comment', () {
final String output = parse(
'_{{! i am a\n comment ! }}_',
).renderString(<dynamic, dynamic>{});
expect(output, equals('__'));
});
});
group('Section', () {
test('Map', () {
final String output = parse(
'{{#section}}_{{var}}_{{/section}}',
).renderString(<String, Map<String, String>>{
'section': <String, String>{'var': 'bob'},
});
expect(output, equals('_bob_'));
});
test('List', () {
final String output = parse(
'{{#section}}_{{var}}_{{/section}}',
).renderString(<String, List<Map<String, String>>>{
'section': <Map<String, String>>[
<String, String>{'var': 'bob'},
<String, String>{'var': 'jim'},
],
});
expect(output, equals('_bob__jim_'));
});
test('Empty List', () {
final String output = parse(
'{{#section}}_{{var}}_{{/section}}',
).renderString(<String, List<dynamic>>{'section': <dynamic>[]});
expect(output, equals(''));
});
test('False', () {
final String output = parse(
'{{#section}}_{{var}}_{{/section}}',
).renderString(<String, bool>{'section': false});
expect(output, equals(''));
});
test('Invalid value', () {
final Exception? ex = renderFail(
'{{#section}}_{{var}}_{{/section}}',
<String, int>{'section': 42},
);
if (ex is TemplateException) {
expect(ex.message, startsWith(VALUE_MISSING));
} else {
fail('Unexpected type: $ex');
}
});
test('Invalid value - lenient mode', () {
final String output = parse(
'{{#var}}_{{var}}_{{/var}}',
lenient: true,
).renderString(<String, int>{'var': 42});
expect(output, equals('_42_'));
});
test('True', () {
final String output = parse(
'{{#section}}_ok_{{/section}}',
).renderString(<String, bool>{'section': true});
expect(output, equals('_ok_'));
});
test('Nested', () {
final String output = parse(
'{{#section}}.{{var}}.{{#nested}}_{{nestedvar}}_{{/nested}}.{{/section}}',
).renderString(<String, Map<String, Object>>{
'section': <String, Object>{
'var': 'bob',
'nested': <Map<String, String>>[
<String, String>{'nestedvar': 'jim'},
<String, String>{'nestedvar': 'sally'},
],
},
});
expect(output, equals('.bob._jim__sally_.'));
});
test('Whitespace in section tags', () {
expect(
parse('{{#foo.bar}}oi{{/foo.bar}}').renderString(
<String, Map<String, bool>>{
'foo': <String, bool>{'bar': true},
},
),
equals('oi'),
);
expect(
parse('{{# foo.bar}}oi{{/foo.bar}}').renderString(
<String, Map<String, bool>>{
'foo': <String, bool>{'bar': true},
},
),
equals('oi'),
);
expect(
parse('{{#foo.bar }}oi{{/foo.bar}}').renderString(
<String, Map<String, bool>>{
'foo': <String, bool>{'bar': true},
},
),
equals('oi'),
);
expect(
parse('{{# foo.bar }}oi{{/foo.bar}}').renderString(
<String, Map<String, bool>>{
'foo': <String, bool>{'bar': true},
},
),
equals('oi'),
);
expect(
parse('{{#foo.bar}}oi{{/ foo.bar}}').renderString(
<String, Map<String, bool>>{
'foo': <String, bool>{'bar': true},
},
),
equals('oi'),
);
expect(
parse('{{#foo.bar}}oi{{/foo.bar }}').renderString(
<String, Map<String, bool>>{
'foo': <String, bool>{'bar': true},
},
),
equals('oi'),
);
expect(
parse('{{#foo.bar}}oi{{/ foo.bar }}').renderString(
<String, Map<String, bool>>{
'foo': <String, bool>{'bar': true},
},
),
equals('oi'),
);
expect(
parse('{{# foo.bar }}oi{{/ foo.bar }}').renderString(
<String, Map<String, bool>>{
'foo': <String, bool>{'bar': true},
},
),
equals('oi'),
);
});
test('Whitespace in variable tags', () {
expect(
parse('{{foo.bar}}').renderString(<String, Map<String, bool>>{
'foo': <String, bool>{'bar': true},
}),
equals('true'),
);
expect(
parse('{{ foo.bar}}').renderString(<String, Map<String, bool>>{
'foo': <String, bool>{'bar': true},
}),
equals('true'),
);
expect(
parse('{{foo.bar }}').renderString(<String, Map<String, bool>>{
'foo': <String, bool>{'bar': true},
}),
equals('true'),
);
expect(
parse('{{ foo.bar }}').renderString(<String, Map<String, bool>>{
'foo': <String, bool>{'bar': true},
}),
equals('true'),
);
});
test('Odd whitespace in tags', () {
void render(String source, dynamic values, dynamic output) => expect(
parse(source, lenient: true).renderString(values),
equals(output),
);
render('{{\t# foo}}oi{{\n/foo}}', <String, bool>{'foo': true}, 'oi');
render(
'{{ # # foo }} {{ oi }} {{ / # foo }}',
<String, List<Map<String, String>>>{
'# foo': <Map<String, String>>[
<String, String>{'oi': 'OI!'},
],
},
' OI! ',
);
render(
'{{ #foo }} {{ oi }} {{ /foo }}',
<String, List<Map<String, String>>>{
'foo': <Map<String, String>>[
<String, String>{'oi': 'OI!'},
],
},
' OI! ',
);
render(
'{{\t#foo }} {{ oi }} {{ /foo }}',
<String, List<Map<String, String>>>{
'foo': <Map<String, String>>[
<String, String>{'oi': 'OI!'},
],
},
' OI! ',
);
render('{{{ #foo }}} {{{ /foo }}}', <String, int>{
'#foo': 1,
'/foo': 2,
}, '1 2');
// Invalid - I'm ok with that for now.
// render(
// "{{{ { }}}",
// {'{': 1},
// '1');
render('{{\nfoo}}', <String, String>{'foo': 'bar'}, 'bar');
render('{{\tfoo}}', <String, String>{'foo': 'bar'}, 'bar');
render('{{\t# foo}}oi{{\n/foo}}', <String, bool>{'foo': true}, 'oi');
render('{{{\tfoo\t}}}', <String, bool>{'foo': true}, 'true');
// TODO(stuartmorgan): Fix and enable this test, which was commented out
// when the source was first imported.
// empty, or error in strict mode.
// render(
// "{{ > }}",
// {'>': 'oi'},
// '');
});
test('Empty source', () {
final Template t = Template('');
expect(t.renderString(<dynamic, dynamic>{}), equals(''));
});
test('Template name', () {
final Template t = Template('', name: 'foo');
expect(t.name, equals('foo'));
});
test('Bad tag', () {
expect(() => Template('{{{ foo }|'), throwsException);
});
});
group('Inverse Section', () {
test('Map', () {
final String output = parse(
'{{^section}}_{{var}}_{{/section}}',
).renderString(<String, Map<String, String>>{
'section': <String, String>{'var': 'bob'},
});
expect(output, equals(''));
});
test('List', () {
final String output = parse(
'{{^section}}_{{var}}_{{/section}}',
).renderString(<String, List<Map<String, String>>>{
'section': <Map<String, String>>[
<String, String>{'var': 'bob'},
<String, String>{'var': 'jim'},
],
});
expect(output, equals(''));
});
test('Empty List', () {
final String output = parse(
'{{^section}}_ok_{{/section}}',
).renderString(<String, List<dynamic>>{'section': <dynamic>[]});
expect(output, equals('_ok_'));
});
test('False', () {
final String output = parse(
'{{^section}}_ok_{{/section}}',
).renderString(<String, bool>{'section': false});
expect(output, equals('_ok_'));
});
test('Invalid value', () {
final Exception? ex = renderFail(
'{{^section}}_{{var}}_{{/section}}',
<String, int>{'section': 42},
);
expect(ex is TemplateException, isTrue);
expect(
(ex! as TemplateException).message,
startsWith(BAD_VALUE_INV_SECTION),
);
});
test('Invalid value - lenient mode', () {
final String output = parse(
'{{^var}}_ok_{{/var}}',
lenient: true,
).renderString(<String, int>{'var': 42});
expect(output, equals(''));
});
test('True', () {
final String output = parse(
'{{^section}}_ok_{{/section}}',
).renderString(<String, bool>{'section': true});
expect(output, equals(''));
});
});
group('Html escape', () {
test('Escape at start', () {
final String output = parse(
'_{{var}}_',
).renderString(<String, String>{'var': '&.'});
expect(output, equals('_&amp;._'));
});
test('Escape at end', () {
final String output = parse(
'_{{var}}_',
).renderString(<String, String>{'var': '.&'});
expect(output, equals('_.&amp;_'));
});
test('&', () {
final String output = parse(
'_{{var}}_',
).renderString(<String, String>{'var': '&'});
expect(output, equals('_&amp;_'));
});
test('<', () {
final String output = parse(
'_{{var}}_',
).renderString(<String, String>{'var': '<'});
expect(output, equals('_&lt;_'));
});
test('>', () {
final String output = parse(
'_{{var}}_',
).renderString(<String, String>{'var': '>'});
expect(output, equals('_&gt;_'));
});
test('"', () {
final String output = parse(
'_{{var}}_',
).renderString(<String, String>{'var': '"'});
expect(output, equals('_&quot;_'));
});
test("'", () {
final String output = parse(
'_{{var}}_',
).renderString(<String, String>{'var': "'"});
expect(output, equals('_&#x27;_'));
});
test('/', () {
final String output = parse(
'_{{var}}_',
).renderString(<String, String>{'var': '/'});
expect(output, equals('_&#x2F;_'));
});
});
group('Invalid format', () {
test('Mismatched tag', () {
const String source = '{{#section}}_{{var}}_{{/notsection}}';
final Exception? ex = renderFail(source, <String, Map<String, String>>{
'section': <String, String>{'var': 'bob'},
});
expectFail(ex, 1, 22, 'Mismatched tag');
});
test('Unexpected EOF', () {
const String source = '{{#section}}_{{var}}_{{/section';
final Exception? ex = renderFail(source, <String, Map<String, String>>{
'section': <String, String>{'var': 'bob'},
});
expectFail(ex, 1, 31, UNEXPECTED_EOF);
});
test('Bad tag name, open section', () {
const String source = r'{{#section$%$^%}}_{{var}}_{{/section}}';
final Exception? ex = renderFail(source, <String, Map<String, String>>{
'section': <String, String>{'var': 'bob'},
});
expectFail(ex, null, null, BAD_TAG_NAME);
});
test('Bad tag name, close section', () {
const String source = r'{{#section}}_{{var}}_{{/section$%$^%}}';
final Exception? ex = renderFail(source, <String, Map<String, String>>{
'section': <String, String>{'var': 'bob'},
});
expectFail(ex, null, null, BAD_TAG_NAME);
});
test('Bad tag name, variable', () {
const String source = r'{{#section}}_{{var$%$^%}}_{{/section}}';
final Exception? ex = renderFail(source, <String, Map<String, String>>{
'section': <String, String>{'var': 'bob'},
});
expectFail(ex, null, null, BAD_TAG_NAME);
});
test('Missing variable', () {
const String source = r'{{#section}}_{{var}}_{{/section}}';
final Exception? ex = renderFail(source, <String, Map<dynamic, dynamic>>{
'section': <dynamic, dynamic>{},
});
expectFail(ex, null, null, VALUE_MISSING);
});
// Null variables shouldn't be a problem.
test('Null variable', () {
final Template t = Template('{{#section}}_{{var}}_{{/section}}');
final String output = t.renderString(<String, Map<String, void>>{
'section': <String, void>{'var': null},
});
expect(output, equals('__'));
});
test('Unclosed section', () {
const String source = r'{{#section}}foo';
final Exception? ex = renderFail(source, <String, Map<dynamic, dynamic>>{
'section': <dynamic, dynamic>{},
});
expectFail(ex, null, null, UNCLOSED_TAG);
});
});
group('Lenient', () {
test('Odd section name', () {
final String output = parse(
r'{{#section$%$^%}}_{{var}}_{{/section$%$^%}}',
lenient: true,
).renderString(<String, Map<String, String>>{
r'section$%$^%': <String, String>{'var': 'bob'},
});
expect(output, equals('_bob_'));
});
test('Odd variable name', () {
final String output = parse(
r'{{#section}}_{{var$%$^%}}_{{/section}}',
lenient: true,
).renderString(<String, Map<String, String>>{
'section': <String, String>{r'var$%$^%': 'bob'},
});
expect(output, equals('_bob_'));
});
test('Null variable', () {
final String output = parse(
r'{{#section}}_{{var}}_{{/section}}',
lenient: true,
).renderString(<String, Map<String, void>>{
'section': <String, void>{'var': null},
});
expect(output, equals('__'));
});
test('Null section', () {
final String output = parse(
'{{#section}}_{{var}}_{{/section}}',
lenient: true,
).renderString(<String, void>{'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('{{{ ... }}}', () {
final String output = parse(
'{{{blah}}}',
).renderString(<String, String>{'blah': '&'});
expect(output, equals('&'));
});
test('{{& ... }}', () {
final String output = parse(
'{{{blah}}}',
).renderString(<String, String>{'blah': '&'});
expect(output, equals('&'));
});
});
group('Partial tag', () {
String partialTest(
Map<String, Object> values,
Map<String, Object> sources,
String renderTemplate, {
bool lenient = false,
}) {
final Map<String, Template> templates = <String, Template>{};
Template? resolver(String name) => templates[name];
for (final String k in sources.keys) {
templates[k] = Template(
sources[k]! as String,
name: k,
lenient: lenient,
partialResolver: resolver,
);
}
final Template? t = resolver(renderTemplate);
return t!.renderString(values);
}
test('basic', () {
final String output = partialTest(
<String, Object>{'foo': 'bar'},
<String, Object>{'root': '{{>partial}}', 'partial': '{{foo}}'},
'root',
);
expect(output, 'bar');
});
test('missing partial strict', () {
bool threw = false;
try {
partialTest(
<String, Object>{'foo': 'bar'},
<String, Object>{'root': '{{>partial}}'},
'root',
);
} on Exception catch (e) {
expect(e is TemplateException, isTrue);
threw = true;
}
expect(threw, isTrue);
});
test('missing partial lenient', () {
final String output = partialTest(
<String, Object>{'foo': 'bar'},
<String, Object>{'root': '{{>partial}}'},
'root',
lenient: true,
);
expect(output, equals(''));
});
test('context', () {
final String output = partialTest(
<String, Object>{'text': 'content'},
<String, Object>{'root': '"{{>partial}}"', 'partial': '*{{text}}*'},
'root',
lenient: true,
);
expect(output, equals('"*content*"'));
});
test('recursion', () {
final String output = partialTest(
<String, Object>{
'content': 'X',
'nodes': <Map<String, Object>>[
<String, Object>{'content': 'Y', 'nodes': <dynamic>[]},
],
},
<String, Object>{
'root': '{{>node}}',
'node': '{{content}}<{{#nodes}}{{>node}}{{/nodes}}>',
},
'root',
lenient: true,
);
expect(output, equals('X<Y<>>'));
});
test('standalone without previous', () {
final String output = partialTest(
<String, Object>{},
<String, Object>{'root': ' {{>partial}}\n>', 'partial': '>\n>'},
'root',
lenient: true,
);
expect(output, equals(' >\n >>'));
});
test('standalone indentation', () {
final String output = partialTest(
<String, Object>{'content': '<\n->'},
<String, Object>{
'root': '\\\n {{>partial}}\n/\n',
'partial': '|\n{{{content}}}\n|\n',
},
'root',
lenient: true,
);
expect(output, equals('\\\n |\n <\n->\n |\n/\n'));
});
});
group('Lambdas', () {
void lambdaTest({
required String template,
dynamic lambda,
dynamic output,
}) => expect(
parse(template).renderString(<String, dynamic>{'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__',
);
});
// TODO(stuartmorgan): Fix and re-enable this test, which was skipped when
// the package was first imported.
test('inverted sections truthy', () {
const String template = '<{{^lambda}}{{static}}{{/lambda}}>';
final Map<String, Object> values = <String, Object>{
'lambda': (_) => false,
'static': 'static',
};
const String output = '<>';
expect(parse(template).renderString(values), equals(output));
}, skip: 'skip test');
test("seth's use case", () {
const String template = '<{{#markdown}}{{content}}{{/markdown}}>';
final Map<String, Object> values = <String, Object>{
'markdown': (LambdaContext ctx) => ctx.renderString().toLowerCase(),
'content': 'OI YOU!',
};
const String output = '<oi you!>';
expect(parse(template).renderString(values), equals(output));
});
test('Lambda v2', () {
const String template = '<{{#markdown}}{{content}}{{/markdown}}>';
final Map<String, Object> values = <String, Object>{
'markdown': (LambdaContext ctx) => ctx.source,
'content': 'OI YOU!',
};
const String output = '<{{content}}>';
expect(parse(template).renderString(values), equals(output));
});
test('Lambda v2...', () {
const String template =
'<{{#markdown}}dsfsf dsfsdf dfsdfsd{{/markdown}}>';
final Map<String, dynamic Function(dynamic ctx)> values =
<String, dynamic Function(dynamic ctx)>{
// ignore: avoid_dynamic_calls
'markdown': (dynamic ctx) => ctx.source,
};
const String 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.
const String template = '{{= | | =}}\nHello, (|&lambda|)!';
//function() { return "|planet| => {{planet}}" }
final Map<String, Object> values = <String, Object>{
'planet': 'world',
'lambda':
(LambdaContext ctx) => ctx.renderSource('|planet| => {{planet}}'),
};
const String 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.
const String template = '{{= | | =}}<|#lambda|-|/lambda|>';
//function() { return "|planet| => {{planet}}" }
final Map<String, Object> values = <String, Object>{
'planet': 'Earth',
'lambda': (LambdaContext ctx) {
final String txt = ctx.source;
return ctx.renderSource('$txt{{planet}} => |planet|$txt');
},
};
const String output = '<-{{planet}} => Earth->';
expect(parse(template).renderString(values), equals(output));
});
test('LambdaContext.lookup', () {
final Template t = Template('{{ foo }}');
final String s = t.renderString(<String, Object>{
'foo': (LambdaContext lc) => lc.lookup('bar'),
'bar': 'jim',
});
expect(s, equals('jim'));
});
test('LambdaContext.lookup closed', () {
final Template t = Template('{{ foo }}');
LambdaContext? lc2;
t.renderString(<String, Object>{
'foo': (LambdaContext lc) => lc2 = lc,
'bar': 'jim',
});
expect(lc2, isNotNull);
expect(() => lc2?.lookup('foo'), throwsException);
});
});
group('Other', () {
test('Standalone line', () {
final String val = parse(
'|\n{{#bob}}\n{{/bob}}\n|',
).renderString(<String, List<dynamic>>{'bob': <dynamic>[]});
expect(val, equals('|\n|'));
});
});
group('Array indexing', () {
test('Basic', () {
final String val = parse('{{array.1}}').renderString(<String, List<int>>{
'array': <int>[1, 2, 3],
});
expect(val, equals('2'));
});
test('RangeError', () {
final Exception? error = renderFail('{{array.5}}', <String, List<int>>{
'array': <int>[1, 2, 3],
});
expect(error, isA<TemplateException>());
});
});
group('Delimiters', () {
test('Basic', () {
final String val = parse(
'{{=<% %>=}}(<%text%>)',
).renderString(<String, String>{'text': 'Hey!'});
expect(val, equals('(Hey!)'));
});
test('Single delimiters', () {
final String val = parse(
'({{=[ ]=}}[text])',
).renderString(<String, String>{'text': 'It worked!'});
expect(val, equals('(It worked!)'));
});
});
group('Template with custom delimiters', () {
test('Basic', () {
final Template t = Template('(<%text%>)', delimiters: '<% %>');
final String val = t.renderString(<String, String>{'text': 'Hey!'});
expect(val, equals('(Hey!)'));
});
});
group('Lambda context', () {
test('LambdaContext write', () {
const String template = '<{{#markdown}}{{content}}{{/markdown}}>';
final Map<String, Null Function(LambdaContext ctx)> values =
<String, Null Function(LambdaContext ctx)>{
'markdown': (LambdaContext ctx) {
ctx.write('foo');
},
};
const String output = '<foo>';
expect(parse(template).renderString(values), equals(output));
});
test('LambdaContext render', () {
const String template = '<{{#markdown}}{{content}}{{/markdown}}>';
final Map<String, Object> values = <String, Object>{
'content': 'bar',
'markdown': (LambdaContext ctx) {
ctx.render();
},
};
const String output = '<bar>';
expect(parse(template).renderString(values), equals(output));
});
test('LambdaContext render with value', () {
const String template = '<{{#markdown}}{{content}}{{/markdown}}>';
final Map<String, Null Function(LambdaContext ctx)> values =
<String, Null Function(LambdaContext ctx)>{
'markdown': (LambdaContext ctx) {
ctx.render(value: <String, String>{'content': 'oi!'});
},
};
const String output = '<oi!>';
expect(parse(template).renderString(values), equals(output));
});
test('LambdaContext renderString with value', () {
const String template = '<{{#markdown}}{{content}}{{/markdown}}>';
final Map<String, String Function(LambdaContext ctx)> values =
<String, String Function(LambdaContext ctx)>{
'markdown': (LambdaContext ctx) {
return ctx.renderString(
value: <String, String>{'content': 'oi!'},
);
},
};
const String output = '<oi!>';
expect(parse(template).renderString(values), equals(output));
});
test('LambdaContext write and return', () {
const String template = '<{{#markdown}}{{content}}{{/markdown}}>';
final Map<String, String Function(LambdaContext ctx)> values =
<String, String Function(LambdaContext ctx)>{
'markdown': (LambdaContext ctx) {
ctx.write('foo');
return 'bar';
},
};
const String output = '<foobar>';
expect(parse(template).renderString(values), equals(output));
});
test('LambdaContext renderSource with value', () {
const String template = '<{{#markdown}}{{content}}{{/markdown}}>';
final Map<String, String Function(LambdaContext ctx)> values =
<String, String Function(LambdaContext ctx)>{
'markdown': (LambdaContext ctx) {
return ctx.renderSource(
ctx.source,
value: <String, String>{'content': 'oi!'},
);
},
};
const String output = '<oi!>';
expect(parse(template).renderString(values), equals(output));
});
});
}
Exception? renderFail(String source, Object values) {
try {
parse(source).renderString(values);
return null;
} on Exception catch (e) {
return e;
}
}
void expectFail(
Exception? ex,
int? line,
int? column, [
String? msgStartsWith,
]) {
if (ex is! TemplateException) {
fail('Unexpected type: $ex');
}
if (line != null) {
expect(ex.line, equals(line));
}
if (column != null) {
expect(ex.column, equals(column));
}
if (msgStartsWith != null) {
expect(ex.message, startsWith(msgStartsWith));
}
}