Skip to content

Commit 8ccfde7

Browse files
committed
Rewrite the regex as a straight loop
1 parent 617e1fa commit 8ccfde7

File tree

2 files changed

+46
-17
lines changed

2 files changed

+46
-17
lines changed

src/Compiler/Microsoft.AspNetCore.Razor.Language/test/HtmlConventionsTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public static TheoryData HtmlConversionData
2121
{ "CAPSOnOUTSIDE", "caps-on-outside" },
2222
{ "ALLCAPS", "allcaps" },
2323
{ "One1Two2Three3", "one1-two2-three3" },
24-
{ "ONE1TWO2THREE3", "one1two2three3" },
24+
{ "ONE1TWO2THREE3", "one1-two2-three3" },
2525
{ "First_Second_ThirdHi", "first_second_third-hi" }
2626
};
2727
}
@@ -35,6 +35,6 @@ public void ToHtmlCase_ReturnsExpectedConversions(string input, string expectedO
3535
var output = HtmlConventions.ToHtmlCase(input);
3636

3737
// Assert
38-
Assert.Equal(output, expectedOutput);
38+
Assert.Equal(expectedOutput, output);
3939
}
4040
}

src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/HtmlConventions.cs

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,14 @@
44
#nullable disable
55

66
using System;
7-
using System.Text.RegularExpressions;
7+
using Microsoft.AspNetCore.Razor.PooledObjects;
88

99
namespace Microsoft.AspNetCore.Razor.Language;
1010

1111
public static class HtmlConventions
1212
{
13-
private const string HtmlCaseRegexReplacement = "-$1$2";
1413
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+
['@', '!', '<', '/', '?', '[', '>', ']', '=', '"', '\'', '*'];
2715

2816
internal static bool IsInvalidNonWhitespaceHtmlCharacters(char testChar)
2917
{
@@ -52,6 +40,47 @@ internal static bool IsInvalidNonWhitespaceHtmlCharacters(char testChar)
5240
/// </example>
5341
public static string ToHtmlCase(string name)
5442
{
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();
5685
}
5786
}

0 commit comments

Comments
 (0)