/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.template.soy.jssrc.internal;
import static com.google.template.soy.jssrc.dsl.CodeChunk.WithValue.LITERAL_EMPTY_STRING;
import static com.google.template.soy.jssrc.dsl.CodeChunk.stringLiteral;
import static com.google.template.soy.jssrc.internal.JsRuntime.GOOG_GET_CSS_NAME;
import static com.google.template.soy.jssrc.internal.JsRuntime.XID;
import com.google.common.base.Preconditions;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.error.SoyErrorKind;
import com.google.template.soy.exprtree.ExprRootNode;
import com.google.template.soy.jssrc.dsl.CodeChunk;
import com.google.template.soy.jssrc.dsl.CodeChunkUtils;
import com.google.template.soy.jssrc.dsl.ConditionalExpressionBuilder;
import com.google.template.soy.jssrc.dsl.SoyJsPluginUtils;
import com.google.template.soy.jssrc.restricted.SoyJsSrcPrintDirective;
import com.google.template.soy.soytree.AbstractSoyNodeVisitor;
import com.google.template.soy.soytree.CallNode;
import com.google.template.soy.soytree.CallParamContentNode;
import com.google.template.soy.soytree.CssNode;
import com.google.template.soy.soytree.IfCondNode;
import com.google.template.soy.soytree.IfElseNode;
import com.google.template.soy.soytree.IfNode;
import com.google.template.soy.soytree.MsgHtmlTagNode;
import com.google.template.soy.soytree.MsgPlaceholderNode;
import com.google.template.soy.soytree.PrintDirectiveNode;
import com.google.template.soy.soytree.PrintNode;
import com.google.template.soy.soytree.RawTextNode;
import com.google.template.soy.soytree.SoyNode;
import com.google.template.soy.soytree.SoyNode.ParentSoyNode;
import com.google.template.soy.soytree.TemplateNode;
import com.google.template.soy.soytree.XidNode;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Visitor for generating JS expressions for parse tree nodes.
*
* <p> Precondition: MsgNode should not exist in the tree.
*
*/
public class GenJsExprsVisitor extends AbstractSoyNodeVisitor<List<CodeChunk.WithValue>> {
/**
* Injectable factory for creating an instance of this class.
*/
public interface GenJsExprsVisitorFactory {
/**
* @param templateAliases A mapping for looking up the function name for a given fully
* qualified name.
*/
GenJsExprsVisitor create(
TranslationContext translationContext,
TemplateAliases templateAliases,
ErrorReporter errorReporter);
}
private static final SoyErrorKind ARITY_MISMATCH =
SoyErrorKind.of("Print directive ''{0}'' called with {1} arguments, expected {2}.");
private static final SoyErrorKind UNKNOWN_SOY_JS_SRC_PRINT_DIRECTIVE =
SoyErrorKind.of("Unknown SoyJsSrcPrintDirective ''{0}''.");
private final Map<String, SoyJsSrcPrintDirective> soyJsSrcDirectivesMap;
private final JsExprTranslator jsExprTranslator;
private final GenCallCodeUtils genCallCodeUtils;
protected final IsComputableAsJsExprsVisitor isComputableAsJsExprsVisitor;
private final GenJsExprsVisitorFactory genJsExprsVisitorFactory;
private final TranslationContext translationContext;
private final ErrorReporter errorReporter;
/** List to collect the results. */
protected List<CodeChunk.WithValue> chunks;
/**
* Used for looking up the local name for a given template call to a fully qualified template
* name.
*/
private final TemplateAliases templateAliases;
/**
* @param soyJsSrcDirectivesMap Map of all SoyJsSrcPrintDirectives (name to directive).
* @param jsExprTranslator Instance of JsExprTranslator to use.
* @param genCallCodeUtils Instance of GenCallCodeUtils to use.
* @param isComputableAsJsExprsVisitor The IsComputableAsJsExprsVisitor used by this instance
* (when needed).
* @param genJsExprsVisitorFactory Factory for creating an instance of GenJsExprsVisitor.
* @param templateAliases A mapping for looking up the function name for a given fully
* qualified name.
*/
@AssistedInject
public GenJsExprsVisitor(
Map<String, SoyJsSrcPrintDirective> soyJsSrcDirectivesMap,
JsExprTranslator jsExprTranslator,
GenCallCodeUtils genCallCodeUtils,
IsComputableAsJsExprsVisitor isComputableAsJsExprsVisitor,
GenJsExprsVisitorFactory genJsExprsVisitorFactory,
@Assisted TranslationContext translationContext,
@Assisted ErrorReporter errorReporter,
@Assisted TemplateAliases templateAliases) {
this.soyJsSrcDirectivesMap = soyJsSrcDirectivesMap;
this.jsExprTranslator = jsExprTranslator;
this.genCallCodeUtils = genCallCodeUtils;
this.isComputableAsJsExprsVisitor = isComputableAsJsExprsVisitor;
this.genJsExprsVisitorFactory = genJsExprsVisitorFactory;
this.translationContext = translationContext;
this.errorReporter = errorReporter;
this.templateAliases = templateAliases;
}
@Override public List<CodeChunk.WithValue> exec(SoyNode node) {
Preconditions.checkArgument(isComputableAsJsExprsVisitor.exec(node));
chunks = new ArrayList<>();
visit(node);
return chunks;
}
/**
* Executes this visitor on the children of the given node, without visiting the given node
* itself.
*/
public List<CodeChunk.WithValue> execOnChildren(ParentSoyNode<?> node) {
Preconditions.checkArgument(isComputableAsJsExprsVisitor.execOnChildren(node));
chunks = new ArrayList<>();
visitChildren(node);
return chunks;
}
// -----------------------------------------------------------------------------------------------
// Implementations for specific nodes.
@Override protected void visitTemplateNode(TemplateNode node) {
visitChildren(node);
}
/**
* Example:
* <pre>
* I'm feeling lucky!
* </pre>
* generates
* <pre>
* 'I\'m feeling lucky!'
* </pre>
*/
@Override protected void visitRawTextNode(RawTextNode node) {
chunks.add(stringLiteral(node.getRawText()));
}
@Override protected void visitMsgPlaceholderNode(MsgPlaceholderNode node) {
visitChildren(node);
}
/**
* Example:
* <pre>
* <a href="{$url}">
* </pre>
* might generate
* <pre>
* '<a href="' + opt_data.url + '">'
* </pre>
*/
@Override protected void visitMsgHtmlTagNode(MsgHtmlTagNode node) {
visitChildren(node);
}
/**
* Example:
* <pre>
* {$boo.foo}
* {$goo.moo + 5}
* </pre>
* might generate
* <pre>
* opt_data.boo.foo
* gooData4.moo + 5
* </pre>
*/
@Override protected void visitPrintNode(PrintNode node) {
CodeChunk.WithValue expr =
jsExprTranslator.translateToCodeChunk(node.getExpr(), translationContext, errorReporter);
// Process directives.
for (PrintDirectiveNode directiveNode : node.getChildren()) {
// Get directive.
SoyJsSrcPrintDirective directive = soyJsSrcDirectivesMap.get(directiveNode.getName());
if (directive == null) {
// TODO(lukes): this should be dead, delete it
errorReporter.report(
node.getSourceLocation(), UNKNOWN_SOY_JS_SRC_PRINT_DIRECTIVE, directiveNode.getName());
return;
}
// Get directive args.
List<ExprRootNode> argNodes = directiveNode.getArgs();
if (!directive.getValidArgsSizes().contains(argNodes.size())) {
// TODO(lukes): this should be dead, delete it
errorReporter.report(
node.getSourceLocation(),
ARITY_MISMATCH,
directiveNode.getName(),
argNodes.size(),
directive.getValidArgsSizes());
return;
}
// Convert args to CodeChunks.
List<CodeChunk.WithValue> argChunks = new ArrayList<>(argNodes.size());
for (ExprRootNode argNode : argNodes) {
argChunks.add(
jsExprTranslator.translateToCodeChunk(argNode, translationContext, errorReporter));
}
// Apply directive.
expr = SoyJsPluginUtils.applyDirective(
translationContext.codeGenerator(),
expr,
directive,
argChunks);
}
chunks.add(expr);
}
/**
* Example:
* <pre>
* {xid selected-option}
* </pre>
* might generate
* <pre>
* xid('selected-option')
* </pre>
*/
@Override protected void visitXidNode(XidNode node) {
chunks.add(XID.call(stringLiteral(node.getText())));
}
/**
* Note: We would only see a CssNode if the css-handling scheme is BACKEND_SPECIFIC.
* <p>
* Example:
* <pre>
* {css selected-option}
* {css $foo, bar}
* </pre>
* might generate
* <pre>
* goog.getCssName('selected-option')
* goog.getCssName(opt_data.foo, 'bar')
* </pre>
* </p>
*/
@Override protected void visitCssNode(CssNode node) {
List<CodeChunk.WithValue> args = new ArrayList<>();
ExprRootNode componentName = node.getComponentNameExpr();
if (componentName != null) {
args.add(
jsExprTranslator.translateToCodeChunk(componentName, translationContext, errorReporter));
}
args.add(stringLiteral(node.getSelectorText()));
chunks.add(GOOG_GET_CSS_NAME.call(args));
}
/**
* Example:
* <pre>
* {if $boo}
* AAA
* {elseif $foo}
* BBB
* {else}
* CCC
* {/if}
* </pre>
* might generate
* <pre>
* (opt_data.boo) ? AAA : (opt_data.foo) ? BBB : CCC
* </pre>
*/
@Override protected void visitIfNode(IfNode node) {
// Create another instance of this visitor class for generating JS expressions from children.
GenJsExprsVisitor genJsExprsVisitor =
genJsExprsVisitorFactory.create(translationContext, templateAliases, errorReporter);
CodeChunk.Generator generator = translationContext.codeGenerator();
List<CodeChunk.WithValue> ifs = new ArrayList<>();
List<CodeChunk.WithValue> thens = new ArrayList<>();
CodeChunk.WithValue trailingElse = null;
for (SoyNode child : node.getChildren()) {
if (child instanceof IfCondNode) {
IfCondNode ifCond = (IfCondNode) child;
ifs.add(
jsExprTranslator.translateToCodeChunk(
ifCond.getExpr(), translationContext, errorReporter));
thens.add(CodeChunkUtils.concatChunks(genJsExprsVisitor.exec(ifCond)));
} else if (child instanceof IfElseNode) {
trailingElse = CodeChunkUtils.concatChunks(genJsExprsVisitor.exec(child));
} else {
throw new AssertionError();
}
}
Preconditions.checkState(ifs.size() == thens.size());
ConditionalExpressionBuilder builder = CodeChunk.ifExpression(ifs.get(0), thens.get(0));
for (int i = 1; i < ifs.size(); i++) {
builder.elseif_(ifs.get(i), thens.get(i));
}
CodeChunk.WithValue ifChunk =
trailingElse != null
? builder.else_(trailingElse).build(generator)
: builder.else_(LITERAL_EMPTY_STRING).build(generator);
chunks.add(ifChunk);
}
@Override protected void visitIfCondNode(IfCondNode node) {
visitChildren(node);
}
@Override protected void visitIfElseNode(IfElseNode node) {
visitChildren(node);
}
/**
* Example:
* <pre>
* {call some.func data="all" /}
* {call some.func data="$boo.foo" /}
* {call some.func}
* {param key="goo" value="$moo" /}
* {/call}
* {call some.func data="$boo"}
* {param key="goo"}Blah{/param}
* {/call}
* </pre>
* might generate
* <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))
* </pre>
*/
@Override protected void visitCallNode(CallNode node) {
CodeChunk.WithValue call =
genCallCodeUtils.gen(node, templateAliases, translationContext, errorReporter);
chunks.add(call);
}
@Override protected void visitCallParamContentNode(CallParamContentNode node) {
visitChildren(node);
}
}