/*
* 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.truth.Truth.assertThat;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.truth.StringSubject;
import com.google.template.soy.base.internal.IncrementingIdGenerator;
import com.google.template.soy.base.internal.SoyFileKind;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.error.ExplodingErrorReporter;
import com.google.template.soy.soyparse.SoyFileParser;
import com.google.template.soy.soytree.HtmlAttributeNode;
import com.google.template.soy.soytree.HtmlAttributeValueNode;
import com.google.template.soy.soytree.HtmlCloseTagNode;
import com.google.template.soy.soytree.HtmlOpenTagNode;
import com.google.template.soy.soytree.RawTextNode;
import com.google.template.soy.soytree.SoyFileNode;
import com.google.template.soy.soytree.SoyNode;
import com.google.template.soy.soytree.SoyNode.ParentSoyNode;
import com.google.template.soy.soytree.SoyTreeUtils;
import com.google.template.soy.soytree.TemplateNode;
import com.google.template.soy.types.SoyTypeRegistry;
import java.io.StringReader;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public final class HtmlRewritePassTest {
@Test
public void testTags() {
TemplateNode node = runPass("<div></div>");
assertThat(node.getChild(0)).isInstanceOf(RawTextNode.class);
assertThat(node.getChild(1)).isInstanceOf(HtmlOpenTagNode.class);
assertThat(node.getChild(2)).isInstanceOf(HtmlCloseTagNode.class);
assertThatSourceString(node).isEqualTo("<div></div>");
assertThatASTString(node)
.isEqualTo(
"HTML_OPEN_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ "HTML_CLOSE_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ "");
}
@Test
public void testAttributes() {
TemplateNode node = runPass("<div class=\"foo\"></div>");
assertThatSourceString(node).isEqualTo("<div class=\"foo\"></div>");
String structure =
""
+ "HTML_OPEN_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " HTML_ATTRIBUTE_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " HTML_ATTRIBUTE_VALUE_NODE\n"
+ " RAW_TEXT_NODE\n"
+ "HTML_CLOSE_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ "";
assertThatASTString(node).isEqualTo(structure);
// test alternate quotation marks
node = runPass("<div class='foo'></div>");
assertThatSourceString(node).isEqualTo("<div class='foo'></div>");
assertThatASTString(node).isEqualTo(structure);
node = runPass("<div class=foo></div>");
assertThatSourceString(node).isEqualTo("<div class=foo></div>");
assertThatASTString(node).isEqualTo(structure);
// This is a tricky case, according to the spec the '/' belongs to the attribute, not the tag
node = runPass("<div class=foo/>");
assertThatSourceString(node).isEqualTo("<div class=foo/>");
HtmlOpenTagNode openTag = (HtmlOpenTagNode) node.getChild(1);
assertThat(openTag.isSelfClosing()).isFalse();
HtmlAttributeValueNode attributeValue =
(HtmlAttributeValueNode) ((HtmlAttributeNode) openTag.getChild(1)).getChild(1);
assertThat(attributeValue.getQuotes()).isEqualTo(HtmlAttributeValueNode.Quotes.NONE);
assertThat(((RawTextNode) attributeValue.getChild(0)).getRawText()).isEqualTo("foo/");
}
@Test
public void testLetAttributes() {
TemplateNode node = runPass("{let $foo kind=\"attributes\"}class='foo'{/let}");
assertThatSourceString(node).isEqualTo("{let $foo kind=\"attributes\"}class='foo'{/let}");
String structure =
""
+ "LET_CONTENT_NODE\n"
+ " HTML_ATTRIBUTE_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " HTML_ATTRIBUTE_VALUE_NODE\n"
+ " RAW_TEXT_NODE\n";
assertThatASTString(node).isEqualTo(structure);
}
@Test
public void testSelfClosingTag() {
TemplateNode node = runPass("<input/>");
assertThatSourceString(node).isEqualTo("<input/>");
// NOTE: the whitespace difference
node = runPass("<input />");
assertThatSourceString(node).isEqualTo("<input/>");
}
@Test
public void testTextNodes() {
TemplateNode node = runPass("x x<div>content</div> <div>{sp}</div>");
assertThatSourceString(node).isEqualTo("x x<div>content</div> <div> </div>");
assertThatASTString(node)
.isEqualTo(
""
+ "RAW_TEXT_NODE\n"
+ "HTML_OPEN_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ "RAW_TEXT_NODE\n"
+ "HTML_CLOSE_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ "RAW_TEXT_NODE\n"
+ "HTML_OPEN_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ "RAW_TEXT_NODE\n"
+ "HTML_CLOSE_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ "");
}
@Test
public void testDynamicTagName() {
TemplateNode node = runPass("{let $t : 'div' /}<{$t}>content</{$t}>");
assertThatSourceString(node).isEqualTo("{let $t : 'div' /}<{$t}>content</{$t}>");
// NOTE: the print nodes don't end up in the AST due to how TagName works, this is probably a
// bad idea in the long run. We should probably make TagName be a node.
assertThatASTString(node)
.isEqualTo(
""
+ "LET_VALUE_NODE\n"
+ "HTML_OPEN_TAG_NODE\n"
+ " PRINT_NODE\n"
+ "RAW_TEXT_NODE\n"
+ "HTML_CLOSE_TAG_NODE\n"
+ " PRINT_NODE\n"
+ "");
}
@Test
public void testDynamicAttributeValue() {
TemplateNode node = runPass("{let $t : 'x' /}<div a={$t}>content</div>");
assertThatSourceString(node).isEqualTo("{let $t : 'x' /}<div a={$t}>content</div>");
assertThatASTString(node)
.isEqualTo(
""
+ "LET_VALUE_NODE\n"
+ "HTML_OPEN_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " HTML_ATTRIBUTE_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " HTML_ATTRIBUTE_VALUE_NODE\n"
+ " PRINT_NODE\n"
+ "RAW_TEXT_NODE\n"
+ "HTML_CLOSE_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ "");
// try alternate quotes
node = runPass("{let $t : 'x' /}<div a=\"{$t}\">content</div>");
assertThatSourceString(node).isEqualTo("{let $t : 'x' /}<div a=\"{$t}\">content</div>");
node = runPass("{let $t : 'x' /}<div a='{$t}'>content</div>");
assertThatSourceString(node).isEqualTo("{let $t : 'x' /}<div a='{$t}'>content</div>");
}
@Test
public void testDynamicAttribute() {
TemplateNode node = runPass("{let $t : 'x' /}<div {$t}>content</div>");
assertThatSourceString(node).isEqualTo("{let $t : 'x' /}<div {$t}>content</div>");
assertThatASTString(node)
.isEqualTo(
""
+ "LET_VALUE_NODE\n"
+ "HTML_OPEN_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " HTML_ATTRIBUTE_NODE\n"
+ " PRINT_NODE\n"
+ "RAW_TEXT_NODE\n"
+ "HTML_CLOSE_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ "");
// and with a value
node = runPass("{let $t : 'x' /}<div {$t}=x>content</div>");
assertThatSourceString(node).isEqualTo("{let $t : 'x' /}<div {$t}=x>content</div>");
assertThatASTString(node)
.isEqualTo(
""
+ "LET_VALUE_NODE\n"
+ "HTML_OPEN_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " HTML_ATTRIBUTE_NODE\n"
+ " PRINT_NODE\n"
+ " HTML_ATTRIBUTE_VALUE_NODE\n"
+ " RAW_TEXT_NODE\n"
+ "RAW_TEXT_NODE\n"
+ "HTML_CLOSE_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ "");
}
@Test
public void testConditionalAttribute() {
TemplateNode node = runPass("{let $t : 'x' /}<div {if $t}foo{else}bar{/if}>content</div>");
assertThatSourceString(node)
.isEqualTo("{let $t : 'x' /}<div{if $t} foo{else} bar{/if}>content</div>");
assertThatASTString(node)
.isEqualTo(
""
+ "LET_VALUE_NODE\n"
+ "HTML_OPEN_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " IF_NODE\n"
+ " IF_COND_NODE\n"
+ " HTML_ATTRIBUTE_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " IF_ELSE_NODE\n"
+ " HTML_ATTRIBUTE_NODE\n"
+ " RAW_TEXT_NODE\n"
+ "RAW_TEXT_NODE\n"
+ "HTML_CLOSE_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ "");
}
@Test
public void testConditionalAttributeValue() {
TemplateNode node =
runPass("{let $t : 'x' /}<div class=\"{if $t}foo{else}bar{/if}\">content</div>");
assertThatSourceString(node)
.isEqualTo("{let $t : 'x' /}<div class=\"{if $t}foo{else}bar{/if}\">content</div>");
assertThatASTString(node)
.isEqualTo(
""
+ "LET_VALUE_NODE\n"
+ "HTML_OPEN_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " HTML_ATTRIBUTE_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " HTML_ATTRIBUTE_VALUE_NODE\n"
+ " IF_NODE\n"
+ " IF_COND_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " IF_ELSE_NODE\n"
+ " RAW_TEXT_NODE\n"
+ "RAW_TEXT_NODE\n"
+ "HTML_CLOSE_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ "");
}
// TODO(lukes): ideally these would all be implemented in the CompilerIntegrationTests but the
// ContextualAutoescaper rejects these forms. once we stop 'desuraging' prior to the autoescaper
// we can move these tests over.
@Test
public void testConditionalContextMerging() {
TemplateNode node = runPass("{@param p : ?}<div {if $p}foo=bar{else}baz{/if}>");
assertThatSourceString(node).isEqualTo("<div{if $p} foo=bar{else} baz{/if}>");
assertThatASTString(node)
.isEqualTo(
""
+ "HTML_OPEN_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " IF_NODE\n"
+ " IF_COND_NODE\n"
+ " HTML_ATTRIBUTE_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " HTML_ATTRIBUTE_VALUE_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " IF_ELSE_NODE\n"
+ " HTML_ATTRIBUTE_NODE\n"
+ " RAW_TEXT_NODE\n"
+ "");
node = runPass("{@param p : ?}<div {if $p}class=x{else}style=\"baz\"{/if}>");
assertThatSourceString(node).isEqualTo("<div{if $p} class=x{else} style=\"baz\"{/if}>");
node = runPass("{@param p : ?}<div {if $p}class='x'{else}style=\"baz\"{/if}>");
assertThatSourceString(node).isEqualTo("<div{if $p} class='x'{else} style=\"baz\"{/if}>");
}
// Ideally, we wouldn't support this pattern since it adds a fair bit of complexity
@Test
public void testConditionalQuotedAttributeValues() {
TemplateNode node = runPass("{@param p : ?}<div x={if $p}'foo'{else}'bar'{/if} {$p}>");
assertThatSourceString(node).isEqualTo("<div x={if $p}'foo'{else}'bar'{/if} {$p}>");
assertThatASTString(node)
.isEqualTo(
""
+ "HTML_OPEN_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " HTML_ATTRIBUTE_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " IF_NODE\n"
+ " IF_COND_NODE\n"
+ " HTML_ATTRIBUTE_VALUE_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " IF_ELSE_NODE\n"
+ " HTML_ATTRIBUTE_VALUE_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " HTML_ATTRIBUTE_NODE\n"
+ " PRINT_NODE\n"
+ "");
node =
runPass(
"{@param p : ?}{@param p2 : ?}<div x={if $p}{if $p2}'foo'{else}'bar'{/if}"
+ "{else}{if $p2}'foo'{else}'bar'{/if}{/if} {$p}>");
assertThatSourceString(node)
.isEqualTo(
"<div x={if $p}{if $p2}'foo'{else}'bar'{/if}{else}{if $p2}'foo'{else}'bar'{/if}{/if}"
+ " {$p}>");
assertThatASTString(node)
.isEqualTo(
""
+ "HTML_OPEN_TAG_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " HTML_ATTRIBUTE_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " IF_NODE\n"
+ " IF_COND_NODE\n"
+ " IF_NODE\n"
+ " IF_COND_NODE\n"
+ " HTML_ATTRIBUTE_VALUE_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " IF_ELSE_NODE\n"
+ " HTML_ATTRIBUTE_VALUE_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " IF_ELSE_NODE\n"
+ " IF_NODE\n"
+ " IF_COND_NODE\n"
+ " HTML_ATTRIBUTE_VALUE_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " IF_ELSE_NODE\n"
+ " HTML_ATTRIBUTE_VALUE_NODE\n"
+ " RAW_TEXT_NODE\n"
+ " HTML_ATTRIBUTE_NODE\n"
+ " PRINT_NODE\n"
+ "");
}
@Test
public void testConditionalUnquotedAttributeValue() {
TemplateNode node = runPass("{@param p : ?}<div class={if $p}x{else}y{/if}>");
assertThatSourceString(node).isEqualTo("<div class={if $p}x{else}y{/if}>");
}
private static TemplateNode runPass(String input) {
return runPass(input, ExplodingErrorReporter.get());
}
/** Parses the given input as a template content. */
private static TemplateNode runPass(String input, ErrorReporter errorReporter) {
String soyFile =
Joiner.on('\n')
.join("{namespace ns}", "", "{template .t stricthtml=\"true\"}", input, "{/template}");
IncrementingIdGenerator nodeIdGen = new IncrementingIdGenerator();
SoyFileNode node =
new SoyFileParser(
new SoyTypeRegistry(),
nodeIdGen,
new StringReader(soyFile),
SoyFileKind.SRC,
"test.soy",
errorReporter)
.parseSoyFile();
if (node != null) {
new HtmlRewritePass(ImmutableList.of("stricthtml"), errorReporter).run(node, nodeIdGen);
return node.getChild(0);
}
return null;
}
private static StringSubject assertThatSourceString(TemplateNode node) {
SoyFileNode parent = SoyTreeUtils.cloneNode(node.getParent());
new DesugarHtmlNodesPass().run(parent, new IncrementingIdGenerator());
StringBuilder sb = new StringBuilder();
parent.getChild(0).appendSourceStringForChildren(sb);
return assertThat(sb.toString());
}
private static StringSubject assertThatASTString(TemplateNode node) {
SoyFileNode parent = SoyTreeUtils.cloneNode(node.getParent());
new CombineConsecutiveRawTextNodesVisitor(new IncrementingIdGenerator()).exec(parent);
return assertThat(buildAstString(parent.getChild(0), 0, new StringBuilder()).toString());
}
private static StringBuilder buildAstString(ParentSoyNode<?> node, int indent, StringBuilder sb) {
for (SoyNode child : node.getChildren()) {
sb.append(Strings.repeat(" ", indent)).append(child.getKind()).append('\n');
if (child instanceof ParentSoyNode) {
buildAstString((ParentSoyNode<?>) child, indent + 1, sb);
}
}
return sb;
}
}