diff --git a/DiscordChatExporter.Cli.Tests/TestData/ChannelIds.cs b/DiscordChatExporter.Cli.Tests/Infra/ChannelIds.cs similarity index 86% rename from DiscordChatExporter.Cli.Tests/TestData/ChannelIds.cs rename to DiscordChatExporter.Cli.Tests/Infra/ChannelIds.cs index e75f243..56944fd 100644 --- a/DiscordChatExporter.Cli.Tests/TestData/ChannelIds.cs +++ b/DiscordChatExporter.Cli.Tests/Infra/ChannelIds.cs @@ -1,6 +1,6 @@ using DiscordChatExporter.Core.Discord; -namespace DiscordChatExporter.Cli.Tests.TestData; +namespace DiscordChatExporter.Cli.Tests.Infra; public static class ChannelIds { @@ -14,6 +14,8 @@ public static class ChannelIds public static Snowflake FilterTestCases { get; } = Snowflake.Parse("866744075033641020"); + public static Snowflake MarkdownTestCases { get; } = Snowflake.Parse("866459526819348521"); + public static Snowflake MentionTestCases { get; } = Snowflake.Parse("866458801389174794"); public static Snowflake ReplyTestCases { get; } = Snowflake.Parse("866459871934677052"); diff --git a/DiscordChatExporter.Cli.Tests/Specs/CsvContentSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/CsvContentSpecs.cs index 74ff088..e0f047d 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/CsvContentSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/CsvContentSpecs.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; using DiscordChatExporter.Cli.Tests.Infra; -using DiscordChatExporter.Cli.Tests.TestData; using FluentAssertions; using Xunit; diff --git a/DiscordChatExporter.Cli.Tests/Specs/DateRangeSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/DateRangeSpecs.cs index b1edbaa..4bea979 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/DateRangeSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/DateRangeSpecs.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using CliFx.Infrastructure; using DiscordChatExporter.Cli.Commands; using DiscordChatExporter.Cli.Tests.Infra; -using DiscordChatExporter.Cli.Tests.TestData; using DiscordChatExporter.Cli.Tests.Utils; using DiscordChatExporter.Core.Discord; using DiscordChatExporter.Core.Exporting; diff --git a/DiscordChatExporter.Cli.Tests/Specs/FilterSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/FilterSpecs.cs index 7eea6d5..85b8483 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/FilterSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/FilterSpecs.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using CliFx.Infrastructure; using DiscordChatExporter.Cli.Commands; using DiscordChatExporter.Cli.Tests.Infra; -using DiscordChatExporter.Cli.Tests.TestData; using DiscordChatExporter.Cli.Tests.Utils; using DiscordChatExporter.Core.Exporting; using DiscordChatExporter.Core.Exporting.Filtering; diff --git a/DiscordChatExporter.Cli.Tests/Specs/HtmlAttachmentSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/HtmlAttachmentSpecs.cs index c4432ff..b3a9b7c 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/HtmlAttachmentSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/HtmlAttachmentSpecs.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using AngleSharp.Dom; using DiscordChatExporter.Cli.Tests.Infra; -using DiscordChatExporter.Cli.Tests.TestData; using DiscordChatExporter.Core.Discord; using FluentAssertions; using Xunit; diff --git a/DiscordChatExporter.Cli.Tests/Specs/HtmlContentSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/HtmlContentSpecs.cs index 1c20cab..dc89674 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/HtmlContentSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/HtmlContentSpecs.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using AngleSharp.Dom; using DiscordChatExporter.Cli.Tests.Infra; -using DiscordChatExporter.Cli.Tests.TestData; using DiscordChatExporter.Core.Discord; using FluentAssertions; using Xunit; diff --git a/DiscordChatExporter.Cli.Tests/Specs/HtmlEmbedSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/HtmlEmbedSpecs.cs index 98ba088..1f4846e 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/HtmlEmbedSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/HtmlEmbedSpecs.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using AngleSharp.Dom; using DiscordChatExporter.Cli.Tests.Infra; -using DiscordChatExporter.Cli.Tests.TestData; using DiscordChatExporter.Core.Discord; using FluentAssertions; using Xunit; @@ -33,7 +32,7 @@ public class HtmlEmbedSpecs } [Fact] - public async Task Message_containing_an_image_link_is_rendered_with_an_image_embed() + public async Task Message_with_an_image_link_is_rendered_with_an_image_embed() { // https://github.com/Tyrrrz/DiscordChatExporter/issues/537 @@ -53,7 +52,7 @@ public class HtmlEmbedSpecs } [Fact] - public async Task Message_containing_an_image_link_and_nothing_else_is_rendered_without_text_content() + public async Task Message_with_an_image_link_and_nothing_else_is_rendered_without_text_content() { // https://github.com/Tyrrrz/DiscordChatExporter/issues/682 @@ -69,7 +68,7 @@ public class HtmlEmbedSpecs } [Fact] - public async Task Message_containing_a_gifv_link_is_rendered_with_a_video_embed() + public async Task Message_with_a_GIFV_link_is_rendered_with_a_video_embed() { // Act var message = await ExportWrapper.GetMessageAsHtmlAsync( @@ -87,7 +86,7 @@ public class HtmlEmbedSpecs } [Fact] - public async Task Message_containing_a_gifv_link_and_nothing_else_is_rendered_without_text_content() + public async Task Message_with_a_GIFV_link_and_nothing_else_is_rendered_without_text_content() { // Act var message = await ExportWrapper.GetMessageAsHtmlAsync( @@ -101,7 +100,7 @@ public class HtmlEmbedSpecs } [Fact] - public async Task Message_containing_a_Spotify_track_link_is_rendered_with_a_track_embed() + public async Task Message_with_a_Spotify_track_link_is_rendered_with_a_track_embed() { // https://github.com/Tyrrrz/DiscordChatExporter/issues/657 @@ -117,7 +116,7 @@ public class HtmlEmbedSpecs } [Fact] - public async Task Message_containing_a_YouTube_video_link_is_rendered_with_a_video_embed() + public async Task Message_with_a_YouTube_video_link_is_rendered_with_a_video_embed() { // https://github.com/Tyrrrz/DiscordChatExporter/issues/570 @@ -133,7 +132,7 @@ public class HtmlEmbedSpecs } [Fact] - public async Task Message_containing_a_Twitter_post_link_with_multiple_images_is_rendered_as_a_single_embed() + public async Task Message_with_a_Twitter_post_link_with_multiple_images_is_rendered_as_a_single_embed() { // https://github.com/Tyrrrz/DiscordChatExporter/issues/695 diff --git a/DiscordChatExporter.Cli.Tests/Specs/HtmlGroupingSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/HtmlGroupingSpecs.cs index e504fb8..052683b 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/HtmlGroupingSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/HtmlGroupingSpecs.cs @@ -5,7 +5,6 @@ using AngleSharp.Dom; using CliFx.Infrastructure; using DiscordChatExporter.Cli.Commands; using DiscordChatExporter.Cli.Tests.Infra; -using DiscordChatExporter.Cli.Tests.TestData; using DiscordChatExporter.Cli.Tests.Utils; using DiscordChatExporter.Core.Exporting; using FluentAssertions; diff --git a/DiscordChatExporter.Cli.Tests/Specs/HtmlMarkdownSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/HtmlMarkdownSpecs.cs new file mode 100644 index 0000000..e443672 --- /dev/null +++ b/DiscordChatExporter.Cli.Tests/Specs/HtmlMarkdownSpecs.cs @@ -0,0 +1,136 @@ +using System.Threading.Tasks; +using AngleSharp.Dom; +using DiscordChatExporter.Cli.Tests.Infra; +using DiscordChatExporter.Core.Discord; +using FluentAssertions; +using Xunit; + +namespace DiscordChatExporter.Cli.Tests.Specs; + +public class HtmlMarkdownSpecs +{ + [Fact] + public async Task Message_with_a_timestamp_is_rendered_correctly() + { + // Act + var message = await ExportWrapper.GetMessageAsHtmlAsync( + ChannelIds.MarkdownTestCases, + Snowflake.Parse("1074323136411078787") + ); + + // Assert + message.Text().Should().Contain("Default timestamp: 12-Feb-23 03:36 PM"); + message.InnerHtml.Should().Contain("Sunday, February 12, 2023 3:36 PM"); + } + + [Fact] + public async Task Message_with_a_short_time_timestamp_is_rendered_correctly() + { + // Act + var message = await ExportWrapper.GetMessageAsHtmlAsync( + ChannelIds.MarkdownTestCases, + Snowflake.Parse("1074323205268967596") + ); + + // Assert + message.Text().Should().Contain("Short time timestamp: 3:36 PM"); + message.InnerHtml.Should().Contain("Sunday, February 12, 2023 3:36 PM"); + } + + [Fact] + public async Task Message_with_a_long_time_timestamp_is_rendered_correctly() + { + // Act + var message = await ExportWrapper.GetMessageAsHtmlAsync( + ChannelIds.MarkdownTestCases, + Snowflake.Parse("1074323235342139483") + ); + + // Assert + message.Text().Should().Contain("Long time timestamp: 3:36:12 PM"); + message.InnerHtml.Should().Contain("Sunday, February 12, 2023 3:36 PM"); + } + + [Fact] + public async Task Message_with_a_short_date_timestamp_is_rendered_correctly() + { + // Act + var message = await ExportWrapper.GetMessageAsHtmlAsync( + ChannelIds.MarkdownTestCases, + Snowflake.Parse("1074323326727634984") + ); + + // Assert + message.Text().Should().Contain("Short date timestamp: 02/12/2023"); + message.InnerHtml.Should().Contain("Sunday, February 12, 2023 3:36 PM"); + } + + [Fact] + public async Task Message_with_a_long_date_timestamp_is_rendered_correctly() + { + // Act + var message = await ExportWrapper.GetMessageAsHtmlAsync( + ChannelIds.MarkdownTestCases, + Snowflake.Parse("1074323350731640863") + ); + + // Assert + message.Text().Should().Contain("Long date timestamp: February 12, 2023"); + message.InnerHtml.Should().Contain("Sunday, February 12, 2023 3:36 PM"); + } + + [Fact] + public async Task Message_with_a_full_timestamp_is_rendered_correctly() + { + // Act + var message = await ExportWrapper.GetMessageAsHtmlAsync( + ChannelIds.MarkdownTestCases, + Snowflake.Parse("1074323374379118593") + ); + + // Assert + message.Text().Should().Contain("Full timestamp: February 12, 2023 3:36 PM"); + message.InnerHtml.Should().Contain("Sunday, February 12, 2023 3:36 PM"); + } + + [Fact] + public async Task Message_with_a_full_long_timestamp_is_rendered_correctly() + { + // Act + var message = await ExportWrapper.GetMessageAsHtmlAsync( + ChannelIds.MarkdownTestCases, + Snowflake.Parse("1074323409095376947") + ); + + // Assert + message.Text().Should().Contain("Full long timestamp: Sunday, February 12, 2023 3:36 PM"); + message.InnerHtml.Should().Contain("Sunday, February 12, 2023 3:36 PM"); + } + + [Fact] + public async Task Message_with_a_relative_timestamp_is_rendered_as_the_default_timestamp() + { + // Act + var message = await ExportWrapper.GetMessageAsHtmlAsync( + ChannelIds.MarkdownTestCases, + Snowflake.Parse("1074323436853285004") + ); + + // Assert + message.Text().Should().Contain("Relative timestamp: 12-Feb-23 03:36 PM"); + message.InnerHtml.Should().Contain("Sunday, February 12, 2023 3:36 PM"); + } + + [Fact] + public async Task Message_with_an_invalid_timestamp_is_rendered_correctly() + { + // Act + var message = await ExportWrapper.GetMessageAsHtmlAsync( + ChannelIds.MarkdownTestCases, + Snowflake.Parse("1074328534409019563") + ); + + // Assert + message.Text().Should().Contain("Invalid timestamp: Invalid date"); + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Cli.Tests/Specs/HtmlMentionSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/HtmlMentionSpecs.cs index 038e99d..34efad3 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/HtmlMentionSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/HtmlMentionSpecs.cs @@ -1,7 +1,6 @@ using System.Threading.Tasks; using AngleSharp.Dom; using DiscordChatExporter.Cli.Tests.Infra; -using DiscordChatExporter.Cli.Tests.TestData; using DiscordChatExporter.Core.Discord; using FluentAssertions; using Xunit; @@ -11,7 +10,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs; public class HtmlMentionSpecs { [Fact] - public async Task User_mention_is_rendered_correctly() + public async Task Message_with_a_user_mention_is_rendered_correctly() { // Act var message = await ExportWrapper.GetMessageAsHtmlAsync( @@ -25,7 +24,7 @@ public class HtmlMentionSpecs } [Fact] - public async Task Text_channel_mention_is_rendered_correctly() + public async Task Message_with_a_text_channel_mention_is_rendered_correctly() { // Act var message = await ExportWrapper.GetMessageAsHtmlAsync( @@ -38,7 +37,7 @@ public class HtmlMentionSpecs } [Fact] - public async Task Voice_channel_mention_is_rendered_correctly() + public async Task Message_with_a_voice_channel_mention_is_rendered_correctly() { // Act var message = await ExportWrapper.GetMessageAsHtmlAsync( @@ -51,7 +50,7 @@ public class HtmlMentionSpecs } [Fact] - public async Task Role_mention_is_rendered_correctly() + public async Task Message_with_a_role_mention_is_rendered_correctly() { // Act var message = await ExportWrapper.GetMessageAsHtmlAsync( diff --git a/DiscordChatExporter.Cli.Tests/Specs/HtmlReplySpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/HtmlReplySpecs.cs index 28d2614..8fcbb94 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/HtmlReplySpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/HtmlReplySpecs.cs @@ -1,7 +1,6 @@ using System.Threading.Tasks; using AngleSharp.Dom; using DiscordChatExporter.Cli.Tests.Infra; -using DiscordChatExporter.Cli.Tests.TestData; using DiscordChatExporter.Core.Discord; using FluentAssertions; using Xunit; @@ -11,7 +10,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs; public class HtmlReplySpecs { [Fact] - public async Task Reply_to_a_normal_message_is_rendered_correctly() + public async Task Message_with_a_reply_is_rendered_correctly() { // Act var message = await ExportWrapper.GetMessageAsHtmlAsync( @@ -25,7 +24,7 @@ public class HtmlReplySpecs } [Fact] - public async Task Reply_to_a_deleted_message_is_rendered_correctly() + public async Task Message_with_a_reply_to_a_deleted_message_is_rendered_correctly() { // https://github.com/Tyrrrz/DiscordChatExporter/issues/645 @@ -43,7 +42,7 @@ public class HtmlReplySpecs } [Fact] - public async Task Reply_to_an_empty_message_with_attachment_is_rendered_correctly() + public async Task Message_with_a_reply_to_an_empty_message_with_attachment_is_rendered_correctly() { // https://github.com/Tyrrrz/DiscordChatExporter/issues/634 diff --git a/DiscordChatExporter.Cli.Tests/Specs/HtmlStickerSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/HtmlStickerSpecs.cs index a50b520..3ade155 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/HtmlStickerSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/HtmlStickerSpecs.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; using DiscordChatExporter.Cli.Tests.Infra; -using DiscordChatExporter.Cli.Tests.TestData; using DiscordChatExporter.Core.Discord; using FluentAssertions; using Xunit; diff --git a/DiscordChatExporter.Cli.Tests/Specs/JsonAttachmentSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/JsonAttachmentSpecs.cs index 585f8af..583e9fe 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/JsonAttachmentSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/JsonAttachmentSpecs.cs @@ -1,7 +1,6 @@ using System.Linq; using System.Threading.Tasks; using DiscordChatExporter.Cli.Tests.Infra; -using DiscordChatExporter.Cli.Tests.TestData; using DiscordChatExporter.Core.Discord; using FluentAssertions; using Xunit; diff --git a/DiscordChatExporter.Cli.Tests/Specs/JsonContentSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/JsonContentSpecs.cs index cf6a17b..c9d3883 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/JsonContentSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/JsonContentSpecs.cs @@ -1,7 +1,6 @@ using System.Linq; using System.Threading.Tasks; using DiscordChatExporter.Cli.Tests.Infra; -using DiscordChatExporter.Cli.Tests.TestData; using FluentAssertions; using Xunit; diff --git a/DiscordChatExporter.Cli.Tests/Specs/JsonEmbedSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/JsonEmbedSpecs.cs index b22b43e..562688e 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/JsonEmbedSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/JsonEmbedSpecs.cs @@ -1,7 +1,6 @@ using System.Linq; using System.Threading.Tasks; using DiscordChatExporter.Cli.Tests.Infra; -using DiscordChatExporter.Cli.Tests.TestData; using DiscordChatExporter.Core.Discord; using FluentAssertions; using Xunit; diff --git a/DiscordChatExporter.Cli.Tests/Specs/JsonMentionSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/JsonMentionSpecs.cs index 26de91a..dad6654 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/JsonMentionSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/JsonMentionSpecs.cs @@ -1,7 +1,6 @@ using System.Linq; using System.Threading.Tasks; using DiscordChatExporter.Cli.Tests.Infra; -using DiscordChatExporter.Cli.Tests.TestData; using DiscordChatExporter.Core.Discord; using FluentAssertions; using Xunit; @@ -11,7 +10,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs; public class JsonMentionSpecs { [Fact] - public async Task User_mention_is_rendered_correctly() + public async Task Message_with_a_user_mention_is_rendered_correctly() { // Act var message = await ExportWrapper.GetMessageAsJsonAsync( @@ -31,7 +30,7 @@ public class JsonMentionSpecs } [Fact] - public async Task Text_channel_mention_is_rendered_correctly() + public async Task Message_with_a_text_channel_mention_is_rendered_correctly() { // Act var message = await ExportWrapper.GetMessageAsJsonAsync( @@ -44,7 +43,7 @@ public class JsonMentionSpecs } [Fact] - public async Task Voice_channel_mention_is_rendered_correctly() + public async Task Message_with_a_voice_channel_mention_is_rendered_correctly() { // Act var message = await ExportWrapper.GetMessageAsJsonAsync( @@ -57,7 +56,7 @@ public class JsonMentionSpecs } [Fact] - public async Task Role_mention_is_rendered_correctly() + public async Task Message_with_a_role_mention_is_rendered_correctly() { // Act var message = await ExportWrapper.GetMessageAsJsonAsync( diff --git a/DiscordChatExporter.Cli.Tests/Specs/JsonStickerSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/JsonStickerSpecs.cs index 559ef01..074b576 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/JsonStickerSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/JsonStickerSpecs.cs @@ -1,7 +1,6 @@ using System.Linq; using System.Threading.Tasks; using DiscordChatExporter.Cli.Tests.Infra; -using DiscordChatExporter.Cli.Tests.TestData; using DiscordChatExporter.Core.Discord; using FluentAssertions; using Xunit; diff --git a/DiscordChatExporter.Cli.Tests/Specs/PartitioningSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/PartitioningSpecs.cs index d7360c7..41ba695 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/PartitioningSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/PartitioningSpecs.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using CliFx.Infrastructure; using DiscordChatExporter.Cli.Commands; using DiscordChatExporter.Cli.Tests.Infra; -using DiscordChatExporter.Cli.Tests.TestData; using DiscordChatExporter.Cli.Tests.Utils; using DiscordChatExporter.Core.Exporting; using DiscordChatExporter.Core.Exporting.Partitioning; diff --git a/DiscordChatExporter.Cli.Tests/Specs/PlainTextContentSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/PlainTextContentSpecs.cs index bbd4f18..c4fbc7c 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/PlainTextContentSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/PlainTextContentSpecs.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; using DiscordChatExporter.Cli.Tests.Infra; -using DiscordChatExporter.Cli.Tests.TestData; using FluentAssertions; using Xunit; diff --git a/DiscordChatExporter.Cli.Tests/Specs/SelfContainedSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/SelfContainedSpecs.cs index b058281..9bf4a8a 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/SelfContainedSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/SelfContainedSpecs.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using CliFx.Infrastructure; using DiscordChatExporter.Cli.Commands; using DiscordChatExporter.Cli.Tests.Infra; -using DiscordChatExporter.Cli.Tests.TestData; using DiscordChatExporter.Cli.Tests.Utils; using DiscordChatExporter.Core.Exporting; using FluentAssertions; diff --git a/DiscordChatExporter.Core/Discord/Data/Channel.cs b/DiscordChatExporter.Core/Discord/Data/Channel.cs index e829935..4ac1acc 100644 --- a/DiscordChatExporter.Core/Discord/Data/Channel.cs +++ b/DiscordChatExporter.Core/Discord/Data/Channel.cs @@ -19,7 +19,7 @@ public partial record Channel( string? Topic, Snowflake? LastMessageId) : IHasId { - public bool SupportsVoice => Kind is ChannelKind.GuildVoiceChat or ChannelKind.GuildStageVoice; + public bool IsVoice => Kind is ChannelKind.GuildVoiceChat or ChannelKind.GuildStageVoice; } public partial record Channel @@ -92,4 +92,4 @@ public partial record Channel lastMessageId ); } -} +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Discord/Snowflake.cs b/DiscordChatExporter.Core/Discord/Snowflake.cs index d8c64e3..194b166 100644 --- a/DiscordChatExporter.Core/Discord/Snowflake.cs +++ b/DiscordChatExporter.Core/Discord/Snowflake.cs @@ -18,8 +18,8 @@ public partial record struct Snowflake { public static Snowflake Zero { get; } = new(0); - public static Snowflake FromDate(DateTimeOffset date) => new( - ((ulong)date.ToUnixTimeMilliseconds() - 1420070400000UL) << 22 + public static Snowflake FromDate(DateTimeOffset instant) => new( + ((ulong)instant.ToUnixTimeMilliseconds() - 1420070400000UL) << 22 ); public static Snowflake? TryParse(string? str, IFormatProvider? formatProvider = null) @@ -34,9 +34,9 @@ public partial record struct Snowflake } // As date - if (DateTimeOffset.TryParse(str, formatProvider, DateTimeStyles.None, out var date)) + if (DateTimeOffset.TryParse(str, formatProvider, DateTimeStyles.None, out var instant)) { - return FromDate(date); + return FromDate(instant); } return null; diff --git a/DiscordChatExporter.Core/Exporting/ExportAssetDownloader.cs b/DiscordChatExporter.Core/Exporting/ExportAssetDownloader.cs index ad44e15..8288c88 100644 --- a/DiscordChatExporter.Core/Exporting/ExportAssetDownloader.cs +++ b/DiscordChatExporter.Core/Exporting/ExportAssetDownloader.cs @@ -50,8 +50,8 @@ internal partial class ExportAssetDownloader try { var lastModified = response.Content.Headers.TryGetValue("Last-Modified")?.Pipe(s => - DateTimeOffset.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) - ? date + DateTimeOffset.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out var instant) + ? instant : (DateTimeOffset?) null ); diff --git a/DiscordChatExporter.Core/Exporting/ExportContext.cs b/DiscordChatExporter.Core/Exporting/ExportContext.cs index cd79e7b..923aa9a 100644 --- a/DiscordChatExporter.Core/Exporting/ExportContext.cs +++ b/DiscordChatExporter.Core/Exporting/ExportContext.cs @@ -38,11 +38,11 @@ internal class ExportContext _assetDownloader = new ExportAssetDownloader(request.OutputAssetsDirPath, request.ShouldReuseAssets); } - public string FormatDate(DateTimeOffset date) => Request.DateFormat switch + public string FormatDate(DateTimeOffset instant) => Request.DateFormat switch { - "unix" => date.ToUnixTimeSeconds().ToString(), - "unixms" => date.ToUnixTimeMilliseconds().ToString(), - var format => date.ToLocalString(format) + "unix" => instant.ToUnixTimeSeconds().ToString(), + "unixms" => instant.ToUnixTimeMilliseconds().ToString(), + var format => instant.ToLocalString(format) }; public Member? TryGetMember(Snowflake id) => Members.FirstOrDefault(m => m.Id == id); diff --git a/DiscordChatExporter.Core/Exporting/HtmlMarkdownVisitor.cs b/DiscordChatExporter.Core/Exporting/HtmlMarkdownVisitor.cs index ecd92a3..edafa6b 100644 --- a/DiscordChatExporter.Core/Exporting/HtmlMarkdownVisitor.cs +++ b/DiscordChatExporter.Core/Exporting/HtmlMarkdownVisitor.cs @@ -40,33 +40,45 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor var (openingTag, closingTag) = formatting.Kind switch { FormattingKind.Bold => ( + // language=HTML "", + // language=HTML "" ), FormattingKind.Italic => ( + // language=HTML "", + // language=HTML "" ), FormattingKind.Underline => ( + // language=HTML "", + // language=HTML "" ), FormattingKind.Strikethrough => ( + // language=HTML "", + // language=HTML "" ), FormattingKind.Spoiler => ( - "", - "" + // language=HTML + """""", + // language=HTML + """""" ), FormattingKind.Quote => ( - "
", - "
" + // language=HTML + """
""", + // language=HTML + """
""" ), _ => throw new InvalidOperationException($"Unknown formatting kind '{formatting.Kind}'.") @@ -83,10 +95,12 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor InlineCodeBlockNode inlineCodeBlock, CancellationToken cancellationToken = default) { - _buffer - .Append("") - .Append(HtmlEncode(inlineCodeBlock.Code)) - .Append(""); + _buffer.Append( + // language=HTML + $""" + {HtmlEncode(inlineCodeBlock.Code)} + """ + ); return await base.VisitInlineCodeBlockAsync(inlineCodeBlock, cancellationToken); } @@ -95,14 +109,16 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor MultiLineCodeBlockNode multiLineCodeBlock, CancellationToken cancellationToken = default) { - var highlightCssClass = !string.IsNullOrWhiteSpace(multiLineCodeBlock.Language) + var highlightClass = !string.IsNullOrWhiteSpace(multiLineCodeBlock.Language) ? $"language-{multiLineCodeBlock.Language}" : "nohighlight"; - _buffer - .Append($"") - .Append(HtmlEncode(multiLineCodeBlock.Code)) - .Append(""); + _buffer.Append( + // language=HTML + $""" + {HtmlEncode(multiLineCodeBlock.Code)} + """ + ); return await base.VisitMultiLineCodeBlockAsync(multiLineCodeBlock, cancellationToken); } @@ -111,7 +127,7 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor LinkNode link, CancellationToken cancellationToken = default) { - // Try to extract message ID if the link refers to a Discord message + // Try to extract the message ID if the link points to a Discord message var linkedMessageId = Regex.Match( link.Url, "^https?://(?:discord|discordapp).com/channels/.*?/(\\d+)/?$" @@ -119,11 +135,15 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor _buffer.Append( !string.IsNullOrWhiteSpace(linkedMessageId) - ? $"" - : $"" + // language=HTML + ? $"""""" + // language=HTML + : $"""""" ); var result = await base.VisitLinkAsync(link, cancellationToken); + + // language=HTML _buffer.Append(""); return result; @@ -137,13 +157,15 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor var jumboClass = _isJumbo ? "chatlog__emoji--large" : ""; _buffer.Append( - $"" + // language=HTML + $""" + {emoji.Name} + """ ); return await base.VisitEmojiAsync(emoji, cancellationToken); @@ -155,17 +177,21 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor { if (mention.Kind == MentionKind.Everyone) { - _buffer - .Append("") - .Append("@everyone") - .Append(""); + _buffer.Append( + // language=HTML + """ + @everyone + """ + ); } else if (mention.Kind == MentionKind.Here) { - _buffer - .Append("") - .Append("@here") - .Append(""); + _buffer.Append( + // language=HTML + """ + @here + """ + ); } else if (mention.Kind == MentionKind.User) { @@ -173,21 +199,25 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor var fullName = member?.User.FullName ?? "Unknown"; var nick = member?.Nick ?? "Unknown"; - _buffer - .Append($"") - .Append('@').Append(HtmlEncode(nick)) - .Append(""); + _buffer.Append( + // language=HTML + $""" + @{HtmlEncode(nick)} + """ + ); } else if (mention.Kind == MentionKind.Channel) { var channel = mention.TargetId?.Pipe(_context.TryGetChannel); - var symbol = channel?.SupportsVoice == true ? "🔊" : "#"; + var symbol = channel?.IsVoice == true ? "🔊" : "#"; var name = channel?.Name ?? "deleted-channel"; - _buffer - .Append("") - .Append(symbol).Append(HtmlEncode(name)) - .Append(""); + _buffer.Append( + // language=HTML + $""" + {symbol}{HtmlEncode(name)} + """ + ); } else if (mention.Kind == MentionKind.Role) { @@ -196,38 +226,42 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor var color = role?.Color; var style = color is not null - ? $"color: rgb({color.Value.R}, {color.Value.G}, {color.Value.B}); " + - $"background-color: rgba({color.Value.R}, {color.Value.G}, {color.Value.B}, 0.1);" + ? $""" + color: rgb({color.Value.R}, {color.Value.G}, {color.Value.B}); background-color: rgba({color.Value.R}, {color.Value.G}, {color.Value.B}, 0.1); + """ : ""; - _buffer - .Append($"") - .Append('@').Append(HtmlEncode(name)) - .Append(""); + _buffer.Append( + // language=HTML + $""" + @{HtmlEncode(name)} + """ + ); } return await base.VisitMentionAsync(mention, cancellationToken); } - protected override async ValueTask VisitUnixTimestampAsync( - UnixTimestampNode timestamp, + protected override async ValueTask VisitTimestampAsync( + TimestampNode timestamp, CancellationToken cancellationToken = default) { - var dateString = timestamp.Date is not null - ? _context.FormatDate(timestamp.Date.Value) + var formatted = timestamp.Instant is not null + ? !string.IsNullOrWhiteSpace(timestamp.Format) + ? timestamp.Instant.Value.ToLocalString(timestamp.Format) + : _context.FormatDate(timestamp.Instant.Value) : "Invalid date"; - // Timestamp tooltips always use full date regardless of the configured format - var longDateString = timestamp.Date is not null - ? timestamp.Date.Value.ToLocalString("dddd, MMMM d, yyyy h:mm tt") - : "Invalid date"; + var formattedLong = timestamp.Instant?.ToLocalString("dddd, MMMM d, yyyy h:mm tt") ?? ""; - _buffer - .Append($"") - .Append(HtmlEncode(dateString)) - .Append(""); + _buffer.Append( + // language=HTML + $""" + {HtmlEncode(formatted)} + """ + ); - return await base.VisitUnixTimestampAsync(timestamp, cancellationToken); + return await base.VisitTimestampAsync(timestamp, cancellationToken); } } @@ -248,9 +282,7 @@ internal partial class HtmlMarkdownVisitor nodes.All(n => n is EmojiNode || n is TextNode textNode && string.IsNullOrWhiteSpace(textNode.Text)); var buffer = new StringBuilder(); - - await new HtmlMarkdownVisitor(context, buffer, isJumbo) - .VisitAsync(nodes, cancellationToken); + await new HtmlMarkdownVisitor(context, buffer, isJumbo).VisitAsync(nodes, cancellationToken); return buffer.ToString(); } diff --git a/DiscordChatExporter.Core/Exporting/MessageGroupTemplate.cshtml b/DiscordChatExporter.Core/Exporting/MessageGroupTemplate.cshtml index c1b41c4..ba4e0e8 100644 --- a/DiscordChatExporter.Core/Exporting/MessageGroupTemplate.cshtml +++ b/DiscordChatExporter.Core/Exporting/MessageGroupTemplate.cshtml @@ -18,8 +18,8 @@ ValueTask ResolveAssetUrlAsync(string url) => ExportContext.ResolveAssetUrlAsync(url, CancellationToken); - string FormatDate(DateTimeOffset date) => - ExportContext.FormatDate(date); + string FormatDate(DateTimeOffset instant) => + ExportContext.FormatDate(instant); ValueTask FormatMarkdownAsync(string markdown) => HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown, true, CancellationToken); diff --git a/DiscordChatExporter.Core/Exporting/PlainTextMarkdownVisitor.cs b/DiscordChatExporter.Core/Exporting/PlainTextMarkdownVisitor.cs index 4543642..463ce0a 100644 --- a/DiscordChatExporter.Core/Exporting/PlainTextMarkdownVisitor.cs +++ b/DiscordChatExporter.Core/Exporting/PlainTextMarkdownVisitor.cs @@ -66,7 +66,7 @@ internal partial class PlainTextMarkdownVisitor : MarkdownVisitor _buffer.Append($"#{name}"); // Voice channel marker - if (channel?.SupportsVoice == true) + if (channel?.IsVoice == true) _buffer.Append(" [voice]"); } else if (mention.Kind == MentionKind.Role) @@ -80,17 +80,19 @@ internal partial class PlainTextMarkdownVisitor : MarkdownVisitor return await base.VisitMentionAsync(mention, cancellationToken); } - protected override async ValueTask VisitUnixTimestampAsync( - UnixTimestampNode timestamp, + protected override async ValueTask VisitTimestampAsync( + TimestampNode timestamp, CancellationToken cancellationToken = default) { _buffer.Append( - timestamp.Date is not null - ? _context.FormatDate(timestamp.Date.Value) + timestamp.Instant is not null + ? !string.IsNullOrWhiteSpace(timestamp.Format) + ? timestamp.Instant.Value.ToLocalString(timestamp.Format) + : _context.FormatDate(timestamp.Instant.Value) : "Invalid date" ); - return await base.VisitUnixTimestampAsync(timestamp, cancellationToken); + return await base.VisitTimestampAsync(timestamp, cancellationToken); } } @@ -102,10 +104,9 @@ internal partial class PlainTextMarkdownVisitor CancellationToken cancellationToken = default) { var nodes = MarkdownParser.ParseMinimal(markdown); - var buffer = new StringBuilder(); - await new PlainTextMarkdownVisitor(context, buffer) - .VisitAsync(nodes, cancellationToken); + var buffer = new StringBuilder(); + await new PlainTextMarkdownVisitor(context, buffer).VisitAsync(nodes, cancellationToken); return buffer.ToString(); } diff --git a/DiscordChatExporter.Core/Exporting/PreambleTemplate.cshtml b/DiscordChatExporter.Core/Exporting/PreambleTemplate.cshtml index 431e7dc..5a0b1af 100644 --- a/DiscordChatExporter.Core/Exporting/PreambleTemplate.cshtml +++ b/DiscordChatExporter.Core/Exporting/PreambleTemplate.cshtml @@ -22,8 +22,8 @@ ValueTask ResolveAssetUrlAsync(string url) => ExportContext.ResolveAssetUrlAsync(url, CancellationToken); - string FormatDate(DateTimeOffset date) => - ExportContext.FormatDate(date); + string FormatDate(DateTimeOffset instant) => + ExportContext.FormatDate(instant); ValueTask FormatMarkdownAsync(string markdown) => HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown, true, CancellationToken); @@ -660,6 +660,7 @@ .chatlog__markdown-spoiler { background-color: @Themed("rgba(255, 255, 255, 0.1)", "rgba(0, 0, 0, 0.1)"); + padding: 0 2px; border-radius: 3px; } @@ -728,9 +729,9 @@ } .chatlog__markdown-timestamp { - border-radius: 3px; + background-color: @Themed("rgba(255, 255, 255, 0.1)", "rgba(0, 0, 0, 0.1)"); padding: 0 2px; - color: @Themed("#a3a6aa", "#5e6772"); + border-radius: 3px; } .chatlog__emoji { diff --git a/DiscordChatExporter.Core/Markdown/Parsing/MarkdownParser.cs b/DiscordChatExporter.Core/Markdown/Parsing/MarkdownParser.cs index 399cf51..b063400 100644 --- a/DiscordChatExporter.Core/Markdown/Parsing/MarkdownParser.cs +++ b/DiscordChatExporter.Core/Markdown/Parsing/MarkdownParser.cs @@ -275,29 +275,37 @@ internal static partial class MarkdownParser /* Misc */ - private static readonly IMatcher UnixTimestampNodeMatcher = new RegexMatcher( + private static readonly IMatcher TimestampNodeMatcher = new RegexMatcher( // Capture or - new Regex(@"", DefaultRegexOptions), + new Regex(@"", 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 Parse(StringSegment segment, IMatcher matcher) => diff --git a/DiscordChatExporter.Core/Markdown/Parsing/MarkdownVisitor.cs b/DiscordChatExporter.Core/Markdown/Parsing/MarkdownVisitor.cs index 0da5e6a..36e17c5 100644 --- a/DiscordChatExporter.Core/Markdown/Parsing/MarkdownVisitor.cs +++ b/DiscordChatExporter.Core/Markdown/Parsing/MarkdownVisitor.cs @@ -48,8 +48,8 @@ internal abstract class MarkdownVisitor CancellationToken cancellationToken = default) => new(mention); - protected virtual ValueTask VisitUnixTimestampAsync( - UnixTimestampNode timestamp, + protected virtual ValueTask 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)) }; diff --git a/DiscordChatExporter.Core/Markdown/TimestampNode.cs b/DiscordChatExporter.Core/Markdown/TimestampNode.cs new file mode 100644 index 0000000..195ef5f --- /dev/null +++ b/DiscordChatExporter.Core/Markdown/TimestampNode.cs @@ -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); +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Markdown/UnixTimestampNode.cs b/DiscordChatExporter.Core/Markdown/UnixTimestampNode.cs deleted file mode 100644 index 23991c7..0000000 --- a/DiscordChatExporter.Core/Markdown/UnixTimestampNode.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System; - -namespace DiscordChatExporter.Core.Markdown; - -// Null date means invalid timestamp -internal record UnixTimestampNode(DateTimeOffset? Date) : MarkdownNode; \ No newline at end of file diff --git a/DiscordChatExporter.Core/Utils/Extensions/DateExtensions.cs b/DiscordChatExporter.Core/Utils/Extensions/DateExtensions.cs index 4f1c47a..055af03 100644 --- a/DiscordChatExporter.Core/Utils/Extensions/DateExtensions.cs +++ b/DiscordChatExporter.Core/Utils/Extensions/DateExtensions.cs @@ -5,6 +5,6 @@ namespace DiscordChatExporter.Core.Utils.Extensions; public static class DateExtensions { - public static string ToLocalString(this DateTimeOffset dateTime, string format) => - dateTime.ToLocalTime().ToString(format, CultureInfo.InvariantCulture); + public static string ToLocalString(this DateTimeOffset instant, string format) => + instant.ToLocalTime().ToString(format, CultureInfo.InvariantCulture); } \ No newline at end of file diff --git a/DiscordChatExporter.Gui/Views/Components/DashboardView.xaml b/DiscordChatExporter.Gui/Views/Components/DashboardView.xaml index 8b3c07f..262a883 100644 --- a/DiscordChatExporter.Gui/Views/Components/DashboardView.xaml +++ b/DiscordChatExporter.Gui/Views/Components/DashboardView.xaml @@ -358,10 +358,10 @@