From 650c55bbd16afd9dbe2f68ce30467287ffb13d1b Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Sat, 17 Jul 2021 23:53:13 +0300 Subject: [PATCH] Refactor --- .../Commands/Base/ExportCommandBase.cs | 10 +- .../DiscordChatExporter.Cli.csproj | 4 +- .../Filtering/BinaryExpressionKind.cs | 2 +- .../BinaryExpressionMessageFilter.cs | 10 +- .../Filtering/ContainsMessageFilter.cs | 17 +-- .../Exporting/Filtering/FromMessageFilter.cs | 8 +- .../Exporting/Filtering/HasMessageFilter.cs | 33 +++--- .../Filtering/MentionsMessageFilter.cs | 16 +-- .../Filtering/MessageContentMatchKind.cs | 12 ++ .../Exporting/Filtering/MessageFilter.cs | 28 +---- .../Filtering/NegatedMessageFilter.cs | 2 +- .../Exporting/Filtering/NullMessageFilter.cs | 4 +- .../Filtering/Parsing/FilterGrammar.cs | 106 ++++++++++++++++++ .../Filtering/Parsing/FilterParser.cs | 68 ----------- .../Filtering/Parsing/FilterToken.cs | 18 --- .../Filtering/Parsing/FilterTokenizer.cs | 23 ---- .../Partitioning/FileSizePartitionLimit.cs | 2 +- .../MessageCountPartitionLimit.cs | 2 +- .../Partitioning/NullPartitionLimit.cs | 4 +- .../Exporting/Partitioning/PartitionLimit.cs | 2 + .../MarkdownVisitors/HtmlMarkdownVisitor.cs | 10 +- .../PlainTextMarkdownVisitor.cs | 10 +- .../Markdown/Ast/MentionNode.cs | 17 --- .../Markdown/Ast/MentionType.cs | 10 -- .../Markdown/{Ast => }/EmojiNode.cs | 2 +- .../Markdown/{Ast => }/FormattedNode.cs | 2 +- .../Markdown/{Ast => }/InlineCodeBlockNode.cs | 2 +- .../Markdown/{Ast => }/LinkNode.cs | 2 +- .../Markdown/{Ast => }/MarkdownNode.cs | 2 +- .../Markdown/MentionKind.cs | 10 ++ .../Markdown/MentionNode.cs | 17 +++ .../{Ast => }/MultiLineCodeBlockNode.cs | 2 +- .../{Matching => Parsing}/AggregateMatcher.cs | 2 +- .../{Matching => Parsing}/IMatcher.cs | 2 +- .../Markdown/{ => Parsing}/MarkdownParser.cs | 26 +++-- .../Markdown/{ => Parsing}/MarkdownVisitor.cs | 3 +- .../{Matching => Parsing}/ParsedMatch.cs | 2 +- .../{Matching => Parsing}/RegexMatcher.cs | 2 +- .../{Matching => Parsing}/StringMatcher.cs | 2 +- .../{Matching => Parsing}/StringPart.cs | 2 +- .../Markdown/{Ast => }/TextFormatting.cs | 2 +- .../Markdown/{Ast => }/TextNode.cs | 2 +- .../Utils/Extensions/SuperpowerExtensions.cs | 24 ++++ DiscordChatExporter.Core/Utils/Http.cs | 10 +- .../Dialogs/ExportSetupViewModel.cs | 4 +- .../Views/Dialogs/ExportSetupView.xaml | 4 +- DiscordChatExporter.Gui/Views/RootView.xaml | 2 +- 47 files changed, 280 insertions(+), 266 deletions(-) create mode 100644 DiscordChatExporter.Core/Exporting/Filtering/MessageContentMatchKind.cs create mode 100644 DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterGrammar.cs delete mode 100644 DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterParser.cs delete mode 100644 DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterToken.cs delete mode 100644 DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterTokenizer.cs delete mode 100644 DiscordChatExporter.Core/Markdown/Ast/MentionNode.cs delete mode 100644 DiscordChatExporter.Core/Markdown/Ast/MentionType.cs rename DiscordChatExporter.Core/Markdown/{Ast => }/EmojiNode.cs (95%) rename DiscordChatExporter.Core/Markdown/{Ast => }/FormattedNode.cs (90%) rename DiscordChatExporter.Core/Markdown/{Ast => }/InlineCodeBlockNode.cs (83%) rename DiscordChatExporter.Core/Markdown/{Ast => }/LinkNode.cs (88%) rename DiscordChatExporter.Core/Markdown/{Ast => }/MarkdownNode.cs (51%) create mode 100644 DiscordChatExporter.Core/Markdown/MentionKind.cs create mode 100644 DiscordChatExporter.Core/Markdown/MentionNode.cs rename DiscordChatExporter.Core/Markdown/{Ast => }/MultiLineCodeBlockNode.cs (87%) rename DiscordChatExporter.Core/Markdown/{Matching => Parsing}/AggregateMatcher.cs (96%) rename DiscordChatExporter.Core/Markdown/{Matching => Parsing}/IMatcher.cs (97%) rename DiscordChatExporter.Core/Markdown/{ => Parsing}/MarkdownParser.cs (96%) rename DiscordChatExporter.Core/Markdown/{ => Parsing}/MarkdownVisitor.cs (94%) rename DiscordChatExporter.Core/Markdown/{Matching => Parsing}/ParsedMatch.cs (82%) rename DiscordChatExporter.Core/Markdown/{Matching => Parsing}/RegexMatcher.cs (96%) rename DiscordChatExporter.Core/Markdown/{Matching => Parsing}/StringMatcher.cs (95%) rename DiscordChatExporter.Core/Markdown/{Matching => Parsing}/StringPart.cs (94%) rename DiscordChatExporter.Core/Markdown/{Ast => }/TextFormatting.cs (74%) rename DiscordChatExporter.Core/Markdown/{Ast => }/TextNode.cs (81%) create mode 100644 DiscordChatExporter.Core/Utils/Extensions/SuperpowerExtensions.cs diff --git a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs index e99fc98..9237868 100644 --- a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs +++ b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs @@ -33,11 +33,11 @@ namespace DiscordChatExporter.Cli.Commands.Base [CommandOption("before", Description = "Only include messages sent before this date or message ID.")] public Snowflake? Before { get; init; } - [CommandOption("partition", 'p', Description = "Split output into partitions, each limited to this number of messages (e.g. 100) or file size (e.g. 10mb).")] - public PartitionLimit PartitionLimit { get; init; } = NullPartitionLimit.Instance; + [CommandOption("partition", 'p', Description = "Split output into partitions, each limited to this number of messages (e.g. '100') or file size (e.g. '10mb').")] + public PartitionLimit PartitionLimit { get; init; } = PartitionLimit.Null; - [CommandOption("filter", Description = "Only include messages that satisfy this filter (e.g. from:foo#1234).")] - public MessageFilter MessageFilter { get; init; } = NullMessageFilter.Instance; + [CommandOption("filter", Description = "Only include messages that satisfy this filter (e.g. 'from:foo#1234' or 'has:image').")] + public MessageFilter MessageFilter { get; init; } = MessageFilter.Null; [CommandOption("parallel", Description = "Limits how many channels can be exported in parallel.")] public int ParallelLimit { get; init; } = 1; @@ -133,8 +133,6 @@ namespace DiscordChatExporter.Cli.Commands.Base { throw new CommandException("Export failed."); } - - await console.Output.WriteLineAsync("Done."); } public override ValueTask ExecuteAsync(IConsole console) diff --git a/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj b/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj index 587735b..3a85ea1 100644 --- a/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj +++ b/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/DiscordChatExporter.Core/Exporting/Filtering/BinaryExpressionKind.cs b/DiscordChatExporter.Core/Exporting/Filtering/BinaryExpressionKind.cs index 962ee06..16bd5d7 100644 --- a/DiscordChatExporter.Core/Exporting/Filtering/BinaryExpressionKind.cs +++ b/DiscordChatExporter.Core/Exporting/Filtering/BinaryExpressionKind.cs @@ -1,6 +1,6 @@ namespace DiscordChatExporter.Core.Exporting.Filtering { - public enum BinaryExpressionKind + internal enum BinaryExpressionKind { Or, And diff --git a/DiscordChatExporter.Core/Exporting/Filtering/BinaryExpressionMessageFilter.cs b/DiscordChatExporter.Core/Exporting/Filtering/BinaryExpressionMessageFilter.cs index 1946726..6a187cd 100644 --- a/DiscordChatExporter.Core/Exporting/Filtering/BinaryExpressionMessageFilter.cs +++ b/DiscordChatExporter.Core/Exporting/Filtering/BinaryExpressionMessageFilter.cs @@ -1,9 +1,9 @@ -using DiscordChatExporter.Core.Discord.Data; -using System; +using System; +using DiscordChatExporter.Core.Discord.Data; namespace DiscordChatExporter.Core.Exporting.Filtering { - public class BinaryExpressionMessageFilter : MessageFilter + internal class BinaryExpressionMessageFilter : MessageFilter { private readonly MessageFilter _first; private readonly MessageFilter _second; @@ -19,8 +19,8 @@ namespace DiscordChatExporter.Core.Exporting.Filtering public override bool Filter(Message message) => _kind switch { BinaryExpressionKind.Or => _first.Filter(message) || _second.Filter(message), - BinaryExpressionKind.And => _first.Filter(message) && _second.Filter(message), + BinaryExpressionKind.And => _first.Filter(message) && _second.Filter(message), _ => throw new InvalidOperationException($"Unknown binary expression kind '{_kind}'.") }; } -} +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Exporting/Filtering/ContainsMessageFilter.cs b/DiscordChatExporter.Core/Exporting/Filtering/ContainsMessageFilter.cs index 9b59460..d956d60 100644 --- a/DiscordChatExporter.Core/Exporting/Filtering/ContainsMessageFilter.cs +++ b/DiscordChatExporter.Core/Exporting/Filtering/ContainsMessageFilter.cs @@ -1,15 +1,18 @@ -using DiscordChatExporter.Core.Discord.Data; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; +using DiscordChatExporter.Core.Discord.Data; namespace DiscordChatExporter.Core.Exporting.Filtering { - public class ContainsMessageFilter : MessageFilter + internal class ContainsMessageFilter : MessageFilter { - private readonly string _value; + private readonly string _text; - public ContainsMessageFilter(string value) => _value = value; + public ContainsMessageFilter(string text) => _text = text; - public override bool Filter(Message message) => - Regex.IsMatch(message.Content, $@"\b{Regex.Escape(_value)}\b", RegexOptions.IgnoreCase | DefaultRegexOptions); + public override bool Filter(Message message) => Regex.IsMatch( + message.Content, + "\\b" + _text + "\\b", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant + ); } } \ No newline at end of file diff --git a/DiscordChatExporter.Core/Exporting/Filtering/FromMessageFilter.cs b/DiscordChatExporter.Core/Exporting/Filtering/FromMessageFilter.cs index fb2f863..f6da2c7 100644 --- a/DiscordChatExporter.Core/Exporting/Filtering/FromMessageFilter.cs +++ b/DiscordChatExporter.Core/Exporting/Filtering/FromMessageFilter.cs @@ -1,9 +1,9 @@ -using DiscordChatExporter.Core.Discord.Data; -using System; +using System; +using DiscordChatExporter.Core.Discord.Data; namespace DiscordChatExporter.Core.Exporting.Filtering { - public class FromMessageFilter : MessageFilter + internal class FromMessageFilter : MessageFilter { private readonly string _value; @@ -14,4 +14,4 @@ namespace DiscordChatExporter.Core.Exporting.Filtering string.Equals(_value, message.Author.FullName, StringComparison.OrdinalIgnoreCase) || string.Equals(_value, message.Author.Id.ToString(), StringComparison.OrdinalIgnoreCase); } -} +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Exporting/Filtering/HasMessageFilter.cs b/DiscordChatExporter.Core/Exporting/Filtering/HasMessageFilter.cs index 53c8fca..4ef94f5 100644 --- a/DiscordChatExporter.Core/Exporting/Filtering/HasMessageFilter.cs +++ b/DiscordChatExporter.Core/Exporting/Filtering/HasMessageFilter.cs @@ -1,26 +1,25 @@ -using DiscordChatExporter.Core.Discord.Data; -using System; +using System; using System.Linq; using System.Text.RegularExpressions; +using DiscordChatExporter.Core.Discord.Data; namespace DiscordChatExporter.Core.Exporting.Filtering { - public class HasMessageFilter : MessageFilter + internal class HasMessageFilter : MessageFilter { - private readonly string _value; + private readonly MessageContentMatchKind _kind; - public HasMessageFilter(string value) => _value = value; + public HasMessageFilter(MessageContentMatchKind kind) => _kind = kind; - public override bool Filter(Message message) => - _value switch - { - "link" => Regex.IsMatch(message.Content, "https?://\\S*[^\\.,:;\"\'\\s]", DefaultRegexOptions), - "embed" => message.Embeds.Any(), - "file" => message.Attachments.Any(), - "video" => message.Attachments.Any(file => file.IsVideo), - "image" => message.Attachments.Any(file => file.IsImage), - "sound" => message.Attachments.Any(file => file.IsAudio), - _ => throw new InvalidOperationException($"Invalid value provided for the 'has' message filter: '{_value}'") - }; + public override bool Filter(Message message) => _kind switch + { + MessageContentMatchKind.Link => Regex.IsMatch(message.Content, "https?://\\S*[^\\.,:;\"\'\\s]"), + MessageContentMatchKind.Embed => message.Embeds.Any(), + MessageContentMatchKind.File => message.Attachments.Any(), + MessageContentMatchKind.Video => message.Attachments.Any(file => file.IsVideo), + MessageContentMatchKind.Image => message.Attachments.Any(file => file.IsImage), + MessageContentMatchKind.Sound => message.Attachments.Any(file => file.IsAudio), + _ => throw new InvalidOperationException($"Unknown message content match kind '{_kind}'.") + }; } -} +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Exporting/Filtering/MentionsMessageFilter.cs b/DiscordChatExporter.Core/Exporting/Filtering/MentionsMessageFilter.cs index 9bc2179..f96bc88 100644 --- a/DiscordChatExporter.Core/Exporting/Filtering/MentionsMessageFilter.cs +++ b/DiscordChatExporter.Core/Exporting/Filtering/MentionsMessageFilter.cs @@ -1,19 +1,19 @@ -using DiscordChatExporter.Core.Discord.Data; -using System; +using System; using System.Linq; +using DiscordChatExporter.Core.Discord.Data; namespace DiscordChatExporter.Core.Exporting.Filtering { - public class MentionsMessageFilter : MessageFilter + internal class MentionsMessageFilter : MessageFilter { private readonly string _value; public MentionsMessageFilter(string value) => _value = value; - public override bool Filter(Message message) => - message.MentionedUsers.Any(user => - string.Equals(_value, user.Name, StringComparison.OrdinalIgnoreCase) || - string.Equals(_value, user.FullName, StringComparison.OrdinalIgnoreCase) || - string.Equals(_value, user.Id.ToString(), StringComparison.OrdinalIgnoreCase)); + public override bool Filter(Message message) => message.MentionedUsers.Any(user => + string.Equals(_value, user.Name, StringComparison.OrdinalIgnoreCase) || + string.Equals(_value, user.FullName, StringComparison.OrdinalIgnoreCase) || + string.Equals(_value, user.Id.ToString(), StringComparison.OrdinalIgnoreCase) + ); } } \ No newline at end of file diff --git a/DiscordChatExporter.Core/Exporting/Filtering/MessageContentMatchKind.cs b/DiscordChatExporter.Core/Exporting/Filtering/MessageContentMatchKind.cs new file mode 100644 index 0000000..549a38f --- /dev/null +++ b/DiscordChatExporter.Core/Exporting/Filtering/MessageContentMatchKind.cs @@ -0,0 +1,12 @@ +namespace DiscordChatExporter.Core.Exporting.Filtering +{ + internal enum MessageContentMatchKind + { + Link, + Embed, + File, + Video, + Image, + Sound + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Exporting/Filtering/MessageFilter.cs b/DiscordChatExporter.Core/Exporting/Filtering/MessageFilter.cs index 6593f05..a7bbb09 100644 --- a/DiscordChatExporter.Core/Exporting/Filtering/MessageFilter.cs +++ b/DiscordChatExporter.Core/Exporting/Filtering/MessageFilter.cs @@ -1,6 +1,4 @@ -using System; -using System.Text.RegularExpressions; -using DiscordChatExporter.Core.Discord.Data; +using DiscordChatExporter.Core.Discord.Data; using DiscordChatExporter.Core.Exporting.Filtering.Parsing; using Superpower; @@ -13,26 +11,8 @@ namespace DiscordChatExporter.Core.Exporting.Filtering public partial class MessageFilter { - protected const RegexOptions DefaultRegexOptions = RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Multiline; + public static MessageFilter Null { get; } = new NullMessageFilter(); - internal static MessageFilter CreateFilter(string text) => new ContainsMessageFilter(text); - - internal static MessageFilter CreateFilter(string key, string value) - { - return key.ToLowerInvariant() switch - { - "from" => new FromMessageFilter(value), - "has" => new HasMessageFilter(value), - "mentions" => new MentionsMessageFilter(value), - _ => throw new ArgumentException($"Invalid filter type '{key}'.", nameof(key)) - }; - } - - public static MessageFilter Parse(string value, IFormatProvider? formatProvider = null) - { - var tokens = FilterTokenizer.Instance.Tokenize(value); - var parsed = FilterParser.Instance.Parse(tokens); - return parsed; - } + public static MessageFilter Parse(string value) => FilterGrammar.Filter.Parse(value); } -} +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Exporting/Filtering/NegatedMessageFilter.cs b/DiscordChatExporter.Core/Exporting/Filtering/NegatedMessageFilter.cs index e2a554c..2c144d0 100644 --- a/DiscordChatExporter.Core/Exporting/Filtering/NegatedMessageFilter.cs +++ b/DiscordChatExporter.Core/Exporting/Filtering/NegatedMessageFilter.cs @@ -2,7 +2,7 @@ namespace DiscordChatExporter.Core.Exporting.Filtering { - public class NegatedMessageFilter : MessageFilter + internal class NegatedMessageFilter : MessageFilter { private readonly MessageFilter _filter; diff --git a/DiscordChatExporter.Core/Exporting/Filtering/NullMessageFilter.cs b/DiscordChatExporter.Core/Exporting/Filtering/NullMessageFilter.cs index d213d19..d68b6b6 100644 --- a/DiscordChatExporter.Core/Exporting/Filtering/NullMessageFilter.cs +++ b/DiscordChatExporter.Core/Exporting/Filtering/NullMessageFilter.cs @@ -2,10 +2,8 @@ namespace DiscordChatExporter.Core.Exporting.Filtering { - public class NullMessageFilter : MessageFilter + internal class NullMessageFilter : MessageFilter { - public static NullMessageFilter Instance { get; } = new(); - public override bool Filter(Message message) => true; } } \ No newline at end of file diff --git a/DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterGrammar.cs b/DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterGrammar.cs new file mode 100644 index 0000000..d1d186b --- /dev/null +++ b/DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterGrammar.cs @@ -0,0 +1,106 @@ +using System.Linq; +using DiscordChatExporter.Core.Utils.Extensions; +using Superpower; +using Superpower.Parsers; + +namespace DiscordChatExporter.Core.Exporting.Filtering.Parsing +{ + internal static class FilterGrammar + { + // Choice(a, b) looks cleaner than a.Or(b) + private static TextParser Choice(params TextParser[] parsers) => + parsers.Aggregate((current, next) => current.Or(next)); + + private static readonly TextParser EscapedCharacter = + Character.EqualTo('\\').IgnoreThen(Character.AnyChar); + + private static readonly TextParser QuotedString = + from open in Character.In('"', '\'') + from value in Choice(EscapedCharacter, Character.Except(open)).Many().Text() + from close in Character.EqualTo(open) + select value; + + private static readonly TextParser FreeCharacter = + Character.Matching(c => + !char.IsWhiteSpace(c) && + // Avoid all special tokens used by the grammar + c is not ('(' or ')' or '"' or '\'' or '-' or '|' or '&'), + "any character except whitespace or `(`, `)`, `\"`, `'`, `-`, `|`, `&`" + ); + + private static readonly TextParser UnquotedString = + Choice(EscapedCharacter, FreeCharacter).AtLeastOnce().Text(); + + private static readonly TextParser String = + Choice(QuotedString, UnquotedString).Named("text string"); + + private static readonly TextParser ContainsFilter = + 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 MentionsFilter = Span + .EqualToIgnoreCase("mentions:") + .IgnoreThen(String) + .Select(v => (MessageFilter) new MentionsMessageFilter(v)) + .Named("mentions:"); + + private static readonly TextParser HasFilter = Span + .EqualToIgnoreCase("has:") + .IgnoreThen(Choice( + Span.EqualToIgnoreCase("link").IgnoreThen(Parse.Return(MessageContentMatchKind.Link)), + Span.EqualToIgnoreCase("embed").IgnoreThen(Parse.Return(MessageContentMatchKind.Embed)), + Span.EqualToIgnoreCase("video").IgnoreThen(Parse.Return(MessageContentMatchKind.Video)), + Span.EqualToIgnoreCase("image").IgnoreThen(Parse.Return(MessageContentMatchKind.Image)), + Span.EqualToIgnoreCase("sound").IgnoreThen(Parse.Return(MessageContentMatchKind.Sound)) + )) + .Select(k => (MessageFilter) new HasMessageFilter(k)) + .Named("has:"); + + private static readonly TextParser NegatedFilter = Character + .EqualTo('-') + .IgnoreThen(Parse.Ref(() => StandaloneFilter)) + .Select(f => (MessageFilter) new NegatedMessageFilter(f)); + + private static readonly TextParser GroupedFilter = + from open in Character.EqualTo('(') + from content in Parse.Ref(() => BinaryExpressionFilter).Token() + from close in Character.EqualTo(')') + select content; + + private static readonly TextParser StandaloneFilter = Choice( + GroupedFilter, + FromFilter, + MentionsFilter, + HasFilter, + ContainsFilter + ); + + private static readonly TextParser UnaryExpressionFilter = Choice( + NegatedFilter, + StandaloneFilter + ); + + private static readonly TextParser BinaryExpressionFilter = Parse.Chain( + Choice( + // Explicit operator + Character.In('|', '&').Token().Try(), + // Implicit operator (resolves to 'and') + Character.WhiteSpace.AtLeastOnce().IgnoreThen(Parse.Return(' ')) + ), + UnaryExpressionFilter, + (op, left, right) => op switch + { + '|' => new BinaryExpressionMessageFilter(left, right, BinaryExpressionKind.Or), + _ => new BinaryExpressionMessageFilter(left, right, BinaryExpressionKind.And) + } + ); + + public static readonly TextParser Filter = + BinaryExpressionFilter.Token().AtEnd(); + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterParser.cs b/DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterParser.cs deleted file mode 100644 index 79b5785..0000000 --- a/DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterParser.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Superpower; -using Superpower.Model; -using Superpower.Parsers; -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; - -namespace DiscordChatExporter.Core.Exporting.Filtering.Parsing -{ - public static class FilterParser - { - public static TextParser QuotedString { get; } = - from open in Character.EqualTo('"') - from content in Character.EqualTo('\\').IgnoreThen(Character.AnyChar).Try() - .Or(Character.Except('"')) - .Many() - from close in Character.EqualTo('"') - select new string(content); - - public static TextParser UnquotedString { get; } = - from content in Character.EqualTo('\\').IgnoreThen(Character.In('"', '/')).Try() - .Or(Character.Except(c => char.IsWhiteSpace(c) || "():-|\"".Contains(c), "non-whitespace character except for (, ), :, -, |, and \"")) - .AtLeastOnce() - select new string(content); - - public static TokenListParser AnyString { get; } = - Token.EqualTo(FilterToken.QuotedString).Apply(QuotedString) - .Or(Token.EqualTo(FilterToken.UnquotedString).Apply(UnquotedString)); - - public static TokenListParser AnyFilter { get; } = - from minus in Token.EqualTo(FilterToken.Minus).Optional() - from content in KeyValueFilter.Or(TextFilter).Or(GroupedFilter) - select minus.HasValue ? new NegatedMessageFilter(content) : content; - - public static TokenListParser TextFilter { get; } = - from value in AnyString - select MessageFilter.CreateFilter(value); - - public static TokenListParser KeyValueFilter { get; } = - from key in AnyString.Try() - from colon in Token.EqualTo(FilterToken.Colon).Try() - from value in AnyString - select MessageFilter.CreateFilter(key, value); - - public static TokenListParser GroupedFilter { get; } = - from open in Token.EqualTo(FilterToken.LParen) - from content in BinaryExpression - from close in Token.EqualTo(FilterToken.RParen) - select content; - - public static TokenListParser OrBinaryExpression { get; } = - from first in AnyFilter - from vbar in Token.EqualTo(FilterToken.VBar) - from rest in BinaryExpression - select (MessageFilter)new BinaryExpressionMessageFilter(first, rest, BinaryExpressionKind.Or); - - public static TokenListParser AndBinaryExpression { get; } = - from first in AnyFilter - from rest in BinaryExpression - select (MessageFilter)new BinaryExpressionMessageFilter(first, rest, BinaryExpressionKind.And); - - public static TokenListParser BinaryExpression { get; } = OrBinaryExpression.Try().Or(AndBinaryExpression.Try()).Or(AnyFilter); - - public static TokenListParser Instance { get; } = BinaryExpression.AtEnd(); - } -} diff --git a/DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterToken.cs b/DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterToken.cs deleted file mode 100644 index 222525a..0000000 --- a/DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterToken.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace DiscordChatExporter.Core.Exporting.Filtering.Parsing -{ - public enum FilterToken - { - None, - LParen, - RParen, - Colon, - Minus, - VBar, - UnquotedString, - QuotedString - } -} diff --git a/DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterTokenizer.cs b/DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterTokenizer.cs deleted file mode 100644 index 11bd3e3..0000000 --- a/DiscordChatExporter.Core/Exporting/Filtering/Parsing/FilterTokenizer.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Superpower; -using Superpower.Parsers; -using Superpower.Tokenizers; -using System; -using System.Collections.Generic; -using System.Text; - -namespace DiscordChatExporter.Core.Exporting.Filtering.Parsing -{ - public static class FilterTokenizer - { - public static Tokenizer Instance { get; } = new TokenizerBuilder() - .Ignore(Span.WhiteSpace) - .Match(Character.EqualTo('('), FilterToken.LParen) - .Match(Character.EqualTo(')'), FilterToken.RParen) - .Match(Character.EqualTo(':'), FilterToken.Colon) - .Match(Character.EqualTo('-'), FilterToken.Minus) - .Match(Character.EqualTo('|'), FilterToken.VBar) - .Match(FilterParser.QuotedString, FilterToken.QuotedString) - .Match(FilterParser.UnquotedString, FilterToken.UnquotedString) - .Build(); - } -} diff --git a/DiscordChatExporter.Core/Exporting/Partitioning/FileSizePartitionLimit.cs b/DiscordChatExporter.Core/Exporting/Partitioning/FileSizePartitionLimit.cs index 1baffbb..a2e6bac 100644 --- a/DiscordChatExporter.Core/Exporting/Partitioning/FileSizePartitionLimit.cs +++ b/DiscordChatExporter.Core/Exporting/Partitioning/FileSizePartitionLimit.cs @@ -1,6 +1,6 @@ namespace DiscordChatExporter.Core.Exporting.Partitioning { - public class FileSizePartitionLimit : PartitionLimit + internal class FileSizePartitionLimit : PartitionLimit { private readonly long _limit; diff --git a/DiscordChatExporter.Core/Exporting/Partitioning/MessageCountPartitionLimit.cs b/DiscordChatExporter.Core/Exporting/Partitioning/MessageCountPartitionLimit.cs index 318ffc6..83f3379 100644 --- a/DiscordChatExporter.Core/Exporting/Partitioning/MessageCountPartitionLimit.cs +++ b/DiscordChatExporter.Core/Exporting/Partitioning/MessageCountPartitionLimit.cs @@ -1,6 +1,6 @@ namespace DiscordChatExporter.Core.Exporting.Partitioning { - public class MessageCountPartitionLimit : PartitionLimit + internal class MessageCountPartitionLimit : PartitionLimit { private readonly long _limit; diff --git a/DiscordChatExporter.Core/Exporting/Partitioning/NullPartitionLimit.cs b/DiscordChatExporter.Core/Exporting/Partitioning/NullPartitionLimit.cs index d38546f..4017a37 100644 --- a/DiscordChatExporter.Core/Exporting/Partitioning/NullPartitionLimit.cs +++ b/DiscordChatExporter.Core/Exporting/Partitioning/NullPartitionLimit.cs @@ -1,9 +1,7 @@ namespace DiscordChatExporter.Core.Exporting.Partitioning { - public class NullPartitionLimit : PartitionLimit + internal class NullPartitionLimit : PartitionLimit { - public static NullPartitionLimit Instance { get; } = new(); - public override bool IsReached(long messagesWritten, long bytesWritten) => false; } } \ No newline at end of file diff --git a/DiscordChatExporter.Core/Exporting/Partitioning/PartitionLimit.cs b/DiscordChatExporter.Core/Exporting/Partitioning/PartitionLimit.cs index 1a82259..f1336d6 100644 --- a/DiscordChatExporter.Core/Exporting/Partitioning/PartitionLimit.cs +++ b/DiscordChatExporter.Core/Exporting/Partitioning/PartitionLimit.cs @@ -11,6 +11,8 @@ namespace DiscordChatExporter.Core.Exporting.Partitioning public partial class PartitionLimit { + public static PartitionLimit Null { get; } = new NullPartitionLimit(); + private static long? TryParseFileSizeBytes(string value, IFormatProvider? formatProvider = null) { var match = Regex.Match(value, @"^\s*(\d+[\.,]?\d*)\s*(\w)?b\s*$", RegexOptions.IgnoreCase); diff --git a/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/HtmlMarkdownVisitor.cs b/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/HtmlMarkdownVisitor.cs index f6e1776..4b565d4 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/HtmlMarkdownVisitor.cs +++ b/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/HtmlMarkdownVisitor.cs @@ -6,7 +6,7 @@ using System.Text.RegularExpressions; using DiscordChatExporter.Core.Discord; using DiscordChatExporter.Core.Discord.Data; using DiscordChatExporter.Core.Markdown; -using DiscordChatExporter.Core.Markdown.Ast; +using DiscordChatExporter.Core.Markdown.Parsing; using DiscordChatExporter.Core.Utils.Extensions; namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors @@ -78,14 +78,14 @@ namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors protected override MarkdownNode VisitMention(MentionNode mention) { var mentionId = Snowflake.TryParse(mention.Id); - if (mention.Type == MentionType.Meta) + if (mention.Kind == MentionKind.Meta) { _buffer .Append("") .Append("@").Append(HtmlEncode(mention.Id)) .Append(""); } - else if (mention.Type == MentionType.User) + else if (mention.Kind == MentionKind.User) { var member = mentionId?.Pipe(_context.TryGetMember); var fullName = member?.User.FullName ?? "Unknown"; @@ -96,7 +96,7 @@ namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors .Append("@").Append(HtmlEncode(nick)) .Append(""); } - else if (mention.Type == MentionType.Channel) + else if (mention.Kind == MentionKind.Channel) { var channel = mentionId?.Pipe(_context.TryGetChannel); var name = channel?.Name ?? "deleted-channel"; @@ -106,7 +106,7 @@ namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors .Append("#").Append(HtmlEncode(name)) .Append(""); } - else if (mention.Type == MentionType.Role) + else if (mention.Kind == MentionKind.Role) { var role = mentionId?.Pipe(_context.TryGetRole); var name = role?.Name ?? "deleted-role"; diff --git a/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/PlainTextMarkdownVisitor.cs b/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/PlainTextMarkdownVisitor.cs index 5b9bcd2..f5aa61e 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/PlainTextMarkdownVisitor.cs +++ b/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/PlainTextMarkdownVisitor.cs @@ -1,7 +1,7 @@ using System.Text; using DiscordChatExporter.Core.Discord; using DiscordChatExporter.Core.Markdown; -using DiscordChatExporter.Core.Markdown.Ast; +using DiscordChatExporter.Core.Markdown.Parsing; using DiscordChatExporter.Core.Utils.Extensions; namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors @@ -26,25 +26,25 @@ namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors protected override MarkdownNode VisitMention(MentionNode mention) { var mentionId = Snowflake.TryParse(mention.Id); - if (mention.Type == MentionType.Meta) + if (mention.Kind == MentionKind.Meta) { _buffer.Append($"@{mention.Id}"); } - else if (mention.Type == MentionType.User) + else if (mention.Kind == MentionKind.User) { var member = mentionId?.Pipe(_context.TryGetMember); var name = member?.User.Name ?? "Unknown"; _buffer.Append($"@{name}"); } - else if (mention.Type == MentionType.Channel) + else if (mention.Kind == MentionKind.Channel) { var channel = mentionId?.Pipe(_context.TryGetChannel); var name = channel?.Name ?? "deleted-channel"; _buffer.Append($"#{name}"); } - else if (mention.Type == MentionType.Role) + else if (mention.Kind == MentionKind.Role) { var role = mentionId?.Pipe(_context.TryGetRole); var name = role?.Name ?? "deleted-role"; diff --git a/DiscordChatExporter.Core/Markdown/Ast/MentionNode.cs b/DiscordChatExporter.Core/Markdown/Ast/MentionNode.cs deleted file mode 100644 index 900198f..0000000 --- a/DiscordChatExporter.Core/Markdown/Ast/MentionNode.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace DiscordChatExporter.Core.Markdown.Ast -{ - internal class MentionNode : MarkdownNode - { - public string Id { get; } - - public MentionType Type { get; } - - public MentionNode(string id, MentionType type) - { - Id = id; - Type = type; - } - - public override string ToString() => $"<{Type} mention> {Id}"; - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Markdown/Ast/MentionType.cs b/DiscordChatExporter.Core/Markdown/Ast/MentionType.cs deleted file mode 100644 index ef3a5f0..0000000 --- a/DiscordChatExporter.Core/Markdown/Ast/MentionType.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace DiscordChatExporter.Core.Markdown.Ast -{ - internal enum MentionType - { - Meta, - User, - Channel, - Role - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Markdown/Ast/EmojiNode.cs b/DiscordChatExporter.Core/Markdown/EmojiNode.cs similarity index 95% rename from DiscordChatExporter.Core/Markdown/Ast/EmojiNode.cs rename to DiscordChatExporter.Core/Markdown/EmojiNode.cs index a36a9c8..9e4aebc 100644 --- a/DiscordChatExporter.Core/Markdown/Ast/EmojiNode.cs +++ b/DiscordChatExporter.Core/Markdown/EmojiNode.cs @@ -1,6 +1,6 @@ using DiscordChatExporter.Core.Utils; -namespace DiscordChatExporter.Core.Markdown.Ast +namespace DiscordChatExporter.Core.Markdown { internal class EmojiNode : MarkdownNode { diff --git a/DiscordChatExporter.Core/Markdown/Ast/FormattedNode.cs b/DiscordChatExporter.Core/Markdown/FormattedNode.cs similarity index 90% rename from DiscordChatExporter.Core/Markdown/Ast/FormattedNode.cs rename to DiscordChatExporter.Core/Markdown/FormattedNode.cs index 0a78a07..c30cad3 100644 --- a/DiscordChatExporter.Core/Markdown/Ast/FormattedNode.cs +++ b/DiscordChatExporter.Core/Markdown/FormattedNode.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace DiscordChatExporter.Core.Markdown.Ast +namespace DiscordChatExporter.Core.Markdown { internal class FormattedNode : MarkdownNode { diff --git a/DiscordChatExporter.Core/Markdown/Ast/InlineCodeBlockNode.cs b/DiscordChatExporter.Core/Markdown/InlineCodeBlockNode.cs similarity index 83% rename from DiscordChatExporter.Core/Markdown/Ast/InlineCodeBlockNode.cs rename to DiscordChatExporter.Core/Markdown/InlineCodeBlockNode.cs index ebea177..dbb7637 100644 --- a/DiscordChatExporter.Core/Markdown/Ast/InlineCodeBlockNode.cs +++ b/DiscordChatExporter.Core/Markdown/InlineCodeBlockNode.cs @@ -1,4 +1,4 @@ -namespace DiscordChatExporter.Core.Markdown.Ast +namespace DiscordChatExporter.Core.Markdown { internal class InlineCodeBlockNode : MarkdownNode { diff --git a/DiscordChatExporter.Core/Markdown/Ast/LinkNode.cs b/DiscordChatExporter.Core/Markdown/LinkNode.cs similarity index 88% rename from DiscordChatExporter.Core/Markdown/Ast/LinkNode.cs rename to DiscordChatExporter.Core/Markdown/LinkNode.cs index 2c9f481..1b364d1 100644 --- a/DiscordChatExporter.Core/Markdown/Ast/LinkNode.cs +++ b/DiscordChatExporter.Core/Markdown/LinkNode.cs @@ -1,4 +1,4 @@ -namespace DiscordChatExporter.Core.Markdown.Ast +namespace DiscordChatExporter.Core.Markdown { internal class LinkNode : MarkdownNode { diff --git a/DiscordChatExporter.Core/Markdown/Ast/MarkdownNode.cs b/DiscordChatExporter.Core/Markdown/MarkdownNode.cs similarity index 51% rename from DiscordChatExporter.Core/Markdown/Ast/MarkdownNode.cs rename to DiscordChatExporter.Core/Markdown/MarkdownNode.cs index 6bf3baf..b6ca6a2 100644 --- a/DiscordChatExporter.Core/Markdown/Ast/MarkdownNode.cs +++ b/DiscordChatExporter.Core/Markdown/MarkdownNode.cs @@ -1,4 +1,4 @@ -namespace DiscordChatExporter.Core.Markdown.Ast +namespace DiscordChatExporter.Core.Markdown { internal abstract class MarkdownNode { diff --git a/DiscordChatExporter.Core/Markdown/MentionKind.cs b/DiscordChatExporter.Core/Markdown/MentionKind.cs new file mode 100644 index 0000000..824a1cd --- /dev/null +++ b/DiscordChatExporter.Core/Markdown/MentionKind.cs @@ -0,0 +1,10 @@ +namespace DiscordChatExporter.Core.Markdown +{ + internal enum MentionKind + { + Meta, + User, + Channel, + Role + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Markdown/MentionNode.cs b/DiscordChatExporter.Core/Markdown/MentionNode.cs new file mode 100644 index 0000000..646d1c8 --- /dev/null +++ b/DiscordChatExporter.Core/Markdown/MentionNode.cs @@ -0,0 +1,17 @@ +namespace DiscordChatExporter.Core.Markdown +{ + internal class MentionNode : MarkdownNode + { + public string Id { get; } + + public MentionKind Kind { get; } + + public MentionNode(string id, MentionKind kind) + { + Id = id; + Kind = kind; + } + + public override string ToString() => $"<{Kind} mention> {Id}"; + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Markdown/Ast/MultiLineCodeBlockNode.cs b/DiscordChatExporter.Core/Markdown/MultiLineCodeBlockNode.cs similarity index 87% rename from DiscordChatExporter.Core/Markdown/Ast/MultiLineCodeBlockNode.cs rename to DiscordChatExporter.Core/Markdown/MultiLineCodeBlockNode.cs index d53406d..afa3e0d 100644 --- a/DiscordChatExporter.Core/Markdown/Ast/MultiLineCodeBlockNode.cs +++ b/DiscordChatExporter.Core/Markdown/MultiLineCodeBlockNode.cs @@ -1,4 +1,4 @@ -namespace DiscordChatExporter.Core.Markdown.Ast +namespace DiscordChatExporter.Core.Markdown { internal class MultiLineCodeBlockNode : MarkdownNode { diff --git a/DiscordChatExporter.Core/Markdown/Matching/AggregateMatcher.cs b/DiscordChatExporter.Core/Markdown/Parsing/AggregateMatcher.cs similarity index 96% rename from DiscordChatExporter.Core/Markdown/Matching/AggregateMatcher.cs rename to DiscordChatExporter.Core/Markdown/Parsing/AggregateMatcher.cs index a507e0d..07b30bc 100644 --- a/DiscordChatExporter.Core/Markdown/Matching/AggregateMatcher.cs +++ b/DiscordChatExporter.Core/Markdown/Parsing/AggregateMatcher.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace DiscordChatExporter.Core.Markdown.Matching +namespace DiscordChatExporter.Core.Markdown.Parsing { internal class AggregateMatcher : IMatcher { diff --git a/DiscordChatExporter.Core/Markdown/Matching/IMatcher.cs b/DiscordChatExporter.Core/Markdown/Parsing/IMatcher.cs similarity index 97% rename from DiscordChatExporter.Core/Markdown/Matching/IMatcher.cs rename to DiscordChatExporter.Core/Markdown/Parsing/IMatcher.cs index 0958c18..0e3d1ec 100644 --- a/DiscordChatExporter.Core/Markdown/Matching/IMatcher.cs +++ b/DiscordChatExporter.Core/Markdown/Parsing/IMatcher.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace DiscordChatExporter.Core.Markdown.Matching +namespace DiscordChatExporter.Core.Markdown.Parsing { internal interface IMatcher { diff --git a/DiscordChatExporter.Core/Markdown/MarkdownParser.cs b/DiscordChatExporter.Core/Markdown/Parsing/MarkdownParser.cs similarity index 96% rename from DiscordChatExporter.Core/Markdown/MarkdownParser.cs rename to DiscordChatExporter.Core/Markdown/Parsing/MarkdownParser.cs index 05f9f57..ca8cd4d 100644 --- a/DiscordChatExporter.Core/Markdown/MarkdownParser.cs +++ b/DiscordChatExporter.Core/Markdown/Parsing/MarkdownParser.cs @@ -1,11 +1,9 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; -using DiscordChatExporter.Core.Markdown.Ast; -using DiscordChatExporter.Core.Markdown.Matching; using DiscordChatExporter.Core.Utils; -namespace DiscordChatExporter.Core.Markdown +namespace DiscordChatExporter.Core.Markdown.Parsing { // The following parsing logic is meant to replicate Discord's markdown grammar as close as possible internal static partial class MarkdownParser @@ -120,31 +118,31 @@ namespace DiscordChatExporter.Core.Markdown // Capture @everyone private static readonly IMatcher EveryoneMentionNodeMatcher = new StringMatcher( "@everyone", - _ => new MentionNode("everyone", MentionType.Meta) + _ => new MentionNode("everyone", MentionKind.Meta) ); // Capture @here private static readonly IMatcher HereMentionNodeMatcher = new StringMatcher( "@here", - _ => new MentionNode("here", MentionType.Meta) + _ => new MentionNode("here", MentionKind.Meta) ); // Capture <@123456> or <@!123456> private static readonly IMatcher UserMentionNodeMatcher = new RegexMatcher( new Regex("<@!?(\\d+)>", DefaultRegexOptions), - (_, m) => new MentionNode(m.Groups[1].Value, MentionType.User) + (_, m) => new MentionNode(m.Groups[1].Value, MentionKind.User) ); // Capture <#123456> private static readonly IMatcher ChannelMentionNodeMatcher = new RegexMatcher( new Regex("<#(\\d+)>", DefaultRegexOptions), - (_, m) => new MentionNode(m.Groups[1].Value, MentionType.Channel) + (_, m) => new MentionNode(m.Groups[1].Value, MentionKind.Channel) ); // Capture <@&123456> private static readonly IMatcher RoleMentionNodeMatcher = new RegexMatcher( new Regex("<@&(\\d+)>", DefaultRegexOptions), - (_, m) => new MentionNode(m.Groups[1].Value, MentionType.Role) + (_, m) => new MentionNode(m.Groups[1].Value, MentionKind.Role) ); /* Emojis */ @@ -293,12 +291,16 @@ namespace DiscordChatExporter.Core.Markdown internal static partial class MarkdownParser { - private static IReadOnlyList Parse(StringPart stringPart) => Parse(stringPart, AggregateNodeMatcher); + private static IReadOnlyList Parse(StringPart stringPart) => + Parse(stringPart, AggregateNodeMatcher); - private static IReadOnlyList ParseMinimal(StringPart stringPart) => Parse(stringPart, MinimalAggregateNodeMatcher); + private static IReadOnlyList ParseMinimal(StringPart stringPart) => + Parse(stringPart, MinimalAggregateNodeMatcher); - public static IReadOnlyList Parse(string input) => Parse(new StringPart(input)); + public static IReadOnlyList Parse(string input) => + Parse(new StringPart(input)); - public static IReadOnlyList ParseMinimal(string input) => ParseMinimal(new StringPart(input)); + public static IReadOnlyList ParseMinimal(string input) => + ParseMinimal(new StringPart(input)); } } \ No newline at end of file diff --git a/DiscordChatExporter.Core/Markdown/MarkdownVisitor.cs b/DiscordChatExporter.Core/Markdown/Parsing/MarkdownVisitor.cs similarity index 94% rename from DiscordChatExporter.Core/Markdown/MarkdownVisitor.cs rename to DiscordChatExporter.Core/Markdown/Parsing/MarkdownVisitor.cs index 56100ff..ad953ab 100644 --- a/DiscordChatExporter.Core/Markdown/MarkdownVisitor.cs +++ b/DiscordChatExporter.Core/Markdown/Parsing/MarkdownVisitor.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; -using DiscordChatExporter.Core.Markdown.Ast; -namespace DiscordChatExporter.Core.Markdown +namespace DiscordChatExporter.Core.Markdown.Parsing { internal abstract class MarkdownVisitor { diff --git a/DiscordChatExporter.Core/Markdown/Matching/ParsedMatch.cs b/DiscordChatExporter.Core/Markdown/Parsing/ParsedMatch.cs similarity index 82% rename from DiscordChatExporter.Core/Markdown/Matching/ParsedMatch.cs rename to DiscordChatExporter.Core/Markdown/Parsing/ParsedMatch.cs index a144e03..0713335 100644 --- a/DiscordChatExporter.Core/Markdown/Matching/ParsedMatch.cs +++ b/DiscordChatExporter.Core/Markdown/Parsing/ParsedMatch.cs @@ -1,4 +1,4 @@ -namespace DiscordChatExporter.Core.Markdown.Matching +namespace DiscordChatExporter.Core.Markdown.Parsing { internal class ParsedMatch { diff --git a/DiscordChatExporter.Core/Markdown/Matching/RegexMatcher.cs b/DiscordChatExporter.Core/Markdown/Parsing/RegexMatcher.cs similarity index 96% rename from DiscordChatExporter.Core/Markdown/Matching/RegexMatcher.cs rename to DiscordChatExporter.Core/Markdown/Parsing/RegexMatcher.cs index a141113..7a9ffb9 100644 --- a/DiscordChatExporter.Core/Markdown/Matching/RegexMatcher.cs +++ b/DiscordChatExporter.Core/Markdown/Parsing/RegexMatcher.cs @@ -1,7 +1,7 @@ using System; using System.Text.RegularExpressions; -namespace DiscordChatExporter.Core.Markdown.Matching +namespace DiscordChatExporter.Core.Markdown.Parsing { internal class RegexMatcher : IMatcher { diff --git a/DiscordChatExporter.Core/Markdown/Matching/StringMatcher.cs b/DiscordChatExporter.Core/Markdown/Parsing/StringMatcher.cs similarity index 95% rename from DiscordChatExporter.Core/Markdown/Matching/StringMatcher.cs rename to DiscordChatExporter.Core/Markdown/Parsing/StringMatcher.cs index 842ae9d..380819b 100644 --- a/DiscordChatExporter.Core/Markdown/Matching/StringMatcher.cs +++ b/DiscordChatExporter.Core/Markdown/Parsing/StringMatcher.cs @@ -1,6 +1,6 @@ using System; -namespace DiscordChatExporter.Core.Markdown.Matching +namespace DiscordChatExporter.Core.Markdown.Parsing { internal class StringMatcher : IMatcher { diff --git a/DiscordChatExporter.Core/Markdown/Matching/StringPart.cs b/DiscordChatExporter.Core/Markdown/Parsing/StringPart.cs similarity index 94% rename from DiscordChatExporter.Core/Markdown/Matching/StringPart.cs rename to DiscordChatExporter.Core/Markdown/Parsing/StringPart.cs index c38c7a4..487d591 100644 --- a/DiscordChatExporter.Core/Markdown/Matching/StringPart.cs +++ b/DiscordChatExporter.Core/Markdown/Parsing/StringPart.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; -namespace DiscordChatExporter.Core.Markdown.Matching +namespace DiscordChatExporter.Core.Markdown.Parsing { internal readonly struct StringPart { diff --git a/DiscordChatExporter.Core/Markdown/Ast/TextFormatting.cs b/DiscordChatExporter.Core/Markdown/TextFormatting.cs similarity index 74% rename from DiscordChatExporter.Core/Markdown/Ast/TextFormatting.cs rename to DiscordChatExporter.Core/Markdown/TextFormatting.cs index b272277..f6f30b7 100644 --- a/DiscordChatExporter.Core/Markdown/Ast/TextFormatting.cs +++ b/DiscordChatExporter.Core/Markdown/TextFormatting.cs @@ -1,4 +1,4 @@ -namespace DiscordChatExporter.Core.Markdown.Ast +namespace DiscordChatExporter.Core.Markdown { internal enum TextFormatting { diff --git a/DiscordChatExporter.Core/Markdown/Ast/TextNode.cs b/DiscordChatExporter.Core/Markdown/TextNode.cs similarity index 81% rename from DiscordChatExporter.Core/Markdown/Ast/TextNode.cs rename to DiscordChatExporter.Core/Markdown/TextNode.cs index 66bad0e..95e7a71 100644 --- a/DiscordChatExporter.Core/Markdown/Ast/TextNode.cs +++ b/DiscordChatExporter.Core/Markdown/TextNode.cs @@ -1,4 +1,4 @@ -namespace DiscordChatExporter.Core.Markdown.Ast +namespace DiscordChatExporter.Core.Markdown { internal class TextNode : MarkdownNode { diff --git a/DiscordChatExporter.Core/Utils/Extensions/SuperpowerExtensions.cs b/DiscordChatExporter.Core/Utils/Extensions/SuperpowerExtensions.cs new file mode 100644 index 0000000..a70296f --- /dev/null +++ b/DiscordChatExporter.Core/Utils/Extensions/SuperpowerExtensions.cs @@ -0,0 +1,24 @@ +using System; +using Superpower; +using Superpower.Parsers; + +namespace DiscordChatExporter.Core.Utils.Extensions +{ + public static class SuperpowerExtensions + { + public static TextParser Text(this TextParser parser) => + parser.Select(chars => new string(chars)); + + public static TextParser Token(this TextParser parser) => + parser.Between(Character.WhiteSpace.IgnoreMany(), Character.WhiteSpace.IgnoreMany()); + + // From: https://twitter.com/nblumhardt/status/1389349059786264578 + public static TextParser Log(this TextParser parser, string description) => i => + { + Console.WriteLine($"Trying {description} ->"); + var r = parser(i); + Console.WriteLine($"Result was {r}"); + return r; + }; + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Utils/Http.cs b/DiscordChatExporter.Core/Utils/Http.cs index dd4e2a1..fec0cbf 100644 --- a/DiscordChatExporter.Core/Utils/Http.cs +++ b/DiscordChatExporter.Core/Utils/Http.cs @@ -21,14 +21,15 @@ namespace DiscordChatExporter.Core.Utils .OrResult(m => m.StatusCode == HttpStatusCode.TooManyRequests) .OrResult(m => m.StatusCode == HttpStatusCode.RequestTimeout) .OrResult(m => m.StatusCode >= HttpStatusCode.InternalServerError) - .WaitAndRetryAsync(8, + .WaitAndRetryAsync( + 8, (i, result, _) => { // If rate-limited, use retry-after as a guide if (result.Result?.StatusCode == HttpStatusCode.TooManyRequests) { - // Only start respecting retry-after after a few attempts. - // The reason is that Discord often sends unreasonable (20+ minutes) retry-after + // Only start respecting retry-after after a few attempts, because + // Discord often sends unreasonable (20+ minutes) retry-after // on the very first request. if (i > 3) { @@ -40,7 +41,8 @@ namespace DiscordChatExporter.Core.Utils return TimeSpan.FromSeconds(Math.Pow(2, i) + 1); }, - (_, _, _, _) => Task.CompletedTask); + (_, _, _, _) => Task.CompletedTask + ); private static HttpStatusCode? TryGetStatusCodeFromException(HttpRequestException ex) => // This is extremely frail, but there's no other way diff --git a/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs b/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs index a1d5cc5..9c6a2df 100644 --- a/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs @@ -52,13 +52,13 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs public PartitionLimit PartitionLimit => !string.IsNullOrWhiteSpace(PartitionLimitValue) ? PartitionLimit.Parse(PartitionLimitValue) - : NullPartitionLimit.Instance; + : PartitionLimit.Null; public string? MessageFilterValue { get; set; } public MessageFilter MessageFilter => !string.IsNullOrWhiteSpace(MessageFilterValue) ? MessageFilter.Parse(MessageFilterValue) - : NullMessageFilter.Instance; + : MessageFilter.Null; public bool ShouldDownloadMedia { get; set; } diff --git a/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml b/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml index 6a0d7eb..98f4415 100644 --- a/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml +++ b/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml @@ -131,7 +131,7 @@ materialDesign:HintAssist.Hint="Partition limit" materialDesign:HintAssist.IsFloating="True" Text="{Binding PartitionLimitValue}" - ToolTip="Split output into partitions, each limited to this number of messages (e.g. 100) or file size (e.g. 10mb)" /> + ToolTip="Split output into partitions, each limited to this number of messages (e.g. '100') or file size (e.g. '10mb')" /> + ToolTip="Only include messages that satisfy this filter (e.g. 'from:foo#1234' or 'has:image')." /> diff --git a/DiscordChatExporter.Gui/Views/RootView.xaml b/DiscordChatExporter.Gui/Views/RootView.xaml index d5660d4..c26f5ba 100644 --- a/DiscordChatExporter.Gui/Views/RootView.xaml +++ b/DiscordChatExporter.Gui/Views/RootView.xaml @@ -1,4 +1,4 @@ -