blob: 8b6003a8ef1850b6c9b633c2145d86c17bf47eea [file] [log] [blame]
/**
* Copyright 2010 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.
*/
// Copyright 2006 Google Inc. All Rights Reserved.
// Author: dpeng@google.com (Daniel Peng)
#include "webutil/css/parser.h"
#include <memory>
#include "base/scoped_ptr.h"
#include "base/callback.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/scoped_ptr.h"
#include "testing/base/public/googletest.h"
#include "testing/base/public/gunit.h"
namespace Css {
class ParserTest : public testing::Test {
public:
// Accessor for private method.
Value* ParseAny(Parser* p) {
return p->ParseAny();
}
// The various Test* functions below check that parselen characters
// are parsed. Pass -1 to indicate that the entire string should be
// parsed.
// Checks that unescaping s returns value.
void TestUnescape(const char* s, int parselen, char32 value) {
SCOPED_TRACE(s);
Parser a(s);
if (parselen == -1) parselen = strlen(s);
EXPECT_EQ(value, a.ParseEscape());
EXPECT_EQ(parselen, a.getpos() - s);
}
// Checks that ParseIdent(s) returns utf8golden.
void TestIdent(const char* s, int parselen, string utf8golden) {
SCOPED_TRACE(s);
Parser a(s);
if (parselen == -1) parselen = strlen(s);
EXPECT_EQ(utf8golden, UnicodeTextToUTF8(a.ParseIdent()));
EXPECT_EQ(parselen, a.getpos() - s);
}
// Checks that ParseString<">(s) returns utf8golden.
void TestDstring(const char* s, int parselen, string utf8golden) {
SCOPED_TRACE(s);
Parser a(s);
if (parselen == -1) parselen = strlen(s);
EXPECT_EQ(utf8golden, UnicodeTextToUTF8(a.ParseString<'"'>()));
EXPECT_EQ(parselen, a.getpos() - s);
}
// Checks that ParseString<'>(s) returns utf8golden.
void TestSstring(const char* s, int parselen, string utf8golden) {
SCOPED_TRACE(s);
Parser a(s);
if (parselen == -1) parselen = strlen(s);
EXPECT_EQ(utf8golden, UnicodeTextToUTF8(a.ParseString<'\''>()));
EXPECT_EQ(parselen, a.getpos() - s);
}
// Checks that ParseAny(s) returns goldennum with goldenunit unit.
void TestAnyNum(const char* s, int parselen, double goldennum,
Value::Unit goldenunit, bool preservation_mode,
string verbatim_text) {
SCOPED_TRACE(s);
Parser a(s);
a.set_preservation_mode(preservation_mode);
if (parselen == -1) parselen = strlen(s);
scoped_ptr<Value> t(a.ParseAny());
EXPECT_EQ(t->GetLexicalUnitType(), Value::NUMBER);
EXPECT_EQ(t->GetDimension(), goldenunit);
EXPECT_DOUBLE_EQ(t->GetFloatValue(), goldennum);
EXPECT_EQ(parselen, a.getpos() - s);
EXPECT_EQ(verbatim_text, t->bytes_in_original_buffer());
}
// Checks that ParseAny(s) returns goldennum with OTHER unit (with
// unit text goldenunit).
void TestAnyNumOtherUnit(const char* s, int parselen, double goldennum,
string goldenunit) {
SCOPED_TRACE(s);
Parser a(s);
if (parselen == -1) parselen = strlen(s);
scoped_ptr<Value> t(a.ParseAny());
EXPECT_EQ(t->GetLexicalUnitType(), Value::NUMBER);
EXPECT_EQ(t->GetDimension(), Value::OTHER);
EXPECT_EQ(t->GetDimensionUnitText(), goldenunit);
EXPECT_EQ(parselen, a.getpos() - s);
}
// Checks that ParseAny(s) returns a string-type value with type goldenty
// and value utf8golden.
void TestAnyString(const char* s, int parselen, Value::ValueType goldenty,
string utf8golden) {
SCOPED_TRACE(s);
Parser a(s);
if (parselen == -1) parselen = strlen(s);
scoped_ptr<Value> t(a.ParseAny());
EXPECT_EQ(goldenty, t->GetLexicalUnitType());
EXPECT_EQ(utf8golden, UnicodeTextToUTF8(t->GetStringValue()));
EXPECT_EQ(parselen, a.getpos() - s);
}
// Checks that ParseAny(s) returns a ident value with identifier goldenty
// and value utf8golden.
void TestAnyIdent(const char* s, int parselen, Identifier::Ident goldenty) {
SCOPED_TRACE(s);
Parser a(s);
if (parselen == -1) parselen = strlen(s);
scoped_ptr<Value> t(a.ParseAny());
EXPECT_EQ(Value::IDENT, t->GetLexicalUnitType());
EXPECT_EQ(goldenty, t->GetIdentifier().ident());
EXPECT_EQ(parselen, a.getpos() - s);
}
// Checks that ParseAny(s) returns OTHER identifier (with text goldenident).
void TestAnyOtherIdent(const char* s, int parselen,
const string& goldenident) {
SCOPED_TRACE(s);
Parser a(s);
if (parselen == -1) parselen = strlen(s);
scoped_ptr<Value> t(a.ParseAny());
EXPECT_EQ(Value::IDENT, t->GetLexicalUnitType());
EXPECT_EQ(Identifier::OTHER, t->GetIdentifier().ident());
EXPECT_EQ(goldenident, UnicodeTextToUTF8(t->GetIdentifierText()));
EXPECT_EQ(parselen, a.getpos() - s);
}
Declarations* ParseAndExpandBackground(const string& str,
bool quirks_mode=true) {
scoped_ptr<Parser> p(new Parser(str));
p->set_quirks_mode(quirks_mode);
scoped_ptr<Values> vals(p->ParseValues(Property::BACKGROUND));
if (vals.get() == NULL || vals->size() == 0) {
return NULL;
}
Declaration background(Property::BACKGROUND, vals.release(), false);
scoped_ptr<Declarations> decls(new Declarations);
Parser::ExpandBackground(background, decls.get());
if (decls->size() == 0) {
return NULL;
}
return decls.release();
}
void TestBackgroundPosition(const string& str, const string& x,
const string& y) {
scoped_ptr<Declarations> decls(ParseAndExpandBackground(str));
// Find and check position x and y values.
bool found_x = false;
bool found_y = false;
for (Declarations::const_iterator iter = decls->begin();
iter != decls->end(); ++iter) {
Declaration* decl = *iter;
switch (decl->prop()) {
case Property::BACKGROUND_POSITION_X:
EXPECT_EQ(x, decl->values()->get(0)->ToString());
found_x = true;
break;
case Property::BACKGROUND_POSITION_Y:
EXPECT_EQ(y, decl->values()->get(0)->ToString());
found_y = true;
break;
default:
break;
}
}
EXPECT_TRUE(found_x);
EXPECT_TRUE(found_y);
}
enum MethodToTest {
PARSE_STYLESHEET,
PARSE_CHARSET,
EXTRACT_CHARSET,
};
void TrapEOF(StringPiece contents) {
TrapEOF(contents, PARSE_STYLESHEET);
}
void TrapEOF(StringPiece contents, MethodToTest method) {
int size = contents.size();
if (size == 0) {
// new char[0] doesn't seem to work correctly with ASAN (maybe it gets
// optimized out?) So we use NULL, which shouldn't be dereferenced.
StringPiece copy_contents(NULL, 0);
TryParse(copy_contents, method);
} else {
// We copy the data region of contents into it's own buffer which is
// not NULL-terminated. Therefore a single check past the end of the
// buffer will be a buffer overflow.
char* copy = new char[size];
memcpy(copy, contents.data(), size);
StringPiece copy_contents(copy, size);
TryParse(copy_contents, method);
delete [] copy;
}
}
void TryParse(StringPiece contents, MethodToTest method) {
Parser parser(contents);
switch (method) {
case PARSE_STYLESHEET:
delete parser.ParseStylesheet();
EXPECT_NE(Parser::kNoError, parser.errors_seen_mask());
break;
case PARSE_CHARSET:
parser.ParseCharset();
break;
case EXTRACT_CHARSET:
parser.ExtractCharset();
break;
}
}
const char* SkipPast(char delim, StringPiece input_text) {
Parser p(input_text);
EXPECT_TRUE(p.SkipPastDelimiter(delim)) << input_text;
return p.in_; // Note: This is a pointer into the buffer owned by caller.
}
void FailureSkipPast(char delim, StringPiece input_text) {
Parser p(input_text);
EXPECT_FALSE(p.SkipPastDelimiter(delim)) << input_text;
EXPECT_TRUE(p.Done());
}
};
// Like util_callback::IgnoreResult, but deletes the result.
template<typename Result>
class DeleteResultImpl : public Closure {
public:
explicit DeleteResultImpl(ResultCallback<Result*>* callback)
: callback_(CHECK_NOTNULL(callback)) {
}
void Run() {
CHECK(callback_ != NULL);
if (callback_->IsRepeatable()) {
delete callback_->Run();
} else {
delete callback_.release()->Run();
delete this;
}
}
bool IsRepeatable() const {
CHECK(callback_ != NULL);
return callback_->IsRepeatable();
}
private:
scoped_ptr<ResultCallback<Result*> > callback_;
DISALLOW_COPY_AND_ASSIGN(DeleteResultImpl);
};
template<typename Result>
static Closure* DeleteResult(ResultCallback<Result*>* callback) {
return new DeleteResultImpl<Result>(callback);
}
TEST_F(ParserTest, ErrorNumber) {
EXPECT_EQ(0, Parser::ErrorNumber(Parser::kUtf8Error));
EXPECT_EQ(1, Parser::ErrorNumber(Parser::kDeclarationError));
EXPECT_EQ(8, Parser::ErrorNumber(Parser::kRulesetError));
EXPECT_EQ(14, Parser::ErrorNumber(Parser::kAtRuleError));
}
TEST_F(ParserTest, unescape) {
// Invalid Unicode char.
TestUnescape("\\abcdef aabc", 8, ' ');
TestUnescape("\\A", 2, 0xA);
TestUnescape("\\A0b5C\r\n", 8, 0xa0b5C);
TestUnescape("\\AB ", 4, 0xAB);
}
TEST_F(ParserTest, ident) {
// We're a little more forgiving than the standard:
//
// In CSS 2.1, identifiers (including element names, classes, and
// IDs in selectors) can contain only the characters [A-Za-z0-9]
// and ISO 10646 characters U+00A1 and higher, plus the hyphen (-)
// and the underscore (_); they cannot start with a digit, or a
// hyphen followed by a digit. Only properties, values, units,
// pseudo-classes, pseudo-elements, and at-rules may start with a
// hyphen (-); other identifiers (e.g. element names, classes, or
// IDs) may not. Identifiers can also contain escaped characters
// and any ISO 10646 character as a numeric code (see next
// item). For instance, the identifier "B&W?" may be written as
// "B\&W\?" or "B\26 W\3F".
TestIdent("abcd rexo\n", 4, "abcd");
TestIdent("台灣華語", 12, "台灣華語");
TestIdent("\\41\\42 \\43 \\44", 14, "ABCD");
TestIdent("\\41\\42 \\43 \\44g'r,'rcg.,',", 15, "ABCDg");
TestIdent("\\41\\42 \\43 \\44\r\ng'r,'rcg.,',", 17, "ABCDg");
TestIdent("-blah-_67", 9, "-blah-_67");
TestIdent("\\!\\&\\^\\*\\\\e", 11, "!&^*\\e");
}
TEST_F(ParserTest, string) {
TestSstring("'ab\\'aoe\"\\'eo灣'灣", 17, "ab'aoe\"'eo灣");
TestDstring("\"ab'aoe\\\"'eo灣\"灣", 16, "ab'aoe\"'eo灣");
TestSstring("'ab\naoeu", 3, "ab");
TestDstring("\"ab\naoeu", 3, "ab");
TestDstring("\"ab\\\naoeu\"", 10, "abaoeu");
}
TEST_F(ParserTest, anynum) {
TestAnyNum("3.1415 4aone", 6, 3.1415, Value::NO_UNIT, false, "");
TestAnyNum(".1415 4aone", 5, 0.1415, Value::NO_UNIT, true, ".1415");
TestAnyNum("5 4aone", 1, 5, Value::NO_UNIT, true, "5");
TestAnyNum("0.1415pt 4aone", 8, 0.1415, Value::PT, true, "0.1415");
TestAnyNum(".1415pc 4aone", 7, 0.1415, Value::PC, true, ".1415");
TestAnyNum("5s 4aone", 2, 5, Value::S, false, "");
TestAnyNumOtherUnit("5sacks 4aone", 6, 5, "sacks");
TestAnyNumOtherUnit("5灣 4aone", 4, 5, "灣");
}
TEST_F(ParserTest, anystring) {
TestAnyIdent("none b c d e", 4, Identifier::NONE);
TestAnyIdent("none; b c d e", 4, Identifier::NONE);
TestAnyIdent("none ; b c d e", 4, Identifier::NONE);
TestAnyOtherIdent("a b c d e", 1, "a");
TestAnyOtherIdent("a; b c d e", 1, "a");
TestAnyOtherIdent("a ; b c d e", 1, "a");
TestAnyString("'ab\\'aoe\"\\'eo灣'灣 ; b c d e", 17,
Value::STRING, "ab'aoe\"'eo灣");
}
TEST_F(ParserTest, color) {
// allowed in quirks mode
scoped_ptr<Parser> a(new Parser("abCdEF brc.,aoek"));
EXPECT_EQ(a->ParseColor().ToString(), "#abcdef");
// not allowed in stanard compliant mode.
a.reset(new Parser("abCdEF brc.,aoek"));
a->set_quirks_mode(false);
EXPECT_FALSE(a->ParseColor().IsDefined());
// this is allowed
a.reset(new Parser("#abCdEF brc.,aoek"));
a->set_quirks_mode(false);
EXPECT_EQ(a->ParseColor().ToString(), "#abcdef");
a.reset(new Parser("abC btneo"));
EXPECT_EQ(a->ParseColor().ToString(), "#aabbcc");
// no longer allowed
a.reset(new Parser("#white something"));
EXPECT_FALSE(a->ParseColor().IsDefined());
a.reset(new Parser("#white something"));
a->set_quirks_mode(false);
EXPECT_FALSE(a->ParseColor().IsDefined());
// this is allowed
a.reset(new Parser("white something"));
EXPECT_EQ(a->ParseColor().ToString(), "#ffffff");
a.reset(new Parser("white something"));
a->set_quirks_mode(false);
EXPECT_EQ(a->ParseColor().ToString(), "#ffffff");
// system color
a.reset(new Parser("buttonface something"));
EXPECT_EQ(a->ParseColor().ToString(), "#ece9d8");
// string patterns
a.reset(new Parser("'abCdEF' brc.,aoek"));
EXPECT_EQ(a->ParseColor().ToString(), "#abcdef");
a.reset(new Parser("'abCdEF' brc.,aoek"));
a->set_quirks_mode(false);
EXPECT_FALSE(a->ParseColor().IsDefined());
// this is not allowed since color values must end on string boundary
a.reset(new Parser("'#abCdEF brc'.,aoek"));
a->set_quirks_mode(false);
EXPECT_FALSE(a->ParseColor().IsDefined());
a.reset(new Parser("\"abC\" btneo"));
EXPECT_EQ(a->ParseColor().ToString(), "#aabbcc");
// no longer allowed
a.reset(new Parser("'#white' something"));
EXPECT_FALSE(a->ParseColor().IsDefined());
a.reset(new Parser("'#white' something"));
a->set_quirks_mode(false);
EXPECT_FALSE(a->ParseColor().IsDefined());
// this is allowed
a.reset(new Parser("'white' something"));
EXPECT_EQ(a->ParseColor().ToString(), "#ffffff");
a.reset(new Parser("'white' something"));
a->set_quirks_mode(false);
EXPECT_EQ(a->ParseColor().ToString(), "#ffffff");
// no longer allowed
a.reset(new Parser("100%"));
EXPECT_FALSE(a->ParseColor().IsDefined());
// no longer allowed
a.reset(new Parser("100px"));
EXPECT_FALSE(a->ParseColor().IsDefined());
// this is allowed
a.reset(new Parser("100"));
EXPECT_EQ(a->ParseColor().ToString(), "#110000");
// should be parsed as a number
a.reset(new Parser("100px"));
scoped_ptr<Value> t(a->ParseAnyExpectingColor());
EXPECT_EQ(Value::NUMBER, t->GetLexicalUnitType());
EXPECT_EQ("100px", t->ToString());
a.reset(new Parser("rgb(12,25,30)"));
t.reset(a->ParseAny());
EXPECT_EQ(t->GetColorValue().ToString(), "#0c191e");
a.reset(new Parser("rgb( 12% , 25%, 30%)"));
t.reset(a->ParseAny());
EXPECT_EQ(t->GetColorValue().ToString(), "#1e3f4c");
a.reset(new Parser("rgb( 12% , 25% 30%)"));
t.reset(a->ParseAny());
EXPECT_FALSE(t.get());
// Parsed as color in quirks-mode.
a.reset(new Parser("0000ff"));
t.reset(a->ParseAnyExpectingColor());
EXPECT_EQ(Value::COLOR, t->GetLexicalUnitType());
EXPECT_EQ("#0000ff", t->ToString());
EXPECT_EQ(Parser::kNoError, a->errors_seen_mask());
// Parsed as dimension in standards-mode.
a.reset(new Parser("0000ff"));
a->set_quirks_mode(false);
t.reset(a->ParseAnyExpectingColor());
EXPECT_EQ(Value::NUMBER, t->GetLexicalUnitType());
EXPECT_EQ("0ff", t->ToString());
EXPECT_EQ(Parser::kNoError, a->errors_seen_mask());
// Original preserved in preservation-mode + standards-mode.
a.reset(new Parser("0000ff"));
a->set_quirks_mode(false);
a->set_preservation_mode(true);
t.reset(a->ParseAnyExpectingColor());
EXPECT_EQ(Value::NUMBER, t->GetLexicalUnitType());
EXPECT_EQ("0ff", t->ToString());
// ValueError assures that we will preserve the original string.
EXPECT_EQ(Parser::kValueError, a->errors_seen_mask());
}
TEST_F(ParserTest, url) {
scoped_ptr<Parser> a(new Parser("url(blah)"));
scoped_ptr<Value> t(a->ParseAny());
EXPECT_EQ(Value::URI, t->GetLexicalUnitType());
EXPECT_EQ("blah", UnicodeTextToUTF8(t->GetStringValue()));
a.reset(new Parser("url( blah )"));
t.reset(a->ParseAny());
EXPECT_EQ(Value::URI, t->GetLexicalUnitType());
EXPECT_EQ("blah", UnicodeTextToUTF8(t->GetStringValue()));
a.reset(new Parser("url( blah extra)"));
t.reset(a->ParseAny());
EXPECT_EQ(static_cast<Value *>(NULL), t.get());
}
TEST_F(ParserTest, rect) {
// rect can be either comma or space delimited
scoped_ptr<Parser> a(new Parser("rect( 12, 10,auto 200px)"));
scoped_ptr<Value> t(a->ParseAny());
EXPECT_EQ(Value::RECT, t->GetLexicalUnitType());
ASSERT_EQ(4, t->GetParameters()->size());
EXPECT_EQ(Value::NUMBER,
t->GetParameters()->get(0)->GetLexicalUnitType());
EXPECT_EQ(12, t->GetParameters()->get(0)->GetIntegerValue());
EXPECT_EQ(Value::IDENT,
t->GetParameters()->get(2)->GetLexicalUnitType());
EXPECT_EQ(Identifier::AUTO,
t->GetParameters()->get(2)->GetIdentifier().ident());
a.reset(new Parser("rect(auto)"));
t.reset(a->ParseAny());
EXPECT_EQ(static_cast<Value *>(NULL), t.get());
a.reset(new Parser("rect()"));
t.reset(a->ParseAny());
EXPECT_EQ(static_cast<Value *>(NULL), t.get());
a.reset(new Parser("rect(13 10 auto 4)"));
t.reset(a->ParseAny());
EXPECT_EQ(13, t->GetParameters()->get(0)->GetIntegerValue());
a.reset(new Parser("rect(14,10,1,2)"));
t.reset(a->ParseAny());
EXPECT_EQ(14, t->GetParameters()->get(0)->GetIntegerValue());
a.reset(new Parser("rect(15 10 1)"));
t.reset(a->ParseAny());
EXPECT_EQ(static_cast<Value *>(NULL), t.get());
a.reset(new Parser("rect(16 10 1 2 3)"));
t.reset(a->ParseAny());
EXPECT_EQ(static_cast<Value *>(NULL), t.get());
}
TEST_F(ParserTest, background) {
scoped_ptr<Declarations> decls(ParseAndExpandBackground("#333"));
EXPECT_EQ(6, decls->size());
decls.reset(ParseAndExpandBackground("fff"));
EXPECT_TRUE(decls.get());
// Not valid for quirks_mode=false
EXPECT_FALSE(ParseAndExpandBackground("fff", false));
decls.reset(ParseAndExpandBackground("fff000"));
EXPECT_TRUE(decls.get());
// Not valid for quirks_mode=false
EXPECT_FALSE(ParseAndExpandBackground("fff000", false));
// This should now be parsed as background position instead of color.
decls.reset(ParseAndExpandBackground("100%"));
ASSERT_TRUE(decls.get());
ASSERT_EQ(6, decls->size());
EXPECT_EQ(Property::BACKGROUND_COLOR, decls->get(0)->prop());
EXPECT_EQ(Identifier::TRANSPARENT,
decls->get(0)->values()->get(0)->GetIdentifier().ident());
EXPECT_EQ(Property::BACKGROUND_POSITION_X, decls->get(4)->prop());
EXPECT_EQ("100%", decls->get(4)->values()->get(0)->ToString());
EXPECT_FALSE(ParseAndExpandBackground(""));
EXPECT_FALSE(ParseAndExpandBackground(";"));
EXPECT_FALSE(ParseAndExpandBackground("\"string\""));
EXPECT_FALSE(ParseAndExpandBackground("normal"));
decls.reset(ParseAndExpandBackground("inherit"));
ASSERT_EQ(6, decls->size());
for (int i = 0; i < 6; ++i) {
EXPECT_EQ(Identifier::INHERIT,
decls->get(i)->values()->get(0)->GetIdentifier().ident());
}
EXPECT_FALSE(ParseAndExpandBackground("inherit none"));
EXPECT_FALSE(ParseAndExpandBackground("none inherit"));
decls.reset(ParseAndExpandBackground("none"));
EXPECT_EQ(Identifier::TRANSPARENT,
decls->get(0)->values()->get(0)->GetIdentifier().ident());
EXPECT_EQ(Identifier::NONE,
decls->get(1)->values()->get(0)->GetIdentifier().ident());
EXPECT_EQ(Identifier::REPEAT,
decls->get(2)->values()->get(0)->GetIdentifier().ident());
EXPECT_EQ(Identifier::SCROLL,
decls->get(3)->values()->get(0)->GetIdentifier().ident());
decls.reset(ParseAndExpandBackground("fixed"));
EXPECT_EQ(Identifier::FIXED,
decls->get(3)->values()->get(0)->GetIdentifier().ident());
decls.reset(ParseAndExpandBackground("transparent"));
EXPECT_EQ(Identifier::TRANSPARENT,
decls->get(0)->values()->get(0)->GetIdentifier().ident());
// IE specific. Firefox should bail out.
decls.reset(ParseAndExpandBackground("none url(abc)"));
EXPECT_EQ(Value::URI, decls->get(1)->values()->get(0)->GetLexicalUnitType());
decls.reset(ParseAndExpandBackground("none red fixed"));
EXPECT_EQ("#ff0000",
decls->get(0)->values()->get(0)->GetColorValue().ToString());
EXPECT_EQ(Identifier::NONE,
decls->get(1)->values()->get(0)->GetIdentifier().ident());
EXPECT_EQ(Identifier::FIXED,
decls->get(3)->values()->get(0)->GetIdentifier().ident());
// The rest are position tests
TestBackgroundPosition("none", "0%", "0%");
TestBackgroundPosition("10", "10", "50%");
TestBackgroundPosition("10 20%", "10", "20%");
TestBackgroundPosition("10 100%", "10", "100%");
TestBackgroundPosition("top left", "left", "top");
TestBackgroundPosition("left top", "left", "top");
TestBackgroundPosition("bottom", "50%", "bottom");
TestBackgroundPosition("bottom center", "center", "bottom");
TestBackgroundPosition("center bottom", "center", "bottom");
TestBackgroundPosition("left", "left", "50%");
TestBackgroundPosition("left center", "left", "center");
TestBackgroundPosition("center left", "left", "center");
TestBackgroundPosition("center", "center", "50%");
TestBackgroundPosition("center center", "center", "center");
TestBackgroundPosition("center 30%", "center", "30%");
TestBackgroundPosition("30% center", "30%", "center");
TestBackgroundPosition("30% bottom", "30%", "bottom");
TestBackgroundPosition("left 30%", "left", "30%");
TestBackgroundPosition("30% left", "left", "30%");
// IE specific
TestBackgroundPosition("30% 20% 50%", "30%", "20%");
TestBackgroundPosition("bottom center right", "center", "bottom");
TestBackgroundPosition("bottom right top", "right", "bottom");
TestBackgroundPosition("bottom top right", "right", "top");
TestBackgroundPosition("top right left", "right", "top");
TestBackgroundPosition("right left top", "left", "top");
}
TEST_F(ParserTest, font_family) {
scoped_ptr<Parser> a(
new Parser(" Arial font, 'Sans', system, menu new "));
scoped_ptr<Values> t(new Values);
EXPECT_TRUE(a->ParseFontFamily(t.get()));
ASSERT_EQ(4, t->size());
EXPECT_EQ(Value::IDENT, t->get(0)->GetLexicalUnitType());
EXPECT_EQ("Arial font", UnicodeTextToUTF8(t->get(0)->GetIdentifierText()));
EXPECT_EQ(Value::STRING, t->get(1)->GetLexicalUnitType());
EXPECT_EQ("system", UnicodeTextToUTF8(t->get(2)->GetIdentifierText()));
EXPECT_EQ("menu new", UnicodeTextToUTF8(t->get(3)->GetIdentifierText()));
a.reset(new Parser("Verdana 3"));
t.reset(new Values);
EXPECT_FALSE(a->ParseFontFamily(t.get()));
a.reset(new Parser("Verdana :"));
t.reset(new Values);
EXPECT_FALSE(a->ParseFontFamily(t.get()));
a.reset(new Parser("Verdana ;"));
t.reset(new Values);
EXPECT_TRUE(a->ParseFontFamily(t.get()));
ASSERT_EQ(1, t->size());
EXPECT_EQ(Value::IDENT, t->get(0)->GetLexicalUnitType());
EXPECT_EQ("Verdana", UnicodeTextToUTF8(t->get(0)->GetIdentifierText()));
// Legal base example.
scoped_ptr<Declarations> d;
a.reset(new Parser("font-family: foo"));
d.reset(a->ParseRawDeclarations());
ASSERT_TRUE(NULL != d.get());
ASSERT_EQ(1, d->size());
EXPECT_EQ(1, d->at(0)->values()->size());
// Illegal leading comma.
a.reset(new Parser("font-family: ,foo"));
d.reset(a->ParseRawDeclarations());
ASSERT_TRUE(NULL != d.get());
EXPECT_EQ(0, d->size());
// Illegal trailing comma.
a.reset(new Parser("font-family: foo,"));
d.reset(a->ParseRawDeclarations());
ASSERT_TRUE(NULL != d.get());
EXPECT_EQ(0, d->size());
// Legal empty string with separating comma.
a.reset(new Parser("font-family: '',foo"));
d.reset(a->ParseRawDeclarations());
ASSERT_TRUE(NULL != d.get());
ASSERT_EQ(1, d->size());
EXPECT_EQ(2, d->at(0)->values()->size());
// Illegal empty elements in comma-separated list.
a.reset(new Parser("font-family: '',,foo"));
d.reset(a->ParseRawDeclarations());
ASSERT_TRUE(NULL != d.get());
EXPECT_EQ(0, d->size());
// Fonts must be comma separated.
a.reset(new Parser("font-family: 'bar' foo"));
d.reset(a->ParseRawDeclarations());
ASSERT_TRUE(NULL != d.get());
EXPECT_EQ(0, d->size());
a.reset(new Parser("font-family: 'bar' 'foo'"));
d.reset(a->ParseRawDeclarations());
ASSERT_TRUE(NULL != d.get());
EXPECT_EQ(0, d->size());
a.reset(new Parser("font-family: bar 'foo'"));
d.reset(a->ParseRawDeclarations());
ASSERT_TRUE(NULL != d.get());
EXPECT_EQ(0, d->size());
a.reset(new Parser("font-family: 'bar'foo"));
d.reset(a->ParseRawDeclarations());
ASSERT_TRUE(NULL != d.get());
EXPECT_EQ(0, d->size());
}
TEST_F(ParserTest, font) {
scoped_ptr<Parser> a(new Parser("font: caption"));
scoped_ptr<Declarations> declarations(a->ParseDeclarations());
const char expected_caption_expansion[] =
"font: caption; "
"font-style: normal; "
"font-variant: normal; "
"font-weight: normal; "
"font-size: 10.6667px; "
"line-height: normal; "
"font-family: caption";
EXPECT_EQ(expected_caption_expansion, declarations->ToString());
a.reset(new Parser("font: inherit"));
declarations.reset(a->ParseDeclarations());
const char expected_inherit_expansion[] =
"font: inherit; "
"font-style: inherit; "
"font-variant: inherit; "
"font-weight: inherit; "
"font-size: inherit; "
"line-height: inherit; "
"font-family: inherit";
EXPECT_EQ(expected_inherit_expansion, declarations->ToString());
a.reset(new Parser("normal 10px /120% Arial 'Sans'"));
scoped_ptr<Values> t(a->ParseFont());
EXPECT_TRUE(NULL == t.get());
a.reset(new Parser("normal 10px /120% Arial, 'Sans'"));
t.reset(a->ParseFont());
ASSERT_EQ(7, t->size());
EXPECT_DOUBLE_EQ(10, t->get(3)->GetFloatValue());
EXPECT_EQ(Value::PERCENT, t->get(4)->GetDimension());
a.reset(new Parser("italic 10px Arial, Sans"));
t.reset(a->ParseFont());
ASSERT_EQ(7, t->size());
EXPECT_DOUBLE_EQ(10, t->get(3)->GetFloatValue());
EXPECT_EQ(Identifier::NORMAL, t->get(4)->GetIdentifier().ident());
a.reset(new Parser("SMALL-caps normal x-large Arial"));
t.reset(a->ParseFont());
ASSERT_EQ(6, t->size());
EXPECT_EQ(Identifier::NORMAL, t->get(0)->GetIdentifier().ident());
EXPECT_EQ(Identifier::SMALL_CAPS, t->get(1)->GetIdentifier().ident());
EXPECT_EQ(Identifier::X_LARGE, t->get(3)->GetIdentifier().ident());
EXPECT_EQ(Identifier::NORMAL, t->get(4)->GetIdentifier().ident());
a.reset(new Parser("bolder 100 120 Arial"));
t.reset(a->ParseFont());
ASSERT_EQ(6, t->size());
EXPECT_EQ(100, t->get(2)->GetIntegerValue());
EXPECT_EQ(120, t->get(3)->GetIntegerValue());
EXPECT_EQ(Identifier::NORMAL, t->get(4)->GetIdentifier().ident());
a.reset(new Parser("10px normal"));
t.reset(a->ParseFont());
ASSERT_EQ(6, t->size());
EXPECT_EQ(10, t->get(3)->GetIntegerValue());
EXPECT_EQ(Identifier::NORMAL, t->get(5)->GetIdentifier().ident());
a.reset(new Parser("normal 10px "));
t.reset(a->ParseFont());
EXPECT_EQ(5, t->size()) << "missing font-family should be allowed";
a.reset(new Parser("10px/12pt "));
t.reset(a->ParseFont());
EXPECT_EQ(5, t->size()) << "missing font-family should be allowed";
a.reset(new Parser("menu 10px"));
t.reset(a->ParseFont());
EXPECT_EQ(static_cast<Values *>(NULL), t.get())
<< "system font with extra value";
a.reset(new Parser("Arial, menu "));
t.reset(a->ParseFont());
EXPECT_EQ(static_cast<Values *>(NULL), t.get()) << "missing font-size";
a.reset(new Parser("transparent 10px "));
t.reset(a->ParseFont());
EXPECT_EQ(static_cast<Values *>(NULL), t.get()) << "unknown property";
a.reset(new Parser("normal / 10px Arial"));
t.reset(a->ParseFont());
EXPECT_EQ(static_cast<Values *>(NULL), t.get())
<< "line-height without font-size";
a.reset(new Parser("normal 10px/ Arial"));
t.reset(a->ParseFont());
EXPECT_EQ(static_cast<Values *>(NULL), t.get())
<< "slash without line-height";
a.reset(new Parser("normal 10px Arial #333"));
t.reset(a->ParseFont());
EXPECT_EQ(static_cast<Values *>(NULL), t.get()) << "invalid type";
}
TEST_F(ParserTest, numbers) {
scoped_ptr<Parser> p;
scoped_ptr<Value> v;
p.reset(new Parser("1"));
v.reset(p->ParseNumber());
ASSERT_EQ(Value::NUMBER, v->GetLexicalUnitType());
EXPECT_EQ(1, v->GetIntegerValue());
EXPECT_EQ(Value::NO_UNIT, v->GetDimension());
EXPECT_TRUE(p->Done());
p.reset(new Parser("1;"));
v.reset(p->ParseNumber());
ASSERT_EQ(Value::NUMBER, v->GetLexicalUnitType());
EXPECT_EQ(1, v->GetIntegerValue());
EXPECT_EQ(Value::NO_UNIT, v->GetDimension());
EXPECT_EQ(';', *p->in_);
p.reset(new Parser("1em;"));
v.reset(p->ParseNumber());
ASSERT_EQ(Value::NUMBER, v->GetLexicalUnitType());
EXPECT_EQ(1, v->GetIntegerValue());
EXPECT_EQ(Value::EM, v->GetDimension());
EXPECT_EQ(';', *p->in_);
p.reset(new Parser("1.1em;"));
v.reset(p->ParseNumber());
ASSERT_EQ(Value::NUMBER, v->GetLexicalUnitType());
EXPECT_EQ(1.1, v->GetFloatValue());
EXPECT_EQ(Value::EM, v->GetDimension());
EXPECT_EQ(';', *p->in_);
p.reset(new Parser(".1"));
v.reset(p->ParseNumber());
ASSERT_EQ(Value::NUMBER, v->GetLexicalUnitType());
EXPECT_EQ(.1, v->GetFloatValue());
EXPECT_EQ(Value::NO_UNIT, v->GetDimension());
EXPECT_TRUE(p->Done());
// Note: 1.em is *not* parsed as 1.0em, instead it needs to be parsed as
// INT(1) DELIM(.) IDENT(em)
p.reset(new Parser("1.em;"));
v.reset(p->ParseNumber());
ASSERT_EQ(Value::NUMBER, v->GetLexicalUnitType());
EXPECT_EQ(1, v->GetIntegerValue());
EXPECT_EQ(Value::NO_UNIT, v->GetDimension()); // Unit is not parsed.
EXPECT_EQ('.', *p->in_); // Parsing ends on dot.
// Make sure this also works if file ends with dot.
p.reset(new Parser("1."));
v.reset(p->ParseNumber());
ASSERT_EQ(Value::NUMBER, v->GetLexicalUnitType());
EXPECT_EQ(1, v->GetIntegerValue());
EXPECT_EQ(Value::NO_UNIT, v->GetDimension());
EXPECT_EQ('.', *p->in_);
}
TEST_F(ParserTest, values) {
scoped_ptr<Parser> a(new Parser(
"rgb(12,25,30) url(blah) url('blah.png') 12% !important 'arial'"));
scoped_ptr<Values> t(a->ParseValues(Property::OTHER));
ASSERT_EQ(4, t->size());
EXPECT_EQ(Value::COLOR, t->get(0)->GetLexicalUnitType());
EXPECT_EQ(Value::URI, t->get(1)->GetLexicalUnitType());
EXPECT_EQ(Value::URI, t->get(2)->GetLexicalUnitType());
EXPECT_EQ(Value::NUMBER, t->get(3)->GetLexicalUnitType());
EXPECT_EQ(Value::PERCENT, t->get(3)->GetDimension());
a.reset(new Parser("rgb( 12, 25,30) @ignored url( blah )"
" rect(12 10 auto 200px)"
" { should be {nested }discarded } ident;"));
t.reset(a->ParseValues(Property::OTHER));
ASSERT_EQ(4, t->size());
EXPECT_EQ(Value::COLOR, t->get(0)->GetLexicalUnitType());
EXPECT_EQ(Value::URI, t->get(1)->GetLexicalUnitType());
EXPECT_EQ(Value::RECT, t->get(2)->GetLexicalUnitType());
EXPECT_EQ(Value::IDENT, t->get(3)->GetLexicalUnitType());
EXPECT_EQ("ident", UnicodeTextToUTF8(t->get(3)->GetIdentifierText()));
// test value copy constructor.
scoped_ptr<Value> val(new Value(*(t->get(2))));
EXPECT_EQ(Value::RECT, val->GetLexicalUnitType());
ASSERT_EQ(4, val->GetParameters()->size());
EXPECT_EQ(Value::NUMBER,
val->GetParameters()->get(0)->GetLexicalUnitType());
EXPECT_EQ(12, val->GetParameters()->get(0)->GetIntegerValue());
EXPECT_EQ(Value::IDENT,
val->GetParameters()->get(2)->GetLexicalUnitType());
EXPECT_EQ("auto", UnicodeTextToUTF8(val->GetParameters()->get(2)
->GetIdentifierText()));
}
TEST_F(ParserTest, SkipCornerCases) {
// Comments are not nested.
scoped_ptr<Parser> p(new Parser("\f /* foobar /* */ foobar */"));
p->SkipSpace();
EXPECT_STREQ("foobar */", p->in_);
// Proper nesting. Ignore escaped closing chars.
p.reset(new Parser("{[ (]}) foo\\]\\}bar ] \\} } Now it's closed. }"));
EXPECT_TRUE(p->SkipMatching());
EXPECT_STREQ(" Now it's closed. }", p->in_);
// Ignore closing chars in comments and strings.
p.reset(new Parser("[/*]*/ 'fake ]' () { \"also fake }\" ]} ] Finally."));
EXPECT_TRUE(p->SkipMatching());
EXPECT_STREQ(" Finally.", p->in_);
// False on unclosed.
p.reset(new Parser("("));
EXPECT_FALSE(p->SkipMatching());
EXPECT_STREQ("", p->in_);
p.reset(new Parser("foo({[)]}, bar\\)(), ')', /*)*/,), baz"));
EXPECT_TRUE(p->SkipPastDelimiter(','));
EXPECT_STREQ(" baz", p->in_);
p.reset(new Parser("{[](} f\\(oo)} @rule bar"));
EXPECT_TRUE(p->SkipToNextAny());
EXPECT_STREQ("bar", p->in_);
// First {} block ends @media statement.
p.reset(new Parser("not all and (color), print { .a { color: red; } } "
".b { color: green; }"));
EXPECT_TRUE(p->SkipToAtRuleEnd());
EXPECT_STREQ(" .b { color: green; }", p->in_);
// But not nested inside parentheses.
p.reset(new Parser("and(\"don't\" { stop, here }) { } .b { color: green; }"));
EXPECT_TRUE(p->SkipToAtRuleEnd());
EXPECT_STREQ(" .b { color: green; }", p->in_);
// ; technically also ends a @media statement.
p.reset(new Parser("screen; .a { color: red; }"));
EXPECT_TRUE(p->SkipToAtRuleEnd());
EXPECT_STREQ(" .a { color: red; }", p->in_);
// Or it runs to EOF.
p.reset(new Parser("screen and (color, print"));
EXPECT_FALSE(p->SkipToAtRuleEnd());
EXPECT_STREQ("", p->in_);
// Commas separate each media query.
p.reset(new Parser("not all and (color), print { .a { color: red; } }"));
p->SkipToMediaQueryEnd();
EXPECT_STREQ(", print { .a { color: red; } }", p->in_);
// But not nested inside parentheses.
p.reset(new Parser("and(\"don't\", stop, here), screen { }"));
p->SkipToMediaQueryEnd();
EXPECT_STREQ(", screen { }", p->in_);
// { also signals end of media query.
p.reset(new Parser("screen { .a { color: red; } }"));
p->SkipToMediaQueryEnd();
EXPECT_STREQ("{ .a { color: red; } }", p->in_);
// ; technically also ends a media query.
p.reset(new Parser("screen; .a { color: red; }"));
p->SkipToMediaQueryEnd();
EXPECT_STREQ("; .a { color: red; }", p->in_);
// Or it runs to EOF.
p.reset(new Parser("screen and (color, print"));
p->SkipToMediaQueryEnd();
EXPECT_STREQ("", p->in_);
}
TEST_F(ParserTest, SkipMatching) {
static const char* truetestcases[] = {
"{{{{}}}} serif",
"{ { { { } } } } serif", // whitespace
"{@ident1{{ @ident {}}}} serif", // @-idents
"{{ident{{}ident2}}} serif", // idents
};
for (int i = 0; i < arraysize(truetestcases); ++i) {
SCOPED_TRACE(truetestcases[i]);
Parser p(truetestcases[i]);
EXPECT_TRUE(p.SkipMatching());
Values values;
EXPECT_TRUE(p.ParseFontFamily(&values));
ASSERT_EQ(1, values.size());
EXPECT_EQ(Value::IDENT, values.get(0)->GetLexicalUnitType());
EXPECT_EQ("serif", UnicodeTextToUTF8(values.get(0)->GetIdentifierText()));
}
static const char* falsetestcases[] = {
"{{{{}}} serif", // too many opens
"{{{{}}}}} serif", // too many closes
"{{{{}}}}}", // no tokenxs
};
for (int i = 0; i < arraysize(falsetestcases); ++i) {
SCOPED_TRACE(falsetestcases[i]);
Parser p(falsetestcases[i]);
p.SkipMatching();
Values values;
p.ParseFontFamily(&values);
EXPECT_EQ(0, values.size());
}
}
TEST_F(ParserTest, declarations) {
scoped_ptr<Parser> a(
new Parser("color: #333; line-height: 1.3;"
"text-align: justify; font-family: \"Gill Sans MT\","
"\"Gill Sans\", GillSans, Arial, Helvetica, sans-serif"));
scoped_ptr<Declarations> t(a->ParseDeclarations());
// Declarations is a vector of Declaration, and we go through them:
ASSERT_EQ(4, t->size());
EXPECT_EQ(Property::COLOR, t->get(0)->prop());
EXPECT_EQ(Property::LINE_HEIGHT, t->get(1)->prop());
EXPECT_EQ(Property::TEXT_ALIGN, t->get(2)->prop());
EXPECT_EQ(Property::FONT_FAMILY, t->get(3)->prop());
ASSERT_EQ(1, t->get(0)->values()->size());
EXPECT_EQ(Value::COLOR, t->get(0)->values()->get(0)->GetLexicalUnitType());
EXPECT_EQ("#333333", t->get(0)->values()->get(0)->GetColorValue().ToString());
ASSERT_EQ(6, t->get(3)->values()->size());
EXPECT_EQ(Value::STRING,
t->get(3)->values()->get(0)->GetLexicalUnitType());
EXPECT_EQ("Gill Sans MT",
UnicodeTextToUTF8(t->get(3)->values()->get(0)->GetStringValue()));
a.reset(new Parser(
"background-color: 333; color: \"abcdef\";"
"background-color: #red; color: \"white\";"
"background-color: rgb(255, 10%, 10)"));
t.reset(a->ParseDeclarations());
ASSERT_EQ(4, t->size()) << "#red is not valid";
EXPECT_EQ(Property::BACKGROUND_COLOR, t->get(0)->prop());
EXPECT_EQ(Property::COLOR, t->get(1)->prop());
EXPECT_EQ(Property::COLOR, t->get(2)->prop());
EXPECT_EQ(Property::BACKGROUND_COLOR, t->get(3)->prop());
ASSERT_EQ(1, t->get(0)->values()->size());
EXPECT_EQ(Value::COLOR, t->get(0)->values()->get(0)->GetLexicalUnitType());
EXPECT_EQ("#333333", t->get(0)->values()->get(0)->GetColorValue().ToString());
ASSERT_EQ(1, t->get(1)->values()->size());
EXPECT_EQ(Value::COLOR, t->get(1)->values()->get(0)->GetLexicalUnitType());
EXPECT_EQ("#abcdef", t->get(1)->values()->get(0)->GetColorValue().ToString());
ASSERT_EQ(1, t->get(2)->values()->size());
EXPECT_EQ(Value::COLOR, t->get(2)->values()->get(0)->GetLexicalUnitType());
EXPECT_EQ("#ffffff", t->get(2)->values()->get(0)->GetColorValue().ToString());
ASSERT_EQ(1, t->get(3)->values()->size());
EXPECT_EQ(Value::COLOR, t->get(3)->values()->get(0)->GetLexicalUnitType());
EXPECT_EQ("#ff190a", t->get(3)->values()->get(0)->GetColorValue().ToString());
// expand background
a.reset(new Parser("background: #333 fixed no-repeat; "));
t.reset(a->ParseDeclarations());
ASSERT_EQ(7, t->size());
EXPECT_EQ(Property::BACKGROUND, t->get(0)->prop());
EXPECT_EQ(3, t->get(0)->values()->size());
EXPECT_EQ(Property::BACKGROUND_COLOR, t->get(1)->prop());
EXPECT_EQ(1, t->get(1)->values()->size());
EXPECT_EQ(Property::BACKGROUND_IMAGE, t->get(2)->prop());
EXPECT_EQ(1, t->get(2)->values()->size());
EXPECT_EQ(Property::BACKGROUND_REPEAT, t->get(3)->prop());
EXPECT_EQ(1, t->get(3)->values()->size());
EXPECT_EQ(Property::BACKGROUND_ATTACHMENT, t->get(4)->prop());
EXPECT_EQ(1, t->get(4)->values()->size());
EXPECT_EQ(Property::BACKGROUND_POSITION_X, t->get(5)->prop());
EXPECT_EQ(1, t->get(5)->values()->size());
EXPECT_EQ(Property::BACKGROUND_POSITION_Y, t->get(6)->prop());
EXPECT_EQ(1, t->get(6)->values()->size());
// expand font
a.reset(new Parser("font: small-caps 24px Arial, 'Sans', monospace; "));
t.reset(a->ParseDeclarations());
ASSERT_EQ(7, t->size());
EXPECT_EQ(Property::FONT, t->get(0)->prop());
EXPECT_EQ(8, t->get(0)->values()->size());
EXPECT_EQ(Property::FONT_STYLE, t->get(1)->prop());
EXPECT_EQ(Property::FONT_VARIANT, t->get(2)->prop());
EXPECT_EQ(Property::FONT_WEIGHT, t->get(3)->prop());
EXPECT_EQ(Property::FONT_SIZE, t->get(4)->prop());
EXPECT_EQ(Property::LINE_HEIGHT, t->get(5)->prop());
EXPECT_EQ(Property::FONT_FAMILY, t->get(6)->prop());
ASSERT_EQ(3, t->get(6)->values()->size());
EXPECT_EQ("monospace", UnicodeTextToUTF8(t->get(6)->values()->get(2)->
GetIdentifierText()));
a.reset(new Parser(
"{font-size: #333; color:red"));
t.reset(a->ParseDeclarations());
ASSERT_EQ(1, t->size());
EXPECT_EQ(Property::COLOR, t->get(0)->prop());
a.reset(new Parser(
"{font-size: #333; color:red"));
a->set_quirks_mode(false);
t.reset(a->ParseDeclarations());
EXPECT_EQ(0, t->size());
a.reset(new Parser(
"font-size {background: #333; color:red"));
t.reset(a->ParseDeclarations());
ASSERT_EQ(1, t->size());
EXPECT_EQ(Property::COLOR, t->get(0)->prop());
a.reset(new Parser(
"font-size {background: #333; color:red"));
a->set_quirks_mode(false);
t.reset(a->ParseDeclarations());
EXPECT_EQ(0, t->size());
a.reset(new Parser(
"font-size }background: #333; color:red"));
t.reset(a->ParseDeclarations());
EXPECT_EQ(0, t->size());
a.reset(new Parser(
"font-size }background: #333; color:red"));
a->set_quirks_mode(false);
t.reset(a->ParseDeclarations());
EXPECT_EQ(0, t->size());
a.reset(new Parser(
"top:1px; {font-size: #333; color:red}"));
t.reset(a->ParseDeclarations());
ASSERT_EQ(2, t->size());
EXPECT_EQ(Property::TOP, t->get(0)->prop());
EXPECT_EQ(Property::COLOR, t->get(1)->prop());
a.reset(new Parser(
"top:1px; {font-size: #333; color:red}"));
a->set_quirks_mode(false);
t.reset(a->ParseDeclarations());
ASSERT_EQ(1, t->size());
EXPECT_EQ(Property::TOP, t->get(0)->prop());
}
TEST_F(ParserTest, illegal_constructs) {
scoped_ptr<Parser> a(new Parser("width: {$width}"));
scoped_ptr<Declarations> t(a->ParseDeclarations());
// From CSS2.1 spec http://www.w3.org/TR/CSS2/syndata.html#parsing-errors:
// User agents must ignore a declaration with an illegal value.
EXPECT_EQ(0, t->size());
a.reset(new Parser("font-family: \"Gill Sans MT;"));
t.reset(a->ParseDeclarations());
ASSERT_EQ(1, t->size());
EXPECT_EQ(Property::FONT_FAMILY, t->get(0)->prop());
ASSERT_EQ(1, t->get(0)->values()->size());
EXPECT_EQ(Value::STRING, t->get(0)->values()->get(0)->GetLexicalUnitType());
EXPECT_EQ("Gill Sans MT;",
UnicodeTextToUTF8(t->get(0)->values()->get(0)->GetStringValue()));
a.reset(new Parser("font-family: 'Gill Sans MT"));
t.reset(a->ParseDeclarations());
ASSERT_EQ(1, t->size());
EXPECT_EQ(Property::FONT_FAMILY, t->get(0)->prop());
ASSERT_EQ(1, t->get(0)->values()->size());
EXPECT_EQ(Value::STRING, t->get(0)->values()->get(0)->GetLexicalUnitType());
EXPECT_EQ("Gill Sans MT",
UnicodeTextToUTF8(t->get(0)->values()->get(0)->GetStringValue()));
}
TEST_F(ParserTest, value_validation) {
scoped_ptr<Parser> a(new Parser("width: {$width}"));
scoped_ptr<Declarations> t(a->ParseDeclarations());
// Let's take border-color as an example. It only accepts color and the
// transparent keyword in particular (and inherit is a common one).
a.reset(new Parser(
"border-color: \"string\"; "
"border-color: url(\"abc\"); "
"border-color: 12345; "
"border-color: none; "
"border-color: inherited; "
"border-color: red; "
"border-color: #123456; "
"border-color: transparent; "
"border-color: inherit; "
"border-color: unknown; "
));
t.reset(a->ParseDeclarations());
ASSERT_EQ(4, t->size());
EXPECT_EQ(Value::COLOR, t->get(0)->values()->get(0)->GetLexicalUnitType());
EXPECT_EQ(Value::COLOR, t->get(1)->values()->get(0)->GetLexicalUnitType());
EXPECT_EQ(Value::IDENT, t->get(2)->values()->get(0)->GetLexicalUnitType());
EXPECT_EQ(Identifier::TRANSPARENT,
t->get(2)->values()->get(0)->GetIdentifier().ident());
EXPECT_EQ(Value::IDENT, t->get(3)->values()->get(0)->GetLexicalUnitType());
EXPECT_EQ(Identifier::INHERIT,
t->get(3)->values()->get(0)->GetIdentifier().ident());
}
TEST_F(ParserTest, universalselector) {
Parser p("*");
scoped_ptr<SimpleSelectors> t(p.ParseSimpleSelectors(false));
EXPECT_EQ(SimpleSelectors::NONE, t->combinator());
ASSERT_EQ(1, t->size());
EXPECT_EQ(SimpleSelector::UNIVERSAL, t->get(0)->type());
}
TEST_F(ParserTest, universalselectorcondition) {
scoped_ptr<Parser> a(new Parser(" *[foo=bar]"));
scoped_ptr<SimpleSelectors> t(a->ParseSimpleSelectors(true));
EXPECT_EQ(SimpleSelectors::DESCENDANT, t->combinator());
ASSERT_EQ(2, t->size());
EXPECT_EQ(SimpleSelector::UNIVERSAL, t->get(0)->type());
EXPECT_EQ(SimpleSelector::EXACT_ATTRIBUTE, t->get(1)->type());
a.reset(new Parser(" *[foo="));
t.reset(a->ParseSimpleSelectors(true));
// This is not a valid selector.
EXPECT_FALSE(t.get());
}
TEST_F(ParserTest, comment_breaking_descendant_combinator) {
Parser p(" a b/*foo*/c /*foo*/d/*foo*/ e { }");
scoped_ptr<Ruleset> t(p.ParseRuleset());
ASSERT_EQ(1, t->selectors().size());
const Selector* s = t->selectors().get(0);
ASSERT_EQ(5, s->size());
EXPECT_EQ(SimpleSelectors::NONE, s->get(0)->combinator());
EXPECT_EQ(SimpleSelectors::DESCENDANT, s->get(1)->combinator());
EXPECT_EQ(SimpleSelectors::DESCENDANT, s->get(2)->combinator());
EXPECT_EQ(SimpleSelectors::DESCENDANT, s->get(3)->combinator());
EXPECT_EQ(SimpleSelectors::DESCENDANT, s->get(4)->combinator());
}
TEST_F(ParserTest, comment_breaking_child_combinator) {
Parser p(" a >b/*f>oo*/>c /*fo>o*/>d/*f>oo*/ > e>f { }");
scoped_ptr<Ruleset> t(p.ParseRuleset());
ASSERT_EQ(1, t->selectors().size());
const Selector* s = t->selectors().get(0);
ASSERT_EQ(6, s->size());
EXPECT_EQ(SimpleSelectors::NONE, s->get(0)->combinator());
EXPECT_EQ(SimpleSelectors::CHILD, s->get(1)->combinator());
EXPECT_EQ(SimpleSelectors::CHILD, s->get(2)->combinator());
EXPECT_EQ(SimpleSelectors::CHILD, s->get(3)->combinator());
EXPECT_EQ(SimpleSelectors::CHILD, s->get(4)->combinator());
EXPECT_EQ(SimpleSelectors::CHILD, s->get(5)->combinator());
}
TEST_F(ParserTest, ruleset_starts_with_combinator) {
Parser p(" >a { }");
scoped_ptr<Ruleset> t(p.ParseRuleset());
// This is not a valid selector.
EXPECT_FALSE(t.get());
}
TEST_F(ParserTest, simple_selectors) {
// First, a basic case
scoped_ptr<Parser> a(new Parser("*[lang|=fr]"));
scoped_ptr<SimpleSelectors> t(a->ParseSimpleSelectors(false));
EXPECT_EQ(SimpleSelectors::NONE, t->combinator());
{
const SimpleSelector* c = t->get(0);
ASSERT_TRUE(c != NULL);
ASSERT_EQ(SimpleSelector::UNIVERSAL, c->type());
}
{
const SimpleSelector* c = t->get(1);
ASSERT_TRUE(c != NULL);
ASSERT_EQ(SimpleSelector::BEGIN_HYPHEN_ATTRIBUTE, c->type());
EXPECT_EQ("lang", UnicodeTextToUTF8(c->attribute()));
EXPECT_EQ("fr", UnicodeTextToUTF8(c->value()));
}
// Now, a very complex one.
a.reset(new Parser(
"> P:first_child:hover[class~='hidden'][width]#content"
"[id*=logo][id^=logo][id$=\"logo\"]"
"[lang=en].anotherclass.moreclass #next"));
t.reset(a->ParseSimpleSelectors(true));
EXPECT_EQ(SimpleSelectors::CHILD, t->combinator());
// We're going to go through the conditions in reverse order, for kicks.
SimpleSelectors::const_reverse_iterator it = t->rbegin();
// .moreclass
{
SimpleSelector* c = *it++;
ASSERT_EQ(SimpleSelector::CLASS, c->type());
EXPECT_EQ("moreclass", UnicodeTextToUTF8(c->value()));
}
// .anotherclass
{
SimpleSelector* c = *it++;
ASSERT_EQ(SimpleSelector::CLASS, c->type());
EXPECT_EQ("anotherclass", UnicodeTextToUTF8(c->value()));
}
// EXACT_ATTRIBUTE [lang=en]
{
SimpleSelector* c = *it++;
ASSERT_EQ(SimpleSelector::EXACT_ATTRIBUTE, c->type());
EXPECT_EQ("lang", UnicodeTextToUTF8(c->attribute()));
EXPECT_EQ("en", UnicodeTextToUTF8(c->value()));
}
// END_WITH_ATTRIBUTE [id$="logo"]
{
SimpleSelector* c = *it++;
ASSERT_EQ(SimpleSelector::END_WITH_ATTRIBUTE, c->type());
EXPECT_EQ("id", UnicodeTextToUTF8(c->attribute()));
EXPECT_EQ("logo", UnicodeTextToUTF8(c->value()));
}
// BEGIN_WITH_ATTRIBUTE [id^=logo]
{
SimpleSelector* c = *it++;
ASSERT_EQ(SimpleSelector::BEGIN_WITH_ATTRIBUTE, c->type());
EXPECT_EQ("id", UnicodeTextToUTF8(c->attribute()));
EXPECT_EQ("logo", UnicodeTextToUTF8(c->value()));
}
// SUBSTRING_ATTRIBUTE [id*=logo]
{
SimpleSelector* c = *it++;
ASSERT_EQ(SimpleSelector::SUBSTRING_ATTRIBUTE, c->type());
EXPECT_EQ("id", UnicodeTextToUTF8(c->attribute()));
EXPECT_EQ("logo", UnicodeTextToUTF8(c->value()));
}
// ID #content
{
SimpleSelector* c = *it++;
ASSERT_EQ(SimpleSelector::ID, c->type());
EXPECT_EQ("content", UnicodeTextToUTF8(c->value()));
}
// EXIST_ATTRIBUTE [width]
{
SimpleSelector* c = *it++;
ASSERT_EQ(SimpleSelector::EXIST_ATTRIBUTE, c->type());
EXPECT_EQ("width", UnicodeTextToUTF8(c->attribute()));
}
// ONE_OF_ATTRIBUTE [class~=hidden]
{
SimpleSelector* c = *it++;
ASSERT_EQ(SimpleSelector::ONE_OF_ATTRIBUTE, c->type());
EXPECT_EQ("class", UnicodeTextToUTF8(c->attribute()));
EXPECT_EQ("hidden", UnicodeTextToUTF8(c->value()));
}
// PSEUDOCLASS :hover
{
SimpleSelector* c = *it++;
ASSERT_EQ(SimpleSelector::PSEUDOCLASS, c->type());
EXPECT_EQ("hover", UnicodeTextToUTF8(c->pseudoclass()));
}
// PSEUDOCLASS :first_child
{
SimpleSelector* c = *it++;
ASSERT_EQ(SimpleSelector::PSEUDOCLASS, c->type());
EXPECT_EQ("first_child", UnicodeTextToUTF8(c->pseudoclass()));
}
// P
{
SimpleSelector* c = *it++;
ASSERT_EQ(SimpleSelector::ELEMENT_TYPE, c->type());
EXPECT_EQ("P", UnicodeTextToUTF8(c->element_text()));
EXPECT_EQ(kHtmlTagP, c->element_type());
}
ASSERT_TRUE(it == t->rend());
}
TEST_F(ParserTest, bad_simple_selectors) {
scoped_ptr<Parser> a;
scoped_ptr<SimpleSelectors> t;
// valid or not?
a.reset(new Parser(""));
t.reset(a->ParseSimpleSelectors(false));
EXPECT_FALSE(t.get());
a.reset(new Parser("{}"));
t.reset(a->ParseSimpleSelectors(false));
EXPECT_FALSE(t.get());
a.reset(new Parser("#"));
t.reset(a->ParseSimpleSelectors(false));
EXPECT_FALSE(t.get());
a.reset(new Parser("# {"));
t.reset(a->ParseSimpleSelectors(false));
EXPECT_FALSE(t.get());
a.reset(new Parser("#{"));
t.reset(a->ParseSimpleSelectors(false));
EXPECT_FALSE(t.get());
a.reset(new Parser("##"));
t.reset(a->ParseSimpleSelectors(false));
EXPECT_FALSE(t.get());
a.reset(new Parser("*[class="));
t.reset(a->ParseSimpleSelectors(false));
EXPECT_FALSE(t.get());
a.reset(new Parser("*[class=hidden];"));
t.reset(a->ParseSimpleSelectors(false));
EXPECT_FALSE(t.get());
a.reset(new Parser("*[class=hidden].;"));
t.reset(a->ParseSimpleSelectors(false));
EXPECT_FALSE(t.get());
a.reset(new Parser("#a {"));
t.reset(a->ParseSimpleSelectors(false));
EXPECT_TRUE(t.get());
}
TEST_F(ParserTest, selectors) {
scoped_ptr<Parser> a(new Parser("h1 p #id {"));
scoped_ptr<Selectors> t(a->ParseSelectors());
EXPECT_TRUE(t.get());
ASSERT_EQ(1, t->size());
EXPECT_EQ(3, (*t)[0]->size());
EXPECT_EQ('{', *a->getpos());
a.reset(new Parser(" h1 p #id , div.p > h2 > div.t #id"));
t.reset(a->ParseSelectors());
EXPECT_TRUE(t.get());
ASSERT_EQ(2, t->size());
EXPECT_EQ(3, (*t)[0]->size());
EXPECT_EQ(4, (*t)[1]->size());
EXPECT_TRUE(a->Done());
a.reset(new Parser("/*c*/h1 p #id/*c*/,/*c*/div.p > h2 > div.t #id/*c*/"));
t.reset(a->ParseSelectors());
EXPECT_TRUE(t.get());
ASSERT_EQ(2, t->size());
EXPECT_EQ(3, (*t)[0]->size());
EXPECT_EQ(4, (*t)[1]->size());
EXPECT_TRUE(a->Done());
a.reset(new Parser("{}"));
t.reset(a->ParseSelectors());
EXPECT_FALSE(t.get());
EXPECT_EQ('{', *a->getpos());
a.reset(new Parser(""));
t.reset(a->ParseSelectors());
EXPECT_FALSE(t.get());
EXPECT_TRUE(a->Done());
a.reset(new Parser(" ,h1 p #id {"));
t.reset(a->ParseSelectors());
EXPECT_FALSE(t.get());
EXPECT_EQ('{', *a->getpos());
a.reset(new Parser(" , {"));
t.reset(a->ParseSelectors());
EXPECT_FALSE(t.get());
EXPECT_EQ('{', *a->getpos());
a.reset(new Parser("h1 p #id, {"));
t.reset(a->ParseSelectors());
EXPECT_FALSE(t.get());
EXPECT_EQ('{', *a->getpos());
a.reset(new Parser("h1 p #id, {"));
t.reset(a->ParseSelectors());
EXPECT_FALSE(t.get());
EXPECT_EQ('{', *a->getpos());
a.reset(new Parser("h1 p #id;"));
t.reset(a->ParseSelectors());
EXPECT_FALSE(t.get());
EXPECT_TRUE(a->Done());
a.reset(new Parser(" h1 p[class=/*{*/ #id , h2 #id"));
t.reset(a->ParseSelectors());
EXPECT_FALSE(t.get());
EXPECT_TRUE(a->Done());
a.reset(new Parser(" h1 #id. , h2 #id"));
t.reset(a->ParseSelectors());
EXPECT_FALSE(t.get());
EXPECT_TRUE(a->Done());
}
TEST_F(ParserTest, rulesets) {
scoped_ptr<Parser> a(new Parser("h1 p #id ;"));
scoped_ptr<Ruleset> t(a->ParseRuleset());
EXPECT_EQ(static_cast<Ruleset *>(NULL), t.get());
a.reset(new Parser(", { }"));
t.reset(a->ParseRuleset());
EXPECT_FALSE(t.get());
a.reset(new Parser(", h1 p #id, { };"));
t.reset(a->ParseRuleset());
EXPECT_FALSE(t.get());
a.reset(new Parser(
"h1 p + #id { font-size: 7px; width:10pt !important;}"));
t.reset(a->ParseRuleset());
ASSERT_EQ(1, t->selectors().size());
ASSERT_EQ(3, t->selector(0).size());
EXPECT_EQ(SimpleSelectors::SIBLING,
t->selectors()[0]->at(2)->combinator());
ASSERT_EQ(2, t->declarations().size());
EXPECT_EQ(false, t->declarations()[0]->IsImportant());
EXPECT_EQ(Property::WIDTH, t->declarations()[1]->prop());
EXPECT_EQ(true, t->declarations()[1]->IsImportant());
a.reset(new Parser("h1 p + #id , h1:first_child { font-size: 10px; }"));
t.reset(a->ParseRuleset());
ASSERT_EQ(2, t->selectors().size());
ASSERT_EQ(3, t->selector(0).size());
ASSERT_EQ(1, t->selector(1).size());
EXPECT_EQ(SimpleSelectors::SIBLING,
t->selectors()[0]->at(2)->combinator());
ASSERT_EQ(1, t->declarations().size());
EXPECT_EQ(false, t->declarations()[0]->IsImportant());
}
TEST_F(ParserTest, atrules) {
scoped_ptr<Parser> a(
new Parser("@IMPORT url(assets/style.css) screen,printer;"));
scoped_ptr<Stylesheet> t(new Stylesheet());
a->ParseStatement(NULL, t.get());
ASSERT_EQ(1, t->imports().size());
EXPECT_EQ("assets/style.css", UnicodeTextToUTF8(t->import(0).link()));
EXPECT_EQ(2, t->import(0).media_queries().size());
EXPECT_EQ(true, a->Done());
a.reset(new Parser("@import url(foo.css)"));
t.reset(new Stylesheet());
a->ParseStatement(NULL, t.get());
// We should raise an error for unclosed @import.
EXPECT_NE(Parser::kNoError, a->errors_seen_mask());
// But also still record it.
EXPECT_EQ(1, t->imports().size());
a.reset(new Parser("@charset \"ISO-8859-1\" ;"));
t.reset(new Stylesheet());
a->ParseStatement(NULL, t.get());
EXPECT_EQ(true, a->Done());
a.reset(new Parser(
"@media print,screen {\n\tbody { font-size: 10pt }\n}"));
t.reset(new Stylesheet());
a->ParseStatement(NULL, t.get());
ASSERT_EQ(1, t->rulesets().size());
ASSERT_EQ(1, t->ruleset(0).selectors().size());
ASSERT_EQ(2, t->ruleset(0).media_queries().size());
EXPECT_EQ(MediaQuery::NO_QUALIFIER,
t->ruleset(0).media_query(0).qualifier());
EXPECT_EQ("print",
UnicodeTextToUTF8(t->ruleset(0).media_query(0).media_type()));
EXPECT_EQ(0, t->ruleset(0).media_query(0).expressions().size());
EXPECT_EQ(MediaQuery::NO_QUALIFIER,
t->ruleset(0).media_query(1).qualifier());
EXPECT_EQ("screen",
UnicodeTextToUTF8(t->ruleset(0).media_query(1).media_type()));
EXPECT_EQ(0, t->ruleset(0).media_query(1).expressions().size());
ASSERT_EQ(1, t->ruleset(0).selectors()[0]->size());
EXPECT_EQ(kHtmlTagBody,
t->ruleset(0).selector(0)[0]->get(0)->element_type());
ASSERT_EQ(1, t->ruleset(0).declarations().size());
EXPECT_EQ(Property::FONT_SIZE,
t->ruleset(0).declarations()[0]->prop());
EXPECT_EQ(true, a->Done());
a.reset(new Parser(
"@page :left { margin-left: 4cm; margin-right: 3cm; }"));
t.reset(new Stylesheet());
a->ParseStatement(NULL, t.get());
EXPECT_EQ(0, t->rulesets().size());
EXPECT_EQ(true, a->Done());
// Make sure media strings can be shared between multiple rulesets.
a.reset(new Parser(
"@media print { a { color: red; } p { color: blue; } }"));
t.reset(new Stylesheet());
a->ParseStatement(NULL, t.get());
ASSERT_EQ(2, t->rulesets().size());
ASSERT_EQ(1, t->ruleset(0).media_queries().size());
EXPECT_EQ("print",
UnicodeTextToUTF8(t->ruleset(0).media_query(0).media_type()));
ASSERT_EQ(1, t->ruleset(1).media_queries().size());
EXPECT_EQ("print",
UnicodeTextToUTF8(t->ruleset(1).media_query(0).media_type()));
t->ToString(); // Make sure it can be written as a string.
a.reset(new Parser(
"@font-face { font-family: 'Cabin'; src: local('Wingdings'); }"));
t.reset(new Stylesheet());
a->ParseStatement(NULL, t.get());
EXPECT_EQ(0, t->rulesets().size());
ASSERT_EQ(1, t->font_faces().size());
EXPECT_EQ(2, t->font_face(0).declarations().size());
}
TEST_F(ParserTest, stylesheets) {
scoped_ptr<Parser> a(
new Parser("\n"
"\t@import \"mystyle.css\" all; "
"@import url(\"mystyle.css\" );\n"
"\tBODY {\n"
"color:black !important; \n"
"background: white !important; }\n"
"* {\n"
"\tcolor: inherit !important;\n"
"background: transparent;\n"
"}\n"
"\n"
"<!-- html comments * { font-size: 1 } -->\n"
"H1 + *[REL-up] {}"));
scoped_ptr<Stylesheet> t(a->ParseStylesheet());
EXPECT_EQ(Parser::kNoError, a->errors_seen_mask());
ASSERT_EQ(2, t->imports().size());
EXPECT_EQ("mystyle.css", UnicodeTextToUTF8(t->import(0).link()));
ASSERT_EQ(1, t->import(0).media_queries().size());
EXPECT_EQ("all",
UnicodeTextToUTF8(t->import(0).media_queries()[0]->media_type()));
EXPECT_EQ("mystyle.css", UnicodeTextToUTF8(t->import(1).link()));
// html-style comment should NOT work
EXPECT_EQ(4, t->rulesets().size());
EXPECT_TRUE(a->Done());
}
TEST_F(ParserTest, ParseRawStylesheetDoesNotExpand) {
{
Parser p("a { background: none; }");
scoped_ptr<Stylesheet> stylesheet(p.ParseRawStylesheet());
ASSERT_EQ(1, stylesheet->rulesets().size());
ASSERT_EQ(1, stylesheet->ruleset(0).declarations().size());
ASSERT_EQ(1, stylesheet->ruleset(0).declaration(0).values()->size());
EXPECT_TRUE(p.Done());
}
{
Parser p("a { font: 12px verdana; }");
scoped_ptr<Stylesheet> stylesheet(p.ParseRawStylesheet());
ASSERT_EQ(1, stylesheet->rulesets().size());
ASSERT_EQ(1, stylesheet->ruleset(0).declarations().size());
const Values& values = *stylesheet->ruleset(0).declaration(0).values();
ASSERT_EQ(6, values.size());
// ParseRaw will expand the values out to:
// font: normal normal normal 12px/normal verdana
// But it will not expand out the six other declarations.
// TODO(sligocki): There has got to be a nicer way to test this.
EXPECT_EQ(Identifier::NORMAL, values[0]->GetIdentifier().ident());
EXPECT_EQ(Identifier::NORMAL, values[1]->GetIdentifier().ident());
EXPECT_EQ(Identifier::NORMAL, values[2]->GetIdentifier().ident());
EXPECT_DOUBLE_EQ(12.0, values[3]->GetFloatValue());
EXPECT_EQ(Value::PX, values[3]->GetDimension());
EXPECT_EQ(Identifier::NORMAL, values[4]->GetIdentifier().ident());
EXPECT_EQ("verdana", UnicodeTextToUTF8(values[5]->GetIdentifierText()));
EXPECT_TRUE(p.Done());
}
}
TEST_F(ParserTest, ParseStylesheetDoesExpand) {
{
Parser p("a { background: none; }");
scoped_ptr<Stylesheet> stylesheet(p.ParseStylesheet());
ASSERT_EQ(1, stylesheet->rulesets().size());
const Declarations& declarations = stylesheet->ruleset(0).declarations();
ASSERT_EQ(7, declarations.size());
EXPECT_EQ(Property::BACKGROUND, declarations[0]->prop());
EXPECT_EQ(Property::BACKGROUND_COLOR, declarations[1]->prop());
EXPECT_EQ(Property::BACKGROUND_IMAGE, declarations[2]->prop());
EXPECT_EQ(Property::BACKGROUND_REPEAT, declarations[3]->prop());
EXPECT_EQ(Property::BACKGROUND_ATTACHMENT, declarations[4]->prop());
EXPECT_EQ(Property::BACKGROUND_POSITION_X, declarations[5]->prop());
EXPECT_EQ(Property::BACKGROUND_POSITION_Y, declarations[6]->prop());
EXPECT_TRUE(p.Done());
}
{
Parser p("a { font: 12px verdana; }");
scoped_ptr<Stylesheet> stylesheet(p.ParseStylesheet());
ASSERT_EQ(1, stylesheet->rulesets().size());
const Declarations& declarations = stylesheet->ruleset(0).declarations();
ASSERT_EQ(7, declarations.size());
EXPECT_EQ(Property::FONT, declarations[0]->prop());
EXPECT_EQ(6, declarations[0]->values()->size());
EXPECT_EQ(Property::FONT_STYLE, declarations[1]->prop());
EXPECT_EQ(Property::FONT_VARIANT, declarations[2]->prop());
EXPECT_EQ(Property::FONT_WEIGHT, declarations[3]->prop());
EXPECT_EQ(Property::FONT_SIZE, declarations[4]->prop());
EXPECT_EQ(Property::LINE_HEIGHT, declarations[5]->prop());
EXPECT_EQ(Property::FONT_FAMILY, declarations[6]->prop());
}
}
TEST_F(ParserTest, percentage_colors) {
Value hundred(100.0, Value::PERCENT);
EXPECT_EQ(255, Parser::ValueToRGB(&hundred));
Value zero(0.0, Value::PERCENT);
EXPECT_EQ(0, Parser::ValueToRGB(&zero));
}
TEST_F(ParserTest, value_equality) {
Value hundred(100.0, Value::PERCENT);
Value hundred2(100.0, Value::PERCENT);
Value zero(0.0, Value::PERCENT);
Identifier auto_ident(Identifier::AUTO);
Value ident(auto_ident);
EXPECT_TRUE(hundred.Equals(hundred2));
EXPECT_FALSE(hundred.Equals(zero));
EXPECT_FALSE(hundred.Equals(ident));
}
TEST_F(ParserTest, Utf8Error) {
Parser p("font-family: \"\xCB\xCE\xCC\xE5\"");
scoped_ptr<Declarations> declarations(p.ParseDeclarations());
EXPECT_EQ(1, declarations->size());
EXPECT_EQ(Parser::kUtf8Error, p.errors_seen_mask());
}
TEST_F(ParserTest, DeclarationError) {
scoped_ptr<Parser> p(new Parser("font-family ; "));
scoped_ptr<Declarations> declarations(p->ParseDeclarations());
EXPECT_EQ(0, declarations->size());
EXPECT_EQ(Parser::kDeclarationError, p->errors_seen_mask());
p.reset(new Parser("padding-top: 1.em"));
declarations.reset(p->ParseDeclarations());
EXPECT_TRUE(Parser::kDeclarationError & p->errors_seen_mask());
p.reset(new Parser("color: red !ie"));
declarations.reset(p->ParseDeclarations());
EXPECT_TRUE(Parser::kDeclarationError & p->errors_seen_mask());
p.reset(new Parser("color: red !important really"));
declarations.reset(p->ParseDeclarations());
EXPECT_TRUE(Parser::kDeclarationError & p->errors_seen_mask());
}
TEST_F(ParserTest, SelectorError) {
Parser p(".bold: { font-weight: bold }");
scoped_ptr<Stylesheet> stylesheet(p.ParseStylesheet());
EXPECT_EQ(0, stylesheet->rulesets().size());
EXPECT_TRUE(Parser::kSelectorError & p.errors_seen_mask());
Parser p2("div:nth-child(1n) { color: red; }");
stylesheet.reset(p2.ParseStylesheet());
EXPECT_TRUE(Parser::kSelectorError & p2.errors_seen_mask());
// Note: We fail to parse the (1n). If this is fixed, this test should be
// updated accordingly.
EXPECT_EQ("/* AUTHOR */\n\n\n\ndiv:nth-child {color: #ff0000}\n",
stylesheet->ToString());
Parser p3("}}");
stylesheet.reset(p3.ParseStylesheet());
EXPECT_EQ(0, stylesheet->rulesets().size());
EXPECT_TRUE(Parser::kSelectorError & p3.errors_seen_mask());
Parser p4("div[too=many=equals] { color: red; }");
stylesheet.reset(p4.ParseStylesheet());
EXPECT_TRUE(Parser::kSelectorError & p4.errors_seen_mask());
EXPECT_EQ("/* AUTHOR */\n\n\n\ndiv[too=\"many\"] {color: #ff0000}\n",
stylesheet->ToString());
}
TEST_F(ParserTest, MediaError) {
scoped_ptr<Parser> p(
new Parser("@media screen and (max-width^?`) { .a { color: red; } }"));
scoped_ptr<Stylesheet> stylesheet(p->ParseStylesheet());
EXPECT_TRUE(Parser::kMediaError & p->errors_seen_mask());
// Note: User agents are to represent a media query as "not all" when one
// of the specified media features is not known.
// http://www.w3.org/TR/css3-mediaqueries/#error-handling
EXPECT_EQ("/* AUTHOR */\n\n\n\n"
"@media not all { .a {color: #ff0000} }\n",
stylesheet->ToString());
p.reset(new Parser(
"@media screen and (max-width^?`), print { .a { color: red; } }"));
stylesheet.reset(p->ParseStylesheet());
EXPECT_TRUE(Parser::kMediaError & p->errors_seen_mask());
// Note: First media query should be treated as "not all", but the second
// one should be used normally.
EXPECT_EQ("/* AUTHOR */\n\n\n\n"
"@media not all, print { .a {color: #ff0000} }\n",
stylesheet->ToString());
p.reset(new Parser("@media { .a { color: red; } }"));
stylesheet.reset(p->ParseStylesheet());
EXPECT_FALSE(Parser::kMediaError & p->errors_seen_mask());
// Note: Empty media query means no media restrictions.
EXPECT_EQ("/* AUTHOR */\n\n\n\n"
".a {color: #ff0000}\n",
stylesheet->ToString());
}
TEST_F(ParserTest, HtmlCommentError) {
Parser good("<!-- a { color: red } -->");
scoped_ptr<Stylesheet> stylesheet(good.ParseStylesheet());
EXPECT_EQ(Parser::kNoError, good.errors_seen_mask());
EXPECT_EQ("/* AUTHOR */\n\n\n\na {color: #ff0000}\n", stylesheet->ToString());
const char* bad_strings[] = {
"< a { color: red } -->",
"<! a { color: red } -->",
"<!- a { color: red } -->",
"<!-- a { color: red } --",
"<!-- a { color: red } ->",
"<!-- a { color: red } -",
"<>a { color: red }",
};
for (int i = 0; i < arraysize(bad_strings); ++i) {
Parser bad(bad_strings[i]);
stylesheet.reset(bad.ParseStylesheet());
EXPECT_TRUE(Parser::kHtmlCommentError & bad.errors_seen_mask());
}
}
TEST_F(ParserTest, ValueError) {
Parser p("(12)");
scoped_ptr<Value> value(p.ParseAny());
EXPECT_TRUE(Parser::kValueError & p.errors_seen_mask());
EXPECT_TRUE(NULL == value.get());
}
TEST_F(ParserTest, SkippedTokenError) {
Parser p("12pt @foo Arial");
scoped_ptr<Values> values(p.ParseValues(Property::FONT));
EXPECT_TRUE(Parser::kSkippedTokenError & p.errors_seen_mask());
EXPECT_EQ("12pt Arial", values->ToString());
}
TEST_F(ParserTest, CharsetError) {
// Valid
Parser p("@charset \"UTF-8\";");
scoped_ptr<Stylesheet> stylesheet(p.ParseStylesheet());
EXPECT_EQ(Parser::kNoError, p.errors_seen_mask());
EXPECT_EQ("/* AUTHOR */\n@charset \"UTF-8\";\n\n\n\n",
stylesheet->ToString());
// Error: Identifier instead of string.
Parser p2("@charset foobar;");
stylesheet.reset(p2.ParseStylesheet());
EXPECT_EQ(Parser::kCharsetError, p2.errors_seen_mask());
EXPECT_EQ("/* AUTHOR */\n\n\n\n\n", stylesheet->ToString());
// Error: Bad format.
Parser p3("@charset \"UTF-8\" \"or 9\";");
stylesheet.reset(p3.ParseStylesheet());
EXPECT_EQ(Parser::kCharsetError, p3.errors_seen_mask());
EXPECT_EQ("/* AUTHOR */\n\n\n\n\n", stylesheet->ToString());
// Error: No closing ;
Parser p4("@charset \"UTF-8\"");
stylesheet.reset(p4.ParseStylesheet());
EXPECT_EQ(Parser::kCharsetError, p4.errors_seen_mask());
// @charset is still recorded even though it was unclosed.
EXPECT_EQ("/* AUTHOR */\n@charset \"UTF-8\";\n\n\n\n",
stylesheet->ToString());
}
TEST_F(ParserTest, AcceptCorrectValues) {
// http://code.google.com/p/modpagespeed/issues/detail?id=128
Parser p("list-style-type: none");
scoped_ptr<Declarations> declarations(p.ParseDeclarations());
EXPECT_EQ(1, declarations->size());
EXPECT_EQ(Parser::kNoError, p.errors_seen_mask());
EXPECT_EQ("list-style-type: none", declarations->ToString());
}
TEST_F(ParserTest, AcceptAllValues) {
Parser p("display: -moz-inline-box");
p.set_preservation_mode(true);
scoped_ptr<Declarations> declarations(p.ParseDeclarations());
EXPECT_EQ(Parser::kNoError, p.errors_seen_mask());
ASSERT_EQ(1, declarations->size());
ASSERT_EQ(1, declarations->at(0)->values()->size());
const Value* value = declarations->at(0)->values()->at(0);
EXPECT_EQ(Value::IDENT, value->GetLexicalUnitType());
EXPECT_EQ(Identifier::OTHER, value->GetIdentifier().ident());
EXPECT_EQ("-moz-inline-box",
UnicodeTextToUTF8(value->GetIdentifier().ident_text()));
EXPECT_EQ("display: -moz-inline-box", declarations->ToString());
Parser p2("display: -moz-inline-box");
p2.set_preservation_mode(false);
declarations.reset(p2.ParseDeclarations());
EXPECT_EQ(Parser::kDeclarationError, p2.errors_seen_mask());
EXPECT_EQ(0, declarations->size());
EXPECT_EQ("", declarations->ToString());
}
TEST_F(ParserTest, VerbatimDeclarations) {
Parser p("color: red; z-i ndex: 42; width: 1px");
p.set_preservation_mode(false);
scoped_ptr<Declarations> declarations(p.ParseDeclarations());
EXPECT_EQ(Parser::kDeclarationError, p.errors_seen_mask());
ASSERT_EQ(2, declarations->size());
EXPECT_EQ(Property::COLOR, declarations->at(0)->prop());
EXPECT_EQ(Property::WIDTH, declarations->at(1)->prop());
// Unparsed declartion is ignored.
EXPECT_EQ("color: #ff0000; width: 1px", declarations->ToString());
Parser p2("color: red; z-i ndex: 42; width: 1px");
p2.set_preservation_mode(true);
declarations.reset(p2.ParseDeclarations());
EXPECT_EQ(Parser::kNoError, p2.errors_seen_mask());
EXPECT_EQ(Parser::kDeclarationError, p2.unparseable_sections_seen_mask());
ASSERT_EQ(3, declarations->size());
EXPECT_EQ(Property::COLOR, declarations->at(0)->prop());
EXPECT_EQ(Property::UNPARSEABLE, declarations->at(1)->prop());
EXPECT_EQ("z-i ndex: 42", declarations->at(1)->bytes_in_original_buffer());
EXPECT_EQ(Property::WIDTH, declarations->at(2)->prop());
EXPECT_EQ("color: #ff0000; /* Unparsed declaration: */ z-i ndex: 42; "
"width: 1px", declarations->ToString());
}
TEST_F(ParserTest, CssHacks) {
Parser p("*border: 0px");
p.set_preservation_mode(false);
scoped_ptr<Declarations> declarations(p.ParseDeclarations());
EXPECT_EQ(Parser::kDeclarationError, p.errors_seen_mask());
Parser p2("*border: 0px");
p2.set_preservation_mode(true);
declarations.reset(p2.ParseDeclarations());
EXPECT_EQ(Parser::kNoError, p2.errors_seen_mask());
ASSERT_EQ(1, declarations->size());
// * is not a valid identifier char, so we don't parse it into prop_text.
EXPECT_EQ(Property::UNPARSEABLE, declarations->at(0)->prop());
EXPECT_EQ("/* Unparsed declaration: */ *border: 0px",
declarations->ToString());
Parser p3("width: 1px; _width: 3px;");
declarations.reset(p3.ParseDeclarations());
EXPECT_EQ(Parser::kNoError, p3.errors_seen_mask());
ASSERT_EQ(2, declarations->size());
EXPECT_EQ(Property::WIDTH, declarations->at(0)->prop());
EXPECT_EQ(Property::OTHER, declarations->at(1)->prop());
// _ is a valid identifier char, so we do parse it into prop_text.
EXPECT_EQ("_width", declarations->at(1)->prop_text());
EXPECT_EQ("width: 1px; _width: 3px", declarations->ToString());
}
TEST_F(ParserTest, Function) {
Parser p("box-shadow: -1px -2px 2px rgba(0, 13, 255, .15)");
scoped_ptr<Declarations> declarations(p.ParseDeclarations());
EXPECT_EQ(Parser::kNoError, p.errors_seen_mask());
ASSERT_EQ(1, declarations->size());
EXPECT_EQ(4, declarations->at(0)->values()->size());
const Value* val = declarations->at(0)->values()->at(3);
EXPECT_EQ(Value::FUNCTION, val->GetLexicalUnitType());
EXPECT_EQ(UTF8ToUnicodeText("rgba"), val->GetFunctionName());
const Values& params = *val->GetParameters();
ASSERT_EQ(4, params.size());
EXPECT_EQ(Value::NUMBER, params[0]->GetLexicalUnitType());
EXPECT_EQ(0, params[0]->GetIntegerValue());
EXPECT_EQ(Value::NUMBER, params[1]->GetLexicalUnitType());
EXPECT_EQ(13, params[1]->GetIntegerValue());
EXPECT_EQ(Value::NUMBER, params[2]->GetLexicalUnitType());
EXPECT_EQ(255, params[2]->GetIntegerValue());
EXPECT_EQ(Value::NUMBER, params[3]->GetLexicalUnitType());
EXPECT_DOUBLE_EQ(0.15, params[3]->GetFloatValue());
EXPECT_EQ("box-shadow: -1px -2px 2px rgba(0, 13, 255, 0.15)",
declarations->ToString());
}
// Functions inside functions and mixed use of commas and spaces, oh my.
TEST_F(ParserTest, ComplexFunction) {
Parser p(
"-webkit-gradient(linear, left top, left bottom, from(#ccc), to(#ddd))");
scoped_ptr<Value> val(ParseAny(&p));
EXPECT_EQ(Value::FUNCTION, val->GetLexicalUnitType());
EXPECT_EQ(Parser::kNoError, p.errors_seen_mask());
EXPECT_EQ("-webkit-gradient(linear, left top, left bottom, "
"from(#cccccc), to(#dddddd))", val->ToString());
}
TEST_F(ParserTest, MaxNestedFunctions) {
Parser p("a(b(1,2,3))");
p.set_max_function_depth(1);
scoped_ptr<Value> val(ParseAny(&p));
EXPECT_TRUE(NULL == val.get());
EXPECT_TRUE(Parser::kFunctionError & p.errors_seen_mask());
}
TEST_F(ParserTest, Counter) {
Parser p("content: \"Section \" counter(section)");
scoped_ptr<Declarations> declarations(p.ParseDeclarations());
EXPECT_EQ(Parser::kNoError, p.errors_seen_mask());
ASSERT_EQ(1, declarations->size());
ASSERT_EQ(2, declarations->at(0)->values()->size());
const Value* val = declarations->at(0)->values()->at(1);
EXPECT_EQ(Value::FUNCTION, val->GetLexicalUnitType());
EXPECT_EQ(UTF8ToUnicodeText("counter"), val->GetFunctionName());
const Values& params = *val->GetParameters();
ASSERT_EQ(1, params.size());
EXPECT_EQ(Value::IDENT, params[0]->GetLexicalUnitType());
EXPECT_EQ(UTF8ToUnicodeText("section"), params[0]->GetIdentifierText());
EXPECT_EQ("content: \"Section \" counter(section)",
declarations->ToString());
}
TEST_F(ParserTest, ParseNextImport) {
scoped_ptr<Parser> parser(
new Parser("@IMPORT url(assets/style.css) screen,printer;"));
scoped_ptr<Import> import(parser->ParseNextImport());
EXPECT_TRUE(import.get() != NULL);
EXPECT_TRUE(parser->Done());
if (import.get() != NULL) {
EXPECT_EQ("assets/style.css", UnicodeTextToUTF8(import->link()));
EXPECT_EQ(2, import->media_queries().size());
}
parser.reset(new Parser("\n\t@import \"mystyle.css\" all; \n"));
import.reset(parser->ParseNextImport());
EXPECT_TRUE(import.get() != NULL);
EXPECT_TRUE(parser->Done());
if (import.get() != NULL) {
EXPECT_EQ("mystyle.css", UnicodeTextToUTF8(import->link()));
EXPECT_EQ(1, import->media_queries().size());
}
parser.reset(new Parser("\n\t@import url(\"mystyle.css\"); \n"));
import.reset(parser->ParseNextImport());
EXPECT_TRUE(import.get() != NULL);
EXPECT_TRUE(parser->Done());
if (import.get() != NULL) {
EXPECT_EQ("mystyle.css", UnicodeTextToUTF8(import->link()));
EXPECT_EQ(0, import->media_queries().size());
}
parser.reset(new Parser("*border: 0px"));
import.reset(parser->ParseNextImport());
EXPECT_TRUE(import.get() == NULL);
EXPECT_FALSE(parser->Done());
parser.reset(new Parser("@import \"mystyle.css\" all;\n"
"*border: 0px"));
import.reset(parser->ParseNextImport());
EXPECT_TRUE(import.get() != NULL);
EXPECT_FALSE(parser->Done());
parser.reset(new Parser("@import \"mystyle.css\" all;\n"
"@import url(\"mystyle.css\" );\n"));
import.reset(parser->ParseNextImport());
EXPECT_TRUE(import.get() != NULL);
EXPECT_FALSE(parser->Done());
import.reset(parser->ParseNextImport());
EXPECT_TRUE(import.get() != NULL);
EXPECT_TRUE(parser->Done());
import.reset(parser->ParseNextImport());
EXPECT_TRUE(import.get() == NULL);
EXPECT_TRUE(parser->Done());
parser.reset(new Parser("@import \"mystyle.css\" all;\n"
"@import url(\"mystyle.css\" );\n"
"*border: 0px"));
import.reset(parser->ParseNextImport());
EXPECT_TRUE(import.get() != NULL);
EXPECT_FALSE(parser->Done());
import.reset(parser->ParseNextImport());
EXPECT_TRUE(import.get() != NULL);
EXPECT_FALSE(parser->Done());
import.reset(parser->ParseNextImport());
EXPECT_TRUE(import.get() == NULL);
EXPECT_FALSE(parser->Done());
parser.reset(new Parser("@charset \"ISO-8859-1\";\n"
"@import \"mystyle.css\" all;"));
import.reset(parser->ParseNextImport());
EXPECT_TRUE(import.get() == NULL);
}
TEST_F(ParserTest, ParseSingleImport) {
scoped_ptr<Parser> parser(
new Parser("@IMPORT url(assets/style.css) screen,printer;"));
scoped_ptr<Import> import(parser->ParseAsSingleImport());
EXPECT_TRUE(import.get() != NULL);
if (import.get() != NULL) {
EXPECT_EQ("assets/style.css", UnicodeTextToUTF8(import->link()));
EXPECT_EQ(2, import->media_queries().size());
}
parser.reset(new Parser("\n\t@import \"mystyle.css\" all; \n"));
import.reset(parser->ParseAsSingleImport());
EXPECT_TRUE(import.get() != NULL);
if (import.get() != NULL) {
EXPECT_EQ("mystyle.css", UnicodeTextToUTF8(import->link()));
EXPECT_EQ(1, import->media_queries().size());
}
parser.reset(new Parser("\n\t@import url(\"mystyle.css\"); \n"));
import.reset(parser->ParseAsSingleImport());
EXPECT_TRUE(import.get() != NULL);
if (import.get() != NULL) {
EXPECT_EQ("mystyle.css", UnicodeTextToUTF8(import->link()));
EXPECT_EQ(0, import->media_queries().size());
}
parser.reset(new Parser("*border: 0px"));
import.reset(parser->ParseAsSingleImport());
EXPECT_TRUE(import.get() == NULL);
parser.reset(new Parser("@import \"mystyle.css\" all;\n"
"@import url(\"mystyle.css\" );\n"));
import.reset(parser->ParseAsSingleImport());
EXPECT_TRUE(import.get() == NULL);
parser.reset(new Parser("@charset \"ISO-8859-1\";\n"
"@import \"mystyle.css\" all;"));
import.reset(parser->ParseAsSingleImport());
EXPECT_TRUE(import.get() == NULL);
}
TEST_F(ParserTest, MediaQueries) {
Parser p("@import url(a.css);\n"
"@import url(b.css) screen;\n"
"@import url(c.css) NOT (max-width: 300px) and (color);\n"
"@import url(d.css) only print and (color), not screen;\n"
"@media { .a { color: red; } }\n"
"@media onLy screen And (max-width: 250px) { .a { color: green } }\n"
".a { color: blue; }\n"
"@media (nonsense: foo(')', \")\")) { body { color: red } }\n");
scoped_ptr<Stylesheet> s(p.ParseStylesheet());
ASSERT_EQ(4, s->imports().size());
EXPECT_EQ(0, s->import(0).media_queries().size());
ASSERT_EQ(1, s->import(1).media_queries().size());
EXPECT_EQ(MediaQuery::NO_QUALIFIER,
s->import(1).media_queries()[0]->qualifier());
EXPECT_EQ("screen", UnicodeTextToUTF8(
s->import(1).media_queries()[0]->media_type()));
EXPECT_EQ(0, s->import(1).media_queries()[0]->expressions().size());
ASSERT_EQ(1, s->import(2).media_queries().size());
EXPECT_EQ(MediaQuery::NOT, s->import(2).media_queries()[0]->qualifier());
EXPECT_EQ("", UnicodeTextToUTF8(
s->import(2).media_queries()[0]->media_type()));
ASSERT_EQ(2, s->import(2).media_queries()[0]->expressions().size());
EXPECT_EQ("max-width", UnicodeTextToUTF8(
s->import(2).media_queries()[0]->expression(0).name()));
ASSERT_TRUE(s->import(2).media_queries()[0]->expression(0).has_value());
EXPECT_EQ("300px", UnicodeTextToUTF8(
s->import(2).media_queries()[0]->expression(0).value()));
EXPECT_EQ("color", UnicodeTextToUTF8(
s->import(2).media_queries()[0]->expression(1).name()));
ASSERT_FALSE(s->import(2).media_queries()[0]->expression(1).has_value());
ASSERT_EQ(2, s->import(3).media_queries().size());
EXPECT_EQ(MediaQuery::ONLY, s->import(3).media_queries()[0]->qualifier());
EXPECT_EQ("print", UnicodeTextToUTF8(
s->import(3).media_queries()[0]->media_type()));
ASSERT_EQ(1, s->import(3).media_queries()[0]->expressions().size());
EXPECT_EQ("color", UnicodeTextToUTF8(
s->import(3).media_queries()[0]->expression(0).name()));
ASSERT_FALSE(s->import(3).media_queries()[0]->expression(0).has_value());
EXPECT_EQ(MediaQuery::NOT, s->import(3).media_queries()[1]->qualifier());
EXPECT_EQ("screen", UnicodeTextToUTF8(
s->import(3).media_queries()[1]->media_type()));
EXPECT_EQ(0, s->import(3).media_queries()[1]->expressions().size());
ASSERT_EQ(4, s->rulesets().size());
EXPECT_EQ(0, s->ruleset(0).media_queries().size());
ASSERT_EQ(1, s->ruleset(1).media_queries().size());
EXPECT_EQ(MediaQuery::ONLY, s->ruleset(1).media_query(0).qualifier());
EXPECT_EQ("screen", UnicodeTextToUTF8(
s->ruleset(1).media_query(0).media_type()));
ASSERT_EQ(1, s->ruleset(1).media_query(0).expressions().size());
EXPECT_EQ("max-width", UnicodeTextToUTF8(
s->ruleset(1).media_query(0).expression(0).name()));
ASSERT_TRUE(s->ruleset(1).media_query(0).expression(0).has_value());
EXPECT_EQ("250px", UnicodeTextToUTF8(
s->ruleset(1).media_query(0).expression(0).value()));
EXPECT_EQ(0, s->ruleset(2).media_queries().size());
ASSERT_EQ(1, s->ruleset(3).media_queries().size());
EXPECT_EQ(MediaQuery::NO_QUALIFIER, s->ruleset(3).media_query(0).qualifier());
EXPECT_EQ("", UnicodeTextToUTF8(s->ruleset(3).media_query(0).media_type()));
ASSERT_EQ(1, s->ruleset(3).media_query(0).expressions().size());
EXPECT_EQ("nonsense", UnicodeTextToUTF8(
s->ruleset(3).media_query(0).expression(0).name()));
ASSERT_TRUE(s->ruleset(3).media_query(0).expression(0).has_value());
EXPECT_EQ("foo(')', \")\")", UnicodeTextToUTF8(
s->ruleset(3).media_query(0).expression(0).value()));
}
// Test that we do not "fix" malformed @media queries.
TEST_F(ParserTest, InvalidMediaQueries) {
scoped_ptr<Parser> p;
scoped_ptr<Stylesheet> s;
// This @media query is technically invalid because CSS is defined to
// be lexed context-free first and defines the flex primitive:
// FUNCTION {ident}\(
// Thus "and(color)" will be parsed as a function instead of an identifier
// followed by a media expression.
// See: b/7694757 and
// http://lists.w3.org/Archives/Public/www-style/2012Dec/0263.html
p.reset(new Parser("@media all and(color) { a { color: red; } }"));
s.reset(p->ParseStylesheet());
EXPECT_TRUE(Parser::kMediaError & p->errors_seen_mask());
// Missing "and" between "all" and "(color)".
p.reset(new Parser("@media all (color) { a { color: red; } }"));
s.reset(p->ParseStylesheet());
EXPECT_TRUE(Parser::kMediaError & p->errors_seen_mask());
// Missing "and" and space between "all" and "(color)".
p.reset(new Parser("@media all(color) { a { color: red; } }"));
s.reset(p->ParseStylesheet());
EXPECT_TRUE(Parser::kMediaError & p->errors_seen_mask());
// Too many "and"s.
p.reset(new Parser("@media all and and (color) { a { color: red; } }"));
s.reset(p->ParseStylesheet());
EXPECT_TRUE(Parser::kMediaError & p->errors_seen_mask());
// Too many "and"s and missing space.
p.reset(new Parser("@media all and and(color) { a { color: red; } }"));
s.reset(p->ParseStylesheet());
EXPECT_TRUE(Parser::kMediaError & p->errors_seen_mask());
// Trailing "and".
p.reset(new Parser("@media all and { a { color: red; } }"));
s.reset(p->ParseStylesheet());
EXPECT_TRUE(Parser::kMediaError & p->errors_seen_mask());
// Starting "and".
p.reset(new Parser("@media and (color) { a { color: red; } }"));
s.reset(p->ParseStylesheet());
EXPECT_TRUE(Parser::kMediaError & p->errors_seen_mask());
// Starting "and" and no space.
p.reset(new Parser("@media and(color) { a { color: red; } }"));
s.reset(p->ParseStylesheet());
EXPECT_TRUE(Parser::kMediaError & p->errors_seen_mask());
}
TEST_F(ParserTest, ExtractCharset) {
scoped_ptr<Parser> parser;
UnicodeText charset;
parser.reset(new Parser("@charset \"ISO-8859-1\" ;"));
charset = parser->ExtractCharset();
EXPECT_EQ(Parser::kNoError, parser->errors_seen_mask());
EXPECT_EQ("ISO-8859-1", UnicodeTextToUTF8(charset));
parser.reset(new Parser("@charset foobar;"));
charset = parser->ExtractCharset();
EXPECT_EQ(Parser::kCharsetError, parser->errors_seen_mask());
EXPECT_EQ("", UnicodeTextToUTF8(charset));
parser.reset(new Parser("@charset \"UTF-8\" \"or 9\";"));
charset = parser->ExtractCharset();
EXPECT_EQ(Parser::kCharsetError, parser->errors_seen_mask());
EXPECT_EQ("", UnicodeTextToUTF8(charset));
parser.reset(new Parser("@charsets \"UTF-8\" and \"ISO-8859-1\";"));
charset = parser->ExtractCharset();
EXPECT_EQ(Parser::kNoError, parser->errors_seen_mask());
EXPECT_EQ("", UnicodeTextToUTF8(charset));
parser.reset(new Parser("@IMPORT url(assets/style.css) screen,printer"));
charset = parser->ExtractCharset();
EXPECT_EQ(Parser::kNoError, parser->errors_seen_mask());
EXPECT_EQ("", UnicodeTextToUTF8(charset));
parser.reset(new Parser("wotcha!"));
charset = parser->ExtractCharset();
EXPECT_EQ(Parser::kNoError, parser->errors_seen_mask());
EXPECT_EQ("", UnicodeTextToUTF8(charset));
}
TEST_F(ParserTest, AtFontFace) {
scoped_ptr<Parser> parser;
scoped_ptr<Stylesheet> stylesheet;
// @font-face is parsed.
parser.reset(new Parser(
"@font-face{font-family:'Ubuntu';font-style:normal}\n"
".foo { width: 1px; }"));
stylesheet.reset(parser->ParseStylesheet());
EXPECT_EQ(Parser::kNoError, parser->errors_seen_mask());
EXPECT_EQ("/* AUTHOR */\n\n\n"
"@font-face { font-family: \"Ubuntu\"; font-style: normal }\n"
".foo {width: 1px}\n", stylesheet->ToString());
// Same in preservation mode.
parser.reset(new Parser(
"@font-face{font-family:'Ubuntu';font-style:normal}"
".foo { width: 1px; }"));
parser->set_preservation_mode(true);
stylesheet.reset(parser->ParseStylesheet());
EXPECT_EQ(Parser::kNoError, parser->errors_seen_mask());
EXPECT_EQ("/* AUTHOR */\n\n\n"
"@font-face { font-family: \"Ubuntu\"; font-style: normal }\n"
".foo {width: 1px}\n", stylesheet->ToString());
// Inside @media
parser.reset(new Parser(
"@media print {\n"
" .foo { width: 1px; }\n"
" @font-face { font-family: 'Ubuntu'; font-style: normal; }\n"
" .bar { height: 2em; }\n"
"}\n"));
stylesheet.reset(parser->ParseStylesheet());
EXPECT_EQ(Parser::kNoError, parser->errors_seen_mask());
EXPECT_EQ("/* AUTHOR */\n\n\n"
// Failed to parse complex src: value.
"@media print { @font-face { font-family: \"Ubuntu\"; "
"font-style: normal } }\n"
"@media print { .foo {width: 1px} }\n"
"@media print { .bar {height: 2em} }\n", stylesheet->ToString());
// Complex src values.
parser.reset(new Parser(
"@media print {\n"
" @font-face { font-family: 'Dothraki'; src: local('Khal'),"
" url('dothraki.woff') format('woff'); }\n"
"}\n"));
stylesheet.reset(parser->ParseStylesheet());
EXPECT_EQ(Parser::kNoError, parser->errors_seen_mask());
EXPECT_EQ("/* AUTHOR */\n\n\n"
"@media print { @font-face { font-family: \"Dothraki\"; "
"src: local(\"Khal\") , url(dothraki.woff) format(\"woff\") } }\n"
"\n", stylesheet->ToString());
// @font-face with all properties.
parser.reset(new Parser(
"@font-face {\n"
" font-family: MainText;\n"
" src: url(gentium.eot);\n" // For use with older UAs.
// Overrides last src.
" src: local(\"Gentium\"), url('gentium.ttf') format('truetype'), "
"url(gentium.woff);\n"
" font-style: italic;\n"
" font-weight: 800;\n"
" font-stretch: ultra-condensed;\n"
// TODO(sligocki): Parse unicode-range and font-variant.
" unicode-range: U+590-5ff, u+4??, U+1F63B;\n"
" font-variant: historical-forms, character-variant(cv13), "
"annotiation(circled);\n"
" font-feature-settings: 'hwid', 'swsh' 2;\n"
"}\n"));
stylesheet.reset(parser->ParseStylesheet());
EXPECT_NE(Parser::kNoError, parser->errors_seen_mask());
EXPECT_EQ("/* AUTHOR */\n\n\n"
"@font-face { font-family: MainText; src: url(gentium.eot);"
" src: local(\"Gentium\") ,"
" url(gentium.ttf) format(\"truetype\") ,"
" url(gentium.woff);"
" font-style: italic; font-weight: 800;"
" font-stretch: ultra-condensed;"
" font-feature-settings: \"hwid\" , \"swsh\" 2 }\n\n",
stylesheet->ToString());
}
TEST_F(ParserTest, UnexpectedAtRule) {
scoped_ptr<Parser> parser;
scoped_ptr<Stylesheet> stylesheet;
// Unexpected at-rule with block.
parser.reset(new Parser(
"@creature { toughness: 2; power: 2; abilities: double-strike; "
"protection: black green; }\n"
".foo {width: 1px}\n"));
stylesheet.reset(parser->ParseStylesheet());
EXPECT_TRUE(Parser::kAtRuleError & parser->errors_seen_mask());
EXPECT_EQ("/* AUTHOR */\n\n\n\n.foo {width: 1px}\n", stylesheet->ToString());
// preservation mode.
parser.reset(new Parser(
"@creature { toughness: 2; power: 2; abilities: double-strike; "
"protection: black green; }\n"
".foo {width: 1px}\n"));
parser->set_preservation_mode(true);
stylesheet.reset(parser->ParseStylesheet());
EXPECT_EQ(Parser::kNoError, parser->errors_seen_mask());
EXPECT_EQ("/* AUTHOR */\n\n\n\n"
"/* Unparsed region: */ @creature { toughness: 2; power: 2; "
"abilities: double-strike; protection: black green; }\n"
".foo {width: 1px}\n", stylesheet->ToString());
// ... and with extra selectors.
parser.reset(new Parser(
"@page :first { margin-top: 8cm; }\n"
".foo { width: 1px; }"));
stylesheet.reset(parser->ParseStylesheet());
EXPECT_TRUE(Parser::kAtRuleError & parser->errors_seen_mask());
EXPECT_EQ("/* AUTHOR */\n\n\n\n.foo {width: 1px}\n", stylesheet->ToString());
// ... and with subblocks inside block.
parser.reset(new Parser(
"@keyframes wiggle {\n"
" 0% {transform:rotate(6deg);}\n"
" 50% {transform:rotate(6deg);}\n"
" 100% {transform:rotate(6deg);}\n"
"}\n"
"@-webkit-keyframes wiggle {\n"
" 0% {transform:rotate(6deg);}\n"
" 50% {transform:rotate(6deg);}\n"
" 100% {transform:rotate(6deg);}\n"
"}\n"
".foo { width: 1px; }"));
stylesheet.reset(parser->ParseStylesheet());
EXPECT_TRUE(Parser::kAtRuleError & parser->errors_seen_mask());
EXPECT_EQ("/* AUTHOR */\n\n\n\n.foo {width: 1px}\n", stylesheet->ToString());
parser.reset(new Parser(
"@font-feature-values Jupiter Sans {\n"
" @swash {\n"
" delicate: 1;\n"
" flowing: 2;\n"
" }\n"
"}\n"
".foo { width: 2px; }"));
stylesheet.reset(parser->ParseStylesheet());
EXPECT_TRUE(Parser::kAtRuleError & parser->errors_seen_mask());
EXPECT_EQ("/* AUTHOR */\n\n\n\n.foo {width: 2px}\n", stylesheet->ToString());
// Unexpected at-rule ending in ';'.
parser.reset(new Parser(
"@namespace foo \"http://example.com/ns/foo\";\n"
".foo { width: 1px; }"));
stylesheet.reset(parser->ParseStylesheet());
EXPECT_TRUE(Parser::kAtRuleError & parser->errors_seen_mask());
EXPECT_EQ("/* AUTHOR */\n\n\n\n.foo {width: 1px}\n", stylesheet->ToString());
// Unexpected at-rule with nothing else to parse before ';'.
parser.reset(new Parser(
"@use-klingon;\n"
".foo { width: 1px; }"));
stylesheet.reset(parser->ParseStylesheet());
EXPECT_TRUE(Parser::kAtRuleError & parser->errors_seen_mask());
EXPECT_EQ("/* AUTHOR */\n\n\n\n.foo {width: 1px}\n", stylesheet->ToString());
// Unexpected at-keyword in a block.
parser.reset(new Parser(
"@media screen {\n"
" .bar { height: 2px; on-hover: @use-klingon}\n"
" .baz { height: 4px }\n"
"}\n"
".foo {\n"
" three-dee: @three-dee { @background-lighting { azimuth: 30deg; } };\n"
" width: 1px;\n"
"}\n"));
stylesheet.reset(parser->ParseStylesheet());
// Note: These don't actually call the at-rule parsing code because they
// are not full at-rules, but just at-keywords and are skipped by
// SkipToNextAny().
EXPECT_NE(Parser::kNoError, parser->errors_seen_mask());
EXPECT_EQ("/* AUTHOR */\n\n\n\n"
"@media screen { .bar {height: 2px} }\n"
"@media screen { .baz {height: 4px} }\n"
".foo {width: 1px}\n", stylesheet->ToString());
}
// Make sure parser does not overflow buffers when file ends abruptly.
// Note: You must test with --config=asan for these tests to detect overflows.
TEST_F(ParserTest, EOFMedia) {
TrapEOF("@media");
TrapEOF("@media ");
TrapEOF("@media (");
TrapEOF("@media ( ");
TrapEOF("@media (size");
TrapEOF("@media (size ");
TrapEOF("@media (size:");
TrapEOF("@media (size: ");
TrapEOF("@media (size: foo");
TrapEOF("@media (size: foo ");
TrapEOF("@media (size: foo)");
TrapEOF("@media (size: foo) ");
TrapEOF("@media ( size : foo ) ");
}
TEST_F(ParserTest, EOFOther) {
TrapEOF(".a { margin: 5");
TrapEOF(".a { margin: 5.5");
TrapEOF(".a { color: rgb");
TrapEOF(".a { color: rgb(80, 80, 80");
TrapEOF(".a[");
TrapEOF("", EXTRACT_CHARSET);
TrapEOF("", PARSE_CHARSET);
TrapEOF("'foo'", PARSE_CHARSET);
}
// Check that SkipPastDelimiter() correctly respects matching delimiters.
TEST_F(ParserTest, SkipPastDelimiter) {
EXPECT_STREQ(" 6 7 8 9", SkipPast('5', "1 2 3 4 5 6 7 8 9"));
EXPECT_STREQ(" 1, bar", SkipPast(',', "foo(a, b), 1, bar"));
EXPECT_STREQ(" bar }", SkipPast('}', "foo: 'end brace: }'; } bar }"));
EXPECT_STREQ(" h1 { color: blue}\n",
SkipPast('}',
"@three-dee {\n"
" @background-lighting {n"
" azimuth: 30deg;\n"
" elevation: 190deg;\n"
" }\n"
" h1 { color: red}\n"
"}\n"
"} h1 { color: blue}\n"));
// Make sure we match malformed strings correctly.
EXPECT_STREQ("\nfoo4: bar4\n",
SkipPast('}',
"foo1: 'bar1}'\n"
"foo2: 'bar2}\n" // Notice missing closing '
"foo3: bar3}\n" // Actual closing }
"foo4: bar4\n"));
// Make sure we match delimiters correct. Correct matching is specified
// by the letters in the comment below. Two symbols with the same letter
// above them should be matched. '-' mark closing delimiters that do not
// match any opening ones. '*' marks the actual matching '}'.
// ABC--CB-D-E----EDA-*---
EXPECT_STREQ("\"}", SkipPast('}', "(([))])}{)'})\"'}))}\"}"));
FailureSkipPast('5', "abcdef");
// Make sure we fail when a string is closed by EOF.
FailureSkipPast('}', "'}");
// Pattern: ABC--CB-D-E----EDA--F----
FailureSkipPast('}', "(([))])}{)'})\"'}))\"}'[]");
}
// Make sure we don't allow SkipPastDelimiter to recurse arbitrarily deep
// and fill the stack with stack frames.
// See b/7733984
TEST_F(ParserTest, SkipPastDelimiterRecusiveDepth) {
string bad(1000000, '{');
FailureSkipPast('}', bad);
}
TEST_F(ParserTest, ParseMediaQueries) {
scoped_ptr<Parser> a(new Parser("screen"));
scoped_ptr<MediaQueries> q(a->ParseMediaQueries());
ASSERT_TRUE(NULL != q.get());
EXPECT_EQ(1, q->size());
EXPECT_EQ(MediaQuery::NO_QUALIFIER, (*q)[0]->qualifier());
EXPECT_EQ("screen", UnicodeTextToUTF8((*q)[0]->media_type()));
// qualifier
a.reset(new Parser("only screen"));
q.reset(a->ParseMediaQueries());
EXPECT_EQ(MediaQuery::ONLY, (*q)[0]->qualifier());
EXPECT_EQ("screen", UnicodeTextToUTF8((*q)[0]->media_type()));
// media expression
a.reset(new Parser("screen and (max-width: 640px)"));
q.reset(a->ParseMediaQueries());
EXPECT_EQ("screen", UnicodeTextToUTF8((*q)[0]->media_type()));
ASSERT_EQ(1, (*q)[0]->expressions().size());
EXPECT_EQ("max-width", UnicodeTextToUTF8((*q)[0]->expression(0).name()));
ASSERT_TRUE((*q)[0]->expression(0).has_value());
EXPECT_EQ("640px", UnicodeTextToUTF8((*q)[0]->expression(0).value()));
// tailing whitespaces of values are not trimmed.
a.reset(new Parser("screen and (max-width: 640 px )"));
q.reset(a->ParseMediaQueries());
EXPECT_EQ("screen", UnicodeTextToUTF8((*q)[0]->media_type()));
ASSERT_EQ(1, (*q)[0]->expressions().size());
EXPECT_EQ("max-width", UnicodeTextToUTF8((*q)[0]->expression(0).name()));
ASSERT_TRUE((*q)[0]->expression(0).has_value());
EXPECT_EQ("640 px ", UnicodeTextToUTF8((*q)[0]->expression(0).value()));
// multiple queries
a.reset(new Parser(
"not screen and (max-width: 500px), projection and (color)"));
q.reset(a->ParseMediaQueries());
EXPECT_EQ(2, q->size());
EXPECT_EQ(MediaQuery::NOT, (*q)[0]->qualifier());
EXPECT_EQ("screen", UnicodeTextToUTF8((*q)[0]->media_type()));
ASSERT_EQ(1, (*q)[0]->expressions().size());
EXPECT_EQ("max-width", UnicodeTextToUTF8((*q)[0]->expression(0).name()));
ASSERT_TRUE((*q)[0]->expression(0).has_value());
EXPECT_EQ("500px", UnicodeTextToUTF8((*q)[0]->expression(0).value()));
EXPECT_EQ("projection", UnicodeTextToUTF8((*q)[1]->media_type()));
ASSERT_EQ(1, (*q)[1]->expressions().size());
EXPECT_EQ("color", UnicodeTextToUTF8((*q)[1]->expression(0).name()));
ASSERT_FALSE((*q)[1]->expression(0).has_value());
// empty input. never return NULL.
a.reset(new Parser(""));
q.reset(a->ParseMediaQueries());
EXPECT_TRUE(NULL != q.get());
EXPECT_EQ(0, q->size());
// any media_type is allowed
a.reset(new Parser("foobar"));
q.reset(a->ParseMediaQueries());
EXPECT_EQ(1, q->size());
EXPECT_EQ("foobar", UnicodeTextToUTF8((*q)[0]->media_type()));
// Basic media query
a.reset(new Parser("screen and (max-width: 640px)"));
q.reset(a->ParseMediaQueries());
EXPECT_EQ("screen and (max-width: 640px)", q->ToString());
// Missing "and" invalidates media query.
a.reset(new Parser("screen (max-width: 640px)"));
q.reset(a->ParseMediaQueries());
EXPECT_EQ("not all", q->ToString());
}
TEST_F(ParserTest, ImportInMiddle) {
scoped_ptr<Parser> p(
new Parser(".a { color: red; }\n"
"@import url('foo.css');\n"
".b { color: blue; }\n"));
scoped_ptr<Stylesheet> s(p->ParseStylesheet());
EXPECT_EQ(0, s->imports().size());
EXPECT_EQ(2, s->rulesets().size());
EXPECT_TRUE(Parser::kImportError & p->errors_seen_mask());
EXPECT_EQ("/* AUTHOR */\n\n\n\n.a {color: #ff0000}\n.b {color: #0000ff}\n",
s->ToString());
p.reset(new Parser("@font-face { font-family: 'InFront'; }\n"
"@import url('foo.css');\n"
".b { color: blue; }\n"));
s.reset(p->ParseStylesheet());
EXPECT_EQ(0, s->imports().size());
EXPECT_EQ(1, s->font_faces().size());
EXPECT_EQ(1, s->rulesets().size());
EXPECT_TRUE(Parser::kImportError & p->errors_seen_mask());
EXPECT_EQ("/* AUTHOR */\n\n\n@font-face { font-family: \"InFront\" }\n"
".b {color: #0000ff}\n",
s->ToString());
}
TEST_F(ParserTest, ParseAnyParens) {
scoped_ptr<Parser> p(new Parser("(2 + 3) 9 7)"));
scoped_ptr<Value> value(p->ParseAny());
// ParseAny() should parse past exactly "(2 + 3)".
EXPECT_STREQ(" 9 7)", p->in_);
}
TEST_F(ParserTest, BadPartialImport) {
const char kBadPartialImport[] = "@import url(R\xd5\x9b";
Parser parser(kBadPartialImport);
delete parser.ParseStylesheet();
EXPECT_NE(Parser::kNoError, parser.errors_seen_mask());
}
TEST_F(ParserTest, BadPartialImportEncoding) {
const char kBadPartialImportEncoding[] = "@import url(R\xd5";
Parser parser(kBadPartialImportEncoding);
delete parser.ParseStylesheet();
EXPECT_NE(Parser::kNoError, parser.errors_seen_mask());
}
} // namespace Css