/*
* 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.incrementaldomsrc;
import static com.google.template.soy.incrementaldomsrc.IncrementalDomRuntime.INCREMENTAL_DOM_ATTR;
import static com.google.template.soy.incrementaldomsrc.IncrementalDomRuntime.INCREMENTAL_DOM_ELEMENT_CLOSE;
import static com.google.template.soy.incrementaldomsrc.IncrementalDomRuntime.INCREMENTAL_DOM_ELEMENT_OPEN;
import static com.google.template.soy.incrementaldomsrc.IncrementalDomRuntime.INCREMENTAL_DOM_ELEMENT_OPEN_END;
import static com.google.template.soy.incrementaldomsrc.IncrementalDomRuntime.INCREMENTAL_DOM_ELEMENT_OPEN_START;
import static com.google.template.soy.incrementaldomsrc.IncrementalDomRuntime.INCREMENTAL_DOM_TEXT;
import static com.google.template.soy.incrementaldomsrc.IncrementalDomRuntime.SOY_IDOM_PRINT;
import static com.google.template.soy.incrementaldomsrc.IncrementalDomRuntime.SOY_IDOM_RENDER_DYNAMIC_CONTENT;
import static com.google.template.soy.jssrc.dsl.CodeChunk.WithValue.LITERAL_EMPTY_STRING;
import static com.google.template.soy.jssrc.dsl.CodeChunk.declare;
import static com.google.template.soy.jssrc.dsl.CodeChunk.id;
import static com.google.template.soy.jssrc.dsl.CodeChunk.stringLiteral;
import static com.google.template.soy.jssrc.internal.JsRuntime.GOOG_ASSERTS_ASSERT;
import static com.google.template.soy.jssrc.internal.JsRuntime.GOOG_STRING_UNESCAPE_ENTITIES;
import static com.google.template.soy.jssrc.internal.JsRuntime.SOY_ESCAPE_HTML;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.template.soy.data.SanitizedContent.ContentKind;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.error.SoyErrorKind;
import com.google.template.soy.exprtree.ExprNode;
import com.google.template.soy.exprtree.OperatorNodes.NullCoalescingOpNode;
import com.google.template.soy.exprtree.StringNode;
import com.google.template.soy.exprtree.VarRefNode;
import com.google.template.soy.html.HtmlDefinitions;
import com.google.template.soy.html.IncrementalHtmlAttributeNode;
import com.google.template.soy.html.IncrementalHtmlCloseTagNode;
import com.google.template.soy.html.IncrementalHtmlOpenTagNode;
import com.google.template.soy.incrementaldomsrc.GenIncrementalDomExprsVisitor.GenIncrementalDomExprsVisitorFactory;
import com.google.template.soy.jssrc.SoyJsSrcOptions;
import com.google.template.soy.jssrc.dsl.CodeChunk;
import com.google.template.soy.jssrc.dsl.CodeChunk.Generator;
import com.google.template.soy.jssrc.dsl.CodeChunkUtils;
import com.google.template.soy.jssrc.internal.CanInitOutputVarVisitor;
import com.google.template.soy.jssrc.internal.GenCallCodeUtils;
import com.google.template.soy.jssrc.internal.GenJsCodeVisitor;
import com.google.template.soy.jssrc.internal.GenJsCodeVisitorAssistantForMsgs;
import com.google.template.soy.jssrc.internal.GenJsExprsVisitor;
import com.google.template.soy.jssrc.internal.IsComputableAsJsExprsVisitor;
import com.google.template.soy.jssrc.internal.JsCodeBuilder;
import com.google.template.soy.jssrc.internal.JsExprTranslator;
import com.google.template.soy.jssrc.internal.TemplateAliases;
import com.google.template.soy.jssrc.internal.TranslationContext;
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.HtmlContext;
import com.google.template.soy.soytree.IfNode;
import com.google.template.soy.soytree.LetContentNode;
import com.google.template.soy.soytree.MsgFallbackGroupNode;
import com.google.template.soy.soytree.MsgHtmlTagNode;
import com.google.template.soy.soytree.MsgPlaceholderNode;
import com.google.template.soy.soytree.PrintNode;
import com.google.template.soy.soytree.RawTextNode;
import com.google.template.soy.soytree.SoyNode;
import com.google.template.soy.soytree.SoyNode.ParentSoyNode;
import com.google.template.soy.soytree.SoyNode.RenderUnitNode;
import com.google.template.soy.soytree.SoyNode.StandaloneNode;
import com.google.template.soy.soytree.TemplateNode;
import com.google.template.soy.types.SoyType;
import com.google.template.soy.types.SoyTypeOps;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
import javax.inject.Inject;
/**
* Generates a series of JavaScript control statements and function calls for rendering one or more
* templates as HTML. This heavily leverages {@link GenJsCodeVisitor}, adding logic to print the
* function calls and changing how statements are combined.
*/
public final class GenIncrementalDomCodeVisitor extends GenJsCodeVisitor {
private static final SoyErrorKind PRINT_ATTR_INVALID_KIND =
SoyErrorKind.of(
"For Incremental DOM, '{print}' statements in attributes context can only be "
+ "of kind attributes (since they must compile to semantic attribute declarations)."
+ "{0} is not allowed.");
private static final SoyErrorKind PRINT_ATTR_INVALID_VALUE =
SoyErrorKind.of(
"Attribute values that cannot be evalutated to simple expressions are not yet supported "
+ "for Incremental DOM code generation.");
private static final SoyErrorKind NULL_COALESCING_NON_EMPTY =
SoyErrorKind.of(
"The only supported conditional for attribute and HTML values in incremental DOM is "
+ "'{'$value ?: '''''}'. The right operand must be empty.");
private static final String NAMESPACE_EXTENSION = ".incrementaldom";
private static final String KEY_ATTRIBUTE_NAME = "key";
@Inject
GenIncrementalDomCodeVisitor(
SoyJsSrcOptions jsSrcOptions,
JsExprTranslator jsExprTranslator,
IncrementalDomDelTemplateNamer incrementalDomDelTemplateNamer,
IncrementalDomGenCallCodeUtils genCallCodeUtils,
IsComputableAsIncrementalDomExprsVisitor isComputableAsJsExprsVisitor,
CanInitOutputVarVisitor canInitOutputVarVisitor,
GenIncrementalDomExprsVisitorFactory genIncrementalDomExprsVisitorFactory,
SoyTypeOps typeOps) {
super(
jsSrcOptions,
jsExprTranslator,
incrementalDomDelTemplateNamer,
genCallCodeUtils,
isComputableAsJsExprsVisitor,
canInitOutputVarVisitor,
genIncrementalDomExprsVisitorFactory,
typeOps);
}
@Override
protected JsCodeBuilder createCodeBuilder() {
return new IncrementalDomCodeBuilder();
}
@Override
protected IncrementalDomCodeBuilder createChildJsCodeBuilder() {
return new IncrementalDomCodeBuilder(getJsCodeBuilder());
}
@Override
protected IncrementalDomCodeBuilder getJsCodeBuilder() {
return (IncrementalDomCodeBuilder) super.getJsCodeBuilder();
}
/**
* Changes module namespaces, adding an extension of '.incrementaldom' to allow it to co-exist
* with templates generated by jssrc.
*/
@Override
protected String getGoogModuleNamespace(String soyNamespace) {
return soyNamespace + NAMESPACE_EXTENSION;
}
@Override
protected String getTemplateReturnType(TemplateNode node) {
// TODO(sparhami) need to deal with URI types properly (like the JS code gen does) so that the
// usage is safe. For now, don't include any return type so compilation will fail if someone
// tries to create a template of kind="uri".
if (node.getContentKind() == ContentKind.TEXT) {
return "string";
}
// This template does not return any content but rather contains Incremental DOM instructions.
return "void";
}
@Override
protected void visitTemplateNode(TemplateNode node) {
getJsCodeBuilder().setContentKind(node.getContentKind());
super.visitTemplateNode(node);
}
@Override
protected void generateFunctionBody(TemplateNode node) {
IncrementalDomCodeBuilder jsCodeBuilder = getJsCodeBuilder();
boolean isTextTemplate = isTextContent(node.getContentKind());
// Note: we do not try to combine this into a single return statement if the content is
// computable as a JsExpr. A JavaScript compiler, such as Closure Compiler, is able to perform
// the transformation.
if (isTextTemplate) {
jsCodeBuilder.appendLine("var output = '';");
// We do our own initialization, so mark it as such.
jsCodeBuilder.pushOutputVar("output").setOutputVarInited();
}
genParamTypeChecks(node);
visitChildren(node);
if (isTextTemplate) {
jsCodeBuilder.appendLine("return output;");
jsCodeBuilder.popOutputVar();
}
}
/**
* Visits the children of a ParentSoyNode. This function is overridden to not do all of the work
* that {@link GenJsCodeVisitor} does.
*/
@Override
protected void visitChildren(ParentSoyNode<?> node) {
for (SoyNode child : node.getChildren()) {
visit(child);
}
}
/**
* Generates the content of a {@code let} or {@code param} statement. For HTML and attribute
* let/param statements, the generated instructions inside the node are wrapped in a function
* which will be optionally passed to another template and invoked in the correct location. All
* other kinds of let statements are generated as a simple variable.
*/
private void visitLetParamContentNode(RenderUnitNode node, String generatedVarName) {
IncrementalDomCodeBuilder jsCodeBuilder = getJsCodeBuilder();
ContentKind prevContentKind = jsCodeBuilder.getContentKind();
// We do our own initialization, so mark it as such.
jsCodeBuilder.pushOutputVar(generatedVarName).setOutputVarInited();
jsCodeBuilder.setContentKind(node.getContentKind());
// The html transform step, performed by HTMLTransformVisitor, ensures that
// we always have a content kind specified.
Preconditions.checkState(node.getContentKind() != null);
switch (node.getContentKind()) {
case HTML:
case ATTRIBUTES:
jsCodeBuilder.appendLine("var " + generatedVarName, " = function() {");
jsCodeBuilder.increaseIndent();
visitChildren(node);
jsCodeBuilder.decreaseIndent();
jsCodeBuilder.appendLine("};");
break;
default:
jsCodeBuilder.append(declare(generatedVarName, LITERAL_EMPTY_STRING));
visitChildren(node);
break;
}
jsCodeBuilder.setContentKind(prevContentKind);
jsCodeBuilder.popOutputVar();
}
/**
* Generates the content of a {@code let} statement. For HTML and attribute let statements, the
* generated instructions inside the node are wrapped in a function which will be optionally
* passed to another template and invoked in the correct location. All other kinds of let/param
* statements are generated as a simple variable.
*/
@Override
protected void visitLetContentNode(LetContentNode node) {
// TODO(slaks): Call base class for non-HTML to get {msg} inlining.
String generatedVarName = node.getUniqueVarName();
visitLetParamContentNode(node, generatedVarName);
templateTranslationContext
.soyToJsVariableMappings()
.put(node.getVarName(), id(generatedVarName));
}
@Override
protected void visitCallParamContentNode(CallParamContentNode node) {
String generatedVarName = "param" + node.getId();
visitLetParamContentNode(node, generatedVarName);
}
@Override
protected void visitCallNode(CallNode node) {
// If this node has any CallParamContentNode children those contents are not computable as JS
// expressions, visit them to generate code to define their respective 'param<n>' variables.
for (CallParamNode child : node.getChildren()) {
if (child instanceof CallParamContentNode && !isComputableAsJsExprsVisitor.exec(child)) {
visit(child);
}
}
CodeChunk.WithValue call =
genCallCodeUtils.gen(node, templateAliases, templateTranslationContext, errorReporter);
switch (getJsCodeBuilder().getContentKind()) {
case ATTRIBUTES:
getJsCodeBuilder().append(call);
break;
case HTML:
Optional<ContentKind> kind = templateRegistry.getCallContentKind(node);
// We are in a type of compilation where we don't have information on external templates
// such as dynamic recompilation.
if (!kind.isPresent()) {
call = SOY_IDOM_RENDER_DYNAMIC_CONTENT.call(call);
} else if (isTextContent(kind.get())) {
call = generateTextCall(call);
}
getJsCodeBuilder().append(call);
break;
case JS:
case URI:
case TRUSTED_RESOURCE_URI:
case CSS:
case TEXT:
// If the current content kind (due to a let, param or template) is a text-like, simply
// concatentate the result of the call to the current output variable.
getJsCodeBuilder().addChunkToOutputVar(call);
break;
}
}
/**
* Generates calls in HTML/Attributes content as non-JsExprs, since Incremental DOM instructions
* are needed and not a JavaScript expression.
*/
@Override
protected void visitIfNode(IfNode node) {
IncrementalDomCodeBuilder jsCodeBuilder = getJsCodeBuilder();
ContentKind currentContentKind = jsCodeBuilder.getContentKind();
if (currentContentKind == ContentKind.ATTRIBUTES || currentContentKind == ContentKind.HTML) {
super.generateNonExpressionIfNode(node);
} else {
super.visitIfNode(node);
}
}
/**
* Generates a call to generate a text node, asserting that the value generated by the expression
* is not null. Generates code that looks like:
*
* <pre>
* var $tmp = foo;
* goog.asserts.assert($tmp != null);
* IncrementalDom.text($tmp);
* </pre>
*
* <p>If asserts are enabled, the expression evaluates to `foo`, as expressions in JavaScript
* evaluate to the right most comma-delimited part.
*
* <p>If asserts are not enabled and the assert part of the expression is dropped by a JavaScript
* compiler (e.g. Closure Compiler), then the expression simply becomes `foo`.
*/
private CodeChunk.WithValue generateTextCall(CodeChunk.WithValue textValue) {
Generator cg = templateTranslationContext.codeGenerator();
CodeChunk.WithValue var = cg.declare(textValue).ref();
return INCREMENTAL_DOM_TEXT
.call(var)
.withInitialStatements(
ImmutableList.of(
GOOG_ASSERTS_ASSERT.call(var.doubleNotEquals(CodeChunk.WithValue.LITERAL_NULL))));
}
/**
* Determines if a given type of content represents text or some sort of HTML.
*
* @param contentKind The kind of content to check.
* @return True if the content represents text, false otherwise.
*/
private boolean isTextContent(ContentKind contentKind) {
return contentKind != ContentKind.HTML && contentKind != ContentKind.ATTRIBUTES;
}
/**
* Visits the {@link IncrementalHtmlAttributeNode}. The attribute nodes will typically be children
* of the corresponding {@link IncrementalHtmlOpenTagNode} or in a let/param of kind attributes,
* e.g.
*
* <pre>
* {let $attrs kind="attributes"}
* attr="value"
* {/let}
* </pre>
*
* This method prints the attribute declaration calls. For example, given
*
* <pre>
* <div {if $condition}attr="value"{/if}>
* </pre>
*
* it would print the call to {@code incrementalDom.attr}, resulting in:
*
* <pre>
* if (condition) {
* IncrementalDom.attr(attr, "value");
* }
* </pre>
*/
@Override
protected void visitIncrementalHtmlAttributeNode(IncrementalHtmlAttributeNode node) {
IncrementalDomCodeBuilder jsCodeBuilder = getJsCodeBuilder();
jsCodeBuilder.append(
INCREMENTAL_DOM_ATTR.call(
stringLiteral(node.getName()), CodeChunkUtils.concatChunks(getAttributeValues(node))));
}
/** Returns a list of attribute values. */
private List<CodeChunk.WithValue> getAttributeValues(IncrementalHtmlAttributeNode node) {
if (node.getChildren().isEmpty()) {
// No attribute value, e.g. "<button disabled></button>". Need to put an empty string so that
// the runtime knows to create an attribute.
return ImmutableList.of(LITERAL_EMPTY_STRING);
}
if (!isComputableAsJsExprsVisitor.execOnChildren(node)) {
errorReporter.report(node.getSourceLocation(), PRINT_ATTR_INVALID_VALUE);
return ImmutableList.of();
}
return genJsExprsVisitor.execOnChildren(node);
}
/**
* Visits the subtree of a node and wraps the resulting code in a pair of {@code
* incrementalDom.elementOpenStart} and {@code incrementalDom.elementOpenEnd} calls.
*/
private void emitOpenStartEndAndVisitSubtree(IncrementalHtmlOpenTagNode node, String tagName) {
IncrementalDomCodeBuilder jsCodeBuilder = getJsCodeBuilder();
List<CodeChunk.WithValue> args = new ArrayList<>();
args.add(stringLiteral(tagName));
CodeChunk.WithValue keyValue = maybeGetKeyNodeValue(node);
if (keyValue != null) {
args.add(keyValue);
}
jsCodeBuilder.append(INCREMENTAL_DOM_ELEMENT_OPEN_START.call(args));
jsCodeBuilder.increaseIndentTwice();
visitChildren(node);
jsCodeBuilder.decreaseIndentTwice();
jsCodeBuilder.append(INCREMENTAL_DOM_ELEMENT_OPEN_END.call());
}
/**
* Visits an {@link IncrementalHtmlOpenTagNode}, which occurs when an HTML tag is opened with no
* conditional attributes. For example:
*
* <pre>
* <div attr="value" attr2="{$someVar}">...</div>
* </pre>
*
* generates
*
* <pre>
* IncrementalDom.elementOpen('div');
* IncrementalDom.attr('attr', 'value');
* IncrementalDom.attr('attr2', someVar);
* IncrementalDom.elementClose();
* </pre>
*/
@Override
protected void visitIncrementalHtmlOpenTagNode(IncrementalHtmlOpenTagNode node) {
IncrementalDomCodeBuilder jsCodeBuilder = getJsCodeBuilder();
if (node.getChildren().isEmpty()) {
List<CodeChunk.WithValue> args = new ArrayList<>();
args.add(stringLiteral(node.getTagName()));
CodeChunk.WithValue keyValue = maybeGetKeyNodeValue(node);
if (keyValue != null) {
args.add(keyValue);
}
jsCodeBuilder.append(INCREMENTAL_DOM_ELEMENT_OPEN.call(args));
} else {
emitOpenStartEndAndVisitSubtree(node, node.getTagName());
}
jsCodeBuilder.increaseIndent();
if (HtmlDefinitions.HTML5_VOID_ELEMENTS.contains(node.getTagName())) {
emitClose(node.getTagName());
}
}
/**
* Gets the 'key' for an element to use in Incremental DOM to be used in the {@code
* incrementalDom.elementOpen} or {@code incrementalDom.elementVoid} calls.
*
* <pre>
* <div key="test" /div>
* </pre>
*
* generates
*
* <pre>
* incrementalDom.elementVoid('div', 'test')
* </pre>
*
* @param parentNode The SoyNode representing the parent.
* @return A string containing the JavaScript expression to retrieve the key, or null if the
* parent has no attribute child.
*/
@Nullable
private CodeChunk.WithValue maybeGetKeyNodeValue(IncrementalHtmlOpenTagNode parentNode) {
for (StandaloneNode childNode : parentNode.getChildren()) {
if (!(childNode instanceof IncrementalHtmlAttributeNode)) {
continue;
}
IncrementalHtmlAttributeNode htmlAttributeNode = (IncrementalHtmlAttributeNode) childNode;
if (htmlAttributeNode.getName().equals(KEY_ATTRIBUTE_NAME)) {
Preconditions.checkState(
isComputableAsJsExprsVisitor.execOnChildren(htmlAttributeNode),
"Attribute values that cannot be evalutated to simple expressions is not yet supported "
+ "for Incremental DOM code generation");
List<CodeChunk.WithValue> chunks = genJsExprsVisitor.execOnChildren(htmlAttributeNode);
// OK to use concatChunks() instead of concatChunksForceString(), children are guaranteed
// to be string (RawTextNode or PrintNode)
return CodeChunkUtils.concatChunks(chunks);
}
}
return null;
}
/**
* Visits an {@link IncrementalHtmlCloseTagNode}, which occurs when an HTML tag is closed. For
* example:
*
* <pre>
* </div>
* </pre>
*
* generates
*
* <pre>
* incrementalDom.elementClose('div');
* </pre>
*/
@Override
protected void visitIncrementalHtmlCloseTagNode(IncrementalHtmlCloseTagNode node) {
if (!HtmlDefinitions.HTML5_VOID_ELEMENTS.contains(node.getTagName())) {
emitClose(node.getTagName());
}
}
/**
* Emits a close tag. For example:
*
* <pre>
* <incrementalDom.elementClose('div');>
* </pre>
*/
private void emitClose(String tagName) {
IncrementalDomCodeBuilder jsCodeBuilder = getJsCodeBuilder();
jsCodeBuilder.decreaseIndent();
jsCodeBuilder.append(INCREMENTAL_DOM_ELEMENT_CLOSE.call(stringLiteral(tagName)));
}
/**
* Visits a {@link RawTextNode}, which occurs either as a child of any BlockNode or the 'child' of
* an HTML tag. Note that in the soy tree, tags and their logical HTML children do not have a
* parent-child relationship, but are rather siblings. For example:
*
* <pre>
* <div>Hello world</div>
* </pre>
*
* The text "Hello world" translates to
*
* <pre>
* incrementalDom.text('Hello world');
* </pre>
*/
@Override
protected void visitRawTextNode(RawTextNode node) {
CodeChunk.WithValue textArg = stringLiteral(node.getRawText());
JsCodeBuilder jsCodeBuilder = getJsCodeBuilder();
if (node.getHtmlContext() == HtmlContext.HTML_PCDATA) {
// Note - we don't use generateTextCall since this text can never be null.
jsCodeBuilder.append(INCREMENTAL_DOM_TEXT.call(textArg));
} else {
jsCodeBuilder.addChunkToOutputVar(textArg);
}
}
/**
* Visit an {@link PrintNode}, with special cases for a variable being printed within an attribute
* declaration or as HTML content.
*
* <p>For attributes, if the variable is of kind attributes, it is invoked. Any other kind of
* variable is an error.
*
* <p>For HTML, if the variable is of kind HTML, it is invoked. Any other kind of variable gets
* wrapped in a call to {@code incrementalDom.text}, resulting in a Text node.
*/
@Override
protected void visitPrintNode(PrintNode node) {
ExprNode firstNode = node.getExpr().getRoot();
// TODO(sparhami): Raise an error if there are any directives.
switch (node.getHtmlContext()) {
case HTML_TAG:
if (tryGenerateFunctionCall(SoyType.Kind.ATTRIBUTES, firstNode)
== GenerateFunctionCallResult.INDIRECT_NODE) {
// Inside an HTML tag, we cannot emit indirect calls (like incrementalDom.text); the only
// valid commands
// are idom incrementalDom.attr() calls (which direct ATTRIBUTES functions will call).
// If we can't emit the print node as a direct call, give up and report an error.
errorReporter.report(
node.getSourceLocation(), PRINT_ATTR_INVALID_KIND, firstNode.getType().getKind());
}
break;
case HTML_PCDATA:
// If the expression is an HTML function, print() will call it.
// But if we statically know that it's an HTML function, we can call it directly.
if (tryGenerateFunctionCall(SoyType.Kind.HTML, firstNode)
== GenerateFunctionCallResult.INDIRECT_NODE) {
List<CodeChunk.WithValue> chunks = genJsExprsVisitor.exec(node);
CodeChunk.WithValue printCall = SOY_IDOM_PRINT.call(CodeChunkUtils.concatChunks(chunks));
JsCodeBuilder codeBuilder = getJsCodeBuilder();
codeBuilder.append(printCall);
}
break;
default:
super.visitPrintNode(node);
break;
}
}
private enum GenerateFunctionCallResult {
/** We emitted a direct call in jsCodeBuilder; no further action is necessary. */
EMITTED,
/** This node cannot be printed at all; we reported an error. No further action is necessary. */
ILLEGAL_NODE,
/** This node cannot be called directly, but it might be printable as dynamic text. */
INDIRECT_NODE
}
/**
* Emits a call to a value of type ATTRIBUTES or HTML, which is actually a JS function. Currently,
* the only supported expressions for this operation are direct variable references and {X ?: ''}.
*
* @param expectedKind The kind of content that the expression must match.
*/
private GenerateFunctionCallResult tryGenerateFunctionCall(
SoyType.Kind expectedKind, ExprNode expr) {
IncrementalDomCodeBuilder jsCodeBuilder = getJsCodeBuilder();
if (expr instanceof VarRefNode && expr.getType().getKind() == expectedKind) {
VarRefNode varRefNode = (VarRefNode) expr;
CodeChunk.WithValue call =
templateTranslationContext.soyToJsVariableMappings().get(varRefNode.getName()).call();
jsCodeBuilder.append(call);
return GenerateFunctionCallResult.EMITTED;
}
if (!(expr instanceof NullCoalescingOpNode)) {
return GenerateFunctionCallResult.INDIRECT_NODE;
}
// ResolveExpressionTypesVisitor will resolve {$attributes ?: ''} to String because '' is not of
// type ATTRIBUTES. Therefore, we must check the type of the first operand, not the whole node.
NullCoalescingOpNode opNode = (NullCoalescingOpNode) expr;
if (!(opNode.getLeftChild() instanceof VarRefNode)
|| !(opNode.getRightChild() instanceof StringNode)
|| opNode.getLeftChild().getType().getKind() != expectedKind) {
return GenerateFunctionCallResult.INDIRECT_NODE;
}
if (!((StringNode) opNode.getRightChild()).getValue().isEmpty()) {
errorReporter.report(expr.getSourceLocation(), NULL_COALESCING_NON_EMPTY);
return GenerateFunctionCallResult.ILLEGAL_NODE;
}
VarRefNode varRefNode = (VarRefNode) opNode.getLeftChild();
CodeChunk.WithValue varName =
templateTranslationContext.soyToJsVariableMappings().get(varRefNode.getName());
CodeChunk conditionalCall = CodeChunk.ifStatement(varName, varName.call()).build();
jsCodeBuilder.append(conditionalCall);
return GenerateFunctionCallResult.EMITTED;
}
@Override
protected void visitMsgFallbackGroupNode(MsgFallbackGroupNode node) {
String msgExpression;
switch (node.getHtmlContext()) {
case HTML_PCDATA:
new AssistantForHtmlMsgs(
this /* master */,
jsSrcOptions,
jsExprTranslator,
genCallCodeUtils,
isComputableAsJsExprsVisitor,
templateAliases,
genJsExprsVisitor,
templateTranslationContext,
errorReporter)
.generateMsgGroupCode(node);
break;
// Messages in attribute values are plain text. However, since the translated content
// includes entities (because other Soy backends treat these messages as HTML source), we
// must unescape the translations before passing them to the idom APIs.
case HTML_NORMAL_ATTR_VALUE:
msgExpression =
new AssistantForAttributeMsgs(
this /* master */,
jsSrcOptions,
jsExprTranslator,
genCallCodeUtils,
isComputableAsJsExprsVisitor,
templateAliases,
genJsExprsVisitor,
templateTranslationContext,
errorReporter)
.generateMsgGroupVariable(node);
getJsCodeBuilder()
.addChunkToOutputVar(GOOG_STRING_UNESCAPE_ENTITIES.call(id(msgExpression)));
break;
default:
msgExpression = getAssistantForMsgs().generateMsgGroupVariable(node);
getJsCodeBuilder().addChunkToOutputVar(id(msgExpression));
break;
}
}
@Override
protected void visitMsgHtmlTagNode(MsgHtmlTagNode node) {
visitChildren(node);
}
/**
* Handles <code>{msg}</code> commands in attribute context for idom. The literal text in the
* translated message must be unescaped after translation, because we pass the text directly to
* DOM text APIs, whereas translators write HTML with entities. Therefore, we must first escape
* all interpolated placeholders (which can only be TEXT values).
*
* <p>In non-idom, this happens in the contextual auto-escaper.
*/
private static final class AssistantForAttributeMsgs extends GenJsCodeVisitorAssistantForMsgs {
AssistantForAttributeMsgs(
GenIncrementalDomCodeVisitor master,
SoyJsSrcOptions jsSrcOptions,
JsExprTranslator jsExprTranslator,
GenCallCodeUtils genCallCodeUtils,
IsComputableAsJsExprsVisitor isComputableAsJsExprsVisitor,
TemplateAliases functionAliases,
GenJsExprsVisitor genJsExprsVisitor,
TranslationContext translationContext,
ErrorReporter errorReporter) {
super(
master,
jsSrcOptions,
jsExprTranslator,
genCallCodeUtils,
isComputableAsJsExprsVisitor,
functionAliases,
genJsExprsVisitor,
translationContext,
errorReporter);
}
@Override
protected CodeChunk.WithValue genGoogMsgPlaceholder(MsgPlaceholderNode msgPhNode) {
CodeChunk.WithValue toEscape = super.genGoogMsgPlaceholder(msgPhNode);
return SOY_ESCAPE_HTML.call(toEscape);
}
}
}