CssParser
The CSS parser.
See http://dev.w3.org/csswg/css-syntax/#parsing for more details.
using AngleSharp.DOM;
using AngleSharp.DOM.Collections;
using AngleSharp.DOM.Css;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
namespace AngleSharp.Css
{
public class CssParser : IParser
{
private bool started;
private bool quirksFlag;
private CssTokenizer tokenizer;
private CSSStyleSheet sheet;
private TaskCompletionSource<bool> tcs;
private StringBuilder buffer;
private Stack<CSSRule> open;
private bool ignore;
public bool IsAsync => tcs != null;
public CSSStyleSheet Result {
get {
Parse();
return sheet;
}
}
public bool IsQuirksMode {
get {
return quirksFlag;
}
set {
quirksFlag = value;
}
}
internal CSSRule CurrentRule {
get {
if (open.Count <= 0)
return null;
return open.Peek();
}
}
public event EventHandler<ParseErrorEventArgs> ErrorOccurred;
public CssParser(string source)
: this(new CSSStyleSheet(), new SourceManager(source))
{
}
public CssParser(Stream stream)
: this(new CSSStyleSheet(), new SourceManager(stream))
{
}
public CssParser(CSSStyleSheet stylesheet, string source)
: this(stylesheet, new SourceManager(source))
{
}
public CssParser(CSSStyleSheet stylesheet, Stream stream)
: this(stylesheet, new SourceManager(stream))
{
}
internal CssParser(CSSStyleSheet stylesheet, SourceManager source)
{
ignore = true;
buffer = new StringBuilder();
tokenizer = new CssTokenizer(source);
CssTokenizer cssTokenizer = tokenizer;
EventHandler<ParseErrorEventArgs> value = delegate(object s, ParseErrorEventArgs ev) {
if (this.ErrorOccurred != null)
this.ErrorOccurred(this, ev);
};
cssTokenizer.ErrorOccurred += value;
started = false;
sheet = stylesheet;
open = new Stack<CSSRule>();
}
public Task ParseAsync()
{
if (!started) {
started = true;
tcs = new TaskCompletionSource<bool>();
return tcs.Task;
}
if (tcs == null) {
TaskCompletionSource<bool> taskCompletionSource = new TaskCompletionSource<bool>();
taskCompletionSource.SetResult(true);
return taskCompletionSource.Task;
}
return tcs.Task;
}
public void Parse()
{
if (!started) {
started = true;
AppendRules(tokenizer.Iterator, sheet.CssRules.List);
}
}
private void AppendRules(IEnumerator<CssToken> source, List<CSSRule> rules)
{
while (source.MoveNext()) {
switch (source.Current.Type) {
case CssTokenType.AtKeyword:
rules.Add(CreateAtRule(source));
break;
default:
rules.Add(CreateStyleRule(source));
break;
case CssTokenType.Cdo:
case CssTokenType.Cdc:
case CssTokenType.Whitespace:
break;
}
}
}
private void AppendDeclarations(IEnumerator<CssToken> source, List<CSSProperty> declarations)
{
while (source.MoveNext()) {
switch (source.Current.Type) {
case CssTokenType.Ident: {
IEnumerable<CssToken> enumerable = LimitToSemicolon(source);
IEnumerator<CssToken> enumerator = enumerable.GetEnumerator();
enumerator.MoveNext();
CSSProperty cSSProperty = CreateDeclaration(enumerator);
if (cSSProperty != null)
declarations.Add(cSSProperty);
break;
}
default:
RaiseErrorOccurred(ErrorCode.InvalidCharacter);
SkipToNextSemicolon(source);
break;
case CssTokenType.Semicolon:
case CssTokenType.Whitespace:
break;
}
}
}
private void AppendMediaList(IEnumerator<CssToken> source, MediaList media, CssTokenType endToken = CssTokenType.Semicolon)
{
do {
if (source.Current.Type != CssTokenType.Whitespace) {
if (source.Current.Type == endToken)
break;
while (source.Current.Type != CssTokenType.Comma && source.Current.Type != endToken) {
if (source.Current.Type == CssTokenType.Whitespace)
buffer.Append(' ');
else
buffer.Append(source.Current.ToValue());
if (!source.MoveNext())
break;
}
media.AppendMedium(buffer.ToString());
buffer.Clear();
if (source.Current.Type == endToken)
break;
}
} while (source.MoveNext());
}
private List<CSSValueList> CreateMultipleValues(IEnumerator<CssToken> source)
{
List<CSSValueList> list = new List<CSSValueList>();
do {
CSSValueList cSSValueList = CreateValueList(source);
if (cSSValueList.Length > 0)
list.Add(cSSValueList);
} while (source.Current != null && source.Current.Type == CssTokenType.Comma);
return list;
}
private CSSValueList CreateValueList(IEnumerator<CssToken> source)
{
List<CSSValue> list = new List<CSSValue>();
while (SkipToNextNonWhitespace(source)) {
if (source.Current.Type == CssTokenType.Semicolon)
break;
if (source.Current.Type == CssTokenType.Comma)
break;
CSSValue cSSValue = CreateValue(source);
if (cSSValue == null) {
SkipToNextSemicolon(source);
break;
}
list.Add(cSSValue);
}
return new CSSValueList(list);
}
private CSSValue CreateValue(IEnumerator<CssToken> source)
{
CSSValue result = null;
HtmlColor htmlColor;
switch (source.Current.Type) {
case CssTokenType.String:
result = new CSSPrimitiveValue(UnitType.String, ((CssStringToken)source.Current).Data);
break;
case CssTokenType.Url:
result = new CSSPrimitiveValue(UnitType.Uri, ((CssStringToken)source.Current).Data);
break;
case CssTokenType.Ident:
result = new CSSPrimitiveValue(UnitType.Ident, ((CssKeywordToken)source.Current).Data);
break;
case CssTokenType.Percentage:
result = new CSSPrimitiveValue(UnitType.Percentage, ((CssUnitToken)source.Current).Data);
break;
case CssTokenType.Dimension:
result = new CSSPrimitiveValue(((CssUnitToken)source.Current).Unit, ((CssUnitToken)source.Current).Data);
break;
case CssTokenType.Number:
result = new CSSPrimitiveValue(UnitType.Number, ((CssNumberToken)source.Current).Data);
break;
case CssTokenType.Hash:
if (HtmlColor.TryFromHex(((CssKeywordToken)source.Current).Data, out htmlColor))
result = new CSSPrimitiveValue(htmlColor);
break;
case CssTokenType.Delim:
if (((CssDelimToken)source.Current).Data == '#') {
string text = string.Empty;
while (source.MoveNext()) {
bool flag = false;
switch (source.Current.Type) {
case CssTokenType.Ident:
case CssTokenType.Number:
case CssTokenType.Dimension: {
string text2 = source.Current.ToValue();
if (text.Length + text2.Length <= 6)
text += text2;
else
flag = true;
break;
}
default:
flag = true;
break;
}
if (flag || text.Length == 6)
break;
}
if (HtmlColor.TryFromHex(text, out htmlColor))
result = new CSSPrimitiveValue(htmlColor);
}
break;
case CssTokenType.Function:
result = CreateFunction(source);
break;
}
return result;
}
private CSSFunction CreateFunction(IEnumerator<CssToken> source)
{
string data = ((CssKeywordToken)source.Current).Data;
CSSValueList arguments = new CSSValueList();
while (source.MoveNext() && source.Current.Type != CssTokenType.RoundBracketClose) {
}
return CSSFunction.Create(data, arguments);
}
private CSSStyleRule CreateStyleRule(IEnumerator<CssToken> source)
{
CSSStyleRule cSSStyleRule = new CSSStyleRule();
CssSelectorConstructor cssSelectorConstructor = new CssSelectorConstructor();
cssSelectorConstructor.IgnoreErrors = ignore;
cSSStyleRule.ParentStyleSheet = sheet;
cSSStyleRule.ParentRule = CurrentRule;
open.Push(cSSStyleRule);
do {
if (source.Current.Type == CssTokenType.CurlyBracketOpen) {
if (SkipToNextNonWhitespace(source)) {
IEnumerable<CssToken> enumerable = LimitToCurrentBlock(source);
AppendDeclarations(enumerable.GetEnumerator(), cSSStyleRule.Style.List);
}
break;
}
cssSelectorConstructor.PickSelector(source);
} while (source.MoveNext());
cSSStyleRule.Selector = cssSelectorConstructor.Result;
open.Pop();
return cSSStyleRule;
}
private CSSRule CreateAtRule(IEnumerator<CssToken> source)
{
string data = ((CssKeywordToken)source.Current).Data;
SkipToNextNonWhitespace(source);
switch (data) {
case "media":
return CreateMediaRule(source);
case "page":
return CreatePageRule(source);
case "import":
return CreateImportRule(source);
case "font-face":
return CreateFontFaceRule(source);
case "charset":
return CreateCharsetRule(source);
case "namespace":
return CreateNamespaceRule(source);
case "supports":
return CreateSupportsRule(source);
case "keyframes":
return CreateKeyframesRule(source);
default:
return CreateUnknownRule(data, source);
}
}
private CSSProperty CreateDeclaration(IEnumerator<CssToken> source)
{
string data = ((CssKeywordToken)source.Current).Data;
CSSProperty cSSProperty = null;
CSSValue value = CSSValue.Inherit;
bool flag = SkipToNextNonWhitespace(source) && source.Current.Type == CssTokenType.Colon;
if (flag)
value = CreateValueList(source);
switch (data) {
default:
cSSProperty = new CSSProperty(data);
cSSProperty.Value = value;
if (flag && source.Current.Type == CssTokenType.Delim && ((CssDelimToken)source.Current).Data == '!' && SkipToNextNonWhitespace(source))
cSSProperty.Important = (source.Current.Type == CssTokenType.Ident && ((CssKeywordToken)source.Current).Data.Equals("important", StringComparison.OrdinalIgnoreCase));
SkipBehindNextSemicolon(source);
return cSSProperty;
}
}
private CSSUnknownRule CreateUnknownRule(string name, IEnumerator<CssToken> source)
{
CSSUnknownRule cSSUnknownRule = new CSSUnknownRule();
int num = 0;
cSSUnknownRule.ParentStyleSheet = sheet;
cSSUnknownRule.ParentRule = CurrentRule;
open.Push(cSSUnknownRule);
buffer.Append(name).Append(" ");
do {
if (source.Current.Type == CssTokenType.Semicolon && num == 0) {
source.MoveNext();
break;
}
buffer.Append(source.Current.ToString());
if (source.Current.Type == CssTokenType.CurlyBracketOpen)
num++;
else if (source.Current.Type == CssTokenType.CurlyBracketClose && --num == 0) {
break;
}
} while (source.MoveNext());
cSSUnknownRule.SetText(buffer.ToString());
buffer.Clear();
open.Pop();
return cSSUnknownRule;
}
private CSSKeyframesRule CreateKeyframesRule(IEnumerator<CssToken> source)
{
CSSKeyframesRule cSSKeyframesRule = new CSSKeyframesRule();
cSSKeyframesRule.ParentStyleSheet = sheet;
cSSKeyframesRule.ParentRule = CurrentRule;
open.Push(cSSKeyframesRule);
if (source.Current.Type == CssTokenType.Ident) {
cSSKeyframesRule.Name = ((CssKeywordToken)source.Current).Data;
SkipToNextNonWhitespace(source);
if (source.Current.Type == CssTokenType.CurlyBracketOpen) {
SkipToNextNonWhitespace(source);
IEnumerator<CssToken> enumerator = LimitToCurrentBlock(source).GetEnumerator();
while (SkipToNextNonWhitespace(enumerator)) {
cSSKeyframesRule.CssRules.List.Add(CreateKeyframeRule(enumerator));
}
}
}
open.Pop();
return cSSKeyframesRule;
}
private CSSKeyframeRule CreateKeyframeRule(IEnumerator<CssToken> source)
{
CSSKeyframeRule cSSKeyframeRule = new CSSKeyframeRule();
cSSKeyframeRule.ParentStyleSheet = sheet;
cSSKeyframeRule.ParentRule = CurrentRule;
open.Push(cSSKeyframeRule);
do {
if (source.Current.Type == CssTokenType.CurlyBracketOpen) {
if (SkipToNextNonWhitespace(source)) {
IEnumerable<CssToken> enumerable = LimitToCurrentBlock(source);
AppendDeclarations(enumerable.GetEnumerator(), cSSKeyframeRule.Style.List);
}
break;
}
buffer.Append(source.Current.ToString());
} while (source.MoveNext());
cSSKeyframeRule.KeyText = buffer.ToString();
buffer.Clear();
open.Pop();
return cSSKeyframeRule;
}
private CSSSupportsRule CreateSupportsRule(IEnumerator<CssToken> source)
{
CSSSupportsRule cSSSupportsRule = new CSSSupportsRule();
cSSSupportsRule.ParentStyleSheet = sheet;
cSSSupportsRule.ParentRule = CurrentRule;
open.Push(cSSSupportsRule);
do {
if (source.Current.Type == CssTokenType.CurlyBracketOpen) {
if (SkipToNextNonWhitespace(source)) {
IEnumerable<CssToken> enumerable = LimitToCurrentBlock(source);
AppendRules(enumerable.GetEnumerator(), cSSSupportsRule.CssRules.List);
}
break;
}
buffer.Append(source.Current.ToString());
} while (source.MoveNext());
cSSSupportsRule.ConditionText = buffer.ToString();
buffer.Clear();
open.Pop();
return cSSSupportsRule;
}
private CSSNamespaceRule CreateNamespaceRule(IEnumerator<CssToken> source)
{
CSSNamespaceRule cSSNamespaceRule = new CSSNamespaceRule();
cSSNamespaceRule.ParentStyleSheet = sheet;
if (source.Current.Type == CssTokenType.Ident) {
cSSNamespaceRule.Prefix = source.Current.ToValue();
SkipToNextNonWhitespace(source);
if (source.Current.Type == CssTokenType.String)
cSSNamespaceRule.NamespaceURI = source.Current.ToValue();
}
SkipToNextSemicolon(source);
return cSSNamespaceRule;
}
private CSSCharsetRule CreateCharsetRule(IEnumerator<CssToken> source)
{
CSSCharsetRule cSSCharsetRule = new CSSCharsetRule();
cSSCharsetRule.ParentStyleSheet = sheet;
if (source.Current.Type == CssTokenType.String)
cSSCharsetRule.Encoding = ((CssStringToken)source.Current).Data;
SkipToNextSemicolon(source);
return cSSCharsetRule;
}
private CSSFontFaceRule CreateFontFaceRule(IEnumerator<CssToken> source)
{
CSSFontFaceRule cSSFontFaceRule = new CSSFontFaceRule();
cSSFontFaceRule.ParentStyleSheet = sheet;
cSSFontFaceRule.ParentRule = CurrentRule;
open.Push(cSSFontFaceRule);
if (source.Current.Type == CssTokenType.CurlyBracketOpen && SkipToNextNonWhitespace(source)) {
IEnumerable<CssToken> enumerable = LimitToCurrentBlock(source);
AppendDeclarations(enumerable.GetEnumerator(), cSSFontFaceRule.CssRules.List);
}
open.Pop();
return cSSFontFaceRule;
}
private CSSImportRule CreateImportRule(IEnumerator<CssToken> source)
{
CSSImportRule cSSImportRule = new CSSImportRule();
cSSImportRule.ParentStyleSheet = sheet;
cSSImportRule.ParentRule = CurrentRule;
open.Push(cSSImportRule);
switch (source.Current.Type) {
case CssTokenType.Semicolon:
source.MoveNext();
break;
case CssTokenType.String:
case CssTokenType.Url:
cSSImportRule.Href = ((CssStringToken)source.Current).Data;
AppendMediaList(source, cSSImportRule.Media, CssTokenType.Semicolon);
break;
default:
SkipToNextSemicolon(source);
break;
}
open.Pop();
return cSSImportRule;
}
private CSSPageRule CreatePageRule(IEnumerator<CssToken> source)
{
CSSPageRule cSSPageRule = new CSSPageRule();
cSSPageRule.ParentStyleSheet = sheet;
cSSPageRule.ParentRule = CurrentRule;
open.Push(cSSPageRule);
CssSelectorConstructor cssSelectorConstructor = new CssSelectorConstructor();
cssSelectorConstructor.IgnoreErrors = ignore;
do {
if (source.Current.Type == CssTokenType.CurlyBracketOpen && SkipToNextNonWhitespace(source)) {
IEnumerable<CssToken> enumerable = LimitToCurrentBlock(source);
AppendDeclarations(enumerable.GetEnumerator(), cSSPageRule.Style.List);
break;
}
cssSelectorConstructor.PickSelector(source);
} while (source.MoveNext());
cSSPageRule.Selector = cssSelectorConstructor.Result;
open.Pop();
return cSSPageRule;
}
private CSSMediaRule CreateMediaRule(IEnumerator<CssToken> source)
{
CSSMediaRule cSSMediaRule = new CSSMediaRule();
cSSMediaRule.ParentStyleSheet = sheet;
cSSMediaRule.ParentRule = CurrentRule;
open.Push(cSSMediaRule);
AppendMediaList(source, cSSMediaRule.Media, CssTokenType.CurlyBracketOpen);
if (source.Current.Type == CssTokenType.CurlyBracketOpen && SkipToNextNonWhitespace(source)) {
IEnumerable<CssToken> enumerable = LimitToCurrentBlock(source);
AppendRules(enumerable.GetEnumerator(), cSSMediaRule.CssRules.List);
}
open.Pop();
return cSSMediaRule;
}
private static bool SkipToNextNonWhitespace(IEnumerator<CssToken> source)
{
while (source.MoveNext()) {
if (source.Current.Type != CssTokenType.Whitespace)
return true;
}
return false;
}
private static bool SkipToNextSemicolon(IEnumerator<CssToken> source)
{
do {
if (source.Current.Type == CssTokenType.Semicolon)
return true;
} while (source.MoveNext());
return false;
}
private static bool SkipBehindNextSemicolon(IEnumerator<CssToken> source)
{
do {
if (source.Current.Type == CssTokenType.Semicolon) {
source.MoveNext();
return true;
}
} while (source.MoveNext());
return false;
}
private static IEnumerable<CssToken> LimitToSemicolon(IEnumerator<CssToken> source)
{
while (source.Current.Type != CssTokenType.Semicolon) {
yield return source.Current;
if (!source.MoveNext())
break;
}
}
private static IEnumerable<CssToken> LimitToCurrentBlock(IEnumerator<CssToken> source)
{
int open = 1;
do {
if (source.Current.Type == CssTokenType.CurlyBracketOpen)
open++;
else if (source.Current.Type == CssTokenType.CurlyBracketClose) {
int num;
open = (num = open - 1);
if (num == 0)
break;
}
yield return source.Current;
} while (source.MoveNext());
}
public static Selector ParseSelector(string selector, bool quirksMode = false)
{
CssParser cssParser = new CssParser(selector);
cssParser.IsQuirksMode = quirksMode;
IEnumerator<CssToken> iterator = cssParser.tokenizer.Iterator;
CssSelectorConstructor cssSelectorConstructor = new CssSelectorConstructor();
while (iterator.MoveNext()) {
cssSelectorConstructor.PickSelector(iterator);
}
return cssSelectorConstructor.Result;
}
public static CSSStyleSheet ParseStyleSheet(string stylesheet, bool quirksMode = false)
{
CssParser cssParser = new CssParser(stylesheet);
cssParser.IsQuirksMode = quirksMode;
return cssParser.Result;
}
public static CSSRule ParseRule(string rule, bool quirksMode = false)
{
CssParser cssParser = new CssParser(rule);
cssParser.ignore = false;
cssParser.IsQuirksMode = quirksMode;
IEnumerator<CssToken> iterator = cssParser.tokenizer.Iterator;
if (SkipToNextNonWhitespace(iterator)) {
if (iterator.Current.Type == CssTokenType.Cdo || iterator.Current.Type == CssTokenType.Cdc)
throw new DOMException(ErrorCode.SyntaxError);
if (iterator.Current.Type != CssTokenType.AtKeyword)
return cssParser.CreateStyleRule(iterator);
return cssParser.CreateAtRule(iterator);
}
return new CSSUnknownRule();
}
public static CSSStyleDeclaration ParseDeclarations(string declarations, bool quirksMode = false)
{
CssParser cssParser = new CssParser(declarations);
cssParser.IsQuirksMode = quirksMode;
cssParser.ignore = false;
IEnumerator<CssToken> iterator = cssParser.tokenizer.Iterator;
CSSStyleDeclaration cSSStyleDeclaration = new CSSStyleDeclaration();
cssParser.AppendDeclarations(iterator, cSSStyleDeclaration.List);
return cSSStyleDeclaration;
}
public static CSSValue ParseValue(string source, bool quirksMode = false)
{
CssParser cssParser = new CssParser(source);
cssParser.IsQuirksMode = quirksMode;
cssParser.ignore = false;
IEnumerator<CssToken> iterator = cssParser.tokenizer.Iterator;
SkipToNextNonWhitespace(iterator);
return cssParser.CreateValue(iterator);
}
internal static CSSValueList ParseValueList(string source, bool quirksMode = false)
{
CssParser cssParser = new CssParser(source);
cssParser.IsQuirksMode = quirksMode;
cssParser.ignore = false;
IEnumerator<CssToken> iterator = cssParser.tokenizer.Iterator;
return cssParser.CreateValueList(iterator);
}
internal static List<CSSValueList> ParseMultipleValues(string source, bool quirksMode = false)
{
CssParser cssParser = new CssParser(source);
cssParser.IsQuirksMode = quirksMode;
cssParser.ignore = false;
IEnumerator<CssToken> iterator = cssParser.tokenizer.Iterator;
return cssParser.CreateMultipleValues(iterator);
}
internal static CSSKeyframeRule ParseKeyframeRule(string rule, bool quirksMode = false)
{
CssParser cssParser = new CssParser(rule);
cssParser.IsQuirksMode = quirksMode;
cssParser.ignore = false;
IEnumerator<CssToken> iterator = cssParser.tokenizer.Iterator;
if (SkipToNextNonWhitespace(iterator)) {
if (iterator.Current.Type == CssTokenType.Cdo || iterator.Current.Type == CssTokenType.Cdc)
throw new DOMException(ErrorCode.SyntaxError);
return cssParser.CreateKeyframeRule(iterator);
}
return null;
}
private void RaiseErrorOccurred(ErrorCode code)
{
if (this.ErrorOccurred != null) {
ParseErrorEventArgs parseErrorEventArgs = new ParseErrorEventArgs((int)code, Errors.GetError(code));
parseErrorEventArgs.Line = tokenizer.Stream.Line;
parseErrorEventArgs.Column = tokenizer.Stream.Column;
this.ErrorOccurred(this, parseErrorEventArgs);
}
}
}
}