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({'var': 'bob'}); expect(output, equals('_bob_')); }); test('Comment', () { final String output = parse( '_{{! i am a\n comment ! }}_', ).renderString({}); expect(output, equals('__')); }); test('Emoji', () { final String output = parse( 'Hello! 🖖👍🏽\nBye! 🏳️‍🌈', ).renderString({}); expect(output, equals('Hello! 🖖👍🏽\nBye! 🏳️‍🌈')); }); }); group('Section', () { test('Map', () { final String output = parse( '{{#section}}_{{var}}_{{/section}}', ).renderString(>{ 'section': {'var': 'bob'}, }); expect(output, equals('_bob_')); }); test('List', () { final String output = parse( '{{#section}}_{{var}}_{{/section}}', ).renderString(>>{ 'section': >[ {'var': 'bob'}, {'var': 'jim'}, ], }); expect(output, equals('_bob__jim_')); }); test('Empty List', () { final String output = parse( '{{#section}}_{{var}}_{{/section}}', ).renderString(>{'section': []}); expect(output, equals('')); }); test('False', () { final String output = parse( '{{#section}}_{{var}}_{{/section}}', ).renderString({'section': false}); expect(output, equals('')); }); test('Invalid value', () { final Exception? ex = renderFail( '{{#section}}_{{var}}_{{/section}}', {'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({'var': 42}); expect(output, equals('_42_')); }); test('True', () { final String output = parse( '{{#section}}_ok_{{/section}}', ).renderString({'section': true}); expect(output, equals('_ok_')); }); test('Nested', () { final String output = parse( '{{#section}}.{{var}}.{{#nested}}_{{nestedvar}}_{{/nested}}.{{/section}}', ).renderString(>{ 'section': { 'var': 'bob', 'nested': >[ {'nestedvar': 'jim'}, {'nestedvar': 'sally'}, ], }, }); expect(output, equals('.bob._jim__sally_.')); }); 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', () { void render(String source, dynamic values, dynamic 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'); // 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({}), 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(>{ 'section': {'var': 'bob'}, }); expect(output, equals('')); }); test('List', () { final String output = parse( '{{^section}}_{{var}}_{{/section}}', ).renderString(>>{ 'section': >[ {'var': 'bob'}, {'var': 'jim'}, ], }); expect(output, equals('')); }); test('Empty List', () { final String output = parse( '{{^section}}_ok_{{/section}}', ).renderString(>{'section': []}); expect(output, equals('_ok_')); }); test('False', () { final String output = parse( '{{^section}}_ok_{{/section}}', ).renderString({'section': false}); expect(output, equals('_ok_')); }); test('Invalid value', () { final Exception? ex = renderFail( '{{^section}}_{{var}}_{{/section}}', {'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({'var': 42}); expect(output, equals('')); }); test('True', () { final String output = parse( '{{^section}}_ok_{{/section}}', ).renderString({'section': true}); expect(output, equals('')); }); }); group('Html escape', () { test('Escape at start', () { final String output = parse( '_{{var}}_', ).renderString({'var': '&.'}); expect(output, equals('_&._')); }); test('Escape at end', () { final String output = parse( '_{{var}}_', ).renderString({'var': '.&'}); expect(output, equals('_.&_')); }); test('&', () { final String output = parse( '_{{var}}_', ).renderString({'var': '&'}); expect(output, equals('_&_')); }); test('<', () { final String output = parse( '_{{var}}_', ).renderString({'var': '<'}); expect(output, equals('_<_')); }); test('>', () { final String output = parse( '_{{var}}_', ).renderString({'var': '>'}); expect(output, equals('_>_')); }); test('"', () { final String output = parse( '_{{var}}_', ).renderString({'var': '"'}); expect(output, equals('_"_')); }); test("'", () { final String output = parse( '_{{var}}_', ).renderString({'var': "'"}); expect(output, equals('_'_')); }); test('/', () { final String output = parse( '_{{var}}_', ).renderString({'var': '/'}); expect(output, equals('_/_')); }); }); group('Invalid format', () { test('Mismatched tag', () { const String source = '{{#section}}_{{var}}_{{/notsection}}'; final Exception? ex = renderFail(source, >{ 'section': {'var': 'bob'}, }); expectFail(ex, 1, 22, 'Mismatched tag'); }); test('Unexpected EOF', () { const String source = '{{#section}}_{{var}}_{{/section'; final Exception? ex = renderFail(source, >{ 'section': {'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, >{ 'section': {'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, >{ 'section': {'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, >{ 'section': {'var': 'bob'}, }); expectFail(ex, null, null, BAD_TAG_NAME); }); test('Missing variable', () { const String source = r'{{#section}}_{{var}}_{{/section}}'; final Exception? ex = renderFail(source, >{ 'section': {}, }); 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(>{ 'section': {'var': null}, }); expect(output, equals('__')); }); test('Unclosed section', () { const String source = r'{{#section}}foo'; final Exception? ex = renderFail(source, >{ 'section': {}, }); expectFail(ex, null, null, UNCLOSED_TAG); }); }); group('Lenient', () { test('Odd section name', () { final String output = parse( r'{{#section$%$^%}}_{{var}}_{{/section$%$^%}}', lenient: true, ).renderString(>{ r'section$%$^%': {'var': 'bob'}, }); expect(output, equals('_bob_')); }); test('Odd variable name', () { final String output = parse( r'{{#section}}_{{var$%$^%}}_{{/section}}', lenient: true, ).renderString(>{ 'section': {r'var$%$^%': 'bob'}, }); expect(output, equals('_bob_')); }); test('Null variable', () { final String output = parse( r'{{#section}}_{{var}}_{{/section}}', lenient: true, ).renderString(>{ 'section': {'var': null}, }); expect(output, equals('__')); }); test('Null section', () { final String 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('{{{ ... }}}', () { final String output = parse( '{{{blah}}}', ).renderString({'blah': '&'}); expect(output, equals('&')); }); test('{{& ... }}', () { final String output = parse( '{{{blah}}}', ).renderString({'blah': '&'}); expect(output, equals('&')); }); }); group('Partial tag', () { String partialTest( Map values, Map sources, String renderTemplate, { bool lenient = false, }) { final Map templates = {}; 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( {'foo': 'bar'}, {'root': '{{>partial}}', 'partial': '{{foo}}'}, 'root', ); expect(output, 'bar'); }); test('missing partial strict', () { bool threw = false; try { partialTest( {'foo': 'bar'}, {'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( {'foo': 'bar'}, {'root': '{{>partial}}'}, 'root', lenient: true, ); expect(output, equals('')); }); test('context', () { final String output = partialTest( {'text': 'content'}, {'root': '"{{>partial}}"', 'partial': '*{{text}}*'}, 'root', lenient: true, ); expect(output, equals('"*content*"')); }); test('recursion', () { final String output = partialTest( { 'content': 'X', 'nodes': >[ {'content': 'Y', 'nodes': []}, ], }, { 'root': '{{>node}}', 'node': '{{content}}<{{#nodes}}{{>node}}{{/nodes}}>', }, 'root', lenient: true, ); expect(output, equals('X>')); }); test('standalone without previous', () { final String output = partialTest( {}, {'root': ' {{>partial}}\n>', 'partial': '>\n>'}, 'root', lenient: true, ); expect(output, equals(' >\n >>')); }); test('standalone indentation', () { final String 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', () { void lambdaTest({ required String template, dynamic lambda, dynamic 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: '<>>', ); }); 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 values = { '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 values = { 'markdown': (LambdaContext ctx) => ctx.renderString().toLowerCase(), 'content': 'OI YOU!', }; const String output = ''; expect(parse(template).renderString(values), equals(output)); }); test('Lambda v2', () { const String template = '<{{#markdown}}{{content}}{{/markdown}}>'; final Map values = { '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 values = { // ignore: avoid_dynamic_calls 'markdown': (dynamic ctx) => ctx.source, }; const String output = ''; 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 values = { '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 values = { '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({ '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({ '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(>{'bob': []}); expect(val, equals('|\n|')); }); }); group('Array indexing', () { test('Basic', () { final String val = parse('{{array.1}}').renderString(>{ 'array': [1, 2, 3], }); expect(val, equals('2')); }); test('RangeError', () { final Exception? error = renderFail('{{array.5}}', >{ 'array': [1, 2, 3], }); expect(error, isA()); }); }); group('Delimiters', () { test('Basic', () { final String val = parse( '{{=<% %>=}}(<%text%>)', ).renderString({'text': 'Hey!'}); expect(val, equals('(Hey!)')); }); test('Single delimiters', () { final String val = parse( '({{=[ ]=}}[text])', ).renderString({'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({'text': 'Hey!'}); expect(val, equals('(Hey!)')); }); }); group('Lambda context', () { test('LambdaContext write', () { const String template = '<{{#markdown}}{{content}}{{/markdown}}>'; final Map values = { 'markdown': (LambdaContext ctx) { ctx.write('foo'); }, }; const String output = ''; expect(parse(template).renderString(values), equals(output)); }); test('LambdaContext render', () { const String template = '<{{#markdown}}{{content}}{{/markdown}}>'; final Map values = { 'content': 'bar', 'markdown': (LambdaContext ctx) { ctx.render(); }, }; const String output = ''; expect(parse(template).renderString(values), equals(output)); }); test('LambdaContext render with value', () { const String template = '<{{#markdown}}{{content}}{{/markdown}}>'; final Map values = { 'markdown': (LambdaContext ctx) { ctx.render(value: {'content': 'oi!'}); }, }; const String output = ''; expect(parse(template).renderString(values), equals(output)); }); test('LambdaContext renderString with value', () { const String template = '<{{#markdown}}{{content}}{{/markdown}}>'; final Map values = { 'markdown': (LambdaContext ctx) { return ctx.renderString( value: {'content': 'oi!'}, ); }, }; const String output = ''; expect(parse(template).renderString(values), equals(output)); }); test('LambdaContext write and return', () { const String template = '<{{#markdown}}{{content}}{{/markdown}}>'; final Map values = { 'markdown': (LambdaContext ctx) { ctx.write('foo'); return 'bar'; }, }; const String output = ''; expect(parse(template).renderString(values), equals(output)); }); test('LambdaContext renderSource with value', () { const String template = '<{{#markdown}}{{content}}{{/markdown}}>'; final Map values = { 'markdown': (LambdaContext ctx) { return ctx.renderSource( ctx.source, value: {'content': 'oi!'}, ); }, }; const String output = ''; 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)); } }