/* * 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.common.css.compiler.ast; import com.google.common.collect.ImmutableList; import com.google.common.css.SourceCode; import com.google.common.css.SourceCodeLocation; import com.google.common.css.compiler.passes.CompactPrinter; import com.google.common.css.compiler.passes.testing.AstPrinter; import junit.framework.TestCase; import java.util.List; /** * Unit tests for the {@link GssParser}. * * @author fbenz@google.com (Florian Benz) */ public class GssParserTest extends TestCase { private CssTree testValid(String gss) throws GssParserException { CssTree tree = parse(gss); assertNotNull(tree); return tree; } private void testTree(String gss, String output) throws GssParserException { CssTree tree = parse(gss); assertNotNull(tree); CssRootNode root = tree.getRoot(); assertNotNull(root); assertEquals(output, AstPrinter.print(tree)); } public void testManySources() throws Exception { CssTree tree = parse(ImmutableList.of( new SourceCode("test1", "a {}"), new SourceCode("test2", "@component c { x {y: z} }"), new SourceCode("test3", "b {}"))); CssRootNode root = tree.getRoot(); assertNotNull(root); assertEquals("[[a]{[]}@component [c]{[x]{[y:[[z]];]}}[b]{[]}]", AstPrinter.print(tree)); } public void testAst1() throws Exception { testTree("a {}", "[[a]{[]}]"); } public void testAst2() throws Exception { testTree("a.b c#d > e.f + g {}", "[[a.b c#d>e.f+g]{[]}]"); } public void testAst3() throws Exception { testTree("a {x: y}", "[[a]{[x:[[y]];]}]"); } public void testAst4() throws Exception { testTree("a {w: x; y: z}", "[[a]{[w:[[x]];y:[[z]];]}]"); } public void testAst5() throws Exception { testTree("a {b: 1em}", "[[a]{[b:[[1em]];]}]"); } public void testAst6() throws Exception { testTree("a {b: 1.5em}", "[[a]{[b:[[1.5em]];]}]"); } public void testAst7() throws Exception { testTree("a {b: 'x'}", "[[a]{[b:[['x']];]}]"); } public void testAst8() throws Exception { testTree("a {b: url(#x)}", "[[a]{[b:[url(#x)];]}]"); } public void testAst9() throws Exception { testTree("a {b: url('#x')}", "[[a]{[b:[url('#x')];]}]"); } public void testAst10() throws Exception { testTree("a {b: x y z}", "[[a]{[b:[[x][y][z]];]}]"); } public void testAst11() throws Exception { testTree("a {b: c,d,e/f g,h i j,k}", "[[a]{[b:[[[c],[d],[[e]/[f]]][[g],[h]][i][[j],[k]]];]}]"); } public void testAst12() throws Exception { testTree("a {b: rgb(0,0,0)}", "[[a]{[b:[rgb(0,0,0)];]}]"); } public void testAst13() throws Exception { testTree("a {b: custom(0,0)}", "[[a]{[b:[custom(0,0)];]}]"); } public void testAst14() throws Exception { testTree("@def a b;", "[@def [a] [b];]"); } public void testAst15() throws Exception { testTree("@component a { x {y: z} }", "[@component [a]{[x]{[y:[[z]];]}}]"); } public void testAst16() throws Exception { testTree("a:foo {\n bla : d ; }", "[[a:foo]{[bla:[[d]];]}]"); } public void testAst17() throws Exception { testTree("foo {f: rgb(o=0);}", "[[foo]{[f:[rgb([[o]=[0]])];]}]"); } public void testAst18() throws Exception { testTree("a:lang(c) { d: e }", "[[a:lang(c)]{[d:[[e]];]}]"); } public void testAst19() throws Exception { testTree("a~b { d: e }", "[[a~b]{[d:[[e]];]}]"); } public void testAst20() throws Exception { testTree("a:b(-2n+3) { d: e }", "[[a:b(-2n+3)]{[d:[[e]];]}]"); } public void testAst21() throws Exception { testTree("a:not(#id) { d: e }", "[[a:not(#id)]{[d:[[e]];]}]"); } public void testAst22() throws Exception { testTree(".a { d:e,f }", "[[.a]{[d:[[[e],[f]]];]}]"); } public void testAst23() throws Exception { testTree(".a { d:e f,g h }", "[[.a]{[d:[[e][[f],[g]][h]];]}]"); } public void testAst24() throws Exception { testTree("a~b/deep/c { d: e }", "[[a~b/deep/c]{[d:[[e]];]}]"); } public void testParsingRules1() throws Exception { testValid("css_rule33 {\n" + "border: black ; /* comment */\n" + "height : 1em\n" + " }" ); } // We don't test for comments between '!' and 'important'. See the comment on // the IMPORTANT_SYM in the grammar for the reason. public void testParsingRules2() throws Exception { testValid("ul.navbar {\n" + " position: absolute;\n" + " top: top;\n" + " left: down;\n" + " width: nice }\n" + "\n" + ".foo {\n" + " position: absolute ! important ;\n" + "}\n" + ".bar {\n" + " position: absolute ! important;\n\n\n" + "}" ); } public void testParsingRules3() throws Exception { testValid("css_rule33 test2 {\n" + "border: black ; /* comment */\n" + "height : 1em\n" + " }" ); } public void testParsingRules4() throws Exception { testValid("p:before {content: counter(par-num, upper-roman) \". \"}"); } public void testParsingSelector1() throws Exception { testValid("a b { x: y}"); } public void testParsingSelector2() throws Exception { testValid("a > b { x: y}"); } public void testParsingSelector3() throws Exception { testValid("a + b { x: y}"); } public void testParsingSelector4() throws Exception { testValid("a + b > c d e.f + g { x: y}"); } public void testParsingSelector5() throws Exception { testValid("a + b > c d e.f#d + g {}"); } public void testParsingSelector6() throws Exception { testValid("a ~ b { x: y}"); } public void testParsingSelector7() throws Exception { testValid("a /deep/ b { x: y}"); } public void testParsingExpr1() throws Exception { testValid("aab {x:s r t}"); } public void testParsingExpr2() throws Exception { testValid("aab {x:s 1em t}"); } public void testParsingExpr3() throws Exception { testValid("aab {x:-1px +1px -1px 1.7px}"); } public void testParsingURL() throws Exception { testValid("a { x: url('http://test.com') }"); } public void testParsingHexcolor() throws Exception { testValid("a { x: #fff }"); } public void testParsingFunction1Arg() throws Exception { testValid("a { x: f(1) }"); } public void testParsingFunctionManyArgs() throws Exception { testValid("a { x: f(1, 2, 3) }"); } public void testParsingFilterFunctions() throws Exception { testValid("a { filter: drop-shadow(1 2 3) custom(1 2 3);" + "filter: drop-shadow(1, 2, 3) custom(1, 2, 3);}"); } public void testParsingWebkitFilterFunctions() throws Exception { testValid("a { filter: -webkit-drop-shadow(1 2) -webkit-custom(1 2);" + "filter: -webkit-drop-shadow(1, 2) -webkit-custom(1, 2);}"); } public void testParsingLocalFunctions() throws Exception { testValid("@font-face { src: local(Gentium), url(Gentium.woff);" + "src: local(Gentium Bold), local(Gentium-Bold), url(GentiumBold.woff);}"); } public void testParsingAt1() throws Exception { testValid("@import url('http://test.com/test.css');"); } public void testParsingAt2() throws Exception { testValid("@import url(http://test.com/test.css);"); } public void testParsingAt3() throws Exception { testValid("@component a extends b {\n" + "@def z 1;\n" + "x {y: z}\n" + "}"); } public void testParsingDef1() throws Exception { testValid("@def RC_TOP_LEFT tl;\n" + "@def RC_TOP_RIGHT tr;\n" + "@def BASE_WARNING_LINK_COLOR #c3d9ff; /* light blue */" ); } public void testParsingDef3() throws Exception { testValid("@def A_B /* @default */ inherit;"); } public void testParsingAttribute1() throws Exception { testValid("a[href=\"http://www.w3.org/\"]{\n" + "bla:d\n" + "}"); } public void testParsingAttribute2() throws Exception { testValid("*[lang|=\"en\"] { color : red }"); } public void testParsingPseudo1() throws Exception { testValid("a:foo {\n bla : d ; }"); } public void testParsingPseudo2() throws Exception { testValid("a:lang(en) {\n bla : d ; }"); } public void testParsingIf1() throws Exception { testValid("@if (RTL_LANG) {\n" + " @def RTL_FLAG 1; \n" + " @def LEFT right;\n" + "} @else {\n" + " @def IMGURL url('images/image.gif');\n" + "}"); } public void testParsingIf2() throws Exception { testValid("@if BROWSER_IE6 {\n" + " @def FUNBOX_MARGIN 0;\n" + "} @elseif BROWSER_IE {\n" + " @def FUNBOX_MARGIN 1 0 -1px 0;\n" + "} @elseif BROWSER_FF3_OR_HIGHER {\n" + " @def FUNBOX_MARGIN -2px 0 0 0;\n" + "} @else {\n" + " @if(A) { @def BB 23; }\n" + " @def FUNBOX_MARGIN -2px 0 -1px 0;\n" + "}"); } public void testParsingIf3() throws Exception { testValid("@if (RTL_LANG) {\n" + " CSS_RULE2.CLASS#id{ d:34em; }\n" + "} @else {\n" + "}"); } public void testParsingParenthesizedTerm() throws Exception { testValid("@if (FOO) { x { y: z } }"); } public void testParsingBooleanTerm1() throws Exception { testValid("@if ( A && (!B || C )) { @def RTL_FLAG 1;}"); } public void testParsingBooleanTerm2() throws Exception { testValid("@if (!A && !B || C || !(F && G ) ) { @def RTL_FLAG 1;}"); } public void testParsingComplexDef1() throws Exception { testValid("@def A a, b, c;"); } public void testParsingComplexDef2() throws Exception { testValid("@def FONT a, b, c 14px/2em #fff;"); } public void testParsingEqualsOperator() throws Exception { testValid(".CSS_ {\n" + " filter: alpha(opacity = 85) ;\n" + "}"); } public void testParsingColonFunctionName() throws Exception { testValid("x {y: a.b:c(d)}"); } public void testParsingColonFunctionName2() throws Exception { testValid(".CSS_ {\n" + "-ms-filter: \"progid:DXImageTr.Microsoft.Alpha(Opacity=80)\" ;\n" + "filter: progid:DXImageTr.Microsoft.AlphaImageLoader" + "(src='images/muc_bubble_left.png', sizingMethod='scale' );\n" + "}"); } public void testParsingEmptyPseudo() throws Exception { testValid("::a, :a[b]::c { x: y}"); } public void testParsingArbitraryDim() throws Exception { testValid("a {x: 2emelet 3x 5t}"); } public void testSelectorWithSpace() throws Exception { testValid("a /* x */ , b {x: y}"); } public void testIeRect() throws Exception { // Non-standard IE workaround. testValid(".a { clip: rect(0 0 0 0);}"); } public void testEllipse() throws Exception { testValid(".a { clip-path: ellipse(150px 300px at 50% 50%);}"); } public void testInset() throws Exception { testValid(".a { clip-path: inset(100px 100px 100px 100px);}"); } public void testCircle() throws Exception { testValid(".a { clip-path: circle(50% at right 5px bottom 10px);}"); } public void testPolygon() throws Exception { testValid(".a { clip-path: polygon(0 0, 0 300px, 300px 600px);}"); } public void testEqualAttribute() throws Exception { testValid("h1[foo=\"bar\"] {x : y}"); } public void testCaretEqualAttribute() throws Exception { testValid("h1[foo^=\"bar\"] {x : y}"); } public void testDollarEqualAttribute() throws Exception { testValid("h1[foo$=\"bar\"] {x : y}"); } public void testAsteriskEqualAttribute() throws Exception { testValid("h1[foo*=\"bar\"] {x : y}"); } public void testPipeEqualAttribute() throws Exception { testValid("h1[foo|=\"bar\"] {x : y}"); } public void testImageSet() throws Exception { testValid("div:before {" + "content: -webkit-image-set(url(a.png) 1x, url(b.png) 2x);" + "content: -moz-image-set(url(a.png) 1x, url(b.png) 2x);" + "content: -o-image-set(url(a.png) 1x, url(b.png) 2x);" + "content: image-set(url(a.png) 1x, url(b.png) 2x);" + "}"); } public void testWebkitGradient() throws Exception { CssTree tree = testValid(".CSS { background: " + "-webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#ddd)) }"); CssRootNode root = tree.getRoot(); assertNotNull(root); assertEquals("[[.CSS]{[background:[" + "-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#ddd))];]}]", AstPrinter.print(tree)); CssRulesetNode ruleset = (CssRulesetNode) tree.getRoot().getBody().getChildAt(0); CssDeclarationNode decl = (CssDeclarationNode) ruleset.getDeclarations().getChildAt(0); CssFunctionNode function = (CssFunctionNode) decl.getPropertyValue().getChildAt(0); CssFunctionArgumentsNode args = function.getArguments(); assertEquals("The argument list should be flattened, and contain " + "7 arguments + 6 separators (4 commas and 2 meaningful spaces).", 13, args.numChildren()); } public void testGradients() throws Exception { testValid("div {" + "a:radial-gradient(-88px, -500px, #6A6A7A, #333, #000);" + "b:radial-gradient(30% 30%, closest-corner, white, black);" + "c:radial-gradient(center, 5em 40px, white, black);" + "d:linear-gradient(bottom left, red 20px, yellow, green," + "blue 90%);" + "e:repeating-linear-gradient(left, red 10%, blue 30%);" + "f:repeating-radial-gradient(top left, circle, red, blue 10%," + "red 20%);" + "}"); } /* http://www.webkit.org/blog/1424/css3-gradients/ */ public void testWebkitGradients() throws Exception { testValid("div {" + "a:-webkit-radial-gradient(-88px, -500px, #6A6A7A, #333, #000);" + "b:-webkit-radial-gradient(30% 30%, closest-corner, white, black);" + "c:-webkit-radial-gradient(center, 5em 40px, white, black);" + "d:-webkit-linear-gradient(bottom left, red 20px, yellow, green," + "blue 90%);" + "e:-webkit-repeating-linear-gradient(left, red 10%, blue 30%);" + "f:-webkit-repeating-radial-gradient(top left, circle, red, blue 10%," + "red 20%);" + "}"); } public void testMozillaGradients() throws Exception { testValid("div {" + "a:-moz-radial-gradient(-88px, -500px, #6A6A7A, #333, #000);" + "b:-moz-radial-gradient(30% 30%, closest-corner, white, black);" + "c:-moz-radial-gradient(center, 5em 40px, white, black);" + "d:-moz-linear-gradient(bottom left, red 20px, yellow, green," + "blue 90%);" + "e:-moz-repeating-linear-gradient(left, red 10%, blue 30%);" + "f:-moz-repeating-radial-gradient(top left, circle, red, blue 10%," + "red 20%);" + "}"); } public void testOperaGradients() throws Exception { testValid("div {" + "a:-o-radial-gradient(-88px, -500px, #6A6A7A, #333, #000);" + "b:-o-radial-gradient(30% 30%, closest-corner, white, black);" + "c:-o-radial-gradient(center, 5em 40px, white, black);" + "d:-o-linear-gradient(bottom left, red 20px, yellow, green," + "blue 90%);" + "e:-o-repeating-linear-gradient(left, red 10%, blue 30%);" + "f:-o-repeating-radial-gradient(top left, circle, red, blue 10%," + "red 20%);" + "}"); } public void testInternetExplorerGradients() throws Exception { testValid("div {" + "a:-ms-radial-gradient(-88px, -500px, #6A6A7A, #333, #000);" + "b:-ms-radial-gradient(30% 30%, closest-corner, white, black);" + "c:-ms-radial-gradient(center, 5em 40px, white, black);" + "d:-ms-linear-gradient(bottom left, red 20px, yellow, green," + "blue 90%);" + "e:-ms-repeating-linear-gradient(left, red 10%, blue 30%);" + "f:-ms-repeating-radial-gradient(top left, circle, red, blue 10%," + "red 20%);" + "}"); } public void testKonquererGradients() throws Exception { // Taken from http://twitter.github.com/bootstrap/1.4.0/bootstrap.css testValid("div {" + "background-image: -khtml-gradient(linear, left top, left bottom, " + " from(#333333), to(#222222));" + "}"); } public void testWebkitMinDevicePixelRatio() throws Exception { testValid("@media screen and (-webkit-min-device-pixel-ratio:0) {}"); } public void testMediaQuery() throws Exception { testValid("@media screen and (max-height: 300px) and (min-width: 20px) {}"); } public void testMediaQueryRatioNoSpaces() throws Exception { testValid("@media screen and (aspect-ratio: 3/4) {}"); } public void testMediaQueryRatioWithSpaces() throws Exception { testValid("@media screen and (aspect-ratio: 3 / 4) {}"); } public void testMediaQueryRatioWithManyLeadingSpaces() throws Exception { testValid("@media screen and (aspect-ratio: 3 / 4) {}"); } public void testMediaQueryRatioWithTrailingSpaces() throws Exception { testValid("@media screen and (aspect-ratio: 3/ 4) {}"); } public void testMediaQueryRatioWithNoTrailingSpaces() throws Exception { testValid("@media screen and (aspect-ratio: 3 /4) {}"); } public void testMozLinearGradient() throws Exception { testValid(".CSS { background-image: " + "-moz-linear-gradient(bottom, #c0c0c0 0%, #dddddd 90%) }"); } public void testParsingWebkitKeyframes1() throws Exception { testValid("@-webkit-keyframes bounce {\n" + " from {\n" + " left: 0px;\n" + " }\n" + " to {\n" + " left: 200px;\n" + " }\n" + "}\n"); } public void testParsingMozKeyframes1() throws Exception { testValid("@-moz-keyframes bounce {\n" + " from {\n" + " left: 0px;\n" + " }\n" + " to {\n" + " left: 200px;\n" + " }\n" + "}\n"); } public void testParsingWebkitKeyframes2() throws Exception { testValid("@-webkit-keyframes pulse {\n" + " 0% {\n" + " background-color: red;\n" + " opacity: 1.0;\n" + " -webkit-transform: scale(1.0) rotate(0deg);\n" + " }\n" + " 33.33% {\n" + " background-color: blue;\n" + " opacity: 0.75;\n" + " -webkit-transform: scale(1.1) rotate(-5deg);\n" + " }\n" + " 66.66% {\n" + " background-color: green;\n" + " opacity: 0.5;\n" + " -webkit-transform: scale(1.1) rotate(5deg);\n" + " }\n" + " 100% {\n" + " background-color: red;\n" + " opacity: 1.0;\n" + " -webkit-transform: scale(1.0) rotate(0deg);\n" + " }\n" + "}"); } public void testParsingWebkitKeyframes3() throws Exception { testValid("@-webkit-keyframes bounce {\n" + " 0%, 51.2% {\n" + " left: 0px;\n" + " background: red;\n" + " }\n" + " 25%, 90.5% {\n" + " left: 200px;\n" + " background: green;\n" + " }\n" + " 25% {\n" + " background: blue;\n" + " }\n" + "}"); } public void testParsingWebkitKeyframes4() throws Exception { testValid("@-webkit-keyframes from {}"); testValid("@-webkit-keyframes to {}"); testValid("from {}"); testValid("to {}"); } public void testEscapingInDoubleQuoteString() throws Exception { testValid("body {content: \"\\0af9bcHH\"}"); testValid("body {content: \"\\0HH\"}"); testValid("body {content: \"\\aHH\"}"); testValid("body {content: \"\\gHH\"}"); testValid("body {content: \"\\\"'HH\"}"); } public void testEscapingInSingleQuoteString() throws Exception { testValid("body {content: '\\0af9bcHH'}"); testValid("body {content: '\\0HH'}"); testValid("body {content: '\\aHH'}"); testValid("body {content: '\\gHH'}"); testValid("body {content: '\"\\'HH'}"); } public void testPseudoFunction() throws Exception { testValid("div :lang(en) { color: #FFF; }"); testValid(":lang(fr) { color: #FFF; }"); } public void testPseudoNth() throws Exception { testValid("div :nth-child(1n+1) { color: #FFF; }"); testValid("div :nth-child(n+1) { color: #FFF; }"); testValid("div :nth-child(+n+2) { color: #FFF; }"); testValid("div :nth-child(n-1) { color: #FFF; }"); testValid("div :nth-child(-n-1) { color: #FFF; }"); testValid("div :nth-child(+2n+3) { color: #FFF; }"); testValid("div :nth-child(-5n+1) { color: #FFF; }"); // just 'n' is not supported by WebKit yet testValid("div :nth-child(n) { color: #FFF; }"); testValid("div :nth-child(-n) { color: #FFF; }"); testValid("div :nth-child(+n) { color: #FFF; }"); testValid("div :nth-child(n-0) { color: #FFF; }"); testValid("div :nth-child(0n+0) { color: #FFF; }"); testValid("div :nth-child(1) { color: #FFF; }"); testValid("div :nth-child(+7) { color: #FFF; }"); testValid("div :nth-child(-9) { color: #FFF; }"); testValid("div :nth-child(odd) { color: #FFF; }"); testValid("div :nth-child(even) { color: #FFF; }"); } public void testPseudoNot() throws Exception { testValid("p :not(.classy) { color: #123; }"); testValid("p :not(div) { color: #123; }"); testValid("p:not(div) { color: #123; }"); testValid("p :not( div ) { color: #123; }"); testValid("p :not(#id) { color: #123; }"); testValid("*:not(:link):not(:visited) {}"); } public void testPseudoElements() throws Exception { testValid("p::first-line { text-transform: uppercase }"); testValid("p::first-letter { color: green; font-size: 200% }"); testValid("div::after { color: #123; }"); testValid("div::before { color: #123; }"); } public void testOldPseudoElements() throws Exception { testValid("p:first-line { text-transform: uppercase }"); testValid("p:first-letter { color: green; font-size: 200% }"); testValid("div:after { color: #123; }"); testValid("div:before { color: #123; }"); } public void testMixinDefinitions() throws Exception { testValid("@defmixin name(PAR1, PAR2) { prop1: PAR1; prop2: PAR2 }"); testValid("@defmixin name( PAR1 , PAR2 )" + "{ prop1: PAR1; prop2: PAR2 }"); testValid("@defmixin name(PAR1, PAR2) { prop1: PAR1; prop2: CONST; }"); } public void testMixins() throws Exception { testValid("div { @mixin name(); }"); testValid("div { @mixin name( ) ; }"); testValid("div { prop1: val; @mixin defname(2px, #fff, 23%); }"); testValid("div { prop1: val; @mixin defname(); p:v;}"); testValid("div { @mixin foo(1px/1em); }"); testValid("div { @mixin foo(1px 1px); }"); } public void testUnquotedUrl() throws Exception { testValid("div { background-image: url(http://google.com/logo.png) }"); } public void testFunctionApplicationUrl() throws Exception { testValid("div { background-image: url(dataUrl('s')) }"); } public void testUrlOfFunctionOfId() throws Exception { // Bare URLs in function arguments are deprecated, but // we have some dependent code to cleanup before removing // the feature. testValid("div { background-image: url(dataUrl(x)); }"); } public void testFn() throws Exception { testValid("div { background-image: url(http://foo) }"); } public void testUrlPrefix() throws Exception { testTree("div { background-image: url-prefix(http://fo); }", "[[div]{[background-image:[url-prefix(http://fo)];]}]"); } public void testUrlPrefix2() throws Exception { testTree("div { background-image: url-prefix(fn(0)); }", "[[div]{[background-image:[url-prefix(fn(0))];]}]"); } public void testEmptyUrl() throws Exception { testValid("div { background-image: url() }"); } public void testUrlWithWhitespace() throws Exception { testTree("div { background-image: url( 'http://google.com/logo.png'); }", "[[div]{[background-image:" + "[url('http://google.com/logo.png')];]}]"); } public void testUnquotedUrlWithWhitespace() throws Exception { testTree("div { background-image: url( http://google.com/logo.png); }", "[[div]{[background-image:" + "[url(http://google.com/logo.png)];]}]"); } public void testCdoCdc() throws Exception { testTree( "<!--\ndiv { color: red; }\n-->", "[[div]{[color:[[red]];]}]"); } public void testIntraPropertyCdoCdc() throws Exception { String css = ".foo{border:1px<!--solid-->blue;}"; try { parse(css); fail("CDO should not be accepted in property values."); } catch (GssParserException e) { assertEquals( "The error should reflect that CDO is not accepted in property " + "values.", css.indexOf("<!--"), e.getGssError().getLocation().getBeginCharacterIndex()); } } public void testMicrosoftListAtRule() throws Exception { // This is syntactically valid according to CSS3, so we should // be able to ignore the proprietary @list rule and not fail // the whole parse. String[] samples = new String[] { "@list l0\n" + "{mso-list-id:792754432;}\n" + "div { border: solid thin black }", "@list l0:level1\n" + "{mso-list-id:792754432;}\n" + "div { border: solid thin black }"}; for (String css : samples) { // no exceptions the first time CssTree t1 = parse(css); String output1 = CompactPrinter.printCompactly(t1); // also no exceptions the second time CssTree t2 = parse(output1); // and the we've reached a fixed point assertEquals( AstPrinter.print(t1), AstPrinter.print(t2)); } } public void testRunawayMicrosoftListAtRule() throws Exception { String[] samples = new String[] { // unterminated block "@list l0 {mso-list-id:792754432;", // unterminated nested paren "@list l0 {mso-list-id:792754432;(}", // improper nesting with parens "@list l0 {mso-list-id:792754432;(})", // unterminated block, unmatched open bracket "@list l0 {mso-list-id:792754432;[", // unterminated block, close bracket without matching open bracket "@list l0 {mso-list-id:792754432;]"}; for (String css : samples) { try { parse(css); fail("The compiler should only accept complete @list rules, not " + css); } catch (GssParserException e) { // expected } } } public void testCustomBorderProperty() throws Exception { testTree( "a { border-height: 1em; }", "[[a]{[border-height:[[1em]];]}]"); testTree( "a { border-left-height: 1em; }", "[[a]{[border-left-height:[[1em]];]}]"); testTree( "a { border-right-height: 1em; }", "[[a]{[border-right-height:[[1em]];]}]"); testTree( "a { border-top-height: 1em; }", "[[a]{[border-top-height:[[1em]];]}]"); testTree( "a { border-bottom-height: 1em; }", "[[a]{[border-bottom-height:[[1em]];]}]"); } public void testForLoop() throws Exception { testTree( "@for $i from 1 to 6 {}", "[@for [$i] [from] [1] [to] [6]{}]"); } public void testForLoopWithStep() throws Exception { testTree( "@for $i from 1 to 6 step 2 {}", "[@for [$i] [from] [1] [to] [6] [step] [2]{}]"); } public void testForLoopWithVariables() throws Exception { testTree( "@for $i from $x to $y step $z {}", "[@for [$i] [from] [$x] [to] [$y] [step] [$z]{}]"); } public void testForLoopWithVariablesInBlock() throws Exception { testTree( "@for $i from 1 to 2 { .foo-$i { padding: $i } }", "[@for [$i] [from] [1] [to] [2]{[.foo-$i]{[padding:[[$i]];]}}]"); } public void testComments() throws GssParserException { testTree("div {}/*comment*/", "[[div]{[]}]"); testTree("div {}/*comment*/p {}", "[[div]{[]}[p]{[]}]"); testTree("div {}/***comment**/p {}", "[[div]{[]}[p]{[]}]"); testTree("div {}/***c/o**m//m***e////nt**/p {}", "[[div]{[]}[p]{[]}]"); testTree("div {}/***c/o**m//m/***e////nt/***/p {}", "[[div]{[]}[p]{[]}]"); testTree("div {}/****************/p {}", "[[div]{[]}[p]{[]}]"); testTree("div {}/**/p {}", "[[div]{[]}[p]{[]}]"); testTree("div {}/**/p {}/**/", "[[div]{[]}[p]{[]}]"); testTree("div {}/**/p {}/**/div {}", "[[div]{[]}[p]{[]}[div]{[]}]"); } public void testUnicodeRange() throws Exception { testValid("@font-face { unicode-range: U+26;}"); testValid("@font-face { unicode-range: U+0015-00FF;}"); testValid("@font-face { unicode-range: U+A015-C0FF;}"); testValid("@font-face { unicode-range: U+26??;}"); } public void testNumericNodeLocation() throws GssParserException { CssTree tree = new GssParser(new SourceCode(null, "div{width:99px;}")).parse(); final CssNumericNode[] resultHolder = new CssNumericNode[1]; tree.getVisitController().startVisit(new DefaultTreeVisitor() { @Override public boolean enterValueNode(CssValueNode value) { if (value instanceof CssNumericNode) { assertNull(resultHolder[0]); resultHolder[0] = (CssNumericNode) value; } return true; } }); assertNotNull(resultHolder[0]); SourceCodeLocation location = resultHolder[0].getSourceCodeLocation(); assertEquals(3, location.getEndCharacterIndex() - location.getBeginCharacterIndex()); } private CssTree parse(List<SourceCode> sources) throws GssParserException { GssParser parser = new GssParser(sources); return parser.parse(); } private CssTree parse(String gss) throws GssParserException { return parse(ImmutableList.of(new SourceCode("test", gss))); } }