Skip to content

Commit

Permalink
Null or empty attribute values are now written with "empty attribute …
Browse files Browse the repository at this point in the history
…syntax" instead of an empty string.
  • Loading branch information
RyanLamansky committed Aug 3, 2024
1 parent efe4579 commit 8151443
Show file tree
Hide file tree
Showing 4 changed files with 27 additions and 40 deletions.
4 changes: 3 additions & 1 deletion HtmlUtilities.Tests/Validated/ValidatedAttributeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
15 changes: 9 additions & 6 deletions HtmlUtilities.Tests/Validated/ValidatedAttributeValueTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,27 @@ 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());
}

[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]
Expand Down
10 changes: 2 additions & 8 deletions HtmlUtilities/Validated/ValidatedAttributeName.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ public ValidatedAttributeName(ReadOnlySpan<char> name)

internal static void Validate(ReadOnlySpan<char> name, ref ArrayBuilder<byte> writer)
{
if (name.Length == 0)
if (name.IsEmpty)
throw new ArgumentException("name cannot be an empty string.", nameof(name));

writer.Write(' ');
foreach (var codePoint in CodePoint.GetEnumerable(name))
{
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)
{
Expand All @@ -65,10 +65,4 @@ internal static void Validate(ReadOnlySpan<char> name, ref ArrayBuilder<byte> wr
/// </summary>
/// <returns>The string representation of the validated name.</returns>
public override string ToString() => Encoding.UTF8.GetString(value);

/// <summary>
/// Creates a new <see cref="ValidatedAttribute"/> from the provided validated name and no value.
/// </summary>
/// <param name="name">A validated name.</param>
public static implicit operator ValidatedAttribute(ValidatedAttributeName name) => new(name);
}
38 changes: 13 additions & 25 deletions HtmlUtilities/Validated/ValidatedAttributeValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,28 @@ namespace HtmlUtilities.Validated;
/// <summary>
/// A pre-validated and formatted attribute value ready to be written.
/// </summary>
/// <remarks>The rules described by https://html.spec.whatwg.org/#attributes-2 are closely followed.</remarks>
public readonly struct ValidatedAttributeValue
{
private static readonly byte[] Empty = "=\"\""u8.ToArray();

internal readonly ReadOnlyMemory<byte> value;

/// <summary>
/// Creates a new <see cref="ValidatedAttributeValue"/> from the provided <see cref="ReadOnlySpan{T}"/> of type <see cref="char"/>.
/// </summary>
/// <param name="value">The value to prepare as an attribute.</param>
public ValidatedAttributeValue(ReadOnlySpan<char> value)
{
// See https://html.spec.whatwg.org/#attributes-2 for reference.
public ValidatedAttributeValue(ReadOnlySpan<char> value) => this.value = ToUtf8Array(value);

if (value.Length == 0)
{
this.value = Empty;
return;
}
private static ReadOnlyMemory<byte> ToUtf8Array(ReadOnlySpan<char> value)
{
if (value.IsEmpty)
return Array.Empty<byte>(); // Empty attribute syntax implicitly has a value of an empty string.

var writer = new ArrayBuilder<byte>(value.Length);

try
{
Validate(value, ref writer);
this.value = writer;
return writer;
}
finally
{
Expand All @@ -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());
}
Expand All @@ -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());
}
Expand All @@ -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());
}
Expand All @@ -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());
}
Expand Down Expand Up @@ -233,6 +218,9 @@ internal static void ToUtf8(ulong value, Span<byte> result)

internal static void Validate(ReadOnlySpan<char> value, ref ArrayBuilder<byte> writer)
{
if (value.IsEmpty)
return;

foreach (var codePoint in CodePoint.GetEnumerable(value))
{
switch (codePoint.Value)
Expand Down Expand Up @@ -311,8 +299,8 @@ private static void EmitQuoted(ReadOnlySpan<char> value, ref ArrayBuilder<byte>
public static implicit operator ValidatedAttributeValue(ReadOnlySpan<char> value) => new(value);

/// <summary>
/// Creates a new <see cref="ValidatedAttributeValue"/> from the provided <see cref="ReadOnlySpan{T}"/> of type <see cref="char"/>.
/// Creates a new <see cref="ValidatedAttributeValue"/> from the provided <see cref="string"/>.
/// </summary>
/// <param name="value">The value to prepare as an attribute.</param>
public static implicit operator ValidatedAttributeValue(string value) => new(value);
public static implicit operator ValidatedAttributeValue(string? value) => new(value);
}

0 comments on commit 8151443

Please sign in to comment.