Package com.google.template.soy.jssrc.internal

Source Code of com.google.template.soy.jssrc.internal.TranslateToJsExprVisitor$TranslateToJsExprVisitorFactory

/*
* 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 com.google.common.collect.ImmutableSet;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import com.google.template.soy.base.BaseUtils;
import com.google.template.soy.base.SoySyntaxException;
import com.google.template.soy.exprtree.AbstractReturningExprNodeVisitor;
import com.google.template.soy.exprtree.DataRefAccessIndexNode;
import com.google.template.soy.exprtree.DataRefAccessKeyNode;
import com.google.template.soy.exprtree.DataRefAccessNode;
import com.google.template.soy.exprtree.DataRefNode;
import com.google.template.soy.exprtree.ExprNode;
import com.google.template.soy.exprtree.ExprNode.ConstantNode;
import com.google.template.soy.exprtree.ExprNode.OperatorNode;
import com.google.template.soy.exprtree.ExprNode.PrimitiveNode;
import com.google.template.soy.exprtree.ExprRootNode;
import com.google.template.soy.exprtree.FunctionNode;
import com.google.template.soy.exprtree.GlobalNode;
import com.google.template.soy.exprtree.ListLiteralNode;
import com.google.template.soy.exprtree.MapLiteralNode;
import com.google.template.soy.exprtree.Operator;
import com.google.template.soy.exprtree.OperatorNodes.AndOpNode;
import com.google.template.soy.exprtree.OperatorNodes.NotOpNode;
import com.google.template.soy.exprtree.OperatorNodes.OrOpNode;
import com.google.template.soy.exprtree.StringNode;
import com.google.template.soy.jssrc.SoyJsSrcOptions;
import com.google.template.soy.jssrc.restricted.JsExpr;
import com.google.template.soy.jssrc.restricted.SoyJsCodeUtils;
import com.google.template.soy.jssrc.restricted.SoyJsSrcFunction;
import com.google.template.soy.shared.internal.NonpluginFunction;

import java.util.Deque;
import java.util.List;
import java.util.Map;


/**
* Visitor for translating a Soy expression (in the form of an {@code ExprNode}) into an
* equivalent JS expression.
*
* <p> Important: Do not use outside of Soy code (treat as superpackage-private).
*
* @author Kai Huang
*/
public class TranslateToJsExprVisitor extends AbstractReturningExprNodeVisitor<JsExpr> {


  /**
   * Injectable factory for creating an instance of this class.
   */
  public static interface TranslateToJsExprVisitorFactory {

    /**
     * @param localVarTranslations The current stack of replacement JS expressions for the local
     *     variables (and foreach-loop special functions) current in scope.
     */
    public TranslateToJsExprVisitor create(Deque<Map<String, JsExpr>> localVarTranslations);
  }


  /** Map of all SoyJsSrcFunctions (name to function). */
  private final Map<String, SoyJsSrcFunction> soyJsSrcFunctionsMap;

  /** The options for generating JS source code. */
  private final SoyJsSrcOptions jsSrcOptions;

  /** The current stack of replacement JS expressions for the local variables (and foreach-loop
   *  special functions) current in scope. */
  private final Deque<Map<String, JsExpr>> localVarTranslations;


  /**
   * @param soyJsSrcFunctionsMap Map of all SoyJsSrcFunctions (name to function).
   * @param localVarTranslations The current stack of replacement JS expressions for the local
   *     variables (and foreach-loop special functions) current in scope.
   */
  @AssistedInject
  TranslateToJsExprVisitor(
      Map<String, SoyJsSrcFunction> soyJsSrcFunctionsMap, SoyJsSrcOptions jsSrcOptions,
      @Assisted Deque<Map<String, JsExpr>> localVarTranslations) {
    this.soyJsSrcFunctionsMap = soyJsSrcFunctionsMap;
    this.jsSrcOptions = jsSrcOptions;
    this.localVarTranslations = localVarTranslations;
  }


  // -----------------------------------------------------------------------------------------------
  // Implementation for a dummy root node.


  @Override protected JsExpr visitExprRootNode(ExprRootNode<?> node) {
    return visit(node.getChild(0));
  }


  // -----------------------------------------------------------------------------------------------
  // Implementations for primitives.


  @Override protected JsExpr visitStringNode(StringNode node) {

    // Note: StringNode.toSourceString() produces a Soy string, which is usually a valid JS string.
    // The rare exception is a string containing a Unicode Format character (Unicode category "Cf")
    // because of the JavaScript language quirk that requires all category "Cf" characters to be
    // escaped in JS strings. Therefore, we must call JsSrcUtils.escapeUnicodeFormatChars() on the
    // result.
    return new JsExpr(
        JsSrcUtils.escapeUnicodeFormatChars(node.toSourceString()),
        Integer.MAX_VALUE);
  }


  @Override protected JsExpr visitPrimitiveNode(PrimitiveNode node) {

    // Note: ExprNode.toSourceString() technically returns a Soy expression. In the case of
    // primitives, the result is usually also the correct JS expression.
    // Note: The rare exception to the above note is a StringNode containing a Unicode Format
    // character (Unicode category "Cf") because of the JavaScript language quirk that requires all
    // category "Cf" characters to be escaped in JS strings. Therefore, we have a separate
    // implementation above for visitStringNode(StringNode).
    return new JsExpr(node.toSourceString(), Integer.MAX_VALUE);
  }


  // -----------------------------------------------------------------------------------------------
  // Implementations for collections.


  @Override protected JsExpr visitListLiteralNode(ListLiteralNode node) {

    StringBuilder exprTextSb = new StringBuilder();
    exprTextSb.append('[');

    boolean isFirst = true;
    for (ExprNode child : node.getChildren()) {
      if (isFirst) {
        isFirst = false;
      } else {
        exprTextSb.append(", ");
      }
      exprTextSb.append(visit(child).getText());
    }

    exprTextSb.append(']');

    return new JsExpr(exprTextSb.toString(), Integer.MAX_VALUE);
  }


  @Override protected JsExpr visitMapLiteralNode(MapLiteralNode node) {
    return visitMapLiteralNodeHelper(node, false);
  }


  /**
   * Helper to visit a MapLiteralNode, with the extra option of whether to quote keys.
   */
  private JsExpr visitMapLiteralNodeHelper(MapLiteralNode node, boolean doQuoteKeys) {

    // If there are only string keys, then the expression will be
    //     {aa: 11, bb: 22}    or    {'aa': 11, 'bb': 22}
    // where the former is with unquoted keys and the latter with quoted keys.
    // If there are both string and nonstring keys, then the expression will be
    //     (function() { var map_s = {'aa': 11}; map_s[opt_data.bb] = 22; return map_s; })()

    StringBuilder strKeysEntriesSnippet = new StringBuilder();
    StringBuilder nonstrKeysEntriesSnippet = new StringBuilder();

    boolean isProbablyUsingClosureCompiler =
        jsSrcOptions.shouldGenerateJsdoc() ||
        jsSrcOptions.shouldProvideRequireSoyNamespaces() ||
        jsSrcOptions.shouldProvideRequireJsFunctions();

    for (int i = 0, n = node.numChildren(); i < n; i += 2) {
      ExprNode keyNode = node.getChild(i);
      ExprNode valueNode = node.getChild(i + 1);

      if (keyNode instanceof StringNode) {
        if (strKeysEntriesSnippet.length() > 0) {
          strKeysEntriesSnippet.append(", ");
        }
        if (doQuoteKeys) {
          strKeysEntriesSnippet.append(visit(keyNode).getText());
        } else {
          String key = ((StringNode) keyNode).getValue();
          if (BaseUtils.isIdentifier(key)) {
            strKeysEntriesSnippet.append(key);
          } else {
            if (isProbablyUsingClosureCompiler) {
              throw SoySyntaxException.createWithoutMetaInfo(
                  "Map literal with non-identifier key must be wrapped in quoteKeysIfJs()" +
                      " (found non-identifier key \"" + keyNode.toSourceString() +
                      "\" in map literal \"" + node.toSourceString() + "\").");
            } else {
              strKeysEntriesSnippet.append(visit(keyNode).getText());
            }
          }
        }
        strKeysEntriesSnippet.append(": ").append(visit(valueNode).getText());

      } else if (keyNode instanceof ConstantNode) {
        throw SoySyntaxException.createWithoutMetaInfo(
            "Map literal must have keys that are strings or expressions that will evaluate to" +
                " strings at render time (found non-string key \"" + keyNode.toSourceString() +
                "\" in map literal \"" + node.toSourceString() + "\").");

      } else {
        if (isProbablyUsingClosureCompiler && ! doQuoteKeys) {
          throw SoySyntaxException.createWithoutMetaInfo(
              "Map literal with expression key must be wrapped in quoteKeysIfJs()" +
                  " (found expression key \"" + keyNode.toSourceString() +
                  "\" in map literal \"" + node.toSourceString() + "\").");
        }
        nonstrKeysEntriesSnippet
            .append(" map_s[soy.$$checkMapKey(").append(visit(keyNode).getText()).append(")] = ")
            .append(visit(valueNode).getText()).append(';');
      }
    }

    String fullExprText;
    if (nonstrKeysEntriesSnippet.length() == 0) {
      fullExprText = "{" + strKeysEntriesSnippet.toString() + "}";
    } else {
      fullExprText = "(function() { var map_s = {" + strKeysEntriesSnippet.toString() + "};" +
          nonstrKeysEntriesSnippet.toString() + " return map_s; })()";
    }

    return new JsExpr(fullExprText, Integer.MAX_VALUE);
  }


  // -----------------------------------------------------------------------------------------------
  // Implementations for data references.


  @Override protected JsExpr visitDataRefNode(DataRefNode node) {

    // Note: Using String instead of StringBuilder for readability. No performance concern here.
    String nullSafetyPrefix = "";
    String refText;

    // ------ Translate first key, which may reference a variable, data, or injected data. ------
    String firstKey = node.getFirstKey();
    if (node.isIjDataRef()) {
      // Case 1: Injected data reference.
      refText = "opt_ijData" + genCodeForKeyAccess(firstKey);
      if (node.isNullSafeIjDataRef()) {
        nullSafetyPrefix = "(opt_ijData == null) ? null : ";
      }
    } else {
      JsExpr translation = getLocalVarTranslation(firstKey);
      if (translation != null) {
        // Case 2: In-scope local var.
        refText = translation.getText();
      } else {
        // Case 3: Data reference.
        refText = "opt_data" + genCodeForKeyAccess(firstKey);
      }
    }

    // ------ Translate the rest of the keys, if any. ------
    for (ExprNode child : node.getChildren()) {
      DataRefAccessNode accessNode = (DataRefAccessNode) child;

      if (accessNode.isNullSafe()) {
        // Note: In JavaScript, "x == null" is equivalent to "x === undefined || x === null".
        nullSafetyPrefix += "(" + refText + " == null) ? null : ";
      }

      switch (accessNode.getKind()) {
        case DATA_REF_ACCESS_KEY_NODE:
          refText += genCodeForKeyAccess(((DataRefAccessKeyNode) accessNode).getKey());
          break;
        case DATA_REF_ACCESS_INDEX_NODE:
          refText += "[" + ((DataRefAccessIndexNode) accessNode).getIndex() + "]";
          break;
        case DATA_REF_ACCESS_EXPR_NODE:
          JsExpr keyJsExpr = visit(accessNode.getChild(0));
          refText += "[" + keyJsExpr.getText() + "]";
          break;
        default:
          throw new AssertionError();
      }
    }

    if (nullSafetyPrefix.length() == 0) {
      return new JsExpr(refText, Integer.MAX_VALUE);
    } else {
      return new JsExpr(nullSafetyPrefix + refText, Operator.CONDITIONAL.getPrecedence());
    }
  }


  /**
   * Private helper for {@code visitDataRefNode()} to generate the code for a key access, e.g.
   * ".foo" or "['class']". Handles JS reserved words.
   * @param key The key.
   */
  private static String genCodeForKeyAccess(String key) {
    return JS_RESERVED_WORDS.contains(key) ? "['" + key + "']" : "." + key;
  }


  /**
   * Set of words that JavaScript considers reserved words.  These words cannot
   * be used as identifiers.  This list is from the ECMA-262 v5, section 7.6.1:
   * http://www.ecma-international.org/publications/files/drafts/tc39-2009-050.pdf
   * plus the keywords for boolean values and {@code null}.
   */
  private static final ImmutableSet<String> JS_RESERVED_WORDS = ImmutableSet.of(
      "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do",
      "else", "enum", "export", "extends", "false", "finally", "for", "function", "if",
      "implements", "import", "in", "instanceof", "interface", "let", "null", "new", "package",
      "private", "protected", "public", "return", "static", "super", "switch", "this", "throw",
      "true", "try", "typeof", "var", "void", "while", "with", "yield");


  @Override protected JsExpr visitGlobalNode(GlobalNode node) {
    return new JsExpr(node.toSourceString(), Integer.MAX_VALUE);
  }


  // -----------------------------------------------------------------------------------------------
  // Implementations for operators.


  @Override protected JsExpr visitNotOpNode(NotOpNode node) {
    // Note: Since we're using Soy syntax for the 'not' operator, we'll end up generating code with
    // a space between the token '!' and the subexpression that it negates. This isn't the usual
    // style, but it should be fine (besides, it's more readable with the extra space).
    return genJsExprUsingSoySyntaxWithNewToken(node, "!");
  }


  @Override protected JsExpr visitAndOpNode(AndOpNode node) {
    return genJsExprUsingSoySyntaxWithNewToken(node, "&&");
  }


  @Override protected JsExpr visitOrOpNode(OrOpNode node) {
    return genJsExprUsingSoySyntaxWithNewToken(node, "||");
  }


  @Override protected JsExpr visitOperatorNode(OperatorNode node) {
    return genJsExprUsingSoySyntax(node);
  }


  // -----------------------------------------------------------------------------------------------
  // Implementations for functions.


  @Override protected JsExpr visitFunctionNode(FunctionNode node) {

    String fnName = node.getFunctionName();
    int numArgs = node.numChildren();

    // Handle nonplugin functions.
    NonpluginFunction nonpluginFn = NonpluginFunction.forFunctionName(fnName);
    if (nonpluginFn != null) {
      if (numArgs != nonpluginFn.getNumArgs()) {
        throw SoySyntaxException.createWithoutMetaInfo(
            "Function '" + fnName + "' called with the wrong number of arguments" +
                " (function call \"" + node.toSourceString() + "\").");
      }
      switch (nonpluginFn) {
        case IS_FIRST:
          return visitIsFirstFunction(node);
        case IS_LAST:
          return visitIsLastFunction(node);
        case INDEX:
          return visitIndexFunction(node);
        case QUOTE_KEYS_IF_JS:
          return visitMapLiteralNodeHelper((MapLiteralNode) node.getChild(0), true);
        default:
          throw new AssertionError();
      }
    }

    // Handle plugin functions.
    SoyJsSrcFunction fn = soyJsSrcFunctionsMap.get(fnName);
    if (fn != null) {
      if (! fn.getValidArgsSizes().contains(numArgs)) {
        throw SoySyntaxException.createWithoutMetaInfo(
            "Function '" + fnName + "' called with the wrong number of arguments" +
                " (function call \"" + node.toSourceString() + "\").");
      }
      List<JsExpr> args = visitChildren(node);
      try {
        return fn.computeForJsSrc(args);
      } catch (Exception e) {
        throw SoySyntaxException.createCausedWithoutMetaInfo(
            "Error in function call \"" + node.toSourceString() + "\": " + e.getMessage(), e);
      }
    }

    // Function not found.
    throw SoySyntaxException.createWithoutMetaInfo(
        "Failed to find SoyJsSrcFunction with name '" + fnName + "'" +
            " (function call \"" + node.toSourceString() + "\").");
  }


  private JsExpr visitIsFirstFunction(FunctionNode node) {
    String varName = ((DataRefNode) node.getChild(0)).getFirstKey();
    return getLocalVarTranslation(varName + "__isFirst");
  }


  private JsExpr visitIsLastFunction(FunctionNode node) {
    String varName = ((DataRefNode) node.getChild(0)).getFirstKey();
    return getLocalVarTranslation(varName + "__isLast");
  }


  private JsExpr visitIndexFunction(FunctionNode node) {
    String varName = ((DataRefNode) node.getChild(0)).getFirstKey();
    return getLocalVarTranslation(varName + "__index");
  }


  // -----------------------------------------------------------------------------------------------
  // Private helpers.


  /**
   * Gets the translated expression for an in-scope local variable (or special "variable" derived
   * from a foreach-loop var), or null if not found.
   * @param ident The Soy local variable to translate.
   * @return The translated expression for the given variable, or null if not found.
   */
  private JsExpr getLocalVarTranslation(String ident) {

    for (Map<String, JsExpr> localVarTranslationsFrame : localVarTranslations) {
      JsExpr translation = localVarTranslationsFrame.get(ident);
      if (translation != null) {
        return translation;
      }
    }

    return null;
  }


  /**
   * Generates a JS expression for the given OperatorNode's subtree assuming that the JS expression
   * for the operator uses the same syntax format as the Soy operator.
   * @param opNode The OperatorNode whose subtree to generate a JS expression for.
   * @return The generated JS expression.
   */
  private JsExpr genJsExprUsingSoySyntax(OperatorNode opNode) {
    return genJsExprUsingSoySyntaxWithNewToken(opNode, null);
  }


  /**
   * Generates a JS expression for the given OperatorNode's subtree assuming that the JS expression
   * for the operator uses the same syntax format as the Soy operator, with the exception that the
   * JS operator uses a different token (e.g. "!" instead of "not").
   * @param opNode The OperatorNode whose subtree to generate a JS expression for.
   * @param newToken The equivalent JS operator's token.
   * @return The generated JS expression.
   */
  private JsExpr genJsExprUsingSoySyntaxWithNewToken(OperatorNode opNode, String newToken) {

    List<JsExpr> operandJsExprs = visitChildren(opNode);

    return SoyJsCodeUtils.genJsExprUsingSoySyntaxWithNewToken(
        opNode.getOperator(), operandJsExprs, newToken);
  }

}
TOP

Related Classes of com.google.template.soy.jssrc.internal.TranslateToJsExprVisitor$TranslateToJsExprVisitorFactory

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.