/* * 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.pysrc.internal; import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; import com.google.template.soy.error.ErrorReporter; import com.google.template.soy.msgs.internal.IcuSyntaxUtils; import com.google.template.soy.msgs.internal.MsgUtils; import com.google.template.soy.msgs.internal.MsgUtils.MsgPartsAndIds; import com.google.template.soy.msgs.restricted.SoyMsgPart; import com.google.template.soy.msgs.restricted.SoyMsgPart.Case; import com.google.template.soy.msgs.restricted.SoyMsgPlaceholderPart; import com.google.template.soy.msgs.restricted.SoyMsgPluralCaseSpec; import com.google.template.soy.msgs.restricted.SoyMsgPluralPart; import com.google.template.soy.msgs.restricted.SoyMsgRawTextPart; import com.google.template.soy.pysrc.internal.GenPyExprsVisitor.GenPyExprsVisitorFactory; import com.google.template.soy.pysrc.restricted.PyExpr; import com.google.template.soy.pysrc.restricted.PyExprUtils; import com.google.template.soy.pysrc.restricted.PyFunctionExprBuilder; import com.google.template.soy.pysrc.restricted.PyStringExpr; import com.google.template.soy.soytree.AbstractParentSoyNode; import com.google.template.soy.soytree.CallNode; import com.google.template.soy.soytree.MsgHtmlTagNode; import com.google.template.soy.soytree.MsgNode; import com.google.template.soy.soytree.MsgPlaceholderNode; import com.google.template.soy.soytree.MsgPluralNode; import com.google.template.soy.soytree.MsgSelectNode; 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.MsgSubstUnitNode; import com.google.template.soy.soytree.SoyNode.ParentSoyNode; import java.util.LinkedHashMap; import java.util.Map; /** * Class to generate python code for one {@link MsgNode}. * */ public final class MsgFuncGenerator { /** Factory for assisted injection */ public static interface MsgFuncGeneratorFactory { MsgFuncGenerator create( MsgNode node, LocalVariableStack localVarExprs, ErrorReporter errorReporter); } /** The msg node to generate the function calls from. */ private final MsgNode msgNode; /** The generated msg id with the same algorithm for translation service. */ private final long msgId; private final ImmutableList<SoyMsgPart> msgParts; private final GenPyExprsVisitor genPyExprsVisitor; /** The function builder for the prepare_*() method */ private final PyFunctionExprBuilder prepareFunc; /** The function builder for the render_*() method */ private final PyFunctionExprBuilder renderFunc; private final TranslateToPyExprVisitor translateToPyExprVisitor; @AssistedInject MsgFuncGenerator( GenPyExprsVisitorFactory genPyExprsVisitorFactory, @Assisted MsgNode msgNode, @Assisted LocalVariableStack localVarExprs, @Assisted ErrorReporter errorReporter) { this.msgNode = msgNode; this.genPyExprsVisitor = genPyExprsVisitorFactory.create(localVarExprs, errorReporter); this.translateToPyExprVisitor = new TranslateToPyExprVisitor(localVarExprs, errorReporter); String translator = PyExprUtils.TRANSLATOR_NAME; if (this.msgNode.isPlrselMsg()) { if (this.msgNode.isPluralMsg()) { this.prepareFunc = new PyFunctionExprBuilder(translator + ".prepare_plural"); this.renderFunc = new PyFunctionExprBuilder(translator + ".render_plural"); } else { this.prepareFunc = new PyFunctionExprBuilder(translator + ".prepare_icu"); this.renderFunc = new PyFunctionExprBuilder(translator + ".render_icu"); } } else if (this.msgNode.isRawTextMsg()) { this.prepareFunc = new PyFunctionExprBuilder(translator + ".prepare_literal"); this.renderFunc = new PyFunctionExprBuilder(translator + ".render_literal"); } else { this.prepareFunc = new PyFunctionExprBuilder(translator + ".prepare"); this.renderFunc = new PyFunctionExprBuilder(translator + ".render"); } MsgPartsAndIds msgPartsAndIds = MsgUtils.buildMsgPartsAndComputeMsgIdForDualFormat(msgNode); Preconditions.checkNotNull(msgPartsAndIds); this.msgId = msgPartsAndIds.id; this.msgParts = msgPartsAndIds.parts; Preconditions.checkState(!msgParts.isEmpty()); } /** * Return the PyStringExpr for the render function call, because we know render always return a * string in Python runtime. */ PyStringExpr getPyExpr() { if (this.msgNode.isPlrselMsg()) { return this.msgNode.isPluralMsg() ? pyFuncForPluralMsg() : pyFuncForSelectMsg(); } else { return this.msgNode.isRawTextMsg() ? pyFuncForRawTextMsg() : pyFuncForGeneralMsg(); } } private PyStringExpr pyFuncForRawTextMsg() { String pyMsgText = processMsgPartsHelper(msgParts, escaperForPyFormatString); prepareFunc.addArg(msgId).addArg(pyMsgText); return renderFunc.addArg(prepareFunc.asPyExpr()).asPyStringExpr(); } private PyStringExpr pyFuncForGeneralMsg() { String pyMsgText = processMsgPartsHelper(msgParts, escaperForPyFormatString); Map<PyExpr, PyExpr> nodePyVarToPyExprMap = collectVarNameListAndToPyExprMap(); prepareFunc .addArg(msgId) .addArg(pyMsgText) .addArg(PyExprUtils.convertIterableToPyTupleExpr(nodePyVarToPyExprMap.keySet())); return renderFunc .addArg(prepareFunc.asPyExpr()) .addArg(PyExprUtils.convertMapToPyExpr(nodePyVarToPyExprMap)) .asPyStringExpr(); } private PyStringExpr pyFuncForPluralMsg() { SoyMsgPluralPart pluralPart = (SoyMsgPluralPart) msgParts.get(0); MsgPluralNode pluralNode = msgNode.getRepPluralNode(pluralPart.getPluralVarName()); Map<PyExpr, PyExpr> nodePyVarToPyExprMap = collectVarNameListAndToPyExprMap(); Map<PyExpr, PyExpr> caseSpecStrToMsgTexts = new LinkedHashMap<>(); for (Case<SoyMsgPluralCaseSpec> pluralCase : pluralPart.getCases()) { caseSpecStrToMsgTexts.put( new PyStringExpr("'" + pluralCase.spec() + "'"), new PyStringExpr("'" + processMsgPartsHelper(pluralCase.parts(), nullEscaper) + "'")); } prepareFunc .addArg(msgId) .addArg(PyExprUtils.convertMapToPyExpr(caseSpecStrToMsgTexts)) .addArg(PyExprUtils.convertIterableToPyTupleExpr(nodePyVarToPyExprMap.keySet())); // Translates {@link MsgPluralNode#pluralExpr} into a Python lookup expression. // Note that pluralExpr represent the Soy expression inside the attributes of a plural tag. PyExpr pluralPyExpr = translateToPyExprVisitor.exec(pluralNode.getExpr()); return renderFunc .addArg(prepareFunc.asPyExpr()) .addArg(pluralPyExpr) .addArg(PyExprUtils.convertMapToPyExpr(nodePyVarToPyExprMap)) .asPyStringExpr(); } private PyStringExpr pyFuncForSelectMsg() { Map<PyExpr, PyExpr> nodePyVarToPyExprMap = collectVarNameListAndToPyExprMap(); ImmutableList<SoyMsgPart> msgPartsInIcuSyntax = IcuSyntaxUtils.convertMsgPartsToEmbeddedIcuSyntax(msgParts, true); String pyMsgText = processMsgPartsHelper(msgPartsInIcuSyntax, nullEscaper); prepareFunc .addArg(msgId) .addArg(pyMsgText) .addArg(PyExprUtils.convertIterableToPyTupleExpr(nodePyVarToPyExprMap.keySet())); return renderFunc .addArg(prepareFunc.asPyExpr()) .addArg(PyExprUtils.convertMapToPyExpr(nodePyVarToPyExprMap)) .asPyStringExpr(); } /** * Private helper to process and collect all variables used within this msg node for code * generation. * * @return A Map populated with all the variables used with in this message node, using {@link * MsgPlaceholderInitialNode#genBasePhName}. */ private Map<PyExpr, PyExpr> collectVarNameListAndToPyExprMap() { Map<PyExpr, PyExpr> nodePyVarToPyExprMap = new LinkedHashMap<>(); for (Map.Entry<String, MsgSubstUnitNode> entry : msgNode.getVarNameToRepNodeMap().entrySet()) { MsgSubstUnitNode substUnitNode = entry.getValue(); PyExpr substPyExpr = null; if (substUnitNode instanceof MsgPlaceholderNode) { SoyNode phInitialNode = ((AbstractParentSoyNode<?>) substUnitNode).getChild(0); if (phInitialNode instanceof PrintNode || phInitialNode instanceof CallNode || phInitialNode instanceof RawTextNode) { substPyExpr = PyExprUtils.concatPyExprs(genPyExprsVisitor.exec(phInitialNode)).toPyString(); } // when the placeholder is generated by HTML tags if (phInitialNode instanceof MsgHtmlTagNode) { substPyExpr = PyExprUtils.concatPyExprs( genPyExprsVisitor.execOnChildren((ParentSoyNode<?>) phInitialNode)) .toPyString(); } } else if (substUnitNode instanceof MsgPluralNode) { // Translates {@link MsgPluralNode#pluralExpr} into a Python lookup expression. // Note that {@code pluralExpr} represents the soy expression of the {@code plural} attr, // i.e. the {@code $numDrafts} in {@code {plural $numDrafts}...{/plural}}. substPyExpr = translateToPyExprVisitor.exec(((MsgPluralNode) substUnitNode).getExpr()); } else if (substUnitNode instanceof MsgSelectNode) { substPyExpr = translateToPyExprVisitor.exec(((MsgSelectNode) substUnitNode).getExpr()); } if (substPyExpr != null) { nodePyVarToPyExprMap.put(new PyStringExpr("'" + entry.getKey() + "'"), substPyExpr); } } return nodePyVarToPyExprMap; } /** * Private helper to build valid Python string for a list of {@link SoyMsgPart}s. * * <p>It only processes {@link SoyMsgRawTextPart} and {@link SoyMsgPlaceholderPart} and ignores * others, because we didn't generate a direct string for plural and select nodes. * * <p>For {@link SoyMsgRawTextPart}, it appends the raw text and applies necessary escaping; For * {@link SoyMsgPlaceholderPart}, it turns the placeholder's variable name into Python replace * format. * * @param parts The SoyMsgPart parts to convert. * @param escaper A Function which provides escaping for raw text. * @return A String representing all the {@code parts} in Python. */ private static String processMsgPartsHelper( ImmutableList<SoyMsgPart> parts, Function<String, String> escaper) { StringBuilder rawMsgTextSb = new StringBuilder(); for (SoyMsgPart part : parts) { if (part instanceof SoyMsgRawTextPart) { rawMsgTextSb.append(escaper.apply(((SoyMsgRawTextPart) part).getRawText())); } if (part instanceof SoyMsgPlaceholderPart) { String phName = ((SoyMsgPlaceholderPart) part).getPlaceholderName(); rawMsgTextSb.append("{" + phName + "}"); } } return rawMsgTextSb.toString(); } /** * A mapper to apply escaping for python format string. * * <p>It escapes '{' and '}' to '{{' and '}}' in the String. * * @see "https://docs.python.org/2/library/string.html#formatstrings" */ private static final Function<String, String> escaperForPyFormatString = new Function<String, String>() { @Override public String apply(String str) { return str.replaceAll("\\{", "{{").replaceAll("\\}", "}}").replace("'", "\\\'"); } }; /** A mapper which does nothing. */ private static final Function<String, String> nullEscaper = new Function<String, String>() { @Override public String apply(String str) { return str; } }; }