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($"
");
+
+ 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($"
");
+ .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