import { parse, print } from '../dist/esm'; import { equals, shouldThrow } from './utils'; describe('parser', function() { function astFor(template) { let ast = parse(template); return print(ast); } it('parses simple mustaches', function() { equals(astFor('{{123}}'), '{{ NUMBER{123} [] }}\n'); equals(astFor('{{"foo"}}'), '{{ "foo" [] }}\n'); equals(astFor('{{false}}'), '{{ BOOLEAN{false} [] }}\n'); equals(astFor('{{true}}'), '{{ BOOLEAN{true} [] }}\n'); equals(astFor('{{foo}}'), '{{ PATH:foo [] }}\n'); equals(astFor('{{foo?}}'), '{{ PATH:foo? [] }}\n'); equals(astFor('{{foo_}}'), '{{ PATH:foo_ [] }}\n'); equals(astFor('{{foo-}}'), '{{ PATH:foo- [] }}\n'); equals(astFor('{{foo:}}'), '{{ PATH:foo: [] }}\n'); }); it('parses simple mustaches with data', function() { equals(astFor('{{@foo}}'), '{{ @PATH:foo [] }}\n'); }); it('parses simple mustaches with data paths', function() { equals(astFor('{{@../foo}}'), '{{ @PATH:foo [] }}\n'); }); it('parses mustaches with paths', function() { equals(astFor('{{foo/bar}}'), '{{ PATH:foo/bar [] }}\n'); }); it('parses mustaches with this/foo', function() { equals(astFor('{{this/foo}}'), '{{ PATH:foo [] }}\n'); }); it('parses mustaches with - in a path', function() { equals(astFor('{{foo-bar}}'), '{{ PATH:foo-bar [] }}\n'); }); it('parses mustaches with escaped [] in a path', function() { equals(astFor('{{[foo[\\]]}}'), '{{ PATH:foo[] [] }}\n'); }); it('parses escaped \\\\ in path', function() { equals(astFor('{{[foo\\\\]}}'), '{{ PATH:foo\\ [] }}\n'); }); it('parses mustaches with parameters', function() { equals(astFor('{{foo bar}}'), '{{ PATH:foo [PATH:bar] }}\n'); }); it('parses mustaches with string parameters', function() { equals(astFor('{{foo bar "baz" }}'), '{{ PATH:foo [PATH:bar, "baz"] }}\n'); }); it('parses mustaches with NUMBER parameters', function() { equals(astFor('{{foo 1}}'), '{{ PATH:foo [NUMBER{1}] }}\n'); }); it('parses mustaches with BOOLEAN parameters', function() { equals(astFor('{{foo true}}'), '{{ PATH:foo [BOOLEAN{true}] }}\n'); equals(astFor('{{foo false}}'), '{{ PATH:foo [BOOLEAN{false}] }}\n'); }); it('parses mustaches with undefined and null paths', function() { equals(astFor('{{undefined}}'), '{{ UNDEFINED [] }}\n'); equals(astFor('{{null}}'), '{{ NULL [] }}\n'); }); it('parses mustaches with undefined and null parameters', function() { equals( astFor('{{foo undefined null}}'), '{{ PATH:foo [UNDEFINED, NULL] }}\n' ); }); it('parses mustaches with DATA parameters', function() { equals(astFor('{{foo @bar}}'), '{{ PATH:foo [@PATH:bar] }}\n'); }); it('parses mustaches with hash arguments', function() { equals(astFor('{{foo bar=baz}}'), '{{ PATH:foo [] HASH{bar=PATH:baz} }}\n'); equals(astFor('{{foo bar=1}}'), '{{ PATH:foo [] HASH{bar=NUMBER{1}} }}\n'); equals( astFor('{{foo bar=true}}'), '{{ PATH:foo [] HASH{bar=BOOLEAN{true}} }}\n' ); equals( astFor('{{foo bar=false}}'), '{{ PATH:foo [] HASH{bar=BOOLEAN{false}} }}\n' ); equals( astFor('{{foo bar=@baz}}'), '{{ PATH:foo [] HASH{bar=@PATH:baz} }}\n' ); equals( astFor('{{foo bar=baz bat=bam}}'), '{{ PATH:foo [] HASH{bar=PATH:baz, bat=PATH:bam} }}\n' ); equals( astFor('{{foo bar=baz bat="bam"}}'), '{{ PATH:foo [] HASH{bar=PATH:baz, bat="bam"} }}\n' ); equals(astFor("{{foo bat='bam'}}"), '{{ PATH:foo [] HASH{bat="bam"} }}\n'); equals( astFor('{{foo omg bar=baz bat="bam"}}'), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam"} }}\n' ); equals( astFor('{{foo omg bar=baz bat="bam" baz=1}}'), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=NUMBER{1}} }}\n' ); equals( astFor('{{foo omg bar=baz bat="bam" baz=true}}'), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{true}} }}\n' ); equals( astFor('{{foo omg bar=baz bat="bam" baz=false}}'), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{false}} }}\n' ); }); it('parses contents followed by a mustache', function() { equals( astFor('foo bar {{baz}}'), "CONTENT[ 'foo bar ' ]\n{{ PATH:baz [] }}\n" ); }); it('parses a partial', function() { equals(astFor('{{> foo }}'), '{{> PARTIAL:foo }}\n'); equals(astFor('{{> "foo" }}'), '{{> PARTIAL:foo }}\n'); equals(astFor('{{> 1 }}'), '{{> PARTIAL:1 }}\n'); }); it('parses a partial with context', function() { equals(astFor('{{> foo bar}}'), '{{> PARTIAL:foo PATH:bar }}\n'); }); it('parses a partial with hash', function() { equals( astFor('{{> foo bar=bat}}'), '{{> PARTIAL:foo HASH{bar=PATH:bat} }}\n' ); }); it('parses a partial with context and hash', function() { equals( astFor('{{> foo bar bat=baz}}'), '{{> PARTIAL:foo PATH:bar HASH{bat=PATH:baz} }}\n' ); }); it('parses a partial with a complex name', function() { equals( astFor('{{> shared/partial?.bar}}'), '{{> PARTIAL:shared/partial?.bar }}\n' ); }); it('parsers partial blocks', function() { equals( astFor('{{#> foo}}bar{{/foo}}'), "{{> PARTIAL BLOCK:foo PROGRAM:\n CONTENT[ 'bar' ]\n }}\n" ); }); it('should handle parser block mismatch', function() { shouldThrow( function() { astFor('{{#> goodbyes}}{{/hellos}}'); }, Error, /goodbyes doesn't match hellos/ ); }); it('parsers partial blocks with arguments', function() { equals( astFor('{{#> foo context hash=value}}bar{{/foo}}'), "{{> PARTIAL BLOCK:foo PATH:context HASH{hash=PATH:value} PROGRAM:\n CONTENT[ 'bar' ]\n }}\n" ); }); it('parses a comment', function() { equals( astFor('{{! this is a comment }}'), "{{! ' this is a comment ' }}\n" ); }); it('parses a multi-line comment', function() { equals( astFor('{{!\nthis is a multi-line comment\n}}'), "{{! '\nthis is a multi-line comment\n' }}\n" ); }); it('parses an inverse section', function() { equals( astFor('{{#foo}} bar {{^}} baz {{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n" ); }); it('parses an inverse (else-style) section', function() { equals( astFor('{{#foo}} bar {{else}} baz {{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n" ); }); it('parses multiple inverse sections', function() { equals( astFor('{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n PATH:if [PATH:bar]\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]\n" ); }); it('parses empty blocks', function() { equals(astFor('{{#foo}}{{/foo}}'), 'BLOCK:\n PATH:foo []\n PROGRAM:\n'); }); it('parses empty blocks with empty inverse section', function() { equals( astFor('{{#foo}}{{^}}{{/foo}}'), 'BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n' ); }); it('parses empty blocks with empty inverse (else-style) section', function() { equals( astFor('{{#foo}}{{else}}{{/foo}}'), 'BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n' ); }); it('parses non-empty blocks with empty inverse section', function() { equals( astFor('{{#foo}} bar {{^}}{{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n" ); }); it('parses non-empty blocks with empty inverse (else-style) section', function() { equals( astFor('{{#foo}} bar {{else}}{{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n" ); }); it('parses empty blocks with non-empty inverse section', function() { equals( astFor('{{#foo}}{{^}} bar {{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n" ); }); it('parses empty blocks with non-empty inverse (else-style) section', function() { equals( astFor('{{#foo}}{{else}} bar {{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n" ); }); it('parses a standalone inverse section', function() { equals( astFor('{{^foo}}bar{{/foo}}'), "BLOCK:\n PATH:foo []\n {{^}}\n CONTENT[ 'bar' ]\n" ); }); it('throws on old inverse section', function() { shouldThrow(function() { astFor('{{else foo}}bar{{/foo}}'); }, Error); }); it('parses block with block params', function() { equals( astFor('{{#foo as |bar baz|}}content{{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n" ); }); it('parses mustaches with sub-expressions as the callable', function() { equals( astFor('{{(my-helper foo)}}'), '{{ PATH:my-helper [PATH:foo] [] }}\n' ); }); it('parses mustaches with sub-expressions as the callable (with args)', function() { equals( astFor('{{(my-helper foo) bar}}'), '{{ PATH:my-helper [PATH:foo] [PATH:bar] }}\n' ); }); it('parses sub-expressions with a sub-expression as the callable', function() { equals( astFor('{{((my-helper foo))}}'), '{{ PATH:my-helper [PATH:foo] [] [] }}\n' ); }); it('parses sub-expressions with a sub-expression as the callable (with args)', function() { equals( astFor('{{((my-helper foo) bar)}}'), '{{ PATH:my-helper [PATH:foo] [PATH:bar] [] }}\n' ); }); it('parses arguments with a sub-expression as the callable (with args)', function() { equals( astFor('{{my-helper ((foo) bar) baz=((foo bar))}}'), '{{ PATH:my-helper [PATH:foo [] [PATH:bar]] HASH{baz=PATH:foo [PATH:bar] []} }}\n' ); }); it('parses inverse block with block params', function() { equals( astFor('{{^foo as |bar baz|}}content{{/foo}}'), "BLOCK:\n PATH:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n" ); }); it('parses chained inverse block with block params', function() { equals( astFor('{{#foo}}{{else foo as |bar baz|}}content{{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n" ); }); it("raises if there's a Parse error", function() { shouldThrow( function() { astFor('foo{{^}}bar'); }, Error, /Parse error on line 1/ ); shouldThrow( function() { astFor('{{foo}'); }, Error, /Parse error on line 1/ ); shouldThrow( function() { astFor('{{foo &}}'); }, Error, /Parse error on line 1/ ); shouldThrow( function() { astFor('{{#goodbyes}}{{/hellos}}'); }, Error, /goodbyes doesn't match hellos/ ); shouldThrow( function() { astFor('{{{{goodbyes}}}} {{{{/hellos}}}}'); }, Error, /goodbyes doesn't match hellos/ ); }); it('should handle invalid paths', function() { shouldThrow( function() { astFor('{{foo/../bar}}'); }, Error, /Invalid path: foo\/\.\. - 1:2/ ); shouldThrow( function() { astFor('{{foo/./bar}}'); }, Error, /Invalid path: foo\/\. - 1:2/ ); shouldThrow( function() { astFor('{{foo/this/bar}}'); }, Error, /Invalid path: foo\/this - 1:2/ ); }); it('knows how to report the correct line number in errors', function() { shouldThrow( function() { astFor('hello\nmy\n{{foo}'); }, Error, /Parse error on line 3/ ); shouldThrow( function() { astFor('hello\n\nmy\n\n{{foo}'); }, Error, /Parse error on line 5/ ); }); it('knows how to report the correct line number in errors when the first character is a newline', function() { shouldThrow( function() { astFor('\n\nhello\n\nmy\n\n{{foo}'); }, Error, /Parse error on line 7/ ); }); describe('externally compiled AST', function() { it('can pass through an already-compiled AST', function() { equals( astFor({ type: 'Program', body: [{ type: 'ContentStatement', value: 'Hello' }] }), "CONTENT[ 'Hello' ]\n" ); }); }); describe('directives', function() { it('should parse block directives', function() { equals( astFor('{{#* foo}}{{/foo}}'), 'DIRECTIVE BLOCK:\n PATH:foo []\n PROGRAM:\n' ); }); it('should parse directives', function() { equals(astFor('{{* foo}}'), '{{ DIRECTIVE PATH:foo [] }}\n'); }); it('should fail if directives have inverse', function() { shouldThrow( function() { astFor('{{#* foo}}{{^}}{{/foo}}'); }, Error, /Unexpected inverse/ ); }); }); it('GH1024 - should track program location properly', function() { let p = parse( '\n' + ' {{#if foo}}\n' + ' {{bar}}\n' + ' {{else}} {{baz}}\n' + '\n' + ' {{/if}}\n' + ' ' ); // We really need a deep equals but for now this should be stable... equals( JSON.stringify(p.loc), JSON.stringify({ start: { line: 1, column: 0 }, end: { line: 7, column: 4 } }) ); equals( JSON.stringify(p.body[1].program.loc), JSON.stringify({ start: { line: 2, column: 13 }, end: { line: 4, column: 7 } }) ); equals( JSON.stringify(p.body[1].inverse.loc), JSON.stringify({ start: { line: 4, column: 15 }, end: { line: 6, column: 5 } }) ); }); });