From 5622bee4a96f86410c9383ff8c3b2cd40ca9162d Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Sat, 25 Jul 2026 17:21:59 +0200 Subject: [PATCH 1/2] Updated to 1.0.0 and improved doc structure --- CHANGELOG.md | 6 ++++++ docs/README.md | 8 ++++---- docs/general/07-Supported-At-Rules.md | 4 ++++ docs/general/08-Supported-Declarations.md | 4 ++++ docs/tutorials/02-Examples.md | 4 ++-- docs/tutorials/{04-Render-Tree.md => 03-Render-Tree.md} | 0 docs/tutorials/{03-Questions.md => 04-Questions.md} | 0 src/AngleSharp.Css.Docs/package.json | 2 +- src/Directory.Build.props | 2 +- 9 files changed, 22 insertions(+), 8 deletions(-) rename docs/tutorials/{04-Render-Tree.md => 03-Render-Tree.md} (100%) rename docs/tutorials/{03-Questions.md => 04-Questions.md} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dd9e08..b4f8102 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 1.0.0 + +Released on Sunday, July 26 2026. + +- First stable version + # 0.18.0 Released on Saturday, July 25 2026. diff --git a/docs/README.md b/docs/README.md index d179b64..5b45897 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,8 +17,8 @@ We have more detailed information regarding the following subjects: - [API Documentation](tutorials/01-API.md) - [Examples](tutorials/02-Examples.md) -- [FAQ](tutorials/03-Questions.md) -- [Render Tree Examples](tutorials/04-Render-Tree.md) +- [Render Tree Examples](tutorials/03-Render-Tree.md) +- [FAQ](tutorials/04-Questions.md) ## Recommended Reading Order @@ -29,9 +29,9 @@ If you are new to AngleSharp.Css, this order gives a practical ramp-up: 3. [API Documentation](tutorials/01-API.md) for deeper understanding of the CSSOM model. 4. [CSSOM](general/03-CSSOM.md) and [Core Interfaces](general/04-Core-Interfaces.md) once you need a deeper model understanding. 5. [Supported At-Rules](general/07-Supported-At-Rules.md) and [Supported Declarations](general/08-Supported-Declarations.md) when checking implementation coverage and constraints. -6. [Value Model](general/02-Values.md) and [Render Tree Examples](tutorials/04-Render-Tree.md) for style computation workflows. +6. [Value Model](general/02-Values.md) and [Render Tree Examples](tutorials/03-Render-Tree.md) for style computation workflows. 7. [Extensibility](general/05-Extensibility.md) and [Provided Services](general/06-Provided-Services.md) for custom integrations. -8. [FAQ](tutorials/03-Questions.md) for common pitfalls and output customization. +8. [FAQ](tutorials/04-Questions.md) for common pitfalls and output customization. ## What You Can Do With AngleSharp.Css diff --git a/docs/general/07-Supported-At-Rules.md b/docs/general/07-Supported-At-Rules.md index 49248c2..2157d3a 100644 --- a/docs/general/07-Supported-At-Rules.md +++ b/docs/general/07-Supported-At-Rules.md @@ -1,3 +1,7 @@ +--- +title: "Supported Rules" +section: "AngleSharp.Css" +--- # Supported At-Rules This page documents the at-rules recognized by the AngleSharp.Css parser (`CssBuilder.AtRuleMap`) and the corresponding CSSOM rule implementations. diff --git a/docs/general/08-Supported-Declarations.md b/docs/general/08-Supported-Declarations.md index 69f6526..59e14c0 100644 --- a/docs/general/08-Supported-Declarations.md +++ b/docs/general/08-Supported-Declarations.md @@ -1,3 +1,7 @@ +--- +title: "Supported Declarations" +section: "AngleSharp.Css" +--- # Supported Declarations This page documents declaration/property support in AngleSharp.Css based on the registration in `DefaultDeclarationFactory` and declaration classes in `src/AngleSharp.Css/Declarations`. diff --git a/docs/tutorials/02-Examples.md b/docs/tutorials/02-Examples.md index 6c1f690..5edffa7 100644 --- a/docs/tutorials/02-Examples.md +++ b/docs/tutorials/02-Examples.md @@ -200,5 +200,5 @@ This preserves comments, but not their exact original positions in every case. D ## 11. Where To Go Next - Read [API Documentation](01-API.md) for deeper CSSOM details. -- Read [Render Tree Examples](04-Render-Tree.md) for style-aware tree traversal and resource download workflows. -- Read [FAQ](03-Questions.md) for serializer behavior and common edge cases. +- Read [Render Tree Examples](03-Render-Tree.md) for style-aware tree traversal and resource download workflows. +- Read [FAQ](04-Questions.md) for serializer behavior and common edge cases. diff --git a/docs/tutorials/04-Render-Tree.md b/docs/tutorials/03-Render-Tree.md similarity index 100% rename from docs/tutorials/04-Render-Tree.md rename to docs/tutorials/03-Render-Tree.md diff --git a/docs/tutorials/03-Questions.md b/docs/tutorials/04-Questions.md similarity index 100% rename from docs/tutorials/03-Questions.md rename to docs/tutorials/04-Questions.md diff --git a/src/AngleSharp.Css.Docs/package.json b/src/AngleSharp.Css.Docs/package.json index 27de660..2ba7b5b 100644 --- a/src/AngleSharp.Css.Docs/package.json +++ b/src/AngleSharp.Css.Docs/package.json @@ -1,6 +1,6 @@ { "name": "@anglesharp/css", - "version": "0.18.0", + "version": "1.0.0", "preview": true, "description": "The doclet for the AngleSharp.Css documentation.", "keywords": [ diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 2443fb9..929f154 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ Extends the CSSOM from the core AngleSharp library. AngleSharp.Css - 0.18.0 + 1.0.0 enable latest true From f22dfa6c5b991aef671aa531d746bb9fde4c2b30 Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Sat, 25 Jul 2026 23:58:43 +0200 Subject: [PATCH 2/2] Performance improvements --- src/AngleSharp.Css/Parser/CssBuilder.cs | 43 +++++++--------- .../Parser/CssTokenExtensions.cs | 12 ++--- src/AngleSharp.Css/Parser/CssTokenizer.cs | 50 ++++++++++--------- .../Parser/Micro/IdentParser.cs | 40 +++++++++++++-- .../Parser/Micro/NumberParser.cs | 12 ++++- .../Parser/StringSourceExtensions.cs | 26 ++-------- .../Parser/Tokens/CssCommentToken.cs | 32 ------------ .../Parser/Tokens/CssStringToken.cs | 32 ------------ src/AngleSharp.Css/Parser/Tokens/CssToken.cs | 7 +-- .../Parser/Tokens/CssUrlToken.cs | 32 ------------ 10 files changed, 107 insertions(+), 179 deletions(-) delete mode 100644 src/AngleSharp.Css/Parser/Tokens/CssCommentToken.cs delete mode 100644 src/AngleSharp.Css/Parser/Tokens/CssStringToken.cs delete mode 100644 src/AngleSharp.Css/Parser/Tokens/CssUrlToken.cs diff --git a/src/AngleSharp.Css/Parser/CssBuilder.cs b/src/AngleSharp.Css/Parser/CssBuilder.cs index 050ebff..344369c 100644 --- a/src/AngleSharp.Css/Parser/CssBuilder.cs +++ b/src/AngleSharp.Css/Parser/CssBuilder.cs @@ -887,34 +887,13 @@ private void CollectTrivia(ICssStyleSheet owner, ref CssToken token) { if (token.Type == CssTokenType.Comment) { - var comment = NormalizeCommentData(token.Data); - _commentRuleTarget?.Add(new CssCommentRule(owner, comment)); + _commentRuleTarget?.Add(new CssCommentRule(owner, token.Data)); } token = _tokenizer.Get(); } } - private static String NormalizeCommentData(String comment) - { - if (!String.IsNullOrEmpty(comment) && comment.StartsWith("/*", StringComparison.Ordinal) && comment.EndsWith("*/", StringComparison.Ordinal)) - { - return comment.Substring(2, comment.Length - 4); - } - - if (!String.IsNullOrEmpty(comment) && comment.StartsWith("/*", StringComparison.Ordinal)) - { - return comment.Substring(2); - } - - if (!String.IsNullOrEmpty(comment) && comment.EndsWith("*/", StringComparison.Ordinal)) - { - return comment.Substring(0, comment.Length - 2); - } - - return comment; - } - private void SkipDeclarations(CssToken token) { RaiseErrorOccurred(CssParseError.InvalidToken, token.Position); @@ -932,9 +911,25 @@ private String CreateValue(ref CssToken token, out Boolean important) { var keyword = CssKeywords.BangImportant; var value = _tokenizer.ContentFrom(token.Position.Position); - important = value.EndsWith(keyword, StringComparison.OrdinalIgnoreCase); + var keywordStart = value.Length - keyword.Length; + important = keywordStart >= 0 && + value[keywordStart] == Symbols.ExclamationMark && + String.Compare(value, keywordStart, keyword, 0, keyword.Length, StringComparison.OrdinalIgnoreCase) == 0; token = NextToken(); - return important ? value.Substring(0, value.Length - keyword.Length).Trim() : value; + + if (!important) + { + return value; + } + + var end = keywordStart; + + while (end > 0 && value[end - 1].IsSpaceCharacter()) + { + end--; + } + + return value.Substring(0, end); } private String GetArgument(ref CssToken token) diff --git a/src/AngleSharp.Css/Parser/CssTokenExtensions.cs b/src/AngleSharp.Css/Parser/CssTokenExtensions.cs index 716f4e7..cfae152 100644 --- a/src/AngleSharp.Css/Parser/CssTokenExtensions.cs +++ b/src/AngleSharp.Css/Parser/CssTokenExtensions.cs @@ -9,7 +9,7 @@ namespace AngleSharp.Css.Parser /// static class CssTokenExtensions { - public static Boolean IsPotentiallyNested(this CssToken token) + public static Boolean IsPotentiallyNested(this in CssToken token) { if (token.Is(CssTokenType.Hash, CssTokenType.Colon, CssTokenType.SquareBracketOpen)) { @@ -35,7 +35,7 @@ public static Boolean IsPotentiallyNested(this CssToken token) /// The first type to match. /// The alternative match for the token. /// Result of the examination. - public static Boolean Is(this CssToken token, CssTokenType a, CssTokenType b) + public static Boolean Is(this in CssToken token, CssTokenType a, CssTokenType b) { var type = token.Type; return type == a || type == b; @@ -49,7 +49,7 @@ public static Boolean Is(this CssToken token, CssTokenType a, CssTokenType b) /// The 2nd type to match. /// The 3rd type to match. /// Result of the examination. - public static Boolean Is(this CssToken token, CssTokenType a, CssTokenType b, CssTokenType c) + public static Boolean Is(this in CssToken token, CssTokenType a, CssTokenType b, CssTokenType c) { var type = token.Type; return type == a || type == b || type == c; @@ -68,7 +68,7 @@ public static Boolean Is(this CssToken token, CssTokenType a, CssTokenType b, Cs /// The 7th type to match. /// The 8th type to match. /// Result of the examination. - public static Boolean Is(this CssToken token, CssTokenType a, CssTokenType b, CssTokenType c, CssTokenType d, CssTokenType e, CssTokenType f, CssTokenType g, CssTokenType h) + public static Boolean Is(this in CssToken token, CssTokenType a, CssTokenType b, CssTokenType c, CssTokenType d, CssTokenType e, CssTokenType f, CssTokenType g, CssTokenType h) { var type = token.Type; return type == a || type == b || type == c || type == d || type == e || type == f || type == g || type == h; @@ -82,7 +82,7 @@ public static Boolean Is(this CssToken token, CssTokenType a, CssTokenType b, Cs /// The first type to unmatch. /// The alternative unmatch for the token. /// Result of the examination. - public static Boolean IsNot(this CssToken token, CssTokenType a, CssTokenType b) + public static Boolean IsNot(this in CssToken token, CssTokenType a, CssTokenType b) { var type = token.Type; return type != a && type != b; @@ -97,7 +97,7 @@ public static Boolean IsNot(this CssToken token, CssTokenType a, CssTokenType b) /// The alternative unmatch for the token. /// The final unmatch for the token. /// Result of the examination. - public static Boolean IsNot(this CssToken token, CssTokenType a, CssTokenType b, CssTokenType c) + public static Boolean IsNot(this in CssToken token, CssTokenType a, CssTokenType b, CssTokenType c) { var type = token.Type; return type != a && type != b && type != c; diff --git a/src/AngleSharp.Css/Parser/CssTokenizer.cs b/src/AngleSharp.Css/Parser/CssTokenizer.cs index 649e072..47cd672 100644 --- a/src/AngleSharp.Css/Parser/CssTokenizer.cs +++ b/src/AngleSharp.Css/Parser/CssTokenizer.cs @@ -52,7 +52,6 @@ public CssTokenizer(TextSource source) public CssToken Get() { var current = GetNext(); - _position = GetCurrentPosition(); return Data(current); } @@ -70,27 +69,37 @@ public String ContentFrom(Int32 position) while (current != Symbols.EndOfFile) { - if (current is Symbols.DoubleQuote or Symbols.SingleQuote && previous != Symbols.ReverseSolidus) + if (current == Symbols.Semicolon || current == Symbols.CurlyBracketOpen || current == Symbols.CurlyBracketClose) + { + break; + } + + if ((current == Symbols.DoubleQuote || current == Symbols.SingleQuote) && previous != Symbols.ReverseSolidus) { trailingWhitespace = 0; var quote = current; sb.Append(current); + previous = current; current = GetNext(); while (current != Symbols.EndOfFile && current != quote) { + sb.Append(current); + if (current == Symbols.ReverseSolidus) { - sb.Append(current); + previous = current; current = GetNext(); if (current == Symbols.EndOfFile) { break; } + + sb.Append(current); } - sb.Append(current); + previous = current; current = GetNext(); } @@ -100,27 +109,23 @@ public String ContentFrom(Int32 position) previous = current; current = GetNext(); } + + continue; } - else if (current is Symbols.Semicolon or Symbols.CurlyBracketOpen or Symbols.CurlyBracketClose) + + sb.Append(current); + + if (current == Symbols.Space || current == Symbols.Tab || current == Symbols.LineFeed || current == Symbols.CarriageReturn || current == Symbols.FormFeed) { - break; + trailingWhitespace++; } else { - sb.Append(current); - - if (current.IsSpaceCharacter()) - { - trailingWhitespace++; - } - else - { - trailingWhitespace = 0; - } - - previous = current; - current = GetNext(); + trailingWhitespace = 0; } + + previous = current; + current = GetNext(); } if (trailingWhitespace > 0) @@ -603,24 +608,23 @@ private CssToken HashRest() private CssToken Comment() { var current = GetNext(); - StringBuffer.Append(Symbols.Solidus).Append(Symbols.Asterisk); while (current != Symbols.EndOfFile) { - StringBuffer.Append(current); - if (current == Symbols.Asterisk) { current = GetNext(); - StringBuffer.Append(current); if (current == Symbols.Solidus) { return NewComment(FlushBuffer()); } + + StringBuffer.Append(Symbols.Asterisk).Append(current); } else { + StringBuffer.Append(current); current = GetNext(); } } diff --git a/src/AngleSharp.Css/Parser/Micro/IdentParser.cs b/src/AngleSharp.Css/Parser/Micro/IdentParser.cs index 49799a0..53f458d 100644 --- a/src/AngleSharp.Css/Parser/Micro/IdentParser.cs +++ b/src/AngleSharp.Css/Parser/Micro/IdentParser.cs @@ -102,13 +102,25 @@ public static ICssValue ParseConstant(this StringSource source, IDictionary public static Boolean IsFunction(this StringSource source, String name) { + var pos = source.Index; var rest = source.Content.Length - source.Index; if (rest >= name.Length + 2) { + var content = source.Content; + var hasEscape = content.IndexOf(Symbols.ReverseSolidus, pos, name.Length) >= 0; + + if (!hasEscape && + content[pos + name.Length] == Symbols.RoundBracketOpen && + String.Compare(content, pos, name, 0, name.Length, StringComparison.OrdinalIgnoreCase) == 0) + { + source.NextTo(pos + name.Length + 1); + source.SkipSpacesAndComments(); + return true; + } + var length = 0; var current = source.Current; - var pos = source.Index; while (length < name.Length) { @@ -122,7 +134,7 @@ public static Boolean IsFunction(this StringSource source, String name) current = next[0]; } - if (Char.ToLowerInvariant(current) != Char.ToLowerInvariant(name[length])) + if (!EqualsIgnoreCase(current, name[length])) break; length++; @@ -245,18 +257,38 @@ public static String ParseLiteral(this StringSource source) public static String ParseAnimatableIdent(this StringSource source) { var pos = source.Index; - var test = source.ParseNormalizedIdent(); + var test = source.ParseIdent(); //TODO Replace Animatables with call to DeclarationFactory - get from flags if (test != null && (test.Isi(CssKeywords.All) || Animatables.Contains(test))) { - return test; + return test.ToLowerFast(); } source.BackTo(pos); return null; } + private static Boolean EqualsIgnoreCase(Char a, Char b) + { + if (a == b) + { + return true; + } + + if (a >= 'A' && a <= 'Z') + { + a = (Char)(a + 32); + } + + if (b >= 'A' && b <= 'Z') + { + b = (Char)(b + 32); + } + + return a == b; + } + private static String Start(StringSource source, Char current, StringBuilder buffer) { if (current == Symbols.Minus) diff --git a/src/AngleSharp.Css/Parser/Micro/NumberParser.cs b/src/AngleSharp.Css/Parser/Micro/NumberParser.cs index b3ceafa..ceed84d 100644 --- a/src/AngleSharp.Css/Parser/Micro/NumberParser.cs +++ b/src/AngleSharp.Css/Parser/Micro/NumberParser.cs @@ -171,7 +171,17 @@ public static class NumberParser } var negative = value.StartsWith("-"); - var allNumbers = (negative ? value.Substring(1) : value).All(m => m.IsDigit()); + var allNumbers = true; + var start = negative ? 1 : 0; + + for (var i = start; i < value.Length; i++) + { + if (!value[i].IsDigit()) + { + allNumbers = false; + break; + } + } if (allNumbers) { diff --git a/src/AngleSharp.Css/Parser/StringSourceExtensions.cs b/src/AngleSharp.Css/Parser/StringSourceExtensions.cs index 48ff2ff..b446317 100644 --- a/src/AngleSharp.Css/Parser/StringSourceExtensions.cs +++ b/src/AngleSharp.Css/Parser/StringSourceExtensions.cs @@ -77,9 +77,9 @@ public static String TakeUntilClosed(this StringSource source) } var end = source.Index; - source.BackTo(lastNonWhitespace); + source.Back(source.Index - lastNonWhitespace); var content = source.Substring(start); - source.NextTo(end); + source.Next(end - source.Index); return content; } @@ -127,16 +127,7 @@ public static Char SkipCurrentAndSpaces(this StringSource source) /// public static Char BackTo(this StringSource source, Int32 index) { - var diff = source.Index - index; - var current = Symbols.Null; - - while (diff > 0) - { - current = source.Back(); - diff--; - } - - return current; + return source.Back(source.Index - index); } /// @@ -144,16 +135,7 @@ public static Char BackTo(this StringSource source, Int32 index) /// public static Char NextTo(this StringSource source, Int32 index) { - var diff = index - source.Index; - var current = Symbols.Null; - - while (diff > 0) - { - current = source.Next(); - diff--; - } - - return current; + return source.Next(index - source.Index); } } } diff --git a/src/AngleSharp.Css/Parser/Tokens/CssCommentToken.cs b/src/AngleSharp.Css/Parser/Tokens/CssCommentToken.cs deleted file mode 100644 index d21ffd6..0000000 --- a/src/AngleSharp.Css/Parser/Tokens/CssCommentToken.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace AngleSharp.Css.Parser.Tokens -{ - using System; - - /// - /// Represents a CSS comment token. - /// - sealed class CssCommentToken : CssToken - { - #region Fields - - private readonly Boolean _bad; - - #endregion - - #region ctor - - public CssCommentToken(String data, Boolean bad) - : base(CssTokenType.Comment, data) - { - _bad = bad; - } - - #endregion - - #region Properties - - public Boolean IsBad => _bad; - - #endregion - } -} diff --git a/src/AngleSharp.Css/Parser/Tokens/CssStringToken.cs b/src/AngleSharp.Css/Parser/Tokens/CssStringToken.cs deleted file mode 100644 index 348c90f..0000000 --- a/src/AngleSharp.Css/Parser/Tokens/CssStringToken.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace AngleSharp.Css.Parser.Tokens -{ - using System; - - /// - /// Represents a CSS string token. - /// - sealed class CssStringToken : CssToken - { - #region Fields - - private readonly Boolean _bad; - - #endregion - - #region ctor - - public CssStringToken(String data, Boolean bad = false) - : base(CssTokenType.String, data) - { - _bad = bad; - } - - #endregion - - #region Properties - - public Boolean IsBad => _bad; - - #endregion - } -} diff --git a/src/AngleSharp.Css/Parser/Tokens/CssToken.cs b/src/AngleSharp.Css/Parser/Tokens/CssToken.cs index 7de428f..893fe3a 100644 --- a/src/AngleSharp.Css/Parser/Tokens/CssToken.cs +++ b/src/AngleSharp.Css/Parser/Tokens/CssToken.cs @@ -6,12 +6,13 @@ namespace AngleSharp.Css.Parser.Tokens /// /// The base class token for the CSS parser. /// - class CssToken + struct CssToken { #region Fields private readonly CssTokenType _type; private readonly String _data; + private TextPosition _position; #endregion @@ -33,8 +34,8 @@ public CssToken(CssTokenType type, String data) public TextPosition Position { - get; - set; + get => _position; + set => _position = value; } #endregion diff --git a/src/AngleSharp.Css/Parser/Tokens/CssUrlToken.cs b/src/AngleSharp.Css/Parser/Tokens/CssUrlToken.cs deleted file mode 100644 index bd1bfa0..0000000 --- a/src/AngleSharp.Css/Parser/Tokens/CssUrlToken.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace AngleSharp.Css.Parser.Tokens -{ - using System; - - /// - /// Represents a CSS URL token. - /// - sealed class CssUrlToken : CssToken - { - #region Fields - - private readonly Boolean _bad; - - #endregion - - #region ctor - - public CssUrlToken(String data, Boolean bad = false) - : base(CssTokenType.Url, data) - { - _bad = bad; - } - - #endregion - - #region Properties - - public Boolean IsBad => _bad; - - #endregion - } -}