Add support for timestamp markers

Closes #637
pull/678/head
Tyrrrz 3 years ago
parent ae42554621
commit abf7498667

@ -141,10 +141,16 @@
border-radius: 3px; border-radius: 3px;
padding: 0 2px; padding: 0 2px;
color: #7289da; color: #7289da;
background: rgba(114, 137, 218, .1); background-color: rgba(114, 137, 218, .1);
font-weight: 500; 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 { .emoji {
width: 1.325em; width: 1.325em;
height: 1.325em; height: 1.325em;
@ -588,7 +594,7 @@
border-radius: 3px; border-radius: 3px;
vertical-align: middle; vertical-align: middle;
line-height: 1.3; line-height: 1.3;
background: #5865F2; background-color: #5865F2;
color: #ffffff; color: #ffffff;
font-size: 0.625em; font-size: 0.625em;
font-weight: 500; font-weight: 500;

@ -75,6 +75,40 @@ namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors
return base.VisitMultiLineCodeBlock(multiLineCodeBlock); 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($"<a href=\"{Uri.EscapeUriString(link.Url)}\" onclick=\"scrollToMessage(event, '{linkedMessageId}')\">")
.Append(HtmlEncode(link.Title))
.Append("</a>");
}
else
{
_buffer
.Append($"<a href=\"{Uri.EscapeUriString(link.Url)}\">")
.Append(HtmlEncode(link.Title))
.Append("</a>");
}
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($"<img loading=\"lazy\" class=\"emoji {jumboClass}\" alt=\"{emoji.Name}\" title=\"{emoji.Code}\" src=\"{emojiImageUrl}\">");
return base.VisitEmoji(emoji);
}
protected override MarkdownNode VisitMention(MentionNode mention) protected override MarkdownNode VisitMention(MentionNode mention)
{ {
var mentionId = Snowflake.TryParse(mention.Id); var mentionId = Snowflake.TryParse(mention.Id);
@ -126,38 +160,14 @@ namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors
return base.VisitMention(mention); 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($"<img loading=\"lazy\" class=\"emoji {jumboClass}\" alt=\"{emoji.Name}\" title=\"{emoji.Code}\" src=\"{emojiImageUrl}\">");
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($"<a href=\"{Uri.EscapeUriString(link.Url)}\" onclick=\"scrollToMessage(event, '{linkedMessageId}')\">")
.Append(HtmlEncode(link.Title))
.Append("</a>");
}
else
{ {
_buffer _buffer
.Append($"<a href=\"{Uri.EscapeUriString(link.Url)}\">") .Append("<span class=\"timestamp\">")
.Append(HtmlEncode(link.Title)) .Append(HtmlEncode(_context.FormatDate(timestamp.Value)))
.Append("</a>"); .Append("</span>");
}
return base.VisitLink(link); return base.VisitUnixTimestamp(timestamp);
} }
} }

@ -23,6 +23,17 @@ namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors
return base.VisitText(text); 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) protected override MarkdownNode VisitMention(MentionNode mention)
{ {
var mentionId = Snowflake.TryParse(mention.Id); var mentionId = Snowflake.TryParse(mention.Id);
@ -59,15 +70,13 @@ namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors
return base.VisitMention(mention); return base.VisitMention(mention);
} }
protected override MarkdownNode VisitEmoji(EmojiNode emoji) protected override MarkdownNode VisitUnixTimestamp(UnixTimestampNode timestamp)
{ {
_buffer.Append( _buffer.Append(
emoji.IsCustomEmoji _context.FormatDate(timestamp.Value)
? $":{emoji.Name}:"
: emoji.Name
); );
return base.VisitEmoji(emoji); return base.VisitUnixTimestamp(timestamp);
} }
} }

@ -1,4 +1,6 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using DiscordChatExporter.Core.Utils; using DiscordChatExporter.Core.Utils;
@ -225,6 +227,26 @@ namespace DiscordChatExporter.Core.Markdown.Parsing
(_, m) => new TextNode(m.Groups[1].Value) (_, m) => new TextNode(m.Groups[1].Value)
); );
/* Misc */
// Capture <t:12345678> or <t:12345678:R>
private static readonly IMatcher<MarkdownNode> UnixTimestampNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("<t:(\\d+)(?::\\w)?>", 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 // 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
private static readonly IMatcher<MarkdownNode> AggregateNodeMatcher = new AggregateMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> AggregateNodeMatcher = new AggregateMatcher<MarkdownNode>(
@ -266,7 +288,10 @@ namespace DiscordChatExporter.Core.Markdown.Parsing
// Emoji // Emoji
StandardEmojiNodeMatcher, StandardEmojiNodeMatcher,
CustomEmojiNodeMatcher, CustomEmojiNodeMatcher,
CodedStandardEmojiNodeMatcher CodedStandardEmojiNodeMatcher,
// Misc
UnixTimestampNodeMatcher
); );
// Minimal set of matchers for non-multimedia formats (e.g. plain text) // Minimal set of matchers for non-multimedia formats (e.g. plain text)
@ -279,7 +304,10 @@ namespace DiscordChatExporter.Core.Markdown.Parsing
RoleMentionNodeMatcher, RoleMentionNodeMatcher,
// Emoji // Emoji
CustomEmojiNodeMatcher CustomEmojiNodeMatcher,
// Misc
UnixTimestampNodeMatcher
); );
private static IReadOnlyList<MarkdownNode> Parse(StringPart stringPart, IMatcher<MarkdownNode> matcher) => private static IReadOnlyList<MarkdownNode> Parse(StringPart stringPart, IMatcher<MarkdownNode> matcher) =>

@ -5,7 +5,8 @@ namespace DiscordChatExporter.Core.Markdown.Parsing
{ {
internal abstract class MarkdownVisitor internal abstract class MarkdownVisitor
{ {
protected virtual MarkdownNode VisitText(TextNode text) => text; protected virtual MarkdownNode VisitText(TextNode text) =>
text;
protected virtual MarkdownNode VisitFormatted(FormattedNode formatted) protected virtual MarkdownNode VisitFormatted(FormattedNode formatted)
{ {
@ -13,15 +14,23 @@ namespace DiscordChatExporter.Core.Markdown.Parsing
return formatted; 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 public MarkdownNode Visit(MarkdownNode node) => node switch
{ {
@ -32,6 +41,7 @@ namespace DiscordChatExporter.Core.Markdown.Parsing
LinkNode link => VisitLink(link), LinkNode link => VisitLink(link),
EmojiNode emoji => VisitEmoji(emoji), EmojiNode emoji => VisitEmoji(emoji),
MentionNode mention => VisitMention(mention), MentionNode mention => VisitMention(mention),
UnixTimestampNode timestamp => VisitUnixTimestamp(timestamp),
_ => throw new ArgumentOutOfRangeException(nameof(node)) _ => throw new ArgumentOutOfRangeException(nameof(node))
}; };

@ -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();
}
}
Loading…
Cancel
Save