diff --git a/Dalamud.Test/Dalamud.Test.csproj b/Dalamud.Test/Dalamud.Test.csproj index ab73840ba..96ae59c05 100644 --- a/Dalamud.Test/Dalamud.Test.csproj +++ b/Dalamud.Test/Dalamud.Test.csproj @@ -59,6 +59,7 @@ + diff --git a/Dalamud.Test/Plugin/Sanitizer/SanitizerTests.cs b/Dalamud.Test/Plugin/Sanitizer/SanitizerTests.cs new file mode 100644 index 000000000..25aefd183 --- /dev/null +++ b/Dalamud.Test/Plugin/Sanitizer/SanitizerTests.cs @@ -0,0 +1,32 @@ +// ReSharper disable StringLiteralTypo + +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Dalamud.Test.Plugin.Sanitizer { + + public class SanitizerTests { + private global::Dalamud.Plugin.Sanitizer.Sanitizer sanitizer; + + [Theory] + [InlineData( ClientLanguage.English, "Pixie Cotton Hood of Healing", "Pixie Cotton Hood of Healing" )] + [InlineData( ClientLanguage.Japanese, "アラガントームストーン:真理", "アラガントームストーン:真理" )] + [InlineData( ClientLanguage.German, "Anemos-Pan\x02\x16\x01\x03zer\x02\x16\x01\x03hand\x02\x16\x01\x03schu\x02\x16\x01\x03he des Drachenbluts", "Anemos-Panzerhandschuhe des Drachenbluts" )] + [InlineData( ClientLanguage.German, "Bienen-Spatha †", "Bienen-Spatha" )] + [InlineData( ClientLanguage.French, "Le Diademe\x02\x1D\x01\x03: terrains de chasse|Le Diademe\x02\x1D\x01\x03: terrains de chasse", "Le Diademe: terrains de chasse|Le Diademe: terrains de chasse" )] + [InlineData( ClientLanguage.French, "Cuir de bœuf", "Cuir de boeuf" )] + public void StringsAreSanitizedCorrectly( + ClientLanguage clientLanguage, string unsanitizedString, string sanitizedString) + { + var sanitizedStrings = new List {unsanitizedString}; + sanitizer = new global::Dalamud.Plugin.Sanitizer.Sanitizer(clientLanguage); + Assert.Equal(sanitizedString, sanitizer.Sanitize(unsanitizedString)); + Assert.Equal(sanitizedString, sanitizer.Sanitize(sanitizedStrings).First()); + + sanitizer = new global::Dalamud.Plugin.Sanitizer.Sanitizer(ClientLanguage.English); + Assert.Equal(sanitizedString, sanitizer.Sanitize(unsanitizedString, clientLanguage)); + Assert.Equal(sanitizedString, sanitizer.Sanitize(sanitizedStrings, clientLanguage).First()); + } + } +} diff --git a/Dalamud.sln.DotSettings b/Dalamud.sln.DotSettings index 8b8890e01..af8f4d833 100644 --- a/Dalamud.sln.DotSettings +++ b/Dalamud.sln.DotSettings @@ -47,4 +47,5 @@ True True True - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index 6ea358135..c8dced858 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -19,6 +19,11 @@ namespace Dalamud.Data /// public class DataManager : IDisposable { + /// + /// The current game client language. + /// + internal ClientLanguage Language; + private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex"; /// @@ -26,8 +31,6 @@ namespace Dalamud.Data /// private Lumina.GameData gameData; - private ClientLanguage language; - private Thread luminaResourceThread; /// @@ -39,7 +42,7 @@ namespace Dalamud.Data // Set up default values so plugins do not null-reference when data is being loaded. this.ServerOpCodes = new ReadOnlyDictionary(new Dictionary()); - this.language = language; + this.Language = language; } /// @@ -94,14 +97,14 @@ namespace Dalamud.Data PanicOnSheetChecksumMismatch = false, #endif - DefaultExcelLanguage = this.language switch { - ClientLanguage.Japanese => Language.Japanese, - ClientLanguage.English => Language.English, - ClientLanguage.German => Language.German, - ClientLanguage.French => Language.French, + DefaultExcelLanguage = this.Language switch { + ClientLanguage.Japanese => Lumina.Data.Language.Japanese, + ClientLanguage.English => Lumina.Data.Language.English, + ClientLanguage.German => Lumina.Data.Language.German, + ClientLanguage.French => Lumina.Data.Language.French, _ => throw new ArgumentOutOfRangeException( - nameof(this.language), - "Unknown Language: " + this.language), + nameof(this.Language), + "Unknown Language: " + this.Language), }, }; @@ -156,11 +159,11 @@ namespace Dalamud.Data public ExcelSheet GetExcelSheet(ClientLanguage language) where T : ExcelRow { var lang = language switch { - ClientLanguage.Japanese => Language.Japanese, - ClientLanguage.English => Language.English, - ClientLanguage.German => Language.German, - ClientLanguage.French => Language.French, - _ => throw new ArgumentOutOfRangeException(nameof(this.language), "Unknown Language: " + this.language) + ClientLanguage.Japanese => Lumina.Data.Language.Japanese, + ClientLanguage.English => Lumina.Data.Language.English, + ClientLanguage.German => Lumina.Data.Language.German, + ClientLanguage.French => Lumina.Data.Language.French, + _ => throw new ArgumentOutOfRangeException(nameof(this.Language), "Unknown Language: " + this.Language) }; return this.Excel.GetSheet(lang); } @@ -207,7 +210,7 @@ namespace Dalamud.Data /// The containing the icon. public TexFile GetIcon(int iconId) { - return this.GetIcon(this.language, iconId); + return this.GetIcon(this.Language, iconId); } /// @@ -223,7 +226,7 @@ namespace Dalamud.Data ClientLanguage.English => "en/", ClientLanguage.German => "de/", ClientLanguage.French => "fr/", - _ => throw new ArgumentOutOfRangeException(nameof(this.language), "Unknown Language: " + this.language) + _ => throw new ArgumentOutOfRangeException(nameof(this.Language), "Unknown Language: " + this.Language) }; return this.GetIcon(type, iconId); diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index c0d9f12e3..dab5995e7 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -13,6 +13,7 @@ using Dalamud.Game.ClientState; using Dalamud.Game.Command; using Dalamud.Game.Internal; using Dalamud.Interface; +using Dalamud.Plugin.Sanitizer; namespace Dalamud.Plugin { @@ -48,6 +49,7 @@ namespace Dalamud.Plugin this.pluginName = pluginName; this.configs = configs; + this.Sanitizer = new Sanitizer.Sanitizer(this.Data.Language); this.UiLanguage = this.dalamud.Configuration.LanguageOverride; dalamud.LocalizationManager.OnLocalizationChanged += this.OnLocalizationChanged; } @@ -132,6 +134,11 @@ namespace Dalamud.Plugin /// public string UiLanguage { get; private set; } + /// + /// Gets serializer class with functions to remove special characters from strings. + /// + public ISanitizer Sanitizer { get; } + /// /// Gets the action that should be executed when any plugin sends a message. /// diff --git a/Dalamud/Plugin/Sanitizer/ISanitizer.cs b/Dalamud/Plugin/Sanitizer/ISanitizer.cs new file mode 100644 index 000000000..21fe8ab77 --- /dev/null +++ b/Dalamud/Plugin/Sanitizer/ISanitizer.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; + +namespace Dalamud.Plugin.Sanitizer +{ + /// + /// Sanitize strings to remove soft hyphens and other special characters. + /// + public interface ISanitizer + { + /// + /// Creates a sanitized string using current clientLanguage. + /// + /// An unsanitized string to sanitize. + /// A sanitized string. + string Sanitize(string unsanitizedString); + + /// + /// Creates a sanitized string using request clientLanguage. + /// + /// An unsanitized string to sanitize. + /// Target language for sanitized strings. + /// A sanitized string. + string Sanitize(string unsanitizedString, ClientLanguage clientLanguage); + + /// + /// Creates a list of sanitized strings using current clientLanguage. + /// + /// List of unsanitized string to sanitize. + /// A list of sanitized strings. + IEnumerable Sanitize(IEnumerable unsanitizedStrings); + + /// + /// Creates a list of sanitized strings using requested clientLanguage. + /// + /// List of unsanitized string to sanitize. + /// Target language for sanitized strings. + /// A list of sanitized strings. + IEnumerable Sanitize(IEnumerable unsanitizedStrings, ClientLanguage clientLanguage); + } +} diff --git a/Dalamud/Plugin/Sanitizer/Sanitizer.cs b/Dalamud/Plugin/Sanitizer/Sanitizer.cs new file mode 100644 index 000000000..58f6fde47 --- /dev/null +++ b/Dalamud/Plugin/Sanitizer/Sanitizer.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Dalamud.Plugin.Sanitizer +{ + /// + /// Sanitize strings to remove soft hyphens and other special characters. + /// + public class Sanitizer : ISanitizer + { + private static readonly Dictionary DESanitizationDict = new Dictionary + { + { "\u0020\u2020", string.Empty }, // dagger + }; + + private static readonly Dictionary FRSanitizationDict = new Dictionary + { + { "\u0153", "\u006F\u0065" }, // ligature oe + }; + + private readonly ClientLanguage defaultClientLanguage; + + /// + /// Initializes a new instance of the class. + /// + /// Default clientLanguage for sanitizing strings. + public Sanitizer(ClientLanguage defaultClientLanguage) + { + this.defaultClientLanguage = defaultClientLanguage; + } + + /// + /// Creates a sanitized string using current clientLanguage. + /// + /// An unsanitized string to sanitize. + /// A sanitized string. + public string Sanitize(string unsanitizedString) + { + return SanitizeByLanguage(unsanitizedString, this.defaultClientLanguage); + } + + /// + /// Creates a sanitized string using request clientLanguage. + /// + /// An unsanitized string to sanitize. + /// Target language for sanitized strings. + /// A sanitized string. + public string Sanitize(string unsanitizedString, ClientLanguage clientLanguage) + { + return SanitizeByLanguage(unsanitizedString, clientLanguage); + } + + /// + /// Creates a list of sanitized strings using current clientLanguage. + /// + /// List of unsanitized string to sanitize. + /// A list of sanitized strings. + public IEnumerable Sanitize(IEnumerable unsanitizedStrings) + { + return SanitizeByLanguage(unsanitizedStrings, this.defaultClientLanguage); + } + + /// + /// Creates a list of sanitized strings using requested clientLanguage. + /// + /// List of unsanitized string to sanitize. + /// Target language for sanitized strings. + /// A list of sanitized strings. + public IEnumerable Sanitize(IEnumerable unsanitizedStrings, ClientLanguage clientLanguage) + { + return SanitizeByLanguage(unsanitizedStrings, clientLanguage); + } + + private static string SanitizeByLanguage(string unsanitizedString, ClientLanguage clientLanguage) + { + var sanitizedString = FilterUnprintableCharacters(unsanitizedString); + switch (clientLanguage) + { + case ClientLanguage.Japanese: + case ClientLanguage.English: + return sanitizedString; + case ClientLanguage.German: + return FilterByDict(sanitizedString, DESanitizationDict); + case ClientLanguage.French: + return FilterByDict(sanitizedString, FRSanitizationDict); + default: + throw new ArgumentOutOfRangeException(nameof(clientLanguage), clientLanguage, null); + } + } + + private static IEnumerable SanitizeByLanguage( + IEnumerable unsanitizedStrings, ClientLanguage clientLanguage) + { + var sanitizedStrings = new List(); + switch (clientLanguage) + { + case ClientLanguage.Japanese: + case ClientLanguage.English: + sanitizedStrings.AddRange(unsanitizedStrings.Select(FilterUnprintableCharacters)); + return sanitizedStrings; + case ClientLanguage.German: + sanitizedStrings.AddRange( + unsanitizedStrings.Select( + unsanitizedString => + FilterByDict(FilterUnprintableCharacters(unsanitizedString), DESanitizationDict))); + return sanitizedStrings; + case ClientLanguage.French: + sanitizedStrings.AddRange( + unsanitizedStrings.Select( + unsanitizedString => + FilterByDict(FilterUnprintableCharacters(unsanitizedString), FRSanitizationDict))); + return sanitizedStrings; + default: + throw new ArgumentOutOfRangeException(nameof(clientLanguage), clientLanguage, null); + } + } + + private static string FilterUnprintableCharacters(string str) + { + return new string(str?.Where(ch => ch >= 0x20).ToArray()); + } + + private static string FilterByDict(string str, Dictionary dict) + { + return dict.Aggregate( + str, (current, kvp) => + current.Replace(kvp.Key, kvp.Value)); + } + } +}