/*
* Copyright 2009 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.template.soy.jssrc.dsl.CodeChunk.WithValue.LITERAL_EMPTY_STRING;
import static com.google.template.soy.jssrc.dsl.CodeChunk.WithValue.LITERAL_FALSE;
import static com.google.template.soy.jssrc.dsl.CodeChunk.WithValue.LITERAL_NULL;
import static com.google.template.soy.jssrc.dsl.CodeChunk.WithValue.LITERAL_TRUE;
import static com.google.template.soy.jssrc.dsl.CodeChunk.WithValue.id;
import static com.google.template.soy.jssrc.dsl.CodeChunk.fromExpr;
import static com.google.template.soy.jssrc.dsl.CodeChunk.stringLiteral;
import static com.google.template.soy.jssrc.internal.JsRuntime.SOY_ASSIGN_DEFAULTS;
import static com.google.template.soy.jssrc.internal.JsRuntime.SOY_GET_DELEGATE_FN;
import static com.google.template.soy.jssrc.internal.JsRuntime.sanitizedContentOrdainerFunctionForInternalBlocks;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.exprtree.ExprRootNode;
import com.google.template.soy.jssrc.dsl.CodeChunk;
import com.google.template.soy.jssrc.dsl.CodeChunk.RequiresCollector;
import com.google.template.soy.jssrc.dsl.CodeChunkUtils;
import com.google.template.soy.jssrc.dsl.GoogRequire;
import com.google.template.soy.jssrc.internal.GenJsExprsVisitor.GenJsExprsVisitorFactory;
import com.google.template.soy.jssrc.restricted.JsExpr;
import com.google.template.soy.jssrc.restricted.SoyJsSrcPrintDirective;
import com.google.template.soy.jssrc.restricted.SoyLibraryAssistedJsSrcPrintDirective;
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.CallParamNode;
import com.google.template.soy.soytree.CallParamValueNode;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
/**
* Generates JS code for {call}s and {delcall}s.
*
*/
public class GenCallCodeUtils {
/** All registered JS print directives. */
private final Map<String, SoyJsSrcPrintDirective> soyJsSrcDirectivesMap;
/** Instance of JsExprTranslator to use. */
private final JsExprTranslator jsExprTranslator;
/** Instance of DelTemplateNamer to use. */
private final DelTemplateNamer delTemplateNamer;
/** The IsComputableAsJsExprsVisitor used by this instance. */
private final IsComputableAsJsExprsVisitor isComputableAsJsExprsVisitor;
/** Factory for creating an instance of GenJsExprsVisitor. */
private final GenJsExprsVisitorFactory genJsExprsVisitorFactory;
/**
* @param soyJsSrcDirectivesMap Map of jssrc print directives to their names.
* @param jsExprTranslator Instance of JsExprTranslator to use.
* @param delTemplateNamer Renamer for delegate templates.
* @param isComputableAsJsExprsVisitor The IsComputableAsJsExprsVisitor to be used.
* @param genJsExprsVisitorFactory Factory for creating an instance of GenJsExprsVisitor.
*/
@Inject
protected GenCallCodeUtils(
Map<String, SoyJsSrcPrintDirective> soyJsSrcDirectivesMap,
JsExprTranslator jsExprTranslator,
DelTemplateNamer delTemplateNamer,
IsComputableAsJsExprsVisitor isComputableAsJsExprsVisitor,
GenJsExprsVisitorFactory genJsExprsVisitorFactory) {
this.jsExprTranslator = jsExprTranslator;
this.delTemplateNamer = delTemplateNamer;
this.isComputableAsJsExprsVisitor = isComputableAsJsExprsVisitor;
this.genJsExprsVisitorFactory = genJsExprsVisitorFactory;
this.soyJsSrcDirectivesMap = soyJsSrcDirectivesMap;
}
/**
* Generates the JS expression for a given call.
*
* <p> Important: If there are CallParamContentNode children whose contents are not computable as
* JS expressions, then this function assumes that, elsewhere, code has been generated to define
* their respective 'param<n>' temporary variables.
*
* <p> Here are five example calls:
* <pre>
* {call some.func data="all" /}
* {call some.func data="$boo.foo" /}
* {call some.func}
* {param goo = $moo /}
* {/call}
* {call some.func data="$boo"}
* {param goo}Blah{/param}
* {/call}
* {call some.func}
* {param goo}
* {for $i in range(3)}{$i}{/for}
* {/param}
* {/call}
* </pre>
* Their respective generated calls might be the following:
* <pre>
* some.func(opt_data)
* some.func(opt_data.boo.foo)
* some.func({goo: opt_data.moo})
* some.func(soy.$$assignDefaults({goo: 'Blah'}, opt_data.boo))
* some.func({goo: param65})
* </pre>
* Note that in the last case, the param content is not computable as JS expressions, so we assume
* that code has been generated to define the temporary variable 'param<n>'.
*
* @param callNode The call to generate code for.
* @param templateAliases A mapping of fully qualified calls to a variable in scope.
* @return The JS expression for the call.
*/
public CodeChunk.WithValue gen(
CallNode callNode,
TemplateAliases templateAliases,
TranslationContext translationContext,
ErrorReporter errorReporter) {
// Build the JS CodeChunk for the callee's name.
CodeChunk.WithValue callee;
if (callNode instanceof CallBasicNode) {
// Case 1: Basic call.
// TODO(lukes): add the logic for the goog.require here. The simplest strategy requires a
// TemplateRegistry to detect external templates.
callee =
CodeChunk.dottedIdNoRequire(
templateAliases.get(((CallBasicNode) callNode).getCalleeName()));
} else {
// Case 2: Delegate call.
CallDelegateNode callDelegateNode = (CallDelegateNode) callNode;
CodeChunk.WithValue calleeId =
JsRuntime.SOY_GET_DELTEMPLATE_ID.call(
stringLiteral(delTemplateNamer.getDelegateName(callDelegateNode)));
ExprRootNode variantSoyExpr = callDelegateNode.getDelCalleeVariantExpr();
CodeChunk.WithValue variant;
if (variantSoyExpr == null) {
// Case 2a: Delegate call with empty variant.
variant = LITERAL_EMPTY_STRING;
} else {
// Case 2b: Delegate call with variant expression.
variant = jsExprTranslator.translateToCodeChunk(
variantSoyExpr,
translationContext,
errorReporter);
}
callee =
SOY_GET_DELEGATE_FN.call(
calleeId,
variant,
callDelegateNode.allowsEmptyDefault() ? LITERAL_TRUE : LITERAL_FALSE);
}
// Generate the data object to pass to callee
CodeChunk.WithValue objToPass =
genObjToPass(callNode, templateAliases, translationContext, errorReporter);
// Generate the main call expression.
CodeChunk.WithValue call = callee.call(objToPass, LITERAL_NULL, JsRuntime.OPT_IJ_DATA);
if (callNode.getEscapingDirectiveNames().isEmpty()) {
return call;
}
// Apply escaping directives as necessary.
//
// The print directive system continues to use JsExpr, as it is a publicly available API and
// migrating it to CodeChunk would be a major change. Therefore, we convert our CodeChunks
// to JsExpr and back here.
JsExpr callResult = call.singleExprOrName();
RequiresCollector.IntoImmutableSet collector = new RequiresCollector.IntoImmutableSet();
call.collectRequires(collector);
for (String directiveName : callNode.getEscapingDirectiveNames()) {
SoyJsSrcPrintDirective directive = soyJsSrcDirectivesMap.get(directiveName);
Preconditions.checkNotNull(
directive, "Contextual autoescaping produced a bogus directive: %s", directiveName);
callResult = directive.applyForJsSrc(callResult, ImmutableList.<JsExpr>of());
if (directive instanceof SoyLibraryAssistedJsSrcPrintDirective) {
for (String name :
((SoyLibraryAssistedJsSrcPrintDirective) directive).getRequiredJsLibNames()) {
collector.add(GoogRequire.create(name));
}
}
}
return fromExpr(callResult, collector.get()).withInitialStatements(call.initialStatements());
}
/**
* Generates the JS expression for the object to pass in a given call.
*
* <p> Important: If there are CallParamContentNode children whose contents are not computable as
* JS expressions, then this function assumes that, elsewhere, code has been generated to define
* their respective 'param<n>' temporary variables.
*
* <p> Here are five example calls:
* <pre>
* {call some.func data="all" /}
* {call some.func data="$boo.foo" /}
* {call some.func}
* {param goo = $moo /}
* {/call}
* {call some.func data="$boo"}
* {param goo}Blah{/param}
* {/call}
* {call some.func}
* {param goo}
* {for $i in range(3)}{$i}{/for}
* {/param}
* {/call}
* </pre>
* Their respective objects to pass might be the following:
* <pre>
* opt_data
* opt_data.boo.foo
* {goo: opt_data.moo}
* soy.$$assignDefaults({goo: 'Blah'}, opt_data.boo)
* {goo: param65}
* </pre>
* Note that in the last case, the param content is not computable as JS expressions, so we assume
* that code has been generated to define the temporary variable 'param<n>'.
*
* @param callNode The call to generate code for.
* @param templateAliases A mapping of fully qualified calls to a variable in scope.
* @return The JS expression for the object to pass in the call.
*/
private CodeChunk.WithValue genObjToPass(
CallNode callNode,
TemplateAliases templateAliases,
TranslationContext translationContext,
ErrorReporter errorReporter) {
// ------ Generate the expression for the original data to pass ------
CodeChunk.WithValue dataToPass;
if (callNode.dataAttribute().isPassingAllData()) {
dataToPass = JsRuntime.OPT_DATA;
} else if (callNode.dataAttribute().isPassingData()) {
dataToPass =
jsExprTranslator.translateToCodeChunk(
callNode.dataAttribute().dataExpr(), translationContext, errorReporter);
} else {
dataToPass = LITERAL_NULL;
}
// ------ Case 1: No additional params ------
if (callNode.numChildren() == 0) {
return dataToPass;
}
// ------ Build an object literal containing the additional params ------
ImmutableList.Builder<CodeChunk.WithValue> keys = ImmutableList.builder();
ImmutableList.Builder<CodeChunk.WithValue> values = ImmutableList.builder();
for (CallParamNode child : callNode.getChildren()) {
keys.add(id(child.getKey()));
if (child instanceof CallParamValueNode) {
CallParamValueNode cpvn = (CallParamValueNode) child;
CodeChunk.WithValue value =
jsExprTranslator.translateToCodeChunk(
cpvn.getExpr(), translationContext, errorReporter);
values.add(value);
} else {
CallParamContentNode cpcn = (CallParamContentNode) child;
CodeChunk.WithValue content;
if (isComputableAsJsExprsVisitor.exec(cpcn)) {
List<CodeChunk.WithValue> chunks = genJsExprsVisitorFactory
.create(translationContext, templateAliases, errorReporter)
.exec(cpcn);
content = CodeChunkUtils.concatChunksForceString(chunks);
} else {
// This is a param with content that cannot be represented as JS expressions, so we assume
// that code has been generated to define the temporary variable 'param<n>'.
content = id("param" + cpcn.getId());
}
content = maybeWrapContent(translationContext.codeGenerator(), cpcn, content);
values.add(content);
}
}
CodeChunk.WithValue params = CodeChunk.mapLiteral(keys.build(), values.build());
// ------ Cases 2 and 3: Additional params with and without original data to pass ------
if (callNode.dataAttribute().isPassingData()) {
CodeChunk.WithValue allData = SOY_ASSIGN_DEFAULTS.call(params, dataToPass);
return allData;
} else {
return params;
}
}
/**
* If the param node had a content kind specified, it was autoescaped in the
* corresponding context. Hence the result of evaluating the param block is wrapped
* in a SanitizedContent instance of the appropriate kind.
* <p>
* The expression for the constructor of SanitizedContent of the appropriate kind (e.g.,
* "new SanitizedHtml"), or null if the node has no 'kind' attribute. This uses the
* variant used in internal blocks.
* </p>
*/
protected CodeChunk.WithValue maybeWrapContent(
CodeChunk.Generator generator, CallParamContentNode node, CodeChunk.WithValue content) {
if (node.getContentKind() == null) {
return content;
}
// Use the internal blocks wrapper, to maintain falsiness of empty string
return sanitizedContentOrdainerFunctionForInternalBlocks(node.getContentKind()).call(content);
}
}