/*
* 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.msgs.internal;
import com.google.common.collect.Lists;
import com.google.template.soy.base.internal.IdGenerator;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.error.SoyErrorKind;
import com.google.template.soy.msgs.SoyMsgBundle;
import com.google.template.soy.msgs.restricted.SoyMsg;
import com.google.template.soy.msgs.restricted.SoyMsgPart;
import com.google.template.soy.msgs.restricted.SoyMsgPlaceholderPart;
import com.google.template.soy.msgs.restricted.SoyMsgRawTextPart;
import com.google.template.soy.soytree.AbstractSoyNodeVisitor;
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.MsgPluralNode;
import com.google.template.soy.soytree.MsgSelectNode;
import com.google.template.soy.soytree.RawTextNode;
import com.google.template.soy.soytree.SoyFileSetNode;
import com.google.template.soy.soytree.SoyNode;
import com.google.template.soy.soytree.SoyNode.ParentSoyNode;
import com.google.template.soy.soytree.SoyNode.StandaloneNode;
import java.util.List;
import javax.annotation.Nullable;
/**
* Visitor for inserting translated messages into Soy tree. This pass replaces the
* MsgFallbackGroupNodes in the tree with sequences of RawTextNodes and other nodes. The only
* exception is plural/select messages. This pass currently does not replace MsgFallbackGroupNodes
* that contain plural/select messages.
*
* <p>Important: Do not use outside of Soy code (treat as superpackage-private).
*
* <p>If the Soy tree doesn't contain plural/select messages, then after this pass, the Soy tree
* should no longer contain MsgFallbackGroupNodes, MsgNodes, MsgPlaceholderNodes, or
* MsgHtmlTagNodes. If the Soy tree contains plural/select messages, then the only messages left in
* the tree after this pass runs should be the plural/select messages.
*
* <p>Note that the Soy tree is usually simplifiable after this pass is run (e.g. it usually
* contains consecutive RawTextNodes). It's usually advisable to run a simplification pass after
* this pass.
*
*/
public final class InsertMsgsVisitor extends AbstractSoyNodeVisitor<Void> {
private static final SoyErrorKind ENCOUNTERED_PLURAL_OR_SELECT =
SoyErrorKind.of(
"JS code generation currently only supports plural/select messages when "
+ "shouldGenerateGoogMsgDefs is true.");
@Nullable private final SoyMsgBundle msgBundle;
private final ErrorReporter errorReporter;
private IdGenerator nodeIdGen;
/** The replacement nodes for the current MsgFallbackGroupNode we're visiting (during a pass). */
private List<StandaloneNode> currReplacementNodes;
/**
* @param msgBundle The bundle of translated messages, or null to use the messages from the Soy
* source.
* @param errorReporter For reporting errors.
*/
public InsertMsgsVisitor(@Nullable SoyMsgBundle msgBundle, ErrorReporter errorReporter) {
this.msgBundle = msgBundle;
this.errorReporter = errorReporter;
}
@Override
public Void exec(SoyNode node) {
// Retrieve the node id generator from the root of the parse tree.
nodeIdGen = node.getNearestAncestor(SoyFileSetNode.class).getNodeIdGenerator();
// Execute the pass.
super.exec(node);
return null;
}
// -----------------------------------------------------------------------------------------------
// Implementations for specific nodes.
@Override
protected void visitMsgFallbackGroupNode(MsgFallbackGroupNode node) {
// Check for plural or select message. Either report error or don't replace.
for (MsgNode msg : node.getChildren()) {
if (msg.numChildren() == 1
&& (msg.getChild(0) instanceof MsgSelectNode
|| msg.getChild(0) instanceof MsgPluralNode)) {
errorReporter.report(node.getSourceLocation(), ENCOUNTERED_PLURAL_OR_SELECT);
return;
}
}
// Figure out which message we're going to use, and build its list of replacement nodes.
currReplacementNodes = null;
if (msgBundle != null) {
for (MsgNode msg : node.getChildren()) {
SoyMsg translation = msgBundle.getMsg(MsgUtils.computeMsgIdForDualFormat(msg));
if (translation != null) {
buildReplacementNodesFromTranslation(msg, translation);
break;
}
}
}
if (currReplacementNodes == null) {
buildReplacementNodesFromSource(node.getChild(0));
}
// Replace this MsgFallbackGroupNode with the replacement nodes.
ParentSoyNode<StandaloneNode> parent = node.getParent();
int indexInParent = parent.getChildIndex(node);
parent.removeChild(indexInParent);
parent.addChildren(indexInParent, currReplacementNodes);
currReplacementNodes = null;
}
/**
* Private helper for visitMsgFallbackGroupNode() to build the list of replacement nodes for a
* message from its translation.
*/
private void buildReplacementNodesFromTranslation(MsgNode msg, SoyMsg translation) {
currReplacementNodes = Lists.newArrayList();
for (SoyMsgPart msgPart : translation.getParts()) {
if (msgPart instanceof SoyMsgRawTextPart) {
// Append a new RawTextNode to the currReplacementNodes list.
String rawText = ((SoyMsgRawTextPart) msgPart).getRawText();
currReplacementNodes.add(
new RawTextNode(nodeIdGen.genId(), rawText, msg.getSourceLocation()));
} else if (msgPart instanceof SoyMsgPlaceholderPart) {
// Get the representative placeholder node and iterate through its contents.
String placeholderName = ((SoyMsgPlaceholderPart) msgPart).getPlaceholderName();
MsgPlaceholderNode placeholderNode = msg.getRepPlaceholderNode(placeholderName);
for (StandaloneNode contentNode : placeholderNode.getChildren()) {
// If the content node is a MsgHtmlTagNode, it needs to be replaced by a number of
// consecutive siblings. This is done by visiting the MsgHtmlTagNode. Otherwise, we
// simply add the content node to the currReplacementNodes list being built.
if (contentNode instanceof MsgHtmlTagNode) {
visit(contentNode);
} else {
currReplacementNodes.add(contentNode);
}
}
} else {
throw new AssertionError();
}
}
}
/**
* Private helper for visitMsgFallbackGroupNode() to build the list of replacement nodes for a
* message from its source.
*/
private void buildReplacementNodesFromSource(MsgNode msg) {
currReplacementNodes = Lists.newArrayList();
for (StandaloneNode child : msg.getChildren()) {
if (child instanceof RawTextNode) {
currReplacementNodes.add(child);
} else if (child instanceof MsgPlaceholderNode) {
for (StandaloneNode contentNode : ((MsgPlaceholderNode) child).getChildren()) {
// If the content node is a MsgHtmlTagNode, it needs to be replaced by a number of
// consecutive siblings. This is done by visiting the MsgHtmlTagNode. Otherwise, we
// simply add the content node to the currReplacementNodes list being built.
if (contentNode instanceof MsgHtmlTagNode) {
visit(contentNode);
} else {
currReplacementNodes.add(contentNode);
}
}
} else {
throw new AssertionError();
}
}
}
@Override
protected void visitMsgHtmlTagNode(MsgHtmlTagNode node) {
currReplacementNodes.addAll(node.getChildren());
}
// -----------------------------------------------------------------------------------------------
// Fallback implementation.
@Override
protected void visitSoyNode(SoyNode node) {
if ((node instanceof ParentSoyNode<?>)) {
visitChildrenAllowingConcurrentModification((ParentSoyNode<?>) node);
}
}
}