/* * 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.restricted; import com.google.common.annotations.VisibleForTesting; 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.data.internalutils.NodeContentKinds; import com.google.template.soy.exprtree.Operator; import java.util.List; import javax.annotation.Nullable; /** * Common utilities for dealing with JS expressions. * * <p>Important: This class may only be used in implementing plugins (e.g. functions, directives). * */ public class JsExprUtils { /** Expression constant for empty string. */ private static final JsExpr EMPTY_STRING = new JsExpr("''", Integer.MAX_VALUE); private JsExprUtils() {} /** * Builds one JS expression that computes the concatenation of the given JS expressions. The '+' * operator is used for concatenation. Operands will be protected with an extra pair of * parentheses if and only if needed. * * <p>The resulting expression is not guaranteed to be a string if the operands do not produce * strings when combined with the plus operator; e.g. 2+2 might be 4 instead of '22'. * * @param jsExprs The JS expressions to concatentate. * @return One JS expression that computes the concatenation of the given JS expressions. */ public static JsExpr concatJsExprs(List<? extends JsExpr> jsExprs) { if (jsExprs.isEmpty()) { return EMPTY_STRING; } if (jsExprs.size() == 1) { return jsExprs.get(0); } int plusOpPrec = Operator.PLUS.getPrecedence(); StringBuilder resultSb = new StringBuilder(); boolean isFirst = true; for (JsExpr jsExpr : jsExprs) { // The first operand needs protection only if it's strictly lower precedence. The non-first // operands need protection when they're lower or equal precedence. (This is true for all // left-associative operators.) boolean needsProtection = isFirst ? jsExpr.getPrecedence() < plusOpPrec : jsExpr.getPrecedence() <= plusOpPrec; if (isFirst) { isFirst = false; } else { resultSb.append(" + "); } if (needsProtection) { resultSb.append('(').append(jsExpr.getText()).append(')'); } else { resultSb.append(jsExpr.getText()); } } return new JsExpr(resultSb.toString(), plusOpPrec); } public static boolean isStringLiteral(JsExpr jsExpr) { String jsExprText = jsExpr.getText(); int jsExprTextLastIndex = jsExprText.length() - 1; if (jsExprTextLastIndex < 1 || jsExprText.charAt(0) != '\'' || jsExprText.charAt(jsExprTextLastIndex) != '\'') { return false; } for (int i = 1; i < jsExprTextLastIndex; ++i) { char c = jsExprText.charAt(i); if (c == '\'') { return false; } if (c == '\\') { // We do not bother skipping through the whole escape if it takes up more than one character // beyond the backslash, e.g. \u1234 or \123 or \x12, since none of such escapes' characters // can be an apostrophe, which is all we really care about. Nor do we check that the escape // doesn't include the final apostrophe, since that would mean the JS expression is invalid. ++i; } } return true; } public static JsExpr toString(JsExpr expr) { // If the expression is a string, nothing to do. if (isStringLiteral(expr)) { return expr; } // Add empty string first, which ensures the plus operator always means string concatenation. // Consider: // '' + 6 + 6 + 6 = '666' // 6 + 6 + 6 + '' = '18' return concatJsExprs(ImmutableList.of(EMPTY_STRING, expr)); } /** * Wraps an expression in a function call. * * @param functionExprText expression for the function to invoke, such as a function name or * constructor phrase (such as "new SomeClass"). * @param jsExpr the expression to compute the argument to the function * @return a JS expression consisting of a call to the specified function, applied to the provided * expression. */ @VisibleForTesting static JsExpr wrapWithFunction(String functionExprText, JsExpr jsExpr) { Preconditions.checkNotNull(functionExprText); return new JsExpr(functionExprText + "(" + jsExpr.getText() + ")", Integer.MAX_VALUE); } /** * Wraps with the proper SanitizedContent constructor if contentKind is non-null. * * @param contentKind The kind of sanitized content. * @param jsExpr The expression to wrap. */ public static JsExpr maybeWrapAsSanitizedContent( @Nullable ContentKind contentKind, JsExpr jsExpr) { if (contentKind == null) { return jsExpr; } else { return wrapWithFunction(NodeContentKinds.toJsSanitizedContentOrdainer(contentKind), jsExpr); } } }