/* * Copyright 2008 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.template.soy.soyparse; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.template.soy.ErrorReporterImpl; import com.google.template.soy.SoyFileSetParser; import com.google.template.soy.base.internal.IncrementingIdGenerator; import com.google.template.soy.base.internal.SoyFileKind; import com.google.template.soy.base.internal.SoyFileSupplier; import com.google.template.soy.basetree.SyntaxVersion; import com.google.template.soy.data.SanitizedContent.ContentKind; import com.google.template.soy.error.ErrorReporter; import com.google.template.soy.error.ExplodingErrorReporter; import com.google.template.soy.error.FormattingErrorReporter; import com.google.template.soy.error.PrettyErrorFactory; import com.google.template.soy.error.SnippetFormatter; import com.google.template.soy.exprtree.FieldAccessNode; import com.google.template.soy.exprtree.FunctionNode; import com.google.template.soy.exprtree.IntegerNode; import com.google.template.soy.exprtree.OperatorNodes.GreaterThanOpNode; import com.google.template.soy.exprtree.OperatorNodes.NegativeOpNode; import com.google.template.soy.exprtree.OperatorNodes.PlusOpNode; import com.google.template.soy.exprtree.StringNode; import com.google.template.soy.exprtree.VarRefNode; import com.google.template.soy.passes.CombineConsecutiveRawTextNodesVisitor; import com.google.template.soy.passes.PassManager; import com.google.template.soy.shared.AutoEscapingType; import com.google.template.soy.shared.SharedTestUtils; import com.google.template.soy.shared.SoyGeneralOptions; import com.google.template.soy.shared.restricted.SoyFunction; import com.google.template.soy.soytree.CallBasicNode; import com.google.template.soy.soytree.CallDelegateNode; import com.google.template.soy.soytree.CallNode; import com.google.template.soy.soytree.CallParamContentNode; import com.google.template.soy.soytree.CallParamValueNode; import com.google.template.soy.soytree.CssNode; import com.google.template.soy.soytree.DebuggerNode; import com.google.template.soy.soytree.ForNode; import com.google.template.soy.soytree.ForeachIfemptyNode; import com.google.template.soy.soytree.ForeachNode; import com.google.template.soy.soytree.ForeachNonemptyNode; import com.google.template.soy.soytree.IfCondNode; import com.google.template.soy.soytree.IfElseNode; import com.google.template.soy.soytree.IfNode; import com.google.template.soy.soytree.LetContentNode; import com.google.template.soy.soytree.LetValueNode; import com.google.template.soy.soytree.LogNode; import com.google.template.soy.soytree.MsgFallbackGroupNode; import com.google.template.soy.soytree.MsgHtmlTagNode; import com.google.template.soy.soytree.MsgNode; import com.google.template.soy.soytree.MsgPlaceholderNode; import com.google.template.soy.soytree.MsgPluralCaseNode; import com.google.template.soy.soytree.MsgPluralDefaultNode; import com.google.template.soy.soytree.MsgPluralNode; import com.google.template.soy.soytree.MsgSelectCaseNode; import com.google.template.soy.soytree.MsgSelectDefaultNode; import com.google.template.soy.soytree.MsgSelectNode; import com.google.template.soy.soytree.PrintDirectiveNode; import com.google.template.soy.soytree.PrintNode; import com.google.template.soy.soytree.RawTextNode; import com.google.template.soy.soytree.SoyFileNode; import com.google.template.soy.soytree.SoyNode.StandaloneNode; import com.google.template.soy.soytree.SwitchCaseNode; import com.google.template.soy.soytree.SwitchDefaultNode; import com.google.template.soy.soytree.SwitchNode; import com.google.template.soy.soytree.TemplateNode; import com.google.template.soy.soytree.TemplateSubject; import com.google.template.soy.soytree.XidNode; import com.google.template.soy.soytree.defn.HeaderParam; import com.google.template.soy.soytree.defn.TemplateParam; import com.google.template.soy.types.SoyTypeRegistry; import java.io.StringReader; import java.util.List; import junit.framework.AssertionFailedError; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Unit tests for the template parser. * */ @RunWith(JUnit4.class) public final class TemplateParserTest { private static final ErrorReporter FAIL = ExplodingErrorReporter.get(); // ----------------------------------------------------------------------------------------------- // Tests for recognition only. @Test public void testRecognizeSoyTag() throws Exception { assertIsTemplateBody("{sp}"); assertIsTemplateBody("{space}"); assertIsTemplateBody("{ sp }"); TemplateSubject.assertThatTemplateContent("{{sp}}") .causesError("Soy {{command}} syntax is no longer supported. Use single braces."); TemplateSubject.assertThatTemplateContent("{{print { }}") .causesError("Soy {{command}} syntax is no longer supported. Use single braces."); TemplateSubject.assertThatTemplateContent("a {} b") .causesError("Found 'print' command with empty command text."); TemplateSubject.assertThatTemplateContent("{msg desc=\"\"}a {} b{/msg}") .causesError("Found 'print' command with empty command text."); TemplateSubject.assertThatTemplateContent("{msg desc=\"\"}<a> {} </a>{/msg}") .causesError("Found 'print' command with empty command text."); TemplateSubject.assertThatTemplateContent("{msg desc=\"\"}<a href=\"{}\" />{/msg}") .causesError("Found 'print' command with empty command text."); TemplateSubject.assertThatTemplateContent("{/blah}") .causesError("Unexpected closing tag '{/blah}'."); TemplateSubject.assertThatTemplateContent("}") .causesError("Unexpected '}'; did you mean '{rb}'?"); TemplateSubject.assertThatTemplateContent("{@blah}") .causesError("Invalid declaration '{@blah'."); TemplateSubject.assertThatTemplateContent("{sp ace}") .causesError("Command '{sp ace' cannot have arguments."); TemplateSubject.assertThatTemplateContent("{literal a=b}") .causesError("Command '{literal a=b' cannot have arguments."); TemplateSubject.assertThatTemplateContent("{else(a)}") .causesError("Command '{else(a)' cannot have arguments."); TemplateSubject.assertThatTemplateContent("{template}") .causesError("Command '{template' cannot appear in templates."); TemplateSubject.assertThatTemplateContent("{deltemplate a=b}") .causesError("Command '{deltemplate a=b' cannot appear in templates."); TemplateSubject.assertThatTemplateContent("{namespace()}") .causesError("Command '{namespace()' cannot appear in templates."); TemplateSubject.assertThatTemplateContent("{}") .causesError("Found 'print' command with empty command text."); TemplateSubject.assertThatTemplateContent("{print }") .causesError("Found 'print' command with empty command text."); assertIsTemplateBody("{if $blah == 'phname = \"foo\"'}{/if}"); assertIsNotTemplateBody("{blah phname=\"\"}"); assertIsNotTemplateBody("{"); assertIsNotTemplateBody("{{ {sp} }}"); assertIsNotTemplateBody("{{ {} }}"); assertIsNotTemplateBody("{{ }s{p { }}"); assertIsNotTemplateBody("{}"); assertIsNotTemplateBody("{namespace"); assertIsNotTemplateBody("{sp"); assertIsNotTemplateBody("{sp blah}"); assertIsNotTemplateBody("{print } }"); assertIsNotTemplateBody("{print }}"); assertIsNotTemplateBody("{{}}"); assertIsNotTemplateBody("{{{blah: blah}}}"); assertIsNotTemplateBody("blah}blah"); assertIsNotTemplateBody("blah}}blah"); assertIsNotTemplateBody("{{print {{ }}"); } @Test public void testRecognizeRawText() throws Exception { assertIsTemplateBody("blah>blah<blah<blah>blah>blah>blah>blah<blah"); assertIsTemplateBody("{sp}{nil}{\\n}{\\r}{\\t}{lb}{rb}"); assertIsTemplateBody( "blah{literal}{ {{{ } }{ {}} { }}}}}}}\n" + "}}}}}}}}}{ { {{/literal}blah"); assertIsTemplateBody("{literal}{literal}{/literal}"); assertIsNotTemplateBody("{/literal}"); assertIsNotTemplateBody("{literal attr=\"value\"}"); } @Test public void testRecognizeComments() throws Exception { assertIsTemplateBody( "" + "blah // }\n" + "{$boo}{msg desc=\"\"} //}\n" + "{/msg} // {/msg}\n" + "{foreach $item in $items}\t// }\n" + "{$item.name}{/foreach} //{{{{\n"); assertIsTemplateBody( "" + "blah /* } */\n" + "{msg desc=\"\"} /*}*/{$boo}\n" + "/******************/ {/msg}\n" + "/* {}} { }* }* / }/ * { **} //}{ { } {\n" + "\n } {//*} {* /} { /* /}{} {}/ } **}}} */\n" + "{foreach $item in $items} /* }\n" + "{{{{{*/{$item.name}{/foreach}/*{{{{*/\n"); assertIsTemplateBody( "" + "blah /** } */\n" + "{msg desc=\"\"} /**}*/{$boo}\n" + "/******************/ {/msg}\n" + "/** {}} { }* }* / }/ * { **} //}{ { } {\n" + "\n } {//**} {* /} { /** /}{} {}/ } **}}} */\n" + "{foreach $item in $items} /** }\n" + "{{{{{*/{$item.name}{/foreach}/**{{{{*/\n"); assertIsTemplateBody(" // Not an invalid command: }\n"); assertIsTemplateBody(" // Not an invalid command: {{let}}\n"); assertIsTemplateBody(" // Not an invalid command: {@let }\n"); assertIsTemplateBody(" // Not an invalid command: phname=\"???\"\n"); assertIsTemplateBody("{msg desc=\"\"} // <{/msg}> '<<>\n{/msg}"); assertIsTemplateBody("//}\n"); assertIsTemplateBody(" //}\n"); assertIsTemplateBody("\n//}\n"); assertIsTemplateBody("\n //}\n"); assertIsTemplateBody("/*}*/\n"); assertIsTemplateBody(" /*}*/\n"); assertIsTemplateBody("\n/*}\n}*/\n"); assertIsTemplateBody("\n /*}\n*/\n"); assertIsTemplateBody("/**}*/\n"); assertIsTemplateBody(" /**}*/\n"); assertIsTemplateBody("\n/**}\n}*/\n"); assertIsTemplateBody("\n /**}\n*/\n"); assertIsNotTemplateBody("{css // }"); assertIsNotTemplateBody( "{foreach $item // }\n" + " in $items}\n" + "{$item}{/foreach}\n"); assertIsNotTemplateBody("aa////}\n"); assertIsNotTemplateBody("{nil}//}\n"); } @Test public void testRecognizeHeaderParams() throws Exception { assertIsTemplateContent("{@param foo: int}\n"); assertIsTemplateContent("{@param foo: int}\nBODY"); assertIsTemplateContent(" {@param foo: int}\n BODY"); assertIsTemplateContent("\n{@param foo: int}\n"); assertIsTemplateContent(" \n{@param foo: int}\nBODY"); assertIsTemplateContent(" \n {@param foo:\n int}\n BODY"); assertIsTemplateContent("{@param foo: int|list<[a: map<string, int|string>, b:?|null]>}\n"); assertIsTemplateContent( "" + " {@param foo1: int} {@param foo2: int}\n" + " {@param foo3: int} /** ... */\n" // doc comment + " {@param foo4: int} // ...\n" // nondoc comment + " {@param foo5:\n" + " int} /** ...\n" // doc comment + " ...\n" + " ... */\n" + " /*\n" // nondoc comment + " * ...\n" + " */\n" + " /* ... */\n" // nondoc comment + " {@param foo6: int} /**\n" // doc comment + " ... */ \n" + " {@param foo7: int} /*\n" // nondoc comment + " ... */ \n" + "\n" + " BODY\n"); assertIsTemplateContent( "" + " /** */{@param foo1: int}\n" // doc comment + " /** \n" // doc comment + " */{@param foo2: int}\n" + "\n" + " BODY\n"); assertIsTemplateContent("{@param foo: int}"); assertIsNotTemplateContent("{@ param foo: int}\n"); assertIsNotTemplateContent("{@foo}\n"); assertIsNotTemplateContent("{@foo foo: int}\n"); assertIsTemplateContent( "" + " /** ... */\n" // doc comment + " {@param foo: int}\n" + " BODY\n"); assertIsTemplateContent( "" + " {@param foo1: int}\n" + " /**\n" // doc comment + " * ...\n" + " */\n" + " {@param foo2: int}\n" + " BODY\n"); assertIsTemplateContent( "" + " {@param foo1: int} /*\n" + " */ /** ... */\n" // doc comment + " {@param foo2: int}\n" + " BODY\n"); assertIsNotTemplateContent("{@param 33: int}"); assertIsNotTemplateContent("{@param f-oo: int}"); assertIsNotTemplateContent("{@param foo}"); assertIsNotTemplateContent("{@param foo:}"); assertIsNotTemplateContent("{@param : int}"); assertIsNotTemplateContent("{@param foo int}"); } @Test public void testQuotedStringsInCommands() throws Exception { assertValidTemplate("{let $a: null /}"); assertValidTemplate("{let $a: '' /}"); assertValidTemplate("{let $a: 'a\"b\"c' /}"); assertValidTemplate("{let $a: 'abc\\'def' /}"); assertValidTemplate("{let $a: 'abc\\\\def' /}"); assertValidTemplate("{let $a: 'abc\\\\\\\\def' /}"); assertValidTemplate("{let $a: '\\\\ \\' \\\" \\n \\r \\t \\b \\f \\u00A9 \\u2468' /}"); assertValidTemplate("{let $a: '{} abc {}' /}"); assertValidTemplate("{let $a: '{} abc\\'def {}' /}"); assertValidTemplate("{let $a: '{} abc\\\\def {}' /}"); assertValidTemplate("{let $a: '{} abc\\\\\\\\def {}' /}"); assertValidTemplate("{call blah} {param a: ['blah': '{} abc\\\\\\\\def {}' ] /} {/call}"); assertValidTemplate("{msg desc=\"\"}{/msg}"); assertValidTemplate("{msg desc=\"Hi! I'm short! {}\"}{/msg}"); } @Test public void testRecognizeHeaderInjectedParams() throws Exception { assertIsTemplateContent("{@inject foo: int}\n"); assertIsTemplateContent("{@inject foo: int}\nBODY"); assertIsTemplateContent(" {@inject foo: int}\n BODY"); assertIsTemplateContent("\n{@inject foo: int}\n"); assertIsTemplateContent(" \n{@inject foo: int}\nBODY"); assertIsTemplateContent(" \n {@inject foo:\n int}\n BODY"); assertIsTemplateContent( "" + " {@inject foo1: int} {@inject foo2: int}\n" + " {@inject foo3: int} /** ... */\n" // doc comment + " {@inject foo4: int} // ...\n" // nondoc comment + " {@inject foo5:\n" + " int} /** ...\n" // doc comment + " ...\n" + " ... */\n" + " /*\n" // nondoc comment + " * ...\n" + " */\n" + " /* ... */\n" // nondoc comment + " {@inject foo6: int} /**\n" // doc comment + " ... */ \n" + " {@inject foo7: int} /*\n" // nondoc comment + " ... */ \n" + "\n" + " BODY\n"); assertIsTemplateContent( "" + " /** */{@inject foo1: int}\n" // doc comment + " /** \n" // doc comment + " */{@inject foo2: int}\n" + "\n" + " BODY\n"); assertIsTemplateContent("{@inject foo: int}"); assertIsNotTemplateContent("{@ param foo: int}\n"); assertIsNotTemplateContent("{@foo}\n"); assertIsNotTemplateContent("{@foo foo: int}\n"); assertIsTemplateContent( "" + " /** ... */\n" // doc comment + " {@inject foo: int}\n" + " BODY\n"); assertIsTemplateContent( "" + " {@inject foo1: int}\n" + " /**\n" // doc comment + " * ...\n" + " */\n" + " {@inject foo2: int}\n" + " BODY\n"); assertIsTemplateContent( "" + " {@inject foo1: int} /*\n" + " */ /** ... */\n" // doc comment + " {@inject foo2: int}\n" + " BODY\n"); } @Test public void testRecognizeCommands() throws Exception { assertIsTemplateBody("{formatDate($blah)}"); // Starts with `for` assertIsTemplateBody("{msgblah($blah)}"); // Starts with `msg` assertIsTemplateBody("{let $a: b /}"); // Not a print assertIsTemplateBody( "" + "{msg desc=\"blah\" hidden=\"true\"}\n" + " {$boo} is a <a href=\"{$fooUrl}\">{$foo}</a>.\n" + "{/msg}"); assertIsTemplateBody( "" + "{msg meaning=\"verb\" desc=\"\"}\n" + " Archive\n" + "{fallbackmsg desc=\"\"}\n" + " Archive\n" + "{/msg}"); assertIsTemplateBody("{$aaa + 1}{print $bbb.ccc[$ddd] |noescape}"); assertIsTemplateBody("{css selected-option}{css CSS_SELECTED_OPTION}{css $cssSelectedOption}"); assertIsTemplateBody("{xid selected-option}{xid SELECTED_OPTION_ID}"); assertIsTemplateBody("{if $boo}foo{elseif $goo}moo{else}zoo{/if}"); assertIsTemplateBody( "" + " {switch $boo}\n" + " {case $foo} blah blah\n" + " {case 2, $goo.moo, 'too'} bleh bleh\n" + " {default} bluh bluh\n" + " {/switch}\n"); assertIsTemplateBody("{foreach $item in $items}{index($item)}. {$item.name}<br>{/foreach}"); assertIsTemplateBody( "" + "{for $i in range($boo + 1,\n" + " 88, 11)}\n" + "Number {$i}.{/for}"); assertIsTemplateBody("{call aaa.bbb.ccc data=\"all\" /}"); assertIsTemplateBody( "" + "{call .aaa}\n" + " {param boo: $boo /}\n" + " {param foo}blah blah{/param}\n" + " {param foo kind=\"html\"}blah blah{/param}\n" + "{/call}"); TemplateSubject.assertThatTemplateContent( "{call .aaa}\n" + " {param foo : bar ' baz/}\n" + "{/call}\n") .causesError("Invalid string literal found in Soy command."); TemplateSubject.assertThatTemplateContent( "{call .aaa}\n" + " {param foo : bar \" baz/}\n" + "{/call}\n") .causesError("Invalid string literal found in Soy command."); assertIsTemplateBody("{call aaa.bbb.ccc data=\"all\" /}"); assertIsTemplateBody( "" + "{call .aaa}\n" + " {param boo: $boo /}\n" + " {param foo}blah blah{/param}\n" + "{/call}"); assertIsTemplateBody("{delcall aaa.bbb.ccc data=\"all\" /}"); assertIsTemplateBody( "" + "{delcall ddd.eee}\n" + " {param boo: $boo /}\n" + " {param foo}blah blah{/param}\n" + "{/delcall}"); assertIsTemplateBody( "" + "{msg meaning=\"boo\" desc=\"blah\"}\n" + " {$boo phname=\"foo\"} is a \n" + " <a phname=\"begin_link\" href=\"{$fooUrl}\">\n" + " {$foo |noAutoescape phname=\"booFoo\" }\n" + " </a phname=\"END_LINK\" >.\n" + " {call .aaa data=\"all\"\nphname=\"AaaBbb\"/}\n" + " {call .aaa phname=\"AaaBbb\" data=\"all\"}{/call}\n" + "{/msg}"); assertIsTemplateBody("{log}Blah blah.{/log}"); assertIsTemplateBody("{debugger}"); assertIsTemplateBody("{let $foo : 1 + 2/}\n"); assertIsTemplateBody("{let $foo : '\"'/}\n"); assertIsTemplateBody("{let $foo}Hello{/let}\n"); assertIsTemplateBody("{let $foo kind=\"html\"}Hello{/let}\n"); TemplateSubject.assertThatTemplateContent("{{let a: b}}") .causesError("Soy {{command}} syntax is no longer supported. Use single braces."); // This is parsed as a print command, which shouldn't end in /} TemplateSubject.assertThatTemplateContent("{{let a: b /}}") .causesError( "parse error at '/}': expected }, <CMD_TEXT_DIRECTIVE_NAME>, <CMD_TEXT_PHNAME_ATTR>, " + "or <CMD_TEXT_ARBITRARY_TOKEN>"); assertIsNotTemplateBody("{{let a: b /}}"); assertIsNotTemplateBody("{namespace}"); assertIsNotTemplateBody("{template}\n" + "blah\n" + "{/template}\n"); assertIsNotTemplateBody("{msg}blah{/msg}"); assertIsNotTemplateBody("{/msg}"); assertIsNotTemplateBody("{msg desc=\"\"}<a href=http://www.google.com{/msg}"); assertIsNotTemplateBody("{msg desc=\"\"}blah{msg desc=\"\"}bleh{/msg}bluh{/msg}"); assertIsNotTemplateBody("{msg desc=\"\"}blah{/msg blah}"); TemplateSubject.assertThatTemplateContent( "" + "{msg meaning=\"verb\" desc=\"\"}\n" + " Hi {if blah}a{/if}\n" + "{/msg}") .causesError( "parse error at '{if ': expected " + "text, {literal, {call, {delcall, {fallbackmsg, {/msg}, {print, {plural, " + "{select, {, <, or whitespace"); TemplateSubject.assertThatTemplateContent( "" + "{msg meaning=\"verb\" desc=\"\"}\n" + " Archive\n" + "{fallbackmsg desc=\"\"}\n" + " Archive\n" + "{fallbackmsg desc=\"\"}\n" + " Store\n" + "{/msg}") .causesError( "parse error at '{fallbackmsg ': expected " + "text, {literal, {call, {delcall, {/msg}, {print, {plural, {select, {, <, " + "or whitespace"); assertIsNotTemplateBody("{print $boo /}"); assertIsNotTemplateBody("{if true}aaa{else/}bbb{/if}"); assertIsNotTemplateBody("{call .aaa.bbb /}"); assertIsNotTemplateBody("{delcall ddd.eee}{param foo: 0}{/call}"); assertIsNotTemplateBody("{delcall .dddEee /}"); assertIsNotTemplateBody("{call.aaa}{param boo kind=\"html\": 123 /}{/call}\n"); assertIsNotTemplateBody("{log}"); assertIsNotTemplateBody("{log 'Blah blah.'}"); assertIsNotTemplateBody("{let $foo kind=\"html\" : 1 + 1/}\n"); assertIsNotTemplateBody("{xid a.b-c}"); assertIsNotTemplateBody("{msg desc=\"\"}{$boo phname=\"boo.foo\"}{/msg}"); assertIsNotTemplateBody("{msg desc=\"\"}<br phname=\"boo-foo\" />{/msg}"); assertIsNotTemplateBody("{msg desc=\"\"}{call .boo phname=\"boo\" phname=\"boo\" /}{/msg}"); assertIsNotTemplateBody("{msg desc=\"\"}<br phname=\"break\" phname=\"break\" />{/msg}"); } @Test public void testRecognizeMsgPlural() throws Exception { // Normal, valid plural message. assertIsTemplateBody( " {msg desc=\"A sample plural message\"}\n" + " {plural $num_people offset=\"1\"}\n" + " {case 0}I see no one in {$place}.\n" + " {case 1}I see {$person} in {$place}.\n" + " {case 2}I see {$person} and one other person in {$place}.\n" + " {default}I see {$person} and {remainder($num_people)} " + "other people in {$place}.\n" + " {/plural}\n" + " {/msg}\n"); assertIsTemplateBody( " {let $roundedWeeksSinceStart : 3 /}\n" + " {msg desc=\"Message for number of weeks ago something happened.\"}\n" + " {plural $roundedWeeksSinceStart}\n" + " {case 1} 1 week ago\n" + " {default} {$roundedWeeksSinceStart} weeks ago\n" + " {/plural}\n" + " {/msg}"); // Offset is optional. assertIsTemplateBody( " {msg desc=\"A sample plural message\"}\n" + " {plural $num_people}\n" + " {case 0}I see no one in {$place}.\n" + " {case 1}I see {$person} in {$place}.\n" + " {default}I see {$num_people} in {$place}, including {$person}.\n" + " {/plural}\n" + " {/msg}\n"); // Plural message should have a default clause. assertIsNotTemplateBody( " {msg desc=\"A sample plural message\"}\n" + " {plural $num_people offset=\"1\"}\n" + " {case 0}I see no one in {$place}.\n" + " {case 1}I see {$person} in {$place}.\n" + " {case 2}I see {$person} and one other person in {$place}.\n" + " {/plural}\n" + " {/msg}\n"); // default should be the last clause, after all cases. assertIsNotTemplateBody( " {msg desc=\"A sample plural message\"}\n" + " {plural $num_people offset=\"1\"}\n" + " {default}I see {$person} and {remainder($num_people)} " + "other people in {$place}.\n" + " {case 1}I see {$person} in {$place}.\n" + " {case 0}I see no one in {$place}.\n" + " {case 2}I see {$person} and one other person in {$place}.\n" + " {/plural}\n" + " {/msg}\n"); // Order is irrelevant for cases. assertIsTemplateBody( " {msg desc=\"A sample plural message\"}\n" + " {plural $num_people offset=\"1\"}\n" + " {case 1}I see {$person} in {$place}.\n" + " {case 0}I see no one in {$place}.\n" + " {case 2}I see {$person} and one other person in {$place}.\n" + " {default}I see {$person} and {remainder($num_people)} " + "other people in {$place}.\n" + " {/plural}\n" + " {/msg}\n"); // Offset should not be less than 0. assertIsNotTemplateBody( " {msg desc=\"A sample plural message\"}\n" + " {plural $num_people offset=\"-1\"}\n" + " {case 0}I see no one in {$place}.\n" + " {case 1}I see {$person} in {$place}.\n" + " {case 2}I see {$person} and one other person in {$place}.\n" + " {default}I see {$person} and {remainder($num_people)} " + "other people in {$place}.\n" + " {/plural}\n" + " {/msg}\n"); // Case should not be less than 0. assertIsNotTemplateBody( " {msg desc=\"A sample plural message\"}\n" + " {plural $num_people offset=\"1\"}\n" + " {case 0}I see no one in {$place}.\n" + " {case -1}I see {$person} in {$place}.\n" + " {case 2}I see {$person} and one other person in {$place}.\n" + " {default}I see {$person} and {remainder($num_people)} " + "other people in {$place}.\n" + " {/plural}\n" + " {/msg}\n"); } @Test public void testRecognizeMsgSelect() throws Exception { assertIsTemplateBody( "{msg desc=\"A sample gender message\"}\n" + " {select $gender}\n" + " {case 'female'}{$person} added you to her circle.\n" + " {default}{$person} added you to his circle.\n" + " {/select}\n" + "{/msg}\n"); // Default should be present. assertIsNotTemplateBody( " {select $gender}\n" + " {case 'female'}{$person} added you to her circle.\n" + " {default}{$person} added you to his circle.\n" + " {/select}\n"); // Default should be the last clause. assertIsNotTemplateBody( "{msg desc=\"A sample gender message\"}\n" + " {select $gender}\n" + " {default}{$person} added you to his circle.\n" + " {case 'female'}{$person} added you to her circle.\n" + " {/select}\n" + "{/msg}\n"); // There is no restriction that 'female' and 'male' should not occur together. assertIsTemplateBody( "{msg desc=\"A sample gender message\"}\n" + " {select $gender}\n" + " {case 'female'}{$person} added you to her circle.\n" + " {case 'male'}{$person} added you to his circle.\n" + " {default}{$person} added you to his circle.\n" + " {/select}\n" + "{/msg}\n"); // There is no restriction of case keywords. An arbitrary word like 'neuter' is fine. assertIsTemplateBody( "{msg desc=\"A sample gender message\"}\n" + " {select $gender}\n" + " {case 'female'}{$person} added you to her circle.\n" + " {case 'male'}{$person} added you to his circle.\n" + " {case 'neuter'}{$person} added you to its circle.\n" + " {default}{$person} added you to his circle.\n" + " {/select}\n" + "{/msg}\n"); // It is not possible to have more than one string in a case. assertIsNotTemplateBody( "{msg desc=\"A sample gender message\"}\n" + " {select $job}\n" + " {case 'hw_engineer', 'sw_engineer'}{$person}, an engineer, liked this.\n" + " {default}{$person} liked this.\n" + " {/select}\n" + "{/msg}\n"); // select should have a default. assertIsNotTemplateBody( "{msg desc=\"A sample gender message\"}\n" + " {select $gender}\n" + " {case 'female'}{$person} added you to her circle.\n" + " {case 'male'}{$person} added you to his circle.\n" + " {/select}\n" + "{/msg}\n"); } @Test public void testRecognizeNestedPlrsel() throws Exception { // Select nested inside select should be allowed. assertIsTemplateBody( "{msg desc=\"A sample nested message\"}\n" + " {select $gender}\n" + " {case 'female'}\n" + " {select $gender2}\n" + " {case 'female'}{$person1} added {$person2} and her friends to her circle.\n" + " {default}{$person1} added {$person2} and his friends to her circle.\n" + " {/select}\n" + " {default}\n" + " {select $gender2}\n" + " {case 'female'}{$person1} added {$person2} and her friends to his circle.\n" + " {default}{$person1} added {$person2} and his friends to his circle.\n" + " {/select}\n" + " {/select}\n" + "{/msg}\n"); // Plural nested inside select should be allowed. assertIsTemplateBody( "{msg desc=\"A sample nested message\"}\n" + " {select $gender}\n" + " {case 'female'}\n" + " {plural $num_people}\n" + " {case 1}{$person} added one person to her circle.\n" + " {default}{$person} added {$num_people} to her circle.\n" + " {/plural}\n" + " {default}\n" + " {plural $num_people}\n" + " {case 1}{$person} added one person to his circle.\n" + " {default}{$person} added {$num_people} to his circle.\n" + " {/plural}\n" + " {/select}\n" + "{/msg}\n"); // Plural inside plural should not be allowed. assertIsNotTemplateBody( "{msg desc=\"A sample nested message\"}\n" + " {plural $n_friends}\n" + " {case 1}\n" + " {plural $n_circles}\n" + " {case 1}You have one friend in one circle.\n" + " {default}You have one friend in {$n_circles} circles.\n" + " {/plural}\n" + " {default}\n" + " {plural $n_circles}\n" + " {case 1}You have {$n_friends} friends in one circle.\n" + " {default}You have {$n_friends} friends in {$n_circles} circles.\n" + " {/plural}\n" + " {/plural}\n" + "{/msg}\n"); // Select inside plural should not be allowed. assertIsNotTemplateBody( "{msg desc=\"A sample nested message\"}\n" + " {plural $n_friends}\n" + " {case 1}\n" + " {select $gender}\n" + " {case 'female'}{$person} has one person in her circle.\n" + " {default}{$person} has one person in his circle.\n" + " {/select}\n" + " {default}\n" + " {select $gender}\n" + " {case 'female'}{$person} has {$n_friends} persons in her circle.\n" + " {default}{$person} has {$n_friends} persons in his circle.\n" + " {/select}\n" + " {/plural}\n" + "{/msg}\n"); // Messages with more than one plural/gender clauses should not be allowed. assertIsNotTemplateBody( "{msg desc=\"A sample plural message\"}\n" + " {select $gender}\n" + " {case 'female'}{$person} added you to her circle.\n" + " {default}{$person} added you to his circle.\n" + " {/select}\n" + " {plural $num_people offset=\"1\"}\n" + " {case 0}I see no one in {$place}.\n" + " {case 1}I see {$person} in {$place}.\n" + " {case 2}I see {$person} and one other person in {$place}.\n" + " {default}I see {$person} and {remainder($num_people)} " + "other people in {$place}.\n" + " {/plural}\n" + " {/msg}\n"); } // ----------------------------------------------------------------------------------------------- // Tests for recognition and parse results. @Test public void testParseRawText() throws Exception { String templateBody = " {sp} aaa bbb \n" + " ccc {lb}{rb} ddd {\\n}\n" + " eee <br>\n" + " fff\n" + " {literal}ggg\n" + "hhh }{ {/literal} \n" + " \u2222\uEEEE\u9EC4\u607A\n"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(1, nodes.size()); RawTextNode rtn = (RawTextNode) nodes.get(0); assertEquals( " aaa bbb ccc {} ddd \neee <br>fffggg\nhhh }{ \u2222\uEEEE\u9EC4\u607A", rtn.getRawText()); assertEquals( " aaa bbb ccc {lb}{rb} ddd {\\n}eee <br>fffggg{\\n}hhh {rb}{lb} \u2222\uEEEE\u9EC4\u607A", rtn.toSourceString()); } @Test public void testParseComments() throws Exception { String templateBody = "" + " {sp} // {sp}\n" // first {sp} outside of comments + " /* {sp} {sp} */ // {sp}\n" + " /** {sp} {sp} */ // {sp}\n" + " /* {sp} */{sp}/* {sp} */\n" // middle {sp} outside of comments + " /** {sp} */{sp}/** {sp} */\n" // middle {sp} outside of comments + " /* {sp}\n" + " {sp} */{sp}\n" // last {sp} outside of comments + " /** {sp}\n" + " {sp} */{sp}\n" // last {sp} outside of comments + " {sp}/* {sp}\n" // first {sp} outside of comments + " {sp} */\n" + " {sp}/** {sp}\n" // first {sp} outside of comments + " {sp} */\n" + " // {sp} /* {sp} */\n" + " // {sp} /** {sp} */\n" // not a comment if "//" preceded by a non-space such as ":" + " http://www.google.com\n"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(1, nodes.size()); assertEquals(" http://www.google.com", ((RawTextNode) nodes.get(0)).getRawText()); } @Test public void testParseHeaderDecls() throws Exception { String templateHeaderAndBody = "" + " {@param boo: string} // Something scary. (Not doc comment.)\n" + " {@param foo: list<int>} /** Something random. */\n" + " {@param goo: string}/** Something\n" + " slimy. */\n" + " /* Something strong. (Not doc comment.) */" + " // {@param commentedOut: string}\n" + " {@param moo: string}{@param too: string}\n" + " {@param? woo: string} /** Something exciting. */ {@param hoo: string}\n" + " BODY\n"; TemplateNode result = parseTemplateContent(templateHeaderAndBody, FAIL); assertEquals(7, Iterables.size(result.getAllParams())); assertEquals("BODY", result.getChildren().get(0).toSourceString()); List<TemplateParam> declInfos = ImmutableList.copyOf(result.getAllParams()); assertFalse(declInfos.get(0).isInjected()); assertEquals("boo", declInfos.get(0).name()); assertEquals("string", declInfos.get(0).type().toString()); assertEquals(null, declInfos.get(0).desc()); assertEquals("foo", declInfos.get(1).name()); assertEquals("list<int>", declInfos.get(1).type().toString()); assertEquals(null, declInfos.get(1).desc()); assertEquals("Something random.", declInfos.get(2).desc()); assertEquals("Something\n slimy.", declInfos.get(3).desc()); assertEquals("too", declInfos.get(4).name()); assertEquals(null, declInfos.get(4).desc()); assertEquals("woo", declInfos.get(5).name()); assertEquals(null, declInfos.get(5).desc()); assertEquals("Something exciting.", declInfos.get(6).desc()); } @Test public void testParsePrintStmt() throws Exception { String templateBody = " {$boo.foo}{$boo.foo}\n" + " {$goo + 1 |noAutoescape}\n" + " {print 'blah blahblahblah' |escapeHtml|insertWordBreaks:8}\n"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(4, nodes.size()); PrintNode pn0 = (PrintNode) nodes.get(0); assertTrue(pn0.couldHaveSyntaxVersionAtLeast(SyntaxVersion.V2_0)); assertEquals("$boo.foo", pn0.getExprText()); assertEquals(0, pn0.getChildren().size()); assertEquals("FOO", pn0.genBasePhName()); assertEquals("{$boo.foo}", pn0.toSourceString()); assertTrue(pn0.getExpr().getRoot() instanceof FieldAccessNode); PrintNode pn1 = (PrintNode) nodes.get(1); assertTrue(pn0.genSamenessKey().equals(pn1.genSamenessKey())); assertTrue(pn1.getExpr().getRoot() instanceof FieldAccessNode); PrintNode pn2 = (PrintNode) nodes.get(2); assertTrue(pn2.couldHaveSyntaxVersionAtLeast(SyntaxVersion.V2_0)); assertEquals("$goo + 1", pn2.getExprText()); assertEquals(1, pn2.getChildren().size()); PrintDirectiveNode pn2d0 = pn2.getChild(0); assertTrue(pn2d0.couldHaveSyntaxVersionAtLeast(SyntaxVersion.V2_0)); assertEquals("|noAutoescape", pn2d0.getName()); assertEquals("XXX", pn2.genBasePhName()); assertTrue(pn2.getExpr().getRoot() instanceof PlusOpNode); PrintNode pn3 = (PrintNode) nodes.get(3); assertTrue(pn3.couldHaveSyntaxVersionAtLeast(SyntaxVersion.V2_0)); assertEquals("'blah blahblahblah'", pn3.getExprText()); assertEquals(2, pn3.getChildren().size()); PrintDirectiveNode pn3d0 = pn3.getChild(0); assertTrue(pn3d0.couldHaveSyntaxVersionAtLeast(SyntaxVersion.V2_0)); assertEquals("|escapeHtml", pn3d0.getName()); PrintDirectiveNode pn3d1 = pn3.getChild(1); assertTrue(pn3d1.couldHaveSyntaxVersionAtLeast(SyntaxVersion.V2_0)); assertEquals("|insertWordBreaks", pn3d1.getName()); assertEquals(8, ((IntegerNode) pn3d1.getArgs().get(0).getRoot()).getValue()); assertEquals("XXX", pn3.genBasePhName()); assertTrue(pn3.getExpr().getRoot() instanceof StringNode); assertFalse(pn0.genSamenessKey().equals(pn2.genSamenessKey())); assertFalse(pn3.genSamenessKey().equals(pn0.genSamenessKey())); } @Test public void testParsePrintStmtWithPhname() throws Exception { String templateBody = "" + " {$boo.foo}\n" + " {$boo.foo phname=\"booFoo\"}\n" + " {$boo.foo phname=\"booFoo\" }\n" + " {print $boo.foo phname=\"boo_foo\"}\n"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(4, nodes.size()); PrintNode pn0 = (PrintNode) nodes.get(0); assertEquals("$boo.foo", pn0.getExprText()); assertEquals("FOO", pn0.genBasePhName()); assertEquals("{$boo.foo}", pn0.toSourceString()); PrintNode pn1 = (PrintNode) nodes.get(1); assertEquals("$boo.foo", pn1.getExprText()); assertEquals("BOO_FOO", pn1.genBasePhName()); assertEquals("{$boo.foo phname=\"booFoo\"}", pn1.toSourceString()); assertTrue(pn1.couldHaveSyntaxVersionAtLeast(SyntaxVersion.V2_0)); assertEquals(0, pn1.getChildren().size()); assertTrue(pn1.getExpr().getRoot() instanceof FieldAccessNode); PrintNode pn2 = (PrintNode) nodes.get(2); assertEquals("$boo.foo", pn2.getExprText()); assertEquals("BOO_FOO", pn2.genBasePhName()); assertEquals("{$boo.foo phname=\"booFoo\"}", pn2.toSourceString()); PrintNode pn3 = (PrintNode) nodes.get(3); assertEquals("$boo.foo", pn3.getExprText()); assertEquals("BOO_FOO", pn3.genBasePhName()); assertEquals("{print $boo.foo phname=\"boo_foo\"}", pn3.toSourceString()); assertFalse(pn0.genSamenessKey().equals(pn1.genSamenessKey())); assertTrue(pn1.genSamenessKey().equals(pn2.genSamenessKey())); assertFalse(pn1.genSamenessKey().equals(pn3.genSamenessKey())); } @Test public void testParseCssStmt() throws Exception { String templateBody = "{css selected-option}\n" + "{css CSS_SELECTED_OPTION}\n" + "{css $cssSelectedOption}\n" + "{css %SelectedOption}"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(4, nodes.size()); assertEquals("selected-option", ((CssNode) nodes.get(0)).getCommandText()); assertEquals("CSS_SELECTED_OPTION", ((CssNode) nodes.get(1)).getCommandText()); assertEquals("$cssSelectedOption", ((CssNode) nodes.get(2)).getCommandText()); assertEquals("%SelectedOption", ((CssNode) nodes.get(3)).getCommandText()); } @Test public void testParseXidStmt() throws Exception { String templateBody = "{xid selected-option}\n" + "{xid selected.option}\n" + "{xid XID_SELECTED_OPTION}"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(3, nodes.size()); assertEquals("selected-option", ((XidNode) nodes.get(0)).getText()); assertEquals("selected.option", ((XidNode) nodes.get(1)).getText()); assertEquals("XID_SELECTED_OPTION", ((XidNode) nodes.get(2)).getText()); } @Test public void testParseMsgStmt() throws Exception { String templateBody = " {msg desc=\"Tells user's quota usage.\"}\n" + " You're currently using {$usedMb} MB of your quota.{sp}\n" + " <a href=\"{$learnMoreUrl}\">Learn more</A>\n" + " <br /><br />\n" + " {/msg}\n" + " {msg meaning=\"noun\" desc=\"\" hidden=\"true\"}Archive{/msg}\n" + " {msg meaning=\"noun\" desc=\"The archive (noun).\"}Archive{/msg}\n" + " {msg meaning=\"verb\" desc=\"\"}Archive{/msg}\n" + " {msg desc=\"\"}Archive{/msg}\n"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(5, nodes.size()); MsgNode mn0 = ((MsgFallbackGroupNode) nodes.get(0)).getMsg(); assertEquals("Tells user's quota usage.", mn0.getDesc()); assertEquals(null, mn0.getMeaning()); assertEquals(false, mn0.isHidden()); assertEquals(8, mn0.numChildren()); assertEquals("You're currently using ", ((RawTextNode) mn0.getChild(0)).getRawText()); MsgPlaceholderNode mpn1 = (MsgPlaceholderNode) mn0.getChild(1); assertEquals("$usedMb", ((PrintNode) mpn1.getChild(0)).getExprText()); assertEquals(" MB of your quota. ", ((RawTextNode) mn0.getChild(2)).getRawText()); MsgPlaceholderNode mpn3 = (MsgPlaceholderNode) mn0.getChild(3); MsgHtmlTagNode mhtn3 = (MsgHtmlTagNode) mpn3.getChild(0); assertEquals("a", mhtn3.getLcTagName()); assertEquals("START_LINK", mhtn3.genBasePhName()); assertEquals("<a href=\"{$learnMoreUrl}\">", mhtn3.toSourceString()); assertEquals(3, mhtn3.numChildren()); assertEquals("<a href=\"", ((RawTextNode) mhtn3.getChild(0)).getRawText()); assertEquals("$learnMoreUrl", ((PrintNode) mhtn3.getChild(1)).getExprText()); assertEquals("\">", ((RawTextNode) mhtn3.getChild(2)).getRawText()); assertEquals("Learn more", ((RawTextNode) mn0.getChild(4)).getRawText()); MsgPlaceholderNode mpn5 = (MsgPlaceholderNode) mn0.getChild(5); MsgHtmlTagNode mhtn5 = (MsgHtmlTagNode) mpn5.getChild(0); assertEquals("/a", mhtn5.getLcTagName()); assertEquals("END_LINK", mhtn5.genBasePhName()); assertEquals("</A>", mhtn5.toSourceString()); MsgPlaceholderNode mpn6 = (MsgPlaceholderNode) mn0.getChild(6); MsgHtmlTagNode mhtn6 = (MsgHtmlTagNode) mpn6.getChild(0); assertEquals("BREAK", mhtn6.genBasePhName()); assertTrue(mpn6.shouldUseSameVarNameAs((MsgPlaceholderNode) mn0.getChild(7))); assertFalse(mpn6.shouldUseSameVarNameAs(mpn5)); assertFalse(mpn5.shouldUseSameVarNameAs(mpn3)); MsgFallbackGroupNode mfgn1 = (MsgFallbackGroupNode) nodes.get(1); assertEquals( "{msg meaning=\"noun\" desc=\"\" hidden=\"true\"}Archive{/msg}", mfgn1.toSourceString()); MsgNode mn1 = mfgn1.getMsg(); assertEquals("", mn1.getDesc()); assertEquals("noun", mn1.getMeaning()); assertEquals(true, mn1.isHidden()); assertEquals(1, mn1.numChildren()); assertEquals("Archive", ((RawTextNode) mn1.getChild(0)).getRawText()); } @Test public void testParseMsgHtmlTagWithPhname() throws Exception { String templateBody = "" + " {msg desc=\"\"}\n" + " <a href=\"{$learnMoreUrl}\" phname=\"beginLearnMoreLink\">\n" + " Learn more\n" + " </A phname=\"end_LearnMore_LINK\">\n" + " <br phname=\"breakTag\" /><br phname=\"breakTag\" />" + "<br phname=\"break_tag\" />\n" + " {/msg}\n"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(1, nodes.size()); MsgNode mn0 = ((MsgFallbackGroupNode) nodes.get(0)).getChild(0); assertEquals(6, mn0.numChildren()); MsgPlaceholderNode mpn0 = (MsgPlaceholderNode) mn0.getChild(0); MsgHtmlTagNode mhtn0 = (MsgHtmlTagNode) mpn0.getChild(0); assertEquals("a", mhtn0.getLcTagName()); assertEquals("BEGIN_LEARN_MORE_LINK", mhtn0.genBasePhName()); assertEquals( "<a href=\"{$learnMoreUrl}\" phname=\"beginLearnMoreLink\">", mhtn0.toSourceString()); MsgPlaceholderNode mpn2 = (MsgPlaceholderNode) mn0.getChild(2); MsgHtmlTagNode mhtn2 = (MsgHtmlTagNode) mpn2.getChild(0); assertEquals("/a", mhtn2.getLcTagName()); assertEquals("END_LEARN_MORE_LINK", mhtn2.genBasePhName()); assertEquals("</A phname=\"end_LearnMore_LINK\">", mhtn2.toSourceString()); MsgPlaceholderNode mpn3 = (MsgPlaceholderNode) mn0.getChild(3); MsgHtmlTagNode mhtn3 = (MsgHtmlTagNode) mpn3.getChild(0); assertEquals("br", mhtn3.getLcTagName()); assertEquals("BREAK_TAG", mhtn3.genBasePhName()); assertEquals("<br phname=\"breakTag\"/>", mhtn3.toSourceString()); MsgPlaceholderNode mpn4 = (MsgPlaceholderNode) mn0.getChild(4); MsgHtmlTagNode mhtn4 = (MsgHtmlTagNode) mpn4.getChild(0); MsgPlaceholderNode mpn5 = (MsgPlaceholderNode) mn0.getChild(5); MsgHtmlTagNode mhtn5 = (MsgHtmlTagNode) mpn5.getChild(0); assertEquals("br", mhtn5.getLcTagName()); assertEquals("BREAK_TAG", mhtn5.genBasePhName()); assertEquals("<br phname=\"break_tag\"/>", mhtn5.toSourceString()); assertFalse(mhtn0.genSamenessKey().equals(mhtn2.genSamenessKey())); assertFalse(mhtn0.genSamenessKey().equals(mhtn3.genSamenessKey())); assertTrue(mhtn3.genSamenessKey().equals(mhtn4.genSamenessKey())); assertFalse(mhtn3.genSamenessKey().equals(mhtn5.genSamenessKey())); } @Test public void testParseMsgStmtWithCall() throws Exception { String templateBody = " {msg desc=\"Blah.\"}\n" + " Blah {call .helper_ data=\"all\" /} blah{sp}\n" + " {call .helper_}\n" + " {param foo}Foo{/param}\n" + " {/call}{sp}\n" + " blah.\n" + " {/msg}\n"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(1, nodes.size()); MsgNode mn = ((MsgFallbackGroupNode) nodes.get(0)).getChild(0); assertEquals(5, mn.numChildren()); assertEquals("Blah ", ((RawTextNode) mn.getChild(0)).getRawText()); assertEquals(0, ((CallNode) ((MsgPlaceholderNode) mn.getChild(1)).getChild(0)).numChildren()); assertEquals(" blah ", ((RawTextNode) mn.getChild(2)).getRawText()); assertEquals(1, ((CallNode) ((MsgPlaceholderNode) mn.getChild(3)).getChild(0)).numChildren()); assertEquals(" blah.", ((RawTextNode) mn.getChild(4)).getRawText()); } @Test public void testParseMsgStmtWithIf() throws Exception { TemplateSubject.assertThatTemplateContent( " {msg desc=\"Blah.\"}\n" + " Blah \n" + " {if $boo}\n" + " bleh\n" + " {else}\n" + " bluh\n" + " {/if}\n" + " .\n" + " {/msg}\n") .isNotWellFormed(); } @Test public void testParseMsgStmtWithFallback() throws Exception { String templateBody = "" + "{msg meaning=\"verb\" desc=\"Used as a verb.\"}\n" + " Archive\n" + "{fallbackmsg desc=\"\"}\n" + " Archive\n" + "{/msg}"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(1, nodes.size()); MsgFallbackGroupNode mfgn = (MsgFallbackGroupNode) nodes.get(0); assertEquals(2, mfgn.numChildren()); MsgNode mn0 = mfgn.getChild(0); assertEquals("msg", mn0.getCommandName()); assertEquals("verb", mn0.getMeaning()); assertEquals("Used as a verb.", mn0.getDesc()); assertEquals("Archive", ((RawTextNode) mn0.getChild(0)).getRawText()); MsgNode mn1 = mfgn.getChild(1); assertEquals("fallbackmsg", mn1.getCommandName()); assertEquals(null, mn1.getMeaning()); assertEquals("", mn1.getDesc()); assertEquals("Archive", ((RawTextNode) mn1.getChild(0)).getRawText()); } @Test public void testParseLetStmt() throws Exception { String templateBody = " {let $alpha: $boo.foo /}\n" + " {let $beta}Boo!{/let}\n" + " {let $gamma}\n" + " {for $i in range($alpha)}\n" + " {$i}{$beta}\n" + " {/for}\n" + " {/let}\n" + " {let $delta kind=\"html\"}Boo!{/let}\n"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(4, nodes.size()); LetValueNode alphaNode = (LetValueNode) nodes.get(0); assertEquals("alpha", alphaNode.getVarName()); assertEquals("$boo.foo", alphaNode.getValueExpr().toSourceString()); LetContentNode betaNode = (LetContentNode) nodes.get(1); assertEquals("beta", betaNode.getVarName()); assertEquals("Boo!", ((RawTextNode) betaNode.getChild(0)).getRawText()); assertNull(betaNode.getContentKind()); LetContentNode gammaNode = (LetContentNode) nodes.get(2); assertEquals("gamma", gammaNode.getVarName()); assertTrue(gammaNode.getChild(0) instanceof ForNode); assertNull(gammaNode.getContentKind()); LetContentNode deltaNode = (LetContentNode) nodes.get(3); assertEquals("delta", deltaNode.getVarName()); assertEquals("Boo!", ((RawTextNode) betaNode.getChild(0)).getRawText()); assertEquals(ContentKind.HTML, deltaNode.getContentKind()); // Test error case. TemplateSubject.assertThatTemplateContent("{let $alpha /}") .causesError(LetValueNode.SELF_ENDING_WITHOUT_VALUE) .at(1, 1); // Test error case. TemplateSubject.assertThatTemplateContent("{let $alpha: $boo.foo}{/let}") .causesError(LetContentNode.NON_SELF_ENDING_WITH_VALUE) .at(1, 1); } @Test public void testParseIfStmt() throws Exception { String templateBody = " {if $zoo}{$zoo}{/if}\n" + " {if $boo}\n" + " Blah\n" + " {elseif $foo.goo > 2}\n" + " {$moo}\n" + " {else}\n" + " Blah {$moo}\n" + " {/if}\n"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(2, nodes.size()); IfNode in0 = (IfNode) nodes.get(0); assertEquals(1, in0.numChildren()); IfCondNode in0icn0 = (IfCondNode) in0.getChild(0); assertEquals("$zoo", in0icn0.getCommandText()); assertEquals(1, in0icn0.numChildren()); assertEquals("$zoo", ((PrintNode) in0icn0.getChild(0)).getExprText()); assertTrue(in0icn0.getExpr().getRoot() instanceof VarRefNode); IfNode in1 = (IfNode) nodes.get(1); assertEquals(3, in1.numChildren()); IfCondNode in1icn0 = (IfCondNode) in1.getChild(0); assertEquals("$boo", in1icn0.getCommandText()); assertTrue(in1icn0.getExpr().getRoot() instanceof VarRefNode); IfCondNode in1icn1 = (IfCondNode) in1.getChild(1); assertEquals("$foo.goo > 2", in1icn1.getCommandText()); assertTrue(in1icn1.getExpr().getRoot() instanceof GreaterThanOpNode); assertEquals("", ((IfElseNode) in1.getChild(2)).getCommandText()); assertEquals( "{if $boo}Blah{elseif $foo.goo > 2}{$moo}{else}Blah {$moo}{/if}", in1.toSourceString()); } @Test public void testParseSwitchStmt() throws Exception { String templateBody = " {switch $boo} {case 0}Blah\n" + " {case $foo.goo}\n" + " Bleh\n" + " {case -1, 1, $moo}\n" + " Bluh\n" + " {default}\n" + " Bloh\n" + " {/switch}\n"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(1, nodes.size()); SwitchNode sn = (SwitchNode) nodes.get(0); assertEquals("$boo", sn.getExpr().toSourceString()); assertTrue(sn.getExpr().getRoot() instanceof VarRefNode); assertEquals(4, sn.numChildren()); SwitchCaseNode scn0 = (SwitchCaseNode) sn.getChild(0); assertEquals(1, scn0.getExprList().size()); assertTrue(scn0.getExprList().get(0).getRoot() instanceof IntegerNode); assertEquals(0, ((IntegerNode) scn0.getExprList().get(0).getRoot()).getValue()); SwitchCaseNode scn1 = (SwitchCaseNode) sn.getChild(1); assertEquals(1, scn1.getExprList().size()); assertTrue(scn1.getExprList().get(0).getRoot() instanceof FieldAccessNode); assertEquals("$foo.goo", scn1.getExprList().get(0).getRoot().toSourceString()); SwitchCaseNode scn2 = (SwitchCaseNode) sn.getChild(2); assertEquals(3, scn2.getExprList().size()); assertTrue(scn2.getExprList().get(0).getRoot() instanceof NegativeOpNode); assertTrue(scn2.getExprList().get(1).getRoot() instanceof IntegerNode); assertTrue(scn2.getExprList().get(2).getRoot() instanceof VarRefNode); assertEquals("-1", scn2.getExprList().get(0).getRoot().toSourceString()); assertEquals("1", scn2.getExprList().get(1).getRoot().toSourceString()); assertEquals("$moo", scn2.getExprList().get(2).getRoot().toSourceString()); assertEquals("Bluh", ((RawTextNode) scn2.getChild(0)).getRawText()); assertEquals( "Bloh", ((RawTextNode) ((SwitchDefaultNode) sn.getChild(3)).getChild(0)).getRawText()); assertEquals( "{switch $boo}{case 0}Blah{case $foo.goo}Bleh{case -1, 1, $moo}Bluh{default}Bloh{/switch}", sn.toSourceString()); } @Test public void testParseForeachStmt() throws Exception { String templateBody = " {foreach $goo in $goose}\n" + " {$goose.numKids} goslings.{\\n}\n" + " {/foreach}\n" + " {foreach $boo in $foo.booze}\n" + " Scary drink {$boo.name}!\n" + " {if not isLast($boo)}{\\n}{/if}\n" + " {ifempty}\n" + " Sorry, no booze.\n" + " {/foreach}\n"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(2, nodes.size()); ForeachNode fn0 = (ForeachNode) nodes.get(0); assertEquals("$goose", fn0.getExprText()); assertTrue(fn0.getExpr().getRoot() instanceof VarRefNode); assertEquals(1, fn0.numChildren()); ForeachNonemptyNode fn0fnn0 = (ForeachNonemptyNode) fn0.getChild(0); assertEquals("goo", fn0fnn0.getVarName()); assertEquals(2, fn0fnn0.numChildren()); assertEquals("$goose.numKids", ((PrintNode) fn0fnn0.getChild(0)).getExprText()); assertEquals(" goslings.\n", ((RawTextNode) fn0fnn0.getChild(1)).getRawText()); ForeachNode fn1 = (ForeachNode) nodes.get(1); assertEquals("$foo.booze", fn1.getExprText()); assertTrue(fn1.getExpr().getRoot() instanceof FieldAccessNode); assertEquals(2, fn1.numChildren()); ForeachNonemptyNode fn1fnn0 = (ForeachNonemptyNode) fn1.getChild(0); assertEquals("boo", fn1fnn0.getVarName()); assertEquals("$foo.booze", fn1fnn0.getExprText()); assertEquals("boo", fn1fnn0.getVarName()); assertEquals(4, fn1fnn0.numChildren()); IfNode fn1fnn0in = (IfNode) fn1fnn0.getChild(3); assertEquals(1, fn1fnn0in.numChildren()); assertEquals("not isLast($boo)", ((IfCondNode) fn1fnn0in.getChild(0)).getCommandText()); ForeachIfemptyNode fn1fin1 = (ForeachIfemptyNode) fn1.getChild(1); assertEquals(1, fn1fin1.numChildren()); assertEquals("Sorry, no booze.", ((RawTextNode) fn1fin1.getChild(0)).getRawText()); } @Test public void testParseForStmt() throws Exception { String templateBody = " {for $i in range(10, $itemsLength + 1)}\n" + " {msg desc=\"Numbered item.\"}\n" + " {$i}: {$items[$i - 1]}{\\n}\n" + " {/msg}\n" + " {/for}\n"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(1, nodes.size()); ForNode fn = (ForNode) nodes.get(0); assertEquals("i", fn.getVarName()); ForNode.RangeArgs rangeArgs = fn.getRangeArgs(); assertEquals("1", rangeArgs.increment().toSourceString()); assertEquals("10", rangeArgs.start().toSourceString()); assertEquals("$itemsLength + 1", rangeArgs.limit().toSourceString()); assertThat(rangeArgs.start().getRoot()).isInstanceOf(IntegerNode.class); assertThat(rangeArgs.limit().getRoot()).isInstanceOf(PlusOpNode.class); assertEquals(1, fn.numChildren()); MsgNode mn = ((MsgFallbackGroupNode) ((ForNode) nodes.get(0)).getChild(0)).getChild(0); assertEquals(4, mn.numChildren()); assertEquals( "$i", ((PrintNode) ((MsgPlaceholderNode) mn.getChild(0)).getChild(0)).getExprText()); assertEquals( "$items[$i - 1]", ((PrintNode) ((MsgPlaceholderNode) mn.getChild(2)).getChild(0)).getExprText()); } @SuppressWarnings({"ConstantConditions"}) @Test public void testParseBasicCallStmt() throws Exception { String templateBody = " {call .booTemplate_ /}\n" + " {call foo.goo.mooTemplate data=\"all\" /}\n" + " {call .booTemplate_ /}\n" + " {call .zooTemplate data=\"$animals\"}\n" + " {param yoo: round($too) /}\n" + " {param woo}poo{/param}\n" + " {param zoo: 0 /}\n" + " {param doo kind=\"html\"}doopoo{/param}\n" + " {/call}\n"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertThat(nodes).hasSize(4); CallBasicNode cn0 = (CallBasicNode) nodes.get(0); assertTrue(cn0.couldHaveSyntaxVersionAtLeast(SyntaxVersion.V2_0)); assertEquals(null, cn0.getCalleeName()); assertEquals(".booTemplate_", cn0.getSrcCalleeName()); assertEquals(false, cn0.dataAttribute().isPassingData()); assertEquals(false, cn0.dataAttribute().isPassingAllData()); assertEquals(null, cn0.dataAttribute().dataExpr()); assertEquals("XXX", cn0.genBasePhName()); assertEquals(0, cn0.numChildren()); CallBasicNode cn1 = (CallBasicNode) nodes.get(1); assertTrue(cn1.couldHaveSyntaxVersionAtLeast(SyntaxVersion.V2_0)); assertEquals(null, cn1.getCalleeName()); assertEquals("foo.goo.mooTemplate", cn1.getSrcCalleeName()); assertEquals(true, cn1.dataAttribute().isPassingData()); assertEquals(true, cn1.dataAttribute().isPassingAllData()); assertEquals(null, cn1.dataAttribute().dataExpr()); assertFalse(cn1.genSamenessKey().equals(cn0.genSamenessKey())); assertEquals(0, cn1.numChildren()); CallBasicNode cn2 = (CallBasicNode) nodes.get(2); assertTrue(cn2.couldHaveSyntaxVersionAtLeast(SyntaxVersion.V2_0)); assertEquals(null, cn2.getCalleeName()); assertEquals(".booTemplate_", cn2.getSrcCalleeName()); assertFalse(cn2.dataAttribute().isPassingData()); assertEquals(false, cn2.dataAttribute().isPassingAllData()); assertEquals(null, cn2.dataAttribute().dataExpr()); assertEquals("XXX", cn2.genBasePhName()); assertEquals(0, cn2.numChildren()); CallBasicNode cn3 = (CallBasicNode) nodes.get(3); assertTrue(cn3.couldHaveSyntaxVersionAtLeast(SyntaxVersion.V2_0)); assertEquals(null, cn3.getCalleeName()); assertEquals(".zooTemplate", cn3.getSrcCalleeName()); assertEquals(true, cn3.dataAttribute().isPassingData()); assertEquals(false, cn3.dataAttribute().isPassingAllData()); assertTrue(cn3.dataAttribute().dataExpr().getRoot() != null); assertEquals("$animals", cn3.dataAttribute().dataExpr().toSourceString()); assertEquals(4, cn3.numChildren()); { final CallParamValueNode cn4cpvn0 = (CallParamValueNode) cn3.getChild(0); assertEquals("yoo", cn4cpvn0.getKey()); assertEquals("round($too)", cn4cpvn0.getExprText()); assertTrue(cn4cpvn0.getExpr().getRoot() instanceof FunctionNode); } { final CallParamContentNode cn4cpcn1 = (CallParamContentNode) cn3.getChild(1); assertEquals("woo", cn4cpcn1.getKey()); assertNull(cn4cpcn1.getContentKind()); assertEquals("poo", ((RawTextNode) cn4cpcn1.getChild(0)).getRawText()); } { final CallParamValueNode cn4cpvn2 = (CallParamValueNode) cn3.getChild(2); assertEquals("zoo", cn4cpvn2.getKey()); assertEquals("0", cn4cpvn2.getExprText()); } { final CallParamContentNode cn4cpcn3 = (CallParamContentNode) cn3.getChild(3); assertEquals("doo", cn4cpcn3.getKey()); assertNotNull(cn4cpcn3.getContentKind()); assertEquals(ContentKind.HTML, cn4cpcn3.getContentKind()); assertEquals("doopoo", ((RawTextNode) cn4cpcn3.getChild(0)).getRawText()); } } @SuppressWarnings({"ConstantConditions"}) @Test public void testParseDelegateCallStmt() throws Exception { String templateBody = " {delcall booTemplate /}\n" + " {delcall foo.goo.mooTemplate data=\"all\" /}\n" + " {delcall MySecretFeature.zooTemplate data=\"$animals\"}\n" + " {param yoo: round($too) /}\n" + " {param woo}poo{/param}\n" + " {/delcall}\n"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(3, nodes.size()); CallDelegateNode cn0 = (CallDelegateNode) nodes.get(0); assertTrue(cn0.couldHaveSyntaxVersionAtLeast(SyntaxVersion.V2_0)); assertEquals("booTemplate", cn0.getDelCalleeName()); assertEquals(false, cn0.dataAttribute().isPassingData()); assertEquals(false, cn0.dataAttribute().isPassingAllData()); assertEquals(null, cn0.dataAttribute().dataExpr()); assertEquals("XXX", cn0.genBasePhName()); assertEquals(0, cn0.numChildren()); CallDelegateNode cn1 = (CallDelegateNode) nodes.get(1); assertTrue(cn1.couldHaveSyntaxVersionAtLeast(SyntaxVersion.V2_0)); assertEquals("foo.goo.mooTemplate", cn1.getDelCalleeName()); assertEquals(true, cn1.dataAttribute().isPassingData()); assertEquals(true, cn1.dataAttribute().isPassingAllData()); assertEquals(null, cn1.dataAttribute().dataExpr()); assertFalse(cn1.genSamenessKey().equals(cn0.genSamenessKey())); assertEquals(0, cn1.numChildren()); CallDelegateNode cn2 = (CallDelegateNode) nodes.get(2); assertTrue(cn2.couldHaveSyntaxVersionAtLeast(SyntaxVersion.V2_0)); assertEquals("MySecretFeature.zooTemplate", cn2.getDelCalleeName()); assertEquals(true, cn2.dataAttribute().isPassingData()); assertEquals(false, cn2.dataAttribute().isPassingAllData()); assertTrue(cn2.dataAttribute().dataExpr().getRoot() != null); assertEquals("$animals", cn2.dataAttribute().dataExpr().toSourceString()); assertEquals(2, cn2.numChildren()); CallParamValueNode cn2cpvn0 = (CallParamValueNode) cn2.getChild(0); assertEquals("yoo", cn2cpvn0.getKey()); assertEquals("round($too)", cn2cpvn0.getExprText()); assertTrue(cn2cpvn0.getExpr().getRoot() instanceof FunctionNode); CallParamContentNode cn2cpcn1 = (CallParamContentNode) cn2.getChild(1); assertEquals("woo", cn2cpcn1.getKey()); assertEquals("poo", ((RawTextNode) cn2cpcn1.getChild(0)).getRawText()); } @SuppressWarnings({"ConstantConditions"}) @Test public void testParseCallStmtWithPhname() throws Exception { String templateBody = "" + " {call .booTemplate_ phname=\"booTemplate_\" /}\n" + " {call .booTemplate_ phname=\"booTemplate_\" /}\n" + " {delcall MySecretFeature.zooTemplate data=\"$animals\" phname=\"secret_zoo\"}\n" + " {param zoo: 0 /}\n" + " {/delcall}\n"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(3, nodes.size()); CallBasicNode cn0 = (CallBasicNode) nodes.get(0); assertEquals("BOO_TEMPLATE", cn0.genBasePhName()); assertTrue(cn0.couldHaveSyntaxVersionAtLeast(SyntaxVersion.V2_0)); assertEquals(null, cn0.getCalleeName()); assertEquals(".booTemplate_", cn0.getSrcCalleeName()); assertEquals(false, cn0.dataAttribute().isPassingData()); assertEquals(false, cn0.dataAttribute().isPassingAllData()); assertEquals(null, cn0.dataAttribute().dataExpr()); assertEquals(0, cn0.numChildren()); CallBasicNode cn1 = (CallBasicNode) nodes.get(1); CallDelegateNode cn2 = (CallDelegateNode) nodes.get(2); assertEquals("SECRET_ZOO", cn2.genBasePhName()); assertTrue(cn2.couldHaveSyntaxVersionAtLeast(SyntaxVersion.V2_0)); assertEquals("MySecretFeature.zooTemplate", cn2.getDelCalleeName()); assertEquals(true, cn2.dataAttribute().isPassingData()); assertEquals(false, cn2.dataAttribute().isPassingAllData()); assertTrue(cn2.dataAttribute().dataExpr().getRoot() != null); assertEquals("$animals", cn2.dataAttribute().dataExpr().toSourceString()); assertEquals(1, cn2.numChildren()); assertFalse(cn0.genSamenessKey().equals(cn1.genSamenessKey())); // CallNodes are never same assertFalse(cn2.genSamenessKey().equals(cn0.genSamenessKey())); } @Test public void testParseLogStmt() throws Exception { String templateBody = "{log}Blah {$foo}.{/log}"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(1, nodes.size()); LogNode logNode = (LogNode) nodes.get(0); assertEquals(3, logNode.numChildren()); assertEquals("Blah ", ((RawTextNode) logNode.getChild(0)).getRawText()); assertEquals("$foo", ((PrintNode) logNode.getChild(1)).getExprText()); } @Test public void testParseDebuggerStmt() throws Exception { String templateBody = "{debugger}"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(1, nodes.size()); assertTrue(nodes.get(0) instanceof DebuggerNode); } // ----------------------------------------------------------------------------------------------- // Tests for plural/select messages. @Test public void testParseMsgStmtWithPlural() throws Exception { String templateBody = " {msg desc=\"A sample plural message\"}\n" + " {plural $num_people offset=\"1\"}\n" + " {case 0}I see no one in {$place}.\n" + " {case 1}I see {$person} in {$place}.\n" + " {case 2}I see {$person} and one other person in {$place}.\n" + " {default}I see {$person} and {remainder($num_people)} " + "other people in {$place}.\n" + " {/plural}" + " {/msg}\n"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(1, nodes.size()); MsgNode mn = ((MsgFallbackGroupNode) nodes.get(0)).getChild(0); assertEquals(1, mn.numChildren()); assertEquals("A sample plural message", mn.getDesc()); MsgPluralNode pn = (MsgPluralNode) mn.getChild(0); assertEquals("$num_people offset=\"1\"", pn.getCommandText()); assertEquals(1, pn.getOffset()); assertEquals(4, pn.numChildren()); // 3 cases and default // Case 0 MsgPluralCaseNode cn0 = (MsgPluralCaseNode) pn.getChild(0); assertEquals(3, cn0.numChildren()); assertEquals(0, cn0.getCaseNumber()); RawTextNode rtn01 = (RawTextNode) cn0.getChild(0); assertEquals("I see no one in ", rtn01.getRawText()); MsgPlaceholderNode phn01 = (MsgPlaceholderNode) cn0.getChild(1); assertEquals("{$place}", phn01.toSourceString()); RawTextNode rtn02 = (RawTextNode) cn0.getChild(2); assertEquals(".", rtn02.getRawText()); // Case 1 MsgPluralCaseNode cn1 = (MsgPluralCaseNode) pn.getChild(1); assertEquals(5, cn1.numChildren()); assertEquals(1, cn1.getCaseNumber()); RawTextNode rtn11 = (RawTextNode) cn1.getChild(0); assertEquals("I see ", rtn11.getRawText()); MsgPlaceholderNode phn11 = (MsgPlaceholderNode) cn1.getChild(1); assertEquals("{$person}", phn11.toSourceString()); RawTextNode rtn12 = (RawTextNode) cn1.getChild(2); assertEquals(" in ", rtn12.getRawText()); MsgPlaceholderNode phn12 = (MsgPlaceholderNode) cn1.getChild(3); assertEquals("{$place}", phn12.toSourceString()); RawTextNode rtn13 = (RawTextNode) cn1.getChild(4); assertEquals(".", rtn13.getRawText()); // Case 2 MsgPluralCaseNode cn2 = (MsgPluralCaseNode) pn.getChild(2); assertEquals(5, cn2.numChildren()); assertEquals(2, cn2.getCaseNumber()); RawTextNode rtn21 = (RawTextNode) cn2.getChild(0); assertEquals("I see ", rtn21.getRawText()); MsgPlaceholderNode phn21 = (MsgPlaceholderNode) cn2.getChild(1); assertEquals("{$person}", phn21.toSourceString()); RawTextNode rtn22 = (RawTextNode) cn2.getChild(2); assertEquals(" and one other person in ", rtn22.getRawText()); MsgPlaceholderNode phn22 = (MsgPlaceholderNode) cn2.getChild(3); assertEquals("{$place}", phn22.toSourceString()); RawTextNode rtn23 = (RawTextNode) cn2.getChild(4); assertEquals(".", rtn23.getRawText()); // Default MsgPluralDefaultNode dn = (MsgPluralDefaultNode) pn.getChild(3); assertEquals(7, dn.numChildren()); RawTextNode rtnd1 = (RawTextNode) dn.getChild(0); assertEquals("I see ", rtnd1.getRawText()); MsgPlaceholderNode phnd1 = (MsgPlaceholderNode) dn.getChild(1); assertEquals("{$person}", phnd1.toSourceString()); RawTextNode rtnd2 = (RawTextNode) dn.getChild(2); assertEquals(" and ", rtnd2.getRawText()); MsgPlaceholderNode phnd2 = (MsgPlaceholderNode) dn.getChild(3); assertEquals("{remainder($num_people)}", phnd2.toSourceString()); RawTextNode rtnd3 = (RawTextNode) dn.getChild(4); assertEquals(" other people in ", rtnd3.getRawText()); MsgPlaceholderNode phnd3 = (MsgPlaceholderNode) dn.getChild(5); assertEquals("{$place}", phnd3.toSourceString()); RawTextNode rtnd4 = (RawTextNode) dn.getChild(6); assertEquals(".", rtnd4.getRawText()); } @Test public void testParseMsgStmtWithSelect() throws Exception { String templateBody = "{msg desc=\"A sample gender message\"}\n" + " {select $gender}\n" + " {case 'female'}{$person} added you to her circle.\n" + " {default}{$person} added you to his circle.\n" + " {/select}\n" + "{/msg}\n"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(1, nodes.size()); MsgNode mn = ((MsgFallbackGroupNode) nodes.get(0)).getChild(0); assertEquals(1, mn.numChildren()); assertEquals("A sample gender message", mn.getDesc()); MsgSelectNode sn = (MsgSelectNode) mn.getChild(0); assertEquals("$gender", sn.getCommandText()); assertEquals(2, sn.numChildren()); // female and default // Case 'female' MsgSelectCaseNode cnf = (MsgSelectCaseNode) sn.getChild(0); assertEquals("'female'", cnf.getCommandText()); assertEquals(2, cnf.numChildren()); MsgPlaceholderNode phnf = (MsgPlaceholderNode) cnf.getChild(0); assertEquals("{$person}", phnf.toSourceString()); RawTextNode rtnf = (RawTextNode) cnf.getChild(1); assertEquals(" added you to her circle.", rtnf.getRawText()); // Default MsgSelectDefaultNode dn = (MsgSelectDefaultNode) sn.getChild(1); assertEquals(2, dn.numChildren()); MsgPlaceholderNode phnd = (MsgPlaceholderNode) dn.getChild(0); assertEquals("{$person}", phnd.toSourceString()); RawTextNode rtnd = (RawTextNode) dn.getChild(1); assertEquals(" added you to his circle.", rtnd.getRawText()); } @Test public void testParseMsgStmtWithNestedSelects() throws Exception { String templateBody = "{msg desc=\"A sample nested message\"}\n" + " {select $gender1}\n" + " {case 'female'}\n" + " {select $gender2}\n" + " {case 'female'}{$person1} added {$person2} and her friends to her circle.\n" + " {default}{$person1} added {$person2} and his friends to her circle.\n" + " {/select}\n" + " {default}\n" + " {select $gender2}\n" + " {case 'female'}{$person1} put {$person2} and her friends to his circle.\n" + " {default}{$person1} put {$person2} and his friends to his circle.\n" + " {/select}\n" + " {/select}\n" + "{/msg}\n"; List<StandaloneNode> nodes = parseTemplateBody(templateBody, FAIL).getChildren(); assertEquals(1, nodes.size()); MsgNode mn = ((MsgFallbackGroupNode) nodes.get(0)).getChild(0); assertEquals(1, mn.numChildren()); assertEquals("A sample nested message", mn.getDesc()); // Outer select MsgSelectNode sn = (MsgSelectNode) mn.getChild(0); assertEquals("$gender1", sn.getCommandText()); assertEquals(2, sn.numChildren()); // female and default // Outer select: Case 'female' MsgSelectCaseNode cnf = (MsgSelectCaseNode) sn.getChild(0); assertEquals("'female'", cnf.getCommandText()); assertEquals(1, cnf.numChildren()); // Another select // Outer select: Case 'female': Inner select MsgSelectNode sn2 = (MsgSelectNode) cnf.getChild(0); assertEquals("$gender2", sn2.getCommandText()); assertEquals(2, sn2.numChildren()); // female and default // Outer select: Case 'female': Inner select: Case 'female' MsgSelectCaseNode cnf2 = (MsgSelectCaseNode) sn2.getChild(0); assertEquals("'female'", cnf2.getCommandText()); assertEquals(4, cnf2.numChildren()); // Outer select: Case 'female': Inner select: Case 'female': Placeholder $person1 MsgPlaceholderNode phn1 = (MsgPlaceholderNode) cnf2.getChild(0); assertEquals("{$person1}", phn1.toSourceString()); // Outer select: Case 'female': Inner select: Case 'female': RawText RawTextNode rtn1 = (RawTextNode) cnf2.getChild(1); assertEquals(" added ", rtn1.getRawText()); // Outer select: Case 'female': Inner select: Case 'female': Placeholder $person2 MsgPlaceholderNode phn2 = (MsgPlaceholderNode) cnf2.getChild(2); assertEquals("{$person2}", phn2.toSourceString()); // Outer select: Case 'female': Inner select: Case 'female': RawText RawTextNode rtn2 = (RawTextNode) cnf2.getChild(3); assertEquals(" and her friends to her circle.", rtn2.getRawText()); // Outer select: Case 'female': Inner select: Default MsgSelectDefaultNode dn2 = (MsgSelectDefaultNode) sn2.getChild(1); assertEquals(4, dn2.numChildren()); // Outer select: Case 'female': Inner select: Default: Placeholder $person1 MsgPlaceholderNode phn21 = (MsgPlaceholderNode) dn2.getChild(0); assertEquals("{$person1}", phn21.toSourceString()); // Outer select: Case 'female': Inner select: Default: RawText RawTextNode rtn21 = (RawTextNode) dn2.getChild(1); assertEquals(" added ", rtn21.getRawText()); // Outer select: Case 'female': Inner select: Default: Placeholder $person2 MsgPlaceholderNode phn22 = (MsgPlaceholderNode) dn2.getChild(2); assertEquals("{$person2}", phn22.toSourceString()); // Outer select: Case 'female': Inner select: Default: RawText RawTextNode rtn22 = (RawTextNode) dn2.getChild(3); assertEquals(" and his friends to her circle.", rtn22.getRawText()); // Outer select: Default MsgSelectDefaultNode dn = (MsgSelectDefaultNode) sn.getChild(1); assertEquals(1, dn.numChildren()); // Another select // Outer select: Default: Inner select MsgSelectNode sn3 = (MsgSelectNode) dn.getChild(0); assertEquals("$gender2", sn3.getCommandText()); assertEquals(2, sn3.numChildren()); // female and default // Outer select: Default: Inner select: Case 'female' MsgSelectCaseNode cnf3 = (MsgSelectCaseNode) sn3.getChild(0); assertEquals("'female'", cnf3.getCommandText()); assertEquals(4, cnf3.numChildren()); // Outer select: Default: Inner select: Case 'female': Placeholder $person1 MsgPlaceholderNode phn3 = (MsgPlaceholderNode) cnf3.getChild(0); assertEquals("{$person1}", phn3.toSourceString()); // Outer select: Default: Inner select: Case 'female': RawText RawTextNode rtn3 = (RawTextNode) cnf3.getChild(1); assertEquals(" put ", rtn3.getRawText()); // Outer select: Default: Inner select: Case 'female': Placeholder $person2 MsgPlaceholderNode phn4 = (MsgPlaceholderNode) cnf3.getChild(2); assertEquals("{$person2}", phn4.toSourceString()); // Outer select: Default: Inner select: Case 'female': RawText RawTextNode rtn4 = (RawTextNode) cnf3.getChild(3); assertEquals(" and her friends to his circle.", rtn4.getRawText()); // Outer select: Default: Inner select: Default MsgSelectDefaultNode dn3 = (MsgSelectDefaultNode) sn3.getChild(1); assertEquals(4, dn3.numChildren()); // Outer select: Default: Inner select: Default: Placeholder $person1 MsgPlaceholderNode phn5 = (MsgPlaceholderNode) dn3.getChild(0); assertEquals("{$person1}", phn5.toSourceString()); // Outer select: Default: Inner select: Default: RawText RawTextNode rtn5 = (RawTextNode) dn3.getChild(1); assertEquals(" put ", rtn5.getRawText()); // Outer select: Default: Inner select: Default: Placeholder $person2 MsgPlaceholderNode phn6 = (MsgPlaceholderNode) dn3.getChild(2); assertEquals("{$person2}", phn6.toSourceString()); // Outer select: Default: Inner select: Default: RawText RawTextNode rtn6 = (RawTextNode) dn3.getChild(3); assertEquals(" and his friends to his circle.", rtn6.getRawText()); } @Test public void testMultipleErrors() throws ParseException { FormattingErrorReporter errorReporter = new FormattingErrorReporter(); parseTemplateBody( "{call 123 /}\n" // Invalid callee name "123" for 'call' command. + "{delcall 123 /}\n" // Invalid delegate name "123" for 'delcall' command. + "{foreach foo in bar}{/foreach}\n" // Invalid 'foreach' command text "foo in bar". + "{let /}\n", errorReporter); // Invalid 'let' command text "". List<String> errors = errorReporter.getErrorMessages(); assertThat(errors).hasSize(5); assertThat(errors.get(0)).contains("Invalid callee name \"123\" for 'call' command."); assertThat(errors.get(1)).contains("Invalid delegate name \"123\" for 'delcall' command."); assertThat(errors.get(2)).contains("Invalid 'foreach' command text \"foo in bar\"."); assertThat(errors.get(3)).contains("Invalid 'let' command text."); assertThat(errors.get(4)) .contains( "A 'let' tag should be self-ending (with a trailing '/') if and only if it also " + "contains a value (invalid tag is {let /})."); } // ----------------------------------------------------------------------------------------------- // Helpers. /** * Parses the given input as a template body. * * @param input The input string to parse. * @throws TokenMgrError When the given input has a token error. * @throws ParseException When the given input has a parse error. * @return The parse tree nodes created. */ private static TemplateNode parseTemplateBody(String input, ErrorReporter errorReporter) throws ParseException { TemplateNode result = parseTemplateContent(input, errorReporter); if (result != null) { for (TemplateParam param : result.getAllParams()) { if (param instanceof HeaderParam) { fail("expected no params"); } } } return result; } /** * Parses the given input as a template content (header and body). * * @param input The input string to parse. * @throws TokenMgrError When the given input has a token error. * @return The decl infos and parse tree nodes created. */ private static TemplateNode parseTemplateContent(String input, ErrorReporter errorReporter) { String soyFile = SharedTestUtils.buildTestSoyFileContent( AutoEscapingType.STRICT, ImmutableList.<String>of(), input); IncrementingIdGenerator nodeIdGen = new IncrementingIdGenerator(); SoyFileNode file = new SoyFileParser( new SoyTypeRegistry(), nodeIdGen, new StringReader(soyFile), SoyFileKind.SRC, "test.soy", errorReporter) .parseSoyFile(); if (file != null) { new CombineConsecutiveRawTextNodesVisitor(nodeIdGen).exec(file); return file.getChild(0); } return null; } /** * Asserts that the given input is a valid template, running all parsing phases. * * @param input The input string to parse. */ private static void assertValidTemplate(String input) { ImmutableMap<String, SoyFileSupplier> files = ImmutableMap.of( "example.soy", SoyFileSupplier.Factory.create( "{namespace test}{template .test}\n" + input + "\n{/template}", SoyFileKind.SRC, "example.soy")); ErrorReporterImpl reporter = new ErrorReporterImpl(new PrettyErrorFactory(new SnippetFormatter(files))); SoyFileSetParser fileSetParser = new SoyFileSetParser( new SoyTypeRegistry(), null /* ast cache */, files, new PassManager.Builder() .setErrorReporter(reporter) .setTypeRegistry(new SoyTypeRegistry()) .setSoyFunctionMap(ImmutableMap.<String, SoyFunction>of()) .setDeclaredSyntaxVersion(SyntaxVersion.V1_0) .setGeneralOptions(new SoyGeneralOptions()) .build(), reporter); fileSetParser.parse(); assertThat(reporter.getErrors()).isEmpty(); } /** * Asserts that the given input is a valid template. * * @param input The input string to parse. * @throws TokenMgrError When the given input has a token error. * @throws ParseException When the given input has a parse error. */ private static void assertIsTemplateBody(String input) throws TokenMgrError, ParseException { TemplateSubject.assertThatTemplateContent(input).isWellFormed(); } /** * Asserts that the given input is a valid template content (header and body). * * @param input The input string to parse. * @throws TokenMgrError When the given input has a token error. * @throws ParseException When the given input has a parse error. */ private static void assertIsTemplateContent(String input) throws TokenMgrError, ParseException { TemplateSubject.assertThatTemplateContent(input).isWellFormed(); } /** * Asserts that the given input is not a valid template. * * @param input The input string to parse. * @throws AssertionFailedError When the given input is actually a valid template. */ private static void assertIsNotTemplateBody(String input) throws AssertionFailedError { TemplateSubject.assertThatTemplateContent(input).isNotWellFormed(); } /** * Asserts that the given input is not a valid template content (header and body). * * @param input The input string to parse. * @throws AssertionFailedError When the given input is actually a valid template. */ private static void assertIsNotTemplateContent(String input) throws AssertionFailedError { TemplateSubject.assertThatTemplateContent(input).isNotWellFormed(); } }