|
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;
|
| 5 | +using System.Diagnostics; |
| 6 | +using System.Diagnostics.CodeAnalysis; |
7 | 7 | using Microsoft.AspNetCore.Razor.PooledObjects;
|
8 | 8 |
|
9 | 9 | namespace Microsoft.AspNetCore.Razor.Language;
|
@@ -38,49 +38,81 @@ internal static bool IsInvalidNonWhitespaceHtmlCharacters(char testChar)
|
38 | 38 | /// ONE1TWO2THREE3 => one1two2three3
|
39 | 39 | /// First_Second_ThirdHi => first_second_third-hi
|
40 | 40 | /// </example>
|
41 |
| - public static string ToHtmlCase(string name) |
| 41 | + public static string ToHtmlCase(string input) |
42 | 42 | {
|
43 |
| - if (string.IsNullOrEmpty(name)) |
| 43 | + if (string.IsNullOrEmpty(input)) |
44 | 44 | {
|
45 |
| - return name; |
| 45 | + return input; |
46 | 46 | }
|
47 | 47 |
|
48 |
| - var input = name.AsSpan(); |
| 48 | + return TryGetKebabCaseString(input, out var result) |
| 49 | + ? result |
| 50 | + : input; |
| 51 | + } |
49 | 52 |
|
50 |
| - using var _ = StringBuilderPool.GetPooledObject(out var result); |
51 |
| - result.SetCapacityIfLarger(input.Length + 5); |
| 53 | + private static bool TryGetKebabCaseString(ReadOnlySpan<char> input, [NotNullWhen(true)] out string? result) |
| 54 | + { |
| 55 | + using var _ = StringBuilderPool.GetPooledObject(out var builder); |
52 | 56 |
|
53 |
| - // It's slightly faster to use a foreach and index manually, than to use a for 🤷 |
| 57 | + var allLower = true; |
54 | 58 | var i = 0;
|
55 |
| - foreach (var c in name) |
| 59 | + foreach (var c in input) |
56 | 60 | {
|
57 | 61 | if (char.IsUpper(c))
|
58 | 62 | {
|
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) |
| 63 | + allLower = false; |
| 64 | + |
| 65 | + if (ShouldInsertHyphenBeforeUppercase(input, i)) |
64 | 66 | {
|
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 |
| - } |
| 67 | + builder.Append('-'); |
73 | 68 | }
|
74 |
| - result.Append(char.ToLowerInvariant(c)); |
| 69 | + |
| 70 | + builder.Append(char.ToLowerInvariant(c)); |
75 | 71 | }
|
76 | 72 | else
|
77 | 73 | {
|
78 |
| - result.Append(c); |
| 74 | + builder.Append(c); |
79 | 75 | }
|
80 | 76 |
|
81 | 77 | i++;
|
82 | 78 | }
|
83 | 79 |
|
84 |
| - return result.ToString(); |
| 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 lowercase (e.g. CAPSOn → caps-on) |
| 113 | + return true; ; |
| 114 | + } |
| 115 | + |
| 116 | + return false; |
85 | 117 | }
|
86 | 118 | }
|
0 commit comments