* Add ability to support different types of quotation marks * Added normal quotation mark to list of aliases, removed single quote mark * clean up leftover changes from testing * change quotation mark parsing to use a map of matching pairs * remove commented out code * Fix conventions of the command parser utility functions * change storage type of alias dictionary to be IReadOnlyDictionary * revert type of CommandServiceConfig QuotationMarkAliasMap to Dictionary * minor formatting changes to CommandParser * remove unnecessary whitespace * Move aliases outside of CommandInfo class * copy IReadOnlyDictionary to ImmutableDictionary * minor syntax changes in CommandServiceConfig * add newline before namespace for consistency * newline formatting tweak * simplification of GetMatch method for CommandParser * add more quote unicode punctuation pairs * add check for null value when building ImmutableDictionary * Move default alias map into a separate source file * Ensure that the collection passed into command service is not nulltags/2.0
| @@ -1,4 +1,5 @@ | |||
| using System; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| @@ -13,8 +14,7 @@ namespace Discord.Commands | |||
| Parameter, | |||
| QuotedParameter | |||
| } | |||
| public static async Task<ParseResult> ParseArgsAsync(CommandInfo command, ICommandContext context, IServiceProvider services, string input, int startPos) | |||
| public static async Task<ParseResult> ParseArgsAsync(CommandInfo command, ICommandContext context, bool ignoreExtraArgs, IServiceProvider services, string input, int startPos, IReadOnlyDictionary<char, char> aliasMap) | |||
| { | |||
| ParameterInfo curParam = null; | |||
| StringBuilder argBuilder = new StringBuilder(input.Length); | |||
| @@ -24,7 +24,27 @@ namespace Discord.Commands | |||
| var argList = ImmutableArray.CreateBuilder<TypeReaderResult>(); | |||
| var paramList = ImmutableArray.CreateBuilder<TypeReaderResult>(); | |||
| bool isEscaping = false; | |||
| char c; | |||
| char c, matchQuote = '\0'; | |||
| // local helper functions | |||
| bool IsOpenQuote(IReadOnlyDictionary<char, char> dict, char ch) | |||
| { | |||
| // return if the key is contained in the dictionary if it is populated | |||
| if (dict.Count != 0) | |||
| return dict.ContainsKey(ch); | |||
| // or otherwise if it is the default double quote | |||
| return c == '\"'; | |||
| } | |||
| char GetMatch(IReadOnlyDictionary<char, char> dict, char ch) | |||
| { | |||
| // get the corresponding value for the key, if it exists | |||
| // and if the dictionary is populated | |||
| if (dict.Count != 0 && dict.TryGetValue(c, out var value)) | |||
| return value; | |||
| // or get the default pair of the default double quote | |||
| return '\"'; | |||
| } | |||
| for (int curPos = startPos; curPos <= endPos; curPos++) | |||
| { | |||
| @@ -74,9 +94,11 @@ namespace Discord.Commands | |||
| argBuilder.Append(c); | |||
| continue; | |||
| } | |||
| if (c == '\"') | |||
| if (IsOpenQuote(aliasMap, c)) | |||
| { | |||
| curPart = ParserPart.QuotedParameter; | |||
| matchQuote = GetMatch(aliasMap, c); | |||
| continue; | |||
| } | |||
| curPart = ParserPart.Parameter; | |||
| @@ -97,7 +119,7 @@ namespace Discord.Commands | |||
| } | |||
| else if (curPart == ParserPart.QuotedParameter) | |||
| { | |||
| if (c == '\"') | |||
| if (c == matchQuote) | |||
| { | |||
| argString = argBuilder.ToString(); //Remove quotes | |||
| lastArgEndPos = curPos + 1; | |||
| @@ -1,4 +1,4 @@ | |||
| using System; | |||
| using System; | |||
| using System.Collections.Concurrent; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| @@ -32,6 +32,7 @@ namespace Discord.Commands | |||
| internal readonly RunMode _defaultRunMode; | |||
| internal readonly Logger _cmdLogger; | |||
| internal readonly LogManager _logManager; | |||
| internal readonly IReadOnlyDictionary<char, char> _quotationMarkAliasMap; | |||
| public IEnumerable<ModuleInfo> Modules => _moduleDefs.Select(x => x); | |||
| public IEnumerable<CommandInfo> Commands => _moduleDefs.SelectMany(x => x.Commands); | |||
| @@ -45,6 +46,7 @@ namespace Discord.Commands | |||
| _ignoreExtraArgs = config.IgnoreExtraArgs; | |||
| _separatorChar = config.SeparatorChar; | |||
| _defaultRunMode = config.DefaultRunMode; | |||
| _quotationMarkAliasMap = (config.QuotationMarkAliasMap ?? new Dictionary<char, char>()).ToImmutableDictionary(); | |||
| if (_defaultRunMode == RunMode.Default) | |||
| throw new InvalidOperationException("The default run mode cannot be set to Default."); | |||
| @@ -337,7 +339,6 @@ namespace Discord.Commands | |||
| public async Task<IResult> ExecuteAsync(ICommandContext context, string input, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) | |||
| { | |||
| services = services ?? EmptyServiceProvider.Instance; | |||
| var searchResult = Search(context, input); | |||
| if (!searchResult.IsSuccess) | |||
| return searchResult; | |||
| @@ -1,4 +1,5 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| namespace Discord.Commands | |||
| { | |||
| @@ -18,6 +19,10 @@ namespace Discord.Commands | |||
| /// <summary> Determines whether RunMode.Sync commands should push exceptions up to the caller. </summary> | |||
| public bool ThrowOnError { get; set; } = true; | |||
| /// <summary> Collection of aliases that can wrap strings for command parsing. | |||
| /// represents the opening quotation mark and the value is the corresponding closing mark.</summary> | |||
| public Dictionary<char, char> QuotationMarkAliasMap { get; set; } = QuotationAliasUtils.GetDefaultAliasMap; | |||
| /// <summary> Determines whether extra parameters should be ignored. </summary> | |||
| public bool IgnoreExtraArgs { get; set; } = false; | |||
| } | |||
| @@ -1,4 +1,4 @@ | |||
| using Discord.Commands.Builders; | |||
| using Discord.Commands.Builders; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| @@ -121,7 +121,8 @@ namespace Discord.Commands | |||
| return ParseResult.FromError(preconditionResult); | |||
| string input = searchResult.Text.Substring(startIndex); | |||
| return await CommandParser.ParseArgsAsync(this, context, services, input, 0).ConfigureAwait(false); | |||
| return await CommandParser.ParseArgsAsync(this, context, _commandService._ignoreExtraArgs, services, input, 0, _commandService._quotationMarkAliasMap).ConfigureAwait(false); | |||
| } | |||
| public Task<IResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) | |||
| @@ -0,0 +1,95 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Text; | |||
| using System.Globalization; | |||
| namespace Discord.Commands | |||
| { | |||
| /// <summary> | |||
| /// Utility methods for generating matching pairs of unicode quotation marks for CommandServiceConfig | |||
| /// </summary> | |||
| internal static class QuotationAliasUtils | |||
| { | |||
| /// <summary> | |||
| /// Generates an IEnumerable of characters representing open-close pairs of | |||
| /// quotation punctuation. | |||
| /// </summary> | |||
| internal static Dictionary<char, char> GetDefaultAliasMap | |||
| { | |||
| get | |||
| { | |||
| // Output of a gist provided by https://gist.github.com/ufcpp | |||
| // https://gist.github.com/ufcpp/5b2cf9a9bf7d0b8743714a0b88f7edc5 | |||
| // This was not used for the implementation because of incompatibility with netstandard1.1 | |||
| return new Dictionary<char, char> { | |||
| {'\"', '\"' }, | |||
| {'«', '»' }, | |||
| {'‘', '’' }, | |||
| {'“', '”' }, | |||
| {'„', '‟' }, | |||
| {'‹', '›' }, | |||
| {'‚', '‛' }, | |||
| {'《', '》' }, | |||
| {'〈', '〉' }, | |||
| {'「', '」' }, | |||
| {'『', '』' }, | |||
| {'〝', '〞' }, | |||
| {'﹁', '﹂' }, | |||
| {'﹃', '﹄' }, | |||
| {'"', '"' }, | |||
| {''', ''' }, | |||
| {'「', '」' }, | |||
| {'(', ')' }, | |||
| {'༺', '༻' }, | |||
| {'༼', '༽' }, | |||
| {'᚛', '᚜' }, | |||
| {'⁅', '⁆' }, | |||
| {'⌈', '⌉' }, | |||
| {'⌊', '⌋' }, | |||
| {'❨', '❩' }, | |||
| {'❪', '❫' }, | |||
| {'❬', '❭' }, | |||
| {'❮', '❯' }, | |||
| {'❰', '❱' }, | |||
| {'❲', '❳' }, | |||
| {'❴', '❵' }, | |||
| {'⟅', '⟆' }, | |||
| {'⟦', '⟧' }, | |||
| {'⟨', '⟩' }, | |||
| {'⟪', '⟫' }, | |||
| {'⟬', '⟭' }, | |||
| {'⟮', '⟯' }, | |||
| {'⦃', '⦄' }, | |||
| {'⦅', '⦆' }, | |||
| {'⦇', '⦈' }, | |||
| {'⦉', '⦊' }, | |||
| {'⦋', '⦌' }, | |||
| {'⦍', '⦎' }, | |||
| {'⦏', '⦐' }, | |||
| {'⦑', '⦒' }, | |||
| {'⦓', '⦔' }, | |||
| {'⦕', '⦖' }, | |||
| {'⦗', '⦘' }, | |||
| {'⧘', '⧙' }, | |||
| {'⧚', '⧛' }, | |||
| {'⧼', '⧽' }, | |||
| {'⸂', '⸃' }, | |||
| {'⸄', '⸅' }, | |||
| {'⸉', '⸊' }, | |||
| {'⸌', '⸍' }, | |||
| {'⸜', '⸝' }, | |||
| {'⸠', '⸡' }, | |||
| {'⸢', '⸣' }, | |||
| {'⸤', '⸥' }, | |||
| {'⸦', '⸧' }, | |||
| {'⸨', '⸩' }, | |||
| {'【', '】'}, | |||
| {'〔', '〕' }, | |||
| {'〖', '〗' }, | |||
| {'〘', '〙' }, | |||
| {'〚', '〛' } | |||
| }; | |||
| } | |||
| } | |||
| } | |||
| } | |||