From abf74986677246eae82691f062ced129a820b4b8 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Sun, 18 Jul 2021 16:04:39 +0300 Subject: [PATCH] Add support for timestamp markers Closes #637 --- .../Writers/Html/PreambleTemplate.cshtml | 10 ++- .../MarkdownVisitors/HtmlMarkdownVisitor.cs | 68 +++++++++++-------- .../PlainTextMarkdownVisitor.cs | 19 ++++-- .../Markdown/Parsing/MarkdownParser.cs | 34 +++++++++- .../Markdown/Parsing/MarkdownVisitor.cs | 22 ++++-- .../Markdown/UnixTimestampNode.cs | 13 ++++ 6 files changed, 121 insertions(+), 45 deletions(-) create mode 100644 DiscordChatExporter.Core/Markdown/UnixTimestampNode.cs diff --git a/DiscordChatExporter.Core/Exporting/Writers/Html/PreambleTemplate.cshtml b/DiscordChatExporter.Core/Exporting/Writers/Html/PreambleTemplate.cshtml index 866914d6..366f7bf3 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/Html/PreambleTemplate.cshtml +++ b/DiscordChatExporter.Core/Exporting/Writers/Html/PreambleTemplate.cshtml @@ -141,10 +141,16 @@ border-radius: 3px; padding: 0 2px; color: #7289da; - background: rgba(114, 137, 218, .1); + background-color: rgba(114, 137, 218, .1); font-weight: 500; } + .timestamp { + border-radius: 3px; + padding: 0 2px; + background-color: @Themed("rgba(255, 255, 255, 0.06)", "rgba(6, 6, 7, 0.08)"); + } + .emoji { width: 1.325em; height: 1.325em; @@ -588,7 +594,7 @@ border-radius: 3px; vertical-align: middle; line-height: 1.3; - background: #5865F2; + background-color: #5865F2; color: #ffffff; font-size: 0.625em; font-weight: 500; diff --git a/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/HtmlMarkdownVisitor.cs b/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/HtmlMarkdownVisitor.cs index 973c7786..20b90fbe 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/HtmlMarkdownVisitor.cs +++ b/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/HtmlMarkdownVisitor.cs @@ -75,6 +75,40 @@ namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors return base.VisitMultiLineCodeBlock(multiLineCodeBlock); } + protected override MarkdownNode VisitLink(LinkNode link) + { + // Extract message ID if the link points to a Discord message + var linkedMessageId = Regex.Match(link.Url, "^https?://(?:discord|discordapp).com/channels/.*?/(\\d+)/?$").Groups[1].Value; + + if (!string.IsNullOrWhiteSpace(linkedMessageId)) + { + _buffer + .Append($"") + .Append(HtmlEncode(link.Title)) + .Append(""); + } + else + { + _buffer + .Append($"") + .Append(HtmlEncode(link.Title)) + .Append(""); + } + + return base.VisitLink(link); + } + + protected override MarkdownNode VisitEmoji(EmojiNode emoji) + { + var emojiImageUrl = Emoji.GetImageUrl(emoji.Id, emoji.Name, emoji.IsAnimated); + var jumboClass = _isJumbo ? "emoji--large" : ""; + + _buffer + .Append($"\"{emoji.Name}\""); + + return base.VisitEmoji(emoji); + } + protected override MarkdownNode VisitMention(MentionNode mention) { var mentionId = Snowflake.TryParse(mention.Id); @@ -126,38 +160,14 @@ namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors return base.VisitMention(mention); } - protected override MarkdownNode VisitEmoji(EmojiNode emoji) + protected override MarkdownNode VisitUnixTimestamp(UnixTimestampNode timestamp) { - var emojiImageUrl = Emoji.GetImageUrl(emoji.Id, emoji.Name, emoji.IsAnimated); - var jumboClass = _isJumbo ? "emoji--large" : ""; - _buffer - .Append($"\"{emoji.Name}\""); + .Append("") + .Append(HtmlEncode(_context.FormatDate(timestamp.Value))) + .Append(""); - return base.VisitEmoji(emoji); - } - - protected override MarkdownNode VisitLink(LinkNode link) - { - // Extract message ID if the link points to a Discord message - var linkedMessageId = Regex.Match(link.Url, "^https?://(?:discord|discordapp).com/channels/.*?/(\\d+)/?$").Groups[1].Value; - - if (!string.IsNullOrWhiteSpace(linkedMessageId)) - { - _buffer - .Append($"") - .Append(HtmlEncode(link.Title)) - .Append(""); - } - else - { - _buffer - .Append($"") - .Append(HtmlEncode(link.Title)) - .Append(""); - } - - return base.VisitLink(link); + return base.VisitUnixTimestamp(timestamp); } } diff --git a/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/PlainTextMarkdownVisitor.cs b/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/PlainTextMarkdownVisitor.cs index a4f6cfce..128ff776 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/PlainTextMarkdownVisitor.cs +++ b/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/PlainTextMarkdownVisitor.cs @@ -23,6 +23,17 @@ namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors return base.VisitText(text); } + protected override MarkdownNode VisitEmoji(EmojiNode emoji) + { + _buffer.Append( + emoji.IsCustomEmoji + ? $":{emoji.Name}:" + : emoji.Name + ); + + return base.VisitEmoji(emoji); + } + protected override MarkdownNode VisitMention(MentionNode mention) { var mentionId = Snowflake.TryParse(mention.Id); @@ -59,15 +70,13 @@ namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors return base.VisitMention(mention); } - protected override MarkdownNode VisitEmoji(EmojiNode emoji) + protected override MarkdownNode VisitUnixTimestamp(UnixTimestampNode timestamp) { _buffer.Append( - emoji.IsCustomEmoji - ? $":{emoji.Name}:" - : emoji.Name + _context.FormatDate(timestamp.Value) ); - return base.VisitEmoji(emoji); + return base.VisitUnixTimestamp(timestamp); } } diff --git a/DiscordChatExporter.Core/Markdown/Parsing/MarkdownParser.cs b/DiscordChatExporter.Core/Markdown/Parsing/MarkdownParser.cs index aa6a4ff0..f18dda52 100644 --- a/DiscordChatExporter.Core/Markdown/Parsing/MarkdownParser.cs +++ b/DiscordChatExporter.Core/Markdown/Parsing/MarkdownParser.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using DiscordChatExporter.Core.Utils; @@ -225,6 +227,26 @@ namespace DiscordChatExporter.Core.Markdown.Parsing (_, m) => new TextNode(m.Groups[1].Value) ); + /* Misc */ + + // Capture or + private static readonly IMatcher UnixTimestampNodeMatcher = new RegexMatcher( + new Regex("", DefaultRegexOptions), + (_, m) => + { + // We don't care about the 'R' parameter because we're not going to + // show relative timestamps in an export anyway. + + if (!long.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, + out var offset)) + { + return null; + } + + return new UnixTimestampNode(DateTimeOffset.UnixEpoch + TimeSpan.FromSeconds(offset)); + } + ); + // Combine all matchers into one // Matchers that have similar patterns are ordered from most specific to least specific private static readonly IMatcher AggregateNodeMatcher = new AggregateMatcher( @@ -266,7 +288,10 @@ namespace DiscordChatExporter.Core.Markdown.Parsing // Emoji StandardEmojiNodeMatcher, CustomEmojiNodeMatcher, - CodedStandardEmojiNodeMatcher + CodedStandardEmojiNodeMatcher, + + // Misc + UnixTimestampNodeMatcher ); // Minimal set of matchers for non-multimedia formats (e.g. plain text) @@ -279,7 +304,10 @@ namespace DiscordChatExporter.Core.Markdown.Parsing RoleMentionNodeMatcher, // Emoji - CustomEmojiNodeMatcher + CustomEmojiNodeMatcher, + + // Misc + UnixTimestampNodeMatcher ); private static IReadOnlyList Parse(StringPart stringPart, IMatcher matcher) => diff --git a/DiscordChatExporter.Core/Markdown/Parsing/MarkdownVisitor.cs b/DiscordChatExporter.Core/Markdown/Parsing/MarkdownVisitor.cs index ad953ab4..5b98f7eb 100644 --- a/DiscordChatExporter.Core/Markdown/Parsing/MarkdownVisitor.cs +++ b/DiscordChatExporter.Core/Markdown/Parsing/MarkdownVisitor.cs @@ -5,7 +5,8 @@ namespace DiscordChatExporter.Core.Markdown.Parsing { internal abstract class MarkdownVisitor { - protected virtual MarkdownNode VisitText(TextNode text) => text; + protected virtual MarkdownNode VisitText(TextNode text) => + text; protected virtual MarkdownNode VisitFormatted(FormattedNode formatted) { @@ -13,15 +14,23 @@ namespace DiscordChatExporter.Core.Markdown.Parsing return formatted; } - protected virtual MarkdownNode VisitInlineCodeBlock(InlineCodeBlockNode inlineCodeBlock) => inlineCodeBlock; + protected virtual MarkdownNode VisitInlineCodeBlock(InlineCodeBlockNode inlineCodeBlock) => + inlineCodeBlock; - protected virtual MarkdownNode VisitMultiLineCodeBlock(MultiLineCodeBlockNode multiLineCodeBlock) => multiLineCodeBlock; + protected virtual MarkdownNode VisitMultiLineCodeBlock(MultiLineCodeBlockNode multiLineCodeBlock) => + multiLineCodeBlock; - protected virtual MarkdownNode VisitLink(LinkNode link) => link; + protected virtual MarkdownNode VisitLink(LinkNode link) => + link; - protected virtual MarkdownNode VisitEmoji(EmojiNode emoji) => emoji; + protected virtual MarkdownNode VisitEmoji(EmojiNode emoji) => + emoji; - protected virtual MarkdownNode VisitMention(MentionNode mention) => mention; + protected virtual MarkdownNode VisitMention(MentionNode mention) => + mention; + + protected virtual MarkdownNode VisitUnixTimestamp(UnixTimestampNode timestamp) => + timestamp; public MarkdownNode Visit(MarkdownNode node) => node switch { @@ -32,6 +41,7 @@ namespace DiscordChatExporter.Core.Markdown.Parsing LinkNode link => VisitLink(link), EmojiNode emoji => VisitEmoji(emoji), MentionNode mention => VisitMention(mention), + UnixTimestampNode timestamp => VisitUnixTimestamp(timestamp), _ => throw new ArgumentOutOfRangeException(nameof(node)) }; diff --git a/DiscordChatExporter.Core/Markdown/UnixTimestampNode.cs b/DiscordChatExporter.Core/Markdown/UnixTimestampNode.cs new file mode 100644 index 00000000..aed9a6f9 --- /dev/null +++ b/DiscordChatExporter.Core/Markdown/UnixTimestampNode.cs @@ -0,0 +1,13 @@ +using System; + +namespace DiscordChatExporter.Core.Markdown +{ + internal class UnixTimestampNode : MarkdownNode + { + public DateTimeOffset Value { get; } + + public UnixTimestampNode(DateTimeOffset value) => Value = value; + + public override string ToString() => Value.ToString(); + } +} \ No newline at end of file