/*
* Copyright 2009 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.template.soy.base.internal.IdGenerator;
import com.google.template.soy.data.SanitizedContent.ContentKind;
import com.google.template.soy.html.IncrementalHtmlAttributeNode;
import com.google.template.soy.passes.BuildAllDependeesMapVisitor;
import com.google.template.soy.soytree.AbstractSoyNodeVisitor;
import com.google.template.soy.soytree.LetContentNode;
import com.google.template.soy.soytree.MsgFallbackGroupNode;
import com.google.template.soy.soytree.SoyFileSetNode;
import com.google.template.soy.soytree.SoyNode;
import com.google.template.soy.soytree.SoyNode.BlockNode;
import com.google.template.soy.soytree.SoyNode.LocalVarInlineNode;
import com.google.template.soy.soytree.SoyNode.ParentSoyNode;
import com.google.template.soy.soytree.SoyNode.RenderUnitNode;
import com.google.template.soy.soytree.SoyTreeUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Moves {@link MsgFallbackGroupNode}s to separate <code>{let}</code> and print nodes. We then move
* the let node to the nearest block ancestor, so that the print node can be emitted as an
* expression instead of a statement. After this pass, all MsgFallbackGroupNodes will only appear
* inside {@link LetContentNodes}.
*
* <p>TODO(slaks): Generalize to extract all nodes that create statements but must appear in
* expression context ({@link MsgHtmlTagNode}, {@link CallParamNode}, {@link LogNode}, & idom
* attribute values) to variables. This will let us completely remove the many paths in jssrc that
* conditionally emit temporary variables.
*
* <p>{@link #exec} must be called on a full parse tree.
*
*/
public class ExtractMsgVariablesVisitor extends AbstractSoyNodeVisitor<Void> {
/** The list of MsgFallbackGroupNodes found in the given node's subtree. */
private List<MsgFallbackGroupNode> msgFbGrpNodes;
private Map<SoyNode, List<SoyNode>> allDependeesMap;
@Override
public Void exec(SoyNode node) {
msgFbGrpNodes = new ArrayList<>();
visit(node);
return null;
}
// -----------------------------------------------------------------------------------------------
// Implementations for specific nodes.
@Override
protected void visitSoyFileSetNode(SoyFileSetNode node) {
// We find all the MsgFallbackGroupNodes before replacing them because we don't want the
// modifications to interfere with the traversal.
// This pass simply finds all the MsgFallbackGroupNodes.
visitChildren(node);
// Help figure out where we can move each MsgFallbackGroupNode to.
allDependeesMap = new BuildAllDependeesMapVisitor().exec(node);
// Perform the replacements.
IdGenerator nodeIdGen = node.getNearestAncestor(SoyFileSetNode.class).getNodeIdGenerator();
for (MsgFallbackGroupNode msgFbGrpNode : msgFbGrpNodes) {
wrapMsgFallbackGroupNodeHelper(msgFbGrpNode, nodeIdGen);
}
}
@Override
protected void visitMsgFallbackGroupNode(MsgFallbackGroupNode node) {
msgFbGrpNodes.add(node);
visitChildren(node);
}
// -----------------------------------------------------------------------------------------------
// Fallback implementation.
@Override
protected void visitSoyNode(SoyNode node) {
if (node instanceof ParentSoyNode<?>) {
visitChildren((ParentSoyNode<?>) node);
}
}
// -----------------------------------------------------------------------------------------------
// Helpers.
protected void wrapMsgFallbackGroupNodeHelper(
MsgFallbackGroupNode msgFbGrpNode, IdGenerator nodeIdGen) {
String varName = "msg_" + nodeIdGen.genId();
// Find the actual content kind that this node prints in.
RenderUnitNode container = msgFbGrpNode.getNearestAncestor(RenderUnitNode.class);
ContentKind kind = container.getContentKind();
IncrementalHtmlAttributeNode containingAttribute =
msgFbGrpNode.getNearestAncestor(IncrementalHtmlAttributeNode.class);
if (containingAttribute != null
&& SoyTreeUtils.isDescendantOf(containingAttribute, container)) {
kind = ContentKind.TEXT;
}
// In traditional JS codegen, we will not infer a meaningful ContentKind (other than text). But
// that does not matter, since ContextAutoesc has already added appropriate escaping directives.
// At this point, ContentKind only matters for idom, which uses it to figure out how to emit the
// output (as itext, as parsed HTML or as attribute parameters).
LetContentNode letNode =
LetContentNode.forVariable(
nodeIdGen.genId(), msgFbGrpNode.getSourceLocation(), varName, kind);
msgFbGrpNode
.getParent()
.replaceChild(msgFbGrpNode, msgFbGrpNode.makePrintNode(nodeIdGen, letNode.getVar()));
letNode.addChild(msgFbGrpNode);
insertWrappingLetNode(letNode, allDependeesMap.get(msgFbGrpNode));
}
/** Inserts a wrapping {let} node in the nearest block ancestor. */
private void insertWrappingLetNode(LetContentNode letNode, List<SoyNode> allDependees) {
BlockNode newParent;
int indexUnderNewParent;
SoyNode nearestDependee = allDependees.get(0);
if (nearestDependee instanceof LocalVarInlineNode) {
newParent = (BlockNode) nearestDependee.getParent();
indexUnderNewParent = newParent.getChildIndex((LocalVarInlineNode) nearestDependee) + 1;
} else if (nearestDependee instanceof BlockNode) {
newParent = (BlockNode) nearestDependee;
indexUnderNewParent = 0;
} else {
throw new AssertionError();
}
// TODO(slaks): Move after earlier inserted nodes in case {msg}s reference each-other.
newParent.addChild(indexUnderNewParent, letNode);
}
}