From 8151443a71d972715882eb572f2d2c4b01aa655a Mon Sep 17 00:00:00 2001 From: Ryan Lamansky <13633345+RyanLamansky@users.noreply.github.com> Date: Sat, 3 Aug 2024 10:36:54 -0500 Subject: [PATCH] Null or empty attribute values are now written with "empty attribute syntax" instead of an empty string. --- .../Validated/ValidatedAttributeTests.cs | 4 +- .../Validated/ValidatedAttributeValueTests.cs | 15 +++++--- .../Validated/ValidatedAttributeName.cs | 10 +---- .../Validated/ValidatedAttributeValue.cs | 38 +++++++------------ 4 files changed, 27 insertions(+), 40 deletions(-) diff --git a/HtmlUtilities.Tests/Validated/ValidatedAttributeTests.cs b/HtmlUtilities.Tests/Validated/ValidatedAttributeTests.cs index 5f78bda..aee7295 100644 --- a/HtmlUtilities.Tests/Validated/ValidatedAttributeTests.cs +++ b/HtmlUtilities.Tests/Validated/ValidatedAttributeTests.cs @@ -13,10 +13,12 @@ public static void ValidatedAttributeUnconstructedIsBlank() [Theory] [InlineData("lang", "en", " lang=en")] [InlineData("lang", "en-us", " lang=en-us")] + [InlineData("test", "", " test")] + [InlineData("test", null, " test")] [InlineData("test", "top left", " test=\"top left\"")] [InlineData("test", "d&d", " test=d&d")] [InlineData("test", "Dungeons & Dragons", " test=\"Dungeons & Dragons\"")] - public static void AttributeIsFormattedCorrectly(string name, string value, string expected) + public static void AttributeIsFormattedCorrectly(string name, string? value, string expected) { Assert.Equal(expected, new ValidatedAttribute(name, value).ToString()); } diff --git a/HtmlUtilities.Tests/Validated/ValidatedAttributeValueTests.cs b/HtmlUtilities.Tests/Validated/ValidatedAttributeValueTests.cs index 5415582..1d75082 100644 --- a/HtmlUtilities.Tests/Validated/ValidatedAttributeValueTests.cs +++ b/HtmlUtilities.Tests/Validated/ValidatedAttributeValueTests.cs @@ -11,12 +11,15 @@ public static void ValidatedAttributeValueUnconstructedIsBlank() } [Theory] + [InlineData("", "")] + [InlineData(null, "")] + [InlineData("\"", "=\""\"")] [InlineData("en", "=en")] [InlineData("en-us", "=en-us")] [InlineData("top left", "=\"top left\"")] [InlineData("d&d", "=d&d")] [InlineData("Dungeons & Dragons", "=\"Dungeons & Dragons\"")] - public static void AttributeValueIsFormattedCorrectly(string source, string expected) + public static void AttributeValueIsFormattedCorrectly(string? source, string expected) { Assert.Equal(expected, new ValidatedAttributeValue(source).ToString()); } @@ -24,11 +27,11 @@ public static void AttributeValueIsFormattedCorrectly(string source, string expe [Fact] public static void AttributeValueFromNullIsEmpty() { - Assert.Equal("=\"\"", new ValidatedAttributeValue((string?)null).ToString()); - Assert.Equal("=\"\"", new ValidatedAttributeValue((int?)null).ToString()); - Assert.Equal("=\"\"", new ValidatedAttributeValue((uint?)null).ToString()); - Assert.Equal("=\"\"", new ValidatedAttributeValue((long?)null).ToString()); - Assert.Equal("=\"\"", new ValidatedAttributeValue((ulong?)null).ToString()); + Assert.Equal("", new ValidatedAttributeValue((string?)null).ToString()); + Assert.Equal("", new ValidatedAttributeValue((int?)null).ToString()); + Assert.Equal("", new ValidatedAttributeValue((uint?)null).ToString()); + Assert.Equal("", new ValidatedAttributeValue((long?)null).ToString()); + Assert.Equal("", new ValidatedAttributeValue((ulong?)null).ToString()); } [Theory] diff --git a/HtmlUtilities/Validated/ValidatedAttributeName.cs b/HtmlUtilities/Validated/ValidatedAttributeName.cs index bdcabf4..bea010f 100644 --- a/HtmlUtilities/Validated/ValidatedAttributeName.cs +++ b/HtmlUtilities/Validated/ValidatedAttributeName.cs @@ -30,7 +30,7 @@ public ValidatedAttributeName(ReadOnlySpan name) internal static void Validate(ReadOnlySpan name, ref ArrayBuilder writer) { - if (name.Length == 0) + if (name.IsEmpty) throw new ArgumentException("name cannot be an empty string.", nameof(name)); writer.Write(' '); @@ -38,7 +38,7 @@ internal static void Validate(ReadOnlySpan name, ref ArrayBuilder wr { var categories = codePoint.InfraCategories; if ((categories & CodePointInfraCategory.AsciiWhitespace) == 0 && (categories & (CodePointInfraCategory.Surrogate | CodePointInfraCategory.Control)) != 0) - continue; + throw new ArgumentException($"name has an invalid character, '{(char)codePoint.Value}'.", nameof(name)); switch (codePoint.Value) { @@ -65,10 +65,4 @@ internal static void Validate(ReadOnlySpan name, ref ArrayBuilder wr /// /// The string representation of the validated name. public override string ToString() => Encoding.UTF8.GetString(value); - - /// - /// Creates a new from the provided validated name and no value. - /// - /// A validated name. - public static implicit operator ValidatedAttribute(ValidatedAttributeName name) => new(name); } diff --git a/HtmlUtilities/Validated/ValidatedAttributeValue.cs b/HtmlUtilities/Validated/ValidatedAttributeValue.cs index d4a56c6..d5b3375 100644 --- a/HtmlUtilities/Validated/ValidatedAttributeValue.cs +++ b/HtmlUtilities/Validated/ValidatedAttributeValue.cs @@ -5,31 +5,28 @@ namespace HtmlUtilities.Validated; /// /// A pre-validated and formatted attribute value ready to be written. /// +/// The rules described by https://html.spec.whatwg.org/#attributes-2 are closely followed. public readonly struct ValidatedAttributeValue { - private static readonly byte[] Empty = "=\"\""u8.ToArray(); - internal readonly ReadOnlyMemory value; /// /// Creates a new from the provided of type . /// /// The value to prepare as an attribute. - public ValidatedAttributeValue(ReadOnlySpan value) - { - // See https://html.spec.whatwg.org/#attributes-2 for reference. + public ValidatedAttributeValue(ReadOnlySpan value) => this.value = ToUtf8Array(value); - if (value.Length == 0) - { - this.value = Empty; - return; - } + private static ReadOnlyMemory ToUtf8Array(ReadOnlySpan value) + { + if (value.IsEmpty) + return Array.Empty(); // Empty attribute syntax implicitly has a value of an empty string. var writer = new ArrayBuilder(value.Length); + try { Validate(value, ref writer); - this.value = writer; + return writer; } finally { @@ -53,10 +50,7 @@ public ValidatedAttributeValue(int value) public ValidatedAttributeValue(int? value) { if (value is null) - { - this.value = Empty; return; - } this.value = ToUtf8Array(value.GetValueOrDefault()); } @@ -77,10 +71,7 @@ public ValidatedAttributeValue(uint value) public ValidatedAttributeValue(uint? value) { if (value is null) - { - this.value = Empty; return; - } this.value = ToUtf8Array(value.GetValueOrDefault()); } @@ -101,10 +92,7 @@ public ValidatedAttributeValue(long value) public ValidatedAttributeValue(long? value) { if (value is null) - { - this.value = Empty; return; - } this.value = ToUtf8Array(value.GetValueOrDefault()); } @@ -125,10 +113,7 @@ public ValidatedAttributeValue(ulong value) public ValidatedAttributeValue(ulong? value) { if (value is null) - { - this.value = Empty; return; - } this.value = ToUtf8Array(value.GetValueOrDefault()); } @@ -233,6 +218,9 @@ internal static void ToUtf8(ulong value, Span result) internal static void Validate(ReadOnlySpan value, ref ArrayBuilder writer) { + if (value.IsEmpty) + return; + foreach (var codePoint in CodePoint.GetEnumerable(value)) { switch (codePoint.Value) @@ -311,8 +299,8 @@ private static void EmitQuoted(ReadOnlySpan value, ref ArrayBuilder public static implicit operator ValidatedAttributeValue(ReadOnlySpan value) => new(value); /// - /// Creates a new from the provided of type . + /// Creates a new from the provided . /// /// The value to prepare as an attribute. - public static implicit operator ValidatedAttributeValue(string value) => new(value); + public static implicit operator ValidatedAttributeValue(string? value) => new(value); }