diff --git a/T2MDCli/Cli.cs b/T2MDCli/Cli.cs index 4611dd0..c2d58ad 100644 --- a/T2MDCli/Cli.cs +++ b/T2MDCli/Cli.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading.Tasks; using System.Text.Json.Serialization; +using System.Text.RegularExpressions; namespace GoldenSyrupGames.T2MD { @@ -230,8 +231,10 @@ CliOptions options List boardComments = await GetBoardCommentsAsync(trelloApiBoard.ID); // write the json to file (overwrite). + // without this linux will happily write /'s - string usableBoardName = FileSystem.SanitiseForPath(trelloApiBoard.Name); + string usableBoardName = GetUsableBoardName(trelloApiBoard, options); + string boardOutputFilePath = Path.Combine(_outputPath, $"{usableBoardName}.json"); using FileStream fileStream = File.Create(boardOutputFilePath); using Stream contentStream = await response.Content @@ -272,7 +275,7 @@ await boardJsonStreamReader.ReadToEndAsync().ConfigureAwait(false), // differentiate duplicate list names Dictionary duplicateSuffixes = GetDuplicateSuffixes( orderedLists, - options.MaxCardFilenameTitleLength + options ); // create folders for each list. @@ -306,7 +309,7 @@ await boardJsonStreamReader.ReadToEndAsync().ConfigureAwait(false), // cards will become files and that makes the most sense for the user Dictionary duplicateCardSuffixes = GetDuplicateSuffixes( orderedCards, - options.MaxCardFilenameTitleLength + options ); // process each card @@ -361,6 +364,30 @@ await boardJsonStreamReader.ReadToEndAsync().ConfigureAwait(false), AnsiConsole.MarkupLine($" [green]Finished {trelloApiBoard.Name}[/]"); } + /// + /// Returns the name of the board with any filesystem-incompatible characters removed. + /// + /// Trims preceding and trailing whitespace to avoid Windows being unable to use the folder + /// (and to neaten things up). + /// Replaces multiple whitespace with a single space (usually from removing emoji). + /// If specified in options, emoji are removed here as well. + /// + public static string GetUsableBoardName( + TrelloApiBoardModel trelloApiBoard, + CliOptions options + ) + { + string usableBoardName = FileSystem.SanitiseForPath(trelloApiBoard.Name); + if (options.RemoveEmoji) + { + usableBoardName = Emoji.ReplaceEmoji(usableBoardName, ""); + } + usableBoardName = usableBoardName.Trim(); + Regex multipleSpaces = new Regex("\\s+"); + usableBoardName = multipleSpaces.Replace(usableBoardName, " "); + return usableBoardName; + } + /// /// Returns the commentCard action models for a board. /// Retrieves them from the API because the json export only contains the last 1000 actions. @@ -466,7 +493,7 @@ string duplicateDifferentiator // create a folder for each list. // remove special characters - string usableListName = FileSystem.SanitiseForPath(trelloList.Name); + string usableListName = GetUsableListName(trelloList, options); if (options.NoNumbering && !string.IsNullOrEmpty(duplicateDifferentiator)) { usableListName += $" {duplicateDifferentiator}"; @@ -495,6 +522,27 @@ string duplicateDifferentiator } } + /// + /// Returns the name of the list with any filesystem-incompatible characters removed. + /// + /// Trims preceding and trailing whitespace to avoid Windows being unable to use the folder + /// (and to neaten things up). + /// Replaces multiple whitespace with a single space (usually from removing emoji). + /// If specified in options, emoji are removed here as well. + /// + public static string GetUsableListName(TrelloListModel trelloList, CliOptions options) + { + string usableListName = FileSystem.SanitiseForPath(trelloList.Name); + if (options.RemoveEmoji) + { + usableListName = Emoji.ReplaceEmoji(usableListName, ""); + } + usableListName = usableListName.Trim(); + Regex multipleSpaces = new Regex("\\s+"); + usableListName = multipleSpaces.Replace(usableListName, " "); + return usableListName; + } + /// /// Creates a markdown file each for the board's description, comments, checklists and list /// of attachments. @@ -528,10 +576,7 @@ string duplicateDifferentiator { // restrict the maximum filename length for all files. Just via the title, not any // suffix or prefix - string usableCardName = GetUsableCardName( - trelloCard, - options.MaxCardFilenameTitleLength - ); + string usableCardName = GetUsableCardName(trelloCard, options); if (options.NoNumbering && !string.IsNullOrEmpty(duplicateDifferentiator)) { @@ -612,21 +657,29 @@ await UpdateAttachmentReferencesAsync( /// /// Returns the name of the card limited to the length specified by the user and with any - /// special characters removed. + /// filesystem-incompatible characters removed. + /// Trims preceding and trailing whitespace to avoid Windows being unable to use the folder + /// (and to neaten things up). + /// Replaces multiple whitespace with a single space (usually from removing emoji). + /// If specified in options, emoji are removed here as well. /// - private static string GetUsableCardName( - TrelloCardModel trelloCard, - int maxCardFilenameTitleLength - ) + public static string GetUsableCardName(TrelloCardModel trelloCard, CliOptions options) { int actualOrRestrictedLength = Math.Min( trelloCard.Name.Length, - maxCardFilenameTitleLength + options.MaxCardFilenameTitleLength ); string usableCardName = trelloCard.Name.Substring(0, actualOrRestrictedLength); - // remove special characters + // remove special filesystem characters usableCardName = FileSystem.SanitiseForPath(usableCardName); - + // remove emoji if specified + if (options.RemoveEmoji) + { + usableCardName = Emoji.ReplaceEmoji(usableCardName, ""); + } + usableCardName = usableCardName.Trim(); + Regex multipleSpaces = new Regex("\\s+"); + usableCardName = multipleSpaces.Replace(usableCardName, " "); return usableCardName; } @@ -1058,7 +1111,7 @@ await File.WriteAllTextAsync(commentsPath, replacedCommentsContents) /// public static Dictionary GetDuplicateSuffixes( IEnumerable potentialDuplicates, - int maxCardFilenameTitleLength + CliOptions options ) { var output = new Dictionary(); @@ -1068,7 +1121,7 @@ int maxCardFilenameTitleLength foreach (ITrelloCommon potentialDuplicate in potentialDuplicates) { - string name = GetDuplicateNameKey(potentialDuplicate, maxCardFilenameTitleLength); + string name = GetDuplicateNameKey(potentialDuplicate, options); // increment how many times we've seen this name. int count; // if in there, grab the current value, then increment it. @@ -1094,7 +1147,7 @@ int maxCardFilenameTitleLength string oneAsString = 1.ToString(); foreach ((ITrelloCommon entry, string suffix) in output) { - string name = GetDuplicateNameKey(entry, maxCardFilenameTitleLength); + string name = GetDuplicateNameKey(entry, options); if (suffix == oneAsString && occurrences[name] == 1) { output[entry] = ""; @@ -1105,14 +1158,12 @@ int maxCardFilenameTitleLength } /// - /// Generates the card/list name key used in GetDuplicateSuffixes() so that card names are - /// only differentiated within each list. - /// For lists (or other non-cards) the list name is returned unchanged. - /// For cards the list ID is appended to the card name. + /// Generates the card/list name key used in GetDuplicateSuffixes(). + /// Card names are differentiated within each list. /// private static string GetDuplicateNameKey( ITrelloCommon potentialDuplicate, - int maxCardFilenameTitleLength + CliOptions options ) { // notes: `as` returns null if the cast fails, casting (prefixing with `(type)`) throws @@ -1127,14 +1178,19 @@ int maxCardFilenameTitleLength // - compare case insensitive. We want to write the original case to disk (so it's // not in `GetUsableCardName()`) but still avoid overwriting a different one // that's already been written on case-insensitive systems - string usableCardName = GetUsableCardName(card, maxCardFilenameTitleLength) - .ToLower(); + string usableCardName = GetUsableCardName(card, options).ToLower(); return usableCardName + card.IDList; } - else + + var list = potentialDuplicate as TrelloListModel; + if (list != null) { - return potentialDuplicate.Name.ToLower(); + string usableListName = GetUsableListName(list, options).ToLower(); + return usableListName; } + + // else + return potentialDuplicate.Name.ToLower(); } } } diff --git a/T2MDCli/CliOptions.cs b/T2MDCli/CliOptions.cs index 2d056e6..8e7f8a9 100644 --- a/T2MDCli/CliOptions.cs +++ b/T2MDCli/CliOptions.cs @@ -10,7 +10,7 @@ namespace GoldenSyrupGames.T2MD { // commandline arguments - class CliOptions + public class CliOptions { // The path under which the backups will be stored and the markdown folder structure // created. Will be created if it doesn't exist. @@ -78,6 +78,16 @@ class CliOptions )] public bool NoNumbering { get; set; } = false; + /// + /// Replace emoji with _. + /// + [Option( + "remove-emoji", + Default = false, + HelpText = "Dropbox doesn't support emoji in filenames.\n" + "This switch removes them." + )] + public bool RemoveEmoji { get; set; } = false; + public static Task PrintUsage(IEnumerable errors) { Console.WriteLine( diff --git a/T2MDCli/Helpers.cs b/T2MDCli/Helpers.cs index 2ffb078..fb336bb 100644 --- a/T2MDCli/Helpers.cs +++ b/T2MDCli/Helpers.cs @@ -4,6 +4,7 @@ using System.IO; using System.Net.Http; using System.Text.Json; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -173,4 +174,26 @@ public static string SanitiseForPath(string FolderOrFileName) .TrimEnd('.'); } } + + public static class Emoji + { + /// + /// Returns `input` with all* emoji replaced with `replacement`. Defaults to "_" + /// *See remarks. + /// + /// + /// This is wild. There's a regex that one dude keeps updating because it's so hard to match + /// them all based on the standard. + /// From https://stackoverflow.com/a/48148218 + /// + /// The string to remove emoji from. + /// + public static string ReplaceEmoji(string input, string replacement = "_") + { + Regex emojiPattern = new Regex( + @"[#*0-9]\uFE0F?\u20E3|©\uFE0F?|[®\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA]\uFE0F?|[\u231A\u231B]|[\u2328\u23CF]\uFE0F?|[\u23E9-\u23EC]|[\u23ED-\u23EF]\uFE0F?|\u23F0|[\u23F1\u23F2]\uFE0F?|\u23F3|[\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC]\uFE0F?|[\u25FD\u25FE]|[\u2600-\u2604\u260E\u2611]\uFE0F?|[\u2614\u2615]|\u2618\uFE0F?|\u261D(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|[\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642]\uFE0F?|[\u2648-\u2653]|[\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E]\uFE0F?|\u267F|\u2692\uFE0F?|\u2693|[\u2694-\u2697\u2699\u269B\u269C\u26A0]\uFE0F?|\u26A1|\u26A7\uFE0F?|[\u26AA\u26AB]|[\u26B0\u26B1]\uFE0F?|[\u26BD\u26BE\u26C4\u26C5]|\u26C8\uFE0F?|\u26CE|[\u26CF\u26D1\u26D3]\uFE0F?|\u26D4|\u26E9\uFE0F?|\u26EA|[\u26F0\u26F1]\uFE0F?|[\u26F2\u26F3]|\u26F4\uFE0F?|\u26F5|[\u26F7\u26F8]\uFE0F?|\u26F9(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?|\uFE0F(?:\u200D[\u2640\u2642]\uFE0F?)?)?|[\u26FA\u26FD]|\u2702\uFE0F?|\u2705|[\u2708\u2709]\uFE0F?|[\u270A\u270B](?:\uD83C[\uDFFB-\uDFFF])?|[\u270C\u270D](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|\u270F\uFE0F?|[\u2712\u2714\u2716\u271D\u2721]\uFE0F?|\u2728|[\u2733\u2734\u2744\u2747]\uFE0F?|[\u274C\u274E\u2753-\u2755\u2757]|\u2763\uFE0F?|\u2764(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79)|\uFE0F(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?)?|[\u2795-\u2797]|\u27A1\uFE0F?|[\u27B0\u27BF]|[\u2934\u2935\u2B05-\u2B07]\uFE0F?|[\u2B1B\u2B1C\u2B50\u2B55]|[\u3030\u303D\u3297\u3299]\uFE0F?|\uD83C(?:[\uDC04\uDCCF]|[\uDD70\uDD71\uDD7E\uDD7F]\uFE0F?|[\uDD8E\uDD91-\uDD9A]|\uDDE6\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF]|\uDDE7\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF]|\uDDE8\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF]|\uDDE9\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF]|\uDDEA\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA]|\uDDEB\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7]|\uDDEC\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE]|\uDDED\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA]|\uDDEE\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9]|\uDDEF\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5]|\uDDF0\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF]|\uDDF1\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE]|\uDDF2\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF]|\uDDF3\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF]|\uDDF4\uD83C\uDDF2|\uDDF5\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE]|\uDDF6\uD83C\uDDE6|\uDDF7\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC]|\uDDF8\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF]|\uDDF9\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF]|\uDDFA\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF]|\uDDFB\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA]|\uDDFC\uD83C[\uDDEB\uDDF8]|\uDDFD\uD83C\uDDF0|\uDDFE\uD83C[\uDDEA\uDDF9]|\uDDFF\uD83C[\uDDE6\uDDF2\uDDFC]|\uDE01|\uDE02\uFE0F?|[\uDE1A\uDE2F\uDE32-\uDE36]|\uDE37\uFE0F?|[\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20]|[\uDF21\uDF24-\uDF2C]\uFE0F?|[\uDF2D-\uDF35]|\uDF36\uFE0F?|[\uDF37-\uDF7C]|\uDF7D\uFE0F?|[\uDF7E-\uDF84]|\uDF85(?:\uD83C[\uDFFB-\uDFFF])?|[\uDF86-\uDF93]|[\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F]\uFE0F?|[\uDFA0-\uDFC1]|\uDFC2(?:\uD83C[\uDFFB-\uDFFF])?|[\uDFC3\uDFC4](?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|[\uDFC5\uDFC6]|\uDFC7(?:\uD83C[\uDFFB-\uDFFF])?|[\uDFC8\uDFC9]|\uDFCA(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|[\uDFCB\uDFCC](?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?|\uFE0F(?:\u200D[\u2640\u2642]\uFE0F?)?)?|[\uDFCD\uDFCE]\uFE0F?|[\uDFCF-\uDFD3]|[\uDFD4-\uDFDF]\uFE0F?|[\uDFE0-\uDFF0]|\uDFF3(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08)|\uFE0F(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?)?|\uDFF4(?:\u200D\u2620\uFE0F?|\uDB40\uDC67\uDB40\uDC62\uDB40(?:\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDC73\uDB40\uDC63\uDB40\uDC74|\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F)?|[\uDFF5\uDFF7]\uFE0F?|[\uDFF8-\uDFFF])|\uD83D(?:[\uDC00-\uDC07]|\uDC08(?:\u200D\u2B1B)?|[\uDC09-\uDC14]|\uDC15(?:\u200D\uD83E\uDDBA)?|[\uDC16-\uDC3A]|\uDC3B(?:\u200D\u2744\uFE0F?)?|[\uDC3C-\uDC3E]|\uDC3F\uFE0F?|\uDC40|\uDC41(?:\u200D\uD83D\uDDE8\uFE0F?|\uFE0F(?:\u200D\uD83D\uDDE8\uFE0F?)?)?|[\uDC42\uDC43](?:\uD83C[\uDFFB-\uDFFF])?|[\uDC44\uDC45]|[\uDC46-\uDC50](?:\uD83C[\uDFFB-\uDFFF])?|[\uDC51-\uDC65]|[\uDC66\uDC67](?:\uD83C[\uDFFB-\uDFFF])?|\uDC68(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?|[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92])|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF]|[\uDDAF-\uDDB3\uDDBC\uDDBD])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF]|[\uDDAF-\uDDB3\uDDBC\uDDBD])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|[\uDDAF-\uDDB3\uDDBC\uDDBD])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF]|[\uDDAF-\uDDB3\uDDBC\uDDBD])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE]|[\uDDAF-\uDDB3\uDDBC\uDDBD])))?))?|\uDC69(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?[\uDC68\uDC69]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?|\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92])|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFF]|\uDC8B\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFC-\uDFFF]|[\uDDAF-\uDDB3\uDDBC\uDDBD])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFF]|\uDC8B\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFD-\uDFFF]|[\uDDAF-\uDDB3\uDDBC\uDDBD])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFF]|\uDC8B\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|[\uDDAF-\uDDB3\uDDBC\uDDBD])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFF]|\uDC8B\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFD\uDFFF]|[\uDDAF-\uDDB3\uDDBC\uDDBD])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFF]|\uDC8B\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFE]|[\uDDAF-\uDDB3\uDDBC\uDDBD])))?))?|\uDC6A|[\uDC6B-\uDC6D](?:\uD83C[\uDFFB-\uDFFF])?|\uDC6E(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|\uDC6F(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDC70\uDC71](?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|\uDC72(?:\uD83C[\uDFFB-\uDFFF])?|\uDC73(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|[\uDC74-\uDC76](?:\uD83C[\uDFFB-\uDFFF])?|\uDC77(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|\uDC78(?:\uD83C[\uDFFB-\uDFFF])?|[\uDC79-\uDC7B]|\uDC7C(?:\uD83C[\uDFFB-\uDFFF])?|[\uDC7D-\uDC80]|[\uDC81\uDC82](?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|\uDC83(?:\uD83C[\uDFFB-\uDFFF])?|\uDC84|\uDC85(?:\uD83C[\uDFFB-\uDFFF])?|[\uDC86\uDC87](?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|[\uDC88-\uDC8E]|\uDC8F(?:\uD83C[\uDFFB-\uDFFF])?|\uDC90|\uDC91(?:\uD83C[\uDFFB-\uDFFF])?|[\uDC92-\uDCA9]|\uDCAA(?:\uD83C[\uDFFB-\uDFFF])?|[\uDCAB-\uDCFC]|\uDCFD\uFE0F?|[\uDCFF-\uDD3D]|[\uDD49\uDD4A]\uFE0F?|[\uDD4B-\uDD4E\uDD50-\uDD67]|[\uDD6F\uDD70\uDD73]\uFE0F?|\uDD74(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|\uDD75(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?|\uFE0F(?:\u200D[\u2640\u2642]\uFE0F?)?)?|[\uDD76-\uDD79]\uFE0F?|\uDD7A(?:\uD83C[\uDFFB-\uDFFF])?|[\uDD87\uDD8A-\uDD8D]\uFE0F?|\uDD90(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|[\uDD95\uDD96](?:\uD83C[\uDFFB-\uDFFF])?|\uDDA4|[\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA]\uFE0F?|[\uDDFB-\uDE2D]|\uDE2E(?:\u200D\uD83D\uDCA8)?|[\uDE2F-\uDE34]|\uDE35(?:\u200D\uD83D\uDCAB)?|\uDE36(?:\u200D\uD83C\uDF2B\uFE0F?)?|[\uDE37-\uDE44]|[\uDE45-\uDE47](?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|[\uDE48-\uDE4A]|\uDE4B(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|\uDE4C(?:\uD83C[\uDFFB-\uDFFF])?|[\uDE4D\uDE4E](?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|\uDE4F(?:\uD83C[\uDFFB-\uDFFF])?|[\uDE80-\uDEA2]|\uDEA3(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|[\uDEA4-\uDEB3]|[\uDEB4-\uDEB6](?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|[\uDEB7-\uDEBF]|\uDEC0(?:\uD83C[\uDFFB-\uDFFF])?|[\uDEC1-\uDEC5]|\uDECB\uFE0F?|\uDECC(?:\uD83C[\uDFFB-\uDFFF])?|[\uDECD-\uDECF]\uFE0F?|[\uDED0-\uDED2\uDED5-\uDED7]|[\uDEE0-\uDEE5\uDEE9]\uFE0F?|[\uDEEB\uDEEC]|[\uDEF0\uDEF3]\uFE0F?|[\uDEF4-\uDEFC\uDFE0-\uDFEB])|\uD83E(?:\uDD0C(?:\uD83C[\uDFFB-\uDFFF])?|[\uDD0D\uDD0E]|\uDD0F(?:\uD83C[\uDFFB-\uDFFF])?|[\uDD10-\uDD17]|[\uDD18-\uDD1C](?:\uD83C[\uDFFB-\uDFFF])?|\uDD1D|[\uDD1E\uDD1F](?:\uD83C[\uDFFB-\uDFFF])?|[\uDD20-\uDD25]|\uDD26(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|[\uDD27-\uDD2F]|[\uDD30-\uDD34](?:\uD83C[\uDFFB-\uDFFF])?|\uDD35(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|\uDD36(?:\uD83C[\uDFFB-\uDFFF])?|[\uDD37-\uDD39](?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|\uDD3A|\uDD3C(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD3D\uDD3E](?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|[\uDD3F-\uDD45\uDD47-\uDD76]|\uDD77(?:\uD83C[\uDFFB-\uDFFF])?|[\uDD78\uDD7A-\uDDB4]|[\uDDB5\uDDB6](?:\uD83C[\uDFFB-\uDFFF])?|\uDDB7|[\uDDB8\uDDB9](?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|\uDDBA|\uDDBB(?:\uD83C[\uDFFB-\uDFFF])?|[\uDDBC-\uDDCB]|[\uDDCD-\uDDCF](?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|\uDDD0|\uDDD1(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:\uDD1D\u200D\uD83E\uDDD1|[\uDDAF-\uDDB3\uDDBC\uDDBD]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|[\uDDAF-\uDDB3\uDDBC\uDDBD])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|[\uDDAF-\uDDB3\uDDBC\uDDBD])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|[\uDDAF-\uDDB3\uDDBC\uDDBD])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|[\uDDAF-\uDDB3\uDDBC\uDDBD])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|[\uDDAF-\uDDB3\uDDBC\uDDBD])))?))?|[\uDDD2\uDDD3](?:\uD83C[\uDFFB-\uDFFF])?|\uDDD4(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|\uDDD5(?:\uD83C[\uDFFB-\uDFFF])?|[\uDDD6-\uDDDD](?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF](?:\u200D[\u2640\u2642]\uFE0F?)?)?|[\uDDDE\uDDDF](?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDDE0-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6])" + ); + return emojiPattern.Replace(input, replacement); + } + } } diff --git a/T2MDCli/Properties/launchSettings.json b/T2MDCli/Properties/launchSettings.json index f755078..ca6c314 100644 --- a/T2MDCli/Properties/launchSettings.json +++ b/T2MDCli/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "T2MDCli": { "commandName": "Project", - "commandLineArgs": "--output-folder .\\t2md-test --no-numbering", + "commandLineArgs": "--output-folder .\\t2md-test --no-numbering --remove-emoji", "workingDirectory": "%userprofile%" } } diff --git a/T2MDCli/T2MDCli.csproj b/T2MDCli/T2MDCli.csproj index fcb9e7f..621172d 100644 --- a/T2MDCli/T2MDCli.csproj +++ b/T2MDCli/T2MDCli.csproj @@ -8,7 +8,7 @@ t2md Ben Renninson Golden Syrup Games - 1.5.0 + 1.6.0 2022 Golden Syrup Games diff --git a/T2MDCliTests/CliTests.cs b/T2MDCliTests/CliTests.cs index aa13b94..7394fd0 100644 --- a/T2MDCliTests/CliTests.cs +++ b/T2MDCliTests/CliTests.cs @@ -73,7 +73,8 @@ public void GetDuplicateSuffixes_CardsSameList_ReturnsCorrectSuffixes() { card3, "2" } }; - var cardSuffixes = Cli.GetDuplicateSuffixes(input, 20); + var options = new CliOptions() { MaxCardFilenameTitleLength = 20 }; + var cardSuffixes = Cli.GetDuplicateSuffixes(input, options); CollectionAssert.AreEquivalent(cardSuffixes, output); } @@ -97,7 +98,8 @@ public void GetDuplicateSuffixes_CardsTruncated_ReturnsCorrectSuffixes() { card3, "3" } }; - var cardSuffixes = Cli.GetDuplicateSuffixes(input, 2); + var options = new CliOptions() { MaxCardFilenameTitleLength = 2 }; + var cardSuffixes = Cli.GetDuplicateSuffixes(input, options); CollectionAssert.AreEquivalent(cardSuffixes, output); } @@ -115,7 +117,24 @@ public void GetDuplicateSuffixes_DuplicateCase_ReturnsCorrectSuffixes() var output = new Dictionary() { { card1, "1" }, { card2, "2" } }; - var cardSuffixes = Cli.GetDuplicateSuffixes(input, 20); + var options = new CliOptions() { MaxCardFilenameTitleLength = 20 }; + var cardSuffixes = Cli.GetDuplicateSuffixes(input, options); + + CollectionAssert.AreEquivalent(cardSuffixes, output); + } + + [TestMethod] + public void GetDuplicateSuffixes_CardsEmojiRemoveEmoji_ReturnsCorrectSuffixes() + { + var card1 = new TrelloCardModel() { Name = "Card 💪", IDList = "1" }; + var card2 = new TrelloCardModel() { Name = "Card ❤", IDList = "1" }; + + var input = new TrelloCardModel[] { card1, card2, }; + + var output = new Dictionary() { { card1, "1" }, { card2, "2" } }; + + var options = new CliOptions() { MaxCardFilenameTitleLength = 20, RemoveEmoji = true }; + var cardSuffixes = Cli.GetDuplicateSuffixes(input, options); CollectionAssert.AreEquivalent(cardSuffixes, output); } @@ -136,7 +155,8 @@ public void GetDuplicateSuffixes_Lists_ReturnsCorrectSuffixes() { List3, "2" } }; - var ListSuffixes = Cli.GetDuplicateSuffixes(input, 20); + var options = new CliOptions() { MaxCardFilenameTitleLength = 20 }; + var ListSuffixes = Cli.GetDuplicateSuffixes(input, options); CollectionAssert.AreEquivalent(ListSuffixes, output); } @@ -160,7 +180,8 @@ public void GetDuplicateSuffixes_ListsTruncated_ReturnsCorrectSuffixes() { List3, "2" } }; - var ListSuffixes = Cli.GetDuplicateSuffixes(input, 2); + var options = new CliOptions() { MaxCardFilenameTitleLength = 2 }; + var ListSuffixes = Cli.GetDuplicateSuffixes(input, options); CollectionAssert.AreEquivalent(ListSuffixes, output); } @@ -178,9 +199,65 @@ public void GetDuplicateSuffixes_ListsDuplicateCase_ReturnsCorrectSuffixes() var output = new Dictionary() { { List1, "1" }, { List2, "2" } }; - var ListSuffixes = Cli.GetDuplicateSuffixes(input, 20); + var options = new CliOptions() { MaxCardFilenameTitleLength = 20 }; + var ListSuffixes = Cli.GetDuplicateSuffixes(input, options); CollectionAssert.AreEquivalent(ListSuffixes, output); } + + [TestMethod] + public void GetDuplicateSuffixes_ListsEmojiRemoveEmoji_ReturnsCorrectSuffixes() + { + var list1 = new TrelloListModel() { Name = "List 💪" }; + var list2 = new TrelloListModel() { Name = "List ❤" }; + + var input = new TrelloListModel[] { list1, list2, }; + + var output = new Dictionary() { { list1, "1" }, { list2, "2" } }; + + var options = new CliOptions() { MaxCardFilenameTitleLength = 40, RemoveEmoji = true }; + var listSuffixes = Cli.GetDuplicateSuffixes(input, options); + + CollectionAssert.AreEquivalent(listSuffixes, output); + } + + [TestMethod] + public void GetUsableCardName_PrecedingTrailingInnerDoubleSpaces_TrimmedAndRemoved() + { + var card = new TrelloCardModel() { Name = " card title " }; + + string expectedOutput = "card title"; + + var options = new CliOptions() { MaxCardFilenameTitleLength = 40 }; + string actualOutput = Cli.GetUsableCardName(card, options); + + Assert.AreEqual(expectedOutput, actualOutput); + } + + [TestMethod] + public void GetUsableListName_PrecedingTrailingInnerDoubleSpaces_TrimmedAndRemoved() + { + var list = new TrelloListModel() { Name = " list title " }; + + string expectedOutput = "list title"; + + var options = new CliOptions(); + string actualOutput = Cli.GetUsableListName(list, options); + + Assert.AreEqual(expectedOutput, actualOutput); + } + + [TestMethod] + public void GetUsableBoardName_PrecedingTrailingInnerDoubleSpaces_TrimmedAndRemoved() + { + var board = new TrelloApiBoardModel() { Name = " board title " }; + + string expectedOutput = "board title"; + + var options = new CliOptions(); + string actualOutput = Cli.GetUsableBoardName(board, options); + + Assert.AreEqual(expectedOutput, actualOutput); + } } } diff --git a/T2MDCliTests/EmojiTests.cs b/T2MDCliTests/EmojiTests.cs new file mode 100644 index 0000000..f082838 --- /dev/null +++ b/T2MDCliTests/EmojiTests.cs @@ -0,0 +1,35 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GoldenSyrupGames.T2MD.Tests +{ + [TestClass] + public class EmojiTests + { + [TestMethod] + public void ReplaceEmoji_EmojiInMiddleDefaultReplacement_ReplacesEmojiWithUnderscore() + { + var input = "start 💪☂ end"; + var expectedOutput = "start __ end"; + + string actualOutput = Emoji.ReplaceEmoji(input); + + Assert.AreEqual(expectedOutput, actualOutput); + } + + [TestMethod] + public void ReplaceEmoji_EmojiInMiddleBlankReplacement_RemovesEmoji() + { + var input = "start 💪☂ end"; + var expectedOutput = "start __ end"; + + string actualOutput = Emoji.ReplaceEmoji(input); + + Assert.AreEqual(expectedOutput, actualOutput); + } + } +}