3ef0e302c3
Completes U4-934
653 lines
22 KiB
C#
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
|
|
}
|
|
} |