diff --git a/src/Components/Web/src/Forms/FieldIdGenerator.cs b/src/Components/Web/src/Forms/FieldIdGenerator.cs index f20b2e23dd2f..12d941bca4bc 100644 --- a/src/Components/Web/src/Forms/FieldIdGenerator.cs +++ b/src/Components/Web/src/Forms/FieldIdGenerator.cs @@ -11,10 +11,10 @@ namespace Microsoft.AspNetCore.Components.Forms; /// internal static class FieldIdGenerator { - // Valid characters for HTML 4.01 id attributes (excluding '.' to avoid CSS selector conflicts) - // See: https://www.w3.org/TR/html401/types.html#type-id - private static readonly SearchValues ValidIdChars = - SearchValues.Create("-0123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"); + // Invalid characters for HTML5 id attributes: all Unicode whitespace characters and periods + // Periods are excluded to avoid CSS selector conflicts + private static readonly SearchValues InvalidIdChars = SearchValues.Create( + " \t\n\r\f\v\u0085\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000."); /// /// Sanitizes a field name to create a valid HTML id attribute value. @@ -22,9 +22,9 @@ internal static class FieldIdGenerator /// The field name to sanitize. /// A valid HTML id attribute value, or an empty string if the input is null or empty. /// - /// This method follows HTML 4.01 id attribute rules: - /// - The first character must be a letter (A-Z, a-z) - /// - Subsequent characters can be letters, digits, hyphens, underscores, colons, or periods + /// This method follows HTML5 id attribute rules: + /// - The value must contain at least one character + /// - The value must not contain any whitespace characters /// - Periods are replaced with underscores to avoid CSS selector conflicts /// public static string SanitizeHtmlId(string? fieldName) @@ -35,11 +35,7 @@ public static string SanitizeHtmlId(string? fieldName) } // Fast path: check if sanitization is needed - var firstChar = fieldName[0]; - var startsWithLetter = char.IsAsciiLetter(firstChar); - var indexOfInvalidChar = fieldName.AsSpan(1).IndexOfAnyExcept(ValidIdChars); - - if (startsWithLetter && indexOfInvalidChar < 0) + if (fieldName.AsSpan().IndexOfAny(InvalidIdChars) < 0) { return fieldName; } @@ -47,34 +43,18 @@ public static string SanitizeHtmlId(string? fieldName) // Slow path: build sanitized string var result = new StringBuilder(fieldName.Length); - // First character must be a letter - if (startsWithLetter) - { - result.Append(firstChar); - } - else + foreach (var c in fieldName) { - result.Append('z'); - if (IsValidIdChar(firstChar)) + if (InvalidIdChars.Contains(c)) { - result.Append(firstChar); + result.Append('_'); } else { - result.Append('_'); + result.Append(c); } } - // Process remaining characters - for (var i = 1; i < fieldName.Length; i++) - { - var c = fieldName[i]; - result.Append(IsValidIdChar(c) ? c : '_'); - } - return result.ToString(); } - - private static bool IsValidIdChar(char c) - => ValidIdChars.Contains(c); } diff --git a/src/Components/Web/test/Forms/FieldIdGeneratorTest.cs b/src/Components/Web/test/Forms/FieldIdGeneratorTest.cs index abdf6c0f8140..f7a6100afceb 100644 --- a/src/Components/Web/test/Forms/FieldIdGeneratorTest.cs +++ b/src/Components/Web/test/Forms/FieldIdGeneratorTest.cs @@ -12,71 +12,14 @@ public class FieldIdGeneratorTest [InlineData("", "")] [InlineData("Name", "Name")] [InlineData("name", "name")] - [InlineData("Model.Property", "Model_Property")] [InlineData("Model.Address.Street", "Model_Address_Street")] - [InlineData("Items[0]", "Items_0_")] - [InlineData("Items[0].Name", "Items_0__Name")] - [InlineData("Model.Items[0].Name", "Model_Items_0__Name")] + [InlineData("Model.Items[0].Name", "Model_Items[0]_Name")] + [InlineData("Field\tName\nWith\rVariousWhitespace", "Field_Name_With_VariousWhitespace")] + [InlineData("Field\u00A0Name", "Field_Name")] // Non-breaking space public void SanitizeHtmlId_ProducesValidId(string? input, string expected) { - // Act var result = FieldIdGenerator.SanitizeHtmlId(input); - // Assert Assert.Equal(expected, result); } - - [Fact] - public void SanitizeHtmlId_StartsWithNonLetter_PrependsZ() - { - // Arrange - var input = "123Name"; - - // Act - var result = FieldIdGenerator.SanitizeHtmlId(input); - - // Assert - Assert.StartsWith("z", result); - Assert.Equal("z123Name", result); - } - - [Fact] - public void SanitizeHtmlId_StartsWithInvalidChar_PrependsZAndReplaces() - { - // Arrange - var input = ".Property"; - - // Act - var result = FieldIdGenerator.SanitizeHtmlId(input); - - // Assert - Assert.StartsWith("z", result); - Assert.Equal("z_Property", result); - } - - [Fact] - public void SanitizeHtmlId_AllowsHyphensUnderscoresColons() - { - // Arrange - var input = "my-field_name:value"; - - // Act - var result = FieldIdGenerator.SanitizeHtmlId(input); - - // Assert - Assert.Equal("my-field_name:value", result); - } - - [Fact] - public void SanitizeHtmlId_ReplacesSpacesWithUnderscores() - { - // Arrange - var input = "Field Name"; - - // Act - var result = FieldIdGenerator.SanitizeHtmlId(input); - - // Assert - Assert.Equal("Field_Name", result); - } }