/*
* Copyright 2015 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.jssrc.internal;
import static com.google.common.truth.Truth.assertThat;
import static com.google.template.soy.jssrc.dsl.CodeChunk.id;
import static com.google.template.soy.jssrc.dsl.CodeChunk.number;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.template.soy.SoyFileSetParser.ParseResult;
import com.google.template.soy.SoyFileSetParserBuilder;
import com.google.template.soy.SoyModule;
import com.google.template.soy.base.internal.UniqueNameGenerator;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.error.ErrorReporter.Checkpoint;
import com.google.template.soy.error.ExplodingErrorReporter;
import com.google.template.soy.error.FormattingErrorReporter;
import com.google.template.soy.jssrc.SoyJsSrcOptions;
import com.google.template.soy.jssrc.dsl.CodeChunk;
import com.google.template.soy.jssrc.internal.GenJsExprsVisitor.GenJsExprsVisitorFactory;
import com.google.template.soy.jssrc.restricted.JsExpr;
import com.google.template.soy.jssrc.restricted.SoyLibraryAssistedJsSrcFunction;
import com.google.template.soy.shared.AutoEscapingType;
import com.google.template.soy.shared.SharedTestUtils;
import com.google.template.soy.shared.internal.GuiceSimpleScope;
import com.google.template.soy.shared.restricted.SoyFunction;
import com.google.template.soy.soytree.SoyNode;
import com.google.template.soy.soytree.TemplateNode;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Unit tests for {@link GenJsCodeVisitor}.
*
*/
@RunWith(JUnit4.class)
public final class GenJsCodeVisitorTest {
private static final Joiner JOINER = Joiner.on('\n');
private static final Injector INJECTOR = Guice.createInjector(new SoyModule());
// Let 'goo' simulate a local variable from a 'foreach' loop.
private static final ImmutableMap<String, CodeChunk.WithValue> LOCAL_VAR_TRANSLATIONS =
ImmutableMap.<String, CodeChunk.WithValue>builder()
.put(
"goo",
id("gooData8"))
.put(
"goo__isFirst",
id("gooIndex8")
.doubleEquals(
number(0)))
.put(
"goo__isLast",
id("gooIndex8")
.doubleEquals(
id("gooListLen8")
.minus(
number(1))))
.put(
"goo__index",
id("gooIndex8"))
.build();
private static final TemplateAliases TEMPLATE_ALIASES = AliasUtils.IDENTITY_ALIASES;
private static final SoyFunction NOOP_REQUIRE_SOY_FUNCTION =
new SoyLibraryAssistedJsSrcFunction() {
@Override
public String getName() {
return "noopRequire";
}
@Override
public Set<Integer> getValidArgsSizes() {
return ImmutableSet.of(0);
}
@Override
public ImmutableSet<String> getRequiredJsLibNames() {
return ImmutableSet.of("for.function", "also.for.function");
}
@Override
public JsExpr computeForJsSrc(List<JsExpr> args) {
return new JsExpr("", Integer.MAX_VALUE);
}
};
private SoyJsSrcOptions jsSrcOptions;
private GenJsCodeVisitor genJsCodeVisitor;
private GuiceSimpleScope.InScope inScope;
@Before
public void setUp() {
jsSrcOptions = new SoyJsSrcOptions();
inScope = JsSrcTestUtils.simulateNewApiCall(INJECTOR, jsSrcOptions);
genJsCodeVisitor = INJECTOR.getInstance(GenJsCodeVisitor.class);
genJsCodeVisitor.templateAliases = TEMPLATE_ALIASES;
}
@After
public void tearDown() {
inScope.close();
}
@Test
public void testSoyFile() {
String testFileContent =
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** Test template. */\n"
+ "{template .goo}\n"
+ " {call .goo data=\"all\" /}\n"
+ " {call boo.woo.hoo data=\"all\" /}\n" // not defined in this file
+ "{/template}\n";
ParseResult parseResult = SoyFileSetParserBuilder.forFileContents(testFileContent).parse();
// ------ Not using Closure ------
String expectedJsFileContentStart =
"// This file was automatically generated from no-path.\n"
+ "// Please don't edit this file by hand.\n"
+ "\n"
+ "/**\n"
+ " * @fileoverview Templates in namespace boo.foo.\n"
+ " * @public\n"
+ " */\n"
+ "\n"
+ "if (typeof boo == 'undefined') { var boo = {}; }\n"
+ "if (typeof boo.foo == 'undefined') { boo.foo = {}; }\n"
+ "\n"
+ "\n";
List<String> jsFilesContents =
genJsCodeVisitor.gen(
parseResult.fileSet(), parseResult.registry(), ExplodingErrorReporter.get());
assertThat(jsFilesContents.get(0)).startsWith(expectedJsFileContentStart);
// ------ Using Closure, provide/require Soy namespaces ------
expectedJsFileContentStart =
"// This file was automatically generated from no-path.\n"
+ "// Please don't edit this file by hand.\n"
+ "\n"
+ "/**\n"
+ " * @fileoverview Templates in namespace boo.foo.\n"
+ " * @public\n"
+ " */\n"
+ "\n"
+ "goog.provide('boo.foo');\n"
+ "\n"
+ "goog.require('boo.woo');\n"
+ "\n";
jsSrcOptions.setShouldProvideRequireSoyNamespaces(true);
jsFilesContents =
genJsCodeVisitor.gen(
parseResult.fileSet(), parseResult.registry(), ExplodingErrorReporter.get());
assertThat(jsFilesContents.get(0)).startsWith(expectedJsFileContentStart);
// ------ Using Closure, provide/require JS functions ------
expectedJsFileContentStart =
"// This file was automatically generated from no-path.\n"
+ "// Please don't edit this file by hand.\n"
+ "\n"
+ "/**\n"
+ " * @fileoverview Templates in namespace boo.foo.\n"
+ " * @public\n"
+ " */\n"
+ "\n"
+ "goog.provide('boo.foo.goo');\n"
+ "\n"
+ "goog.require('boo.woo.hoo');\n"
+ "\n";
jsSrcOptions.setShouldProvideRequireSoyNamespaces(false);
jsSrcOptions.setShouldProvideRequireJsFunctions(true);
jsFilesContents =
genJsCodeVisitor.gen(
parseResult.fileSet(), parseResult.registry(), ExplodingErrorReporter.get());
assertThat(jsFilesContents.get(0)).startsWith(expectedJsFileContentStart);
// ------ Using Closure, provide both Soy namespaces and JS functions ------
expectedJsFileContentStart =
"// This file was automatically generated from no-path.\n"
+ "// Please don't edit this file by hand.\n"
+ "\n"
+ "/**\n"
+ " * @fileoverview Templates in namespace boo.foo.\n"
+ " * @public\n"
+ " */\n"
+ "\n"
+ "goog.provide('boo.foo');\n"
+ "goog.provide('boo.foo.goo');\n"
+ "\n"
+ "goog.require('boo.woo');\n"
+ "\n";
jsSrcOptions.setShouldProvideRequireJsFunctions(false);
jsSrcOptions.setShouldProvideRequireSoyNamespaces(true);
jsSrcOptions.setShouldProvideBothSoyNamespacesAndJsFunctions(true);
jsFilesContents =
genJsCodeVisitor.gen(
parseResult.fileSet(), parseResult.registry(), ExplodingErrorReporter.get());
assertThat(jsFilesContents.get(0)).startsWith(expectedJsFileContentStart);
}
@Test
public void testOnlyOneRequireStatementPerNamespace() {
String testFileContent =
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** Test template. */\n"
+ "{template .goo}\n"
+ " {call boo.woo.aaa data=\"all\" /}\n"
+ " {call boo.woo.aaa.bbb data=\"all\" /}\n"
+ " {call boo.woo.bbb data=\"all\" /}\n"
+ "{/template}\n";
ParseResult parseResult = SoyFileSetParserBuilder.forFileContents(testFileContent).parse();
// ------ Using Closure, provide/require Soy namespaces ------
String expectedJsFileContentStart =
"// This file was automatically generated from no-path.\n"
+ "// Please don't edit this file by hand.\n"
+ "\n"
+ "/**\n"
+ " * @fileoverview Templates in namespace boo.foo.\n"
+ " * @public\n"
+ " */\n"
+ "\n"
+ "goog.provide('boo.foo');\n"
+ "\n"
+ "goog.require('boo.woo');\n"
+ "goog.require('boo.woo.aaa');\n"
+ "\n";
jsSrcOptions.setShouldProvideRequireSoyNamespaces(true);
List<String> jsFilesContents =
genJsCodeVisitor.gen(
parseResult.fileSet(), parseResult.registry(), ExplodingErrorReporter.get());
assertThat(jsFilesContents.get(0)).startsWith(expectedJsFileContentStart);
}
@Test
public void testOnlyOneRequireStatementPerPluginNamespace() {
String testFileContent =
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** Test template. */\n"
+ "{template .goo}\n"
+ " {call for.function.aaa data=\"all\" /}\n"
+ " {noopRequire()}\n"
+ "{/template}\n";
ParseResult parseResult =
SoyFileSetParserBuilder.forFileContents(testFileContent)
.addSoyFunction(NOOP_REQUIRE_SOY_FUNCTION)
.parse();
// ------ Using Closure, provide/require Soy namespaces and required JS for function ------
String expectedJsFileContentStart =
"// This file was automatically generated from no-path.\n"
+ "// Please don't edit this file by hand.\n"
+ "\n"
+ "/**\n"
+ " * @fileoverview Templates in namespace boo.foo.\n"
+ " * @public\n"
+ " */\n"
+ "\n"
+ "goog.provide('boo.foo');\n"
+ "\n"
+ "goog.require('also.for.function');\n"
+ "goog.require('for.function');\n"
+ "\n";
jsSrcOptions.setShouldProvideRequireSoyNamespaces(true);
List<String> jsFilesContents =
genJsCodeVisitor.gen(
parseResult.fileSet(), parseResult.registry(), ExplodingErrorReporter.get());
assertThat(jsFilesContents.get(0)).startsWith(expectedJsFileContentStart);
}
@Test
public void testSoyFileWithRequirecssOnNamespace() {
String testFileContent =
""
+ "{namespace boo.foo autoescape=\"deprecated-noncontextual\"\n"
+ " requirecss=\"\n"
+ " ddd.eee.fff.ggg,\n"
+ " aaa.bbb.ccc\"}\n"
+ "\n"
+ "/** Test template. */\n"
+ "{template .goo}\n"
+ " blah\n"
+ "{/template}\n";
ParseResult parseResult = SoyFileSetParserBuilder.forFileContents(testFileContent).parse();
String expectedJsFileContentStart =
""
+ "// This file was automatically generated from no-path.\n"
+ "// Please don't edit this file by hand.\n"
+ "\n"
+ "/**\n"
+ " * @fileoverview Templates in namespace boo.foo.\n"
+ " * @requirecss {aaa.bbb.ccc}\n"
+ " * @requirecss {ddd.eee.fff.ggg}\n"
+ " * @public\n"
+ " */\n"
+ "\n";
List<String> jsFilesContents =
genJsCodeVisitor.gen(
parseResult.fileSet(), parseResult.registry(), ExplodingErrorReporter.get());
assertThat(jsFilesContents.get(0)).startsWith(expectedJsFileContentStart);
}
@Test
public void testSoyFileInDelegatePackage() {
String testFileContent =
"{delpackage MySecretFeature}\n"
+ "{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** Test delegate template. */\n"
+ "{deltemplate myDelegates.goo}\n"
+ " {delcall myDelegates.soo /}\n"
+ "{/deltemplate}\n";
ParseResult parse = SoyFileSetParserBuilder.forFileContents(testFileContent).parse();
String expectedJsFileContent =
""
+ "// This file was automatically generated from no-path.\n"
+ "// Please don't edit this file by hand.\n"
+ "\n"
+ "/**\n"
+ " * @fileoverview Templates in namespace boo.foo.\n"
+ " * @modName {MySecretFeature}\n"
+ " * @hassoydeltemplate {myDelegates.goo}\n"
+ " * @hassoydelcall {myDelegates.soo}\n"
+ " * @public\n"
+ " */\n"
+ "\n"
+ "goog.provide('boo.foo');\n"
+ "\n"
+ "goog.require('soy');\n"
+ "\n"
+ "\n"
+ "boo.foo.__deltemplate_s2_34da4ced = function("
+ "opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " return '' + soy.$$getDelegateFn(soy.$$getDelTemplateId('myDelegates.soo'), "
+ "'', false)(null, null, opt_ijData);\n"
+ "};\n"
+ "if (goog.DEBUG) {\n"
+ " boo.foo.__deltemplate_s2_34da4ced.soyTemplateName = "
+ "'boo.foo.__deltemplate_s2_34da4ced';\n"
+ "}\n"
+ "soy.$$registerDelegateFn(soy.$$getDelTemplateId('myDelegates.goo'), '', 1,"
+ " boo.foo.__deltemplate_s2_34da4ced);\n";
jsSrcOptions.setShouldProvideRequireSoyNamespaces(true);
List<String> jsFilesContents =
genJsCodeVisitor.gen(parse.fileSet(), parse.registry(), ExplodingErrorReporter.get());
assertThat(jsFilesContents.get(0)).isEqualTo(expectedJsFileContent);
}
@Test
public void testDelegateVariantProvideRequiresJsDocAnnotations() {
String testFileContent =
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** Test delegate template. */\n"
+ "{deltemplate myDelegates.goo variant=\"'googoo'\"}\n"
+ " {delcall myDelegates.moo variant=\"'moomoo'\" /}\n"
+ "{/deltemplate}\n";
ParseResult parse = SoyFileSetParserBuilder.forFileContents(testFileContent).parse();
String expectedJsFileContent =
""
+ "// This file was automatically generated from no-path.\n"
+ "// Please don't edit this file by hand.\n"
+ "\n"
+ "/**\n"
+ " * @fileoverview Templates in namespace boo.foo.\n"
+ " * @hassoydeltemplate {myDelegates.goo}\n"
+ " * @hassoydelcall {myDelegates.moo}\n"
+ " * @public\n"
+ " */\n"
+ "\n"
+ "goog.provide('boo.foo');\n"
+ "\n"
+ "goog.require('soy');\n"
+ "\n"
+ "\n"
+ "boo.foo.__deltemplate_s2_784ed7a8 = function("
+ "opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " return '' + soy.$$getDelegateFn(soy.$$getDelTemplateId('myDelegates.moo'), "
+ "'moomoo', false)(null, null, opt_ijData);\n"
+ "};\n"
+ "if (goog.DEBUG) {\n"
+ " boo.foo.__deltemplate_s2_784ed7a8.soyTemplateName = "
+ "'boo.foo.__deltemplate_s2_784ed7a8';\n"
+ "}\n"
+ "soy.$$registerDelegateFn(soy.$$getDelTemplateId('myDelegates.goo'), 'googoo', 0,"
+ " boo.foo.__deltemplate_s2_784ed7a8);\n";
jsSrcOptions.setShouldProvideRequireSoyNamespaces(true);
List<String> jsFilesContents =
genJsCodeVisitor.gen(parse.fileSet(), parse.registry(), ExplodingErrorReporter.get());
assertThat(jsFilesContents.get(0)).isEqualTo(expectedJsFileContent);
}
@Test
public void testTemplate() {
String testFileContent =
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** Test template. */\n"
+ "{template .goo}\n"
+ " Blah\n"
+ "{/template}\n";
TemplateNode template =
(TemplateNode)
SharedTestUtils.getNode(
SoyFileSetParserBuilder.forFileContents(testFileContent).parse().fileSet());
// ------ Code style 'concat' ------
String expectedJsCode =
""
+ "boo.foo.goo = function(opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " return 'Blah';\n"
+ "};\n"
+ "if (goog.DEBUG) {\n"
+ " boo.foo.goo.soyTemplateName = 'boo.foo.goo';\n"
+ "}\n";
// Setup the GenJsCodeVisitor's state before the template is visited.
genJsCodeVisitor.jsCodeBuilder = new JsCodeBuilder();
genJsCodeVisitor.visitForTesting(template, ExplodingErrorReporter.get());
assertThat(genJsCodeVisitor.jsCodeBuilder.getCode()).isEqualTo(expectedJsCode);
}
@Test
public void testTemplateThatShouldEnsureDataIsDefined() {
String testFileContent =
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** @param? moo */\n"
+ "{template .goo}\n"
+ " {$moo}\n"
+ "{/template}\n";
TemplateNode template =
(TemplateNode)
SharedTestUtils.getNode(
SoyFileSetParserBuilder.forFileContents(testFileContent).parse().fileSet());
String expectedJsCode =
""
+ "boo.foo.goo = function(opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " opt_data = opt_data || {};\n"
+ " return '' + opt_data.moo;\n"
+ "};\n"
+ "if (goog.DEBUG) {\n"
+ " boo.foo.goo.soyTemplateName = 'boo.foo.goo';\n"
+ "}\n";
// Setup the GenJsCodeVisitor's state before the template is visited.
genJsCodeVisitor.jsCodeBuilder = new JsCodeBuilder();
genJsCodeVisitor.visitForTesting(template, ExplodingErrorReporter.get());
assertThat(genJsCodeVisitor.jsCodeBuilder.getCode()).isEqualTo(expectedJsCode);
// ------ Should not generate extra statement for injected and local var data refs. ------
testFileContent =
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** Test template. */\n"
+ "{template .goo}\n"
+ " {let $moo: 90 /}\n"
+ " {$moo}{$ij.moo}\n"
+ "{/template}\n";
template =
(TemplateNode)
SharedTestUtils.getNode(
SoyFileSetParserBuilder.forFileContents(testFileContent).parse().fileSet());
expectedJsCode =
""
+ "boo.foo.goo = function(opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " var output = '';\n"
+ " var moo__soy4 = 90;\n"
+ " output += moo__soy4 + opt_ijData.moo;\n"
+ " return output;\n"
+ "};\n"
+ "if (goog.DEBUG) {\n"
+ " boo.foo.goo.soyTemplateName = 'boo.foo.goo';\n"
+ "}\n";
// Setup the GenJsCodeVisitor's state before the template is visited.
genJsCodeVisitor = INJECTOR.getInstance(GenJsCodeVisitor.class);
genJsCodeVisitor.jsCodeBuilder = new JsCodeBuilder();
genJsCodeVisitor.templateAliases = TEMPLATE_ALIASES;
genJsCodeVisitor.visitForTesting(template, ExplodingErrorReporter.get());
assertThat(genJsCodeVisitor.jsCodeBuilder.getCode()).isEqualTo(expectedJsCode);
}
@Test
public void testTemplateWithShouldGenerateJsdoc() {
jsSrcOptions.setShouldGenerateJsdoc(true);
String testFileContent =
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** Test template. */\n"
+ "{template .goo}\n"
+ " Blah\n"
+ "{/template}\n";
TemplateNode template =
(TemplateNode)
SharedTestUtils.getNode(
SoyFileSetParserBuilder.forFileContents(testFileContent).parse().fileSet());
// ------ Code style 'concat' with shouldGenerateJsdoc ------
String expectedJsCode =
""
+ "/**\n"
+ " * @param {Object<string, *>=} opt_data\n"
+ " * @param {Object<string, *>=} opt_ijData\n"
+ " * @param {Object<string, *>=} opt_ijData_deprecated\n"
+ " * @return {string}\n"
+ " * @suppress {checkTypes}\n"
+ " */\n"
+ "boo.foo.goo = function(opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " return 'Blah';\n"
+ "};\n"
+ "if (goog.DEBUG) {\n"
+ " boo.foo.goo.soyTemplateName = 'boo.foo.goo';\n"
+ "}\n";
// Setup the GenJsCodeVisitor's state before the template is visited.
genJsCodeVisitor.jsCodeBuilder = new JsCodeBuilder();
genJsCodeVisitor.visitForTesting(template, ExplodingErrorReporter.get());
assertThat(genJsCodeVisitor.jsCodeBuilder.getCode()).isEqualTo(expectedJsCode);
}
@Test
public void testStrictTemplateShouldGenerateSanitizedContentReturnValue() {
jsSrcOptions.setShouldGenerateJsdoc(true);
String testFileContent =
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** Test template. */\n"
+ "{template .goo autoescape=\"strict\" kind=\"js\"}\n"
+ " alert('Hello World');\n"
+ "{/template}\n";
TemplateNode template =
(TemplateNode)
SharedTestUtils.getNode(
SoyFileSetParserBuilder.forFileContents(testFileContent).parse().fileSet());
// ------ Code style 'concat' with shouldGenerateJsdoc ------
String expectedJsCode =
""
+ "/**\n"
+ " * @param {Object<string, *>=} opt_data\n"
+ " * @param {Object<string, *>=} opt_ijData\n"
+ " * @param {Object<string, *>=} opt_ijData_deprecated\n"
+ " * @return {!goog.soy.data.SanitizedJs}\n"
+ " * @suppress {checkTypes}\n"
+ " */\n"
+ "boo.foo.goo = function(opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " return soydata.VERY_UNSAFE.ordainSanitizedJs('alert(\\'Hello World\\');');\n"
+ "};\n"
+ "if (goog.DEBUG) {\n"
+ " boo.foo.goo.soyTemplateName = 'boo.foo.goo';\n"
+ "}\n";
// Setup the GenJsCodeVisitor's state before the template is visited.
genJsCodeVisitor.jsCodeBuilder = new JsCodeBuilder();
genJsCodeVisitor.visitForTesting(template, ExplodingErrorReporter.get());
assertThat(genJsCodeVisitor.jsCodeBuilder.getCode()).isEqualTo(expectedJsCode);
}
@Test
public void testDelTemplate() {
String testFileContent =
""
+ // note: no delpackage => priority 0
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** Test delegate template. */\n"
+ "{deltemplate myDelegates.goo}\n"
+ " Blah\n"
+ "{/deltemplate}\n";
ParseResult parseResult = SoyFileSetParserBuilder.forFileContents(testFileContent).parse();
// ------ Code style 'concat'. ------
String expectedJsCode =
""
+ "goog.provide('boo.foo');\n"
+ "\n"
+ "goog.require('soy');\n"
+ "\n"
+ "\n"
+ "boo.foo.__deltemplate_s2_ad618961 = function("
+ "opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " return 'Blah';\n"
+ "};\n"
+ "if (goog.DEBUG) {\n"
+ " boo.foo.__deltemplate_s2_ad618961.soyTemplateName = "
+ "'boo.foo.__deltemplate_s2_ad618961';\n"
+ "}\n"
+ "soy.$$registerDelegateFn(soy.$$getDelTemplateId('myDelegates.goo'), '', 0,"
+ " boo.foo.__deltemplate_s2_ad618961);\n";
genJsCodeVisitor.jsSrcOptions.setShouldProvideRequireSoyNamespaces(true);
// Setup the GenJsCodeVisitor's state before the template is visited.
String file =
genJsCodeVisitor
.gen(parseResult.fileSet(), parseResult.registry(), ExplodingErrorReporter.get())
.get(0);
assertThat(file).endsWith(expectedJsCode);
}
@Test
public void testDelTemplateWithVariant() {
String testFileContent =
""
+ "{delpackage MySecretFeature}\n"
+ // note: delpackage => priority 1
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** Test delegate template with variant. */\n"
+ "{deltemplate myDelegates.goo variant=\"'moo'\"}\n"
+ " Blah\n"
+ "{/deltemplate}\n";
TemplateNode template =
(TemplateNode)
SharedTestUtils.getNode(
SoyFileSetParserBuilder.forFileContents(testFileContent).parse().fileSet());
// ------ Code style 'concat'. ------
String expectedJsCode =
""
+ "boo.foo.__deltemplate_s2_b66e4cb3 = function("
+ "opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " return 'Blah';\n"
+ "};\n"
+ "if (goog.DEBUG) {\n"
+ " boo.foo.__deltemplate_s2_b66e4cb3.soyTemplateName = "
+ "'boo.foo.__deltemplate_s2_b66e4cb3';\n"
+ "}\n"
+ "soy.$$registerDelegateFn("
+ "soy.$$getDelTemplateId('myDelegates.goo'), 'moo', 1,"
+ " boo.foo.__deltemplate_s2_b66e4cb3);\n";
// Setup the GenJsCodeVisitor's state before the template is visited.
genJsCodeVisitor.jsCodeBuilder = new JsCodeBuilder();
genJsCodeVisitor.visitForTesting(template, ExplodingErrorReporter.get());
assertThat(genJsCodeVisitor.jsCodeBuilder.getCode()).isEqualTo(expectedJsCode);
}
@Test
public void testRawText() {
assertGeneratedJsCode(
"I'm feeling lucky!\n",
"output += 'I\\'m feeling lucky!';\n");
assertGeneratedJsCode(
"{lb}^_^{rb}{sp}{\\n}\n",
"output += '{^_^} \\n';\n");
}
@Test
public void testGoogMsg() {
String soyCode =
"{@param user : ?}\n"
+ "{@param url : ?}\n"
+ "{msg desc=\"Tells the user to click a link.\"}\n"
+ " Hello {$user.userName}, please click <a href=\"{$url}\">here</a>.\n"
+ "{/msg}\n";
String expectedJsCode =
""
+ "/** @desc Tells the user to click a link. */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'Hello {$userName}, please click {$startLink}here{$endLink}.', "
+ "{'userName': opt_data.user.userName, "
+ "'startLink': '<a href=\"' + opt_data.url + '\">', "
+ "'endLink': '</a>'});\n"
+ "output += MSG_UNNAMED;\n";
assertGeneratedJsCode(soyCode, expectedJsCode);
soyCode =
""
+ "{msg meaning=\"boo\" desc=\"foo\" hidden=\"true\"}\n"
+ " Blah\n"
+ "{/msg}\n";
expectedJsCode =
""
+ "/** @meaning boo\n"
+ " * @desc foo\n"
+ " * @hidden */\n"
+ "var MSG_UNNAMED = goog.getMsg('Blah');\n"
+ "output += MSG_UNNAMED;\n";
assertGeneratedJsCode(soyCode, expectedJsCode);
soyCode =
"{@param boo : ?}\n"
+ "{@param a : ?}\n"
+ "{msg desc=\"A span with generated id.\"}\n"
+ " <span id=\"{for $i in range(3)}{$i}{/for}\">\n"
+ " {call some.func data=\"$boo\"}\n"
+ " {param goo}\n"
+ " {for $i in range(4)}{$i}{/for}\n"
+ " {/param}\n"
+ " {/call}\n"
+ " {$a + 2}\n"
+ "{/msg}\n";
expectedJsCode =
""
+ "var htmlTag15 = '<span id=\"';\n"
+ "for (var i10 = 0; i10 < 3; i10++) {\n"
+ " htmlTag15 += i10;\n"
+ "}\n"
+ "htmlTag15 += '\">';\n"
+ "var param19 = '';\n"
+ "for (var i21 = 0; i21 < 4; i21++) {\n"
+ " param19 += i21;\n"
+ "}\n"
+ "/** @desc A span with generated id. */\n"
+ "var MSG_UNNAMED = goog.getMsg('{$startSpan}{$xxx_1}{$xxx_2}', "
+ "{'startSpan': htmlTag15, "
+ "'xxx_1': some.func(soy.$$assignDefaults({goo: param19}, opt_data.boo), null, "
+ "opt_ijData),"
+ " 'xxx_2': opt_data.a + 2});\n"
+ "output += MSG_UNNAMED;\n";
assertGeneratedJsCode(soyCode, expectedJsCode);
soyCode =
"{msg desc=\"foo\"}\n"
+ " More \u00BB\n"
+ "{/msg}\n";
// Make sure JS code doesn't have literal unicode characters, since they
// don't always get interpreted properly.
expectedJsCode =
""
+ "/** @desc foo */\n"
+ "var MSG_UNNAMED = goog.getMsg('More \\u00BB');\n"
+ "output += MSG_UNNAMED;\n";
assertGeneratedJsCode(soyCode, expectedJsCode);
}
@Test
public void testGoogMsgWithFallback() {
String soyCode =
"{msg meaning=\"verb\" desc=\"Used as a verb.\"}\n"
+ " Archive\n"
+ "{fallbackmsg desc=\"\"}\n"
+ " Archive\n"
+ "{/msg}\n";
String expectedJsCode =
""
+ "/** @meaning verb\n"
+ " * @desc Used as a verb. */\n"
+ "var MSG_UNNAMED = goog.getMsg('Archive');\n"
+ "/** @desc */\n"
+ "var MSG_UNNAMED$$1 = goog.getMsg('Archive');\n"
+ "var msg_s = goog.getMsgWithFallback(MSG_UNNAMED, MSG_UNNAMED$$1);\n"
+ "output += msg_s;\n";
// Note: Using getGeneratedJsCode() directly so that ids are not replaced with ###.
assertThat(getGeneratedJsCode(soyCode, ExplodingErrorReporter.get())).isEqualTo(expectedJsCode);
}
@Test
public void testSoyV1GlobalPlaceholderCompatibility() {
// Test that placeholders for global variables have the same form
// as they appeared in Soy V1 so that teams with internationalized
// strings don't have to re-translate strings with new placeholders.
// global, all caps, underbars between words. Placeholder should
// be lower-case camel case version
String soyCode =
"{msg desc=\"\"}\n"
+ "Unable to reach {PRODUCT_NAME_HTML}. Eeeek!\n"
+ "{/msg}\n";
String expectedJsCode =
""
+ "/** @desc */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'Unable to reach {$productNameHtml}. Eeeek!', "
+ "{'productNameHtml': PRODUCT_NAME_HTML});\n"
+ "output += MSG_UNNAMED;\n";
assertGeneratedJsCode(soyCode, expectedJsCode);
// global, all caps, leading or trailing underbars. Placeholder should
// be lower-case camel case version
soyCode =
"{msg desc=\"\"}\n"
+ "{window.field}{window._AField}{_window_.forest}{window.size.x}"
+ "{window.size._xx_xx_}\n"
+ "{/msg}\n";
expectedJsCode =
""
+ "/** @desc */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{$field}{$aField}{$forest}{$x}{$xxXx}', "
+ "{'field': window.field, "
+ "'aField': window._AField, "
+ "'forest': _window_.forest, "
+ "'x': window.size.x, "
+ "'xxXx': window.size._xx_xx_});\n"
+ "output += MSG_UNNAMED;\n";
assertGeneratedJsCode(soyCode, expectedJsCode);
// global, property name. Placeholder should be lower case
// camel case version of last component.
soyCode = "{msg desc=\"\"}\n" + "{window.FOO.BAR} {window.ORIGINAL_SERVER_NAME}\n" + "{/msg}\n";
expectedJsCode =
""
+ "/** @desc */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{$bar} {$originalServerName}', "
+ "{'bar': window.FOO.BAR, "
+ "'originalServerName': window.ORIGINAL_SERVER_NAME});\n"
+ "output += MSG_UNNAMED;\n";
assertGeneratedJsCode(soyCode, expectedJsCode);
// global, camel case name. Placeholder should be same.
soyCode = "{msg desc=\"\"}\n" + " {camelCaseName}{global.camelCase}. \n" + "{/msg}\n";
expectedJsCode =
""
+ "/** @desc */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{$camelCaseName}{$camelCase}.', "
+ "{'camelCaseName': camelCaseName, "
+ "'camelCase': global.camelCase});\n"
+ "output += MSG_UNNAMED;\n";
assertGeneratedJsCode(soyCode, expectedJsCode);
// Upper case camel case name becomes lower case in placeholder.
soyCode = "{msg desc=\"\"}\n" + "Unable to reach {CamelCaseName}. Eeeek!\n" + "{/msg}\n";
expectedJsCode =
""
+ "/** @desc */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'Unable to reach {$camelCaseName}. Eeeek!', "
+ "{'camelCaseName': CamelCaseName});\n"
+ "output += MSG_UNNAMED;\n";
assertGeneratedJsCode(soyCode, expectedJsCode);
// Leading and trailing underbars are stripped when creating placeholders.
soyCode =
"{msg desc=\"Not actually shown to the user.\"}\n"
+ "{_underbar} {_wunderBar_}\n"
+ "{_ThunderBar_} {underCar__}\n"
+ "{window.__car__}{window.__AnotherBar__}{/msg}\n";
expectedJsCode =
""
+ "/** @desc Not actually shown to the user. */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{$underbar} {$wunderBar}{$thunderBar}"
+ " {$underCar}{$car}{$anotherBar}', "
+ "{'underbar': _underbar, "
+ "'wunderBar': _wunderBar_, "
+ "'thunderBar': _ThunderBar_, "
+ "'underCar': underCar__, "
+ "'car': window.__car__, "
+ "'anotherBar': window.__AnotherBar__});\n"
+ "output += MSG_UNNAMED;\n";
assertGeneratedJsCode(soyCode, expectedJsCode);
}
@Test
public void testSoyV1LocalPlaceholderCompatibility() {
// Test that placeholders for local variables (passed into the template)
// have the same form as they appeared in Soy V1 so that teams
// with internationalized strings don't have to re-translate strings
// with new placeholders.
// local, all caps, underbars between words. Placeholder should
// be lower-case camel case version
String soyCode =
"{@param PRODUCT_NAME_HTML : ?}\n"
+ "{msg desc=\"\"}\n"
+ "Unable to reach {$PRODUCT_NAME_HTML}. Eeeek!\n"
+ "{/msg}\n";
String expectedJsCode =
""
+ "/** @desc */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'Unable to reach {$productNameHtml}. Eeeek!', "
+ "{'productNameHtml': opt_data.PRODUCT_NAME_HTML});\n"
+ "output += MSG_UNNAMED;\n";
assertGeneratedJsCode(soyCode, expectedJsCode);
// local, property name. Placeholder should be lower case
// camel case version of last component.
soyCode =
"{@param myvar : ?}\n"
+ "{@param window : ?}\n"
+ "{msg desc=\"\"}\n"
+ "{$myvar.foo.bar}{$myvar.ORIGINAL_SERVER}"
+ "{$window.size._xx_xx_}\n"
+ "{/msg}\n";
expectedJsCode =
""
+ "/** @desc */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{$bar}{$originalServer}{$xxXx}', "
+ "{'bar': opt_data.myvar.foo.bar, "
+ "'originalServer': opt_data.myvar.ORIGINAL_SERVER, "
+ "'xxXx': opt_data.window.size._xx_xx_});\n"
+ "output += MSG_UNNAMED;\n";
assertGeneratedJsCode(soyCode, expectedJsCode);
// global, property name, with underbars. Placeholder should be lower case
// camel case version of last component, with underbars stripped.
soyCode =
"{@param myvar : ?}\n"
+ "{msg desc=\"\"}\n"
+ "{$myvar.foo._bar}{$myvar.foo.trail_}{$myvar.foo._bar_bar_bar_}\n"
+ "{/msg}\n";
expectedJsCode =
""
+ "/** @desc */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{$bar}{$trail}{$barBarBar}', "
+ "{'bar': opt_data.myvar.foo._bar, "
+ "'trail': opt_data.myvar.foo.trail_, "
+ "'barBarBar': opt_data.myvar.foo._bar_bar_bar_});\n"
+ "output += MSG_UNNAMED;\n";
assertGeneratedJsCode(soyCode, expectedJsCode);
// local, camel case name. Placeholder should be same, in lower case.
soyCode =
"{@param productName : ?}\n"
+ "{@param OtherProductName : ?}\n"
+ "{msg desc=\"\"}\n"
+ " {$productName}{$OtherProductName}\n"
+ "{/msg}\n";
expectedJsCode =
""
+ "/** @desc */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{$productName}{$otherProductName}', "
+ "{'productName': opt_data.productName, "
+ "'otherProductName': opt_data.OtherProductName});\n"
+ "output += MSG_UNNAMED;\n";
assertGeneratedJsCode(soyCode, expectedJsCode);
}
@Test
public void testPrintGoogMsg() {
String soyCode =
"{@param userName : ?}\n"
+ "{@param url : ?}\n"
+ "{msg desc=\"Tells the user to click a link.\"}\n"
+ " Hello {$userName}, please click <a href=\"{$url}\">here</a>.\n"
+ "{/msg}\n";
String expectedJsCode =
""
+ "/** @desc Tells the user to click a link. */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'Hello {$userName}, please click {$startLink}here{$endLink}.', "
+ "{'userName': opt_data.userName, "
+ "'startLink': '<a href=\"' + opt_data.url + '\">', "
+ "'endLink': '</a>'});\n"
+ "output += MSG_UNNAMED;\n";
assertGeneratedJsCode(soyCode, expectedJsCode);
}
@Test
public void testPrint() {
assertGeneratedJsCode("{@param boo : ?}\n{$boo.foo}\n", "output += opt_data.boo.foo;\n");
}
@Test
public void testLet() {
String soyNodeCode =
"{@param boo : ?}\n"
+ "{if $boo}\n"
+ // wrapping in if-block makes using assertGeneratedJsCode easier
" {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\"}Boop!{/let}\n"
+ " {$alpha}{$beta}{$gamma}{$delta}\n"
+ "{/if}\n";
String expectedJsCode =
""
+ "if (opt_data.boo) {\n"
+ " var alpha__soy8 = opt_data.boo.foo;\n"
+ " var beta__soy10 = 'Boo!';\n"
+ " var gamma__soy13 = '';\n"
+ " var i15Limit = alpha__soy8;\n"
+ " for (var i15 = 0; i15 < i15Limit; i15++) {\n"
+ " gamma__soy13 += i15 + beta__soy10;\n"
+ " }\n"
+ " var delta__soy22 = 'Boop!';\n"
+ " delta__soy22 = soydata.VERY_UNSAFE.$$ordainSanitizedHtmlForInternalBlocks("
+ "delta__soy22);\n"
+ " output += alpha__soy8 + beta__soy10 + gamma__soy13 + delta__soy22;\n"
+ "}\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
}
// Regression test for a bug where the logic for ordaining strict letcontent blocks failed to
// propagate the necessary requires for the ordainer functions.
@Test
public void testStrictLetAddsAppropriateRequires() {
jsSrcOptions.setShouldProvideRequireSoyNamespaces(true);
String soyNodeCode = "{let $text kind=\"text\"}foo{/let}{let $html kind=\"html\"}foo{/let}\n";
ParseResult parseResult =
SoyFileSetParserBuilder.forTemplateContents(AutoEscapingType.STRICT, soyNodeCode).parse();
String jsFilesContents =
genJsCodeVisitor
.gen(parseResult.fileSet(), parseResult.registry(), ExplodingErrorReporter.get())
.get(0);
assertThat(jsFilesContents).contains("goog.require('soydata')");
assertThat(jsFilesContents).contains("goog.require('soydata.VERY_UNSAFE')");
}
@Test
public void testIf() {
String soyNodeCode;
String expectedJsCode;
soyNodeCode = JOINER.join(
"{@param boo : ?}",
"{if $boo}",
" Blah",
"{else}",
" Bluh",
"{/if}");
expectedJsCode = "output += opt_data.boo ? 'Blah' : 'Bluh';\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
soyNodeCode =
JOINER.join(
"{@param boo : ?}",
"{@param goo : ?}",
"{if $boo}",
" Blah",
"{elseif not strContains($goo, 'goo')}",
" Bleh",
"{else}",
" Bluh",
"{/if}");
expectedJsCode =
JOINER.join(
"var $tmp = null;",
"if (opt_data.boo) {",
" $tmp = 'Blah';",
"} else if (!(('' + gooData8).indexOf('goo') != -1)) {",
" $tmp = 'Bleh';",
"} else {",
" $tmp = 'Bluh';",
"}",
"output += $tmp;",
"");
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
soyNodeCode =
JOINER.join(
"{@param boo : ?}",
"{@param goo : ?}",
"{if $boo.foo > 0}",
" {for $i in range(4)}",
" {$i+1}<br>",
" {/for}",
"{elseif not strContains($goo, 'goo')}",
" Bleh",
"{else}",
" Bluh",
"{/if}");
expectedJsCode =
""
+ "if (opt_data.boo.foo > 0) {\n"
+ " for (var i9 = 0; i9 < 4; i9++) {\n"
+ " output += i9 + 1 + '<br>';\n"
+ " }\n"
+ "} else if (!(('' + gooData8).indexOf('goo') != -1)) {\n"
+ " output += 'Bleh';\n"
+ "} else {\n"
+ " output += 'Bluh';\n"
+ "}\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
}
@Test
public void testSwitch() {
String soyNodeCode =
"{@param boo : ?}\n"
+ "{@param goo : ?}\n"
+ "{switch $boo}\n"
+ " {case 0}\n"
+ " Blah\n"
+ " {case 1, $goo + 1, 2}\n"
+ " Bleh\n"
+ " {default}\n"
+ " Bluh\n"
+ "{/switch}\n";
String expectedJsCode =
"var $tmp = opt_data.boo;\n"
+ "switch (goog.isObject($tmp) ? $tmp.toString() : $tmp) {\n"
+ " case 0:\n"
+ " output += 'Blah';\n"
+ " break;\n"
+ " case 1:\n"
+ " case gooData8 + 1:\n"
+ " case 2:\n"
+ " output += 'Bleh';\n"
+ " break;\n"
+ " default:\n"
+ " output += 'Bluh';\n"
+ "}\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
}
@Test
public void testSwitch_withNullCoalescing() {
String soyNodeCode =
"{@param alpha : ?}\n"
+ "{@param beta : ?}\n"
+ "{switch $alpha ?: $beta}\n"
+ " {default}\n"
+ " Bluh\n"
+ "{/switch}\n";
String expectedJsCode =
"var $tmp = ($$temp = opt_data.alpha) == null ? opt_data.beta : $$temp;\n"
+ "switch (goog.isObject($tmp) ? $tmp.toString() : $tmp) {\n"
+ " default:\n"
+ " output += 'Bluh';\n"
+ "}\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
}
@Test
public void testForeach() {
String soyNodeCode =
"{@param boo : ?}\n"
+ "{foreach $foo in $boo.foos}\n"
+ " {if not isFirst($foo)}\n"
+ " <br>\n"
+ " {/if}\n"
+ " {$foo} is fool no. {index($foo)}\n"
+ " {if isLast($foo)}\n"
+ " <br>The end.\n"
+ " {/if}\n"
+ "{ifempty}\n"
+ " No fools here.\n"
+ "{/foreach}\n";
String expectedJsCode =
""
+ "var foo19List = opt_data.boo.foos;\n"
+ "var foo19ListLen = foo19List.length;\n"
+ "if (foo19ListLen > 0) {\n"
+ " for (var foo19Index = 0; foo19Index < foo19ListLen; foo19Index++) {\n"
+ " var foo19Data = foo19List[foo19Index];\n"
+ " output += (!(foo19Index == 0) ? '<br>' : '') + foo19Data + ' is fool no. '"
+ " + foo19Index + (foo19Index == foo19ListLen - 1 ? '<br>The end.' : '');\n"
+ " }\n"
+ "} else {\n"
+ " output += 'No fools here.';\n"
+ "}\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
// TODO(user): Test a foreach-loop with initializing statements
}
@Test
public void testFor() {
String soyNodeCode =
"{@param boo : ?}\n"
+ "{@param goo : ?}\n"
+ "{for $i in range(8, 16, 2)}\n"
+ " {$boo[$i] + $goo[$i]}\n"
+ "{/for}\n";
String expectedJsCode =
""
+ "for (var i6 = 8; i6 < 16; i6 += 2) {\n"
+ " output += opt_data.boo[i6] + gooData8[i6];\n"
+ "}\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
soyNodeCode =
""
+ "{@param boo : ?}\n"
+ "{@param goo : ?}\n"
+ "{@param foo : ?}\n"
+ "{for $i in range($boo-$goo, $boo+$goo, $foo)}\n"
+ " {$i + 1}{sp}\n"
+ "{/for}\n";
expectedJsCode =
""
+ "var i7Limit = opt_data.boo + gooData8;\n"
+ "var i7Increment = opt_data.foo;\n"
+ "for (var i7 = opt_data.boo - gooData8; i7 < i7Limit; i7 += i7Increment) {\n"
+ " output += i7 + 1 + ' ';\n"
+ "}\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
soyNodeCode =
""
+ "{let $boo: ['a': [], 'b': [10, 20, 30]] /}\n"
+ "{for $i in range($boo.b[0], $boo.b[1], $boo.b[2])}\n"
+ " {$i}\n"
+ "{/for}\n";
expectedJsCode =
""
+ "var boo__soy4 = {a: [], b: [10, 20, 30]};\n"
+ "var i6Limit = boo__soy4.b[1];\n"
+ "var i6Increment = boo__soy4.b[2];\n"
+ "for (var i6 = boo__soy4.b[0]; i6 < i6Limit; i6 += i6Increment) {\n"
+ " output += i6;\n"
+ "}\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
// TODO(user): Test a for-loop with initializing statements
}
@Test
public void testBasicCall() {
assertGeneratedJsCode(
"{call some.func data=\"all\" /}\n",
"output += some.func(opt_data, null, opt_ijData);\n");
String soyNodeCode =
"{@param moo : ?}\n" + "{call some.func}\n" + " {param goo : $moo /}\n" + "{/call}\n";
assertGeneratedJsCode(
soyNodeCode,
"output += some.func({goo: opt_data.moo}, null, opt_ijData);\n");
soyNodeCode =
"{@param boo : ?}\n"
+ "{call some.func data=\"$boo\"}\n"
+ " {param goo}\n"
+ " {for $i in range(7)}\n"
+ " {$i}\n"
+ " {/for}\n"
+ " {/param}\n"
+ "{/call}\n";
String expectedJsCode =
""
+ "var param6 = '';\n"
+ "for (var i8 = 0; i8 < 7; i8++) {\n"
+ " param6 += i8;\n"
+ "}\n"
+ "output += some.func(soy.$$assignDefaults({goo: param6}, opt_data.boo), null, "
+ "opt_ijData);\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
}
@Test
public void testDelegateCall() {
assertGeneratedJsCode(
"{@param boo : ?}\n" + "{delcall my.delegate data=\"$boo.foo\" /}\n",
"output += soy.$$getDelegateFn(soy.$$getDelTemplateId('my.delegate'), '', false)"
+ "(opt_data.boo.foo, null, opt_ijData);\n");
assertGeneratedJsCode(
"{@param boo : ?}\n"
+ "{@param voo : ?}\n"
+ "{delcall my.delegate variant=\"$voo\" data=\"$boo.foo\" /}\n",
"output += soy.$$getDelegateFn("
+ "soy.$$getDelTemplateId('my.delegate'), opt_data.voo, false)"
+ "(opt_data.boo.foo, null, opt_ijData);\n");
assertGeneratedJsCode(
"{@param boo : ?}\n"
+ "{delcall my.delegate data=\"$boo.foo\" allowemptydefault=\"true\" /}\n",
"output += soy.$$getDelegateFn(soy.$$getDelTemplateId('my.delegate'), '', true)"
+ "(opt_data.boo.foo, null, opt_ijData);\n");
}
@Test
public void testLog() {
assertGeneratedJsCode(
"{@param boo : ?}\n" + "{log}Blah {$boo}.{/log}\n",
"window.console.log('Blah ' + opt_data.boo + '.');\n");
assertGeneratedJsCode(
""
+ "{@param foo : ?}\n"
+ "{@param boo : ?}\n"
+ "{@param moo : ?}\n"
+ "{if true}\n"
+ " {$foo}\n"
+ " {log}Blah {$boo}.{/log}\n"
+ " {$moo}\n"
+ "{/if}\n",
""
+ "if (true) {\n"
+ " output += opt_data.foo;\n"
+ " window.console.log('Blah ' + opt_data.boo + '.');\n"
+ " output += opt_data.moo;\n"
+ "}\n");
}
@Test
public void testDebugger() {
assertGeneratedJsCode("{debugger}\n", "debugger;\n");
assertGeneratedJsCode(
"{@param foo : ?}\n"
+ "{@param moo : ?}\n"
+ ""
+ "{if true}\n"
+ " {$foo}\n"
+ " {debugger}\n"
+ " {$moo}\n"
+ "{/if}\n",
""
+ "if (true) {\n"
+ " output += opt_data.foo;\n"
+ " debugger;\n"
+ " output += opt_data.moo;\n"
+ "}\n");
}
@Test
public void testXid() {
assertGeneratedJsCode("{xid some-id}\n", "output += xid('some-id');\n");
String testFileContent =
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** Test template. */\n"
+ "{template .goo}\n"
+ " {xid some-id}\n"
+ "{/template}\n";
ParseResult parseResult = SoyFileSetParserBuilder.forFileContents(testFileContent).parse();
jsSrcOptions.setShouldProvideRequireSoyNamespaces(true);
List<String> jsFilesContents =
genJsCodeVisitor.gen(
parseResult.fileSet(), parseResult.registry(), ExplodingErrorReporter.get());
assertThat(jsFilesContents.get(0)).contains("goog.require('xid');");
}
// -----------------------------------------------------------------------------------------------
// Tests for plural/select messages.
@Test
public void testMsgWithPlural() {
// A simple plural message with offset and remainder().
String soyNodeCode =
"{@param num_people: ?}\n"
+ "{@param person : ?}\n"
+ "{@param place : ?}\n"
+ " {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";
String expectedJsCode =
""
+ "/** @desc A sample plural message */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{NUM_PEOPLE,plural,offset:1 "
+ "=0{I see no one in {PLACE}.}"
+ "=1{I see {PERSON} in {PLACE}.}"
+ "=2{I see {PERSON} and one other person in {PLACE}.}"
+ "other{ I see {PERSON} and {XXX} other people in {PLACE}.}"
+ "}');\n"
+ "var msg_s = new goog.i18n.MessageFormat(MSG_UNNAMED).formatIgnoringPound("
+ "{'NUM_PEOPLE': opt_data.num_people, "
+ "'PLACE': opt_data.place, "
+ "'PERSON': opt_data.person, "
+ "'XXX': opt_data.num_people - 1});\n"
+ "output += msg_s;\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
// A simple plural message with no offset.
soyNodeCode =
"{@param num_people: ?}\n"
+ "{@param person : ?}\n"
+ "{@param place : ?}\n"
+ " {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} persons in {$place}, including {$person}.\n"
+ " {/plural}\n"
+ " {/msg}\n";
expectedJsCode =
""
+ "/** @desc A sample plural message */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{NUM_PEOPLE_1,plural,"
+ "=0{I see no one in {PLACE}.}"
+ "=1{I see {PERSON} in {PLACE}.}"
+ "other{I see {NUM_PEOPLE_2} persons in {PLACE}, including {PERSON}.}"
+ "}');\n"
+ "var msg_s = new goog.i18n.MessageFormat(MSG_UNNAMED).formatIgnoringPound("
+ "{'NUM_PEOPLE_1': opt_data.num_people, "
+ "'PLACE': opt_data.place, "
+ "'PERSON': opt_data.person, "
+ "'NUM_PEOPLE_2': opt_data.num_people});\n"
+ "output += msg_s;\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
// Same message as above, but with (a) 0 offset explicitly specified and (b) a plural
// expression that is a function call, specifically length(...).
soyNodeCode =
"{@param persons : ?}\n"
+ "{@param place : ?}\n"
+ " {msg desc=\"A sample plural message\"}\n"
+ " {plural length($persons) offset=\"0\"}\n"
+ " {case 0}I see no one in {$place}.\n"
+ " {case 1}I see {$persons[0]} in {$place}.\n"
+ " {default}"
+ " I see {length($persons)} persons in {$place}, including {$persons[0]}.\n"
+ " {/plural}\n"
+ " {/msg}\n";
expectedJsCode =
""
+ "/** @desc A sample plural message */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{NUM,plural,"
+ "=0{I see no one in {PLACE}.}"
+ "=1{I see {XXX_1} in {PLACE}.}"
+ "other{ I see {XXX_2} persons in {PLACE}, including {XXX_1}.}"
+ "}');\n"
+ "var msg_s = new goog.i18n.MessageFormat(MSG_UNNAMED).formatIgnoringPound("
+ "{'NUM': (opt_data.persons.length), "
+ "'PLACE': opt_data.place, "
+ "'XXX_1': opt_data.persons[0], "
+ "'XXX_2': (opt_data.persons.length)});\n"
+ "output += msg_s;\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
// With the plural variable used both as a placeholder and in remainder().
soyNodeCode =
"{@param persons : ?}\n"
+ "{@param num_people : ?}\n"
+ "{msg desc=\"A sample plural with offset\"}\n"
+ " {plural $num_people offset=\"2\"}\n"
+ " {case 0}No people.\n"
+ " {case 1}There is one person: {$persons[0]}.\n"
+ " {case 2}There are two persons: {$persons[0]} and {$persons[1]}.\n"
+ " {default}There are {$num_people} persons: "
+ "{$persons[0]}, {$persons[1]} and {remainder($num_people)} others.\n"
+ " {/plural}\n"
+ "{/msg}\n";
expectedJsCode =
""
+ "/** @desc A sample plural with offset */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{NUM_PEOPLE_1,plural,offset:2 "
+ "=0{No people.}"
+ "=1{There is one person: {XXX_1}.}"
+ "=2{There are two persons: {XXX_1} and {XXX_2}.}"
+ "other{There are {NUM_PEOPLE_2} persons: {XXX_1}, {XXX_2} and {XXX_3} others.}"
+ "}');\n"
+ "var msg_s = new goog.i18n.MessageFormat(MSG_UNNAMED).formatIgnoringPound("
+ "{'NUM_PEOPLE_1': opt_data.num_people, "
+ "'XXX_1': opt_data.persons[0], "
+ "'XXX_2': opt_data.persons[1], "
+ "'NUM_PEOPLE_2': opt_data.num_people, "
+ "'XXX_3': opt_data.num_people - 2});\n"
+ "output += msg_s;\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
}
@Test
public void testMsgWithSelect() {
// Simple select message: Gender with 'female' and other.
String soyNodeCode =
"{@param gender : ?}\n"
+ "{@param person : ?}\n"
+ "{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";
String expectedJsCode =
""
+ "/** @desc A sample gender message */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{GENDER,select,"
+ "female{{PERSON} added you to her circle.}"
+ "other{{PERSON} added you to his circle.}"
+ "}');\n"
+ "var msg_s = new goog.i18n.MessageFormat(MSG_UNNAMED).formatIgnoringPound("
+ "{'GENDER': opt_data.gender, "
+ "'PERSON': opt_data.person});\n"
+ "output += msg_s;\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
// Simple select message: Gender with 'female', 'male', 'neuter' and other.
soyNodeCode =
"{@param gender : ?}\n"
+ "{@param person : ?}\n"
+ "{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";
expectedJsCode =
""
+ "/** @desc A sample gender message */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{GENDER,select,"
+ "female{{PERSON} added you to her circle.}"
+ "male{{PERSON} added you to his circle.}"
+ "neuter{{PERSON} added you to its circle.}"
+ "other{{PERSON} added you to his circle.}"
+ "}');\n"
+ "var msg_s = new goog.i18n.MessageFormat(MSG_UNNAMED).formatIgnoringPound("
+ "{'GENDER': opt_data.gender, "
+ "'PERSON': opt_data.person});\n"
+ "output += msg_s;\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
}
@Test
public void testPlrselMsgWithFallback() {
String soyCode =
""
+ "{@param userGender : ?}\n"
+ "{@param targetGender : ?}\n"
+ "{@param targetName : ?}\n"
+ "{msg genders=\"$userGender, $targetGender\" desc=\"A message with genders.\"}\n"
+ " Join {$targetName}'s community.\n"
+ "{fallbackmsg desc=\"A message without genders.\"}\n"
+ " Join {$targetName}'s community.\n"
+ "{/msg}\n";
String expectedJsCode =
""
+ "/** @desc A message with genders. */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{USER_GENDER,select,"
+ "female{{TARGET_GENDER,select,"
+ "female{Join {TARGET_NAME}\\'s community.}"
+ "male{Join {TARGET_NAME}\\'s community.}"
+ "other{Join {TARGET_NAME}\\'s community.}"
+ "}}"
+ "male{{TARGET_GENDER,select,"
+ "female{Join {TARGET_NAME}\\'s community.}"
+ "male{Join {TARGET_NAME}\\'s community.}"
+ "other{Join {TARGET_NAME}\\'s community.}"
+ "}}"
+ "other{{TARGET_GENDER,select,"
+ "female{Join {TARGET_NAME}\\'s community.}"
+ "male{Join {TARGET_NAME}\\'s community.}"
+ "other{Join {TARGET_NAME}\\'s community.}"
+ "}}"
+ "}');\n"
+ "/** @desc A message without genders. */\n"
+ "var MSG_UNNAMED$$1 = goog.getMsg("
+ "'Join {$targetName}\\'s community.', "
+ "{'targetName': opt_data.targetName});\n"
+ "var msg_s = goog.getMsgWithFallback(MSG_UNNAMED, MSG_UNNAMED$$1);\n"
+ "if (msg_s == MSG_UNNAMED) {\n"
+ " msg_s = new goog.i18n.MessageFormat(MSG_UNNAMED).formatIgnoringPound("
+ "{'USER_GENDER': opt_data.userGender, "
+ "'TARGET_GENDER': opt_data.targetGender, "
+ "'TARGET_NAME': opt_data.targetName});\n"
+ "}\n"
+ "output += msg_s;\n";
// Note: Using getGeneratedJsCode() directly so that ids are not replaced with ###.
assertThat(getGeneratedJsCode(soyCode, ExplodingErrorReporter.get())).isEqualTo(expectedJsCode);
}
@Test
public void testMsgWithNestedSelectPlural() {
// Select nested inside select.
String soyNodeCode =
"{@param gender : ?}\n"
+ "{@param gender2 : ?}\n"
+ "{@param person1 : ?}\n"
+ "{@param person2 : ?}\n"
+ "{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";
String expectedJsCode =
""
+ "/** @desc A sample nested message */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{GENDER,select,"
+ "female{"
+ "{GENDER_2,select,"
+ "female{{PERSON_1} added {PERSON_2} and her friends to her circle.}"
+ "other{{PERSON_1} added {PERSON_2} and his friends to her circle.}"
+ "}"
+ "}"
+ "other{"
+ "{GENDER_2,select,"
+ "female{{PERSON_1} added {PERSON_2} and her friends to his circle.}"
+ "other{{PERSON_1} added {PERSON_2} and his friends to his circle.}"
+ "}"
+ "}"
+ "}');\n"
+ "var msg_s = new goog.i18n.MessageFormat(MSG_UNNAMED).formatIgnoringPound("
+ "{'GENDER': opt_data.gender, "
+ "'GENDER_2': opt_data.gender2, "
+ "'PERSON_1': opt_data.person1, "
+ "'PERSON_2': opt_data.person2});\n"
+ "output += msg_s;\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
// Plural nested inside select.
soyNodeCode =
"{@param gender : ?}\n"
+ "{@param person : ?}\n"
+ "{@param num_people : ?}\n"
+ "{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";
expectedJsCode =
""
+ "/** @desc A sample nested message */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{GENDER,select,"
+ "female{"
+ "{NUM_PEOPLE_1,plural,"
+ "=1{{PERSON} added one person to her circle.}"
+ "other{{PERSON} added {NUM_PEOPLE_2} to her circle.}"
+ "}"
+ "}"
+ "other{"
+ "{NUM_PEOPLE_1,plural,"
+ "=1{{PERSON} added one person to his circle.}"
+ "other{{PERSON} added {NUM_PEOPLE_2} to his circle.}"
+ "}"
+ "}"
+ "}');\n"
+ "var msg_s = new goog.i18n.MessageFormat(MSG_UNNAMED).formatIgnoringPound("
+ "{'GENDER': opt_data.gender, "
+ "'NUM_PEOPLE_1': opt_data.num_people, "
+ "'PERSON': opt_data.person, "
+ "'NUM_PEOPLE_2': opt_data.num_people});\n"
+ "output += msg_s;\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
// Plural inside plural should be invalid.
soyNodeCode =
"{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";
assertFailsInGeneratingJsCode(soyNodeCode, null);
// Select inside plural should be invalid.
soyNodeCode =
"{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";
assertFailsInGeneratingJsCode(soyNodeCode, null);
}
@Test
public void testMsgWithCollidingPlrsel() {
// Nested selects, inside a select, with variables all resolving to the same base name "gender".
String soyNodeCode =
"{@param format : ?}\n"
+ "{@param user : ?}\n"
+ "{@param person1 : ?}\n"
+ "{@param person2 : ?}\n"
+ "{@param friend : ?}\n"
+ "{msg desc=\"A sample nested message\"}\n"
+ " {select $format}\n"
+ " {case 'user-centric'}\n"
+ " {select $user.gender}\n"
+ " {case 'female'}{$person1} added {$person2} and friends to her circle.\n"
+ " {default}{$person1} added {$person2} and friends to his circle.\n"
+ " {/select}\n"
+ " {case 'friend-centric'}\n"
+ " {select $friend.gender}\n"
+ " {case 'female'}{$person1} added {$person2} and her friends to circle.\n"
+ " {default}{$person1} added {$person2} and his friends to circle.\n"
+ " {/select}\n"
+ " {default}\n"
+ " {select $user.gender}\n"
+ " {case 'female'}{$person1} added {$person2} and some friends to her circle.\n"
+ " {default}{$person1} added {$person2} and some friends to his circle.\n"
+ " {/select}\n"
+ " {/select}\n"
+ "{/msg}\n";
String expectedJsCode =
""
+ "/** @desc A sample nested message */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{FORMAT,select,"
+ "user-centric{"
+ "{GENDER_1,select,"
+ "female{{PERSON_1} added {PERSON_2} and friends to her circle.}"
+ "other{{PERSON_1} added {PERSON_2} and friends to his circle.}"
+ "}"
+ "}"
+ "friend-centric{"
+ "{GENDER_2,select,"
+ "female{{PERSON_1} added {PERSON_2} and her friends to circle.}"
+ "other{{PERSON_1} added {PERSON_2} and his friends to circle.}"
+ "}"
+ "}"
+ "other{"
+ "{GENDER_1,select,"
+ "female{{PERSON_1} added {PERSON_2} and some friends to her circle.}"
+ "other{{PERSON_1} added {PERSON_2} and some friends to his circle.}"
+ "}"
+ "}"
+ "}');\n"
+ "var msg_s = new goog.i18n.MessageFormat(MSG_UNNAMED).formatIgnoringPound("
+ "{'FORMAT': opt_data.format, "
+ "'GENDER_1': opt_data.user.gender, "
+ "'GENDER_2': opt_data.friend.gender, "
+ "'PERSON_1': opt_data.person1, "
+ "'PERSON_2': opt_data.person2});\n"
+ "output += msg_s;\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
// Selects nested inside a select, with conflicting top-level names.
// (This doesn't have conflict. It is just another test.)
soyNodeCode =
"{@param format : ?}\n"
+ "{@param gender : ?}\n"
+ "{@param person1 : ?}\n"
+ "{@param person2 : ?}\n"
+ "{msg desc=\"A sample nested message\"}\n"
+ " {select $format}\n"
+ " {case 'user-centric'}\n"
+ " {select $gender.user}\n"
+ " {case 'female'}{$person1} added {$person2} and friends to her circle.\n"
+ " {default}{$person1} added {$person2} and friends to his circle.\n"
+ " {/select}\n"
+ " {case 'friend-centric'}\n"
+ " {select $gender.friend}\n"
+ " {case 'female'}{$person1} added {$person2} and her friends to circle.\n"
+ " {default}{$person1} added {$person2} and his friends to circle.\n"
+ " {/select}\n"
+ " {default}\n"
+ " {select $gender.user}\n"
+ " {case 'female'}{$person1} added {$person2} and some friends to her circle.\n"
+ " {default}{$person1} added {$person2} and some friends to his circle.\n"
+ " {/select}\n"
+ " {/select}\n"
+ "{/msg}\n";
expectedJsCode =
""
+ "/** @desc A sample nested message */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{FORMAT,select,"
+ "user-centric{"
+ "{USER,select,"
+ "female{{PERSON_1} added {PERSON_2} and friends to her circle.}"
+ "other{{PERSON_1} added {PERSON_2} and friends to his circle.}"
+ "}"
+ "}"
+ "friend-centric{"
+ "{FRIEND,select,"
+ "female{{PERSON_1} added {PERSON_2} and her friends to circle.}"
+ "other{{PERSON_1} added {PERSON_2} and his friends to circle.}"
+ "}"
+ "}"
+ "other{"
+ "{USER,select,"
+ "female{{PERSON_1} added {PERSON_2} and some friends to her circle.}"
+ "other{{PERSON_1} added {PERSON_2} and some friends to his circle.}"
+ "}"
+ "}"
+ "}');\n"
+ "var msg_s = new goog.i18n.MessageFormat(MSG_UNNAMED).formatIgnoringPound("
+ "{'FORMAT': opt_data.format, "
+ "'USER': opt_data.gender.user, "
+ "'FRIEND': opt_data.gender.friend, "
+ "'PERSON_1': opt_data.person1, "
+ "'PERSON_2': opt_data.person2});\n"
+ "output += msg_s;\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
// Similar message as the previous, but the variables are complex, falling
// back to default naming.
soyNodeCode =
"{@param format : ?}\n"
+ "{@param gender : ?}\n"
+ "{@param person1 : ?}\n"
+ "{@param person2 : ?}\n"
+ "{msg desc=\"A sample nested message\"}\n"
+ " {select $format}\n"
+ " {case 'user-centric'}\n"
+ " {select $gender[0]}\n"
+ " {case 'female'}{$person1} added {$person2} and friends to her circle.\n"
+ " {default}{$person1} added {$person2} and friends to his circle.\n"
+ " {/select}\n"
+ " {case 'friend-centric'}\n"
+ " {select $gender[1]}\n"
+ " {case 'female'}{$person1} added {$person2} and her friends to circle.\n"
+ " {default}{$person1} added {$person2} and his friends to circle.\n"
+ " {/select}\n"
+ " {default}\n"
+ " {select $gender[0]}\n"
+ " {case 'female'}{$person1} added {$person2} and some friends to her circle.\n"
+ " {default}{$person1} added {$person2} and some friends to his circle.\n"
+ " {/select}\n"
+ " {/select}\n"
+ "{/msg}\n";
expectedJsCode =
""
+ "/** @desc A sample nested message */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{FORMAT,select,"
+ "user-centric{"
+ "{STATUS_1,select,"
+ "female{{PERSON_1} added {PERSON_2} and friends to her circle.}"
+ "other{{PERSON_1} added {PERSON_2} and friends to his circle.}"
+ "}"
+ "}"
+ "friend-centric{"
+ "{STATUS_2,select,"
+ "female{{PERSON_1} added {PERSON_2} and her friends to circle.}"
+ "other{{PERSON_1} added {PERSON_2} and his friends to circle.}"
+ "}"
+ "}"
+ "other{"
+ "{STATUS_1,select,"
+ "female{{PERSON_1} added {PERSON_2} and some friends to her circle.}"
+ "other{{PERSON_1} added {PERSON_2} and some friends to his circle.}"
+ "}"
+ "}"
+ "}');\n"
+ "var msg_s = new goog.i18n.MessageFormat(MSG_UNNAMED).formatIgnoringPound("
+ "{'FORMAT': opt_data.format, "
+ "'STATUS_1': opt_data.gender[0], "
+ "'STATUS_2': opt_data.gender[1], "
+ "'PERSON_1': opt_data.person1, "
+ "'PERSON_2': opt_data.person2});\n"
+ "output += msg_s;\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
// Plurals, nested inside a select, with plural name fallbacks.
soyNodeCode =
"{@param person : ?}\n"
+ "{@param values : ?}\n"
+ "{msg desc=\"A sample nested message\"}\n"
+ " {select $values.gender[0]}\n"
+ " {case 'female'}\n"
+ " {plural $values.people[0]}\n"
+ " {case 1}{$person} added one person to her circle.\n"
+ " {default}{$person} added {$values.people[0]} to her circle.\n"
+ " {/plural}\n"
+ " {case 'male'}\n"
+ " {plural $values.people[1]}\n"
+ " {case 1}{$person} added one person to his circle.\n"
+ " {default}{$person} added {$values.people[1]} to his circle.\n"
+ " {/plural}\n"
+ " {default}\n"
+ " {plural $values.people[1]}\n"
+ " {case 1}{$person} added one person to his/her circle.\n"
+ " {default}{$person} added {$values.people[1]} to his/her circle.\n"
+ " {/plural}\n"
+ " {/select}\n"
+ "{/msg}\n";
expectedJsCode =
""
+ "/** @desc A sample nested message */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{STATUS,select,"
+ "female{"
+ "{NUM_1,plural,"
+ "=1{{PERSON} added one person to her circle.}"
+ "other{{PERSON} added {XXX_1} to her circle.}"
+ "}"
+ "}"
+ "male{"
+ "{NUM_2,plural,"
+ "=1{{PERSON} added one person to his circle.}"
+ "other{{PERSON} added {XXX_2} to his circle.}"
+ "}"
+ "}"
+ "other{"
+ "{NUM_2,plural,"
+ "=1{{PERSON} added one person to his/her circle.}"
+ "other{{PERSON} added {XXX_2} to his/her circle.}"
+ "}"
+ "}"
+ "}');\n"
+ "var msg_s = new goog.i18n.MessageFormat(MSG_UNNAMED).formatIgnoringPound("
+ "{'STATUS': opt_data.values.gender[0], "
+ "'NUM_1': opt_data.values.people[0], "
+ "'NUM_2': opt_data.values.people[1], "
+ "'PERSON': opt_data.person, "
+ "'XXX_1': opt_data.values.people[0], "
+ "'XXX_2': opt_data.values.people[1]});\n"
+ "output += msg_s;\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
// Plurals nested inside select, with conflicts between select var name, plural var names
// and placeholder names.
soyNodeCode =
"{@param gender : ?}\n"
+ "{@param person : ?}\n"
+ "{@param number : ?}\n"
+ "{@param user : ?}\n"
+ "{msg desc=\"A sample nested message\"}\n"
+ " {select $gender.person}\n"
+ " {case 'female'}\n"
+ " {plural $number.person}\n"
+ " {case 1}{$person} added one person to her circle.\n"
+ " {default}{$user.person} added {$number.person} to her circle.\n"
+ " {/plural}\n"
+ " {default}\n"
+ " {plural $number.person}\n"
+ " {case 1}{$person} added one person to his/her circle.\n"
+ " {default}{$user.person} added {$number.person} to his/her circle.\n"
+ " {/plural}\n"
+ " {/select}\n"
+ "{/msg}\n";
expectedJsCode =
""
+ "/** @desc A sample nested message */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{PERSON_1,select,"
+ "female{"
+ "{PERSON_2,plural,"
+ "=1{{PERSON_3} added one person to her circle.}"
+ "other{{PERSON_4} added {PERSON_5} to her circle.}"
+ "}"
+ "}"
+ "other{"
+ "{PERSON_2,plural,"
+ "=1{{PERSON_3} added one person to his/her circle.}"
+ "other{{PERSON_4} added {PERSON_5} to his/her circle.}"
+ "}"
+ "}"
+ "}');\n"
+ "var msg_s = new goog.i18n.MessageFormat(MSG_UNNAMED).formatIgnoringPound("
+ "{'PERSON_1': opt_data.gender.person, "
+ "'PERSON_2': opt_data.number.person, "
+ "'PERSON_3': opt_data.person, "
+ "'PERSON_4': opt_data.user.person, "
+ "'PERSON_5': opt_data.number.person});\n"
+ "output += msg_s;\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
// Same as before, except that plural in one branch has offset, the other one doesn't.
// The result is the same.
soyNodeCode =
"{@param gender : ?}\n"
+ "{@param person : ?}\n"
+ "{@param number : ?}\n"
+ "{@param user : ?}\n"
+ "{msg desc=\"A sample nested message\"}\n"
+ " {select $gender.person}\n"
+ " {case 'female'}\n"
+ " {plural $number.person offset=\"1\"}\n"
+ " {case 1}{$person} added one person to her circle.\n"
+ " {default}{$user.person} added {remainder($number.person)} people to her "
+ "circle.\n"
+ " {/plural}\n"
+ " {default}\n"
+ " {plural $number.person}\n"
+ " {case 1}{$person} added one person to his/her circle.\n"
+ " {default}{$user.person} added {$number.person} people to his/her circle.\n"
+ " {/plural}\n"
+ " {/select}\n"
+ "{/msg}\n";
expectedJsCode =
""
+ "/** @desc A sample nested message */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{PERSON_1,select,"
+ "female{"
+ "{PERSON_2,plural,offset:1 "
+ "=1{{PERSON_4} added one person to her circle.}"
+ "other{{PERSON_5} added {XXX} people to her circle.}"
+ "}"
+ "}"
+ "other{"
+ "{PERSON_3,plural,"
+ "=1{{PERSON_4} added one person to his/her circle.}"
+ "other{{PERSON_5} added {PERSON_6} people to his/her circle.}"
+ "}"
+ "}"
+ "}');\n"
+ "var msg_s = new goog.i18n.MessageFormat(MSG_UNNAMED).formatIgnoringPound("
+ "{'PERSON_1': opt_data.gender.person, "
+ "'PERSON_2': opt_data.number.person, "
+ "'PERSON_3': opt_data.number.person, "
+ "'PERSON_4': opt_data.person, "
+ "'PERSON_5': opt_data.user.person, "
+ "'XXX': opt_data.number.person - 1, "
+ "'PERSON_6': opt_data.number.person});\n"
+ "output += msg_s;\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
// Select inside select with same variable
soyNodeCode =
"{@param friend : ?}\n"
+ "{@param person1 : ?}\n"
+ "{@param person2 : ?}\n"
+ "{@param user : ?}\n"
+ "{msg desc=\"A sample nested message\"}\n"
+ " {select $user.gender}\n"
+ " {case 'female'}\n"
+ " {select $user.gender}\n"
+ " {case 'female'}{$person1} added {$person2} and friends to her circle.\n"
+ " {default}{$person1} added {$person2} and friends to his circle.\n"
+ " {/select}\n"
+ " {default}\n"
+ " {select $friend.gender}\n"
+ " {case 'female'}{$person1} added {$person2} and some friends to her circle.\n"
+ " {default}{$person1} added {$person2} and some friends to his circle.\n"
+ " {/select}\n"
+ " {/select}\n"
+ "{/msg}\n";
expectedJsCode =
""
+ "/** @desc A sample nested message */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{GENDER_1,select,"
+ "female{"
+ "{GENDER_1,select,"
+ "female{{PERSON_1} added {PERSON_2} and friends to her circle.}"
+ "other{{PERSON_1} added {PERSON_2} and friends to his circle.}"
+ "}"
+ "}"
+ "other{"
+ "{GENDER_2,select,"
+ "female{{PERSON_1} added {PERSON_2} and some friends to her circle.}"
+ "other{{PERSON_1} added {PERSON_2} and some friends to his circle.}"
+ "}"
+ "}"
+ "}');\n"
+ "var msg_s = new goog.i18n.MessageFormat(MSG_UNNAMED).formatIgnoringPound("
+ "{'GENDER_1': opt_data.user.gender, "
+ "'GENDER_2': opt_data.friend.gender, "
+ "'PERSON_1': opt_data.person1, "
+ "'PERSON_2': opt_data.person2});\n"
+ "output += msg_s;\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
}
@Test
public void testMsgWithPlrselHtml() {
String soyNodeCode =
"{@param num : ?}\n"
+ "{msg desc=\"\"}\n"
+ " Notify \n"
+ " {sp}<span class=\"{css sharebox-id-email-number}\">{$num}</span>{sp}\n"
+ " people via email ›"
+ "{/msg}\n";
String expectedJsCode =
""
+ "/** @desc */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'Notify {$startSpan}{$num}{$endSpan} people via email ›', "
+ "{'startSpan': '<span class=\"'"
+ " + goog.getCssName('sharebox-id-email-number') + '\">', "
+ "'num': opt_data.num, "
+ "'endSpan': '</span>'});\n"
+ "output += MSG_UNNAMED;\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
soyNodeCode =
"{@param num : ?}\n"
+ "{msg desc=\"[ICU Syntax]\"}\n"
+ " {plural $num}\n"
+ " {case 0}"
+ " Notify people via email ›\n"
+ " {case 1}"
+ " Notify{sp}<span class=\"{css sharebox-id-email-number}\">{$num}</span>{sp}\n"
+ "person via email ›\n"
+ " {default}\n"
+ "Notify{sp}\n<span class=\"{css sharebox-id-email-number}\">"
+ "{$num}</span>{sp}\npeople via email ›\n"
+ " {/plural}\n"
+ "{/msg}\n";
expectedJsCode =
""
+ "/** @desc [ICU Syntax] */\n"
+ "var MSG_UNNAMED = goog.getMsg("
+ "'{NUM_1,plural,=0{ Notify people via email ›}"
+ "=1{ Notify {START_SPAN_1}{NUM_2}{END_SPAN} person via email ›}"
+ "other{Notify {START_SPAN_2}{NUM_2}{END_SPAN} people via email ›}}');\n"
+ "var msg_s = new goog.i18n.MessageFormat(MSG_UNNAMED).formatIgnoringPound("
+ "{'NUM_1': opt_data.num, "
+ "'START_SPAN_1': '<span class=\"' + goog.getCssName('sharebox-id-email-number') "
+ "+ '\">', "
+ "'NUM_2': opt_data.num, "
+ "'END_SPAN': '</span>', "
+ "'START_SPAN_2': '<span class=\"' + goog.getCssName('sharebox-id-email-number')"
+ " + '\">'});\n"
+ "output += msg_s;\n";
assertGeneratedJsCode(soyNodeCode, expectedJsCode);
}
@Test
public void testMsgWithInvalidPlrsel() {
// FAIL: Remainder variable different from plural variable.
String soyNodeCode =
" {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($n)} other people in {$place}.\n"
+ " {/plural}\n"
+ " {/msg}\n";
assertFailsInGeneratingJsCode(soyNodeCode, null);
// FAIL: Remainder in a plural variable with no offset.
soyNodeCode =
" {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"
+ " {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";
assertFailsInGeneratingJsCode(soyNodeCode, null);
// FAIL: Remainder in a plural variable with offset=0.
soyNodeCode =
" {msg desc=\"A sample plural message\"}\n"
+ " {plural $num_people offset=\"0\"}\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";
assertFailsInGeneratingJsCode(soyNodeCode, null);
// FAIL: Remainder variable and plural variable are different but have the same leaf name.
soyNodeCode =
" {msg desc=\"A sample plural message\"}\n"
+ " {plural $users.num offset=\"0\"}\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($friends.num)} other people in {$place}.\n"
+ " {/plural}\n"
+ " {/msg}\n";
assertFailsInGeneratingJsCode(soyNodeCode, null);
}
@Test
public void testStrictMode() {
String testFileContent =
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** Test template. */\n"
+ "{template .goo autoescape=\"strict\" kind=\"html\"}\n"
+ " Blah\n"
+ "{/template}\n";
TemplateNode template =
(TemplateNode)
SharedTestUtils.getNode(
SoyFileSetParserBuilder.forFileContents(testFileContent).parse().fileSet());
// ------ Code style 'concat' ------
String expectedJsCode =
""
+ "boo.foo.goo = function(opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " return soydata.VERY_UNSAFE.ordainSanitizedHtml('Blah');\n"
+ "};\n"
+ "if (goog.DEBUG) {\n"
+ " boo.foo.goo.soyTemplateName = 'boo.foo.goo';\n"
+ "}\n";
// Setup the GenJsCodeVisitor's state before the template is visited.
genJsCodeVisitor.jsCodeBuilder = new JsCodeBuilder();
genJsCodeVisitor.visitForTesting(template, ExplodingErrorReporter.get());
assertThat(genJsCodeVisitor.jsCodeBuilder.getCode()).isEqualTo(expectedJsCode);
}
// -----------------------------------------------------------------------------------------------
// Header params.
@Test
public void testHeaderParamsGeneratesJsRecordType() {
String testFileContent =
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** */\n"
+ "{template .goo}\n"
+ " {@param moo : string}\n"
+ " {@param goo : string|null}\n"
+ " {$moo}\n"
+ " {$goo}\n"
+ "{/template}\n";
ParseResult parseResult = SoyFileSetParserBuilder.forFileContents(testFileContent).parse();
jsSrcOptions.setShouldProvideRequireSoyNamespaces(true);
jsSrcOptions.setShouldGenerateJsdoc(true);
List<String> jsFilesContents =
genJsCodeVisitor.gen(
parseResult.fileSet(), parseResult.registry(), ExplodingErrorReporter.get());
// Ensure that the use of header params generates a record type for opt_data.
assertThat(jsFilesContents.get(0))
.contains(
Joiner.on('\n')
.join(
"@param {{",
" * moo: (!goog.soy.data.SanitizedContent|string),",
" * goo: (!goog.soy.data.SanitizedContent|null|string|undefined)",
" * }} opt_data"));
assertThat(jsFilesContents.get(0))
.contains(
Joiner.on('\n')
.join(
"@typedef {{",
" * moo: (!goog.soy.data.SanitizedContent|string),",
" * goo: (!goog.soy.data.SanitizedContent|null|string|undefined)",
" * }}",
" */",
"boo.foo.goo.Params;"));
}
@Test
public void testHeaderParamRequiresAssertsAndSanitizedData() {
ImmutableSet<String> symbols =
getRequiredSymbols(
"{namespace boo.foo}\n"
+ "\n"
+ "{template .goo}\n"
+ " {@param moo : string}\n"
+ " {$moo}\n"
+ "{/template}\n");
assertThat(symbols).contains("soy.asserts");
assertThat(symbols).contains("goog.soy.data.SanitizedContent");
}
@Test
public void testHeaderParamAggregateTypesPropagateRequiresFromMembers() {
assertThat(
getRequiredSymbols(
"{namespace boo.foo}\n"
+ "\n"
+ "{template .goo}\n"
+ " {@param moo : list<html>}\n"
+ " {$moo}\n"
+ "{/template}\n"))
.contains("goog.soy.data.SanitizedHtml");
assertThat(
getRequiredSymbols(
"{namespace boo.foo}\n"
+ "\n"
+ "{template .goo}\n"
+ " {@param moo : map<string, html>}\n"
+ " {$moo}\n"
+ "{/template}\n"))
.contains("goog.soy.data.SanitizedHtml");
assertThat(
getRequiredSymbols(
"{namespace boo.foo}\n"
+ "\n"
+ "{template .goo}\n"
+ " {@param moo : html|css}\n"
+ " {$moo}\n"
+ "{/template}\n"))
.containsAllOf("goog.soy.data.SanitizedHtml", "goog.soy.data.SanitizedCss");
}
@Test
public void testHeaderParamIntType() {
String testFileContent =
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** */\n"
+ "{template .goo}\n"
+ " {@param moo : int}\n"
+ " {$moo}\n"
+ "{/template}\n";
TemplateNode template =
(TemplateNode)
SharedTestUtils.getNode(
SoyFileSetParserBuilder.forFileContents(testFileContent).parse().fileSet());
String expectedJsCode =
""
+ "boo.foo.goo = function(opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " var moo = soy.asserts.assertType(goog.isNumber(opt_data.moo), 'moo', opt_data.moo, 'number');\n"
+ " return '' + moo;\n"
+ "};\n"
+ "if (goog.DEBUG) {\n"
+ " boo.foo.goo.soyTemplateName = 'boo.foo.goo';\n"
+ "}\n"
+ "";
// Setup the GenJsCodeVisitor's state before the template is visited.
genJsCodeVisitor.jsCodeBuilder = new JsCodeBuilder();
genJsCodeVisitor.visitForTesting(template, ExplodingErrorReporter.get());
assertThat(genJsCodeVisitor.jsCodeBuilder.getCode()).isEqualTo(expectedJsCode);
}
@Test
public void testHeaderParamStringType() {
String testFileContent =
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** */\n"
+ "{template .goo}\n"
+ " {@param moo : string}\n"
+ " {$moo}\n"
+ "{/template}\n";
TemplateNode template =
(TemplateNode)
SharedTestUtils.getNode(
SoyFileSetParserBuilder.forFileContents(testFileContent).parse().fileSet());
String expectedJsCode =
""
+ "boo.foo.goo = function(opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " var moo = soy.asserts.assertType(goog.isString(opt_data.moo) || opt_data.moo instanceof goog.soy.data.SanitizedContent, 'moo', opt_data.moo, '!goog.soy.data.SanitizedContent|string');\n"
+ " return '' + moo;\n"
+ "};\n"
+ "if (goog.DEBUG) {\n"
+ " boo.foo.goo.soyTemplateName = 'boo.foo.goo';\n"
+ "}\n"
+ "";
// Setup the GenJsCodeVisitor's state before the template is visited.
genJsCodeVisitor.jsCodeBuilder = new JsCodeBuilder();
genJsCodeVisitor.visitForTesting(template, ExplodingErrorReporter.get());
assertThat(genJsCodeVisitor.jsCodeBuilder.getCode()).isEqualTo(expectedJsCode);
}
@Test
public void testHeaderParamBoolType() {
String testFileContent =
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** */\n"
+ "{template .goo}\n"
+ " {@param moo : bool}\n"
+ " {@param? noo : bool}\n"
+ " {$moo ? 1 : 0}{$noo ? 1 : 0}\n"
+ "{/template}\n";
TemplateNode template =
(TemplateNode)
SharedTestUtils.getNode(
SoyFileSetParserBuilder.forFileContents(testFileContent).parse().fileSet());
String expectedJsCode =
""
+ "boo.foo.goo = function(opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " var moo = soy.asserts.assertType(goog.isBoolean(opt_data.moo) || opt_data.moo === 1 || opt_data.moo === 0, 'moo', opt_data.moo, 'boolean');\n"
+ " var noo = soy.asserts.assertType(opt_data.noo == null || (goog.isBoolean(opt_data.noo) || opt_data.noo === 1 || opt_data.noo === 0), 'noo', opt_data.noo, 'boolean|null|undefined');\n"
+ " return '' + (moo ? 1 : 0) + (noo ? 1 : 0);\n"
+ "};\n"
+ "if (goog.DEBUG) {\n"
+ " boo.foo.goo.soyTemplateName = 'boo.foo.goo';\n"
+ "}\n"
+ "";
// Setup the GenJsCodeVisitor's state before the template is visited.
genJsCodeVisitor.jsCodeBuilder = new JsCodeBuilder();
genJsCodeVisitor.visitForTesting(template, ExplodingErrorReporter.get());
assertThat(genJsCodeVisitor.jsCodeBuilder.getCode()).isEqualTo(expectedJsCode);
}
@Test
public void testHeaderParamSanitizedType() {
String testFileContent =
"{namespace boo.foo}\n"
+ "\n"
+ "{template .goo}\n"
+ " {@param html: html}\n"
+ " {$html}\n"
+ "{/template}\n";
TemplateNode template =
(TemplateNode)
SharedTestUtils.getNode(
SoyFileSetParserBuilder.forFileContents(testFileContent).parse().fileSet());
String expectedJsCode =
""
+ "boo.foo.goo = function(opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " var html = soy.asserts.assertType(goog.soy.data.SanitizedHtml.isCompatibleWith(opt_data.html), 'html', opt_data.html, '!goog.html.SafeHtml|!goog.soy.data.SanitizedHtml|!goog.soy.data.UnsanitizedText|string');\n"
+ " return soydata.VERY_UNSAFE.ordainSanitizedHtml(html);\n"
+ "};\n"
+ "if (goog.DEBUG) {\n"
+ " boo.foo.goo.soyTemplateName = 'boo.foo.goo';\n"
+ "}\n"
+ "";
// Setup the GenJsCodeVisitor's state before the template is visited.
genJsCodeVisitor.jsCodeBuilder = new JsCodeBuilder();
genJsCodeVisitor.visitForTesting(template, ExplodingErrorReporter.get());
assertThat(genJsCodeVisitor.jsCodeBuilder.getCode()).isEqualTo(expectedJsCode);
}
@Test
public void testHeaderParamUnionType() {
String testFileContent =
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** */\n"
+ "{template .goo}\n"
+ " {@param moo : string|list<int>}\n"
+ " {$moo}\n"
+ "{/template}\n";
TemplateNode template =
(TemplateNode)
SharedTestUtils.getNode(
SoyFileSetParserBuilder.forFileContents(testFileContent).parse().fileSet());
String expectedJsCode =
""
+ "boo.foo.goo = function(opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " var moo = soy.asserts.assertType(goog.isArray(opt_data.moo) || (goog.isString(opt_data.moo) || opt_data.moo instanceof goog.soy.data.SanitizedContent), 'moo', opt_data.moo, '!Array<number>|!goog.soy.data.SanitizedContent|string');\n"
+ " return '' + moo;\n"
+ "};\n"
+ "if (goog.DEBUG) {\n"
+ " boo.foo.goo.soyTemplateName = 'boo.foo.goo';\n"
+ "}\n"
+ "";
// Setup the GenJsCodeVisitor's state before the template is visited.
genJsCodeVisitor.jsCodeBuilder = new JsCodeBuilder();
genJsCodeVisitor.visitForTesting(template, ExplodingErrorReporter.get());
assertThat(genJsCodeVisitor.jsCodeBuilder.getCode()).isEqualTo(expectedJsCode);
}
@Test
public void testHeaderParamReservedWord() {
String testFileContent =
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** */\n"
+ "{template .goo}\n"
+ " {@param export : int}\n"
+ " {$export}\n"
+ "{/template}\n";
TemplateNode template =
(TemplateNode)
SharedTestUtils.getNode(
SoyFileSetParserBuilder.forFileContents(testFileContent).parse().fileSet());
String expectedJsCode =
""
+ "boo.foo.goo = function(opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " var param$export = soy.asserts.assertType(goog.isNumber(opt_data.export), 'export', opt_data.export, 'number');\n"
+ " return '' + param$export;\n"
+ "};\n"
+ "if (goog.DEBUG) {\n"
+ " boo.foo.goo.soyTemplateName = 'boo.foo.goo';\n"
+ "}\n"
+ "";
// Setup the GenJsCodeVisitor's state before the template is visited.
genJsCodeVisitor.jsCodeBuilder = new JsCodeBuilder();
genJsCodeVisitor.visitForTesting(template, ExplodingErrorReporter.get());
assertThat(genJsCodeVisitor.jsCodeBuilder.getCode()).isEqualTo(expectedJsCode);
}
@Test
public void testInjectedHeaderParamStringType() {
String testFileContent =
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** */\n"
+ "{template .goo}\n"
+ " {@inject moo : string}\n"
+ " {$moo}\n"
+ "{/template}\n";
TemplateNode template =
(TemplateNode)
SharedTestUtils.getNode(
SoyFileSetParserBuilder.forFileContents(testFileContent).parse().fileSet());
String expectedJsCode =
""
+ "boo.foo.goo = function(opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " var moo = soy.asserts.assertType(goog.isString(opt_ijData.moo) || opt_ijData.moo instanceof goog.soy.data.SanitizedContent, 'moo', opt_ijData.moo, '!goog.soy.data.SanitizedContent|string');\n"
+ " return '' + moo;\n"
+ "};\n"
+ "if (goog.DEBUG) {\n"
+ " boo.foo.goo.soyTemplateName = 'boo.foo.goo';\n"
+ "}\n"
+ "";
// Setup the GenJsCodeVisitor's state before the template is visited.
genJsCodeVisitor = INJECTOR.getInstance(GenJsCodeVisitor.class);
genJsCodeVisitor.jsCodeBuilder = new JsCodeBuilder();
genJsCodeVisitor.templateAliases = TEMPLATE_ALIASES;
genJsCodeVisitor.visitForTesting(template, ExplodingErrorReporter.get());
assertThat(genJsCodeVisitor.jsCodeBuilder.getCode()).isEqualTo(expectedJsCode);
}
@Test
public void testPrivateTemplateHasPrivateJsDocAnnotationInGencode() {
jsSrcOptions.setShouldGenerateJsdoc(true);
String testFileContent =
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** Test template. */\n"
+ "{template .goo visibility=\"private\"}\n"
+ " Blah\n"
+ "{/template}\n";
TemplateNode template =
(TemplateNode)
SharedTestUtils.getNode(
SoyFileSetParserBuilder.forFileContents(testFileContent).parse().fileSet());
String expectedJsCode =
""
+ "/**\n"
+ " * @param {Object<string, *>=} opt_data\n"
+ " * @param {Object<string, *>=} opt_ijData\n"
+ " * @param {Object<string, *>=} opt_ijData_deprecated\n"
+ " * @return {string}\n"
+ " * @suppress {checkTypes}\n"
+ " * @private\n"
+ " */\n"
+ "boo.foo.goo = function(opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " return 'Blah';\n"
+ "};\n"
+ "if (goog.DEBUG) {\n"
+ " boo.foo.goo.soyTemplateName = 'boo.foo.goo';\n"
+ "}\n";
// Setup the GenJsCodeVisitor's state before the template is visited.
genJsCodeVisitor.jsCodeBuilder = new JsCodeBuilder();
genJsCodeVisitor.visitForTesting(template, ExplodingErrorReporter.get());
assertThat(genJsCodeVisitor.jsCodeBuilder.getCode()).isEqualTo(expectedJsCode);
}
@Test
public void testLegacyPrivateTemplateDoesNotHavePrivateJsDocAnnotationInGencode() {
jsSrcOptions.setShouldGenerateJsdoc(true);
String testFileContent =
"{namespace boo.foo autoescape=\"deprecated-noncontextual\"}\n"
+ "\n"
+ "/** Test template. */\n"
+ "{template .goo private=\"true\"}\n"
+ " Blah\n"
+ "{/template}\n";
TemplateNode template =
(TemplateNode)
SharedTestUtils.getNode(
SoyFileSetParserBuilder.forFileContents(testFileContent).parse().fileSet());
String expectedJsCode =
""
+ "/**\n"
+ " * @param {Object<string, *>=} opt_data\n"
+ " * @param {Object<string, *>=} opt_ijData\n"
+ " * @param {Object<string, *>=} opt_ijData_deprecated\n"
+ " * @return {string}\n"
+ " * @suppress {checkTypes}\n"
+ " */\n"
+ "boo.foo.goo = function(opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " return 'Blah';\n"
+ "};\n"
+ "if (goog.DEBUG) {\n"
+ " boo.foo.goo.soyTemplateName = 'boo.foo.goo';\n"
+ "}\n";
// Setup the GenJsCodeVisitor's state before the template is visited.
genJsCodeVisitor.jsCodeBuilder = new JsCodeBuilder();
genJsCodeVisitor.visitForTesting(template, ExplodingErrorReporter.get());
assertThat(genJsCodeVisitor.jsCodeBuilder.getCode()).isEqualTo(expectedJsCode);
}
@Test
public void testGoogModuleGeneration() {
jsSrcOptions.setShouldDeclareTopLevelNamespaces(false);
jsSrcOptions.setShouldGenerateGoogModules(true);
String testFileContent =
""
+ "{namespace boo.foo}\n"
+ "\n"
+ "/** Test template. */\n"
+ "{template .goo}\n"
+ " {call boo.bar.one /}\n"
+ " {call boo.bar.two /}\n"
+ "{/template}\n";
String expectedJsCode =
""
+ "// This file was automatically generated from no-path.\n"
+ "// Please don't edit this file by hand.\n"
+ "\n"
+ "/**\n"
+ " * @fileoverview Templates in namespace boo.foo.\n"
+ " * @public\n"
+ " */\n"
+ "\n"
+ "goog.module('boo.foo');\n"
+ "\n"
+ "goog.require('soydata.VERY_UNSAFE');\n"
+ "var $import1 = goog.require('boo.bar');\n"
+ "var $templateAlias1 = $import1.one;\n"
+ "var $templateAlias2 = $import1.two;\n"
+ "\n"
+ "\n"
+ "function $goo(opt_data, opt_ijData, opt_ijData_deprecated) {\n"
+ " opt_ijData = opt_ijData_deprecated || opt_ijData;\n"
+ " return soydata.VERY_UNSAFE.ordainSanitizedHtml("
+ "$templateAlias1(null, null, opt_ijData) + "
+ "$templateAlias2(null, null, opt_ijData));\n"
+ "}\n"
+ "exports.goo = $goo;\n"
+ "if (goog.DEBUG) {\n"
+ " $goo.soyTemplateName = 'boo.foo.goo';\n"
+ "}\n";
ParseResult parseResult = SoyFileSetParserBuilder.forFileContents(testFileContent).parse();
assertThat(
genJsCodeVisitor
.gen(parseResult.fileSet(), parseResult.registry(), ExplodingErrorReporter.get())
.get(0))
.isEqualTo(expectedJsCode);
}
// -----------------------------------------------------------------------------------------------
// Helpers.
/**
* @param soyCode The soy code.
* @param expectedJsCode JavaScript code expected to be generated from the Soy code.
*/
private void assertGeneratedJsCode(String soyCode, String expectedJsCode) {
String genCode = getGeneratedJsCode(soyCode, ExplodingErrorReporter.get());
assertThat(genCode).isEqualTo(expectedJsCode);
}
/**
* Asserts that a soy code throws a SoySyntaxException.
*
* @param soyCode The invalid Soy code.
* @param expectedErrorMsg If not null, this is checked against the exception message.
*/
private void assertFailsInGeneratingJsCode(String soyCode, @Nullable String expectedErrorMsg) {
FormattingErrorReporter errorReporter = new FormattingErrorReporter();
String genCode = getGeneratedJsCode(soyCode, errorReporter);
if (errorReporter.getErrorMessages().isEmpty()) {
throw new AssertionError(
"Expected:\n" + soyCode + "\n to fail. But instead generated:\n" + genCode);
}
if (expectedErrorMsg != null && !errorReporter.getErrorMessages().contains(expectedErrorMsg)) {
throw new AssertionError(
"Expected:\n"
+ soyCode
+ "\n to fail with error:\""
+ expectedErrorMsg
+ "\". But instead failed with:"
+ errorReporter.getErrorMessages());
}
}
/**
* Generates JavaScript code from the given soy code.
*
* @param soyCode The Soy code.
*/
private String getGeneratedJsCode(String soyCode, ErrorReporter errorReporter) {
Checkpoint checkPoint = errorReporter.checkpoint();
ParseResult parseResult =
SoyFileSetParserBuilder.forTemplateContents(soyCode)
.errorReporter(errorReporter)
.allowUnboundGlobals(true)
.parse();
new ExtractMsgVariablesVisitor().exec(parseResult.fileSet());
if (errorReporter.errorsSince(checkPoint)) {
return null;
}
TemplateNode templateNode = parseResult.fileSet().getChild(0).getChild(0);
// Setup the GenJsCodeVisitor's state before the node is visited.
genJsCodeVisitor.jsCodeBuilder = new JsCodeBuilder();
genJsCodeVisitor.jsCodeBuilder.pushOutputVar("output");
genJsCodeVisitor.jsCodeBuilder.setOutputVarInited();
UniqueNameGenerator nameGenerator = JsSrcNameGenerators.forLocalVariables();
CodeChunk.Generator codeGenerator = CodeChunk.Generator.create(nameGenerator);
TranslationContext translationContext =
TranslationContext.of(
SoyToJsVariableMappings.startingWith(LOCAL_VAR_TRANSLATIONS),
codeGenerator,
nameGenerator);
genJsCodeVisitor.templateTranslationContext = translationContext;
genJsCodeVisitor.genJsExprsVisitor =
INJECTOR
.getInstance(GenJsExprsVisitorFactory.class)
.create(translationContext, TEMPLATE_ALIASES, errorReporter);
genJsCodeVisitor.assistantForMsgs = null; // will be created when used
for (SoyNode child : templateNode.getChildren()) {
genJsCodeVisitor.visitForTesting(child, errorReporter);
}
return genJsCodeVisitor.jsCodeBuilder.getCode();
}
private ImmutableSet<String> getRequiredSymbols(String soyFile) {
ParseResult parseResult = SoyFileSetParserBuilder.forFileContents(soyFile).parse();
jsSrcOptions.setShouldProvideRequireSoyNamespaces(true);
jsSrcOptions.setShouldGenerateJsdoc(true);
List<String> jsFilesContents =
genJsCodeVisitor.gen(
parseResult.fileSet(), parseResult.registry(), ExplodingErrorReporter.get());
Pattern googRequire = Pattern.compile("goog.require\\('(.*)'\\);");
ImmutableSet.Builder<String> requires = ImmutableSet.builder();
for (String file : jsFilesContents) {
Matcher matcher = googRequire.matcher(file);
while (matcher.find()) {
requires.add(matcher.group(1));
}
}
return requires.build();
}
}