mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2025-05-23 11:16:59 -04:00
Use C#9 features
This commit is contained in:
parent
9dda9cfc27
commit
63803f98aa
18 changed files with 99 additions and 71 deletions
|
@ -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…
Add table
Add a link
Reference in a new issue