Add support for different formats in the timestamp markdown node

Closes #662
This commit is contained in:
Tyrrrz
2023-02-12 16:12:41 +02:00
parent 75b942f66c
commit d99958a9b1
34 changed files with 321 additions and 156 deletions

View File

@@ -275,29 +275,37 @@ internal static partial class MarkdownParser
/* Misc */
private static readonly IMatcher<MarkdownNode> UnixTimestampNodeMatcher = new RegexMatcher<MarkdownNode>(
private static readonly IMatcher<MarkdownNode> TimestampNodeMatcher = new RegexMatcher<MarkdownNode>(
// Capture <t:12345678> or <t:12345678:R>
new Regex(@"<t:(-?\d+)(?::\w)?>", DefaultRegexOptions),
new Regex(@"<t:(-?\d+)(?::(\w))?>", DefaultRegexOptions),
(_, m) =>
{
// TODO: support formatting parameters
// See: https://github.com/Tyrrrz/DiscordChatExporter/issues/662
if (!long.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture,
out var offset))
{
return new UnixTimestampNode(null);
}
try
{
return new UnixTimestampNode(DateTimeOffset.UnixEpoch + TimeSpan.FromSeconds(offset));
var instant = DateTimeOffset.UnixEpoch + TimeSpan.FromSeconds(
long.Parse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture)
);
var format = m.Groups[2].Value switch
{
"t" => "h:mm tt",
"T" => "h:mm:ss tt",
"d" => "MM/dd/yyyy",
"D" => "MMMM dd, yyyy",
"f" => "MMMM dd, yyyy h:mm tt",
"F" => "dddd, MMMM dd, yyyy h:mm tt",
// Relative format is ignored because it doesn't make much sense in a static export
_ => null
};
return new TimestampNode(instant, format);
}
// https://github.com/Tyrrrz/DiscordChatExporter/issues/681
// https://github.com/Tyrrrz/DiscordChatExporter/issues/766
catch (Exception ex) when (ex is ArgumentOutOfRangeException or OverflowException)
catch (Exception ex) when (ex is FormatException or ArgumentOutOfRangeException or OverflowException)
{
return new UnixTimestampNode(null);
// For invalid timestamps, Discord renders "Invalid Date" instead of ignoring the markdown
return TimestampNode.Invalid;
}
}
);
@@ -346,7 +354,7 @@ internal static partial class MarkdownParser
CodedStandardEmojiNodeMatcher,
// Misc
UnixTimestampNodeMatcher
TimestampNodeMatcher
);
// Minimal set of matchers for non-multimedia formats (e.g. plain text)
@@ -362,7 +370,7 @@ internal static partial class MarkdownParser
CustomEmojiNodeMatcher,
// Misc
UnixTimestampNodeMatcher
TimestampNodeMatcher
);
private static IReadOnlyList<MarkdownNode> Parse(StringSegment segment, IMatcher<MarkdownNode> matcher) =>

View File

@@ -48,8 +48,8 @@ internal abstract class MarkdownVisitor
CancellationToken cancellationToken = default) =>
new(mention);
protected virtual ValueTask<MarkdownNode> VisitUnixTimestampAsync(
UnixTimestampNode timestamp,
protected virtual ValueTask<MarkdownNode> VisitTimestampAsync(
TimestampNode timestamp,
CancellationToken cancellationToken = default) =>
new(timestamp);
@@ -78,8 +78,8 @@ internal abstract class MarkdownVisitor
MentionNode mention =>
await VisitMentionAsync(mention, cancellationToken),
UnixTimestampNode timestamp =>
await VisitUnixTimestampAsync(timestamp, cancellationToken),
TimestampNode timestamp =>
await VisitTimestampAsync(timestamp, cancellationToken),
_ => throw new ArgumentOutOfRangeException(nameof(node))
};

View File

@@ -0,0 +1,9 @@
using System;
namespace DiscordChatExporter.Core.Markdown;
// Null date means invalid timestamp
internal record TimestampNode(DateTimeOffset? Instant, string? Format) : MarkdownNode
{
public static TimestampNode Invalid { get; } = new(null, null);
}

View File

@@ -1,6 +0,0 @@
using System;
namespace DiscordChatExporter.Core.Markdown;
// Null date means invalid timestamp
internal record UnixTimestampNode(DateTimeOffset? Date) : MarkdownNode;