/* * Copyright 2012 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.sharedpasses.render; import com.google.common.collect.ImmutableList; import com.google.template.soy.data.SoyDataException; import com.google.template.soy.exprtree.ExprRootNode; import com.google.template.soy.msgs.SoyMsgBundle; import com.google.template.soy.msgs.internal.MsgUtils; import com.google.template.soy.msgs.restricted.SoyMsgPart; import com.google.template.soy.msgs.restricted.SoyMsgPlaceholderPart; import com.google.template.soy.msgs.restricted.SoyMsgPluralPart; import com.google.template.soy.msgs.restricted.SoyMsgPluralRemainderPart; import com.google.template.soy.msgs.restricted.SoyMsgRawTextPart; import com.google.template.soy.msgs.restricted.SoyMsgSelectPart; import com.google.template.soy.soytree.AbstractSoyNodeVisitor; import com.google.template.soy.soytree.CaseOrDefaultNode; import com.google.template.soy.soytree.MsgFallbackGroupNode; 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.MsgPluralCaseNode; import com.google.template.soy.soytree.MsgPluralDefaultNode; import com.google.template.soy.soytree.MsgPluralNode; import com.google.template.soy.soytree.MsgSelectCaseNode; import com.google.template.soy.soytree.MsgSelectDefaultNode; import com.google.template.soy.soytree.MsgSelectNode; import com.google.template.soy.soytree.SoyNode; import com.ibm.icu.util.ULocale; import java.util.List; import javax.annotation.Nullable; /** * Assistant visitor for RenderVisitor to handle messages. * */ final class RenderVisitorAssistantForMsgs extends AbstractSoyNodeVisitor<Void> { /** Master instance of RenderVisitor. */ private final RenderVisitor master; /** The bundle of translated messages, or null to use the messages from the Soy source. */ private final SoyMsgBundle msgBundle; /** * @param master The master RenderVisitor instance. * @param msgBundle The bundle of translated messages, or null to use the messages from the Soy * source. */ RenderVisitorAssistantForMsgs(RenderVisitor master, SoyMsgBundle msgBundle) { this.master = master; this.msgBundle = msgBundle; } @Override public Void exec(SoyNode node) { throw new AssertionError(); } /** This method must only be called by the master RenderVisitor. */ void visitForUseByMaster(SoyNode node) { visit(node); } // ----------------------------------------------------------------------------------------------- // Implementations for specific nodes. @Override protected void visitMsgFallbackGroupNode(MsgFallbackGroupNode node) { boolean foundTranslation = false; if (msgBundle != null) { for (MsgNode msg : node.getChildren()) { ImmutableList<SoyMsgPart> translation = msgBundle.getMsgParts(MsgUtils.computeMsgIdForDualFormat(msg)); if (!translation.isEmpty()) { renderMsgFromTranslation(msg, translation, msgBundle.getLocale()); foundTranslation = true; break; } } } if (!foundTranslation) { renderMsgFromSource(node.getChild(0)); } } /** Private helper for visitMsgFallbackGroupNode() to render a message from its translation. */ private void renderMsgFromTranslation( MsgNode msg, ImmutableList<SoyMsgPart> msgParts, @Nullable ULocale locale) { // TODO(lukes): remove this if statement, it makes no sense. if (!msgParts.isEmpty()) { SoyMsgPart firstPart = msgParts.get(0); if (firstPart instanceof SoyMsgPluralPart) { new PlrselMsgPartsVisitor(msg, locale).visitPart((SoyMsgPluralPart) firstPart); } else if (firstPart instanceof SoyMsgSelectPart) { new PlrselMsgPartsVisitor(msg, locale).visitPart((SoyMsgSelectPart) firstPart); } else { for (SoyMsgPart msgPart : msgParts) { if (msgPart instanceof SoyMsgRawTextPart) { RenderVisitor.append( master.getCurrOutputBufForUseByAssistants(), ((SoyMsgRawTextPart) msgPart).getRawText()); } else if (msgPart instanceof SoyMsgPlaceholderPart) { String placeholderName = ((SoyMsgPlaceholderPart) msgPart).getPlaceholderName(); visit(msg.getRepPlaceholderNode(placeholderName)); } else { throw new AssertionError(); } } } } } /** Private helper for visitMsgFallbackGroupNode() to render a message from its source. */ private void renderMsgFromSource(MsgNode msg) { visitChildren(msg); } @Override protected void visitMsgNode(MsgNode node) { throw new AssertionError(); } @Override protected void visitMsgPluralNode(MsgPluralNode node) { ExprRootNode pluralExpr = node.getExpr(); double pluralValue; try { pluralValue = master.evalForUseByAssistants(pluralExpr, node).numberValue(); } catch (SoyDataException e) { throw RenderException.createWithSource( String.format( "Plural expression \"%s\" doesn't evaluate to number.", pluralExpr.toSourceString()), e, node); } // Check each case. for (CaseOrDefaultNode child : node.getChildren()) { if (child instanceof MsgPluralDefaultNode) { // This means it didn't match any other case. visitChildren(child); break; } else { if (((MsgPluralCaseNode) child).getCaseNumber() == pluralValue) { visitChildren(child); break; } } } } @Override protected void visitMsgSelectNode(MsgSelectNode node) { ExprRootNode selectExpr = node.getExpr(); String selectValue; try { selectValue = master.evalForUseByAssistants(selectExpr, node).stringValue(); } catch (SoyDataException e) { throw RenderException.createWithSource( String.format( "Select expression \"%s\" doesn't evaluate to string.", selectExpr.toSourceString()), e, node); } // Check each case. for (CaseOrDefaultNode child : node.getChildren()) { if (child instanceof MsgSelectDefaultNode) { // This means it didn't match any other case. visitChildren(child); } else { if (((MsgSelectCaseNode) child).getCaseValue().equals(selectValue)) { visitChildren(child); return; } } } } @Override protected void visitMsgPlaceholderNode(MsgPlaceholderNode node) { visitChildren(node); } @Override protected void visitMsgHtmlTagNode(MsgHtmlTagNode node) { // Note: We don't default to the fallback implementation because we don't need to add // another frame to the environment. visitChildren(node); } // ----------------------------------------------------------------------------------------------- // Helper class for traversing a translated plural/select message. /** * Visitor for processing {@code SoyMsgPluralPart} and {@code SoyMsgSelectPart} objects. * * <p>Visits the parts hierarchy, evaluates each part and appends the result into the parent * class' StringBuffer object. * * <p>In addition to writing to output, this inner class uses the outer class's master's eval() * method to evaluate the expressions associated with the nodes. */ private class PlrselMsgPartsVisitor { /** The parent message node for the parts dealt here. */ private final MsgNode msgNode; /** The locale for the translated message considered. */ private final ULocale locale; /** * Constructor. * * @param msgNode The parent message node for the parts dealt here. * @param locale The locale of the Soy message. */ public PlrselMsgPartsVisitor(MsgNode msgNode, ULocale locale) { this.msgNode = msgNode; this.locale = locale; } /** * Processes a {@code SoyMsgSelectPart} and appends the rendered output to the {@code * StringBuilder} object in {@code RenderVisitor}. * * @param selectPart The Select part. */ private void visitPart(SoyMsgSelectPart selectPart) { String selectVarName = selectPart.getSelectVarName(); MsgSelectNode repSelectNode = msgNode.getRepSelectNode(selectVarName); // Associate the select variable with the value. String correctSelectValue; ExprRootNode selectExpr = repSelectNode.getExpr(); try { correctSelectValue = master.evalForUseByAssistants(selectExpr, repSelectNode).stringValue(); } catch (SoyDataException e) { throw RenderException.createWithSource( String.format( "Select expression \"%s\" doesn't evaluate to string.", selectExpr.toSourceString()), e, repSelectNode); } List<SoyMsgPart> caseParts = selectPart.lookupCase(correctSelectValue); if (caseParts != null) { for (SoyMsgPart casePart : caseParts) { if (casePart instanceof SoyMsgSelectPart) { visitPart((SoyMsgSelectPart) casePart); } else if (casePart instanceof SoyMsgPluralPart) { visitPart((SoyMsgPluralPart) casePart); } else if (casePart instanceof SoyMsgPlaceholderPart) { visitPart((SoyMsgPlaceholderPart) casePart); } else if (casePart instanceof SoyMsgRawTextPart) { appendRawTextPart((SoyMsgRawTextPart) casePart); } else { throw RenderException.create( "Unsupported part of type " + casePart.getClass().getName() + " under a select case.") .addStackTraceElement(repSelectNode); } } } } /** * Processes a {@code SoyMsgPluralPart} and appends the rendered output to the {@code * StringBuilder} object in {@code RenderVisitor}. It uses the message node cached in this * object to get the corresponding Plural node, gets its variable value and offset, and computes * the remainder value to be used to render the {@code SoyMsgPluralRemainderPart} later. * * @param pluralPart The Plural part. */ private void visitPart(SoyMsgPluralPart pluralPart) { MsgPluralNode repPluralNode = msgNode.getRepPluralNode(pluralPart.getPluralVarName()); double correctPluralValue; ExprRootNode pluralExpr = repPluralNode.getExpr(); try { correctPluralValue = master.evalForUseByAssistants(pluralExpr, repPluralNode).numberValue(); } catch (SoyDataException e) { throw RenderException.createWithSource( String.format( "Plural expression \"%s\" doesn't evaluate to number.", pluralExpr.toSourceString()), e, repPluralNode); } // Handle cases. List<SoyMsgPart> caseParts = pluralPart.lookupCase((int) correctPluralValue, locale); for (SoyMsgPart casePart : caseParts) { if (casePart instanceof SoyMsgPlaceholderPart) { visitPart((SoyMsgPlaceholderPart) casePart); } else if (casePart instanceof SoyMsgRawTextPart) { appendRawTextPart((SoyMsgRawTextPart) casePart); } else if (casePart instanceof SoyMsgPluralRemainderPart) { appendPluralRemainder(correctPluralValue - pluralPart.getOffset()); } else { // Plural parts will not have nested plural/select parts. So, this is an error. throw RenderException.create( "Unsupported part of type " + casePart.getClass().getName() + " under a plural case.") .addStackTraceElement(repPluralNode); } } } /** * Processes a {@code SoyMsgPluralRemainderPart} and appends the rendered output to the {@code * StringBuilder} object in {@code RenderVisitor}. Since this is precomputed when visiting the * {@code SoyMsgPluralPart} object, it is directly used here. */ private void appendPluralRemainder(double currentPluralRemainderValue) { RenderVisitor.append( master.getCurrOutputBufForUseByAssistants(), String.valueOf(currentPluralRemainderValue)); } /** * Process a {@code SoyMsgPlaceholderPart} and updates the internal data structures. * * @param msgPlaceholderPart the Placeholder part. */ private void visitPart(SoyMsgPlaceholderPart msgPlaceholderPart) { // Since the content of a placeholder is not altered by translation, just render // the corresponding placeholder node. visit(msgNode.getRepPlaceholderNode(msgPlaceholderPart.getPlaceholderName())); } /** * Processes a {@code SoyMsgRawTextPart} and appends the contained text to the {@code * StringBuilder} object in {@code RenderVisitor}. * * @param rawTextPart The raw text part. */ private void appendRawTextPart(SoyMsgRawTextPart rawTextPart) { RenderVisitor.append(master.getCurrOutputBufForUseByAssistants(), rawTextPart.getRawText()); } } // ----------------------------------------------------------------------------------------------- // Fallback implementation. @Override protected void visitSoyNode(SoyNode node) { master.visitForUseByAssistants(node); } }