From 254880e93ac64e4d76692603b2cdae25e5928756 Mon Sep 17 00:00:00 2001 From: Phil Scott Date: Fri, 2 Apr 2021 21:48:30 -0400 Subject: [PATCH] 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. --- src/Spectre.Console.Tests/Unit/EmojiTests.cs | 51 ++++++++++ src/Spectre.Console/Emoji.cs | 98 +++++++++++++++---- .../Extensions/StringBuilderExtensions.cs | 12 +++ src/Spectre.Console/Spectre.Console.csproj | 1 + 4 files changed, 145 insertions(+), 17 deletions(-) diff --git a/src/Spectre.Console.Tests/Unit/EmojiTests.cs b/src/Spectre.Console.Tests/Unit/EmojiTests.cs index 1e01694..97a89c5 100644 --- a/src/Spectre.Console.Tests/Unit/EmojiTests.cs +++ b/src/Spectre.Console.Tests/Unit/EmojiTests.cs @@ -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:!"); + } + } } } diff --git a/src/Spectre.Console/Emoji.cs b/src/Spectre.Console/Emoji.cs index ab2c08c..3a42c52 100644 --- a/src/Spectre.Console/Emoji.cs +++ b/src/Spectre.Console/Emoji.cs @@ -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 /// public static partial class Emoji { - private static readonly Regex _emojiCode = new Regex(@"(:(\S*?):)", RegexOptions.Compiled); private static readonly Dictionary _remappings; static Emoji() @@ -40,6 +41,7 @@ namespace Spectre.Console _remappings[tag] = emoji; } +#if NETSTANDARD2_0 /// /// Replaces emoji markup with corresponding unicode characters. /// @@ -47,24 +49,86 @@ namespace Spectre.Console /// A string with emoji codes replaced with actual emoji. public static string Replace(string value) { - static string ReplaceEmoji(Match match) + return Replace(value.AsSpan()); + } +#endif + + /// + /// Replaces emoji markup with corresponding unicode characters. + /// + /// A string with emojis codes, e.g. "Hello :smiley:!". + /// A string with emoji codes replaced with actual emoji. + public static string Replace(ReadOnlySpan 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 span, char value, int startIndex) + { + var indexInSlice = span.Slice(startIndex).IndexOf(value); + + if (indexInSlice == -1) + { + return -1; + } + + return startIndex + indexInSlice; } } -} +} \ No newline at end of file diff --git a/src/Spectre.Console/Extensions/StringBuilderExtensions.cs b/src/Spectre.Console/Extensions/StringBuilderExtensions.cs index ff394f6..8db370b 100644 --- a/src/Spectre.Console/Extensions/StringBuilderExtensions.cs +++ b/src/Spectre.Console/Extensions/StringBuilderExtensions.cs @@ -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 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 + } } } diff --git a/src/Spectre.Console/Spectre.Console.csproj b/src/Spectre.Console/Spectre.Console.csproj index e1e05e2..302031d 100644 --- a/src/Spectre.Console/Spectre.Console.csproj +++ b/src/Spectre.Console/Spectre.Console.csproj @@ -8,6 +8,7 @@ +