diff --git a/DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterGrammar.cs b/DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterGrammar.cs index c02ad734..f7b3a312 100644 --- a/DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterGrammar.cs +++ b/DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterGrammar.cs @@ -18,60 +18,61 @@ internal static class FilterGrammar private static readonly TextParser UnquotedString = Parse.OneOf( EscapedCharacter, - Character.Matching( - c => - !char.IsWhiteSpace(c) && - // Avoid all special tokens used by the grammar - c is not ('(' or ')' or '"' or '\'' or '-' or '~' or '|' or '&'), - "any character except whitespace or `(`, `)`, `\"`, `'`, `-`, `|`, `&`" - ) + // Avoid whitespace as it's treated as an implicit 'and' operator. + // Also avoid all special tokens used by other parsers. + Character.ExceptIn(' ', '(', ')', '"', '\'', '-', '~', '|', '&') ).AtLeastOnce().Text(); private static readonly TextParser String = Parse.OneOf(QuotedString, UnquotedString).Named("text string"); private static readonly TextParser ContainsFilter = - String.Select(v => (MessageFilter) new ContainsMessageFilter(v)); + String.Select(v => (MessageFilter)new ContainsMessageFilter(v)); - private static readonly TextParser FromFilter = Span - .EqualToIgnoreCase("from:") - .IgnoreThen(String) - .Select(v => (MessageFilter) new FromMessageFilter(v)) - .Named("from:"); + private static readonly TextParser FromFilter = + Span + .EqualToIgnoreCase("from:") + .IgnoreThen(String) + .Select(v => (MessageFilter)new FromMessageFilter(v)) + .Named("from:"); - private static readonly TextParser MentionsFilter = Span - .EqualToIgnoreCase("mentions:") - .IgnoreThen(String) - .Select(v => (MessageFilter) new MentionsMessageFilter(v)) - .Named("mentions:"); + private static readonly TextParser MentionsFilter = + Span + .EqualToIgnoreCase("mentions:") + .IgnoreThen(String) + .Select(v => (MessageFilter)new MentionsMessageFilter(v)) + .Named("mentions:"); - private static readonly TextParser ReactionFilter = Span - .EqualToIgnoreCase("reaction:") - .IgnoreThen(String) - .Select(v => (MessageFilter) new ReactionMessageFilter(v)) - .Named("reaction:"); + private static readonly TextParser ReactionFilter = + Span + .EqualToIgnoreCase("reaction:") + .IgnoreThen(String) + .Select(v => (MessageFilter)new ReactionMessageFilter(v)) + .Named("reaction:"); - private static readonly TextParser HasFilter = Span - .EqualToIgnoreCase("has:") - .IgnoreThen(Parse.OneOf( - Span.EqualToIgnoreCase("link").IgnoreThen(Parse.Return(MessageContentMatchKind.Link)), - Span.EqualToIgnoreCase("embed").IgnoreThen(Parse.Return(MessageContentMatchKind.Embed)), - Span.EqualToIgnoreCase("file").IgnoreThen(Parse.Return(MessageContentMatchKind.File)), - Span.EqualToIgnoreCase("video").IgnoreThen(Parse.Return(MessageContentMatchKind.Video)), - Span.EqualToIgnoreCase("image").IgnoreThen(Parse.Return(MessageContentMatchKind.Image)), - Span.EqualToIgnoreCase("sound").IgnoreThen(Parse.Return(MessageContentMatchKind.Sound)), - Span.EqualToIgnoreCase("pin").IgnoreThen(Parse.Return(MessageContentMatchKind.Pin)) - )) - .Select(k => (MessageFilter) new HasMessageFilter(k)) - .Named("has:"); + private static readonly TextParser HasFilter = + Span + .EqualToIgnoreCase("has:") + .IgnoreThen(Parse.OneOf( + Span.EqualToIgnoreCase("link").IgnoreThen(Parse.Return(MessageContentMatchKind.Link)), + Span.EqualToIgnoreCase("embed").IgnoreThen(Parse.Return(MessageContentMatchKind.Embed)), + Span.EqualToIgnoreCase("file").IgnoreThen(Parse.Return(MessageContentMatchKind.File)), + Span.EqualToIgnoreCase("video").IgnoreThen(Parse.Return(MessageContentMatchKind.Video)), + Span.EqualToIgnoreCase("image").IgnoreThen(Parse.Return(MessageContentMatchKind.Image)), + Span.EqualToIgnoreCase("sound").IgnoreThen(Parse.Return(MessageContentMatchKind.Sound)), + Span.EqualToIgnoreCase("pin").IgnoreThen(Parse.Return(MessageContentMatchKind.Pin)) + )) + .Select(k => (MessageFilter)new HasMessageFilter(k)) + .Named("has:"); - private static readonly TextParser PrimitiveFilter = Parse.OneOf( - FromFilter, - MentionsFilter, - ReactionFilter, - HasFilter, - ContainsFilter - ); + private static readonly TextParser PrimitiveFilter = + Parse.OneOf( + FromFilter, + MentionsFilter, + ReactionFilter, + HasFilter, + ContainsFilter + ); private static readonly TextParser GroupedFilter = from open in Character.EqualTo('(') @@ -79,33 +80,35 @@ internal static class FilterGrammar from close in Character.EqualTo(')') select content; - private static readonly TextParser NegatedFilter = Character - // Dash is annoying to use from CLI due to conflicts with options, so we provide tilde as an alias - .In('-', '~') - .IgnoreThen(Parse.OneOf(GroupedFilter, PrimitiveFilter)) - .Select(f => (MessageFilter) new NegatedMessageFilter(f)); + private static readonly TextParser NegatedFilter = + Character + // Dash is annoying to use from CLI due to conflicts with options, so we provide tilde as an alias + .In('-', '~') + .IgnoreThen(Parse.OneOf(GroupedFilter, PrimitiveFilter)) + .Select(f => (MessageFilter)new NegatedMessageFilter(f)); - private static readonly TextParser ChainedFilter = Parse.Chain( - // Operator - Parse.OneOf( - // Explicit operator - Character.In('|', '&').Token().Try(), - // Implicit operator (resolves to 'and') - Character.WhiteSpace.AtLeastOnce().IgnoreThen(Parse.Return(' ')) - ), - // Operand - Parse.OneOf( - NegatedFilter, - GroupedFilter, - PrimitiveFilter - ), - // Reducer - (op, left, right) => op switch - { - '|' => new BinaryExpressionMessageFilter(left, right, BinaryExpressionKind.Or), - _ => new BinaryExpressionMessageFilter(left, right, BinaryExpressionKind.And) - } - ); + private static readonly TextParser ChainedFilter = + Parse.Chain( + // Operator + Parse.OneOf( + // Explicit operator + Character.In('|', '&').Token().Try(), + // Implicit operator (resolves to 'and') + Character.EqualTo(' ').AtLeastOnce().IgnoreThen(Parse.Return(' ')) + ), + // Operand + Parse.OneOf( + NegatedFilter, + GroupedFilter, + PrimitiveFilter + ), + // Reducer + (op, left, right) => op switch + { + '|' => new BinaryExpressionMessageFilter(left, right, BinaryExpressionKind.Or), + _ => new BinaryExpressionMessageFilter(left, right, BinaryExpressionKind.And) + } + ); public static readonly TextParser Filter = ChainedFilter.Token().AtEnd();