diff --git a/src/AngleSharp.Css/Converters/DictionaryValueConverter.cs b/src/AngleSharp.Css/Converters/DictionaryValueConverter.cs index 163c23f..2fb831a 100644 --- a/src/AngleSharp.Css/Converters/DictionaryValueConverter.cs +++ b/src/AngleSharp.Css/Converters/DictionaryValueConverter.cs @@ -25,7 +25,7 @@ public ICssValue Convert(StringSource source) if (ident != null && _values.TryGetValue(ident, out mode)) { - return new CssConstantValue(ident.ToLowerInvariant(), mode); + return new CssConstantValue(ident.ToLowerFast(), mode); } source.BackTo(pos); diff --git a/src/AngleSharp.Css/Converters/PeriodicValueConverter.cs b/src/AngleSharp.Css/Converters/PeriodicValueConverter.cs index 1d6c554..74f571b 100644 --- a/src/AngleSharp.Css/Converters/PeriodicValueConverter.cs +++ b/src/AngleSharp.Css/Converters/PeriodicValueConverter.cs @@ -34,6 +34,11 @@ public ICssValue Convert(StringSource source) if (length > 0) { + if (length == 4) + { + return new CssPeriodicValue(options); + } + var values = new ICssValue[length]; Array.Copy(options, values, length); return new CssPeriodicValue(values); diff --git a/src/AngleSharp.Css/Dom/Internal/CssProperty.cs b/src/AngleSharp.Css/Dom/Internal/CssProperty.cs index 9bf8d3e..9092dbe 100644 --- a/src/AngleSharp.Css/Dom/Internal/CssProperty.cs +++ b/src/AngleSharp.Css/Dom/Internal/CssProperty.cs @@ -29,7 +29,7 @@ internal class CssProperty : ICssProperty internal CssProperty(String name, IValueConverter converter, PropertyFlags flags = PropertyFlags.None, ICssValue value = null, Boolean important = false) { - _name = name.StartsWith("--") ? name : name.ToLowerInvariant(); + _name = name.StartsWith("--") ? name : name.ToLowerFast(); _converter = converter; _flags = flags; _value = value; diff --git a/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs b/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs index b669bb1..ac6c1c5 100644 --- a/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs +++ b/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs @@ -19,6 +19,7 @@ sealed class CssStyleDeclaration : ICssStyleDeclaration #region Fields private readonly List _declarations; + private readonly Dictionary _declarationIndex; private readonly IBrowsingContext _context; private ICssRule _parent; private Boolean _updating; @@ -36,6 +37,7 @@ sealed class CssStyleDeclaration : ICssStyleDeclaration public CssStyleDeclaration(IBrowsingContext context) { _declarations = new List(); + _declarationIndex = new Dictionary(StringComparer.OrdinalIgnoreCase); _context = context; } @@ -77,9 +79,9 @@ public String CssText public ICssProperty GetProperty(String name) { - for (var i = 0; i < _declarations.Count; i++) + if (_declarationIndex.TryGetValue(name, out var index) && index < _declarations.Count) { - var declaration = _declarations[i]; + var declaration = _declarations[index]; if (declaration.Name.Isi(name)) { @@ -100,6 +102,7 @@ public void Update(String value) if (!_updating) { _declarations.Clear(); + _declarationIndex.Clear(); if (!String.IsNullOrEmpty(value)) { @@ -109,6 +112,7 @@ public void Update(String value) if (decl != null) { _declarations.AddRange(decl); + RebuildIndex(); } } } @@ -304,12 +308,14 @@ public void SetProperty(String propertyName, String propertyValue, String priori public void AddProperty(ICssProperty declaration) { + _declarationIndex[declaration.Name] = _declarations.Count; _declarations.Add(declaration); } public void RemoveProperty(ICssProperty declaration) { _declarations.Remove(declaration); + RebuildIndex(); } #endregion @@ -359,14 +365,24 @@ private void RemovePropertyByName(String propertyName) var info = _context.GetDeclarationInfo(propertyName); var longhands = info.Longhands; - for (var i = 0; i < _declarations.Count; i++) + if (_declarationIndex.TryGetValue(propertyName, out var index) && index < _declarations.Count && + _declarations[index].Name.Is(propertyName)) { - var declaration = _declarations[i]; - - if (declaration.Name.Is(propertyName)) + _declarations.RemoveAt(index); + RebuildIndex(); + } + else + { + for (var i = 0; i < _declarations.Count; i++) { - _declarations.RemoveAt(i); - break; + var declaration = _declarations[i]; + + if (declaration.Name.Is(propertyName)) + { + _declarations.RemoveAt(i); + RebuildIndex(); + break; + } } } @@ -389,23 +405,39 @@ private void ChangeDeclarations(IEnumerable decls, Predicate decls, Predicate + /// Converts to lowercase, returning the original string if it is already lowercase. + /// Avoids allocation for the common case of already-lowercase CSS identifiers. + /// + internal static String ToLowerFast(this String value) + { + for (var i = 0; i < value.Length; i++) + { + var c = value[i]; + + if (c >= 'A' && c <= 'Z') + { + return value.ToLowerInvariant(); + } + } + + return value; + } } } diff --git a/src/AngleSharp.Css/Parser/CssBuilder.cs b/src/AngleSharp.Css/Parser/CssBuilder.cs index 1c78ea0..687789b 100644 --- a/src/AngleSharp.Css/Parser/CssBuilder.cs +++ b/src/AngleSharp.Css/Parser/CssBuilder.cs @@ -1,11 +1,13 @@ #nullable disable namespace AngleSharp.Css.Parser { + using AngleSharp.Common; using AngleSharp.Css.Dom; using AngleSharp.Css.Parser.Tokens; using AngleSharp.Dom; using AngleSharp.Text; using System; + using System.Collections.Generic; /// /// See http://dev.w3.org/csswg/css-syntax/#parsing for details. @@ -14,6 +16,22 @@ sealed class CssBuilder { #region Fields + private static readonly Dictionary AtRuleMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { RuleNames.Media, 1 }, + { RuleNames.FontFace, 2 }, + { RuleNames.Keyframes, 3 }, + { RuleNames.Import, 4 }, + { RuleNames.Charset, 5 }, + { RuleNames.Namespace, 6 }, + { RuleNames.Page, 7 }, + { RuleNames.Supports, 8 }, + { RuleNames.ViewPort, 9 }, + { RuleNames.Document, 10 }, + { RuleNames.CounterStyle, 11 }, + { RuleNames.FontFeatureValues, 12 }, + }; + private readonly CssTokenizer _tokenizer; private readonly CssParserOptions _options; private readonly IBrowsingContext _context; @@ -67,67 +85,26 @@ private ICssRule CreateStyleRule(ICssStyleSheet sheet, CssToken token) private ICssRule CreateAtRule(ICssStyleSheet sheet, CssToken token) { - if (token.Data.Is(RuleNames.Media)) - { - var rule = new CssMediaRule(sheet); - return CreateMedia(rule, token); - } - else if (token.Data.Is(RuleNames.FontFace)) - { - var rule = new CssFontFaceRule(sheet); - return CreateFontFace(rule, token); - } - else if (token.Data.Is(RuleNames.Keyframes)) - { - var rule = new CssKeyframesRule(sheet); - return CreateKeyframes(rule, token); - } - else if (token.Data.Is(RuleNames.Import)) - { - var rule = new CssImportRule(sheet); - return CreateImport(rule, token); - } - else if (token.Data.Is(RuleNames.Charset)) - { - var rule = new CssCharsetRule(sheet); - return CreateCharset(rule, token); - } - else if (token.Data.Is(RuleNames.Namespace)) - { - var rule = new CssNamespaceRule(sheet); - return CreateNamespace(rule, token); - } - else if (token.Data.Is(RuleNames.Page)) - { - var rule = new CssPageRule(sheet); - return CreatePage(rule, token); - } - else if (token.Data.Is(RuleNames.Supports)) + if (AtRuleMap.TryGetValue(token.Data, out var ruleId)) { - var rule = new CssSupportsRule(sheet); - return CreateSupports(rule, token); - } - else if (token.Data.Is(RuleNames.ViewPort)) - { - var rule = new CssViewportRule(sheet); - return CreateViewport(rule, token); - } - else if (token.Data.Is(RuleNames.Document)) - { - var rule = new CssDocumentRule(sheet); - return CreateDocument(rule, token); - } - else if (token.Data.Is(RuleNames.CounterStyle)) - { - var rule = new CssCounterStyleRule(sheet); - return CreateCounterStyle(rule, token); - } - else if (token.Data.Is(RuleNames.FontFeatureValues)) - { - var rule = new CssFontFeatureValuesRule(sheet); - return CreateFontFeatureValues(rule, token); + switch (ruleId) + { + case 1: return CreateMedia(new CssMediaRule(sheet), token); + case 2: return CreateFontFace(new CssFontFaceRule(sheet), token); + case 3: return CreateKeyframes(new CssKeyframesRule(sheet), token); + case 4: return CreateImport(new CssImportRule(sheet), token); + case 5: return CreateCharset(new CssCharsetRule(sheet), token); + case 6: return CreateNamespace(new CssNamespaceRule(sheet), token); + case 7: return CreatePage(new CssPageRule(sheet), token); + case 8: return CreateSupports(new CssSupportsRule(sheet), token); + case 9: return CreateViewport(new CssViewportRule(sheet), token); + case 10: return CreateDocument(new CssDocumentRule(sheet), token); + case 11: return CreateCounterStyle(new CssCounterStyleRule(sheet), token); + case 12: return CreateFontFeatureValues(new CssFontFeatureValuesRule(sheet), token); + } } - else if (_options.IsIncludingUnknownRules) + + if (_options.IsIncludingUnknownRules) { return CreateUnknownAtRule(sheet, token); } @@ -556,10 +533,18 @@ public void CreateDeclarationWith(ICssProperties properties, ref CssToken token) { var name = token.Data; - while (token.Type == CssTokenType.Delim) + if (token.Type == CssTokenType.Delim) { - token = NextToken(); - name += token.Data; + var sb = StringBuilderPool.Obtain(); + sb.Append(name); + + while (token.Type == CssTokenType.Delim) + { + token = NextToken(); + sb.Append(token.Data); + } + + name = sb.ToPool(); } token = NextToken(); diff --git a/src/AngleSharp.Css/Parser/CssTokenizer.cs b/src/AngleSharp.Css/Parser/CssTokenizer.cs index a5220ca..649e072 100644 --- a/src/AngleSharp.Css/Parser/CssTokenizer.cs +++ b/src/AngleSharp.Css/Parser/CssTokenizer.cs @@ -65,44 +65,67 @@ public String ContentFrom(Int32 position) var sb = StringBuilderPool.Obtain(); Back(Position - position); var current = Current; - var spaced = 0; + var trailingWhitespace = 0; + var previous = Symbols.EndOfFile; - while (!(current is Symbols.EndOfFile or Symbols.Semicolon or Symbols.CurlyBracketOpen or Symbols.CurlyBracketClose)) + while (current != Symbols.EndOfFile) { - var token = Data(current); - - if (Current is Symbols.EndOfFile) - { - Back(); - } - - if (token.Type == CssTokenType.Whitespace) + if (current is Symbols.DoubleQuote or Symbols.SingleQuote && previous != Symbols.ReverseSolidus) { - spaced++; + trailingWhitespace = 0; + var quote = current; sb.Append(current); current = GetNext(); + + while (current != Symbols.EndOfFile && current != quote) + { + if (current == Symbols.ReverseSolidus) + { + sb.Append(current); + current = GetNext(); + + if (current == Symbols.EndOfFile) + { + break; + } + } + + sb.Append(current); + current = GetNext(); + } + + if (current != Symbols.EndOfFile) + { + sb.Append(current); + previous = current; + current = GetNext(); + } + } + else if (current is Symbols.Semicolon or Symbols.CurlyBracketOpen or Symbols.CurlyBracketClose) + { + break; } else { - var length = Position - position; - Back(length++); - current = Current; - spaced = 0; + sb.Append(current); - while (length > 0) + if (current.IsSpaceCharacter()) { - sb.Append(current); - --length; - current = GetNext(); + trailingWhitespace++; } + else + { + trailingWhitespace = 0; + } + + previous = current; + current = GetNext(); } - - position = Position; } - if (spaced > 0) + if (trailingWhitespace > 0) { - sb.Remove(sb.Length - spaced, spaced); + sb.Remove(sb.Length - trailingWhitespace, trailingWhitespace); } Back(); @@ -1339,7 +1362,7 @@ private CssToken NewOpenRound() private CssToken NewString(String value, Boolean bad = false) { - return new CssStringToken(value, bad) { Position = _position }; + return new CssToken(CssTokenType.String, value) { Position = _position }; } private CssToken NewHash(String data) @@ -1349,7 +1372,7 @@ private CssToken NewHash(String data) private CssToken NewComment(String data, Boolean bad = false) { - return new CssCommentToken(data, bad) { Position = _position }; + return new CssToken(CssTokenType.Comment, data) { Position = _position }; } private CssToken NewAtKeyword(String data) @@ -1379,7 +1402,7 @@ private CssToken NewDimension(String data) private CssToken NewUrl(String data, Boolean bad = false) { - return new CssUrlToken(data, bad) { Position = _position }; + return new CssToken(CssTokenType.Url, data) { Position = _position }; } private CssToken NewRange(String data) @@ -1389,7 +1412,7 @@ private CssToken NewRange(String data) private CssToken NewWhitespace(Char c) { - return new CssToken(CssTokenType.Whitespace, c.ToString()) { Position = _position }; + return new CssToken(CssTokenType.Whitespace, CachedCharString(c)) { Position = _position }; } private CssToken NewNumber(String data) @@ -1399,7 +1422,26 @@ private CssToken NewNumber(String data) private CssToken NewDelimiter(Char c) { - return new CssToken(CssTokenType.Delim, c.ToString()) { Position = _position }; + return new CssToken(CssTokenType.Delim, CachedCharString(c)) { Position = _position }; + } + + private static readonly String[] CharStringCache = CreateCharStringCache(); + + private static String[] CreateCharStringCache() + { + var cache = new String[128]; + + for (var i = 0; i < cache.Length; i++) + { + cache[i] = ((Char)i).ToString(); + } + + return cache; + } + + private static String CachedCharString(Char c) + { + return c < 128 ? CharStringCache[c] : c.ToString(); } private CssToken NewEof() diff --git a/src/AngleSharp.Css/Parser/Micro/IdentParser.cs b/src/AngleSharp.Css/Parser/Micro/IdentParser.cs index 4258188..49799a0 100644 --- a/src/AngleSharp.Css/Parser/Micro/IdentParser.cs +++ b/src/AngleSharp.Css/Parser/Micro/IdentParser.cs @@ -19,7 +19,7 @@ public static class IdentParser public static String ParseNormalizedIdent(this StringSource source) { var result = source.ParseIdent(); - return result != null ? result.ToLowerInvariant() : result; + return result != null ? result.ToLowerFast() : result; } /// @@ -75,7 +75,7 @@ public static ICssValue ParseConstant(this StringSource source, IDictionary(ident.ToLowerInvariant(), mode); + return mode as ICssValue ?? new CssConstantValue(ident.ToLowerFast(), mode); } source.BackTo(pos); @@ -91,7 +91,7 @@ public static ICssValue ParseConstant(this StringSource source, IDictionary(ident.ToLowerInvariant(), mode); + return new CssConstantValue(ident.ToLowerFast(), mode); } return null;