/* * Copyright 2016 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.passes; import static com.google.common.base.Preconditions.checkState; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.template.soy.base.SourceLocation; import com.google.template.soy.base.internal.IdGenerator; import com.google.template.soy.soytree.AbstractSoyNodeVisitor; import com.google.template.soy.soytree.CallParamContentNode; import com.google.template.soy.soytree.ForNode; import com.google.template.soy.soytree.ForeachNode; import com.google.template.soy.soytree.HtmlAttributeNode; import com.google.template.soy.soytree.HtmlAttributeValueNode; import com.google.template.soy.soytree.HtmlAttributeValueNode.Quotes; import com.google.template.soy.soytree.HtmlCloseTagNode; import com.google.template.soy.soytree.HtmlOpenTagNode; import com.google.template.soy.soytree.IfNode; import com.google.template.soy.soytree.LetContentNode; 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.BlockNode; import com.google.template.soy.soytree.SoyNode.ParentSoyNode; import com.google.template.soy.soytree.SoyNode.StandaloneNode; import com.google.template.soy.soytree.SwitchNode; import com.google.template.soy.soytree.TemplateNode; import com.google.template.soy.soytree.TemplateRegistry; import java.util.ArrayList; import java.util.List; /** * Replaces {@link HtmlOpenTagNode} and {@link HtmlCloseTagNode} with a set of RawTextNodes and the * children. * * <p>This pass ensures that the rest of the compiler can remain agnostic about these nodes. */ final class DesugarHtmlNodesPass extends CompilerFileSetPass { @Override public void run(SoyFileSetNode fileSet, TemplateRegistry registry) { IdGenerator idGenerator = fileSet.getNodeIdGenerator(); run(fileSet, idGenerator); } @VisibleForTesting void run(SoyNode node, IdGenerator idGenerator) { new RewritingVisitor(idGenerator).exec(node); // Whether or not we have replaced any nodes, we still need to merge adjacent RawTextNodes since // the parser may have divided them up in such a way that the Autoescaper can't handle it. In // particular the autoescaper expects to be able to see whole close tags, so it can handle // </script> as a single rawtextnode, but not as </,script,> (3 raw text nodes). new CombineConsecutiveRawTextNodesVisitor(idGenerator).exec(node); } private static final class RewritingVisitor extends AbstractSoyNodeVisitor<Void> { private final IdGenerator idGenerator; /** Tracks whether we need a space character before the next attribute character. */ boolean needsSpaceForAttribute; /** Tracks all the nodes that should replace the current node. */ final List<StandaloneNode> replacements = new ArrayList<>(); RewritingVisitor(IdGenerator idGenerator) { this.idGenerator = idGenerator; } @Override protected void visitTemplateNode(TemplateNode node) { needsSpaceForAttribute = false; visitChildren(node); } @Override protected void visitHtmlCloseTagNode(HtmlCloseTagNode node) { needsSpaceForAttribute = true; visitChildren(node); needsSpaceForAttribute = false; replacements.add(createPrefix("</", node)); replacements.addAll(node.getChildren()); replacements.add(createSuffix(">", node)); } @Override protected void visitHtmlOpenTagNode(HtmlOpenTagNode node) { needsSpaceForAttribute = true; visitChildren(node); needsSpaceForAttribute = false; replacements.add(createPrefix("<", node)); replacements.addAll(node.getChildren()); replacements.add(createSuffix(node.isSelfClosing() ? "/>" : ">", node)); } @Override protected void visitHtmlAttributeValueNode(HtmlAttributeValueNode node) { visitChildren(node); Quotes quotes = node.getQuotes(); if (quotes == Quotes.NONE) { replacements.addAll(node.getChildren()); } else { replacements.add(createPrefix(quotes.getQuotationCharacter(), node)); replacements.addAll(node.getChildren()); replacements.add(createSuffix(quotes.getQuotationCharacter(), node)); } } /** * Returns a new RawTextNode with the given content at the beginning of the {@code context} * node. */ private RawTextNode createPrefix(String prefix, SoyNode context) { SourceLocation location = context.getSourceLocation().getBeginLocation(); // location points to the first character, so if the content is longer than one character // extend the source location to cover it. e.g. content might be "</" if (prefix.length() > 1) { location = location.offsetEndCol(prefix.length() - 1); } return new RawTextNode(idGenerator.genId(), prefix, location); } /** Returns a new RawTextNode with the given content at the end of the {@code context} node. */ private RawTextNode createSuffix(String suffix, SoyNode context) { SourceLocation location = context.getSourceLocation().getEndLocation(); // location points to the last character, so if the content is longer than one character // extend the source location to cover it. e.g. content might be "/>" if (suffix.length() > 1) { location = location.offsetStartCol(suffix.length() - 1); } return new RawTextNode(idGenerator.genId(), suffix, location); } @Override protected void visitHtmlAttributeNode(HtmlAttributeNode node) { visitChildren(node); // prefix the value with a single whitespace character. This makes it unambiguous with the // preceding attribute/tag name. // There are some cases where we don't need this: // 1. if the attribute children don't render anything, e.g. {$foo ?: ''} // -This would only be fixable by modifying the code generators to dynamically insert the // space character // 2. if the preceding node is a quoted attribute value // -This would always work, but is technically out of spec so we should probably avoid it. if (needsSpaceForAttribute) { // TODO(lukes): in this case, if the attribute is dynamic and ultimately renders the // empty string, we will render an extra space. replacements.add( new RawTextNode(idGenerator.genId(), " ", node.getSourceLocation().getBeginLocation())); } else { // After any attribute, the next attribute will need a space character. needsSpaceForAttribute = true; } replacements.add(node.getChild(0)); if (node.hasValue()) { replacements.add(new RawTextNode(idGenerator.genId(), "=", node.getEqualsLocation())); // normally there would only be 1 child, but rewriting may have split it into multiple replacements.addAll(node.getChildren().subList(1, node.numChildren())); } } @Override protected void visitCallParamContentNode(CallParamContentNode node) { boolean oldInTag = needsSpaceForAttribute; needsSpaceForAttribute = false; visitChildren(node); needsSpaceForAttribute = oldInTag; } @Override protected void visitLetContentNode(LetContentNode node) { boolean prev = needsSpaceForAttribute; needsSpaceForAttribute = false; visitChildren(node); needsSpaceForAttribute = prev; } @Override protected void visitIfNode(IfNode node) { visitControlFlowBranches(node.getChildren()); } @Override protected void visitForNode(ForNode node) { visitControlFlowBranches(ImmutableList.<BlockNode>of(node)); } @Override protected void visitForeachNode(ForeachNode node) { visitControlFlowBranches(node.getChildren()); } @Override protected void visitSwitchNode(SwitchNode node) { visitControlFlowBranches(node.getChildren()); } private void visitControlFlowBranches(List<BlockNode> branches) { // If any one of the branches sets needsSpaceForAttribute to true, then it should get set to // true for the whole control flow block. boolean start = needsSpaceForAttribute; boolean end = needsSpaceForAttribute; for (BlockNode child : branches) { visitChildren(child); end |= needsSpaceForAttribute; needsSpaceForAttribute = start; } needsSpaceForAttribute = end; } @Override protected void visitSoyNode(SoyNode node) { if (node instanceof SoyNode.ParentSoyNode) { visitChildren((ParentSoyNode<?>) node); } } @Override protected void visitChildren(ParentSoyNode<?> node) { doVisitChildren(node); } // extracted as a helper method to capture the type variable private <C extends SoyNode> void doVisitChildren(ParentSoyNode<C> parent) { for (int i = 0; i < parent.numChildren(); i++) { C child = parent.getChild(i); visit(child); if (!replacements.isEmpty()) { parent.removeChild(i); // safe because every replacement always replaces a standalone node with other standalone // nodes. @SuppressWarnings("unchecked") List<? extends C> typedReplacements = (List<? extends C>) replacements; parent.addChildren(i, typedReplacements); i += replacements.size() - 1; replacements.clear(); } } checkState(replacements.isEmpty()); } } }