mirror of
				https://github.com/nsnail/spectre.console.git
				synced 2025-10-31 09:09:25 +08:00 
			
		
		
		
	Replaces emoji regex with ReadOnlySpan implementation
The RegEx runtime perf was never anything noticeable - it was the startup time that was eating over a third of time during initialization. This shaves 200ms off the startup time.
This commit is contained in:
		 Phil Scott
					Phil Scott
				
			
				
					committed by
					
						 Patrik Svensson
						Patrik Svensson
					
				
			
			
				
	
			
			
			 Patrik Svensson
						Patrik Svensson
					
				
			
						parent
						
							6f16081f42
						
					
				
				
					commit
					254880e93a
				
			| @@ -41,5 +41,56 @@ namespace Spectre.Console.Tests.Unit | ||||
|                 result.ShouldBe("Hello 🌍!"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public sealed class Parsing | ||||
|         { | ||||
|             [Theory] | ||||
|             [InlineData(":", ":")] | ||||
|             [InlineData("::", "::")] | ||||
|             [InlineData(":::", ":::")] | ||||
|             [InlineData("::::", "::::")] | ||||
|             [InlineData("::i:", "::i:")] | ||||
|             [InlineData(":i:i:", ":i:i:")] | ||||
|             [InlineData("::globe_showing_europe_africa::", ":🌍:")] | ||||
|             [InlineData(":globe_showing_europe_africa::globe_showing_europe_africa:", "🌍🌍")] | ||||
|             [InlineData("::globe_showing_europe_africa:::test:::globe_showing_europe_africa:::", ":🌍::test::🌍::")] | ||||
|             public void Can_Handle_Different_Combinations(string markup, string expected) | ||||
|             { | ||||
|                 // Given | ||||
|                 var console = new FakeAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes); | ||||
|  | ||||
|                 // When | ||||
|                 console.Markup(markup); | ||||
|  | ||||
|                 // Then | ||||
|                 console.Output.ShouldBe(expected); | ||||
|             } | ||||
|  | ||||
|             [Fact] | ||||
|             public void Should_Leave_Single_Colons() | ||||
|             { | ||||
|                 // Given | ||||
|                 var console = new FakeAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes); | ||||
|  | ||||
|                 // When | ||||
|                 console.Markup("Hello :globe_showing_europe_africa:! Output: good"); | ||||
|  | ||||
|                 // Then | ||||
|                 console.Output.ShouldBe("Hello 🌍! Output: good"); | ||||
|             } | ||||
|  | ||||
|             [Fact] | ||||
|             public void Unknown_emojis_should_remain() | ||||
|             { | ||||
|                 // Given | ||||
|                 var console = new FakeAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes); | ||||
|  | ||||
|                 // When | ||||
|                 console.Markup("Hello :globe_showing_flat_earth:!"); | ||||
|  | ||||
|                 // Then | ||||
|                 console.Output.ShouldBe("Hello :globe_showing_flat_earth:!"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel.Design; | ||||
| using System.Text; | ||||
| using System.Text.RegularExpressions; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| @@ -9,7 +11,6 @@ namespace Spectre.Console | ||||
|     /// </summary> | ||||
|     public static partial class Emoji | ||||
|     { | ||||
|         private static readonly Regex _emojiCode = new Regex(@"(:(\S*?):)", RegexOptions.Compiled); | ||||
|         private static readonly Dictionary<string, string> _remappings; | ||||
|  | ||||
|         static Emoji() | ||||
| @@ -40,6 +41,7 @@ namespace Spectre.Console | ||||
|             _remappings[tag] = emoji; | ||||
|         } | ||||
|  | ||||
| #if NETSTANDARD2_0 | ||||
|         /// <summary> | ||||
|         /// Replaces emoji markup with corresponding unicode characters. | ||||
|         /// </summary> | ||||
| @@ -47,24 +49,86 @@ namespace Spectre.Console | ||||
|         /// <returns>A string with emoji codes replaced with actual emoji.</returns> | ||||
|         public static string Replace(string value) | ||||
|         { | ||||
|             static string ReplaceEmoji(Match match) | ||||
|             return Replace(value.AsSpan()); | ||||
|         } | ||||
| #endif | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Replaces emoji markup with corresponding unicode characters. | ||||
|         /// </summary> | ||||
|         /// <param name="value">A string with emojis codes, e.g. "Hello :smiley:!".</param> | ||||
|         /// <returns>A string with emoji codes replaced with actual emoji.</returns> | ||||
|         public static string Replace(ReadOnlySpan<char> value) | ||||
|         { | ||||
|             var output = new StringBuilder(); | ||||
|             var colonPos = value.IndexOf(':'); | ||||
|             if (colonPos == -1) | ||||
|             { | ||||
|                 var key = match.Groups[2].Value; | ||||
|  | ||||
|                 if (_remappings.Count > 0 && _remappings.TryGetValue(key, out var remappedEmoji)) | ||||
|                 { | ||||
|                     return remappedEmoji; | ||||
|                 } | ||||
|  | ||||
|                 if (_emojis.TryGetValue(key, out var emoji)) | ||||
|                 { | ||||
|                     return emoji; | ||||
|                 } | ||||
|  | ||||
|                 return match.Value; | ||||
|                 // No colons, no emoji. return what was passed in with no changes. | ||||
|                 return value.ToString(); | ||||
|             } | ||||
|  | ||||
|             return _emojiCode.Replace(value, ReplaceEmoji); | ||||
|             while ((colonPos = value.IndexOf(':')) != -1) | ||||
|             { | ||||
|                 // Append text up to colon | ||||
|                 output.AppendSpan(value.Slice(0, colonPos)); | ||||
|  | ||||
|                 // Set value equal to that colon and the rest of the string | ||||
|                 value = value.Slice(colonPos); | ||||
|  | ||||
|                 // Find colon after that. if no colon, break out | ||||
|                 var nextColonPos = value.IndexOf(':', 1); | ||||
|                 if (nextColonPos == -1) | ||||
|                 { | ||||
|                     break; | ||||
|                 } | ||||
|  | ||||
|                 // Get the emoji text minus the colons | ||||
|                 var emojiKey = value.Slice(1, nextColonPos - 1).ToString(); | ||||
|                 if (TryGetEmoji(emojiKey, out var emojiValue)) | ||||
|                 { | ||||
|                     output.Append(emojiValue); | ||||
|                     value = value.Slice(nextColonPos + 1); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     output.Append(':'); | ||||
|                     value = value.Slice(1); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             output.AppendSpan(value); | ||||
|             return output.ToString(); | ||||
|         } | ||||
|  | ||||
|         private static bool TryGetEmoji(string emoji, out string value) | ||||
|         { | ||||
|             if (_remappings.TryGetValue(emoji, out var remappedEmojiValue)) | ||||
|             { | ||||
|                 value = remappedEmojiValue; | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             if (_emojis.TryGetValue(emoji, out var emojiValue)) | ||||
|             { | ||||
|                 value = emojiValue; | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             value = string.Empty; | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         private static int IndexOf(this ReadOnlySpan<char> span, char value, int startIndex) | ||||
|         { | ||||
|             var indexInSlice = span.Slice(startIndex).IndexOf(value); | ||||
|  | ||||
|             if (indexInSlice == -1) | ||||
|             { | ||||
|                 return -1; | ||||
|             } | ||||
|  | ||||
|             return startIndex + indexInSlice; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
| @@ -1,3 +1,4 @@ | ||||
| using System; | ||||
| using System.Globalization; | ||||
| using System.Text; | ||||
|  | ||||
| @@ -25,5 +26,16 @@ namespace Spectre.Console | ||||
|  | ||||
|             return builder.Append(value); | ||||
|         } | ||||
|  | ||||
|         public static void AppendSpan(this StringBuilder builder, ReadOnlySpan<char> span) | ||||
|         { | ||||
|             // NetStandard 2 lacks the override for StringBuilder to add the span. We'll need to convert the span | ||||
|             // to a string for it, but for .NET 5.0 we'll use the override. | ||||
| #if NETSTANDARD2_0 | ||||
|             builder.Append(span.ToString()); | ||||
| #else | ||||
|             builder.Append(span); | ||||
| #endif | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -8,6 +8,7 @@ | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Wcwidth" Version="0.2.0" /> | ||||
|     <PackageReference Include="System.Memory" Version="4.5.0" Condition="'$(TargetFramework)' == 'netstandard2.0'" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user