Files
Umbraco-CMS/src/Umbraco.Core/Models/Css/CssParser.cs
T

653 lines
22 KiB
C#

using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
namespace Umbraco.Core.Models.Css
{
internal class CssParser
{
#region Constants
// this defines comments for CSS
private static readonly ReadFilter[] ReadFilters = new ReadFilter[] { new ReadFilter("/*", "*/") };
private readonly object SyncLock = new object();
#endregion Constants
#region Fields
private readonly List<ParseException> errors = new List<ParseException>();
private LineReader reader;
private CssStyleSheet styleSheet;
private string fileContent;
private string source;
#endregion Fields
#region Init
/// <summary>
/// Ctor.
/// </summary>
/// <param name="fileContent">path to source</param>
public CssParser(string fileContent)
: this(fileContent, null)
{
}
/// <summary>
/// Ctor.
/// </summary>
/// <param name="fileContent">path to source</param>
/// <param name="source">actual source</param>
public CssParser(string fileContent, string source)
{
this.fileContent = fileContent;
this.source = source;
}
#endregion Init
#region Properties
public List<ParseException> Errors
{
get { return this.errors; }
}
public CssStyleSheet StyleSheet
{
get
{
if (this.styleSheet == null)
{
lock (this.SyncLock)
{
// check again in case race condition
// so we don't parse twice
if (this.styleSheet == null)
{
this.styleSheet = this.ParseStyleSheet();
}
}
}
return this.styleSheet;
}
}
private int Position
{
get { return this.reader.Position; }
}
#endregion Properties
#region Parse Methods
#region StyleSheet
/// <summary>
/// (BNF) stylesheet : [ CDO | CDC | S | statement ]*;
/// </summary>
/// <returns>CSS StyleSheet parse tree</returns>
private CssStyleSheet ParseStyleSheet()
{
CssStyleSheet styleSheet = new CssStyleSheet();
using (this.reader = new LineReader(this.fileContent, this.source, CssParser.ReadFilters))
{
this.reader.NormalizeWhiteSpace = true;
#if DEBUG
System.Diagnostics.Stopwatch watch = System.Diagnostics.Stopwatch.StartNew();
#endif
char ch;
while (this.Read(out ch))
{
switch (ch)
{
case '\uFEFF': // BOM (UTF byte order mark)
case '\t': //TAB
case '\n': //LF
case '\r': //CR
case ' ': //Space
{
// skip whitespace
continue;
}
case '<':
{
// CDO (Char Data Open?)
if (!this.Read(out ch) || ch != '-' ||
!this.Read(out ch) || ch != '-')
{
throw new SyntaxError("Expected \"<!--\"", this.reader.FilePath, this.reader.Line, this.reader.Column);
}
continue;
}
case '-':
{
// CDC (Char Data Close?)
if (!this.Read(out ch) || ch != '-' ||
!this.Read(out ch) || ch != '>')
{
throw new SyntaxError("Expected \"-->\"", this.reader.FilePath, this.reader.Line, this.reader.Column);
}
continue;
}
default:
{
try
{
CssStatement statement = this.ParseStatement();
styleSheet.Statements.Add(statement);
}
catch (ParseException ex)
{
this.errors.Add(ex);
while (this.Read(out ch) && ch != '}')
{
// restabilize on next statement
}
}
continue;
}
}
}
#if DEBUG
watch.Stop();
Console.WriteLine("CSS parse duration: {0} ms for {1} chars", watch.ElapsedMilliseconds, this.reader.Length);
#endif
}
this.reader = null;
this.source = null;
return styleSheet;
}
#endregion StyleSheet
#region Statement
/// <summary>
/// (BNF) statement : ruleset | at-rule;
/// </summary>
/// <returns></returns>
private CssStatement ParseStatement()
{
if (this.reader.Current == '@')
{
return this.ParseAtRule();
}
else
{
this.PutBack();
return this.ParseRuleSet();
}
}
#endregion Statement
#region At-Rule
/// <summary>
/// (BNF) at-rule : ATKEYWORD S* any* [ block | ';' S* ];
/// </summary>
/// <returns></returns>
/// <remarks>
/// NOTE: each at-rule might parse differently according to CSS3
/// The @media block for example contains a block of statements
/// while other at-rules with a block contain a block of declarations
/// </remarks>
private CssAtRule ParseAtRule()
{
CssAtRule atRule = new CssAtRule();
int start = this.Position + 1;// start with first char of ident
char ch;
while (this.Read(out ch) && !Char.IsWhiteSpace(ch))
{
// continue consuming
}
atRule.Ident = this.Copy(start);
while (this.Read(out ch) && Char.IsWhiteSpace(ch))
{
// consuming whitespace
}
start = this.Position;// start with current char
do
{
switch (ch)
{
case '{': //Block Begin
{
atRule.Value = this.Copy(start);
bool containsRuleSets = String.Equals(atRule.Ident, CssAtRule.MediaIdent, StringComparison.Ordinal);
while (true)
{
while (this.Read(out ch) && Char.IsWhiteSpace(ch))
{
// consume whitespace
}
if (ch == '}')
{
break;
}
try
{
if (containsRuleSets)
{
// includes @media
CssStatement statement = this.ParseStatement();
atRule.Block.Values.Add(statement);
}
else
{
// includes @font-face, @page
this.PutBack();
CssDeclaration declaration = this.ParseDeclaration();
atRule.Block.Values.Add(declaration);
}
}
catch (ParseException ex)
{
this.errors.Add(ex);
while (this.Read(out ch) && ch != '}')
{
// restabilize on block end
}
break;
}
}
return atRule;
}
case ';': //At-Rule End
{
atRule.Value = this.Copy(start);
return atRule;
}
}
} while (this.Read(out ch));
throw new UnexpectedEndOfFile("Unclosed At-Rule", this.reader.FilePath, this.reader.Line, this.reader.Column);
}
#endregion At-Rule
#region RuleSet
/// <summary>
/// (BNF) ruleset : selector? '{' S* declaration? [ ';' S* declaration? ]* '}' S*;
/// </summary>
/// <returns></returns>
private CssRuleSet ParseRuleSet()
{
char ch;
CssRuleSet ruleSet = new CssRuleSet();
ParseSelectors:
while (true)
{
try
{
CssSelector selector = this.ParseSelector();
if (selector == null)
{
break;
}
ruleSet.Selectors.Add(selector);
}
catch (ParseException ex)
{
this.errors.Add(ex);
while (this.Read(out ch))
{
// restabalize on next rulset
switch (ch)
{
case ',':
{
// continue parsing rest of Selectors
goto ParseSelectors;
}
case '{':
{
goto ParseDeclarations;
}
//case ':':// keep going
case ';':
case '}':
{
throw new SyntaxError("Invalid selector list", this.reader.FilePath, this.reader.Line, this.reader.Column);
}
}
}
}
}
ParseDeclarations:
while (true)
{
try
{
CssDeclaration declaration = this.ParseDeclaration();
if (declaration == null)
{
break;
}
ruleSet.Declarations.Add(declaration);
}
catch (ParseException ex)
{
this.errors.Add(ex);
while (this.Read(out ch))
{
// restabalize on next declaration
switch (ch)
{
case '{':
{
throw new SyntaxError("Invalid ruleset", this.reader.FilePath, this.reader.Line, this.reader.Column);
}
//case ':':// keep going
case ';':
{
// continue parsing rest of delcarations
goto ParseDeclarations;
}
case '}':
{
// no more declarations
return ruleSet;
}
}
}
}
}
return ruleSet;
}
#endregion RuleSet
#region Selector
/// <summary>
/// (BNF) selector: any+;
/// </summary>
/// <returns></returns>
private CssSelector ParseSelector()
{
CssSelector selector = new CssSelector();
char ch;
while (this.Read(out ch) && (Char.IsWhiteSpace(ch) || ch == ','))
{
// skip whitespace, and empty selectors
}
// consume property name
switch (ch)
{
case '{':
{
// no more declarations
return null;
}
//case ':':// pseudoclass
case ';':
case '}':
{
throw new SyntaxError("Invalid chars in selector", this.reader.FilePath, this.reader.Line, this.reader.Column);
}
}
int start = this.Position;// start with current char
while (this.Read(out ch))
{
// continue consuming selector
switch (ch)
{
case ',':
case '{':
{
selector.Value = this.Copy(start);
if (ch == '{')
{
this.PutBack();
}
return selector;
}
//case ':':// pseudoclass
case ';':
case '}':
{
throw new SyntaxError("Invalid selector", this.reader.FilePath, this.reader.Line, this.reader.Column);
}
}
}
throw new UnexpectedEndOfFile("Unclosed ruleset", this.reader.FilePath, this.reader.Line, this.reader.Column);
}
#endregion Selector
#region Declaration
/// <summary>
/// (BNF) declaration : property ':' S* value;
/// (BNF) property : IDENT S*;
/// </summary>
/// <returns></returns>
private CssDeclaration ParseDeclaration()
{
CssDeclaration declaration = new CssDeclaration();
char ch;
while (this.Read(out ch) && (Char.IsWhiteSpace(ch) || ch == ';'))
{
// skip whitespace, and empty declarations
}
// consume property name
switch (ch)
{
case '{':
case ':':
//case ';':
{
throw new SyntaxError("Declaration missing property name", this.reader.FilePath, this.reader.Line, this.reader.Column);
}
case '}':
{
// no more declarations
return null;
}
}
// read property, starting with current char
int start = this.Position;
while (this.Read(out ch) && !Char.IsWhiteSpace(ch) && ch != ':')
{
// consume property name
switch (ch)
{
case '{':
//case ':':
case ';':
{
throw new SyntaxError("Invalid CSS property name: " + this.Copy(start), this.reader.FilePath, this.reader.Line, this.reader.Column);
}
case '}':
{
this.PutBack();
goto case ';';
}
}
}
declaration.Property = this.Copy(start);
if (Char.IsWhiteSpace(ch))
{
while (this.Read(out ch) && (Char.IsWhiteSpace(ch)))
{
// skip whitespace
}
}
if (ch != ':')
{
// missing the property delim and value
if (ch == ';' || ch == '}')
{
// these are good chars for resyncing
// so put them back on the stream to
// not create subsequent errors
this.PutBack();
}
throw new SyntaxError("Expected <property> : <value>", this.reader.FilePath, this.reader.Line, this.reader.Column);
}
CssValueList value = this.ParseValue();
declaration.Value = value;
return declaration;
}
#endregion Declaration
#region Value
/// <summary>
/// (BNF) value : [ any | block | ATKEYWORD S* ]+;
/// (BNF) any : [ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING
/// | DELIM | URI | HASH | UNICODE-RANGE | INCLUDES
/// | FUNCTION S* any* ')' | DASHMATCH | '(' S* any* ')'
/// | '[' S* any* ']' ] S*;
/// </summary>
/// <returns></returns>
private CssValueList ParseValue()
{
CssValueList value = new CssValueList();
char ch;
while (this.Read(out ch) && Char.IsWhiteSpace(ch))
{
// skip whitespace, and empty declarations
}
switch (ch)
{
case '{':
case ':':
case ';':
case '}':
{
throw new SyntaxError("Invalid char in CSS property value: '" + ch + "'", this.reader.FilePath, this.reader.Line, this.reader.Column);
}
}
// read value, starting with current char
int start = this.Position;
while (this.Read(out ch))
{
// consume declaration value
switch (ch)
{
case '{':
//case ':':// leave in for "filter: progid:DXImageTransform.Microsoft..."
{
throw new SyntaxError("Invalid CSS property value: " + this.Copy(start), this.reader.FilePath, this.reader.Line, this.reader.Column);
}
case '}':
case ';':
{
//Should this parse the value further?
CssString any = new CssString();
any.Value = this.Copy(start);
value.Values.Add(any);
if (ch == '}')
{
this.PutBack();
}
return value;
}
}
}
throw new UnexpectedEndOfFile("Unclosed declaration", this.reader.FilePath, this.reader.Line, this.reader.Column);
}
#endregion Value
#endregion Parse Methods
#region Methods
public void Write(TextWriter writer, CssCompactor.Options options)
{
this.StyleSheet.Write(writer, options);
}
#endregion Methods
#region Reader Methods
/// <summary>
///
/// </summary>
/// <param name="ch"></param>
/// <returns>Success</returns>
private bool Read(out char ch)
{
if (this.reader.EndOfFile)
{
throw new UnexpectedEndOfFile("Unexpected end of file", this.reader.FilePath, this.reader.Line, this.reader.Column);
}
int c = this.reader.Read();
if (c < 0)
{
ch = '\0';
return false;
}
ch = (char)c;
return true;
}
/// <summary>
/// Copies chars from start until the position before the current position
/// </summary>
/// <returns></returns>
private string Copy(int start)
{
// read block
return this.reader.Copy(start, this.reader.Position - 1);
}
/// <summary>
/// Put one character back
/// </summary>
private void PutBack()
{
this.reader.PutBack();
}
#endregion Reader Methods
}
}