diff --git a/src/Umbraco.Core/Models/Stylesheet.cs b/src/Umbraco.Core/Models/Stylesheet.cs index b9f4c67cd4..7381da0930 100644 --- a/src/Umbraco.Core/Models/Stylesheet.cs +++ b/src/Umbraco.Core/Models/Stylesheet.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Data; using System.Linq; using System.Runtime.Serialization; using System.Text; @@ -15,126 +16,147 @@ namespace Umbraco.Core.Models [DataContract(IsReference = true)] public class Stylesheet : File { - public Stylesheet(string path) : base(path) + public Stylesheet(string path) + : base(path) + { + InitializeProperties(); + } + + private Lazy> _properties; + + private void InitializeProperties() { - base.Path = path; + //if the value is already created, we need to be created and update the collection according to + //what is now in the content + if (_properties != null && _properties.IsValueCreated) + { + //re-parse it so we can check what properties are different and adjust the event handlers + var parsed = StylesheetHelper.ParseRules(Content).ToArray(); + var names = parsed.Select(x => x.Name).ToArray(); + var existing = _properties.Value.Where(x => names.Contains(x.Name)).ToArray(); + //update existing + foreach (var stylesheetProperty in existing) + { + var updateFrom = parsed.Single(x => x.Name == stylesheetProperty.Name); + //remove current event handler while we update, we'll reset it after + stylesheetProperty.PropertyChanged -= Property_PropertyChanged; + stylesheetProperty.Alias = updateFrom.Selector; + stylesheetProperty.Value = updateFrom.Styles; + //re-add + stylesheetProperty.PropertyChanged += Property_PropertyChanged; + } + //remove no longer existing + var nonExisting = _properties.Value.Where(x => names.Contains(x.Name) == false).ToArray(); + foreach (var stylesheetProperty in nonExisting) + { + stylesheetProperty.PropertyChanged -= Property_PropertyChanged; + _properties.Value.Remove(stylesheetProperty); + } + //add new ones + var newItems = parsed.Where(x => _properties.Value.Select(p => p.Name).Contains(x.Name) == false); + foreach (var stylesheetRule in newItems) + { + var prop = new StylesheetProperty(stylesheetRule.Name, stylesheetRule.Selector, stylesheetRule.Styles); + prop.PropertyChanged += Property_PropertyChanged; + _properties.Value.Add(prop); + } + } + + //we haven't read the properties yet so create the lazy delegate + _properties = new Lazy>(() => + { + var parsed = StylesheetHelper.ParseRules(Content); + return parsed.Select(statement => + { + var property = new StylesheetProperty(statement.Name, statement.Selector, statement.Styles); + property.PropertyChanged += Property_PropertyChanged; + return property; + + }).ToList(); + }); } /// - /// Returns a flat list of objects + /// If the property has changed then we need to update the content + /// + /// + /// + void Property_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + var prop = (StylesheetProperty) sender; + + //Ensure we are setting base.Content here so that the properties don't get reset and thus any event handlers would get reset too + base.Content = StylesheetHelper.ReplaceRule(Content, prop.Name, new StylesheetRule + { + Name = prop.Name, + Selector = prop.Alias, + Styles = prop.Value + }); + } + + /// + /// Gets or sets the Content of a File + /// + public override string Content + { + get { return base.Content; } + set + { + base.Content = value; + //re-set the properties so they are re-read from the content + InitializeProperties(); + } + } + + /// + /// Returns a list of umbraco back office enabled stylesheet properties /// /// - /// Please note that the list is flattend by formatting single css selectors with - /// its value(s). Blocks in css @ rules are also flatten, but noted as part of an @ rule - /// by setting the property IsPartOfAtRule=true. - /// This is done to make the stylesheet usable in the backoffice. + /// An umbraco back office enabled stylesheet property has a special prefix, for example: + /// + /// /** umb_name: MyPropertyName */ p { font-size: 1em; } /// [IgnoreDataMember] public IEnumerable Properties { - get - { - var properties = new List(); - //var parser = new CssParser(Content); - - var parsed = StylesheetHelper.ParseRules(Content); - - //foreach (var statement in parsed.StyleSheet.Statements.OfType()) - foreach (var statement in parsed) - { - //var cssBlock = statement.Block; - //if(cssBlock == null) continue; - - //var cssValues = cssBlock.Values; - //if(cssValues == null) continue; - - //properties.AddRange(FormatCss(cssBlock.Values, true)); - - properties.Add(new StylesheetProperty(statement.Name, statement.Styles)); - } - - //var statements = parser.StyleSheet.Statements.Where(s => s is CssRuleSet); - //properties.AddRange(FormatCss(statements, false)); - - return properties; - } + get { return _properties.Value; } } - ///// - ///// Formats a list of statements to a simple object - ///// - ///// Enumerable list of statements - ///// Boolean indicating whether the current list of statements is part of an @ rule - ///// An Enumerable list of objects - //private IEnumerable FormatCss(IEnumerable statements, bool isPartOfAtRule) - //{ - // var properties = new List(); + /// + /// Adds an Umbraco stylesheet property for use in the back office + /// + /// + public void AddProperty(StylesheetProperty property) + { + if (Properties.Any(x => x.Name == property.Name)) + { + throw new DuplicateNameException("The property with the name " + property.Name + " already exists in the collection"); + } - // foreach (var statement in statements.OfType()) - // { - // foreach (var selector in statement.Selectors) - // { - // var declarations = new StringBuilder(); - // foreach (var declaration in statement.Declarations) - // { - // declarations.AppendFormat("{0}:{1};", declaration.Property, FormatCss(declaration.Value)); - // declarations.AppendLine(""); - // } - // properties.Add(new StylesheetProperty(selector.Value.TrimStart('.', '#'), declarations.ToString()) { IsPartOfAtRule = isPartOfAtRule }); - // } - // } + //now we need to serialize out the new property collection over-top of the string Content. + Content = StylesheetHelper.AppendRule(Content, new StylesheetRule + { + Name = property.Name, + Selector = property.Alias, + Styles = property.Value + }); - // return properties; - //} - - ///// - ///// Formats a to a single string - ///// - ///// to format - ///// Value list formatted as a string - //private string FormatCss(CssValueList valueList) - //{ - // bool space = false; - // var values = new StringBuilder(); - - // foreach (CssString value in valueList.Values) - // { - // if (space) - // { - // values.Append(" "); - // } - // else - // { - // space = true; - // } - - // values.Append(value); - // } - - // return values.ToString(); - //} - - ///// - ///// Boolean indicating whether the file is valid css using a css parser - ///// - ///// True if css is valid, otherwise false - //public bool IsFileValidCss() - //{ - // var parser = new CssParser(Content); - - // try - // { - // var styleSheet = parser.StyleSheet;//Get stylesheet to invoke parsing - // } - // catch (Exception ex) - // { - // //Log exception? - // return false; - // } - - // return !parser.Errors.Any(); - //} + //re-set lazy collection + InitializeProperties(); + } + /// + /// Removes an Umbraco stylesheet property + /// + /// + public void RemoveProperty(string name) + { + if (Properties.Any(x => x.Name == name)) + { + Content = StylesheetHelper.ReplaceRule(Content, name, null); + } + } + /// /// Indicates whether the current entity has an identity, which in this case is a path/name. /// diff --git a/src/Umbraco.Core/Models/StylesheetProperty.cs b/src/Umbraco.Core/Models/StylesheetProperty.cs index 7d7f8ae955..302989f621 100644 --- a/src/Umbraco.Core/Models/StylesheetProperty.cs +++ b/src/Umbraco.Core/Models/StylesheetProperty.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; using System.Runtime.Serialization; using Umbraco.Core.Models.EntityBase; @@ -12,17 +13,59 @@ namespace Umbraco.Core.Models /// [Serializable] [DataContract(IsReference = true)] - public class StylesheetProperty : IValueObject + public class StylesheetProperty : TracksChangesEntityBase, IValueObject { - public StylesheetProperty(string @alias, string value) + private string _alias; + private string _value; + + public StylesheetProperty(string name, string @alias, string value) { - Alias = alias; - Value = value; + Name = name; + _alias = alias; + _value = value; } - public string Alias { get; set; } - public string Value { get; set; } - + private static readonly PropertyInfo AliasSelector = ExpressionHelper.GetPropertyInfo(x => x.Alias); + private static readonly PropertyInfo ValueSelector = ExpressionHelper.GetPropertyInfo(x => x.Value); + + /// + /// The CSS rule name that can be used by Umbraco in the back office + /// + public string Name { get; private set; } + + /// + /// This is the CSS Selector + /// + public string Alias + { + get { return _alias; } + set + { + SetPropertyValueAndDetectChanges(o => + { + _alias = value; + return _alias; + }, _alias, AliasSelector); + } + } + + /// + /// The CSS value for the selector + /// + public string Value + { + get { return _value; } + set + { + SetPropertyValueAndDetectChanges(o => + { + _value = value; + return _value; + }, _value, ValueSelector); + } + } + + //public bool IsPartOfAtRule { get; set; } ////This should never be used, it's here because we still have stylesheets based on DB ids and //// we need to wrap the legacy classes diff --git a/src/Umbraco.Core/Persistence/Repositories/ScriptRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ScriptRepository.cs index 8110ffbcc7..ca2b1b262d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ScriptRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ScriptRepository.cs @@ -105,10 +105,10 @@ namespace Umbraco.Core.Persistence.Repositories dirs += "," + SystemDirectories.MvcViews;*/ //Validate file - var validFile = IOHelper.VerifyEditPath(script.Path, dirs.Split(',')); + var validFile = IOHelper.VerifyEditPath(script.VirtualPath, dirs.Split(',')); //Validate extension - var validExtension = IOHelper.VerifyFileExtension(script.Path, exts); + var validExtension = IOHelper.VerifyFileExtension(script.VirtualPath, exts); return validFile && validExtension; } diff --git a/src/Umbraco.Core/Persistence/Repositories/StylesheetRepository.cs b/src/Umbraco.Core/Persistence/Repositories/StylesheetRepository.cs index 207b5fea37..a9bfb61dd5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/StylesheetRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/StylesheetRepository.cs @@ -130,10 +130,10 @@ namespace Umbraco.Core.Persistence.Repositories var dirs = SystemDirectories.Css; //Validate file - var validFile = IOHelper.VerifyEditPath(stylesheet.Path, dirs.Split(',')); + var validFile = IOHelper.VerifyEditPath(stylesheet.VirtualPath, dirs.Split(',')); //Validate extension - var validExtension = IOHelper.VerifyFileExtension(stylesheet.Path, new List { "css" }); + var validExtension = IOHelper.VerifyFileExtension(stylesheet.VirtualPath, new List { "css" }); var fileValid = validFile && validExtension; diff --git a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs index f82a40bb6c..ad79cfc44a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs @@ -631,10 +631,10 @@ namespace Umbraco.Core.Persistence.Repositories dirs += "," + SystemDirectories.MvcViews; //Validate file - var validFile = IOHelper.VerifyEditPath(template.Path, dirs.Split(',')); + var validFile = IOHelper.VerifyEditPath(template.VirtualPath, dirs.Split(',')); //Validate extension - var validExtension = IOHelper.VerifyFileExtension(template.Path, exts); + var validExtension = IOHelper.VerifyFileExtension(template.VirtualPath, exts); return validFile && validExtension; } diff --git a/src/Umbraco.Core/Strings/Css/CssOptions.cs b/src/Umbraco.Core/Strings/Css/CssOptions.cs deleted file mode 100644 index c515ce7c72..0000000000 --- a/src/Umbraco.Core/Strings/Css/CssOptions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Umbraco.Core.Strings.Css -{ - [Flags] - internal enum CssOptions - { - None = 0x00, - PrettyPrint = 0x01, - Overwrite = 0x02 - } - -} \ No newline at end of file diff --git a/src/Umbraco.Core/Strings/Css/CssParser.cs b/src/Umbraco.Core/Strings/Css/CssParser.cs deleted file mode 100644 index f9107e4ce1..0000000000 --- a/src/Umbraco.Core/Strings/Css/CssParser.cs +++ /dev/null @@ -1,652 +0,0 @@ -//using System; -//using System.Collections.Generic; -//using System.IO; - -//namespace Umbraco.Core.Strings.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 errors = new List(); -// private LineReader reader; -// private volatile CssStyleSheet styleSheet; -// private string fileContent; -// private string source; - -// #endregion Fields - -// #region Init - -// /// -// /// Ctor. -// /// -// /// path to source -// public CssParser(string fileContent) -// : this(fileContent, null) -// { -// } - -// /// -// /// Ctor. -// /// -// /// path to source -// /// actual source -// public CssParser(string fileContent, string source) -// { -// this.fileContent = fileContent; -// this.source = source; -// } - -// #endregion Init - -// #region Properties - -// public List 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 - -// /// -// /// (BNF) stylesheet : [ CDO | CDC | S | statement ]*; -// /// -// /// CSS StyleSheet parse tree -// 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; -// } -// 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 - -// /// -// /// (BNF) statement : ruleset | at-rule; -// /// -// /// -// private CssStatement ParseStatement() -// { -// if (this.reader.Current == '@') -// { -// return this.ParseAtRule(); -// } -// else -// { -// this.PutBack(); -// return this.ParseRuleSet(); -// } -// } - -// #endregion Statement - -// #region At-Rule - -// /// -// /// (BNF) at-rule : ATKEYWORD S* any* [ block | ';' S* ]; -// /// -// /// -// /// -// /// 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 -// /// -// 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 - -// /// -// /// (BNF) ruleset : selector? '{' S* declaration? [ ';' S* declaration? ]* '}' S*; -// /// -// /// -// 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 - -// /// -// /// (BNF) selector: any+; -// /// -// /// -// 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 - -// /// -// /// (BNF) declaration : property ':' S* value; -// /// (BNF) property : IDENT S*; -// /// -// /// -// 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 : ", this.reader.FilePath, this.reader.Line, this.reader.Column); -// } - -// CssValueList value = this.ParseValue(); -// declaration.Value = value; - -// return declaration; -// } - -// #endregion Declaration - -// #region Value - -// /// -// /// (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*; -// /// -// /// -// 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, CssOptions options) -// { -// this.StyleSheet.Write(writer, options); -// } - -// #endregion Methods - -// #region Reader Methods - -// /// -// /// -// /// -// /// -// /// Success -// 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; -// } - -// /// -// /// Copies chars from start until the position before the current position -// /// -// /// -// private string Copy(int start) -// { -// // read block -// return this.reader.Copy(start, this.reader.Position - 1); -// } - -// /// -// /// Put one character back -// /// -// private void PutBack() -// { -// this.reader.PutBack(); -// } - -// #endregion Reader Methods -// } -//} \ No newline at end of file diff --git a/src/Umbraco.Core/Strings/Css/CssSyntax.cs b/src/Umbraco.Core/Strings/Css/CssSyntax.cs deleted file mode 100644 index 107ce83f04..0000000000 --- a/src/Umbraco.Core/Strings/Css/CssSyntax.cs +++ /dev/null @@ -1,428 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace Umbraco.Core.Strings.Css -{ - #region Base Types - - /// - /// CSS3 inconsistently specifies more than one grammar: - /// http://www.w3.org/TR/css3-syntax/#style - /// http://www.w3.org/TR/css3-syntax/#detailed-grammar - /// - internal abstract class CssSyntax - { - #region Methods - - public abstract void Write(TextWriter writer, CssOptions options); - - protected static bool IsPrettyPrint(CssOptions options) - { - return (options & CssOptions.PrettyPrint) > 0; - } - - #endregion Methods - - #region Object Overrides - - public override string ToString() - { - var writer = new StringWriter(); - - this.Write(writer, CssOptions.PrettyPrint); - - return writer.ToString(); - } - - #endregion Object Overrides - } - - internal interface ICssValue - { - #region Methods - - void Write(TextWriter writer, CssOptions options); - - #endregion Methods - } - - internal class CssString : CssSyntax - { - #region Fields - - private string value; - - #endregion Fields - - #region Properties - - public virtual string Value - { - get { return this.value; } - set { this.value = value; } - } - - #endregion Properties - - #region Methods - - public override void Write(TextWriter writer, CssOptions options) - { - writer.Write(this.Value); - } - - #endregion Methods - } - - #endregion Base Types - - #region Grammar - - internal class CssStyleSheet : CssSyntax - { - #region Fields - - private readonly List statements = new List(); - - #endregion Fields - - #region Properties - - public List Statements - { - get { return this.statements; } - } - - #endregion Properties - - #region Methods - - public override void Write(TextWriter writer, CssOptions options) - { - bool prettyPrint = IsPrettyPrint(options); - - foreach (CssStatement statement in this.statements) - { - statement.Write(writer, options); - if (prettyPrint) - { - writer.WriteLine(); - } - } - } - - #endregion Methods - } - - internal abstract class CssStatement : CssSyntax, ICssValue - { - } - - /// - /// - /// - /// - /// 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 - /// - internal class CssAtRule : CssStatement - { - #region Constants - - internal const string MediaIdent = "media"; - - #endregion Constants - - #region Fields - - private string ident; - private string value; - - private CssBlock block; - - #endregion Fields - - #region Properties - - public string Ident - { - get { return this.ident; } - set { this.ident = value; } - } - - public string Value - { - get { return this.value; } - set { this.value = value; } - } - - public CssBlock Block - { - get - { - if (this.block == null) - { - this.block = new CssBlock(); - } - return this.block; - } - set { this.block = value; } - } - - #endregion Properties - - #region Methods - - public override void Write(TextWriter writer, CssOptions options) - { - bool prettyPrint = IsPrettyPrint(options); - - writer.Write('@'); - writer.Write(this.ident); - - if (!String.IsNullOrEmpty(this.value)) - { - writer.Write(' '); - writer.Write(this.value); - } - - if (this.block != null) - { - if (prettyPrint) - { - writer.WriteLine(); - } - this.block.Write(writer, options); - } - else - { - writer.Write(';'); - } - - if (prettyPrint) - { - writer.WriteLine(); - } - } - - #endregion Methods - } - - internal class CssBlock : CssSyntax, ICssValue - { - #region Fields - - private readonly List values = new List(); - - #endregion Fields - - #region Properties - - public List Values - { - get { return this.values; } - } - - #endregion Properties - - #region Methods - - public override void Write(TextWriter writer, CssOptions options) - { - bool prettyPrint = IsPrettyPrint(options); - - writer.Write('{'); - if (prettyPrint) - { - writer.WriteLine(); - } - - foreach (ICssValue value in this.Values) - { - value.Write(writer, options); - } - - if (prettyPrint) - { - writer.WriteLine(); - } - writer.Write('}'); - } - - #endregion Methods - } - - internal class CssRuleSet : CssStatement - { - #region Fields - - private readonly List selectors = new List(); - private readonly List declarations = new List(); - - #endregion Fields - - #region Properties - - public List Selectors - { - get { return this.selectors; } - } - - public List Declarations - { - get { return this.declarations; } - } - - #endregion Properties - - #region Methods - - public override void Write(TextWriter writer, CssOptions options) - { - bool prettyPrint = IsPrettyPrint(options); - - bool comma = false; - - foreach (CssString selector in this.Selectors) - { - if (comma) - { - writer.Write(","); - if (prettyPrint) - { - writer.WriteLine(); - } - } - else - { - comma = true; - } - - selector.Write(writer, options); - } - - if (prettyPrint) - { - writer.WriteLine(); - } - writer.Write("{"); - if (prettyPrint) - { - writer.WriteLine(); - } - - foreach (CssDeclaration dec in this.Declarations) - { - dec.Write(writer, options); - } - - writer.Write("}"); - if (prettyPrint) - { - writer.WriteLine(); - } - } - - #endregion Methods - } - - internal class CssSelector : CssString - { - } - - internal class CssDeclaration : CssSyntax, ICssValue - { - #region Fields - - private string property; - private CssValueList value; - - #endregion Fields - - #region Properties - - public string Property - { - get { return this.property; } - set { this.property = value; } - } - - public CssValueList Value - { - get { return this.value; } - set { this.value = value; } - } - - #endregion Properties - - #region Methods - - public override void Write(TextWriter writer, CssOptions options) - { - bool prettyPrint = IsPrettyPrint(options); - if (prettyPrint) - { - writer.Write('\t'); - } - writer.Write(this.Property); - writer.Write(':'); - if (prettyPrint) - { - writer.Write(" "); - } - this.Value.Write(writer, options); - writer.Write(";"); - if (prettyPrint) - { - writer.WriteLine(); - } - } - - #endregion Methods - } - - internal class CssValueList : CssSyntax - { - #region Fields - - private readonly List values = new List(); - - #endregion Fields - - #region Properties - - public List Values - { - get { return this.values; } - } - - #endregion Properties - - #region Methods - - public override void Write(TextWriter writer, CssOptions options) - { - bool space = false; - - foreach (CssString value in this.Values) - { - if (space) - { - writer.Write(" "); - } - else - { - space = true; - } - - value.Write(writer, options); - } - } - - #endregion Methods - } - - #endregion Grammar -} \ No newline at end of file diff --git a/src/Umbraco.Core/Strings/Css/FilterTrie.cs b/src/Umbraco.Core/Strings/Css/FilterTrie.cs deleted file mode 100644 index b3b9f0591f..0000000000 --- a/src/Umbraco.Core/Strings/Css/FilterTrie.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Collections.Generic; - -namespace Umbraco.Core.Strings.Css -{ - /// - /// Creates a Trie out of ReadFilters - /// - internal class FilterTrie : TrieNode - { - #region Constants - - private const int DefaultTrieWidth = 1; - - #endregion Constants - - #region Init - - internal FilterTrie(IEnumerable filters) - : base(DefaultTrieWidth) - { - // load trie - foreach (ReadFilter filter in filters) - { - TrieNode node = this; - - // build out the path for StartToken - foreach (char ch in filter.StartToken) - { - if (!node.Contains(ch)) - { - node[ch] = new TrieNode(DefaultTrieWidth); - } - - node = (TrieNode)node[ch]; - } - - // at the end of StartToken path is the EndToken - node.Value = filter.EndToken; - } - } - - #endregion Init - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Strings/Css/LineReader.cs b/src/Umbraco.Core/Strings/Css/LineReader.cs deleted file mode 100644 index fa61f76216..0000000000 --- a/src/Umbraco.Core/Strings/Css/LineReader.cs +++ /dev/null @@ -1,463 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace Umbraco.Core.Strings.Css -{ - internal class LineReader : TextReader - { - #region Fields - - private int line = 1; - private int column = 0; - private int position = -1; - - private string filePath; - private string source; - - private readonly FilterTrie trie; - - private bool normalizeWhiteSpace = false; - - #endregion Fields - - #region Init - - /// - /// Ctor. - /// - /// - /// - internal LineReader(string filePath, IEnumerable filters) : this(filePath, null, filters) { } - - /// - /// Ctor. - /// - /// - /// - /// - internal LineReader(string filePath, string source, IEnumerable filters) - { - this.trie = new FilterTrie(filters); - this.source = source; - this.filePath = filePath; - - if (this.source == null) - { - this.source = filePath;//Changed to direct content string instead of file. - /*if (System.IO.File.Exists(filePath)) - { - this.source = System.IO.File.ReadAllText(filePath); - } - else - { - throw new FileError("File not found", filePath, 0, 0); - }*/ - } - } - - /// - /// Ctor. - /// - /// - /// - internal LineReader(string filePath, string source) - : this(filePath, source, new ReadFilter[0]) - { - } - - #endregion Init - - #region Properties - - /// - /// Gets the path to the source file - /// - public string FilePath - { - get { return this.filePath; } - } - - /// - /// Gets the size of source file in chars - /// - public int Length - { - get { return this.source.Length; } - } - - /// - /// Gets the current line number - /// - public int Line - { - get { return this.line; } - } - - /// - /// Gets the current col number - /// - public int Column - { - get { return this.column; } - } - - /// - /// Gets the current char position - /// - public int Position - { - get { return this.position; } - } - - /// - /// Gets if at end the end of file - /// - public bool EndOfFile - { - get { return this.position >= this.source.Length; } - } - - /// - /// Gets and sets if whitespace is normalized while reading - /// - public bool NormalizeWhiteSpace - { - get { return this.normalizeWhiteSpace; } - set { this.normalizeWhiteSpace = value; } - } - - /// - /// Gets the current char - /// - public int Current - { - get - { - if (this.EndOfFile) - { - return -1; - } - return this.source[this.position]; - } - } - - #endregion Properties - - #region TextReader Members - - /// - /// Unfiltered look ahead - /// - /// - public override int Peek() - { - return this.Peek(1); - } - - /// - /// Filtered read of the next source char. Counters are incremented. - /// - /// - /// - /// NewLine sequences (CR/LF, LF, CR) are normalized to LF. - /// - public override int Read() - { - return this.Read(true); - } - - #endregion TextReader Members - - #region Utility Methods - - /// - /// Backs the current position up one. - /// - public void PutBack() - { - if (this.position < 0) - { - throw new InvalidOperationException("Already at start of source"); - } - switch (this.source[this.position]) - { - case '\r': //CR - { - // manipulate CR/LF as one char - if ((this.position + 1 < this.Length) && this.source[this.position + 1] == '\n') - { - this.position--; - } - break; - } - case '\n': //LF - case '\f': //FF - { - this.line--; - break; - } - } - this.column--; - this.position--; - } - - /// - /// Copies a range from the source - /// - /// starting position, inclusive - /// ending position, inclusive - /// - public string Copy(int start, int end) - { - if (start < 0) - { - throw new ArgumentOutOfRangeException("start"); - } - if (end < 0) - { - throw new ArgumentOutOfRangeException("end"); - } - if (end < 1) - { - return null; - } - - // set to just before read, next char is start - int copyPosition = start - 1; - - // allocate the full range but may not use due to filtering - char[] buffer = new char[end - start + 1]; - - int count = 0; - while (copyPosition < end) - { - int ch = this.CopyRead(ref copyPosition); - if (ch == -1) - { - throw new UnexpectedEndOfFile("Read past end of file", this.FilePath, this.Line, this.Column); - } - buffer[count] = (char)ch; - count++; - } - - if (count < 1) - { - return null; - } - return new String(buffer, 0, count).Trim(); - } - - #endregion Utility Methods - - #region Filter Methods - - /// - /// Peeks with n chars of lookahead. - /// - /// - /// unfiltered read - protected int Peek(int lookahead) - { - int pos = this.position + lookahead; - if (pos >= this.source.Length) - { - return -1; - } - return this.source[pos]; - } - - /// - /// Reads the next char - /// - /// if filtering - /// the next char, or -1 if at EOF - protected int Read(bool filter) - { - if (this.position + 1 >= this.source.Length) - { - this.position = this.source.Length; - return -1; - } - - // increment counters - this.position++; - this.column++; - char ch = this.source[this.position]; - - if (Char.IsWhiteSpace(ch)) - { - ch = this.NormalizeSpaces(ch, ref this.position, ref this.line, ref this.column); - } - - return filter ? this.Filter(ch) : ch; - } - - /// - /// Normalized CR/CRLF/LF/FF to LF, or all whitespace to SPACE if NormalizeWhiteSpace is true - /// - /// - /// - /// - /// - /// - private char NormalizeSpaces(char ch, ref int pos, ref int line, ref int col) - { - int length = this.source.Length; - if (this.normalizeWhiteSpace) - { - // normalize runs of WhiteSpace to ' ' - while ((pos + 1 < length) && Char.IsWhiteSpace(this.source, pos + 1)) - { - pos++; - col++; - - // increment line count - switch (this.source[pos]) - { - case '\r': //CR - { - // manipulate CR/LF as one char - if ((pos + 1 < length) && this.source[pos + 1] == '\n') - { - pos++; - } - goto case '\n'; - } - case '\n': //LF - case '\f': //FF - { - line++; - col = 0; - break; - } - } - } - ch = ' '; - } - else - { - // normalize NewLines to '\n', increment line count - switch (ch) - { - case '\r': //CR - { - // manipulate CR/LF as one char - if ((pos + 1 < length) && this.source[pos + 1] == '\n') - { - pos++; - } - goto case '\n'; - } - case '\n': //LF - case '\f': //FF - { - line++; - col = 0; - ch = '\n'; - break; - } - } - } - return ch; - } - - /// - /// Read for Copying (doesn't reset line.col counters) - /// - /// - /// - protected int CopyRead(ref int copyPosition) - { - if (copyPosition + 1 >= this.source.Length) - { - return -1; - } - - // increment counters - copyPosition++; - char ch = this.source[copyPosition]; - - if (Char.IsWhiteSpace(ch)) - { - int dummyLine = 0, dummyCol = 0; - ch = this.NormalizeSpaces(ch, ref copyPosition, ref dummyLine, ref dummyCol); - } - - return this.Filter(ch); - } - - /// - /// Filters based upon an internal Trie - /// - /// - /// - private int Filter(char ch) - { - int lookAhead = 0; - ITrieNode node = this.trie[ch]; - - while (node != null) - { - if (node.HasValue) - { - // found StartToken - string endToken = node.Value; - int length = endToken.Length; - - // move to end of StartToken - this.position += lookAhead; - - for (int i = 0; i < length; i++) - { - int ch2 = this.Read(false); - if (ch < 0) - { - throw new UnexpectedEndOfFile("Expected " + endToken, this.FilePath, this.Line, this.Column); - } - if (ch2 != endToken[i]) - { - // reset search - while (i > 0) - { - i--; - this.PutBack(); - } - i--; - } - } - return this.Read(true); - } - else - { - lookAhead++; - int pk = this.Peek(lookAhead); - if (pk < 0) - { - return ch; - } - node = node[(char)pk]; - } - } - - return ch; - } - - #endregion Filter Methods - - #region IDisposable Members - - /// - /// Free source resources. - /// - /// - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - this.source = null; - } - - #endregion IDisposable Members - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Strings/Css/ParseException.cs b/src/Umbraco.Core/Strings/Css/ParseException.cs deleted file mode 100644 index deb3417ad6..0000000000 --- a/src/Umbraco.Core/Strings/Css/ParseException.cs +++ /dev/null @@ -1,224 +0,0 @@ -using System; - -namespace Umbraco.Core.Strings.Css -{ - internal enum ParseExceptionType - { - Warning, - Error - } - - [Serializable] - internal abstract class ParseException : ApplicationException - { - #region Constants - - // this cannot change every char is important or Visual Studio will not list as error/warning - // http://blogs.msdn.com/msbuild/archive/2006/11/03/msbuild-visual-studio-aware-error-messages-and-message-formats.aspx - private const string MSBuildErrorFormat = "{0}({1},{2}): {3} {4}: {5}"; - - #endregion Constants - - #region Fields - - private string file; - private int line; - private int column; - private int code = 0; - - #endregion Fields - - #region Init - - public ParseException(string message, string file, int line, int column) - : base(message) - { - this.file = file; - this.line = line; - this.column = column; - } - - public ParseException(string message, string file, int line, int column, Exception innerException) - : base(message, innerException) - { - this.file = file; - this.line = line; - this.column = column; - } - - #endregion Init - - #region Properties - - public abstract ParseExceptionType Type - { - get; - } - - public virtual int Code - { - get { return this.code; } - } - - public string ErrorCode - { - get - { - string ext = System.IO.Path.GetExtension(file); - if (ext == null || ext.Length < 2) - { - return null; - } - - return ext.Substring(1).ToUpperInvariant() + this.Code.ToString("####0000"); - } - } - - public string File - { - get { return this.file; } - } - - public int Line - { - get { return this.line; } - } - - public int Column - { - get { return this.column; } - } - - #endregion Properties - - #region Methods - - public virtual string GetCompilerMessage() - { - return this.GetCompilerMessage(this.Type == ParseExceptionType.Warning); - } - - public virtual string GetCompilerMessage(bool isWarning) - { - // format exception as a VS2005 error/warning - return String.Format( - ParseException.MSBuildErrorFormat, - this.File, - (this.Line > 0) ? this.Line : 1, - (this.Column > 0) ? this.Column : 1, - isWarning ? "warning" : "error", - this.ErrorCode, - this.Message); - } - - #endregion Methods - } - - [Serializable] - internal class ParseWarning : ParseException - { - #region Init - - public ParseWarning(string message, string file, int line, int column) - : base(message, file, line, column) - { - } - - public ParseWarning(string message, string file, int line, int column, Exception innerException) - : base(message, file, line, column, innerException) - { - } - - #endregion Init - - #region Properties - - public override ParseExceptionType Type - { - get { return ParseExceptionType.Warning; } - } - - #endregion Properties - } - - [Serializable] - internal class ParseError : ParseException - { - #region Init - - public ParseError(string message, string file, int line, int column) - : base(message, file, line, column) - { - } - - public ParseError(string message, string file, int line, int column, Exception innerException) - : base(message, file, line, column, innerException) - { - } - - #endregion Init - - #region Properties - - public override ParseExceptionType Type - { - get { return ParseExceptionType.Error; } - } - - #endregion Properties - } - - [Serializable] - internal class UnexpectedEndOfFile : ParseError - { - #region Init - - public UnexpectedEndOfFile(string message, string file, int line, int column) - : base(message, file, line, column) - { - } - - public UnexpectedEndOfFile(string message, string file, int line, int column, Exception innerException) - : base(message, file, line, column, innerException) - { - } - - #endregion Init - } - - [Serializable] - internal class FileError : ParseWarning - { - #region Init - - public FileError(string message, string file, int line, int column) - : base(message, file, line, column) - { - } - - public FileError(string message, string file, int line, int column, Exception innerException) - : base(message, file, line, column, innerException) - { - } - - #endregion Init - } - - [Serializable] - internal class SyntaxError : ParseError - { - #region Init - - public SyntaxError(string message, string file, int line, int column) - : base(message, file, line, column) - { - } - - public SyntaxError(string message, string file, int line, int column, Exception innerException) - : base(message, file, line, column, innerException) - { - } - - #endregion Init - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Strings/Css/ReadFilter.cs b/src/Umbraco.Core/Strings/Css/ReadFilter.cs deleted file mode 100644 index 1dbd4f5080..0000000000 --- a/src/Umbraco.Core/Strings/Css/ReadFilter.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; - -namespace Umbraco.Core.Strings.Css -{ - /// - /// Defines a character sequence to filter out when reading. - /// - /// - /// If the sequence exists in the read source, it will be read out as if it was never there. - /// - internal struct ReadFilter - { - #region Fields - - public readonly string StartToken; - public readonly string EndToken; - - #endregion Fields - - #region Init - - public ReadFilter(string start, string end) - { - if (String.IsNullOrEmpty(start)) - { - throw new ArgumentNullException("start"); - } - if (String.IsNullOrEmpty(end)) - { - throw new ArgumentNullException("end"); - } - - this.StartToken = start; - this.EndToken = end; - } - - #endregion Init - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Strings/Css/StylesheetHelper.cs b/src/Umbraco.Core/Strings/Css/StylesheetHelper.cs index dcb70dae50..ff7591c6b5 100644 --- a/src/Umbraco.Core/Strings/Css/StylesheetHelper.cs +++ b/src/Umbraco.Core/Strings/Css/StylesheetHelper.cs @@ -7,23 +7,25 @@ namespace Umbraco.Core.Strings.Css { internal class StylesheetHelper { - private const string RuleRegexFormat = @"/\*\s*name:\s*(?{0}?)\s*\*/\s*(?[^\s,{{]*?)\s*{{\s*(?.*?)\s*}}"; + private const string RuleRegexFormat = @"/\*\*\s*umb_name:\s*(?{0}?)\s*\*/\s*(?[^\s,{{]*?)\s*{{\s*(?.*?)\s*}}"; public static IEnumerable ParseRules(string input) { var rules = new List(); - var ruleRegex = new Regex(string.Format(RuleRegexFormat, @"[^\*\r\n]*"), RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); + var ruleRegex = new Regex(string.Format(RuleRegexFormat, @"[^\*\r\n]*"), RegexOptions.IgnoreCase | RegexOptions.Singleline); var contents = input; var ruleMatches = ruleRegex.Matches(contents); foreach (Match match in ruleMatches) { + var name = match.Groups["Name"].Value; + + //If this name already exists, only use the first one + if (rules.Any(x => x.Name == name)) continue; + rules.Add(new StylesheetRule { - //RuleId = new HiveId(new Uri("storage://stylesheets"), string.Empty, new HiveIdValue(input.Id.Value + "/" + match.Groups["Name"].Value)), - //StylesheetId = input.Id, - Name = match.Groups["Name"].Value, Selector = match.Groups["Selector"].Value, // Only match first selector when chained together @@ -37,7 +39,7 @@ namespace Umbraco.Core.Strings.Css public static string ReplaceRule(string input, string oldRuleName, StylesheetRule rule) { var contents = input; - var ruleRegex = new Regex(string.Format(RuleRegexFormat, oldRuleName), RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); + var ruleRegex = new Regex(string.Format(RuleRegexFormat, oldRuleName), RegexOptions.IgnoreCase | RegexOptions.Singleline); contents = ruleRegex.Replace(contents, rule != null ? rule.ToString() : ""); return contents; } diff --git a/src/Umbraco.Core/Strings/Css/StylesheetRule.cs b/src/Umbraco.Core/Strings/Css/StylesheetRule.cs index 391a471ed5..4f45525038 100644 --- a/src/Umbraco.Core/Strings/Css/StylesheetRule.cs +++ b/src/Umbraco.Core/Strings/Css/StylesheetRule.cs @@ -4,15 +4,8 @@ using System.Text; namespace Umbraco.Core.Strings.Css { - public class StylesheetRule + internal class StylesheetRule { - public StylesheetRule() - { } - - //public HiveId StylesheetId { get; set; } - - //public HiveId RuleId { get; set; } - public string Name { get; set; } public string Selector { get; set; } @@ -22,11 +15,13 @@ namespace Umbraco.Core.Strings.Css public override string ToString() { var sb = new StringBuilder(); - sb.Append("/*" + Environment.NewLine); - sb.AppendFormat(" Name: {0}" + Environment.NewLine, Name); - sb.Append("*/" + Environment.NewLine); - sb.AppendFormat("{0} {{" + Environment.NewLine, Selector); - sb.Append(string.Join(Environment.NewLine, string.IsNullOrWhiteSpace(Styles) == false ? string.Join(Environment.NewLine, Styles.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None).Select(x => "\t" + x)) + Environment.NewLine : "")); + sb.Append("/**"); + sb.AppendFormat("umb_name:{0}", Name); + sb.Append("*/"); + sb.Append(Environment.NewLine); + sb.Append(Selector); + sb.Append("{"); + sb.Append(Styles.IsNullOrWhiteSpace() ? "" : Styles.Trim()); sb.Append("}"); return sb.ToString(); diff --git a/src/Umbraco.Core/Strings/Css/TrieNode.cs b/src/Umbraco.Core/Strings/Css/TrieNode.cs deleted file mode 100644 index 2d2b7bf629..0000000000 --- a/src/Umbraco.Core/Strings/Css/TrieNode.cs +++ /dev/null @@ -1,139 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Umbraco.Core.Strings.Css -{ - /// - /// A generic node for building a Trie - /// - /// the Type used for the node path - /// the Type used for the node value - /// - /// http://en.wikipedia.org/wiki/Trie - /// - internal class TrieNode : ITrieNode - { - #region Fields - - private readonly IDictionary> Children; - private TValue value = default(TValue); - - #endregion Fields - - #region Init - - /// - /// Ctor - /// - public TrieNode() - : this(-1) - { - } - - /// - /// Ctor. - /// - /// - public TrieNode(int capacity) - { - if (capacity < 1) - { - this.Children = new Dictionary>(); - } - else - { - this.Children = new Dictionary>(capacity); - } - } - - #endregion Init - - #region Properties - - public ITrieNode this[TKey key] - { - get - { - if (!this.Children.ContainsKey(key)) - { - return null; - } - return this.Children[key]; - } - - // added "internal" to get around change in C# 3.0 modifiers - // this worked fine in C# 2.0 but they fixed that bug - // http://blogs.msdn.com/ericlippert/archive/2008/03/28/why-can-t-i-access-a-protected-member-from-a-derived-class-part-two-why-can-i.aspx - protected internal set { this.Children[key] = value; } - } - - public TValue Value - { - get { return this.value; } - - // added "internal" to get around change in C# 3.0 modifiers - // this worked fine in C# 2.0 but they fixed that bug - // http://blogs.msdn.com/ericlippert/archive/2008/03/28/why-can-t-i-access-a-protected-member-from-a-derived-class-part-two-why-can-i.aspx - protected internal set - { - if (!EqualityComparer.Default.Equals(this.value, default(TValue))) - { - throw new InvalidOperationException("Trie path collision: the value for TrieNode<" + value.GetType().Name + "> has already been assigned."); - } - this.value = value; - } - } - - public bool HasValue - { - get - { - return !EqualityComparer.Default.Equals(this.value, default(TValue)); - } - } - - #endregion Properties - - #region Methods - - /// - /// Determines if child exists - /// - /// - /// - public bool Contains(TKey key) - { - return this.Children.ContainsKey(key); - } - - #endregion Methods - } - - internal interface ITrieNode - { - #region Properties - - ITrieNode this[TKey key] - { - get; - } - - TValue Value - { - get; - } - - bool HasValue - { - get; - } - - #endregion Methods - - #region Methods - - bool Contains(TKey key); - - #endregion Methods - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index f3d3614a23..9daa868dc7 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -320,7 +320,6 @@ - @@ -453,14 +452,7 @@ - - - - - - - diff --git a/src/Umbraco.Tests/Models/StylesheetTests.cs b/src/Umbraco.Tests/Models/StylesheetTests.cs index b83daca44e..21e339f54b 100644 --- a/src/Umbraco.Tests/Models/StylesheetTests.cs +++ b/src/Umbraco.Tests/Models/StylesheetTests.cs @@ -21,115 +21,72 @@ namespace Umbraco.Tests.Models Assert.That(stylesheet.Alias, Is.EqualTo("styles")); } - //[Test] - //public void Can_Validate_Stylesheet() - //{ - // // Arrange - // var stylesheet = new Stylesheet("/css/styles.css"); - // stylesheet.Content = @"body { color:#000; } .bold {font-weight:bold;}"; + [Test] + public void Can_Add_Property() + { + // Arrange + var stylesheet = new Stylesheet("/css/styles.css") {Content = @"body { color:#000; } .bold {font-weight:bold;}"}; - // // Assert - // Assert.That(stylesheet.IsFileValidCss(), Is.True); - // Assert.That(stylesheet.IsValid(), Is.True); - //} + stylesheet.AddProperty(new StylesheetProperty("Test", "p", "font-weight:bold; font-family:Arial;")); - //[Test] - //public void Can_InValidate_Stylesheet() - //{ - // // Arrange - // var stylesheet = new Stylesheet("/css/styles.css"); - // stylesheet.Content = @"body { color:#000; } .bold font-weight:bold;}"; + // Assert + Assert.AreEqual(1, stylesheet.Properties.Count()); + Assert.AreEqual("Test", stylesheet.Properties.Single().Name); + Assert.AreEqual("p", stylesheet.Properties.Single().Alias); + Assert.AreEqual("font-weight:bold; font-family:Arial;", stylesheet.Properties.Single().Value); + } - // // Assert - // Assert.That(stylesheet.IsFileValidCss(), Is.False); - // Assert.That(stylesheet.IsValid(), Is.True); - //} + [Test] + public void Can_Remove_Property() + { + // Arrange + var stylesheet = new Stylesheet("/css/styles.css") { Content = @"body { color:#000; } /**umb_name:Hello*/p{font-size:2em;} .bold {font-weight:bold;}" }; - //[Test] - //public void Can_Validate_Css3_Stylesheet() - //{ - // // Arrange - // var stylesheet = new Stylesheet("/css/styles.css"); - // stylesheet.Content = "@media screen and (min-width: 768px) { body {background: red}}"; + + Assert.AreEqual(1, stylesheet.Properties.Count()); - // // Assert - // Assert.That(stylesheet.IsFileValidCss(), Is.True); - // Assert.That(stylesheet.IsValid(), Is.True); - //} + stylesheet.RemoveProperty("Hello"); -// [Test] -// public void Can_Verify_Mixed_Css_Css3_Property_From_Css() -// { -// // Arrange -// var stylesheet = new Stylesheet("/css/styles.css"); -// stylesheet.Content = @"@media screen and (min-width: 600px) and (min-width: 900px) { -// .class { -// background: #666; -// } -// }"; + Assert.AreEqual(0, stylesheet.Properties.Count()); + Assert.AreEqual(@"body { color:#000; } .bold {font-weight:bold;}", stylesheet.Content); + } -// // Act -// var properties = stylesheet.Properties; + [Test] + public void Can_Update_Property() + { + // Arrange + var stylesheet = new Stylesheet("/css/styles.css") { Content = @"body { color:#000; } /**umb_name:Hello*/p{font-size:2em;} .bold {font-weight:bold;}" }; -// // Assert -// Assert.That(stylesheet.IsFileValidCss(), Is.True); -// Assert.That(properties, Is.Not.Null); -// Assert.That(properties.Any(), Is.True); -// } + var prop = stylesheet.Properties.Single(); + prop.Alias = "li"; + prop.Value = "font-size:5em;"; + + //re-get + prop = stylesheet.Properties.Single(); + Assert.AreEqual("li", prop.Alias); + Assert.AreEqual("font-size:5em;", prop.Value); + Assert.AreEqual("body { color:#000; } /**umb_name:Hello*/\r\nli{font-size:5em;} .bold {font-weight:bold;}", stylesheet.Content); + } [Test] public void Can_Get_Properties_From_Css() { // Arrange var stylesheet = new Stylesheet("/css/styles.css"); - stylesheet.Content = @"body { color:#000; } .bold {font-weight:bold;}"; + stylesheet.Content = @"body { color:#000; } .bold {font-weight:bold;} /**umb_name:Hello */ p { font-size: 1em; } /**umb_name:testing123*/ li:first-child {padding:0px;}"; // Act var properties = stylesheet.Properties; - // Assert - Assert.That(properties, Is.Not.Null); - Assert.That(properties.Any(), Is.True); - Assert.That(properties.Count(), Is.EqualTo(2)); + Assert.AreEqual(2, properties.Count()); + Assert.AreEqual("Hello", properties.First().Name); + Assert.AreEqual("font-size: 1em;", properties.First().Value); + Assert.AreEqual("p", properties.First().Alias); + Assert.AreEqual("testing123", properties.Last().Name); + Assert.AreEqual("padding:0px;", properties.Last().Value); + Assert.AreEqual("li:first-child", properties.Last().Alias); } - [Test] - public void Can_Verify_Property_From_Css() - { - // Arrange - var stylesheet = new Stylesheet("/css/styles.css"); - stylesheet.Content = @"body { color:#000;font-weight:normal; } .bold {font-weight:bold;}"; - - // Act - var properties = stylesheet.Properties; - var property = properties.FirstOrDefault(); - - // Assert - Assert.That(property, Is.Not.Null); - Assert.That(property.Alias, Is.EqualTo("body")); - Assert.That(property.Value, Is.EqualTo("color:#000;\r\nfont-weight:normal;\r\n")); - } - - [Test] - public void Can_Verify_Multiple_Properties_From_Css_Selectors() - { - // Arrange - var stylesheet = new Stylesheet("/css/styles.css"); - stylesheet.Content = @".bold, .my-font {font-weight:bold; color:#000; align:left;} - #column-sidebar { - width: auto; - float: none; - }"; - - // Act - var properties = stylesheet.Properties; - var firstProperty = properties.Any(x => x.Alias == "bold"); - var secondProperty = properties.Any(x => x.Alias == "my-font"); - - // Assert - Assert.That(firstProperty, Is.True); - Assert.That(secondProperty, Is.True); - } [Test] public void Can_Serialize_Without_Error() diff --git a/src/Umbraco.Tests/Persistence/Repositories/StylesheetRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/StylesheetRepositoryTest.cs index d7b63f3443..bd0e399d9e 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/StylesheetRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/StylesheetRepositoryTest.cs @@ -45,7 +45,7 @@ namespace Umbraco.Tests.Persistence.Repositories } [Test] - public void Can_Perform_Add_On_StylesheetRepository() + public void Can_Perform_Add() { // Arrange var provider = new FileUnitOfWorkProvider(); @@ -64,31 +64,7 @@ namespace Umbraco.Tests.Persistence.Repositories } [Test] - public void Can_Read_Properties_Of_Stylesheet() - { - // Arrange - var provider = new FileUnitOfWorkProvider(); - var unitOfWork = provider.GetUnitOfWork(); - var dbUnitOfWork = PetaPocoUnitOfWorkProvider.CreateUnitOfWork(Mock.Of()); - - using (var repository = new StylesheetRepository(unitOfWork, dbUnitOfWork, _fileSystem)) - { - var stylesheet = new Stylesheet("test-add.css") { Content = "body { color:#000; } .bold {font-weight:bold;} /* Name: Test */ p { font-size: 1em; }" }; - repository.AddOrUpdate(stylesheet); - unitOfWork.Commit(); - - //re-get - stylesheet = repository.Get(stylesheet.Name); - var props = stylesheet.Properties; - - - } - - - } - - [Test] - public void Can_Perform_Update_On_StylesheetRepository() + public void Can_Perform_Update() { // Arrange var provider = new FileUnitOfWorkProvider(); @@ -116,7 +92,38 @@ namespace Umbraco.Tests.Persistence.Repositories } [Test] - public void Can_Perform_Delete_On_StylesheetRepository() + public void Can_Perform_Update_With_Property() + { + // Arrange + var provider = new FileUnitOfWorkProvider(); + var unitOfWork = provider.GetUnitOfWork(); + var dbUnitOfWork = PetaPocoUnitOfWorkProvider.CreateUnitOfWork(Mock.Of()); + var repository = new StylesheetRepository(unitOfWork, dbUnitOfWork, _fileSystem); + + // Act + var stylesheet = new Stylesheet("test-update.css") { Content = "body { color:#000; } .bold {font-weight:bold;}" }; + repository.AddOrUpdate(stylesheet); + unitOfWork.Commit(); + dbUnitOfWork.Commit(); + + stylesheet.AddProperty(new StylesheetProperty("Test", "p", "font-size:2em;")); + + repository.AddOrUpdate(stylesheet); + unitOfWork.Commit(); + + //re-get + stylesheet = repository.Get(stylesheet.Name); + + //Assert + Assert.That(stylesheet.Content, Is.EqualTo(@"body { color:#000; } .bold {font-weight:bold;} + +/**umb_name:Test*/ +p{font-size:2em;}")); + Assert.AreEqual(1, stylesheet.Properties.Count()); + } + + [Test] + public void Can_Perform_Delete() { // Arrange var provider = new FileUnitOfWorkProvider(); @@ -137,7 +144,7 @@ namespace Umbraco.Tests.Persistence.Repositories } [Test] - public void Can_Perform_Get_On_StylesheetRepository() + public void Can_Perform_Get() { // Arrange var provider = new FileUnitOfWorkProvider(); @@ -156,7 +163,7 @@ namespace Umbraco.Tests.Persistence.Repositories } [Test] - public void Can_Perform_GetAll_On_StylesheetRepository() + public void Can_Perform_GetAll() { // Arrange var provider = new FileUnitOfWorkProvider(); @@ -180,7 +187,7 @@ namespace Umbraco.Tests.Persistence.Repositories } [Test] - public void Can_Perform_GetAll_With_Params_On_StylesheetRepository() + public void Can_Perform_GetAll_With_Params() { // Arrange var provider = new FileUnitOfWorkProvider(); @@ -204,7 +211,7 @@ namespace Umbraco.Tests.Persistence.Repositories } [Test] - public void Can_Perform_Exists_On_StylesheetRepository() + public void Can_Perform_Exists() { // Arrange var provider = new FileUnitOfWorkProvider(); diff --git a/src/Umbraco.Tests/Strings/StylesheetHelperTests.cs b/src/Umbraco.Tests/Strings/StylesheetHelperTests.cs index d286b3f15a..3f9f972b16 100644 --- a/src/Umbraco.Tests/Strings/StylesheetHelperTests.cs +++ b/src/Umbraco.Tests/Strings/StylesheetHelperTests.cs @@ -8,37 +8,82 @@ namespace Umbraco.Tests.Strings [TestFixture] public class StylesheetHelperTests { + [Test] + public void Replace_Rule() + { + var css = @"body {font-family:Arial;}/** Umb_Name: Test1 */ p { font-size: 1em; } /** umb_name: Test2 */ li {padding:0px;} table {margin:0;}"; + var results = StylesheetHelper.ParseRules(css); + + var result = StylesheetHelper.ReplaceRule(css, results.First().Name, new StylesheetRule() + { + Name = "My new rule", + Selector = "p", + Styles = "font-size:1em; color:blue;" + }); + + Assert.AreEqual(@"body {font-family:Arial;}/**umb_name:My new rule*/ +p{font-size:1em; color:blue;} /** umb_name: Test2 */ li {padding:0px;} table {margin:0;}", result); + } + + [Test] + public void Append_Rule() + { + var css = @"body {font-family:Arial;}/** Umb_Name: Test1 */ p { font-size: 1em; } /** umb_name: Test2 */ li {padding:0px;} table {margin:0;}"; + + var result = StylesheetHelper.AppendRule(css, new StylesheetRule() + { + Name = "My new rule", + Selector = "p", + Styles = "font-size:1em; color:blue;" + }); + + Assert.AreEqual(@"body {font-family:Arial;}/** Umb_Name: Test1 */ p { font-size: 1em; } /** umb_name: Test2 */ li {padding:0px;} table {margin:0;} + +/**umb_name:My new rule*/ +p{font-size:1em; color:blue;}", result); + } + + [Test] + public void Duplicate_Names() + { + var css = @"/** Umb_Name: Test */ p { font-size: 1em; } /** umb_name: Test */ li {padding:0px;}"; + var results = StylesheetHelper.ParseRules(css); + Assert.AreEqual(1, results.Count()); + } + // Standard rule stle - [TestCase("Test", "p", "font-size: 1em;", @"/* - Name: Test + [TestCase("Test", "p", "font-size: 1em;", @"/** + Umb_Name: Test */ p { font-size: 1em; }")] - // All on one line - [TestCase("Test", "p", "font-size: 1em;", @"/* Name: Test */ p { font-size: 1em; }")] + // All on one line, different casing + [TestCase("Test", "p", "font-size: 1em;", @"/** Umb_Name: Test */ p { font-size: 1em; }")] // styles on several lines [TestCase("Test", "p", @"font-size: 1em; color:red; font-weight:bold; -text-align:left;", @"/* Name: Test */ p { font-size: 1em; +text-align:left;", @"/** umb_name: Test */ p { font-size: 1em; color:red; font-weight:bold; text-align:left; }")] // All on one line with no spaces - [TestCase("Test", "p", "font-size: 1em;", @"/*Name:Test*/p{font-size: 1em;}")] + [TestCase("Test", "p", "font-size: 1em;", @"/**UMB_NAME:Test*/p{font-size: 1em;}")] + // Has a name with spaces + [TestCase("Hello world", "p", "font-size: 1em;", @"/**UMB_NAME:Hello world */p{font-size: 1em;}")] // Every part on a new line - [TestCase("Test", "p", "font-size: 1em;", @"/* -Name: + [TestCase("Test", "p", "font-size: 1em;", @"/** +umb_name: Test */ p { font-size: 1em; }")] - public void StylesheetHelperTests_ParseRules_Parses(string name, string selector, string styles, string css) + public void ParseRules_Parses(string name, string selector, string styles, string css) { // Act @@ -54,21 +99,27 @@ font-size: 1em; } // No Name: keyword - [TestCase(@"/* Test2 */ + [TestCase(@"/** Test2 */ p { font-size: 1em; }")] // Has a Name: keyword, but applies to 2 rules, so shouldn't parse - [TestCase(@"/* Name: Test2 */ + [TestCase(@"/** umb_name: Test2 */ p, h2 { font-size: 1em; }")] // Has it's name wrapping over two lines - [TestCase("/* Name: Test\r\n2 */ p { font-size: 1em; }")] - [TestCase("/* Name: Test\n2 */ p { font-size: 1em; }")] - public void StylesheetHelperTests_ParseRules_DoesntParse(string css) + [TestCase("/** umb_name: Test\r\n2 */ p { font-size: 1em; }")] + [TestCase("/** umb_name: Test\n2 */ p { font-size: 1em; }")] + //Only a single asterisk + [TestCase("/* umb_name: Test */ p { font-size: 1em; }")] + // Has a name with spaces over multiple lines + [TestCase(@"/**UMB_NAME:Hello + +world */p{font-size: 1em;}")] + public void ParseRules_DoesntParse(string css) { // Act