/* * 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.common.css.compiler.ast; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.base.Functions; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Unit tests for {@code CssStringNode} * */ @RunWith(JUnit4.class) public class CssStringNodeTest { @Test public void testCssValueNodeRoundtrip() throws Exception { String v = "ordinary"; for (CssStringNode.Type t : CssStringNode.Type.values()) { CssStringNode n = new CssStringNode(t, v); n.setValue(v); assertThat(n.getValue()).isEqualTo(v); } } @Test public void testCssValueNodeFixedPoint() throws Exception { // This test doesn't care if setValue/getValue work in terms of // CSS or abstract values, but we just want to make sure that // eventually what we set is what we get. // Here's a value that must be escaped in CSS. It ensures that // we exercise whatever codec is in operation behind these // accessors. String v = "\\\\\"\""; CssStringNode n = new CssStringNode( CssStringNode.Type.DOUBLE_QUOTED_STRING, v); String v2 = n.getValue(); n.setValue(v2); assertThat(n.getValue()).isEqualTo(v2); } @Test public void testConcreteRoundtrip() throws Exception { // This value is safe for verbatim inclusion in either kind of // string literal, and for each kind it includes: // (a) an escape sequence that denotes a character that must // be escaped in literals of that kind. // (b) a character (possibly as part of an escape sequence) // that must be escaped in literals of that kind // Also, it includes weird escape sequences that are unlikely // to occur together in sane usage, which helps demonstrate // normalization. String v = "\\'\\\"\\041zA\\0000411\\41 1"; // ^single quote // ^double quote // ^weird A // ^z // ^A // ^weird A, second variation // ^1 // ^A with escape code delimiter // ^1 for (CssStringNode.Type t : CssStringNode.Type.values()) { CssStringNode n = new CssStringNode(t, v); n.setConcreteValue(v); assertThat(n.getConcreteValue()).isEqualTo(v); } } @Test public void testShortEscaper() throws Exception { for (String[] io : new String[][] { {"", ""}, {"a", "a"}, {"¤", "\\a4"}, {"ξ", "\\3be"}, {"ξe", "\\3be e"}, {"ξx", "\\3bex"}, {"唐", "\\5510"}, {"𠍱", "\\20371"}, {new String( new byte[] {(byte) 0xf0, (byte) 0xa0, (byte) 0x8d, (byte) 0xb1}, UTF_8), "\\20371"}}) { assertThat(CssStringNode.SHORT_ESCAPER.apply(io[0])).isEqualTo(io[1]); } // Six-hexadecimal-digit codepoints aren't allowed // leading-zeros-padding. This is also an interesting case because // Java chars and Strings are defined in terms of UTF-16, which // represents codepoints in this range as surrogate pairs. Let's // use UTF-8 to specify the input because it's simpler: byte[] puabUtf8 = {(byte) 0xf4, (byte) 0x80, (byte) 0x80, (byte) 0x80}; assertThat(CssStringNode.SHORT_ESCAPER.apply(new String(puabUtf8, UTF_8))) .isEqualTo("\\100000"); assertThat(CssStringNode.SHORT_ESCAPER.apply(String.format("%sa", new String(puabUtf8, UTF_8)))) .isEqualTo("\\100000a"); } @Test public void testInsertsIgnoredWhitespaceAfterEscape() throws Exception { // When parsing, we always discard zero or one whitespace after an // escape sequence. // See http://www.w3.org/TR/CSS2/syndata.html#characters // and http://www.w3.org/TR/css3-syntax/#consume-an-escaped-code-point String stringTemplate = "%s following (%s)"; String cssTemplate = "%s following (%s)"; for (CssStringNode.Type type : CssStringNode.Type.values()) { // We produce escape sequences in three cases: // (1) newline assertThat( CssStringNode.escape( type, CssStringNode.HTML_ESCAPER, String.format(stringTemplate, "\n", type.getClass().getName()))) .isEqualTo(String.format(cssTemplate, "\\00000a", type.getClass().getName())); // (2) no CSS literal representation exists assertThat( CssStringNode.escape( type, CssStringNode.SHORT_ESCAPER, String.format(stringTemplate, "¤", type.getClass().getName()))) .isEqualTo(String.format(cssTemplate, "\\a4", type.getClass().getName())); // (3) HTML/SGML special character when using the HTML_ESCAPER assertThat( CssStringNode.escape( type, CssStringNode.HTML_ESCAPER, String.format(stringTemplate, "<", type.getClass().getName()))) .isEqualTo(String.format(cssTemplate, "\\00003c", type.getClass().getName())); } } @Test public void testHtmlEscaper() throws Exception { for (String[] io : new String[][] { {"", ""}, {"a", "a"}, {"¤", "\\0000a4"}, {"ξ", "\\0003be"}, {"ξe", "\\0003bee"}, {"ξx", "\\0003bex"}, {"唐", "\\005510"}, {"𠍱", "\\020371"}, {new String( new byte[] {(byte) 0xf0, (byte) 0xa0, (byte) 0x8d, (byte) 0xb1}, UTF_8), "\\020371"}}) { assertThat(CssStringNode.HTML_ESCAPER.apply(io[0])).isEqualTo(io[1]); } assertThat(CssStringNode.HTML_ESCAPER.apply("&")).isEqualTo("\\000026"); assertThat(CssStringNode.HTML_ESCAPER.apply("<")).isEqualTo("\\00003c"); assertThat(CssStringNode.HTML_ESCAPER.apply(">")).isEqualTo("\\00003e"); assertThat(CssStringNode.HTML_ESCAPER.apply("\"")).isEqualTo("\\000022"); assertThat(CssStringNode.HTML_ESCAPER.apply("'")).isEqualTo("\\000027"); } @Test public void testEscape() throws Exception { for (String[] io : new String[][] { {"", ""}, {"a", "a"}, {"\\", "\\\\"}, {"19\\3=6", "19\\\\3=6"}, {"say \"hello\"", "say \\\"hello\\\""}, {"say 'goodbye'", "say 'goodbye'"}}) { assertThat( CssStringNode.escape( CssStringNode.Type.DOUBLE_QUOTED_STRING, Functions.<String>identity(), io[0])) .isEqualTo(io[1]); } assertThat( CssStringNode.escape( CssStringNode.Type.SINGLE_QUOTED_STRING, Functions.<String>identity(), "say 'goodbye'")) .isEqualTo("say \\'goodbye\\'"); } @Test public void testUnescape() throws Exception { for (String[] io : new String[][] { {"", ""}, {"a", "a"}, {"\\\\", "\\"}, {"\\\"", "\""}, {"\\41", "A"}, {"\\41 ", "A"}, {"\\41 ", "A "}, // The spec requires us to discard a whitespace following an // escape sequence, even when the escape sequence is as long // as possible and hence there is no ambiguity about where it // ends. {"abc\\000041 ", "abcA"}, {"abc\\000041 ", "abcA "}, {"\\41x", "Ax"}, {"\\0000a4", "¤"}, {"\\0003be", "ξ"}, {"\\0003bee", "ξe"}, {"\\3be e", "ξe"}, {"\\0003be", "ξ"}, {"\\3be z", "ξz"}, {"\\005510", "唐"}, {"\\020371", "𠍱"}, {"\\020371", new String( new byte[] {(byte) 0xf0, (byte) 0xa0, (byte) 0x8d, (byte) 0xb1}, UTF_8)}}) { assertThat(CssStringNode.unescape(io[0])).isEqualTo(io[1]); } // Now let's look at a character that requires a max-length escape // code in CSS and use of surrogate pairs in the JVM byte[] puabUtf8 = {(byte) 0xf4, (byte) 0x80, (byte) 0x80, (byte) 0x80}; assertThat(CssStringNode.unescape("\\100000")).isEqualTo(new String(puabUtf8, UTF_8)); assertThat(CssStringNode.unescape("\\100000a")).isEqualTo(new String(puabUtf8, UTF_8) + "a"); // Here's an escape sequence denoting a code point beyond the Java // char/String repertoire. According to CSS 2.1, we can replace the // escape. int bigCodePoint = Character.MAX_CODE_POINT + 1; assertThat(CssStringNode.unescape(String.format("\\%x", bigCodePoint))) .isEqualTo( // replacement character "\ufffd"); } @Test public void testCopyCtor() { CssStringNode a = new CssStringNode( CssStringNode.Type.DOUBLE_QUOTED_STRING, "foo"); a.setConcreteValue("\\0066oobar"); CssStringNode b = new CssStringNode(a); assertThat(a.getConcreteValue()).isEqualTo("\\0066oobar"); assertThat(b.getConcreteValue()).isEqualTo(a.getConcreteValue()); assertThat(b.getValue()).isEqualTo(a.getValue()); } @Test public void testStringCannotDirectlyContainNewline() { // See http://www.w3.org/TR/CSS2/syndata.html#strings CssStringNode a = new CssStringNode( CssStringNode.Type.SINGLE_QUOTED_STRING, "line1\nline2"); assertWithMessage("We should support the Java String representation of newlines.") .that(a.getValue().contains("\n")) .isTrue(); assertWithMessage( "If we set a Java newline, it should be escaped in the" + " generated concrete value.") .that(a.getConcreteValue().contains("\n")) .isFalse(); assertWithMessage("If we ask for CSS markup, we should escape newlines per the" + " CSS spec.") .that(a.toString(CssStringNode.HTML_ESCAPER).contains("\n")) .isFalse(); assertWithMessage("Escaping a new line shouldn't affect the left hand side") .that(a.toString(CssStringNode.HTML_ESCAPER).startsWith("'line1")) .isTrue(); assertWithMessage("Escaping a new line shouldn't affect the right-hand side") .that(a.toString(CssStringNode.HTML_ESCAPER).endsWith("line2'")) .isTrue(); } }