Browse Source

升级包

懒得勤快 8 months ago
parent
commit
2d3fb6671a

+ 227 - 228
Masuit.Tools.Abstractions/HtmlSanitizer/EventArgs.cs

@@ -5,271 +5,270 @@ using System;
 using System.Collections.Generic;
 using System.ComponentModel;
 
-namespace Ganss.Xss
+namespace Ganss.Xss;
+
+/// <summary>
+/// Provides data for the <see cref="HtmlSanitizer.PostProcessDom"/> event.
+/// </summary>
+/// <remarks>
+/// Initializes a new instance of the <see cref="PostProcessDomEventArgs"/> class.
+/// </remarks>
+public class PostProcessDomEventArgs(IHtmlDocument document) : EventArgs
 {
     /// <summary>
-    /// Provides data for the <see cref="HtmlSanitizer.PostProcessDom"/> event.
+    /// Gets the document.
     /// </summary>
-    /// <remarks>
-    /// Initializes a new instance of the <see cref="PostProcessDomEventArgs"/> class.
-    /// </remarks>
-    public class PostProcessDomEventArgs(IHtmlDocument document) : EventArgs
-    {
-        /// <summary>
-        /// Gets the document.
-        /// </summary>
-        /// <value>
-        /// The document.
-        /// </value>
-        public IHtmlDocument Document { get; private set; } = document;
-    }
+    /// <value>
+    /// The document.
+    /// </value>
+    public IHtmlDocument Document { get; private set; } = document;
+}
 
+/// <summary>
+/// Provides data for the <see cref="HtmlSanitizer.PostProcessNode"/> event.
+/// </summary>
+/// <remarks>
+/// Initializes a new instance of the <see cref="PostProcessNodeEventArgs"/> class.
+/// </remarks>
+public class PostProcessNodeEventArgs(IHtmlDocument document, INode node) : EventArgs
+{
     /// <summary>
-    /// Provides data for the <see cref="HtmlSanitizer.PostProcessNode"/> event.
+    /// Gets the document.
     /// </summary>
-    /// <remarks>
-    /// Initializes a new instance of the <see cref="PostProcessNodeEventArgs"/> class.
-    /// </remarks>
-    public class PostProcessNodeEventArgs(IHtmlDocument document, INode node) : EventArgs
-    {
-        /// <summary>
-        /// Gets the document.
-        /// </summary>
-        /// <value>
-        /// The document.
-        /// </value>
-        public IHtmlDocument Document { get; private set; } = document;
+    /// <value>
+    /// The document.
+    /// </value>
+    public IHtmlDocument Document { get; private set; } = document;
 
-        /// <summary>
-        /// Gets the DOM node to be processed.
-        /// </summary>
-        /// <value>
-        /// The DOM node.
-        /// </value>
-        public INode Node { get; private set; } = node;
+    /// <summary>
+    /// Gets the DOM node to be processed.
+    /// </summary>
+    /// <value>
+    /// The DOM node.
+    /// </value>
+    public INode Node { get; private set; } = node;
 
-        /// <summary>
-        /// Gets the replacement nodes. Leave empty if no replacement should occur.
-        /// </summary>
-        /// <value>
-        /// The replacement nodes.
-        /// </value>
-        public ICollection<INode> ReplacementNodes { get; private set; } = new List<INode>();
-    }
+    /// <summary>
+    /// Gets the replacement nodes. Leave empty if no replacement should occur.
+    /// </summary>
+    /// <value>
+    /// The replacement nodes.
+    /// </value>
+    public ICollection<INode> ReplacementNodes { get; private set; } = [];
+}
 
+/// <summary>
+/// Provides data for the <see cref="HtmlSanitizer.RemovingTag"/> event.
+/// </summary>
+/// <remarks>
+/// Initializes a new instance of the <see cref="RemovingTagEventArgs"/> class.
+/// </remarks>
+/// <param name="tag">The element to be removed.</param>
+/// <param name="reason">The reason why the tag will be removed.</param>
+public class RemovingTagEventArgs(IElement tag, RemoveReason reason) : CancelEventArgs
+{
     /// <summary>
-    /// Provides data for the <see cref="HtmlSanitizer.RemovingTag"/> event.
+    /// Gets the tag to be removed.
     /// </summary>
-    /// <remarks>
-    /// Initializes a new instance of the <see cref="RemovingTagEventArgs"/> class.
-    /// </remarks>
-    /// <param name="tag">The element to be removed.</param>
-    /// <param name="reason">The reason why the tag will be removed.</param>
-    public class RemovingTagEventArgs(IElement tag, RemoveReason reason) : CancelEventArgs
-    {
-        /// <summary>
-        /// Gets the tag to be removed.
-        /// </summary>
-        /// <value>
-        /// The tag.
-        /// </value>
-        public IElement Tag { get; private set; } = tag;
+    /// <value>
+    /// The tag.
+    /// </value>
+    public IElement Tag { get; private set; } = tag;
 
-        /// <summary>
-        /// Gets the reason why the tag will be removed.
-        /// </summary>
-        /// <value>
-        /// The reason.
-        /// </value>
-        public RemoveReason Reason { get; private set; } = reason;
-    }
+    /// <summary>
+    /// Gets the reason why the tag will be removed.
+    /// </summary>
+    /// <value>
+    /// The reason.
+    /// </value>
+    public RemoveReason Reason { get; private set; } = reason;
+}
 
+/// <summary>
+/// Provides data for the <see cref="HtmlSanitizer.RemovingAttribute"/> event.
+/// </summary>
+/// <remarks>
+/// Initializes a new instance of the <see cref="RemovingAttributeEventArgs"/> class.
+/// </remarks>
+/// <param name="tag">The element containing the attribute.</param>
+/// <param name="attribute">The attribute to be removed.</param>
+/// <param name="reason">The reason why the attribute will be removed.</param>
+public class RemovingAttributeEventArgs(IElement tag, IAttr attribute, RemoveReason reason) : CancelEventArgs
+{
     /// <summary>
-    /// Provides data for the <see cref="HtmlSanitizer.RemovingAttribute"/> event.
+    /// Gets the tag containing the attribute to be removed.
     /// </summary>
-    /// <remarks>
-    /// Initializes a new instance of the <see cref="RemovingAttributeEventArgs"/> class.
-    /// </remarks>
-    /// <param name="tag">The element containing the attribute.</param>
-    /// <param name="attribute">The attribute to be removed.</param>
-    /// <param name="reason">The reason why the attribute will be removed.</param>
-    public class RemovingAttributeEventArgs(IElement tag, IAttr attribute, RemoveReason reason) : CancelEventArgs
-    {
-        /// <summary>
-        /// Gets the tag containing the attribute to be removed.
-        /// </summary>
-        /// <value>
-        /// The tag.
-        /// </value>
-        public IElement Tag { get; private set; } = tag;
+    /// <value>
+    /// The tag.
+    /// </value>
+    public IElement Tag { get; private set; } = tag;
 
-        /// <summary>
-        /// Gets the attribute to be removed.
-        /// </summary>
-        /// <value>
-        /// The attribute.
-        /// </value>
-        public IAttr Attribute { get; private set; } = attribute;
+    /// <summary>
+    /// Gets the attribute to be removed.
+    /// </summary>
+    /// <value>
+    /// The attribute.
+    /// </value>
+    public IAttr Attribute { get; private set; } = attribute;
 
-        /// <summary>
-        /// Gets the reason why the attribute will be removed.
-        /// </summary>
-        /// <value>
-        /// The reason.
-        /// </value>
-        public RemoveReason Reason { get; private set; } = reason;
-    }
+    /// <summary>
+    /// Gets the reason why the attribute will be removed.
+    /// </summary>
+    /// <value>
+    /// The reason.
+    /// </value>
+    public RemoveReason Reason { get; private set; } = reason;
+}
 
+/// <summary>
+/// Provides data for the <see cref="HtmlSanitizer.RemovingStyle"/> event.
+/// </summary>
+/// <remarks>
+/// Initializes a new instance of the <see cref="RemovingStyleEventArgs"/> class.
+/// </remarks>
+/// <param name="tag">The element containing the attribute.</param>
+/// <param name="style">The style to be removed.</param>
+/// <param name="reason">The reason why the attribute will be removed.</param>
+public class RemovingStyleEventArgs(IElement tag, ICssProperty style, RemoveReason reason) : CancelEventArgs
+{
     /// <summary>
-    /// Provides data for the <see cref="HtmlSanitizer.RemovingStyle"/> event.
+    /// Gets the tag containing the style to be removed.
     /// </summary>
-    /// <remarks>
-    /// Initializes a new instance of the <see cref="RemovingStyleEventArgs"/> class.
-    /// </remarks>
-    /// <param name="tag">The element containing the attribute.</param>
-    /// <param name="style">The style to be removed.</param>
-    /// <param name="reason">The reason why the attribute will be removed.</param>
-    public class RemovingStyleEventArgs(IElement tag, ICssProperty style, RemoveReason reason) : CancelEventArgs
-    {
-        /// <summary>
-        /// Gets the tag containing the style to be removed.
-        /// </summary>
-        /// <value>
-        /// The tag.
-        /// </value>
-        public IElement Tag { get; private set; } = tag;
+    /// <value>
+    /// The tag.
+    /// </value>
+    public IElement Tag { get; private set; } = tag;
 
-        /// <summary>
-        /// Gets the style to be removed.
-        /// </summary>
-        /// <value>
-        /// The style.
-        /// </value>
-        public ICssProperty Style { get; private set; } = style;
+    /// <summary>
+    /// Gets the style to be removed.
+    /// </summary>
+    /// <value>
+    /// The style.
+    /// </value>
+    public ICssProperty Style { get; private set; } = style;
 
-        /// <summary>
-        /// Gets the reason why the style will be removed.
-        /// </summary>
-        /// <value>
-        /// The reason.
-        /// </value>
-        public RemoveReason Reason { get; private set; } = reason;
-    }
+    /// <summary>
+    /// Gets the reason why the style will be removed.
+    /// </summary>
+    /// <value>
+    /// The reason.
+    /// </value>
+    public RemoveReason Reason { get; private set; } = reason;
+}
 
+/// <summary>
+/// Provides data for the <see cref="HtmlSanitizer.RemovingAtRule"/> event.
+/// </summary>
+/// <remarks>
+/// Initializes a new instance of the <see cref="RemovingAtRuleEventArgs"/> class.
+/// </remarks>
+/// <param name="tag">The element containing the attribute.</param>
+/// <param name="rule">The rule to be removed.</param>
+public class RemovingAtRuleEventArgs(IElement tag, ICssRule rule) : CancelEventArgs
+{
     /// <summary>
-    /// Provides data for the <see cref="HtmlSanitizer.RemovingAtRule"/> event.
+    /// Gets the tag containing the at-rule to be removed.
     /// </summary>
-    /// <remarks>
-    /// Initializes a new instance of the <see cref="RemovingAtRuleEventArgs"/> class.
-    /// </remarks>
-    /// <param name="tag">The element containing the attribute.</param>
-    /// <param name="rule">The rule to be removed.</param>
-    public class RemovingAtRuleEventArgs(IElement tag, ICssRule rule) : CancelEventArgs
-    {
-        /// <summary>
-        /// Gets the tag containing the at-rule to be removed.
-        /// </summary>
-        /// <value>
-        /// The tag.
-        /// </value>
-        public IElement Tag { get; private set; } = tag;
+    /// <value>
+    /// The tag.
+    /// </value>
+    public IElement Tag { get; private set; } = tag;
 
-        /// <summary>
-        /// Gets the rule to be removed.
-        /// </summary>
-        /// <value>
-        /// The rule.
-        /// </value>
-        public ICssRule Rule { get; private set; } = rule;
-    }
+    /// <summary>
+    /// Gets the rule to be removed.
+    /// </summary>
+    /// <value>
+    /// The rule.
+    /// </value>
+    public ICssRule Rule { get; private set; } = rule;
+}
 
+/// <summary>
+/// Provides data for the <see cref="HtmlSanitizer.RemovingComment"/> event.
+/// </summary>
+/// <remarks>
+/// Initializes a new instance of the <see cref="RemovingCommentEventArgs"/> class.
+/// </remarks>
+/// <param name="comment">The comment to be removed.</param>
+public class RemovingCommentEventArgs(IComment comment) : CancelEventArgs
+{
     /// <summary>
-    /// Provides data for the <see cref="HtmlSanitizer.RemovingComment"/> event.
+    /// Gets the comment node to be removed.
     /// </summary>
-    /// <remarks>
-    /// Initializes a new instance of the <see cref="RemovingCommentEventArgs"/> class.
-    /// </remarks>
-    /// <param name="comment">The comment to be removed.</param>
-    public class RemovingCommentEventArgs(IComment comment) : CancelEventArgs
-    {
-        /// <summary>
-        /// Gets the comment node to be removed.
-        /// </summary>
-        /// <value>
-        /// The comment node.
-        /// </value>
-        public IComment Comment { get; private set; } = comment;
-    }
+    /// <value>
+    /// The comment node.
+    /// </value>
+    public IComment Comment { get; private set; } = comment;
+}
 
+/// <summary>
+/// Provides data for the <see cref="HtmlSanitizer.RemovingCssClass"/> event.
+/// </summary>
+/// <remarks>
+/// Initializes a new instance of the <see cref="RemovingCssClassEventArgs"/> class.
+/// </remarks>
+/// <param name="tag">The element containing the attribute.</param>
+/// <param name="cssClass">The CSS class to be removed.</param>
+/// <param name="reason">The reason why the attribute will be removed.</param>
+public class RemovingCssClassEventArgs(IElement tag, string cssClass, RemoveReason reason) : CancelEventArgs
+{
     /// <summary>
-    /// Provides data for the <see cref="HtmlSanitizer.RemovingCssClass"/> event.
+    /// Gets the tag containing the CSS class to be removed.
     /// </summary>
-    /// <remarks>
-    /// Initializes a new instance of the <see cref="RemovingCssClassEventArgs"/> class.
-    /// </remarks>
-    /// <param name="tag">The element containing the attribute.</param>
-    /// <param name="cssClass">The CSS class to be removed.</param>
-    /// <param name="reason">The reason why the attribute will be removed.</param>
-    public class RemovingCssClassEventArgs(IElement tag, string cssClass, RemoveReason reason) : CancelEventArgs
-    {
-        /// <summary>
-        /// Gets the tag containing the CSS class to be removed.
-        /// </summary>
-        /// <value>
-        /// The tag.
-        /// </value>
-        public IElement Tag { get; private set; } = tag;
+    /// <value>
+    /// The tag.
+    /// </value>
+    public IElement Tag { get; private set; } = tag;
 
-        /// <summary>
-        /// Gets the CSS class to be removed.
-        /// </summary>
-        /// <value>
-        /// The CSS class.
-        /// </value>
-        public string CssClass { get; private set; } = cssClass;
+    /// <summary>
+    /// Gets the CSS class to be removed.
+    /// </summary>
+    /// <value>
+    /// The CSS class.
+    /// </value>
+    public string CssClass { get; private set; } = cssClass;
 
-        /// <summary>
-        /// Gets the reason why the CSS class will be removed.
-        /// </summary>
-        /// <value>
-        /// The reason.
-        /// </value>
-        public RemoveReason Reason { get; private set; } = reason;
-    }
+    /// <summary>
+    /// Gets the reason why the CSS class will be removed.
+    /// </summary>
+    /// <value>
+    /// The reason.
+    /// </value>
+    public RemoveReason Reason { get; private set; } = reason;
+}
 
+/// <summary>
+/// Provides data for the <see cref="HtmlSanitizer.FilterUrl"/> event.
+/// </summary>
+/// <remarks>
+/// Initializes a new instance of the <see cref="FilterUrlEventArgs"/> class.
+/// </remarks>
+/// <param name="tag">The tag containing the URI being sanitized.</param>
+/// <param name="originalUrl">The original URL.</param>
+/// <param name="sanitizedUrl">The sanitized URL.</param>
+public class FilterUrlEventArgs(IElement tag, string originalUrl, string? sanitizedUrl = null) : EventArgs
+{
     /// <summary>
-    /// Provides data for the <see cref="HtmlSanitizer.FilterUrl"/> event.
+    /// Gets the original URL.
     /// </summary>
-    /// <remarks>
-    /// Initializes a new instance of the <see cref="FilterUrlEventArgs"/> class.
-    /// </remarks>
-    /// <param name="tag">The tag containing the URI being sanitized.</param>
-    /// <param name="originalUrl">The original URL.</param>
-    /// <param name="sanitizedUrl">The sanitized URL.</param>
-    public class FilterUrlEventArgs(IElement tag, string originalUrl, string? sanitizedUrl = null) : EventArgs
-    {
-        /// <summary>
-        /// Gets the original URL.
-        /// </summary>
-        /// <value>
-        /// The original URL.
-        /// </value>
-        public string OriginalUrl { get; private set; } = originalUrl;
+    /// <value>
+    /// The original URL.
+    /// </value>
+    public string OriginalUrl { get; private set; } = originalUrl;
 
-        /// <summary>
-        /// Gets or sets the sanitized URL.
-        /// </summary>
-        /// <value>
-        /// The sanitized URL. If it is null, it will be removed.
-        /// </value>
-        public string? SanitizedUrl { get; set; } = sanitizedUrl;
+    /// <summary>
+    /// Gets or sets the sanitized URL.
+    /// </summary>
+    /// <value>
+    /// The sanitized URL. If it is null, it will be removed.
+    /// </value>
+    public string? SanitizedUrl { get; set; } = sanitizedUrl;
 
-        /// <summary>
-        /// Gets the tag containing the URI being sanitized.
-        /// </summary>
-        /// <value>
-        /// The tag.
-        /// </value>
-        public IElement Tag { get; private set; } = tag;
-    }
+    /// <summary>
+    /// Gets the tag containing the URI being sanitized.
+    /// </summary>
+    /// <value>
+    /// The tag.
+    /// </value>
+    public IElement Tag { get; private set; } = tag;
 }

+ 51 - 56
Masuit.Tools.Abstractions/HtmlSanitizer/HtmlFormatter.cs

@@ -1,74 +1,69 @@
-using AngleSharp;
-using AngleSharp.Html;
+using AngleSharp.Html;
 using AngleSharp.Dom;
 using System;
-using System.Collections.Generic;
-using System.Linq;
 using System.Text;
-using System.Threading.Tasks;
 
-namespace Ganss.Xss
+namespace Ganss.Xss;
+
+/// <summary>
+/// HTML5 markup formatter. Identical to <see cref="HtmlMarkupFormatter"/> except for &lt; and &gt; which are
+/// encoded in attribute values.
+/// </summary>
+public class HtmlFormatter: HtmlMarkupFormatter
 {
     /// <summary>
-    /// HTML5 markup formatter. Identical to <see cref="HtmlMarkupFormatter"/> except for &lt; and &gt; which are
-    /// encoded in attribute values.
+    /// An instance of <see cref="HtmlFormatter"/>.
     /// </summary>
-    public class HtmlFormatter: HtmlMarkupFormatter
-    {
-        /// <summary>
-        /// An instance of <see cref="HtmlFormatter"/>.
-        /// </summary>
-        new public static readonly HtmlFormatter Instance = new ();
+    new public static readonly HtmlFormatter Instance = new ();
 
-        // disable XML comments warnings
-        #pragma warning disable 1591
+    // disable XML comments warnings
+    #pragma warning disable 1591
 
-        protected override string Attribute(IAttr attr)
-        {
-            var namespaceUri = attr.NamespaceUri;
-            var localName = attr.LocalName;
-            var value = attr.Value;
-            var temp = new StringBuilder();
+    protected override string Attribute(IAttr attr)
+    {
+        var namespaceUri = attr.NamespaceUri;
+        var localName = attr.LocalName;
+        var value = attr.Value;
+        var temp = new StringBuilder();
 
-            if (String.IsNullOrEmpty(namespaceUri))
-            {
-                temp.Append(localName);
-            }
-            else if (namespaceUri == NamespaceNames.XmlUri)
-            {
-                temp.Append(NamespaceNames.XmlPrefix).Append(':').Append(localName);
-            }
-            else if (namespaceUri == NamespaceNames.XLinkUri)
-            {
-                temp.Append(NamespaceNames.XLinkPrefix).Append(':').Append(localName);
-            }
-            else if (namespaceUri == NamespaceNames.XmlNsUri)
-            {
-                temp.Append(XmlNamespaceLocalName(localName));
-            }
-            else
-            {
-                temp.Append(attr.Name);
-            }
+        if (String.IsNullOrEmpty(namespaceUri))
+        {
+            temp.Append(localName);
+        }
+        else if (namespaceUri == NamespaceNames.XmlUri)
+        {
+            temp.Append(NamespaceNames.XmlPrefix).Append(':').Append(localName);
+        }
+        else if (namespaceUri == NamespaceNames.XLinkUri)
+        {
+            temp.Append(NamespaceNames.XLinkPrefix).Append(':').Append(localName);
+        }
+        else if (namespaceUri == NamespaceNames.XmlNsUri)
+        {
+            temp.Append(XmlNamespaceLocalName(localName));
+        }
+        else
+        {
+            temp.Append(attr.Name);
+        }
 
-            temp.Append('=').Append('"');
+        temp.Append('=').Append('"');
 
-            for (var i = 0; i < value.Length; i++)
+        for (var i = 0; i < value.Length; i++)
+        {
+            switch (value[i])
             {
-                switch (value[i])
-                {
-                    case '&': temp.Append("&amp;"); break;
-                    case '\u00a0': temp.Append("&nbsp;"); break;
-                    case '"': temp.Append("&quot;"); break;
-                    case '<': temp.Append("&lt;"); break;
-                    case '>': temp.Append("&gt;"); break;
-                    default: temp.Append(value[i]); break;
-                }
+                case '&': temp.Append("&amp;"); break;
+                case '\u00a0': temp.Append("&nbsp;"); break;
+                case '"': temp.Append("&quot;"); break;
+                case '<': temp.Append("&lt;"); break;
+                case '>': temp.Append("&gt;"); break;
+                default: temp.Append(value[i]); break;
             }
-
-            return temp.Append('"').ToString();
         }
 
-        #pragma warning restore 1591
+        return temp.Append('"').ToString();
     }
+
+    #pragma warning restore 1591
 }

+ 801 - 802
Masuit.Tools.Abstractions/HtmlSanitizer/HtmlSanitizer.cs

@@ -13,969 +13,968 @@ using System.Linq;
 using System.Text;
 using System.Text.RegularExpressions;
 
-namespace Ganss.Xss
+namespace Ganss.Xss;
+
+/// <summary>
+/// Cleans HTML documents and fragments from constructs that can lead to <a href="https://en.wikipedia.org/wiki/Cross-site_scripting">XSS attacks</a>.
+/// </summary>
+/// <remarks>
+/// XSS attacks can occur at several levels within an HTML document or fragment:
+/// <list type="bullet">
+/// <item>HTML tags (e.g. the &lt;script&gt; tag)</item>
+/// <item>HTML attributes (e.g. the "onload" attribute)</item>
+/// <item>CSS styles (url property values)</item>
+/// <item>malformed HTML or HTML that exploits parser bugs in specific browsers</item>
+/// </list>
+/// <para>
+/// The HtmlSanitizer class addresses all of these possible attack vectors by using a sophisticated HTML parser (<a href="https://github.com/AngleSharp/AngleSharp">AngleSharp</a>).
+/// </para>
+/// <para>
+/// In order to facilitate different use cases, HtmlSanitizer can be customized at the levels mentioned above:
+/// <list type="bullet">
+/// <item>You can specify the allowed HTML tags through the property <see cref="AllowedTags"/>. All other tags will be stripped.</item>
+/// <item>You can specify the allowed HTML attributes through the property <see cref="AllowedAttributes"/>. All other attributes will be stripped.</item>
+/// <item>You can specify the allowed CSS property names through the property <see cref="AllowedCssProperties"/>. All other styles will be stripped.</item>
+/// <item>You can specify the allowed URI schemes through the property <see cref="AllowedSchemes"/>. All other URIs will be stripped.</item>
+/// <item>You can specify the HTML attributes that contain URIs (such as "src", "href" etc.) through the property <see cref="UriAttributes"/>.</item>
+/// </list>
+/// </para>
+/// </remarks>
+/// <example>
+/// <code>
+/// <![CDATA[
+/// var sanitizer = new HtmlSanitizer();
+/// var html = @"<script>alert('xss')</script><div onload=""alert('xss')"" style=""background-color: test"">Test<img src=""test.gif"" style=""background-image: url(javascript:alert('xss')); margin: 10px""></div>";
+/// var sanitized = sanitizer.Sanitize(html, "http://www.example.com");
+/// // -> "<div style="background-color: test">Test<img style="margin: 10px" src="http://www.example.com/test.gif"></div>"
+/// ]]>
+/// </code>
+/// </example>
+public class HtmlSanitizer : IHtmlSanitizer
 {
+    private const string StyleAttributeName = "style";
+
+    // from http://genshi.edgewall.org/
+    private static readonly Regex CssUnicodeEscapes = new(@"\\([0-9a-fA-F]{1,6})\s?|\\([^\r\n\f0-9a-fA-F'""{};:()#*])", RegexOptions.Compiled);
+    private static readonly Regex CssComments = new(@"/\*.*?\*/", RegexOptions.Compiled);
+    // IE6 <http://heideri.ch/jso/#80>
+    private static readonly Regex CssExpression = new(@"[eE\uFF25\uFF45][xX\uFF38\uFF58][pP\uFF30\uFF50][rR\u0280\uFF32\uFF52][eE\uFF25\uFF45][sS\uFF33\uFF53]{2}[iI\u026A\uFF29\uFF49][oO\uFF2F\uFF4F][nN\u0274\uFF2E\uFF4E]", RegexOptions.Compiled);
+    private static readonly Regex CssUrl = new(@"[Uu][Rr\u0280][Ll\u029F]\((['""]?)([^'"")]+)(['""]?)", RegexOptions.Compiled);
+    private static readonly Regex WhitespaceRegex = new(@"\s*", RegexOptions.Compiled);
+    private static readonly IConfiguration defaultConfiguration = Configuration.Default.WithCss(new CssParserOptions
+    {
+        IsIncludingUnknownDeclarations = true,
+        IsIncludingUnknownRules = true,
+        IsToleratingInvalidSelectors = true,
+    });
+
+    private static readonly HtmlParser defaultHtmlParser = new(new HtmlParserOptions { IsScripting = true }, BrowsingContext.New(defaultConfiguration));
+
     /// <summary>
-    /// Cleans HTML documents and fragments from constructs that can lead to <a href="https://en.wikipedia.org/wiki/Cross-site_scripting">XSS attacks</a>.
-    /// </summary>
-    /// <remarks>
-    /// XSS attacks can occur at several levels within an HTML document or fragment:
-    /// <list type="bullet">
-    /// <item>HTML tags (e.g. the &lt;script&gt; tag)</item>
-    /// <item>HTML attributes (e.g. the "onload" attribute)</item>
-    /// <item>CSS styles (url property values)</item>
-    /// <item>malformed HTML or HTML that exploits parser bugs in specific browsers</item>
-    /// </list>
-    /// <para>
-    /// The HtmlSanitizer class addresses all of these possible attack vectors by using a sophisticated HTML parser (<a href="https://github.com/AngleSharp/AngleSharp">AngleSharp</a>).
-    /// </para>
-    /// <para>
-    /// In order to facilitate different use cases, HtmlSanitizer can be customized at the levels mentioned above:
-    /// <list type="bullet">
-    /// <item>You can specify the allowed HTML tags through the property <see cref="AllowedTags"/>. All other tags will be stripped.</item>
-    /// <item>You can specify the allowed HTML attributes through the property <see cref="AllowedAttributes"/>. All other attributes will be stripped.</item>
-    /// <item>You can specify the allowed CSS property names through the property <see cref="AllowedCssProperties"/>. All other styles will be stripped.</item>
-    /// <item>You can specify the allowed URI schemes through the property <see cref="AllowedSchemes"/>. All other URIs will be stripped.</item>
-    /// <item>You can specify the HTML attributes that contain URIs (such as "src", "href" etc.) through the property <see cref="UriAttributes"/>.</item>
-    /// </list>
-    /// </para>
-    /// </remarks>
-    /// <example>
-    /// <code>
-    /// <![CDATA[
-    /// var sanitizer = new HtmlSanitizer();
-    /// var html = @"<script>alert('xss')</script><div onload=""alert('xss')"" style=""background-color: test"">Test<img src=""test.gif"" style=""background-image: url(javascript:alert('xss')); margin: 10px""></div>";
-    /// var sanitized = sanitizer.Sanitize(html, "http://www.example.com");
-    /// // -> "<div style="background-color: test">Test<img style="margin: 10px" src="http://www.example.com/test.gif"></div>"
-    /// ]]>
-    /// </code>
-    /// </example>
-    public class HtmlSanitizer : IHtmlSanitizer
-    {
-        private const string StyleAttributeName = "style";
-
-        // from http://genshi.edgewall.org/
-        private static readonly Regex CssUnicodeEscapes = new(@"\\([0-9a-fA-F]{1,6})\s?|\\([^\r\n\f0-9a-fA-F'""{};:()#*])", RegexOptions.Compiled);
-        private static readonly Regex CssComments = new(@"/\*.*?\*/", RegexOptions.Compiled);
-        // IE6 <http://heideri.ch/jso/#80>
-        private static readonly Regex CssExpression = new(@"[eE\uFF25\uFF45][xX\uFF38\uFF58][pP\uFF30\uFF50][rR\u0280\uFF32\uFF52][eE\uFF25\uFF45][sS\uFF33\uFF53]{2}[iI\u026A\uFF29\uFF49][oO\uFF2F\uFF4F][nN\u0274\uFF2E\uFF4E]", RegexOptions.Compiled);
-        private static readonly Regex CssUrl = new(@"[Uu][Rr\u0280][Ll\u029F]\((['""]?)([^'"")]+)(['""]?)", RegexOptions.Compiled);
-        private static readonly Regex WhitespaceRegex = new(@"\s*", RegexOptions.Compiled);
-        private static readonly IConfiguration defaultConfiguration = Configuration.Default.WithCss(new CssParserOptions
-        {
-            IsIncludingUnknownDeclarations = true,
-            IsIncludingUnknownRules = true,
-            IsToleratingInvalidSelectors = true,
-        });
+    /// Initializes a new instance of the <see cref="HtmlSanitizer"/> class
+    /// with the default options.
+    /// </summary>
+    public HtmlSanitizer()
+    {
+        AllowedTags = new HashSet<string>(HtmlSanitizerDefaults.AllowedTags, StringComparer.OrdinalIgnoreCase);
+        AllowedSchemes = new HashSet<string>(HtmlSanitizerDefaults.AllowedSchemes, StringComparer.OrdinalIgnoreCase);
+        AllowedAttributes = new HashSet<string>(HtmlSanitizerDefaults.AllowedAttributes, StringComparer.OrdinalIgnoreCase);
+        UriAttributes = new HashSet<string>(HtmlSanitizerDefaults.UriAttributes, StringComparer.OrdinalIgnoreCase);
+        AllowedCssProperties = new HashSet<string>(HtmlSanitizerDefaults.AllowedCssProperties, StringComparer.OrdinalIgnoreCase);
+        AllowedAtRules = new HashSet<CssRuleType>(HtmlSanitizerDefaults.AllowedAtRules);
+        AllowedClasses = new HashSet<string>(HtmlSanitizerDefaults.AllowedClasses);
+    }
 
-        private static readonly HtmlParser defaultHtmlParser = new(new HtmlParserOptions { IsScripting = true }, BrowsingContext.New(defaultConfiguration));
+    /// <summary>
+    /// Initializes a new instance of the <see cref="HtmlSanitizer"/> class
+    /// with the given options.
+    /// </summary>
+    /// <param name="options">Options to control the sanitizing.</param>
+    public HtmlSanitizer(HtmlSanitizerOptions options)
+    {
+        AllowedTags = new HashSet<string>(options.AllowedTags, StringComparer.OrdinalIgnoreCase);
+        AllowedSchemes = new HashSet<string>(options.AllowedSchemes, StringComparer.OrdinalIgnoreCase);
+        AllowedAttributes = new HashSet<string>(options.AllowedAttributes, StringComparer.OrdinalIgnoreCase);
+        UriAttributes = new HashSet<string>(options.UriAttributes, StringComparer.OrdinalIgnoreCase);
+        AllowedClasses = new HashSet<string>(options.AllowedCssClasses, StringComparer.OrdinalIgnoreCase);
+        AllowedCssProperties = new HashSet<string>(options.AllowedCssProperties, StringComparer.OrdinalIgnoreCase);
+        AllowedAtRules = new HashSet<CssRuleType>(options.AllowedAtRules);
+        AllowCssCustomProperties = options.AllowCssCustomProperties;
+        AllowDataAttributes = options.AllowDataAttributes;
+    }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="HtmlSanitizer"/> class
-        /// with the default options.
-        /// </summary>
-        public HtmlSanitizer()
-        {
-            AllowedTags = new HashSet<string>(HtmlSanitizerDefaults.AllowedTags, StringComparer.OrdinalIgnoreCase);
-            AllowedSchemes = new HashSet<string>(HtmlSanitizerDefaults.AllowedSchemes, StringComparer.OrdinalIgnoreCase);
-            AllowedAttributes = new HashSet<string>(HtmlSanitizerDefaults.AllowedAttributes, StringComparer.OrdinalIgnoreCase);
-            UriAttributes = new HashSet<string>(HtmlSanitizerDefaults.UriAttributes, StringComparer.OrdinalIgnoreCase);
-            AllowedCssProperties = new HashSet<string>(HtmlSanitizerDefaults.AllowedCssProperties, StringComparer.OrdinalIgnoreCase);
-            AllowedAtRules = new HashSet<CssRuleType>(HtmlSanitizerDefaults.AllowedAtRules);
-            AllowedClasses = new HashSet<string>(HtmlSanitizerDefaults.AllowedClasses);
-        }
+    /// <summary>
+    /// Gets or sets the default <see cref="Action{IComment}"/> method that encodes comments.
+    /// </summary>
+    public Action<IComment> EncodeComment { get; set; } = DefaultEncodeComment;
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="HtmlSanitizer"/> class
-        /// with the given options.
-        /// </summary>
-        /// <param name="options">Options to control the sanitizing.</param>
-        public HtmlSanitizer(HtmlSanitizerOptions options)
-        {
-            AllowedTags = new HashSet<string>(options.AllowedTags, StringComparer.OrdinalIgnoreCase);
-            AllowedSchemes = new HashSet<string>(options.AllowedSchemes, StringComparer.OrdinalIgnoreCase);
-            AllowedAttributes = new HashSet<string>(options.AllowedAttributes, StringComparer.OrdinalIgnoreCase);
-            UriAttributes = new HashSet<string>(options.UriAttributes, StringComparer.OrdinalIgnoreCase);
-            AllowedClasses = new HashSet<string>(options.AllowedCssClasses, StringComparer.OrdinalIgnoreCase);
-            AllowedCssProperties = new HashSet<string>(options.AllowedCssProperties, StringComparer.OrdinalIgnoreCase);
-            AllowedAtRules = new HashSet<CssRuleType>(options.AllowedAtRules);
-            AllowCssCustomProperties = options.AllowCssCustomProperties;
-            AllowDataAttributes = options.AllowDataAttributes;
-        }
+    /// <summary>
+    /// Gets or sets the default <see cref="Action{IElement}"/> method that encodes literal text content.
+    /// </summary>
+    public Action<IElement> EncodeLiteralTextElementContent { get; set; } = DefaultEncodeLiteralTextElementContent;
 
-        /// <summary>
-        /// Gets or sets the default <see cref="Action{IComment}"/> method that encodes comments.
-        /// </summary>
-        public Action<IComment> EncodeComment { get; set; } = DefaultEncodeComment;
-
-        /// <summary>
-        /// Gets or sets the default <see cref="Action{IElement}"/> method that encodes literal text content.
-        /// </summary>
-        public Action<IElement> EncodeLiteralTextElementContent { get; set; } = DefaultEncodeLiteralTextElementContent;
-
-        /// <summary>
-        /// Gets or sets the default value indicating whether to keep child nodes of elements that are removed. Default is false.
-        /// </summary>
-        public static bool DefaultKeepChildNodes { get; set; } = false;
-
-        /// <summary>
-        /// Gets or sets a value indicating whether to keep child nodes of elements that are removed. Default is <see cref="DefaultKeepChildNodes"/>.
-        /// </summary>
-        public bool KeepChildNodes { get; set; } = DefaultKeepChildNodes;
-
-        /// <summary>
-        /// Gets or sets the default <see cref="Func{HtmlParser}"/> object that creates the parser used for parsing the input.
-        /// </summary>
-        public static Func<HtmlParser> DefaultHtmlParserFactory { get; set; } = () => defaultHtmlParser;
-
-        /// <summary>
-        /// Gets or sets the <see cref="Func{HtmlParser}"/> object the creates the parser used for parsing the input.
-        /// </summary>
-        public Func<HtmlParser> HtmlParserFactory { get; set; } = DefaultHtmlParserFactory;
-
-        /// <summary>
-        /// Gets or sets the default <see cref="IMarkupFormatter"/> object used for generating output. Default is <see cref="HtmlFormatter.Instance"/>.
-        /// </summary>
-        public static IMarkupFormatter DefaultOutputFormatter { get; set; } = HtmlFormatter.Instance;
-
-        /// <summary>
-        /// Gets or sets the <see cref="IMarkupFormatter"/> object used for generating output. Default is <see cref="DefaultOutputFormatter"/>.
-        /// </summary>
-        public IMarkupFormatter OutputFormatter { get; set; } = DefaultOutputFormatter;
-
-        /// <summary>
-        /// Gets or sets the default <see cref="IStyleFormatter"/> object used for generating CSS output. Default is <see cref="CssStyleFormatter.Instance"/>.
-        /// </summary>
-        public static IStyleFormatter DefaultStyleFormatter { get; set; } = CssStyleFormatter.Instance;
-
-        /// <summary>
-        /// Gets or sets the <see cref="IStyleFormatter"/> object used for generating CSS output. Default is <see cref="DefaultStyleFormatter"/>.
-        /// </summary>
-        public IStyleFormatter StyleFormatter { get; set; } = DefaultStyleFormatter;
-
-        /// <summary>
-        /// Gets or sets the allowed CSS at-rules such as "@media" and "@font-face".
-        /// </summary>
-        /// <value>
-        /// The allowed CSS at-rules.
-        /// </value>
-        public ISet<CssRuleType> AllowedAtRules { get; private set; }
-
-        /// <summary>
-        /// Gets or sets the allowed URI schemes such as "http" and "https".
-        /// </summary>
-        /// <value>
-        /// The allowed URI schemes.
-        /// </value>
-        public ISet<string> AllowedSchemes { get; private set; }
-
-        /// <summary>
-        /// Gets or sets the allowed HTML tag names such as "a" and "div".
-        /// </summary>
-        /// <value>
-        /// The allowed tag names.
-        /// </value>
-        public ISet<string> AllowedTags { get; private set; }
-
-        /// <summary>
-        /// Gets or sets the allowed HTML attributes such as "href" and "alt".
-        /// </summary>
-        /// <value>
-        /// The allowed HTML attributes.
-        /// </value>
-        public ISet<string> AllowedAttributes { get; private set; }
-
-        /// <summary>
-        /// Allow all HTML5 data attributes; the attributes prefixed with <c>data-</c>.
-        /// </summary>
-        public bool AllowDataAttributes { get; set; }
-
-        /// <summary>
-        /// Gets or sets the HTML attributes that can contain a URI such as "href".
-        /// </summary>
-        /// <value>
-        /// The URI attributes.
-        /// </value>
-        public ISet<string> UriAttributes { get; private set; }
-
-        /// <summary>
-        /// Gets or sets the allowed CSS properties such as "font" and "margin".
-        /// </summary>
-        /// <value>
-        /// The allowed CSS properties.
-        /// </value>
-        public ISet<string> AllowedCssProperties { get; private set; }
-
-        /// <summary>
-        /// Allow all custom CSS properties (variables) prefixed with <c>--</c>.
-        /// </summary>
-        public bool AllowCssCustomProperties { get; set; }
-
-        /// <summary>
-        /// Gets or sets a regex that must not match for legal CSS property values.
-        /// </summary>
-        /// <value>
-        /// The regex.
-        /// </value>
-        public Regex DisallowCssPropertyValue { get; set; } = DefaultDisallowedCssPropertyValue;
-
-        /// <summary>
-        /// Gets or sets the allowed CSS classes. If the set is empty, all classes will be allowed.
-        /// </summary>
-        /// <value>
-        /// The allowed CSS classes. An empty set means all classes are allowed.
-        /// </value>
-        public ISet<string> AllowedClasses { get; private set; }
-
-        /// <summary>
-        /// Occurs after sanitizing the document and post processing nodes.
-        /// </summary>
-        public event EventHandler<PostProcessDomEventArgs>? PostProcessDom;
-        /// <summary>
-        /// Occurs for every node after sanitizing.
-        /// </summary>
-        public event EventHandler<PostProcessNodeEventArgs>? PostProcessNode;
-        /// <summary>
-        /// Occurs before a tag is removed.
-        /// </summary>
-        public event EventHandler<RemovingTagEventArgs>? RemovingTag;
-        /// <summary>
-        /// Occurs before an attribute is removed.
-        /// </summary>
-        public event EventHandler<RemovingAttributeEventArgs>? RemovingAttribute;
-        /// <summary>
-        /// Occurs before a style is removed.
-        /// </summary>
-        public event EventHandler<RemovingStyleEventArgs>? RemovingStyle;
-        /// <summary>
-        /// Occurs before an at-rule is removed.
-        /// </summary>
-        public event EventHandler<RemovingAtRuleEventArgs>? RemovingAtRule;
-        /// <summary>
-        /// Occurs before a comment is removed.
-        /// </summary>
-        public event EventHandler<RemovingCommentEventArgs>? RemovingComment;
-        /// <summary>
-        /// Occurs before a CSS class is removed.
-        /// </summary>
-        public event EventHandler<RemovingCssClassEventArgs>? RemovingCssClass;
-        /// <summary>
-        /// Occurs when a URL is being sanitized.
-        /// </summary>
-        public event EventHandler<FilterUrlEventArgs>? FilterUrl;
-
-        /// <summary>
-        /// Raises the <see cref="E:PostProcessDom" /> event.
-        /// </summary>
-        /// <param name="e">The <see cref="PostProcessDomEventArgs"/> instance containing the event data.</param>
-        protected virtual void OnPostProcessDom(PostProcessDomEventArgs e)
-        {
-            PostProcessDom?.Invoke(this, e);
-        }
+    /// <summary>
+    /// Gets or sets the default value indicating whether to keep child nodes of elements that are removed. Default is false.
+    /// </summary>
+    public static bool DefaultKeepChildNodes { get; set; } = false;
 
-        /// <summary>
-        /// Raises the <see cref="E:PostProcessNode" /> event.
-        /// </summary>
-        /// <param name="e">The <see cref="PostProcessNodeEventArgs"/> instance containing the event data.</param>
-        protected virtual void OnPostProcessNode(PostProcessNodeEventArgs e)
-        {
-            PostProcessNode?.Invoke(this, e);
-        }
+    /// <summary>
+    /// Gets or sets a value indicating whether to keep child nodes of elements that are removed. Default is <see cref="DefaultKeepChildNodes"/>.
+    /// </summary>
+    public bool KeepChildNodes { get; set; } = DefaultKeepChildNodes;
 
-        /// <summary>
-        /// Raises the <see cref="E:RemovingTag" /> event.
-        /// </summary>
-        /// <param name="e">The <see cref="RemovingTagEventArgs"/> instance containing the event data.</param>
-        protected virtual void OnRemovingTag(RemovingTagEventArgs e)
-        {
-            RemovingTag?.Invoke(this, e);
-        }
+    /// <summary>
+    /// Gets or sets the default <see cref="Func{HtmlParser}"/> object that creates the parser used for parsing the input.
+    /// </summary>
+    public static Func<HtmlParser> DefaultHtmlParserFactory { get; set; } = () => defaultHtmlParser;
 
-        /// <summary>
-        /// Raises the <see cref="E:RemovingAttribute" /> event.
-        /// </summary>
-        /// <param name="e">The <see cref="RemovingAttributeEventArgs"/> instance containing the event data.</param>
-        protected virtual void OnRemovingAttribute(RemovingAttributeEventArgs e)
-        {
-            RemovingAttribute?.Invoke(this, e);
-        }
+    /// <summary>
+    /// Gets or sets the <see cref="Func{HtmlParser}"/> object the creates the parser used for parsing the input.
+    /// </summary>
+    public Func<HtmlParser> HtmlParserFactory { get; set; } = DefaultHtmlParserFactory;
 
-        /// <summary>
-        /// Raises the <see cref="E:RemovingStyle" /> event.
-        /// </summary>
-        /// <param name="e">The <see cref="RemovingStyleEventArgs"/> instance containing the event data.</param>
-        protected virtual void OnRemovingStyle(RemovingStyleEventArgs e)
-        {
-            RemovingStyle?.Invoke(this, e);
-        }
+    /// <summary>
+    /// Gets or sets the default <see cref="IMarkupFormatter"/> object used for generating output. Default is <see cref="HtmlFormatter.Instance"/>.
+    /// </summary>
+    public static IMarkupFormatter DefaultOutputFormatter { get; set; } = HtmlFormatter.Instance;
 
-        /// <summary>
-        /// Raises the <see cref="E:RemovingAtRule" /> event.
-        /// </summary>
-        /// <param name="e">The <see cref="RemovingAtRuleEventArgs"/> instance containing the event data.</param>
-        protected virtual void OnRemovingAtRule(RemovingAtRuleEventArgs e)
-        {
-            RemovingAtRule?.Invoke(this, e);
-        }
+    /// <summary>
+    /// Gets or sets the <see cref="IMarkupFormatter"/> object used for generating output. Default is <see cref="DefaultOutputFormatter"/>.
+    /// </summary>
+    public IMarkupFormatter OutputFormatter { get; set; } = DefaultOutputFormatter;
 
-        /// <summary>
-        /// Raises the <see cref="E:RemovingComment" /> event.
-        /// </summary>
-        /// <param name="e">The <see cref="RemovingCommentEventArgs"/> instance containing the event data.</param>
-        protected virtual void OnRemovingComment(RemovingCommentEventArgs e)
-        {
-            RemovingComment?.Invoke(this, e);
-        }
+    /// <summary>
+    /// Gets or sets the default <see cref="IStyleFormatter"/> object used for generating CSS output. Default is <see cref="CssStyleFormatter.Instance"/>.
+    /// </summary>
+    public static IStyleFormatter DefaultStyleFormatter { get; set; } = CssStyleFormatter.Instance;
+
+    /// <summary>
+    /// Gets or sets the <see cref="IStyleFormatter"/> object used for generating CSS output. Default is <see cref="DefaultStyleFormatter"/>.
+    /// </summary>
+    public IStyleFormatter StyleFormatter { get; set; } = DefaultStyleFormatter;
 
-        /// <summary>
-        /// The default regex for disallowed CSS property values.
-        /// </summary>
-        public static readonly Regex DefaultDisallowedCssPropertyValue = new(@"[<>]", RegexOptions.Compiled);
+    /// <summary>
+    /// Gets or sets the allowed CSS at-rules such as "@media" and "@font-face".
+    /// </summary>
+    /// <value>
+    /// The allowed CSS at-rules.
+    /// </value>
+    public ISet<CssRuleType> AllowedAtRules { get; private set; }
 
-        /// <summary>
-        /// Raises the <see cref="E:RemovingCSSClass" /> event.
-        /// </summary>
-        /// <param name="e">The <see cref="RemovingCssClassEventArgs"/> instance containing the event data.</param>
-        protected virtual void OnRemovingCssClass(RemovingCssClassEventArgs e)
-        {
-            RemovingCssClass?.Invoke(this, e);
-        }
+    /// <summary>
+    /// Gets or sets the allowed URI schemes such as "http" and "https".
+    /// </summary>
+    /// <value>
+    /// The allowed URI schemes.
+    /// </value>
+    public ISet<string> AllowedSchemes { get; private set; }
+
+    /// <summary>
+    /// Gets or sets the allowed HTML tag names such as "a" and "div".
+    /// </summary>
+    /// <value>
+    /// The allowed tag names.
+    /// </value>
+    public ISet<string> AllowedTags { get; private set; }
+
+    /// <summary>
+    /// Gets or sets the allowed HTML attributes such as "href" and "alt".
+    /// </summary>
+    /// <value>
+    /// The allowed HTML attributes.
+    /// </value>
+    public ISet<string> AllowedAttributes { get; private set; }
+
+    /// <summary>
+    /// Allow all HTML5 data attributes; the attributes prefixed with <c>data-</c>.
+    /// </summary>
+    public bool AllowDataAttributes { get; set; }
+
+    /// <summary>
+    /// Gets or sets the HTML attributes that can contain a URI such as "href".
+    /// </summary>
+    /// <value>
+    /// The URI attributes.
+    /// </value>
+    public ISet<string> UriAttributes { get; private set; }
+
+    /// <summary>
+    /// Gets or sets the allowed CSS properties such as "font" and "margin".
+    /// </summary>
+    /// <value>
+    /// The allowed CSS properties.
+    /// </value>
+    public ISet<string> AllowedCssProperties { get; private set; }
+
+    /// <summary>
+    /// Allow all custom CSS properties (variables) prefixed with <c>--</c>.
+    /// </summary>
+    public bool AllowCssCustomProperties { get; set; }
+
+    /// <summary>
+    /// Gets or sets a regex that must not match for legal CSS property values.
+    /// </summary>
+    /// <value>
+    /// The regex.
+    /// </value>
+    public Regex DisallowCssPropertyValue { get; set; } = DefaultDisallowedCssPropertyValue;
+
+    /// <summary>
+    /// Gets or sets the allowed CSS classes. If the set is empty, all classes will be allowed.
+    /// </summary>
+    /// <value>
+    /// The allowed CSS classes. An empty set means all classes are allowed.
+    /// </value>
+    public ISet<string> AllowedClasses { get; private set; }
+
+    /// <summary>
+    /// Occurs after sanitizing the document and post processing nodes.
+    /// </summary>
+    public event EventHandler<PostProcessDomEventArgs>? PostProcessDom;
+    /// <summary>
+    /// Occurs for every node after sanitizing.
+    /// </summary>
+    public event EventHandler<PostProcessNodeEventArgs>? PostProcessNode;
+    /// <summary>
+    /// Occurs before a tag is removed.
+    /// </summary>
+    public event EventHandler<RemovingTagEventArgs>? RemovingTag;
+    /// <summary>
+    /// Occurs before an attribute is removed.
+    /// </summary>
+    public event EventHandler<RemovingAttributeEventArgs>? RemovingAttribute;
+    /// <summary>
+    /// Occurs before a style is removed.
+    /// </summary>
+    public event EventHandler<RemovingStyleEventArgs>? RemovingStyle;
+    /// <summary>
+    /// Occurs before an at-rule is removed.
+    /// </summary>
+    public event EventHandler<RemovingAtRuleEventArgs>? RemovingAtRule;
+    /// <summary>
+    /// Occurs before a comment is removed.
+    /// </summary>
+    public event EventHandler<RemovingCommentEventArgs>? RemovingComment;
+    /// <summary>
+    /// Occurs before a CSS class is removed.
+    /// </summary>
+    public event EventHandler<RemovingCssClassEventArgs>? RemovingCssClass;
+    /// <summary>
+    /// Occurs when a URL is being sanitized.
+    /// </summary>
+    public event EventHandler<FilterUrlEventArgs>? FilterUrl;
+
+    /// <summary>
+    /// Raises the <see cref="E:PostProcessDom" /> event.
+    /// </summary>
+    /// <param name="e">The <see cref="PostProcessDomEventArgs"/> instance containing the event data.</param>
+    protected virtual void OnPostProcessDom(PostProcessDomEventArgs e)
+    {
+        PostProcessDom?.Invoke(this, e);
+    }
+
+    /// <summary>
+    /// Raises the <see cref="E:PostProcessNode" /> event.
+    /// </summary>
+    /// <param name="e">The <see cref="PostProcessNodeEventArgs"/> instance containing the event data.</param>
+    protected virtual void OnPostProcessNode(PostProcessNodeEventArgs e)
+    {
+        PostProcessNode?.Invoke(this, e);
+    }
+
+    /// <summary>
+    /// Raises the <see cref="E:RemovingTag" /> event.
+    /// </summary>
+    /// <param name="e">The <see cref="RemovingTagEventArgs"/> instance containing the event data.</param>
+    protected virtual void OnRemovingTag(RemovingTagEventArgs e)
+    {
+        RemovingTag?.Invoke(this, e);
+    }
+
+    /// <summary>
+    /// Raises the <see cref="E:RemovingAttribute" /> event.
+    /// </summary>
+    /// <param name="e">The <see cref="RemovingAttributeEventArgs"/> instance containing the event data.</param>
+    protected virtual void OnRemovingAttribute(RemovingAttributeEventArgs e)
+    {
+        RemovingAttribute?.Invoke(this, e);
+    }
+
+    /// <summary>
+    /// Raises the <see cref="E:RemovingStyle" /> event.
+    /// </summary>
+    /// <param name="e">The <see cref="RemovingStyleEventArgs"/> instance containing the event data.</param>
+    protected virtual void OnRemovingStyle(RemovingStyleEventArgs e)
+    {
+        RemovingStyle?.Invoke(this, e);
+    }
+
+    /// <summary>
+    /// Raises the <see cref="E:RemovingAtRule" /> event.
+    /// </summary>
+    /// <param name="e">The <see cref="RemovingAtRuleEventArgs"/> instance containing the event data.</param>
+    protected virtual void OnRemovingAtRule(RemovingAtRuleEventArgs e)
+    {
+        RemovingAtRule?.Invoke(this, e);
+    }
+
+    /// <summary>
+    /// Raises the <see cref="E:RemovingComment" /> event.
+    /// </summary>
+    /// <param name="e">The <see cref="RemovingCommentEventArgs"/> instance containing the event data.</param>
+    protected virtual void OnRemovingComment(RemovingCommentEventArgs e)
+    {
+        RemovingComment?.Invoke(this, e);
+    }
+
+    /// <summary>
+    /// The default regex for disallowed CSS property values.
+    /// </summary>
+    public static readonly Regex DefaultDisallowedCssPropertyValue = new(@"[<>]", RegexOptions.Compiled);
+
+    /// <summary>
+    /// Raises the <see cref="E:RemovingCSSClass" /> event.
+    /// </summary>
+    /// <param name="e">The <see cref="RemovingCssClassEventArgs"/> instance containing the event data.</param>
+    protected virtual void OnRemovingCssClass(RemovingCssClassEventArgs e)
+    {
+        RemovingCssClass?.Invoke(this, e);
+    }
+
+    /// <summary>
+    /// Raises the <see cref="E:FilterUrl" /> event.
+    /// </summary>
+    /// <param name="e">The <see cref="FilterUrlEventArgs"/> instance containing the event data.</param>
+    protected virtual void OnFilteringUrl(FilterUrlEventArgs e)
+    {
+        FilterUrl?.Invoke(this, e);
+    }
 
-        /// <summary>
-        /// Raises the <see cref="E:FilterUrl" /> event.
-        /// </summary>
-        /// <param name="e">The <see cref="FilterUrlEventArgs"/> instance containing the event data.</param>
-        protected virtual void OnFilteringUrl(FilterUrlEventArgs e)
+    /// <summary>
+    /// Return all nested subnodes of a node. The nodes are returned in DOM order.
+    /// </summary>
+    /// <param name="dom">The root node.</param>
+    /// <returns>All nested subnodes.</returns>
+    private static IEnumerable<INode> GetAllNodes(INode dom)
+    {
+        if (dom.ChildNodes.Length == 0) yield break;
+
+        var s = new Stack<INode>();
+        for (var i = dom.ChildNodes.Length - 1; i >= 0; i--)
         {
-            FilterUrl?.Invoke(this, e);
+            s.Push(dom.ChildNodes[i]);
         }
 
-        /// <summary>
-        /// Return all nested subnodes of a node. The nodes are returned in DOM order.
-        /// </summary>
-        /// <param name="dom">The root node.</param>
-        /// <returns>All nested subnodes.</returns>
-        private static IEnumerable<INode> GetAllNodes(INode dom)
+        while (s.Count > 0)
         {
-            if (dom.ChildNodes.Length == 0) yield break;
+            var n = s.Pop();
+            yield return n;
 
-            var s = new Stack<INode>();
-            for (var i = dom.ChildNodes.Length - 1; i >= 0; i--)
+            for (var i = n.ChildNodes.Length - 1; i >= 0; i--)
             {
-                s.Push(dom.ChildNodes[i]);
-            }
-
-            while (s.Count > 0)
-            {
-                var n = s.Pop();
-                yield return n;
-
-                for (var i = n.ChildNodes.Length - 1; i >= 0; i--)
-                {
-                    s.Push(n.ChildNodes[i]);
-                }
+                s.Push(n.ChildNodes[i]);
             }
         }
+    }
 
-        /// <summary>
-        /// Sanitizes the specified HTML body fragment. If a document is given, only the body part will be returned.
-        /// </summary>
-        /// <param name="html">The HTML body fragment to sanitize.</param>
-        /// <param name="baseUrl">The base URL relative URLs are resolved against. No resolution if empty.</param>
-        /// <param name="outputFormatter">The formatter used to render the DOM. Using the <see cref="OutputFormatter"/> if null.</param>
-        /// <returns>The sanitized HTML body fragment.</returns>
-        public string Sanitize(string html, string baseUrl = "", IMarkupFormatter? outputFormatter = null)
-        {
-            using var dom = SanitizeDom(html, baseUrl);
-            if (dom.Body == null) return string.Empty;
-            var output = dom.Body.ChildNodes.ToHtml(outputFormatter ?? OutputFormatter);
+    /// <summary>
+    /// Sanitizes the specified HTML body fragment. If a document is given, only the body part will be returned.
+    /// </summary>
+    /// <param name="html">The HTML body fragment to sanitize.</param>
+    /// <param name="baseUrl">The base URL relative URLs are resolved against. No resolution if empty.</param>
+    /// <param name="outputFormatter">The formatter used to render the DOM. Using the <see cref="OutputFormatter"/> if null.</param>
+    /// <returns>The sanitized HTML body fragment.</returns>
+    public string Sanitize(string html, string baseUrl = "", IMarkupFormatter? outputFormatter = null)
+    {
+        using var dom = SanitizeDom(html, baseUrl);
+        if (dom.Body == null) return string.Empty;
+        var output = dom.Body.ChildNodes.ToHtml(outputFormatter ?? OutputFormatter);
 
-            return output;
-        }
+        return output;
+    }
 
-        /// <summary>
-        /// Sanitizes the specified HTML body fragment. If a document is given, only the body part will be returned.
-        /// </summary>
-        /// <param name="html">The HTML body fragment to sanitize.</param>
-        /// <param name="baseUrl">The base URL relative URLs are resolved against. No resolution if empty.</param>
-        /// <returns>The sanitized HTML document.</returns>
-        public IHtmlDocument SanitizeDom(string html, string baseUrl = "")
-        {
-            var parser = HtmlParserFactory();
-            var dom = parser.ParseDocument("<!doctype html><html><body>" + html);
+    /// <summary>
+    /// Sanitizes the specified HTML body fragment. If a document is given, only the body part will be returned.
+    /// </summary>
+    /// <param name="html">The HTML body fragment to sanitize.</param>
+    /// <param name="baseUrl">The base URL relative URLs are resolved against. No resolution if empty.</param>
+    /// <returns>The sanitized HTML document.</returns>
+    public IHtmlDocument SanitizeDom(string html, string baseUrl = "")
+    {
+        var parser = HtmlParserFactory();
+        var dom = parser.ParseDocument("<!doctype html><html><body>" + html);
 
-            if (dom.Body != null)
-                DoSanitize(dom, dom.Body, baseUrl);
+        if (dom.Body != null)
+            DoSanitize(dom, dom.Body, baseUrl);
 
-            return dom;
-        }
+        return dom;
+    }
 
-        /// <summary>
-        /// Sanitizes the specified parsed HTML body fragment.
-        /// If the document has not been parsed with CSS support then all styles will be removed.
-        /// </summary>
-        /// <param name="document">The parsed HTML document.</param>
-        /// <param name="context">The node within which to sanitize.</param>
-        /// <param name="baseUrl">The base URL relative URLs are resolved against. No resolution if empty.</param>
-        /// <returns>The sanitized HTML document.</returns>
-        public IHtmlDocument SanitizeDom(IHtmlDocument document, IHtmlElement? context = null, string baseUrl = "")
-        {
-            DoSanitize(document, context ?? (IParentNode)document, baseUrl);
+    /// <summary>
+    /// Sanitizes the specified parsed HTML body fragment.
+    /// If the document has not been parsed with CSS support then all styles will be removed.
+    /// </summary>
+    /// <param name="document">The parsed HTML document.</param>
+    /// <param name="context">The node within which to sanitize.</param>
+    /// <param name="baseUrl">The base URL relative URLs are resolved against. No resolution if empty.</param>
+    /// <returns>The sanitized HTML document.</returns>
+    public IHtmlDocument SanitizeDom(IHtmlDocument document, IHtmlElement? context = null, string baseUrl = "")
+    {
+        DoSanitize(document, context ?? (IParentNode)document, baseUrl);
 
-            return document;
-        }
+        return document;
+    }
 
-        /// <summary>
-        /// Sanitizes the specified HTML document. Even if only a fragment is given, a whole document will be returned.
-        /// </summary>
-        /// <param name="html">The HTML document to sanitize.</param>
-        /// <param name="baseUrl">The base URL relative URLs are resolved against. No resolution if empty.</param>
-        /// <param name="outputFormatter">The formatter used to render the DOM. Using the <see cref="OutputFormatter"/> if null.</param>
-        /// <returns>The sanitized HTML document.</returns>
-        public string SanitizeDocument(string html, string baseUrl = "", IMarkupFormatter? outputFormatter = null)
-        {
-            var parser = HtmlParserFactory();
-            using var dom = parser.ParseDocument(html);
+    /// <summary>
+    /// Sanitizes the specified HTML document. Even if only a fragment is given, a whole document will be returned.
+    /// </summary>
+    /// <param name="html">The HTML document to sanitize.</param>
+    /// <param name="baseUrl">The base URL relative URLs are resolved against. No resolution if empty.</param>
+    /// <param name="outputFormatter">The formatter used to render the DOM. Using the <see cref="OutputFormatter"/> if null.</param>
+    /// <returns>The sanitized HTML document.</returns>
+    public string SanitizeDocument(string html, string baseUrl = "", IMarkupFormatter? outputFormatter = null)
+    {
+        var parser = HtmlParserFactory();
+        using var dom = parser.ParseDocument(html);
 
-            DoSanitize(dom, dom, baseUrl);
+        DoSanitize(dom, dom, baseUrl);
 
-            var output = dom.ToHtml(outputFormatter ?? OutputFormatter);
+        var output = dom.ToHtml(outputFormatter ?? OutputFormatter);
 
-            return output;
-        }
+        return output;
+    }
 
-        /// <summary>
-        /// Sanitizes the specified HTML document. Even if only a fragment is given, a whole document will be returned.
-        /// </summary>
-        /// <param name="html">The HTML document to sanitize.</param>
-        /// <param name="baseUrl">The base URL relative URLs are resolved against. No resolution if empty.</param>
-        /// <param name="outputFormatter">The formatter used to render the DOM. Using the <see cref="OutputFormatter"/> if null.</param>
-        /// <returns>The sanitized HTML document.</returns>
-        public string SanitizeDocument(Stream html, string baseUrl = "", IMarkupFormatter? outputFormatter = null)
-        {
-            var parser = HtmlParserFactory();
-            using var dom = parser.ParseDocument(html);
+    /// <summary>
+    /// Sanitizes the specified HTML document. Even if only a fragment is given, a whole document will be returned.
+    /// </summary>
+    /// <param name="html">The HTML document to sanitize.</param>
+    /// <param name="baseUrl">The base URL relative URLs are resolved against. No resolution if empty.</param>
+    /// <param name="outputFormatter">The formatter used to render the DOM. Using the <see cref="OutputFormatter"/> if null.</param>
+    /// <returns>The sanitized HTML document.</returns>
+    public string SanitizeDocument(Stream html, string baseUrl = "", IMarkupFormatter? outputFormatter = null)
+    {
+        var parser = HtmlParserFactory();
+        using var dom = parser.ParseDocument(html);
 
-            DoSanitize(dom, dom, baseUrl);
+        DoSanitize(dom, dom, baseUrl);
 
-            var output = dom.ToHtml(outputFormatter ?? OutputFormatter);
+        var output = dom.ToHtml(outputFormatter ?? OutputFormatter);
 
-            return output;
-        }
+        return output;
+    }
 
-        /// <summary>
-        /// Removes all comment nodes from a list of nodes.
-        /// </summary>
-        /// <param name="context">The node within which to remove comments.</param>
-        /// <returns><c>true</c> if any comments were removed; otherwise, <c>false</c>.</returns>
-        private void RemoveComments(INode context)
+    /// <summary>
+    /// Removes all comment nodes from a list of nodes.
+    /// </summary>
+    /// <param name="context">The node within which to remove comments.</param>
+    /// <returns><c>true</c> if any comments were removed; otherwise, <c>false</c>.</returns>
+    private void RemoveComments(INode context)
+    {
+        foreach (var comment in GetAllNodes(context).OfType<IComment>().ToList())
         {
-            foreach (var comment in GetAllNodes(context).OfType<IComment>().ToList())
-            {
-                EncodeComment(comment);
+            EncodeComment(comment);
 
-                var e = new RemovingCommentEventArgs(comment);
-                OnRemovingComment(e);
+            var e = new RemovingCommentEventArgs(comment);
+            OnRemovingComment(e);
 
-                if (!e.Cancel)
-                    comment.Remove();
-            }
+            if (!e.Cancel)
+                comment.Remove();
         }
+    }
 
-        private static void DefaultEncodeComment(IComment comment)
+    private static void DefaultEncodeComment(IComment comment)
+    {
+        var escapedText = comment.TextContent.Replace("<", "&lt;").Replace(">", "&gt;");
+        if (escapedText != comment.TextContent)
+            comment.TextContent = escapedText;
+    }
+
+    private static void DefaultEncodeLiteralTextElementContent(IElement tag)
+    {
+        var escapedHtml = tag.InnerHtml.Replace("<", "&lt;").Replace(">", "&gt;");
+        if (escapedHtml != tag.InnerHtml)
+            tag.InnerHtml = escapedHtml;
+        if (tag.InnerHtml != escapedHtml) // setting InnerHtml does not work for noscript
+            tag.SetInnerText(escapedHtml);
+    }
+
+    private void DoSanitize(IHtmlDocument dom, IParentNode context, string baseUrl = "")
+    {
+        // remove disallowed tags
+        foreach (var tag in context.QuerySelectorAll("*").Where(t => !IsAllowedTag(t)).ToList())
         {
-            var escapedText = comment.TextContent.Replace("<", "&lt;").Replace(">", "&gt;");
-            if (escapedText != comment.TextContent)
-                comment.TextContent = escapedText;
+            RemoveTag(tag, RemoveReason.NotAllowedTag);
         }
 
-        private static void DefaultEncodeLiteralTextElementContent(IElement tag)
+        // always encode text in raw data content
+        foreach (var tag in context.QuerySelectorAll("*")
+            .Where(t => t is not IHtmlStyleElement
+                && t.Flags.HasFlag(NodeFlags.LiteralText)
+                && !string.IsNullOrWhiteSpace(t.InnerHtml)))
         {
-            var escapedHtml = tag.InnerHtml.Replace("<", "&lt;").Replace(">", "&gt;");
-            if (escapedHtml != tag.InnerHtml)
-                tag.InnerHtml = escapedHtml;
-            if (tag.InnerHtml != escapedHtml) // setting InnerHtml does not work for noscript
-                tag.SetInnerText(escapedHtml);
+            EncodeLiteralTextElementContent(tag);
         }
 
-        private void DoSanitize(IHtmlDocument dom, IParentNode context, string baseUrl = "")
+        SanitizeStyleSheets(dom, baseUrl);
+
+        // cleanup attributes
+        foreach (var tag in context.QuerySelectorAll("*").ToList())
         {
-            // remove disallowed tags
-            foreach (var tag in context.QuerySelectorAll("*").Where(t => !IsAllowedTag(t)).ToList())
+            // remove disallowed attributes
+            foreach (var attribute in tag.Attributes.Where(a => !IsAllowedAttribute(a)).ToList())
             {
-                RemoveTag(tag, RemoveReason.NotAllowedTag);
+                RemoveAttribute(tag, attribute, RemoveReason.NotAllowedAttribute);
             }
 
-            // always encode text in raw data content
-            foreach (var tag in context.QuerySelectorAll("*")
-                .Where(t => t is not IHtmlStyleElement
-                    && t.Flags.HasFlag(NodeFlags.LiteralText)
-                    && !string.IsNullOrWhiteSpace(t.InnerHtml)))
+            // sanitize URLs in URL-marked attributes
+            foreach (var attribute in tag.Attributes.Where(IsUriAttribute).ToList())
             {
-                EncodeLiteralTextElementContent(tag);
+                var url = SanitizeUrl(tag, attribute.Value, baseUrl);
+
+                if (url == null)
+                    RemoveAttribute(tag, attribute, RemoveReason.NotAllowedUrlValue);
+                else
+                    tag.SetAttribute(attribute.Name, url);
             }
 
-            SanitizeStyleSheets(dom, baseUrl);
+            // sanitize the style attribute
+            var oldStyleEmpty = string.IsNullOrEmpty(tag.GetAttribute(StyleAttributeName));
+            SanitizeStyle(tag, baseUrl);
 
-            // cleanup attributes
-            foreach (var tag in context.QuerySelectorAll("*").ToList())
+            // sanitize the value of the attributes
+            foreach (var attribute in tag.Attributes.ToList())
             {
-                // remove disallowed attributes
-                foreach (var attribute in tag.Attributes.Where(a => !IsAllowedAttribute(a)).ToList())
+                // The '& Javascript include' is a possible method to execute Javascript and can lead to XSS.
+                // (see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#.26_JavaScript_includes)
+                if (attribute.Value.Contains("&{"))
                 {
-                    RemoveAttribute(tag, attribute, RemoveReason.NotAllowedAttribute);
+                    RemoveAttribute(tag, attribute, RemoveReason.NotAllowedValue);
                 }
-
-                // sanitize URLs in URL-marked attributes
-                foreach (var attribute in tag.Attributes.Where(IsUriAttribute).ToList())
+                else
                 {
-                    var url = SanitizeUrl(tag, attribute.Value, baseUrl);
-
-                    if (url == null)
-                        RemoveAttribute(tag, attribute, RemoveReason.NotAllowedUrlValue);
-                    else
-                        tag.SetAttribute(attribute.Name, url);
-                }
+                    if (AllowedClasses.Any() && attribute.Name == "class")
+                    {
+                        var removedClasses = tag.ClassList.Except(AllowedClasses).ToArray();
 
-                // sanitize the style attribute
-                var oldStyleEmpty = string.IsNullOrEmpty(tag.GetAttribute(StyleAttributeName));
-                SanitizeStyle(tag, baseUrl);
+                        foreach (var removedClass in removedClasses)
+                            RemoveCssClass(tag, removedClass, RemoveReason.NotAllowedCssClass);
 
-                // sanitize the value of the attributes
-                foreach (var attribute in tag.Attributes.ToList())
-                {
-                    // The '& Javascript include' is a possible method to execute Javascript and can lead to XSS.
-                    // (see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#.26_JavaScript_includes)
-                    if (attribute.Value.Contains("&{"))
-                    {
-                        RemoveAttribute(tag, attribute, RemoveReason.NotAllowedValue);
+                        if (!tag.ClassList.Any())
+                            RemoveAttribute(tag, attribute, RemoveReason.ClassAttributeEmpty);
                     }
-                    else
+                    else if (!oldStyleEmpty && attribute.Name == StyleAttributeName && string.IsNullOrEmpty(attribute.Value))
                     {
-                        if (AllowedClasses.Any() && attribute.Name == "class")
-                        {
-                            var removedClasses = tag.ClassList.Except(AllowedClasses).ToArray();
-
-                            foreach (var removedClass in removedClasses)
-                                RemoveCssClass(tag, removedClass, RemoveReason.NotAllowedCssClass);
-
-                            if (!tag.ClassList.Any())
-                                RemoveAttribute(tag, attribute, RemoveReason.ClassAttributeEmpty);
-                        }
-                        else if (!oldStyleEmpty && attribute.Name == StyleAttributeName && string.IsNullOrEmpty(attribute.Value))
-                        {
-                            RemoveAttribute(tag, attribute, RemoveReason.StyleAttributeEmpty);
-                        }
+                        RemoveAttribute(tag, attribute, RemoveReason.StyleAttributeEmpty);
                     }
                 }
             }
+        }
 
-            if (context is INode node)
+        if (context is INode node)
+        {
+            RemoveComments(node);
+        }
+
+        DoPostProcess(dom, context as INode);
+    }
+
+    private void SanitizeStyleSheets(IHtmlDocument dom, string baseUrl)
+    {
+        foreach (var styleSheet in dom.StyleSheets.OfType<ICssStyleSheet>())
+        {
+            var styleTag = styleSheet.OwnerNode;
+            var i = 0;
+
+            while (i < styleSheet.Rules.Length)
             {
-                RemoveComments(node);
+                var rule = styleSheet.Rules[i];
+                if (!SanitizeStyleRule(rule, styleTag, baseUrl) && RemoveAtRule(styleTag, rule))
+                    styleSheet.RemoveAt(i);
+                else i++;
             }
 
-            DoPostProcess(dom, context as INode);
+            styleTag.InnerHtml = styleSheet.ToCss(StyleFormatter).Replace("<", "\\3c ");
         }
+    }
+
+    private bool SanitizeStyleRule(ICssRule rule, IElement styleTag, string baseUrl)
+    {
+        if (!AllowedAtRules.Contains(rule.Type)) return false;
 
-        private void SanitizeStyleSheets(IHtmlDocument dom, string baseUrl)
+        if (rule is ICssStyleRule styleRule)
+        {
+            SanitizeStyleDeclaration(styleTag, styleRule.Style, baseUrl);
+        }
+        else
         {
-            foreach (var styleSheet in dom.StyleSheets.OfType<ICssStyleSheet>())
+            if (rule is ICssGroupingRule groupingRule)
             {
-                var styleTag = styleSheet.OwnerNode;
                 var i = 0;
 
-                while (i < styleSheet.Rules.Length)
+                while (i < groupingRule.Rules.Length)
                 {
-                    var rule = styleSheet.Rules[i];
-                    if (!SanitizeStyleRule(rule, styleTag, baseUrl) && RemoveAtRule(styleTag, rule))
-                        styleSheet.RemoveAt(i);
+                    var childRule = groupingRule.Rules[i];
+                    if (!SanitizeStyleRule(childRule, styleTag, baseUrl) && RemoveAtRule(styleTag, childRule))
+                        groupingRule.RemoveAt(i);
                     else i++;
                 }
-
-                styleTag.InnerHtml = styleSheet.ToCss(StyleFormatter).Replace("<", "\\3c ");
             }
-        }
-
-        private bool SanitizeStyleRule(ICssRule rule, IElement styleTag, string baseUrl)
-        {
-            if (!AllowedAtRules.Contains(rule.Type)) return false;
-
-            if (rule is ICssStyleRule styleRule)
+            else if (rule is ICssPageRule pageRule)
             {
-                SanitizeStyleDeclaration(styleTag, styleRule.Style, baseUrl);
+                SanitizeStyleDeclaration(styleTag, pageRule.Style, baseUrl);
             }
-            else
+            else if (rule is ICssKeyframesRule keyFramesRule)
             {
-                if (rule is ICssGroupingRule groupingRule)
-                {
-                    var i = 0;
-
-                    while (i < groupingRule.Rules.Length)
-                    {
-                        var childRule = groupingRule.Rules[i];
-                        if (!SanitizeStyleRule(childRule, styleTag, baseUrl) && RemoveAtRule(styleTag, childRule))
-                            groupingRule.RemoveAt(i);
-                        else i++;
-                    }
-                }
-                else if (rule is ICssPageRule pageRule)
-                {
-                    SanitizeStyleDeclaration(styleTag, pageRule.Style, baseUrl);
-                }
-                else if (rule is ICssKeyframesRule keyFramesRule)
+                foreach (var childRule in keyFramesRule.Rules.OfType<ICssKeyframeRule>().ToList())
                 {
-                    foreach (var childRule in keyFramesRule.Rules.OfType<ICssKeyframeRule>().ToList())
-                    {
-                        if (!SanitizeStyleRule(childRule, styleTag, baseUrl) && RemoveAtRule(styleTag, childRule))
-                            keyFramesRule.Remove(childRule.KeyText);
-                    }
-                }
-                else if (rule is ICssKeyframeRule keyFrameRule)
-                {
-                    SanitizeStyleDeclaration(styleTag, keyFrameRule.Style, baseUrl);
+                    if (!SanitizeStyleRule(childRule, styleTag, baseUrl) && RemoveAtRule(styleTag, childRule))
+                        keyFramesRule.Remove(childRule.KeyText);
                 }
             }
-
-            return true;
+            else if (rule is ICssKeyframeRule keyFrameRule)
+            {
+                SanitizeStyleDeclaration(styleTag, keyFrameRule.Style, baseUrl);
+            }
         }
 
-        /// <summary>
-        /// Performs post processing on all nodes in the document.
-        /// </summary>
-        /// <param name="dom">The HTML document.</param>
-        /// <param name="context">The node within which to post process all nodes.</param>
-        private void DoPostProcess(IHtmlDocument dom, INode? context)
+        return true;
+    }
+
+    /// <summary>
+    /// Performs post processing on all nodes in the document.
+    /// </summary>
+    /// <param name="dom">The HTML document.</param>
+    /// <param name="context">The node within which to post process all nodes.</param>
+    private void DoPostProcess(IHtmlDocument dom, INode? context)
+    {
+        if (PostProcessNode != null)
         {
-            if (PostProcessNode != null)
-            {
-                dom.Normalize();
+            dom.Normalize();
 
-                if (context != null)
+            if (context != null)
+            {
+                var nodes = GetAllNodes(context).ToList();
+                foreach (var node in nodes)
                 {
-                    var nodes = GetAllNodes(context).ToList();
-                    foreach (var node in nodes)
+                    var e = new PostProcessNodeEventArgs(dom, node);
+                    OnPostProcessNode(e);
+                    if (e.ReplacementNodes.Any())
                     {
-                        var e = new PostProcessNodeEventArgs(dom, node);
-                        OnPostProcessNode(e);
-                        if (e.ReplacementNodes.Any())
-                        {
-                            ((IChildNode)node).Replace([.. e.ReplacementNodes]);
-                        }
+                        ((IChildNode)node).Replace([.. e.ReplacementNodes]);
                     }
                 }
             }
-
-            if (PostProcessDom != null)
-            {
-                var e = new PostProcessDomEventArgs(dom);
-                OnPostProcessDom(e);
-            }
         }
 
-        /// <summary>
-        /// Determines whether the specified attribute can contain a URI.
-        /// </summary>
-        /// <param name="attribute">The attribute.</param>
-        /// <returns><c>true</c> if the attribute can contain a URI; otherwise, <c>false</c>.</returns>
-        private bool IsUriAttribute(IAttr attribute)
+        if (PostProcessDom != null)
         {
-            return UriAttributes.Contains(attribute.Name);
+            var e = new PostProcessDomEventArgs(dom);
+            OnPostProcessDom(e);
         }
+    }
 
-        /// <summary>
-        /// Determines whether the specified tag is allowed.
-        /// </summary>
-        /// <param name="tag">The tag.</param>
-        /// <returns><c>true</c> if the tag is allowed; otherwise, <c>false</c>.</returns>
-        private bool IsAllowedTag(IElement tag)
-        {
-            return AllowedTags.Contains(tag.NodeName);
-        }
+    /// <summary>
+    /// Determines whether the specified attribute can contain a URI.
+    /// </summary>
+    /// <param name="attribute">The attribute.</param>
+    /// <returns><c>true</c> if the attribute can contain a URI; otherwise, <c>false</c>.</returns>
+    private bool IsUriAttribute(IAttr attribute)
+    {
+        return UriAttributes.Contains(attribute.Name);
+    }
 
-        /// <summary>
-        /// Determines whether the specified attribute is allowed.
-        /// </summary>
-        /// <param name="attribute">The attribute.</param>
-        /// <returns><c>true</c> if the attribute is allowed; otherwise, <c>false</c>.</returns>
-        private bool IsAllowedAttribute(IAttr attribute)
-        {
-            return AllowedAttributes.Contains(attribute.Name)
-                // test html5 data- attributes
-                || (AllowDataAttributes && attribute.Name != null && attribute.Name.StartsWith("data-", StringComparison.OrdinalIgnoreCase));
-        }
+    /// <summary>
+    /// Determines whether the specified tag is allowed.
+    /// </summary>
+    /// <param name="tag">The tag.</param>
+    /// <returns><c>true</c> if the tag is allowed; otherwise, <c>false</c>.</returns>
+    private bool IsAllowedTag(IElement tag)
+    {
+        return AllowedTags.Contains(tag.NodeName);
+    }
+
+    /// <summary>
+    /// Determines whether the specified attribute is allowed.
+    /// </summary>
+    /// <param name="attribute">The attribute.</param>
+    /// <returns><c>true</c> if the attribute is allowed; otherwise, <c>false</c>.</returns>
+    private bool IsAllowedAttribute(IAttr attribute)
+    {
+        return AllowedAttributes.Contains(attribute.Name)
+            // test html5 data- attributes
+            || (AllowDataAttributes && attribute.Name != null && attribute.Name.StartsWith("data-", StringComparison.OrdinalIgnoreCase));
+    }
+
+    /// <summary>
+    /// Sanitizes the style.
+    /// </summary>
+    /// <param name="element">The element.</param>
+    /// <param name="baseUrl">The base URL.</param>
+    protected void SanitizeStyle(IElement element, string baseUrl)
+    {
+        // filter out invalid CSS declarations
+        // see https://github.com/AngleSharp/AngleSharp/issues/101
+        var attribute = element.GetAttribute(StyleAttributeName);
+        if (attribute == null)
+            return;
 
-        /// <summary>
-        /// Sanitizes the style.
-        /// </summary>
-        /// <param name="element">The element.</param>
-        /// <param name="baseUrl">The base URL.</param>
-        protected void SanitizeStyle(IElement element, string baseUrl)
+        if (element.GetStyle() == null)
         {
-            // filter out invalid CSS declarations
-            // see https://github.com/AngleSharp/AngleSharp/issues/101
-            var attribute = element.GetAttribute(StyleAttributeName);
-            if (attribute == null)
-                return;
+            element.RemoveAttribute(StyleAttributeName);
+            return;
+        }
 
-            if (element.GetStyle() == null)
-            {
-                element.RemoveAttribute(StyleAttributeName);
-                return;
-            }
+        element.SetAttribute(StyleAttributeName, element.GetStyle().ToCss(StyleFormatter));
 
-            element.SetAttribute(StyleAttributeName, element.GetStyle().ToCss(StyleFormatter));
+        var styles = element.GetStyle();
+        if (styles == null || styles.Length == 0)
+            return;
 
-            var styles = element.GetStyle();
-            if (styles == null || styles.Length == 0)
-                return;
+        SanitizeStyleDeclaration(element, styles, baseUrl);
+    }
 
-            SanitizeStyleDeclaration(element, styles, baseUrl);
-        }
+    /// <summary>
+    /// Verify if the given CSS property name is allowed. By default this will
+    /// check if the property is in the <see cref="AllowedCssProperties"/> set,
+    /// or if the property is a custom property and <see cref="AllowCssCustomProperties"/> is true.
+    /// </summary>
+    /// <param name="propertyName">The name of the CSS property.</param>
+    /// <returns>True if the property is allowed or not.</returns>
+    protected virtual bool IsAllowedCssProperty(string propertyName)
+    {
+        return AllowedCssProperties.Contains(propertyName)
+            || AllowCssCustomProperties && propertyName != null && propertyName.StartsWith("--");
+    }
 
-        /// <summary>
-        /// Verify if the given CSS property name is allowed. By default this will
-        /// check if the property is in the <see cref="AllowedCssProperties"/> set,
-        /// or if the property is a custom property and <see cref="AllowCssCustomProperties"/> is true.
-        /// </summary>
-        /// <param name="propertyName">The name of the CSS property.</param>
-        /// <returns>True if the property is allowed or not.</returns>
-        protected virtual bool IsAllowedCssProperty(string propertyName)
-        {
-            return AllowedCssProperties.Contains(propertyName)
-                || AllowCssCustomProperties && propertyName != null && propertyName.StartsWith("--");
-        }
+    private void SanitizeStyleDeclaration(IElement element, ICssStyleDeclaration styles, string baseUrl)
+    {
+        var removeStyles = new List<Tuple<ICssProperty, RemoveReason>>();
+        var setStyles = new Dictionary<string, string>();
 
-        private void SanitizeStyleDeclaration(IElement element, ICssStyleDeclaration styles, string baseUrl)
+        foreach (var style in styles)
         {
-            var removeStyles = new List<Tuple<ICssProperty, RemoveReason>>();
-            var setStyles = new Dictionary<string, string>();
+            var key = DecodeCss(style.Name);
+            var val = DecodeCss(style.Value);
 
-            foreach (var style in styles)
+            if (!IsAllowedCssProperty(key))
             {
-                var key = DecodeCss(style.Name);
-                var val = DecodeCss(style.Value);
-
-                if (!IsAllowedCssProperty(key))
-                {
-                    removeStyles.Add(new Tuple<ICssProperty, RemoveReason>(style, RemoveReason.NotAllowedStyle));
-                    continue;
-                }
+                removeStyles.Add(new Tuple<ICssProperty, RemoveReason>(style, RemoveReason.NotAllowedStyle));
+                continue;
+            }
 
-                if (CssExpression.IsMatch(val) || DisallowCssPropertyValue.IsMatch(val))
-                {
-                    removeStyles.Add(new Tuple<ICssProperty, RemoveReason>(style, RemoveReason.NotAllowedValue));
-                    continue;
-                }
+            if (CssExpression.IsMatch(val) || DisallowCssPropertyValue.IsMatch(val))
+            {
+                removeStyles.Add(new Tuple<ICssProperty, RemoveReason>(style, RemoveReason.NotAllowedValue));
+                continue;
+            }
 
-                val = WhitespaceRegex.Replace(val, string.Empty);
+            val = WhitespaceRegex.Replace(val, string.Empty);
 
-                var urls = CssUrl.Matches(val).Cast<Match>().Select(m => (Match: m, Url: SanitizeUrl(element, m.Groups[2].Value, baseUrl)));
+            var urls = CssUrl.Matches(val).Cast<Match>().Select(m => (Match: m, Url: SanitizeUrl(element, m.Groups[2].Value, baseUrl)));
 
-                if (urls.Any())
+            if (urls.Any())
+            {
+                if (urls.Any(u => u.Url == null))
+                    removeStyles.Add(new Tuple<ICssProperty, RemoveReason>(style, RemoveReason.NotAllowedUrlValue));
+                else
                 {
-                    if (urls.Any(u => u.Url == null))
-                        removeStyles.Add(new Tuple<ICssProperty, RemoveReason>(style, RemoveReason.NotAllowedUrlValue));
-                    else
-                    {
-                        var sb = new StringBuilder();
-                        var ix = 0;
+                    var sb = new StringBuilder();
+                    var ix = 0;
 
-                        foreach (var url in urls)
-                        {
-                            sb.Append(val, ix, url.Match.Index - ix);
-                            sb.Append("url(");
-                            sb.Append(url.Match.Groups[1].Value);
-                            sb.Append(url.Url);
-                            sb.Append(url.Match.Groups[3].Value);
-                            ix = url.Match.Index + url.Match.Length;
-                        }
+                    foreach (var url in urls)
+                    {
+                        sb.Append(val, ix, url.Match.Index - ix);
+                        sb.Append("url(");
+                        sb.Append(url.Match.Groups[1].Value);
+                        sb.Append(url.Url);
+                        sb.Append(url.Match.Groups[3].Value);
+                        ix = url.Match.Index + url.Match.Length;
+                    }
 
-                        sb.Append(val, ix, val.Length - ix);
+                    sb.Append(val, ix, val.Length - ix);
 
-                        var s = sb.ToString();
+                    var s = sb.ToString();
 
-                        if (s != val)
+                    if (s != val)
+                    {
+                        if (key != style.Name)
                         {
-                            if (key != style.Name)
-                            {
-                                removeStyles.Add(new Tuple<ICssProperty, RemoveReason>(style, RemoveReason.NotAllowedUrlValue));
-                            }
-                            setStyles[key] = s;
+                            removeStyles.Add(new Tuple<ICssProperty, RemoveReason>(style, RemoveReason.NotAllowedUrlValue));
                         }
+                        setStyles[key] = s;
                     }
                 }
             }
+        }
 
-            foreach (var style in setStyles)
-            {
-                styles.SetProperty(style.Key, style.Value);
-            }
-
-            foreach (var style in removeStyles)
-            {
-                RemoveStyle(element, styles, style.Item1, style.Item2);
-            }
+        foreach (var style in setStyles)
+        {
+            styles.SetProperty(style.Key, style.Value);
         }
 
-        /// <summary>
-        /// Decodes CSS Unicode escapes and removes comments.
-        /// </summary>
-        /// <param name="css">The CSS string.</param>
-        /// <returns>The decoded CSS string.</returns>
-        protected static string DecodeCss(string css)
+        foreach (var style in removeStyles)
         {
-            var r = CssUnicodeEscapes.Replace(css, m =>
-            {
-                if (m.Groups[1].Success)
-                    return ((char)int.Parse(m.Groups[1].Value, NumberStyles.HexNumber)).ToString();
-                var t = m.Groups[2].Value;
-                return t == "\\" ? @"\\" : t;
-            });
+            RemoveStyle(element, styles, style.Item1, style.Item2);
+        }
+    }
 
-            r = CssComments.Replace(r, m => "");
+    /// <summary>
+    /// Decodes CSS Unicode escapes and removes comments.
+    /// </summary>
+    /// <param name="css">The CSS string.</param>
+    /// <returns>The decoded CSS string.</returns>
+    protected static string DecodeCss(string css)
+    {
+        var r = CssUnicodeEscapes.Replace(css, m =>
+        {
+            if (m.Groups[1].Success)
+                return ((char)int.Parse(m.Groups[1].Value, NumberStyles.HexNumber)).ToString();
+            var t = m.Groups[2].Value;
+            return t == "\\" ? @"\\" : t;
+        });
 
-            return r;
-        }
+        r = CssComments.Replace(r, m => "");
 
-        private static readonly Regex SchemeRegex = new(@"^([^\/#]*?)(?:\:|&#0*58|&#x0*3a)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+        return r;
+    }
 
-        /// <summary>
-        /// Tries to create a safe <see cref="Iri"/> object from a string.
-        /// </summary>
-        /// <param name="url">The URL.</param>
-        /// <returns>The <see cref="Iri"/> object or null if no safe <see cref="Iri"/> can be created.</returns>
-        protected Iri? GetSafeIri(string url)
-        {
-            url = url.TrimStart();
+    private static readonly Regex SchemeRegex = new(@"^([^\/#]*?)(?:\:|&#0*58|&#x0*3a)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
 
-            var schemeMatch = SchemeRegex.Match(url);
+    /// <summary>
+    /// Tries to create a safe <see cref="Iri"/> object from a string.
+    /// </summary>
+    /// <param name="url">The URL.</param>
+    /// <returns>The <see cref="Iri"/> object or null if no safe <see cref="Iri"/> can be created.</returns>
+    protected Iri? GetSafeIri(string url)
+    {
+        url = url.TrimStart();
 
-            if (schemeMatch.Success)
-            {
-                var scheme = schemeMatch.Groups[1].Value;
-                return AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase) ? new Iri(url, scheme) : null;
-            }
+        var schemeMatch = SchemeRegex.Match(url);
 
-            return new Iri(url);
+        if (schemeMatch.Success)
+        {
+            var scheme = schemeMatch.Groups[1].Value;
+            return AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase) ? new Iri(url, scheme) : null;
         }
 
-        /// <summary>
-        /// Sanitizes a URL.
-        /// </summary>
-        /// <param name="element">The tag containing the URL being sanitized.</param>
-        /// <param name="url">The URL.</param>
-        /// <param name="baseUrl">The base URL relative URLs are resolved against (empty or null for no resolution).</param>
-        /// <returns>The sanitized URL or <c>null</c> if no safe URL can be created.</returns>
-        protected virtual string? SanitizeUrl(IElement element, string url, string baseUrl)
-        {
-            var iri = GetSafeIri(url);
+        return new Iri(url);
+    }
+
+    /// <summary>
+    /// Sanitizes a URL.
+    /// </summary>
+    /// <param name="element">The tag containing the URL being sanitized.</param>
+    /// <param name="url">The URL.</param>
+    /// <param name="baseUrl">The base URL relative URLs are resolved against (empty or null for no resolution).</param>
+    /// <returns>The sanitized URL or <c>null</c> if no safe URL can be created.</returns>
+    protected virtual string? SanitizeUrl(IElement element, string url, string baseUrl)
+    {
+        var iri = GetSafeIri(url);
 
-            if (iri != null && !iri.IsAbsolute && !string.IsNullOrEmpty(baseUrl))
+        if (iri != null && !iri.IsAbsolute && !string.IsNullOrEmpty(baseUrl))
+        {
+            // resolve relative URI
+            if (Uri.TryCreate(baseUrl, UriKind.Absolute, out Uri baseUri))
             {
-                // resolve relative URI
-                if (Uri.TryCreate(baseUrl, UriKind.Absolute, out Uri baseUri))
+                try
                 {
-                    try
-                    {
-                        var sanitizedUrl = new Uri(baseUri, iri.Value).AbsoluteUri;
-                        var ev = new FilterUrlEventArgs(element, url, sanitizedUrl);
+                    var sanitizedUrl = new Uri(baseUri, iri.Value).AbsoluteUri;
+                    var ev = new FilterUrlEventArgs(element, url, sanitizedUrl);
 
-                        OnFilteringUrl(ev);
+                    OnFilteringUrl(ev);
 
-                        return ev.SanitizedUrl;
-                    }
-                    catch (UriFormatException)
-                    {
-                        iri = null;
-                    }
+                    return ev.SanitizedUrl;
+                }
+                catch (UriFormatException)
+                {
+                    iri = null;
                 }
-                else iri = null;
             }
+            else iri = null;
+        }
 
-            var e = new FilterUrlEventArgs(element, url, iri?.Value);
-            OnFilteringUrl(e);
+        var e = new FilterUrlEventArgs(element, url, iri?.Value);
+        OnFilteringUrl(e);
 
-            return e.SanitizedUrl;
-        }
+        return e.SanitizedUrl;
+    }
 
-        /// <summary>
-        /// Removes a tag from the document.
-        /// </summary>
-        /// <param name="tag">Tag to be removed.</param>
-        /// <param name="reason">Reason for removal.</param>
-        private void RemoveTag(IElement tag, RemoveReason reason)
-        {
-            var e = new RemovingTagEventArgs(tag, reason);
-            OnRemovingTag(e);
+    /// <summary>
+    /// Removes a tag from the document.
+    /// </summary>
+    /// <param name="tag">Tag to be removed.</param>
+    /// <param name="reason">Reason for removal.</param>
+    private void RemoveTag(IElement tag, RemoveReason reason)
+    {
+        var e = new RemovingTagEventArgs(tag, reason);
+        OnRemovingTag(e);
 
-            if (!e.Cancel)
-            {
-                if (KeepChildNodes && tag.HasChildNodes)
-                    tag.Replace([.. tag.ChildNodes]);
-                else
-                    tag.Remove();
-            }
+        if (!e.Cancel)
+        {
+            if (KeepChildNodes && tag.HasChildNodes)
+                tag.Replace([.. tag.ChildNodes]);
+            else
+                tag.Remove();
         }
+    }
 
-        /// <summary>
-        /// Removes an attribute from the document.
-        /// </summary>
-        /// <param name="tag">Tag the attribute belongs to.</param>
-        /// <param name="attribute">Attribute to be removed.</param>
-        /// <param name="reason">Reason for removal.</param>
-        private void RemoveAttribute(IElement tag, IAttr attribute, RemoveReason reason)
-        {
-            var e = new RemovingAttributeEventArgs(tag, attribute, reason);
-            OnRemovingAttribute(e);
+    /// <summary>
+    /// Removes an attribute from the document.
+    /// </summary>
+    /// <param name="tag">Tag the attribute belongs to.</param>
+    /// <param name="attribute">Attribute to be removed.</param>
+    /// <param name="reason">Reason for removal.</param>
+    private void RemoveAttribute(IElement tag, IAttr attribute, RemoveReason reason)
+    {
+        var e = new RemovingAttributeEventArgs(tag, attribute, reason);
+        OnRemovingAttribute(e);
 
-            if (!e.Cancel)
-                tag.RemoveAttribute(attribute.Name);
-        }
+        if (!e.Cancel)
+            tag.RemoveAttribute(attribute.Name);
+    }
 
-        /// <summary>
-        /// Removes a style from the document.
-        /// </summary>
-        /// <param name="tag">Tag the style belongs to.</param>
-        /// <param name="styles">Style rule that contains the style to be removed.</param>
-        /// <param name="style">Style to be removed.</param>
-        /// <param name="reason">Reason for removal.</param>
-        private void RemoveStyle(IElement tag, ICssStyleDeclaration styles, ICssProperty style, RemoveReason reason)
-        {
-            var e = new RemovingStyleEventArgs(tag, style, reason);
-            OnRemovingStyle(e);
+    /// <summary>
+    /// Removes a style from the document.
+    /// </summary>
+    /// <param name="tag">Tag the style belongs to.</param>
+    /// <param name="styles">Style rule that contains the style to be removed.</param>
+    /// <param name="style">Style to be removed.</param>
+    /// <param name="reason">Reason for removal.</param>
+    private void RemoveStyle(IElement tag, ICssStyleDeclaration styles, ICssProperty style, RemoveReason reason)
+    {
+        var e = new RemovingStyleEventArgs(tag, style, reason);
+        OnRemovingStyle(e);
 
-            if (!e.Cancel)
-                styles.RemoveProperty(style.Name);
-        }
+        if (!e.Cancel)
+            styles.RemoveProperty(style.Name);
+    }
 
-        /// <summary>
-        /// Removes an at-rule from the document.
-        /// </summary>
-        /// <param name="tag">Tag the style belongs to.</param>
-        /// <param name="rule">Rule to be removed.</param>
-        /// <returns><c>true</c>, if the rule can be removed; <c>false</c>, otherwise.</returns>
-        private bool RemoveAtRule(IElement tag, ICssRule rule)
-        {
-            var e = new RemovingAtRuleEventArgs(tag, rule);
-            OnRemovingAtRule(e);
+    /// <summary>
+    /// Removes an at-rule from the document.
+    /// </summary>
+    /// <param name="tag">Tag the style belongs to.</param>
+    /// <param name="rule">Rule to be removed.</param>
+    /// <returns><c>true</c>, if the rule can be removed; <c>false</c>, otherwise.</returns>
+    private bool RemoveAtRule(IElement tag, ICssRule rule)
+    {
+        var e = new RemovingAtRuleEventArgs(tag, rule);
+        OnRemovingAtRule(e);
 
-            return !e.Cancel;
-        }
+        return !e.Cancel;
+    }
 
-        /// <summary>
-        /// Removes a CSS class from a class attribute.
-        /// </summary>
-        /// <param name="tag">Tag the style belongs to.</param>
-        /// <param name="cssClass">Class to be removed.</param>
-        /// <param name="reason">Reason for removal.</param>
-        private void RemoveCssClass(IElement tag, string cssClass, RemoveReason reason)
-        {
-            var e = new RemovingCssClassEventArgs(tag, cssClass, reason);
-            OnRemovingCssClass(e);
+    /// <summary>
+    /// Removes a CSS class from a class attribute.
+    /// </summary>
+    /// <param name="tag">Tag the style belongs to.</param>
+    /// <param name="cssClass">Class to be removed.</param>
+    /// <param name="reason">Reason for removal.</param>
+    private void RemoveCssClass(IElement tag, string cssClass, RemoveReason reason)
+    {
+        var e = new RemovingCssClassEventArgs(tag, cssClass, reason);
+        OnRemovingCssClass(e);
 
-            if (!e.Cancel)
-                tag.ClassList.Remove(cssClass);
-        }
+        if (!e.Cancel)
+            tag.ClassList.Remove(cssClass);
     }
 }

+ 347 - 348
Masuit.Tools.Abstractions/HtmlSanitizer/HtmlSanitizerDefaults.cs

@@ -3,362 +3,361 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Immutable;
 
-namespace Ganss.Xss
+namespace Ganss.Xss;
+
+/// <summary>
+/// Default options.
+/// </summary>
+public static class HtmlSanitizerDefaults
 {
     /// <summary>
-    /// Default options.
+    /// The default allowed CSS at-rules.
     /// </summary>
-    public static class HtmlSanitizerDefaults
+    public static ISet<CssRuleType> AllowedAtRules { get; } = new HashSet<CssRuleType>()
     {
-        /// <summary>
-        /// The default allowed CSS at-rules.
-        /// </summary>
-        public static ISet<CssRuleType> AllowedAtRules { get; } = new HashSet<CssRuleType>()
-        {
-            CssRuleType.Style, CssRuleType.Namespace
-        }.ToImmutableHashSet();
+        CssRuleType.Style, CssRuleType.Namespace
+    }.ToImmutableHashSet();
 
-        /// <summary>
-        /// The default allowed URI schemes.
-        /// </summary>
-        public static ISet<string> AllowedSchemes { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
-        {
-            "http", "https"
-        }.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
+    /// <summary>
+    /// The default allowed URI schemes.
+    /// </summary>
+    public static ISet<string> AllowedSchemes { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+    {
+        "http", "https"
+    }.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
 
-        /// <summary>
-        /// The default allowed HTML tag names.
-        /// </summary>
-        public static ISet<string> AllowedTags { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
-        {
-            // https://developer.mozilla.org/en/docs/Web/Guide/HTML/HTML5/HTML5_element_list
-            "a", "abbr", "acronym", "address", "area", "b",
-            "big", "blockquote", "br", "button", "caption", "center", "cite",
-            "code", "col", "colgroup", "dd", "del", "dfn", "dir", "div", "dl", "dt",
-            "em", "fieldset", "font", "form", "h1", "h2", "h3", "h4", "h5", "h6",
-            "hr", "i", "img", "input", "ins", "kbd", "label", "legend", "li", "map",
-            "menu", "ol", "optgroup", "option", "p", "pre", "q", "s", "samp",
-            "select", "small", "span", "strike", "strong", "sub", "sup", "table",
-            "tbody", "td", "textarea", "tfoot", "th", "thead", "tr", "tt", "u",
-            "ul", "var",
-            // HTML5
-            // Sections
-            "section", "nav", "article", "aside", "header", "footer", "main",
-            // Grouping content
-            "figure", "figcaption",
-            // Text-level semantics
-            "data", "time", "mark", "ruby", "rt", "rp", "bdi", "wbr",
-            // Forms
-            "datalist", "keygen", "output", "progress", "meter",
-            // Interactive elements
-            "details", "summary", "menuitem",
-            // document elements
-            "html", "head", "body"
-        }.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
+    /// <summary>
+    /// The default allowed HTML tag names.
+    /// </summary>
+    public static ISet<string> AllowedTags { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+    {
+        // https://developer.mozilla.org/en/docs/Web/Guide/HTML/HTML5/HTML5_element_list
+        "a", "abbr", "acronym", "address", "area", "b",
+        "big", "blockquote", "br", "button", "caption", "center", "cite",
+        "code", "col", "colgroup", "dd", "del", "dfn", "dir", "div", "dl", "dt",
+        "em", "fieldset", "font", "form", "h1", "h2", "h3", "h4", "h5", "h6",
+        "hr", "i", "img", "input", "ins", "kbd", "label", "legend", "li", "map",
+        "menu", "ol", "optgroup", "option", "p", "pre", "q", "s", "samp",
+        "select", "small", "span", "strike", "strong", "sub", "sup", "table",
+        "tbody", "td", "textarea", "tfoot", "th", "thead", "tr", "tt", "u",
+        "ul", "var",
+        // HTML5
+        // Sections
+        "section", "nav", "article", "aside", "header", "footer", "main",
+        // Grouping content
+        "figure", "figcaption",
+        // Text-level semantics
+        "data", "time", "mark", "ruby", "rt", "rp", "bdi", "wbr",
+        // Forms
+        "datalist", "keygen", "output", "progress", "meter",
+        // Interactive elements
+        "details", "summary", "menuitem",
+        // document elements
+        "html", "head", "body"
+    }.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
 
-        /// <summary>
-        /// The default allowed HTML attributes.
-        /// </summary>
-        public static ISet<string> AllowedAttributes { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
-        {
-            // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
-            "abbr", "accept", "accept-charset", "accesskey",
-            "action", "align", "alt", "axis", "bgcolor", "border", "cellpadding",
-            "cellspacing", "char", "charoff", "charset", "checked", "cite", /* "class", */
-            "clear", "cols", "colspan", "color", "compact", "coords", "datetime",
-            "dir", "disabled", "enctype", "for", "frame", "headers", "height",
-            "href", "hreflang", "hspace", /* "id", */ "ismap", "label", "lang",
-            "longdesc", "maxlength", "media", "method", "multiple", "name",
-            "nohref", "noshade", "nowrap", "prompt", "readonly", "rel", "rev",
-            "rows", "rowspan", "rules", "scope", "selected", "shape", "size",
-            "span", "src", "start", "style", "summary", "tabindex", "target", "title",
-            "type", "usemap", "valign", "value", "vspace", "width",
-            // HTML5
-            "high", // <meter>
-            "keytype", // <keygen>
-            "list", // <input>
-            "low", // <meter>
-            "max", // <input>, <meter>, <progress>
-            "min", // <input>, <meter>
-            "novalidate", // <form>
-            "open", // <details>
-            "optimum", // <meter>
-            "pattern", // <input>
-            "placeholder", // <input>, <textarea>
-            "pubdate", // <time>
-            "radiogroup", // <menuitem>
-            "required", // <input>, <select>, <textarea>
-            "reversed", // <ol>
-            "spellcheck", // Global attribute
-            "step", // <input>
-            "wrap", // <textarea>
-            "challenge", // <keygen>
-            "contenteditable", // Global attribute
-            "draggable", // Global attribute
-            "dropzone", // Global attribute
-            "autocomplete", // <form>, <input>
-            "autosave", // <input>
-        }.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
+    /// <summary>
+    /// The default allowed HTML attributes.
+    /// </summary>
+    public static ISet<string> AllowedAttributes { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+    {
+        // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
+        "abbr", "accept", "accept-charset", "accesskey",
+        "action", "align", "alt", "axis", "bgcolor", "border", "cellpadding",
+        "cellspacing", "char", "charoff", "charset", "checked", "cite", /* "class", */
+        "clear", "cols", "colspan", "color", "compact", "coords", "datetime",
+        "dir", "disabled", "enctype", "for", "frame", "headers", "height",
+        "href", "hreflang", "hspace", /* "id", */ "ismap", "label", "lang",
+        "longdesc", "maxlength", "media", "method", "multiple", "name",
+        "nohref", "noshade", "nowrap", "prompt", "readonly", "rel", "rev",
+        "rows", "rowspan", "rules", "scope", "selected", "shape", "size",
+        "span", "src", "start", "style", "summary", "tabindex", "target", "title",
+        "type", "usemap", "valign", "value", "vspace", "width",
+        // HTML5
+        "high", // <meter>
+        "keytype", // <keygen>
+        "list", // <input>
+        "low", // <meter>
+        "max", // <input>, <meter>, <progress>
+        "min", // <input>, <meter>
+        "novalidate", // <form>
+        "open", // <details>
+        "optimum", // <meter>
+        "pattern", // <input>
+        "placeholder", // <input>, <textarea>
+        "pubdate", // <time>
+        "radiogroup", // <menuitem>
+        "required", // <input>, <select>, <textarea>
+        "reversed", // <ol>
+        "spellcheck", // Global attribute
+        "step", // <input>
+        "wrap", // <textarea>
+        "challenge", // <keygen>
+        "contenteditable", // Global attribute
+        "draggable", // Global attribute
+        "dropzone", // Global attribute
+        "autocomplete", // <form>, <input>
+        "autosave", // <input>
+    }.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
 
-        /// <summary>
-        /// The default URI attributes.
-        /// </summary>
-        public static ISet<string> UriAttributes { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
-        {
-            "action", "background", "dynsrc", "href", "lowsrc", "src"
-        }.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
+    /// <summary>
+    /// The default URI attributes.
+    /// </summary>
+    public static ISet<string> UriAttributes { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+    {
+        "action", "background", "dynsrc", "href", "lowsrc", "src"
+    }.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
 
-        /// <summary>
-        /// The default allowed CSS properties.
-        /// </summary>
-        public static ISet<string> AllowedCssProperties { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
-        {
-            // CSS 3 properties <http://www.w3.org/TR/CSS/#properties>
-            "align-content",
-            "align-items",
-            "align-self",
-            "all",
-            "animation",
-            "animation-delay",
-            "animation-direction",
-            "animation-duration",
-            "animation-fill-mode",
-            "animation-iteration-count",
-            "animation-name",
-            "animation-play-state",
-            "animation-timing-function",
-            "backface-visibility",
-            "background",
-            "background-attachment",
-            "background-blend-mode",
-            "background-clip",
-            "background-color",
-            "background-image",
-            "background-origin",
-            "background-position",
-            "background-position-x",
-            "background-position-y",
-            "background-repeat",
-            "background-repeat-x", // see https://github.com/mganss/HtmlSanitizer/issues/243
-            "background-repeat-y",
-            "background-size",
-            "border",
-            "border-bottom",
-            "border-bottom-color",
-            "border-bottom-left-radius",
-            "border-bottom-right-radius",
-            "border-bottom-style",
-            "border-bottom-width",
-            "border-collapse",
-            "border-color",
-            "border-image",
-            "border-image-outset",
-            "border-image-repeat",
-            "border-image-slice",
-            "border-image-source",
-            "border-image-width",
-            "border-left",
-            "border-left-color",
-            "border-left-style",
-            "border-left-width",
-            "border-radius",
-            "border-right",
-            "border-right-color",
-            "border-right-style",
-            "border-right-width",
-            "border-spacing",
-            "border-style",
-            "border-top",
-            "border-top-color",
-            "border-top-left-radius",
-            "border-top-right-radius",
-            "border-top-style",
-            "border-top-width",
-            "border-width",
-            "bottom",
-            "box-decoration-break",
-            "box-shadow",
-            "box-sizing",
-            "break-after",
-            "break-before",
-            "break-inside",
-            "caption-side",
-            "caret-color",
-            "clear",
-            "clip",
-            "color",
-            "column-count",
-            "column-fill",
-            "column-gap",
-            "column-rule",
-            "column-rule-color",
-            "column-rule-style",
-            "column-rule-width",
-            "column-span",
-            "column-width",
-            "columns",
-            "content",
-            "counter-increment",
-            "counter-reset",
-            "cursor",
-            "direction",
-            "display",
-            "empty-cells",
-            "filter",
-            "flex",
-            "flex-basis",
-            "flex-direction",
-            "flex-flow",
-            "flex-grow",
-            "flex-shrink",
-            "flex-wrap",
-            "float",
-            "font",
-            "font-family",
-            "font-feature-settings",
-            "font-kerning",
-            "font-language-override",
-            "font-size",
-            "font-size-adjust",
-            "font-stretch",
-            "font-style",
-            "font-synthesis",
-            "font-variant",
-            "font-variant-alternates",
-            "font-variant-caps",
-            "font-variant-east-asian",
-            "font-variant-ligatures",
-            "font-variant-numeric",
-            "font-variant-position",
-            "font-weight",
-            "gap",
-            "grid",
-            "grid-area",
-            "grid-auto-columns",
-            "grid-auto-flow",
-            "grid-auto-rows",
-            "grid-column",
-            "grid-column-end",
-            "grid-column-gap",
-            "grid-column-start",
-            "grid-gap",
-            "grid-row",
-            "grid-row-end",
-            "grid-row-gap",
-            "grid-row-start",
-            "grid-template",
-            "grid-template-areas",
-            "grid-template-columns",
-            "grid-template-rows",
-            "hanging-punctuation",
-            "height",
-            "hyphens",
-            "image-rendering",
-            "isolation",
-            "justify-content",
-            "left",
-            "letter-spacing",
-            "line-break",
-            "line-height",
-            "list-style",
-            "list-style-image",
-            "list-style-position",
-            "list-style-type",
-            "margin",
-            "margin-bottom",
-            "margin-left",
-            "margin-right",
-            "margin-top",
-            "mask",
-            "mask-clip",
-            "mask-composite",
-            "mask-image",
-            "mask-mode",
-            "mask-origin",
-            "mask-position",
-            "mask-repeat",
-            "mask-size",
-            "mask-type",
-            "max-height",
-            "max-width",
-            "min-height",
-            "min-width",
-            "mix-blend-mode",
-            "object-fit",
-            "object-position",
-            "opacity",
-            "order",
-            "orphans",
-            "outline",
-            "outline-color",
-            "outline-offset",
-            "outline-style",
-            "outline-width",
-            "overflow",
-            "overflow-wrap",
-            "overflow-x",
-            "overflow-y",
-            "padding",
-            "padding-bottom",
-            "padding-left",
-            "padding-right",
-            "padding-top",
-            "page-break-after",
-            "page-break-before",
-            "page-break-inside",
-            "perspective",
-            "perspective-origin",
-            "pointer-events",
-            "position",
-            "quotes",
-            "resize",
-            "right",
-            "row-gap",
-            "scroll-behavior",
-            "tab-size",
-            "table-layout",
-            "text-align",
-            "text-align-last",
-            "text-combine-upright",
-            "text-decoration",
-            "text-decoration-color",
-            "text-decoration-line",
-            "text-decoration-skip",
-            "text-decoration-style",
-            "text-indent",
-            "text-justify",
-            "text-orientation",
-            "text-overflow",
-            "text-shadow",
-            "text-transform",
-            "text-underline-position",
-            "top",
-            "transform",
-            "transform-origin",
-            "transform-style",
-            "transition",
-            "transition-delay",
-            "transition-duration",
-            "transition-property",
-            "transition-timing-function",
-            "unicode-bidi",
-            "user-select",
-            "vertical-align",
-            "visibility",
-            "white-space",
-            "widows",
-            "width",
-            "word-break",
-            "word-spacing",
-            "word-wrap",
-            "writing-mode",
-            "z-index"
-        }.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
+    /// <summary>
+    /// The default allowed CSS properties.
+    /// </summary>
+    public static ISet<string> AllowedCssProperties { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+    {
+        // CSS 3 properties <http://www.w3.org/TR/CSS/#properties>
+        "align-content",
+        "align-items",
+        "align-self",
+        "all",
+        "animation",
+        "animation-delay",
+        "animation-direction",
+        "animation-duration",
+        "animation-fill-mode",
+        "animation-iteration-count",
+        "animation-name",
+        "animation-play-state",
+        "animation-timing-function",
+        "backface-visibility",
+        "background",
+        "background-attachment",
+        "background-blend-mode",
+        "background-clip",
+        "background-color",
+        "background-image",
+        "background-origin",
+        "background-position",
+        "background-position-x",
+        "background-position-y",
+        "background-repeat",
+        "background-repeat-x", // see https://github.com/mganss/HtmlSanitizer/issues/243
+        "background-repeat-y",
+        "background-size",
+        "border",
+        "border-bottom",
+        "border-bottom-color",
+        "border-bottom-left-radius",
+        "border-bottom-right-radius",
+        "border-bottom-style",
+        "border-bottom-width",
+        "border-collapse",
+        "border-color",
+        "border-image",
+        "border-image-outset",
+        "border-image-repeat",
+        "border-image-slice",
+        "border-image-source",
+        "border-image-width",
+        "border-left",
+        "border-left-color",
+        "border-left-style",
+        "border-left-width",
+        "border-radius",
+        "border-right",
+        "border-right-color",
+        "border-right-style",
+        "border-right-width",
+        "border-spacing",
+        "border-style",
+        "border-top",
+        "border-top-color",
+        "border-top-left-radius",
+        "border-top-right-radius",
+        "border-top-style",
+        "border-top-width",
+        "border-width",
+        "bottom",
+        "box-decoration-break",
+        "box-shadow",
+        "box-sizing",
+        "break-after",
+        "break-before",
+        "break-inside",
+        "caption-side",
+        "caret-color",
+        "clear",
+        "clip",
+        "color",
+        "column-count",
+        "column-fill",
+        "column-gap",
+        "column-rule",
+        "column-rule-color",
+        "column-rule-style",
+        "column-rule-width",
+        "column-span",
+        "column-width",
+        "columns",
+        "content",
+        "counter-increment",
+        "counter-reset",
+        "cursor",
+        "direction",
+        "display",
+        "empty-cells",
+        "filter",
+        "flex",
+        "flex-basis",
+        "flex-direction",
+        "flex-flow",
+        "flex-grow",
+        "flex-shrink",
+        "flex-wrap",
+        "float",
+        "font",
+        "font-family",
+        "font-feature-settings",
+        "font-kerning",
+        "font-language-override",
+        "font-size",
+        "font-size-adjust",
+        "font-stretch",
+        "font-style",
+        "font-synthesis",
+        "font-variant",
+        "font-variant-alternates",
+        "font-variant-caps",
+        "font-variant-east-asian",
+        "font-variant-ligatures",
+        "font-variant-numeric",
+        "font-variant-position",
+        "font-weight",
+        "gap",
+        "grid",
+        "grid-area",
+        "grid-auto-columns",
+        "grid-auto-flow",
+        "grid-auto-rows",
+        "grid-column",
+        "grid-column-end",
+        "grid-column-gap",
+        "grid-column-start",
+        "grid-gap",
+        "grid-row",
+        "grid-row-end",
+        "grid-row-gap",
+        "grid-row-start",
+        "grid-template",
+        "grid-template-areas",
+        "grid-template-columns",
+        "grid-template-rows",
+        "hanging-punctuation",
+        "height",
+        "hyphens",
+        "image-rendering",
+        "isolation",
+        "justify-content",
+        "left",
+        "letter-spacing",
+        "line-break",
+        "line-height",
+        "list-style",
+        "list-style-image",
+        "list-style-position",
+        "list-style-type",
+        "margin",
+        "margin-bottom",
+        "margin-left",
+        "margin-right",
+        "margin-top",
+        "mask",
+        "mask-clip",
+        "mask-composite",
+        "mask-image",
+        "mask-mode",
+        "mask-origin",
+        "mask-position",
+        "mask-repeat",
+        "mask-size",
+        "mask-type",
+        "max-height",
+        "max-width",
+        "min-height",
+        "min-width",
+        "mix-blend-mode",
+        "object-fit",
+        "object-position",
+        "opacity",
+        "order",
+        "orphans",
+        "outline",
+        "outline-color",
+        "outline-offset",
+        "outline-style",
+        "outline-width",
+        "overflow",
+        "overflow-wrap",
+        "overflow-x",
+        "overflow-y",
+        "padding",
+        "padding-bottom",
+        "padding-left",
+        "padding-right",
+        "padding-top",
+        "page-break-after",
+        "page-break-before",
+        "page-break-inside",
+        "perspective",
+        "perspective-origin",
+        "pointer-events",
+        "position",
+        "quotes",
+        "resize",
+        "right",
+        "row-gap",
+        "scroll-behavior",
+        "tab-size",
+        "table-layout",
+        "text-align",
+        "text-align-last",
+        "text-combine-upright",
+        "text-decoration",
+        "text-decoration-color",
+        "text-decoration-line",
+        "text-decoration-skip",
+        "text-decoration-style",
+        "text-indent",
+        "text-justify",
+        "text-orientation",
+        "text-overflow",
+        "text-shadow",
+        "text-transform",
+        "text-underline-position",
+        "top",
+        "transform",
+        "transform-origin",
+        "transform-style",
+        "transition",
+        "transition-delay",
+        "transition-duration",
+        "transition-property",
+        "transition-timing-function",
+        "unicode-bidi",
+        "user-select",
+        "vertical-align",
+        "visibility",
+        "white-space",
+        "widows",
+        "width",
+        "word-break",
+        "word-spacing",
+        "word-wrap",
+        "writing-mode",
+        "z-index"
+    }.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
 
-        /// <summary>
-        /// The default allowed CSS classes.
-        /// </summary>
-        public static ISet<string> AllowedClasses { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
-            .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
-    }
+    /// <summary>
+    /// The default allowed CSS classes.
+    /// </summary>
+    public static ISet<string> AllowedClasses { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
 }

+ 49 - 50
Masuit.Tools.Abstractions/HtmlSanitizer/HtmlSanitizerOptions.cs

@@ -2,56 +2,55 @@ using AngleSharp.Css.Dom;
 using System;
 using System.Collections.Generic;
 
-namespace Ganss.Xss
+namespace Ganss.Xss;
+
+/// <summary>
+/// Provides options to be used with <see cref="HtmlSanitizer"/>.
+/// </summary>
+public class HtmlSanitizerOptions
 {
     /// <summary>
-    /// Provides options to be used with <see cref="HtmlSanitizer"/>.
-    /// </summary>
-    public class HtmlSanitizerOptions
-    {
-        /// <summary>
-        /// Gets or sets the allowed tag names such as "a" and "div".
-        /// </summary>
-        public ISet<string> AllowedTags { get; set; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
-
-        /// <summary>
-        /// Gets or sets the allowed HTML attributes such as "href" and "alt".
-        /// </summary>
-        public ISet<string> AllowedAttributes { get; set; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
-
-        /// <summary>
-        /// Gets or sets the allowed CSS classes.
-        /// </summary>
-        public ISet<string> AllowedCssClasses { get; set; } = new HashSet<string>();
-
-        /// <summary>
-        /// Gets or sets the allowed CSS properties such as "font" and "margin".
-        /// </summary>
-        public ISet<string> AllowedCssProperties { get; set; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
-
-        /// <summary>
-        /// Gets or sets the allowed CSS at-rules such as "@media" and "@font-face".
-        /// </summary>
-        public ISet<CssRuleType> AllowedAtRules { get; set; } = new HashSet<CssRuleType>();
-
-        /// <summary>
-        /// Gets or sets the allowed URI schemes such as "http" and "https".
-        /// </summary>
-        public ISet<string> AllowedSchemes { get; set; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
-
-        /// <summary>
-        /// Gets or sets the HTML attributes that can contain a URI such as "href".
-        /// </summary>
-        public ISet<string> UriAttributes { get; set; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
-
-        /// <summary>
-        /// Allow all custom CSS properties (variables) prefixed with <c>--</c>.
-        /// </summary>
-        public bool AllowCssCustomProperties { get; set; }
-
-        /// <summary>
-        /// Allow all HTML5 data attributes; the attributes prefixed with <c>data-</c>.
-        /// </summary>
-        public bool AllowDataAttributes { get; set; }
-    }
+    /// Gets or sets the allowed tag names such as "a" and "div".
+    /// </summary>
+    public ISet<string> AllowedTags { get; set; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+    /// <summary>
+    /// Gets or sets the allowed HTML attributes such as "href" and "alt".
+    /// </summary>
+    public ISet<string> AllowedAttributes { get; set; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+    /// <summary>
+    /// Gets or sets the allowed CSS classes.
+    /// </summary>
+    public ISet<string> AllowedCssClasses { get; set; } = new HashSet<string>();
+
+    /// <summary>
+    /// Gets or sets the allowed CSS properties such as "font" and "margin".
+    /// </summary>
+    public ISet<string> AllowedCssProperties { get; set; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+    /// <summary>
+    /// Gets or sets the allowed CSS at-rules such as "@media" and "@font-face".
+    /// </summary>
+    public ISet<CssRuleType> AllowedAtRules { get; set; } = new HashSet<CssRuleType>();
+
+    /// <summary>
+    /// Gets or sets the allowed URI schemes such as "http" and "https".
+    /// </summary>
+    public ISet<string> AllowedSchemes { get; set; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+    /// <summary>
+    /// Gets or sets the HTML attributes that can contain a URI such as "href".
+    /// </summary>
+    public ISet<string> UriAttributes { get; set; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+    /// <summary>
+    /// Allow all custom CSS properties (variables) prefixed with <c>--</c>.
+    /// </summary>
+    public bool AllowCssCustomProperties { get; set; }
+
+    /// <summary>
+    /// Allow all HTML5 data attributes; the attributes prefixed with <c>data-</c>.
+    /// </summary>
+    public bool AllowDataAttributes { get; set; }
 }

+ 170 - 166
Masuit.Tools.Abstractions/HtmlSanitizer/IHtmlSanitizer.cs

@@ -6,172 +6,176 @@ using System;
 using System.Collections.Generic;
 using System.Text.RegularExpressions;
 
-namespace Ganss.Xss
+namespace Ganss.Xss;
+
+/// <summary>
+/// Enables an inheriting class to implement an HtmlSanitizer class, which cleans HTML documents and fragments
+/// from constructs that can lead to <a href="https://en.wikipedia.org/wiki/Cross-site_scripting">XSS attacks</a>.
+/// </summary>
+public interface IHtmlSanitizer
 {
     /// <summary>
-    /// Enables an inheriting class to implement an HtmlSanitizer class, which cleans HTML documents and fragments
-    /// from constructs that can lead to <a href="https://en.wikipedia.org/wiki/Cross-site_scripting">XSS attacks</a>.
-    /// </summary>
-    public interface IHtmlSanitizer
-    {
-        /// <summary>
-        /// Gets or sets a value indicating whether to keep child nodes of elements that are removed.
-        /// </summary>
-        bool KeepChildNodes { get; set; }
-
-        /// <summary>
-        /// Gets or sets the <see cref="Func{HtmlParser}"/> object the creates the parser used for parsing the input.
-        /// </summary>
-        Func<HtmlParser> HtmlParserFactory { get; set; }
-
-        /// <summary>
-        /// Gets or sets the <see cref="IMarkupFormatter"/> object used for generating output.
-        /// </summary>
-        IMarkupFormatter OutputFormatter { get; set; }
-
-        /// <summary>
-        /// Gets the allowed CSS at-rules such as "@media" and "@font-face".
-        /// </summary>
-        /// <value>
-        /// The allowed CSS at-rules.
-        /// </value>
-        ISet<CssRuleType> AllowedAtRules { get; }
-
-        /// <summary>
-        /// Gets the allowed URI schemes such as "http" and "https".
-        /// </summary>
-        /// <value>
-        /// The allowed URI schemes.
-        /// </value>
-        ISet<string> AllowedSchemes { get; }
-
-        /// <summary>
-        /// Gets the allowed HTML tag names such as "a" and "div".
-        /// </summary>
-        /// <value>
-        /// The allowed tag names.
-        /// </value>
-        ISet<string> AllowedTags { get; }
-
-        /// <summary>
-        /// Gets the allowed HTML attributes such as "href" and "alt".
-        /// </summary>
-        /// <value>
-        /// The allowed HTML attributes.
-        /// </value>
-        ISet<string> AllowedAttributes { get; }
-
-        /// <summary>
-        /// Allow all HTML5 data attributes; the attributes prefixed with data-
-        /// </summary>
-        bool AllowDataAttributes { get; set; }
-
-        /// <summary>
-        /// Gets or sets the HTML attributes that can contain a URI such as "href".
-        /// </summary>
-        /// <value>
-        /// The URI attributes.
-        /// </value>
-        ISet<string> UriAttributes { get; }
-
-        /// <summary>
-        /// Gets or sets the allowed CSS properties such as "font" and "margin".
-        /// </summary>
-        /// <value>
-        /// The allowed CSS properties.
-        /// </value>
-        ISet<string> AllowedCssProperties { get; }
-
-        /// <summary>
-        /// Gets or sets a regex that must not match for legal CSS property values.
-        /// </summary>
-        /// <value>
-        /// The regex.
-        /// </value>
-        Regex DisallowCssPropertyValue { get; set; }
-
-        /// <summary>
-        /// Gets or sets the allowed CSS classes. If the set is empty, all classes will be allowed.
-        /// </summary>
-        /// <value>
-        /// The allowed CSS classes. An empty set means all classes are allowed.
-        /// </value>
-        ISet<string> AllowedClasses { get; }
-
-        /// <summary>
-        /// Occurs after sanitizing the document and post processing nodes.
-        /// </summary>
-        event EventHandler<PostProcessDomEventArgs> PostProcessDom;
-
-        /// <summary>
-        /// Occurs for every node after sanitizing.
-        /// </summary>
-        event EventHandler<PostProcessNodeEventArgs> PostProcessNode;
-
-        /// <summary>
-        /// Occurs before a tag is removed.
-        /// </summary>
-        event EventHandler<RemovingTagEventArgs> RemovingTag;
-
-        /// <summary>
-        /// Occurs before an attribute is removed.
-        /// </summary>
-        event EventHandler<RemovingAttributeEventArgs> RemovingAttribute;
-
-        /// <summary>
-        /// Occurs before a style is removed.
-        /// </summary>
-        event EventHandler<RemovingStyleEventArgs> RemovingStyle;
-
-        /// <summary>
-        /// Occurs before an at-rule is removed.
-        /// </summary>
-        event EventHandler<RemovingAtRuleEventArgs> RemovingAtRule;
-
-        /// <summary>
-        /// Occurs before a comment is removed.
-        /// </summary>
-        event EventHandler<RemovingCommentEventArgs> RemovingComment;
-
-        /// <summary>
-        /// Occurs before a CSS class is removed.
-        /// </summary>
-        event EventHandler<RemovingCssClassEventArgs> RemovingCssClass;
-
-        /// <summary>
-        /// Sanitizes the specified HTML.
-        /// </summary>
-        /// <param name="html">The HTML to sanitize.</param>
-        /// <param name="baseUrl">The base URL relative URLs are resolved against. No resolution if empty.</param>
-        /// <param name="outputFormatter">The formatter used to render the DOM. Using the default formatter if null.</param>
-        /// <returns>The sanitized HTML.</returns>
-        string Sanitize(string html, string baseUrl = "", IMarkupFormatter? outputFormatter = null);
-
-        /// <summary>
-        /// Sanitizes the specified HTML body fragment. If a document is given, only the body part will be returned.
-        /// </summary>
-        /// <param name="html">The HTML body fragment to sanitize.</param>
-        /// <param name="baseUrl">The base URL relative URLs are resolved against. No resolution if empty.</param>
-        /// <returns>The sanitized HTML document.</returns>
-        IHtmlDocument SanitizeDom(string html, string baseUrl = "");
-
-        /// <summary>
-        /// Sanitizes the specified parsed HTML body fragment.
-        /// If the document has not been parsed with CSS support then all styles will be removed.
-        /// </summary>
-        /// <param name="document">The parsed HTML document.</param>
-        /// <param name="context">The node within which to sanitize.</param>
-        /// <param name="baseUrl">The base URL relative URLs are resolved against. No resolution if empty.</param>
-        /// <returns>The sanitized HTML document.</returns>
-        IHtmlDocument SanitizeDom(IHtmlDocument document, IHtmlElement? context = null, string baseUrl = "");
-
-        /// <summary>
-        /// Sanitizes the specified HTML document. Even if only a fragment is given, a whole document will be returned.
-        /// </summary>
-        /// <param name="html">The HTML document to sanitize.</param>
-        /// <param name="baseUrl">The base URL relative URLs are resolved against. No resolution if empty.</param>
-        /// <param name="outputFormatter">The formatter used to render the DOM. Using the <see cref="OutputFormatter"/> if null.</param>
-        /// <returns>The sanitized HTML document.</returns>
-        string SanitizeDocument(string html, string baseUrl = "", IMarkupFormatter? outputFormatter = null);
-    }
+    /// Gets or sets a value indicating whether to keep child nodes of elements that are removed.
+    /// </summary>
+    bool KeepChildNodes { get; set; }
+
+    /// <summary>
+    /// Gets or sets the <see cref="Func{HtmlParser}"/> object the creates the parser used for parsing the input.
+    /// </summary>
+    Func<HtmlParser> HtmlParserFactory { get; set; }
+
+    /// <summary>
+    /// Gets or sets the <see cref="IMarkupFormatter"/> object used for generating output.
+    /// </summary>
+    IMarkupFormatter OutputFormatter { get; set; }
+
+    /// <summary>
+    /// Gets the allowed CSS at-rules such as "@media" and "@font-face".
+    /// </summary>
+    /// <value>
+    /// The allowed CSS at-rules.
+    /// </value>
+    ISet<CssRuleType> AllowedAtRules { get; }
+
+    /// <summary>
+    /// Gets the allowed URI schemes such as "http" and "https".
+    /// </summary>
+    /// <value>
+    /// The allowed URI schemes.
+    /// </value>
+    ISet<string> AllowedSchemes { get; }
+
+    /// <summary>
+    /// Gets the allowed HTML tag names such as "a" and "div".
+    /// </summary>
+    /// <value>
+    /// The allowed tag names.
+    /// </value>
+    ISet<string> AllowedTags { get; }
+
+    /// <summary>
+    /// Gets the allowed HTML attributes such as "href" and "alt".
+    /// </summary>
+    /// <value>
+    /// The allowed HTML attributes.
+    /// </value>
+    ISet<string> AllowedAttributes { get; }
+
+    /// <summary>
+    /// Allow all HTML5 data attributes; the attributes prefixed with data-
+    /// </summary>
+    bool AllowDataAttributes { get; set; }
+
+    /// <summary>
+    /// Gets or sets the HTML attributes that can contain a URI such as "href".
+    /// </summary>
+    /// <value>
+    /// The URI attributes.
+    /// </value>
+    ISet<string> UriAttributes { get; }
+
+    /// <summary>
+    /// Gets or sets the allowed CSS properties such as "font" and "margin".
+    /// </summary>
+    /// <value>
+    /// The allowed CSS properties.
+    /// </value>
+    ISet<string> AllowedCssProperties { get; }
+
+    /// <summary>
+    /// Gets or sets a regex that must not match for legal CSS property values.
+    /// </summary>
+    /// <value>
+    /// The regex.
+    /// </value>
+    Regex DisallowCssPropertyValue { get; set; }
+
+    /// <summary>
+    /// Gets or sets the allowed CSS classes. If the set is empty, all classes will be allowed.
+    /// </summary>
+    /// <value>
+    /// The allowed CSS classes. An empty set means all classes are allowed.
+    /// </value>
+    ISet<string> AllowedClasses { get; }
+
+    /// <summary>
+    /// Occurs after sanitizing the document and post processing nodes.
+    /// </summary>
+    event EventHandler<PostProcessDomEventArgs> PostProcessDom;
+
+    /// <summary>
+    /// Occurs for every node after sanitizing.
+    /// </summary>
+    event EventHandler<PostProcessNodeEventArgs> PostProcessNode;
+
+    /// <summary>
+    /// Occurs before a tag is removed.
+    /// </summary>
+    event EventHandler<RemovingTagEventArgs> RemovingTag;
+
+    /// <summary>
+    /// Occurs before an attribute is removed.
+    /// </summary>
+    event EventHandler<RemovingAttributeEventArgs> RemovingAttribute;
+
+    /// <summary>
+    /// Occurs before a style is removed.
+    /// </summary>
+    event EventHandler<RemovingStyleEventArgs> RemovingStyle;
+
+    /// <summary>
+    /// Occurs before an at-rule is removed.
+    /// </summary>
+    event EventHandler<RemovingAtRuleEventArgs> RemovingAtRule;
+
+    /// <summary>
+    /// Occurs before a comment is removed.
+    /// </summary>
+    event EventHandler<RemovingCommentEventArgs> RemovingComment;
+
+    /// <summary>
+    /// Occurs before a CSS class is removed.
+    /// </summary>
+    event EventHandler<RemovingCssClassEventArgs> RemovingCssClass;
+
+    /// <summary>
+    /// Occurs when a URL is being sanitized.
+    /// </summary>
+    event EventHandler<FilterUrlEventArgs>? FilterUrl;
+
+    /// <summary>
+    /// Sanitizes the specified HTML.
+    /// </summary>
+    /// <param name="html">The HTML to sanitize.</param>
+    /// <param name="baseUrl">The base URL relative URLs are resolved against. No resolution if empty.</param>
+    /// <param name="outputFormatter">The formatter used to render the DOM. Using the default formatter if null.</param>
+    /// <returns>The sanitized HTML.</returns>
+    string Sanitize(string html, string baseUrl = "", IMarkupFormatter? outputFormatter = null);
+
+    /// <summary>
+    /// Sanitizes the specified HTML body fragment. If a document is given, only the body part will be returned.
+    /// </summary>
+    /// <param name="html">The HTML body fragment to sanitize.</param>
+    /// <param name="baseUrl">The base URL relative URLs are resolved against. No resolution if empty.</param>
+    /// <returns>The sanitized HTML document.</returns>
+    IHtmlDocument SanitizeDom(string html, string baseUrl = "");
+
+    /// <summary>
+    /// Sanitizes the specified parsed HTML body fragment.
+    /// If the document has not been parsed with CSS support then all styles will be removed.
+    /// </summary>
+    /// <param name="document">The parsed HTML document.</param>
+    /// <param name="context">The node within which to sanitize.</param>
+    /// <param name="baseUrl">The base URL relative URLs are resolved against. No resolution if empty.</param>
+    /// <returns>The sanitized HTML document.</returns>
+    IHtmlDocument SanitizeDom(IHtmlDocument document, IHtmlElement? context = null, string baseUrl = "");
+
+    /// <summary>
+    /// Sanitizes the specified HTML document. Even if only a fragment is given, a whole document will be returned.
+    /// </summary>
+    /// <param name="html">The HTML document to sanitize.</param>
+    /// <param name="baseUrl">The base URL relative URLs are resolved against. No resolution if empty.</param>
+    /// <param name="outputFormatter">The formatter used to render the DOM. Using the <see cref="OutputFormatter"/> if null.</param>
+    /// <returns>The sanitized HTML document.</returns>
+    string SanitizeDocument(string html, string baseUrl = "", IMarkupFormatter? outputFormatter = null);
 }

+ 29 - 35
Masuit.Tools.Abstractions/HtmlSanitizer/Iri.cs

@@ -1,42 +1,36 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
+namespace Ganss.Xss;
 
-namespace Ganss.Xss
+/// <summary>
+/// Represents an Internationalized Resource Identifier.
+/// </summary>
+/// <remarks>
+/// Initializes a new instance of the <see cref="Iri"/> class.
+/// </remarks>
+/// <param name="value">The value.</param>
+/// <param name="scheme">The scheme.</param>
+public class Iri(string value, string? scheme = null)
 {
     /// <summary>
-    /// Represents an Internationalized Resource Identifier.
+    /// Gets or sets the value of the IRI.
     /// </summary>
-    /// <remarks>
-    /// Initializes a new instance of the <see cref="Iri"/> class.
-    /// </remarks>
-    /// <param name="value">The value.</param>
-    /// <param name="scheme">The scheme.</param>
-    public class Iri(string value, string? scheme = null)
-    {
-        /// <summary>
-        /// Gets or sets the value of the IRI.
-        /// </summary>
-        /// <value>
-        /// The value of the IRI.
-        /// </value>
-        public string Value { get; private set; } = value;
+    /// <value>
+    /// The value of the IRI.
+    /// </value>
+    public string Value { get; private set; } = value;
 
-        /// <summary>
-        /// Gets a value indicating whether the IRI is absolute.
-        /// </summary>
-        /// <value>
-        ///   <c>true</c> if the IRI is absolute; otherwise, <c>false</c>.
-        /// </value>
-        public bool IsAbsolute => !string.IsNullOrEmpty(Scheme);
+    /// <summary>
+    /// Gets a value indicating whether the IRI is absolute.
+    /// </summary>
+    /// <value>
+    ///   <c>true</c> if the IRI is absolute; otherwise, <c>false</c>.
+    /// </value>
+    public bool IsAbsolute => !string.IsNullOrEmpty(Scheme);
 
-        /// <summary>
-        /// Gets or sets the scheme of the IRI, e.g. "https".
-        /// </summary>
-        /// <value>
-        /// The scheme of the IRI.
-        /// </value>
-        public string? Scheme { get; private set; } = scheme;
-    }
+    /// <summary>
+    /// Gets or sets the scheme of the IRI, e.g. "https".
+    /// </summary>
+    /// <value>
+    /// The scheme of the IRI.
+    /// </value>
+    public string? Scheme { get; private set; } = scheme;
 }

+ 37 - 38
Masuit.Tools.Abstractions/HtmlSanitizer/RemoveReason.cs

@@ -1,41 +1,40 @@
-namespace Ganss.Xss
+namespace Ganss.Xss;
+
+/// <summary>
+/// List of reasons why something was identified to get removed from the HTML.
+/// </summary>
+public enum RemoveReason
 {
     /// <summary>
-    /// List of reasons why something was identified to get removed from the HTML.
-    /// </summary>
-    public enum RemoveReason
-    {
-        /// <summary>
-        /// Tag is not allowed.
-        /// </summary>
-        NotAllowedTag,
-        /// <summary>
-        /// Attribute is not allowed.
-        /// </summary>
-        NotAllowedAttribute,
-        /// <summary>
-        /// Style is not allowed.
-        /// </summary>
-        NotAllowedStyle,
-        /// <summary>
-        /// Value is a non-allowed or harmful URL.
-        /// </summary>
-        NotAllowedUrlValue,
-        /// <summary>
-        /// Value is not allowed or harmful.
-        /// </summary>
-        NotAllowedValue,
-        /// <summary>
-        /// CSS class is not allowed.
-        /// </summary>
-        NotAllowedCssClass,
-        /// <summary>
-        /// The class attribute is empty.
-        /// </summary>
-        ClassAttributeEmpty,
-        /// <summary>
-        /// The style attribute is empty.
-        /// </summary>
-        StyleAttributeEmpty,
-    }
+    /// Tag is not allowed.
+    /// </summary>
+    NotAllowedTag,
+    /// <summary>
+    /// Attribute is not allowed.
+    /// </summary>
+    NotAllowedAttribute,
+    /// <summary>
+    /// Style is not allowed.
+    /// </summary>
+    NotAllowedStyle,
+    /// <summary>
+    /// Value is a non-allowed or harmful URL.
+    /// </summary>
+    NotAllowedUrlValue,
+    /// <summary>
+    /// Value is not allowed or harmful.
+    /// </summary>
+    NotAllowedValue,
+    /// <summary>
+    /// CSS class is not allowed.
+    /// </summary>
+    NotAllowedCssClass,
+    /// <summary>
+    /// The class attribute is empty.
+    /// </summary>
+    ClassAttributeEmpty,
+    /// <summary>
+    /// The style attribute is empty.
+    /// </summary>
+    StyleAttributeEmpty,
 }

+ 2 - 2
Masuit.Tools.Abstractions/Masuit.Tools.Abstractions.csproj

@@ -47,8 +47,8 @@
     </ItemGroup>
 
     <ItemGroup>
-        <PackageReference Include="AngleSharp" Version="1.2.0" />
-        <PackageReference Include="AngleSharp.Css" Version="1.0.0-beta.144" />
+        <PackageReference Include="AngleSharp" Version="1.3.0-beta.468" />
+        <PackageReference Include="AngleSharp.Css" Version="1.0.0-beta.151" />
         <PackageReference Include="DnsClient" Version="1.8.0" />
         <PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
         <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.1" />

+ 1 - 1
Test/Masuit.Tools.Abstractions.Test/Masuit.Tools.Abstractions.Test.csproj

@@ -20,7 +20,7 @@
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
-    <PackageReference Include="coverlet.collector" Version="6.0.3">
+    <PackageReference Include="coverlet.collector" Version="6.0.4">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>