33
44#nullable enable
55
6+ using System . Buffers ;
67using System . Diagnostics ;
78using System . Globalization ;
89using System . Text ;
@@ -19,6 +20,11 @@ namespace Microsoft.AspNetCore.Mvc.Rendering;
1920[ DebuggerDisplay ( "{DebuggerToString()}" ) ]
2021public class TagBuilder : IHtmlContent
2122{
23+ // Note '.' is valid according to the HTML 4.01 specification. Disallowed here to avoid
24+ // confusion with CSS class selectors or when using jQuery.
25+ private static readonly SearchValues < char > _html401IdChars =
26+ SearchValues . Create ( "-0123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz" ) ;
27+
2228 private AttributeDictionary ? _attributes ;
2329 private HtmlContentBuilder ? _innerHtml ;
2430
@@ -154,51 +160,39 @@ public static string CreateSanitizedId(string? name, string invalidCharReplaceme
154160 }
155161
156162 // If there are no invalid characters in the string, then we don't have to create the buffer.
157- var firstIndexOfInvalidCharacter = 1 ;
158- for ( ; firstIndexOfInvalidCharacter < name . Length ; firstIndexOfInvalidCharacter ++ )
163+ var indexOfInvalidCharacter = name . AsSpan ( 1 ) . IndexOfAnyExcept ( _html401IdChars ) ;
164+ var firstChar = name [ 0 ] ;
165+ var startsWithAsciiLetter = char . IsAsciiLetter ( firstChar ) ;
166+ if ( startsWithAsciiLetter && indexOfInvalidCharacter < 0 )
159167 {
160- if ( ! Html401IdUtil . IsValidIdCharacter ( name [ firstIndexOfInvalidCharacter ] ) )
161- {
162- break ;
163- }
168+ return name ;
164169 }
165170
166- var firstChar = name [ 0 ] ;
167- var startsWithAsciiLetter = char . IsAsciiLetter ( firstChar ) ;
168171 if ( ! startsWithAsciiLetter )
169172 {
170173 // The first character must be a letter according to the HTML 4.01 specification.
171174 firstChar = 'z' ;
172175 }
173176
174- if ( firstIndexOfInvalidCharacter == name . Length && startsWithAsciiLetter )
175- {
176- return name ;
177- }
178-
179177 var stringBuffer = new StringBuilder ( name . Length ) ;
180178 stringBuffer . Append ( firstChar ) ;
179+ var remainingName = name . AsSpan ( 1 ) ;
181180
182- // Characters until 'firstIndexOfInvalidCharacter' have already been checked for validity.
183- // So just copy them. This avoids running them through Html401IdUtil.IsValidIdCharacter again.
184- for ( var index = 1 ; index < firstIndexOfInvalidCharacter ; index ++ )
185- {
186- stringBuffer . Append ( name [ index ] ) ;
187- }
188-
189- for ( var index = firstIndexOfInvalidCharacter ; index < name . Length ; index ++ )
181+ // Copy values until an invalid character found. Replace the invalid character with the replacement string
182+ // and search for the next invalid character.
183+ while ( remainingName . Length > 0 )
190184 {
191- var thisChar = name [ index ] ;
192- if ( Html401IdUtil . IsValidIdCharacter ( thisChar ) )
185+ if ( indexOfInvalidCharacter < 0 )
193186 {
194- stringBuffer . Append ( thisChar ) ;
195- }
196- else
197- {
198- stringBuffer . Append ( invalidCharReplacement ) ;
187+ stringBuffer . Append ( remainingName ) ;
188+ break ;
199189 }
200- }
201190
191+ stringBuffer . Append ( remainingName . Slice ( 0 , indexOfInvalidCharacter ) ) ;
192+ stringBuffer . Append ( invalidCharReplacement ) ;
193+ remainingName = remainingName . Slice ( indexOfInvalidCharacter + 1 ) ;
194+ indexOfInvalidCharacter = remainingName . IndexOfAnyExcept ( _html401IdChars ) ;
195+ }
202196 return stringBuffer . ToString ( ) ;
203197 }
204198
@@ -418,28 +412,4 @@ public void WriteTo(TextWriter writer, HtmlEncoder encoder)
418412 TagBuilder . WriteTo ( _tagBuilder , writer , encoder , _tagRenderMode ) ;
419413 }
420414 }
421-
422- private static class Html401IdUtil
423- {
424- public static bool IsValidIdCharacter ( char testChar )
425- {
426- return char . IsAsciiLetterOrDigit ( testChar ) || IsAllowableSpecialCharacter ( testChar ) ;
427- }
428-
429- private static bool IsAllowableSpecialCharacter ( char testChar )
430- {
431- switch ( testChar )
432- {
433- case '-' :
434- case '_' :
435- case ':' :
436- // Note '.' is valid according to the HTML 4.01 specification. Disallowed here to avoid
437- // confusion with CSS class selectors or when using jQuery.
438- return true ;
439-
440- default :
441- return false ;
442- }
443- }
444- }
445415}
0 commit comments