1
1
// Licensed to the .NET Foundation under one or more agreements.
2
2
// The .NET Foundation licenses this file to you under the MIT license.
3
3
4
- #nullable disable
5
-
6
4
using System ;
7
- using System . Text . RegularExpressions ;
5
+ using System . Diagnostics ;
6
+ using System . Diagnostics . CodeAnalysis ;
7
+ using Microsoft . AspNetCore . Razor . PooledObjects ;
8
8
9
9
namespace Microsoft . AspNetCore . Razor . Language ;
10
10
11
11
public static class HtmlConventions
12
12
{
13
- private const string HtmlCaseRegexReplacement = "-$1$2" ;
14
13
private static readonly char [ ] InvalidNonWhitespaceHtmlCharacters =
15
- new [ ] { '@' , '!' , '<' , '/' , '?' , '[' , '>' , ']' , '=' , '"' , '\' ' , '*' } ;
16
-
17
- // This matches the following AFTER the start of the input string (MATCH).
18
- // Any letter/number followed by an uppercase letter then lowercase letter: 1(Aa), a(Aa), A(Aa)
19
- // Any lowercase letter followed by an uppercase letter: a(A)
20
- // Each match is then prefixed by a "-" via the ToHtmlCase method.
21
- private static readonly Regex HtmlCaseRegex =
22
- new Regex (
23
- "(?<!^)((?<=[a-zA-Z0-9])[A-Z][a-z])|((?<=[a-z])[A-Z])" ,
24
- RegexOptions . None ,
25
- TimeSpan . FromMilliseconds ( 500 ) ) ;
26
-
14
+ [ '@' , '!' , '<' , '/' , '?' , '[' , '>' , ']' , '=' , '"' , '\' ' , '*' ] ;
27
15
28
16
internal static bool IsInvalidNonWhitespaceHtmlCharacters ( char testChar )
29
17
{
@@ -50,8 +38,81 @@ internal static bool IsInvalidNonWhitespaceHtmlCharacters(char testChar)
50
38
/// ONE1TWO2THREE3 => one1two2three3
51
39
/// First_Second_ThirdHi => first_second_third-hi
52
40
/// </example>
53
- public static string ToHtmlCase ( string name )
41
+ public static string ToHtmlCase ( string input )
42
+ {
43
+ if ( string . IsNullOrEmpty ( input ) )
44
+ {
45
+ return input ;
46
+ }
47
+
48
+ return TryGetKebabCaseString ( input , out var result )
49
+ ? result
50
+ : input ;
51
+ }
52
+
53
+ private static bool TryGetKebabCaseString ( ReadOnlySpan < char > input , [ NotNullWhen ( true ) ] out string ? result )
54
54
{
55
- return HtmlCaseRegex . Replace ( name , HtmlCaseRegexReplacement ) . ToLowerInvariant ( ) ;
55
+ using var _ = StringBuilderPool . GetPooledObject ( out var builder ) ;
56
+
57
+ var allLower = true ;
58
+ var i = 0 ;
59
+ foreach ( var c in input )
60
+ {
61
+ if ( char . IsUpper ( c ) )
62
+ {
63
+ allLower = false ;
64
+
65
+ if ( ShouldInsertHyphenBeforeUppercase ( input , i ) )
66
+ {
67
+ builder . Append ( '-' ) ;
68
+ }
69
+
70
+ builder . Append ( char . ToLowerInvariant ( c ) ) ;
71
+ }
72
+ else
73
+ {
74
+ builder . Append ( c ) ;
75
+ }
76
+
77
+ i ++ ;
78
+ }
79
+
80
+ if ( allLower )
81
+ {
82
+ // If the input is all lowercase, we don't need to realize the builder,
83
+ // it will just be cleared when the pooled object is disposed.
84
+ result = null ;
85
+ return false ;
86
+ }
87
+
88
+ result = builder . ToString ( ) ;
89
+ return true ;
90
+ }
91
+
92
+ private static bool ShouldInsertHyphenBeforeUppercase ( ReadOnlySpan < char > input , int i )
93
+ {
94
+ Debug . Assert ( char . IsUpper ( input [ i ] ) ) ;
95
+
96
+ if ( i == 0 )
97
+ {
98
+ // First character is uppercase, no hyphen needed (e.g. This → this)
99
+ return false ;
100
+ }
101
+
102
+ var prev = input [ i - 1 ] ;
103
+ if ( char . IsLower ( prev ) )
104
+ {
105
+ // Lowercase followed by uppercase (e.g. someThing → some-thing)
106
+ return true ;
107
+ }
108
+
109
+ if ( ( char . IsUpper ( prev ) || char . IsDigit ( prev ) ) &&
110
+ ( i + 1 < input . Length ) && char . IsLower ( input [ i + 1 ] ) )
111
+ {
112
+ // Uppercase or digit followed by uppercase, followed by lowercase (e.g. CAPSOn → caps-on or ONE1Two → ONE1-Two)
113
+ return true ;
114
+ }
115
+
116
+ return false ;
56
117
}
57
118
}
0 commit comments