|
4 | 4 | #nullable disable
|
5 | 5 |
|
6 | 6 | using System;
|
7 |
| -using System.Text.RegularExpressions; |
| 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 | {
|
@@ -52,6 +40,47 @@ internal static bool IsInvalidNonWhitespaceHtmlCharacters(char testChar)
|
52 | 40 | /// </example>
|
53 | 41 | public static string ToHtmlCase(string name)
|
54 | 42 | {
|
55 |
| - return HtmlCaseRegex.Replace(name, HtmlCaseRegexReplacement).ToLowerInvariant(); |
| 43 | + if (string.IsNullOrEmpty(name)) |
| 44 | + { |
| 45 | + return name; |
| 46 | + } |
| 47 | + |
| 48 | + var input = name.AsSpan(); |
| 49 | + |
| 50 | + using var _ = StringBuilderPool.GetPooledObject(out var result); |
| 51 | + result.SetCapacityIfLarger(input.Length + 5); |
| 52 | + |
| 53 | + // It's slightly faster to use a foreach and index manually, than to use a for 🤷 |
| 54 | + var i = 0; |
| 55 | + foreach (var c in name) |
| 56 | + { |
| 57 | + if (char.IsUpper(c)) |
| 58 | + { |
| 59 | + // Insert hyphen if: |
| 60 | + // - Not the first character, and |
| 61 | + // - Previous is lowercase or digit, or |
| 62 | + // - Previous is uppercase and next is lowercase (e.g. CAPSOn → caps-on) |
| 63 | + if (i > 0) |
| 64 | + { |
| 65 | + var prev = input[i - 1]; |
| 66 | + var prevIsLowerOrDigit = char.IsLower(prev) || char.IsDigit(prev); |
| 67 | + var prevIsUpper = char.IsUpper(prev); |
| 68 | + var nextIsLower = (i + 1 < input.Length) && char.IsLower(input[i + 1]); |
| 69 | + if (prevIsLowerOrDigit || (prevIsUpper && nextIsLower)) |
| 70 | + { |
| 71 | + result.Append('-'); |
| 72 | + } |
| 73 | + } |
| 74 | + result.Append(char.ToLowerInvariant(c)); |
| 75 | + } |
| 76 | + else |
| 77 | + { |
| 78 | + result.Append(c); |
| 79 | + } |
| 80 | + |
| 81 | + i++; |
| 82 | + } |
| 83 | + |
| 84 | + return result.ToString(); |
56 | 85 | }
|
57 | 86 | }
|
0 commit comments