Use C#9 features

pull/453/head
Tyrrrz 4 years ago
parent 9dda9cfc27
commit 63803f98aa

@ -44,7 +44,7 @@ namespace DiscordChatExporter.Cli.Commands.Base
Description = "Format used when writing dates.")] Description = "Format used when writing dates.")]
public string DateFormat { get; set; } = "dd-MMM-yy hh:mm tt"; public string DateFormat { get; set; } = "dd-MMM-yy hh:mm tt";
protected ChannelExporter GetChannelExporter() => new ChannelExporter(GetDiscordClient()); protected ChannelExporter GetChannelExporter() => new(GetDiscordClient());
protected async ValueTask ExportAsync(IConsole console, Guild guild, Channel channel) protected async ValueTask ExportAsync(IConsole console, Guild guild, Channel channel)
{ {

@ -17,14 +17,14 @@ namespace DiscordChatExporter.Cli.Commands.Base
Description = "Authorize as a bot.")] Description = "Authorize as a bot.")]
public bool IsBotToken { get; set; } public bool IsBotToken { get; set; }
protected AuthToken GetAuthToken() => new AuthToken( protected AuthToken GetAuthToken() => new(
IsBotToken IsBotToken
? AuthTokenType.Bot ? AuthTokenType.Bot
: AuthTokenType.User, : AuthTokenType.User,
TokenValue TokenValue
); );
protected DiscordClient GetDiscordClient() => new DiscordClient(GetAuthToken()); protected DiscordClient GetDiscordClient() => new(GetAuthToken());
public abstract ValueTask ExecuteAsync(IConsole console); public abstract ValueTask ExecuteAsync(IConsole console);
} }

@ -19,7 +19,7 @@ namespace DiscordChatExporter.Domain.Discord
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private readonly AuthToken _token; private readonly AuthToken _token;
private readonly Uri _baseUri = new Uri("https://discord.com/api/v6/", UriKind.Absolute); private readonly Uri _baseUri = new("https://discord.com/api/v6/", UriKind.Absolute);
public DiscordClient(HttpClient httpClient, AuthToken token) public DiscordClient(HttpClient httpClient, AuthToken token)
{ {

@ -47,17 +47,14 @@ namespace DiscordChatExporter.Domain.Discord.Models
public partial class Attachment public partial class Attachment
{ {
private static readonly HashSet<string> ImageFileExtensions = private static readonly HashSet<string> ImageFileExtensions = new(StringComparer.OrdinalIgnoreCase)
new HashSet<string>(StringComparer.OrdinalIgnoreCase) {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"};
{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"};
private static readonly HashSet<string> VideoFileExtensions = private static readonly HashSet<string> VideoFileExtensions = new(StringComparer.OrdinalIgnoreCase)
new HashSet<string>(StringComparer.OrdinalIgnoreCase) {".mp4", ".webm"};
{".mp4", ".webm"};
private static readonly HashSet<string> AudioFileExtensions = private static readonly HashSet<string> AudioFileExtensions = new(StringComparer.OrdinalIgnoreCase)
new HashSet<string>(StringComparer.OrdinalIgnoreCase) {".mp3", ".wav", ".ogg", ".flac", ".m4a"};
{".mp3", ".wav", ".ogg", ".flac", ".m4a"};
public static Attachment Parse(JsonElement json) public static Attachment Parse(JsonElement json)
{ {

@ -60,6 +60,6 @@ namespace DiscordChatExporter.Domain.Discord.Models.Common
public partial struct FileSize public partial struct FileSize
{ {
public static FileSize FromBytes(long bytes) => new FileSize(bytes); public static FileSize FromBytes(long bytes) => new(bytes);
} }
} }

@ -12,6 +12,6 @@ namespace DiscordChatExporter.Domain.Discord.Models.Common
public partial class IdBasedEqualityComparer public partial class IdBasedEqualityComparer
{ {
public static IdBasedEqualityComparer Instance { get; } = new IdBasedEqualityComparer(); public static IdBasedEqualityComparer Instance { get; } = new();
} }
} }

@ -24,8 +24,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
public partial class Guild public partial class Guild
{ {
public static Guild DirectMessages { get; } = public static Guild DirectMessages { get; } = new("@me", "Direct Messages", GetDefaultIconUrl());
new Guild("@me", "Direct Messages", GetDefaultIconUrl());
private static string GetDefaultIconUrl() => private static string GetDefaultIconUrl() =>
"https://cdn.discordapp.com/embed/avatars/0.png"; "https://cdn.discordapp.com/embed/avatars/0.png";

@ -31,8 +31,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
public partial class Member public partial class Member
{ {
public static Member CreateForUser(User user) => public static Member CreateForUser(User user) => new(user, null, Array.Empty<string>());
new Member(user, null, Array.Empty<string>());
public static Member Parse(JsonElement json) public static Member Parse(JsonElement json)
{ {

@ -18,8 +18,7 @@ namespace DiscordChatExporter.Domain.Exporting
private readonly bool _reuseMedia; private readonly bool _reuseMedia;
// URL -> Local file path // URL -> Local file path
private readonly Dictionary<string, string> _pathCache = private readonly Dictionary<string, string> _pathCache = new(StringComparer.Ordinal);
new Dictionary<string, string>(StringComparer.Ordinal);
public MediaDownloader(HttpClient httpClient, string workingDirPath, bool reuseMedia) public MediaDownloader(HttpClient httpClient, string workingDirPath, bool reuseMedia)
{ {

@ -12,7 +12,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
private readonly TextWriter _writer; private readonly TextWriter _writer;
private readonly string _themeName; private readonly string _themeName;
private readonly List<Message> _messageGroupBuffer = new List<Message>(); private readonly List<Message> _messageGroupBuffer = new();
private long _messageCount; private long _messageCount;

@ -11,7 +11,7 @@ namespace DiscordChatExporter.Domain.Internal
{ {
internal static class Http internal static class Http
{ {
public static HttpClient Client { get; } = new HttpClient(); public static HttpClient Client { get; } = new();
public static IAsyncPolicy<HttpResponseMessage> ResponsePolicy { get; } = public static IAsyncPolicy<HttpResponseMessage> ResponsePolicy { get; } =
Policy Policy
@ -21,7 +21,7 @@ namespace DiscordChatExporter.Domain.Internal
.OrResult(m => m.StatusCode == HttpStatusCode.RequestTimeout) .OrResult(m => m.StatusCode == HttpStatusCode.RequestTimeout)
.OrResult(m => m.StatusCode >= HttpStatusCode.InternalServerError) .OrResult(m => m.StatusCode >= HttpStatusCode.InternalServerError)
.WaitAndRetryAsync(8, .WaitAndRetryAsync(8,
(i, result, ctx) => (i, result, _) =>
{ {
// If rate-limited, use retry-after as a guide // If rate-limited, use retry-after as a guide
if (result.Result?.StatusCode == HttpStatusCode.TooManyRequests) if (result.Result?.StatusCode == HttpStatusCode.TooManyRequests)
@ -39,7 +39,7 @@ namespace DiscordChatExporter.Domain.Internal
return TimeSpan.FromSeconds(Math.Pow(2, i) + 1); return TimeSpan.FromSeconds(Math.Pow(2, i) + 1);
}, },
(response, timespan, retryCount, context) => Task.CompletedTask); (_, _, _, _) => Task.CompletedTask);
private static HttpStatusCode? TryGetStatusCodeFromException(HttpRequestException ex) private static HttpStatusCode? TryGetStatusCodeFromException(HttpRequestException ex)
{ {

@ -10,8 +10,7 @@ namespace DiscordChatExporter.Domain.Internal
{ {
private string _path = ""; private string _path = "";
private readonly Dictionary<string, string?> _queryParameters = private readonly Dictionary<string, string?> _queryParameters = new(StringComparer.OrdinalIgnoreCase);
new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
public UrlBuilder SetPath(string path) public UrlBuilder SetPath(string path)
{ {

@ -9,74 +9,92 @@ namespace DiscordChatExporter.Domain.Markdown
// The following parsing logic is meant to replicate Discord's markdown grammar as close as possible // The following parsing logic is meant to replicate Discord's markdown grammar as close as possible
internal static partial class MarkdownParser internal static partial class MarkdownParser
{ {
private const RegexOptions DefaultRegexOptions = RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Multiline; private const RegexOptions DefaultRegexOptions =
RegexOptions.Compiled |
RegexOptions.CultureInvariant |
RegexOptions.Multiline;
/* Formatting */ /* Formatting */
// Capture any character until the earliest double asterisk not followed by an asterisk // Capture any character until the earliest double asterisk not followed by an asterisk
private static readonly IMatcher<MarkdownNode> BoldFormattedNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> BoldFormattedNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("\\*\\*(.+?)\\*\\*(?!\\*)", DefaultRegexOptions | RegexOptions.Singleline), new Regex("\\*\\*(.+?)\\*\\*(?!\\*)", DefaultRegexOptions | RegexOptions.Singleline),
(p, m) => new FormattedNode(TextFormatting.Bold, Parse(p.Slice(m.Groups[1])))); (p, m) => new FormattedNode(TextFormatting.Bold, Parse(p.Slice(m.Groups[1])))
);
// Capture any character until the earliest single asterisk not preceded or followed by an asterisk // Capture any character until the earliest single asterisk not preceded or followed by an asterisk
// Opening asterisk must not be followed by whitespace // Opening asterisk must not be followed by whitespace
// Closing asterisk must not be preceded by whitespace // Closing asterisk must not be preceded by whitespace
private static readonly IMatcher<MarkdownNode> ItalicFormattedNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> ItalicFormattedNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("\\*(?!\\s)(.+?)(?<!\\s|\\*)\\*(?!\\*)", DefaultRegexOptions | RegexOptions.Singleline), new Regex("\\*(?!\\s)(.+?)(?<!\\s|\\*)\\*(?!\\*)", DefaultRegexOptions | RegexOptions.Singleline),
(p, m) => new FormattedNode(TextFormatting.Italic, Parse(p.Slice(m.Groups[1])))); (p, m) => new FormattedNode(TextFormatting.Italic, Parse(p.Slice(m.Groups[1])))
);
// Capture any character until the earliest triple asterisk not followed by an asterisk // Capture any character until the earliest triple asterisk not followed by an asterisk
private static readonly IMatcher<MarkdownNode> ItalicBoldFormattedNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> ItalicBoldFormattedNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("\\*(\\*\\*.+?\\*\\*)\\*(?!\\*)", DefaultRegexOptions | RegexOptions.Singleline), new Regex("\\*(\\*\\*.+?\\*\\*)\\*(?!\\*)", DefaultRegexOptions | RegexOptions.Singleline),
(p, m) => new FormattedNode(TextFormatting.Italic, Parse(p.Slice(m.Groups[1]), BoldFormattedNodeMatcher))); (p, m) => new FormattedNode(TextFormatting.Italic, Parse(p.Slice(m.Groups[1]), BoldFormattedNodeMatcher))
);
// Capture any character except underscore until an underscore // Capture any character except underscore until an underscore
// Closing underscore must not be followed by a word character // Closing underscore must not be followed by a word character
private static readonly IMatcher<MarkdownNode> ItalicAltFormattedNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> ItalicAltFormattedNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("_([^_]+)_(?!\\w)", DefaultRegexOptions | RegexOptions.Singleline), new Regex("_([^_]+)_(?!\\w)", DefaultRegexOptions | RegexOptions.Singleline),
(p, m) => new FormattedNode(TextFormatting.Italic, Parse(p.Slice(m.Groups[1])))); (p, m) => new FormattedNode(TextFormatting.Italic, Parse(p.Slice(m.Groups[1])))
);
// Capture any character until the earliest double underscore not followed by an underscore // Capture any character until the earliest double underscore not followed by an underscore
private static readonly IMatcher<MarkdownNode> UnderlineFormattedNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> UnderlineFormattedNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("__(.+?)__(?!_)", DefaultRegexOptions | RegexOptions.Singleline), new Regex("__(.+?)__(?!_)", DefaultRegexOptions | RegexOptions.Singleline),
(p, m) => new FormattedNode(TextFormatting.Underline, Parse(p.Slice(m.Groups[1])))); (p, m) => new FormattedNode(TextFormatting.Underline, Parse(p.Slice(m.Groups[1])))
);
// Capture any character until the earliest triple underscore not followed by an underscore // Capture any character until the earliest triple underscore not followed by an underscore
private static readonly IMatcher<MarkdownNode> ItalicUnderlineFormattedNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> ItalicUnderlineFormattedNodeMatcher =
new Regex("_(__.+?__)_(?!_)", DefaultRegexOptions | RegexOptions.Singleline), new RegexMatcher<MarkdownNode>(
(p, m) => new FormattedNode(TextFormatting.Italic, Parse(p.Slice(m.Groups[1]), UnderlineFormattedNodeMatcher))); new Regex("_(__.+?__)_(?!_)", DefaultRegexOptions | RegexOptions.Singleline),
(p, m) => new FormattedNode(TextFormatting.Italic,
Parse(p.Slice(m.Groups[1]), UnderlineFormattedNodeMatcher))
);
// Capture any character until the earliest double tilde // Capture any character until the earliest double tilde
private static readonly IMatcher<MarkdownNode> StrikethroughFormattedNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> StrikethroughFormattedNodeMatcher =
new Regex("~~(.+?)~~", DefaultRegexOptions | RegexOptions.Singleline), new RegexMatcher<MarkdownNode>(
(p, m) => new FormattedNode(TextFormatting.Strikethrough, Parse(p.Slice(m.Groups[1])))); new Regex("~~(.+?)~~", DefaultRegexOptions | RegexOptions.Singleline),
(p, m) => new FormattedNode(TextFormatting.Strikethrough, Parse(p.Slice(m.Groups[1])))
);
// Capture any character until the earliest double pipe // Capture any character until the earliest double pipe
private static readonly IMatcher<MarkdownNode> SpoilerFormattedNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> SpoilerFormattedNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("\\|\\|(.+?)\\|\\|", DefaultRegexOptions | RegexOptions.Singleline), new Regex("\\|\\|(.+?)\\|\\|", DefaultRegexOptions | RegexOptions.Singleline),
(p, m) => new FormattedNode(TextFormatting.Spoiler, Parse(p.Slice(m.Groups[1])))); (p, m) => new FormattedNode(TextFormatting.Spoiler, Parse(p.Slice(m.Groups[1])))
);
// Capture any character until the end of the line // Capture any character until the end of the line
// Opening 'greater than' character must be followed by whitespace // Opening 'greater than' character must be followed by whitespace
private static readonly IMatcher<MarkdownNode> SingleLineQuoteNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> SingleLineQuoteNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("^>\\s(.+\n?)", DefaultRegexOptions), new Regex("^>\\s(.+\n?)", DefaultRegexOptions),
(p, m) => new FormattedNode(TextFormatting.Quote, Parse(p.Slice(m.Groups[1])))); (p, m) => new FormattedNode(TextFormatting.Quote, Parse(p.Slice(m.Groups[1])))
);
// Repeatedly capture any character until the end of the line // Repeatedly capture any character until the end of the line
// This one is tricky as it ends up producing multiple separate captures which need to be joined // This one is tricky as it ends up producing multiple separate captures which need to be joined
private static readonly IMatcher<MarkdownNode> RepeatedSingleLineQuoteNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> RepeatedSingleLineQuoteNodeMatcher =
new Regex("(?:^>\\s(.+\n?)){2,}", DefaultRegexOptions), new RegexMatcher<MarkdownNode>(
(p, m) => new Regex("(?:^>\\s(.+\n?)){2,}", DefaultRegexOptions),
{ (_, m) =>
var content = string.Concat(m.Groups[1].Captures.Select(c => c.Value)); {
return new FormattedNode(TextFormatting.Quote, Parse(content)); var content = string.Concat(m.Groups[1].Captures.Select(c => c.Value));
}); return new FormattedNode(TextFormatting.Quote, Parse(content));
}
);
// Capture any character until the end of the input // Capture any character until the end of the input
// Opening 'greater than' characters must be followed by whitespace // Opening 'greater than' characters must be followed by whitespace
private static readonly IMatcher<MarkdownNode> MultiLineQuoteNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> MultiLineQuoteNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("^>>>\\s(.+)", DefaultRegexOptions | RegexOptions.Singleline), new Regex("^>>>\\s(.+)", DefaultRegexOptions | RegexOptions.Singleline),
(p, m) => new FormattedNode(TextFormatting.Quote, Parse(p.Slice(m.Groups[1])))); (p, m) => new FormattedNode(TextFormatting.Quote, Parse(p.Slice(m.Groups[1])))
);
/* Code blocks */ /* Code blocks */
@ -85,41 +103,48 @@ namespace DiscordChatExporter.Domain.Markdown
// There can be either one or two backticks, but equal number on both sides // There can be either one or two backticks, but equal number on both sides
private static readonly IMatcher<MarkdownNode> InlineCodeBlockNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> InlineCodeBlockNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("(`{1,2})([^`]+)\\1", DefaultRegexOptions | RegexOptions.Singleline), new Regex("(`{1,2})([^`]+)\\1", DefaultRegexOptions | RegexOptions.Singleline),
m => new InlineCodeBlockNode(m.Groups[2].Value.Trim('\r', '\n'))); m => new InlineCodeBlockNode(m.Groups[2].Value.Trim('\r', '\n'))
);
// Capture language identifier and then any character until the earliest triple backtick // Capture language identifier and then any character until the earliest triple backtick
// Language identifier is one word immediately after opening backticks, followed immediately by newline // Language identifier is one word immediately after opening backticks, followed immediately by newline
// Blank lines at the beginning and end of content are trimmed // Blank lines at the beginning and end of content are trimmed
private static readonly IMatcher<MarkdownNode> MultiLineCodeBlockNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> MultiLineCodeBlockNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("```(?:(\\w*)\\n)?(.+?)```", DefaultRegexOptions | RegexOptions.Singleline), new Regex("```(?:(\\w*)\\n)?(.+?)```", DefaultRegexOptions | RegexOptions.Singleline),
m => new MultiLineCodeBlockNode(m.Groups[1].Value, m.Groups[2].Value.Trim('\r', '\n'))); m => new MultiLineCodeBlockNode(m.Groups[1].Value, m.Groups[2].Value.Trim('\r', '\n'))
);
/* Mentions */ /* Mentions */
// Capture @everyone // Capture @everyone
private static readonly IMatcher<MarkdownNode> EveryoneMentionNodeMatcher = new StringMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> EveryoneMentionNodeMatcher = new StringMatcher<MarkdownNode>(
"@everyone", "@everyone",
p => new MentionNode("everyone", MentionType.Meta)); _ => new MentionNode("everyone", MentionType.Meta)
);
// Capture @here // Capture @here
private static readonly IMatcher<MarkdownNode> HereMentionNodeMatcher = new StringMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> HereMentionNodeMatcher = new StringMatcher<MarkdownNode>(
"@here", "@here",
p => new MentionNode("here", MentionType.Meta)); _ => new MentionNode("here", MentionType.Meta)
);
// Capture <@123456> or <@!123456> // Capture <@123456> or <@!123456>
private static readonly IMatcher<MarkdownNode> UserMentionNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> UserMentionNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("<@!?(\\d+)>", DefaultRegexOptions), new Regex("<@!?(\\d+)>", DefaultRegexOptions),
m => new MentionNode(m.Groups[1].Value, MentionType.User)); m => new MentionNode(m.Groups[1].Value, MentionType.User)
);
// Capture <#123456> // Capture <#123456>
private static readonly IMatcher<MarkdownNode> ChannelMentionNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> ChannelMentionNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("<#(\\d+)>", DefaultRegexOptions), new Regex("<#(\\d+)>", DefaultRegexOptions),
m => new MentionNode(m.Groups[1].Value, MentionType.Channel)); m => new MentionNode(m.Groups[1].Value, MentionType.Channel)
);
// Capture <@&123456> // Capture <@&123456>
private static readonly IMatcher<MarkdownNode> RoleMentionNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> RoleMentionNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("<@&(\\d+)>", DefaultRegexOptions), new Regex("<@&(\\d+)>", DefaultRegexOptions),
m => new MentionNode(m.Groups[1].Value, MentionType.Role)); m => new MentionNode(m.Groups[1].Value, MentionType.Role)
);
/* Emojis */ /* Emojis */
@ -129,30 +154,36 @@ namespace DiscordChatExporter.Domain.Markdown
// ... or digit followed by enclosing mark // ... or digit followed by enclosing mark
// (this does not match all emojis in Discord but it's reasonably accurate enough) // (this does not match all emojis in Discord but it's reasonably accurate enough)
private static readonly IMatcher<MarkdownNode> StandardEmojiNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> StandardEmojiNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("((?:[\\uD83C][\\uDDE6-\\uDDFF]){2}|[\\u2600-\\u26FF]|\\p{Cs}{2}|\\d\\p{Me})", DefaultRegexOptions), new Regex("((?:[\\uD83C][\\uDDE6-\\uDDFF]){2}|[\\u2600-\\u26FF]|\\p{Cs}{2}|\\d\\p{Me})",
m => new EmojiNode(m.Groups[1].Value)); DefaultRegexOptions),
m => new EmojiNode(m.Groups[1].Value)
);
// Capture <:lul:123456> or <a:lul:123456> // Capture <:lul:123456> or <a:lul:123456>
private static readonly IMatcher<MarkdownNode> CustomEmojiNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> CustomEmojiNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("<(a)?:(.+?):(\\d+?)>", DefaultRegexOptions), new Regex("<(a)?:(.+?):(\\d+?)>", DefaultRegexOptions),
m => new EmojiNode(m.Groups[3].Value, m.Groups[2].Value, !string.IsNullOrWhiteSpace(m.Groups[1].Value))); m => new EmojiNode(m.Groups[3].Value, m.Groups[2].Value, !string.IsNullOrWhiteSpace(m.Groups[1].Value))
);
/* Links */ /* Links */
// Capture [title](link) // Capture [title](link)
private static readonly IMatcher<MarkdownNode> TitledLinkNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> TitledLinkNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("\\[(.+?)\\]\\((.+?)\\)", DefaultRegexOptions), new Regex("\\[(.+?)\\]\\((.+?)\\)", DefaultRegexOptions),
m => new LinkNode(m.Groups[2].Value, m.Groups[1].Value)); m => new LinkNode(m.Groups[2].Value, m.Groups[1].Value)
);
// Capture any non-whitespace character after http:// or https:// until the last punctuation character or whitespace // Capture any non-whitespace character after http:// or https:// until the last punctuation character or whitespace
private static readonly IMatcher<MarkdownNode> AutoLinkNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> AutoLinkNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("(https?://\\S*[^\\.,:;\"\'\\s])", DefaultRegexOptions), new Regex("(https?://\\S*[^\\.,:;\"\'\\s])", DefaultRegexOptions),
m => new LinkNode(m.Groups[1].Value)); m => new LinkNode(m.Groups[1].Value)
);
// Same as auto link but also surrounded by angular brackets // Same as auto link but also surrounded by angular brackets
private static readonly IMatcher<MarkdownNode> HiddenLinkNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> HiddenLinkNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("<(https?://\\S*[^\\.,:;\"\'\\s])>", DefaultRegexOptions), new Regex("<(https?://\\S*[^\\.,:;\"\'\\s])>", DefaultRegexOptions),
m => new LinkNode(m.Groups[1].Value)); m => new LinkNode(m.Groups[1].Value)
);
/* Text */ /* Text */
@ -160,25 +191,29 @@ namespace DiscordChatExporter.Domain.Markdown
// This escapes it from matching for formatting // This escapes it from matching for formatting
private static readonly IMatcher<MarkdownNode> ShrugTextNodeMatcher = new StringMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> ShrugTextNodeMatcher = new StringMatcher<MarkdownNode>(
@"¯\_(ツ)_/¯", @"¯\_(ツ)_/¯",
p => new TextNode(p.ToString())); p => new TextNode(p.ToString())
);
// Capture some specific emojis that don't get rendered // Capture some specific emojis that don't get rendered
// This escapes it from matching for emoji // This escapes it from matching for emoji
private static readonly IMatcher<MarkdownNode> IgnoredEmojiTextNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> IgnoredEmojiTextNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("(\\u26A7|\\u2640|\\u2642|\\u2695|\\u267E|\\u00A9|\\u00AE|\\u2122)", DefaultRegexOptions), new Regex("(\\u26A7|\\u2640|\\u2642|\\u2695|\\u267E|\\u00A9|\\u00AE|\\u2122)", DefaultRegexOptions),
m => new TextNode(m.Groups[1].Value)); m => new TextNode(m.Groups[1].Value)
);
// Capture any "symbol/other" character or surrogate pair preceded by a backslash // Capture any "symbol/other" character or surrogate pair preceded by a backslash
// This escapes it from matching for emoji // This escapes it from matching for emoji
private static readonly IMatcher<MarkdownNode> EscapedSymbolTextNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> EscapedSymbolTextNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("\\\\(\\p{So}|\\p{Cs}{2})", DefaultRegexOptions), new Regex("\\\\(\\p{So}|\\p{Cs}{2})", DefaultRegexOptions),
m => new TextNode(m.Groups[1].Value)); m => new TextNode(m.Groups[1].Value)
);
// Capture any non-whitespace, non latin alphanumeric character preceded by a backslash // Capture any non-whitespace, non latin alphanumeric character preceded by a backslash
// This escapes it from matching for formatting or other tokens // This escapes it from matching for formatting or other tokens
private static readonly IMatcher<MarkdownNode> EscapedCharacterTextNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> EscapedCharacterTextNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("\\\\([^a-zA-Z0-9\\s])", DefaultRegexOptions), new Regex("\\\\([^a-zA-Z0-9\\s])", DefaultRegexOptions),
m => new TextNode(m.Groups[1].Value)); m => new TextNode(m.Groups[1].Value)
);
// Combine all matchers into one // Combine all matchers into one
// Matchers that have similar patterns are ordered from most specific to least specific // Matchers that have similar patterns are ordered from most specific to least specific

@ -25,7 +25,7 @@ namespace DiscordChatExporter.Domain.Markdown.Matching
{ {
} }
public StringPart Slice(int newStartIndex, int newLength) => new StringPart(Target, newStartIndex, newLength); public StringPart Slice(int newStartIndex, int newLength) => new(Target, newStartIndex, newLength);
public StringPart Slice(int newStartIndex) => Slice(newStartIndex, EndIndex - newStartIndex); public StringPart Slice(int newStartIndex) => Slice(newStartIndex, EndIndex - newStartIndex);

@ -7,7 +7,7 @@ namespace DiscordChatExporter.Gui.Converters
[ValueConversion(typeof(DateTimeOffset?), typeof(DateTime?))] [ValueConversion(typeof(DateTimeOffset?), typeof(DateTime?))]
public class DateTimeOffsetToDateTimeConverter : IValueConverter public class DateTimeOffsetToDateTimeConverter : IValueConverter
{ {
public static DateTimeOffsetToDateTimeConverter Instance { get; } = new DateTimeOffsetToDateTimeConverter(); public static DateTimeOffsetToDateTimeConverter Instance { get; } = new();
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
{ {

@ -8,7 +8,7 @@ namespace DiscordChatExporter.Gui.Converters
[ValueConversion(typeof(ExportFormat), typeof(string))] [ValueConversion(typeof(ExportFormat), typeof(string))]
public class ExportFormatToStringConverter : IValueConverter public class ExportFormatToStringConverter : IValueConverter
{ {
public static ExportFormatToStringConverter Instance { get; } = new ExportFormatToStringConverter(); public static ExportFormatToStringConverter Instance { get; } = new();
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
{ {

@ -7,7 +7,7 @@ namespace DiscordChatExporter.Gui.Converters
[ValueConversion(typeof(bool), typeof(bool))] [ValueConversion(typeof(bool), typeof(bool))]
public class InverseBoolConverter : IValueConverter public class InverseBoolConverter : IValueConverter
{ {
public static InverseBoolConverter Instance { get; } = new InverseBoolConverter(); public static InverseBoolConverter Instance { get; } = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{ {

@ -7,7 +7,7 @@ namespace DiscordChatExporter.Gui.Converters
[ValueConversion(typeof(TimeSpan?), typeof(DateTime?))] [ValueConversion(typeof(TimeSpan?), typeof(DateTime?))]
public class TimeSpanToDateTimeConverter : IValueConverter public class TimeSpanToDateTimeConverter : IValueConverter
{ {
public static TimeSpanToDateTimeConverter Instance { get; } = new TimeSpanToDateTimeConverter(); public static TimeSpanToDateTimeConverter Instance { get; } = new();
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
{ {

Loading…
Cancel
Save