mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2025-05-21 18:35:15 -04:00
Add support for different formats in the timestamp markdown node
Closes #662
This commit is contained in:
parent
75b942f66c
commit
d99958a9b1
34 changed files with 321 additions and 156 deletions
|
@ -1,6 +1,6 @@
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Tests.TestData;
|
namespace DiscordChatExporter.Cli.Tests.Infra;
|
||||||
|
|
||||||
public static class ChannelIds
|
public static class ChannelIds
|
||||||
{
|
{
|
||||||
|
@ -14,6 +14,8 @@ public static class ChannelIds
|
||||||
|
|
||||||
public static Snowflake FilterTestCases { get; } = Snowflake.Parse("866744075033641020");
|
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 MentionTestCases { get; } = Snowflake.Parse("866458801389174794");
|
||||||
|
|
||||||
public static Snowflake ReplyTestCases { get; } = Snowflake.Parse("866459871934677052");
|
public static Snowflake ReplyTestCases { get; } = Snowflake.Parse("866459871934677052");
|
|
@ -1,6 +1,5 @@
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.TestData;
|
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ using System.Threading.Tasks;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands;
|
using DiscordChatExporter.Cli.Commands;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.TestData;
|
|
||||||
using DiscordChatExporter.Cli.Tests.Utils;
|
using DiscordChatExporter.Cli.Tests.Utils;
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Core.Exporting;
|
using DiscordChatExporter.Core.Exporting;
|
||||||
|
|
|
@ -4,7 +4,6 @@ using System.Threading.Tasks;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands;
|
using DiscordChatExporter.Cli.Commands;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.TestData;
|
|
||||||
using DiscordChatExporter.Cli.Tests.Utils;
|
using DiscordChatExporter.Cli.Tests.Utils;
|
||||||
using DiscordChatExporter.Core.Exporting;
|
using DiscordChatExporter.Core.Exporting;
|
||||||
using DiscordChatExporter.Core.Exporting.Filtering;
|
using DiscordChatExporter.Core.Exporting.Filtering;
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.TestData;
|
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.TestData;
|
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.TestData;
|
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
@ -33,7 +32,7 @@ public class HtmlEmbedSpecs
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/537
|
||||||
|
|
||||||
|
@ -53,7 +52,7 @@ public class HtmlEmbedSpecs
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/682
|
||||||
|
|
||||||
|
@ -69,7 +68,7 @@ public class HtmlEmbedSpecs
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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
|
// Act
|
||||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||||
|
@ -87,7 +86,7 @@ public class HtmlEmbedSpecs
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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
|
// Act
|
||||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||||
|
@ -101,7 +100,7 @@ public class HtmlEmbedSpecs
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/657
|
||||||
|
|
||||||
|
@ -117,7 +116,7 @@ public class HtmlEmbedSpecs
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/570
|
||||||
|
|
||||||
|
@ -133,7 +132,7 @@ public class HtmlEmbedSpecs
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/695
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ using AngleSharp.Dom;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands;
|
using DiscordChatExporter.Cli.Commands;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.TestData;
|
|
||||||
using DiscordChatExporter.Cli.Tests.Utils;
|
using DiscordChatExporter.Cli.Tests.Utils;
|
||||||
using DiscordChatExporter.Core.Exporting;
|
using DiscordChatExporter.Core.Exporting;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
|
136
DiscordChatExporter.Cli.Tests/Specs/HtmlMarkdownSpecs.cs
Normal file
136
DiscordChatExporter.Cli.Tests/Specs/HtmlMarkdownSpecs.cs
Normal file
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.TestData;
|
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
@ -11,7 +10,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
||||||
public class HtmlMentionSpecs
|
public class HtmlMentionSpecs
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task User_mention_is_rendered_correctly()
|
public async Task Message_with_a_user_mention_is_rendered_correctly()
|
||||||
{
|
{
|
||||||
// Act
|
// Act
|
||||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||||
|
@ -25,7 +24,7 @@ public class HtmlMentionSpecs
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Text_channel_mention_is_rendered_correctly()
|
public async Task Message_with_a_text_channel_mention_is_rendered_correctly()
|
||||||
{
|
{
|
||||||
// Act
|
// Act
|
||||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||||
|
@ -38,7 +37,7 @@ public class HtmlMentionSpecs
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Voice_channel_mention_is_rendered_correctly()
|
public async Task Message_with_a_voice_channel_mention_is_rendered_correctly()
|
||||||
{
|
{
|
||||||
// Act
|
// Act
|
||||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||||
|
@ -51,7 +50,7 @@ public class HtmlMentionSpecs
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Role_mention_is_rendered_correctly()
|
public async Task Message_with_a_role_mention_is_rendered_correctly()
|
||||||
{
|
{
|
||||||
// Act
|
// Act
|
||||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.TestData;
|
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
@ -11,7 +10,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
||||||
public class HtmlReplySpecs
|
public class HtmlReplySpecs
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Reply_to_a_normal_message_is_rendered_correctly()
|
public async Task Message_with_a_reply_is_rendered_correctly()
|
||||||
{
|
{
|
||||||
// Act
|
// Act
|
||||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||||
|
@ -25,7 +24,7 @@ public class HtmlReplySpecs
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/645
|
||||||
|
|
||||||
|
@ -43,7 +42,7 @@ public class HtmlReplySpecs
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/634
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.TestData;
|
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.TestData;
|
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.TestData;
|
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.TestData;
|
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.TestData;
|
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
@ -11,7 +10,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
||||||
public class JsonMentionSpecs
|
public class JsonMentionSpecs
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task User_mention_is_rendered_correctly()
|
public async Task Message_with_a_user_mention_is_rendered_correctly()
|
||||||
{
|
{
|
||||||
// Act
|
// Act
|
||||||
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
||||||
|
@ -31,7 +30,7 @@ public class JsonMentionSpecs
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Text_channel_mention_is_rendered_correctly()
|
public async Task Message_with_a_text_channel_mention_is_rendered_correctly()
|
||||||
{
|
{
|
||||||
// Act
|
// Act
|
||||||
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
||||||
|
@ -44,7 +43,7 @@ public class JsonMentionSpecs
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Voice_channel_mention_is_rendered_correctly()
|
public async Task Message_with_a_voice_channel_mention_is_rendered_correctly()
|
||||||
{
|
{
|
||||||
// Act
|
// Act
|
||||||
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
||||||
|
@ -57,7 +56,7 @@ public class JsonMentionSpecs
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Role_mention_is_rendered_correctly()
|
public async Task Message_with_a_role_mention_is_rendered_correctly()
|
||||||
{
|
{
|
||||||
// Act
|
// Act
|
||||||
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.TestData;
|
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
|
@ -3,7 +3,6 @@ using System.Threading.Tasks;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands;
|
using DiscordChatExporter.Cli.Commands;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.TestData;
|
|
||||||
using DiscordChatExporter.Cli.Tests.Utils;
|
using DiscordChatExporter.Cli.Tests.Utils;
|
||||||
using DiscordChatExporter.Core.Exporting;
|
using DiscordChatExporter.Core.Exporting;
|
||||||
using DiscordChatExporter.Core.Exporting.Partitioning;
|
using DiscordChatExporter.Core.Exporting.Partitioning;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.TestData;
|
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ using System.Threading.Tasks;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands;
|
using DiscordChatExporter.Cli.Commands;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.TestData;
|
|
||||||
using DiscordChatExporter.Cli.Tests.Utils;
|
using DiscordChatExporter.Cli.Tests.Utils;
|
||||||
using DiscordChatExporter.Core.Exporting;
|
using DiscordChatExporter.Core.Exporting;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
|
|
@ -19,7 +19,7 @@ public partial record Channel(
|
||||||
string? Topic,
|
string? Topic,
|
||||||
Snowflake? LastMessageId) : IHasId
|
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
|
public partial record Channel
|
||||||
|
@ -92,4 +92,4 @@ public partial record Channel
|
||||||
lastMessageId
|
lastMessageId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -18,8 +18,8 @@ public partial record struct Snowflake
|
||||||
{
|
{
|
||||||
public static Snowflake Zero { get; } = new(0);
|
public static Snowflake Zero { get; } = new(0);
|
||||||
|
|
||||||
public static Snowflake FromDate(DateTimeOffset date) => new(
|
public static Snowflake FromDate(DateTimeOffset instant) => new(
|
||||||
((ulong)date.ToUnixTimeMilliseconds() - 1420070400000UL) << 22
|
((ulong)instant.ToUnixTimeMilliseconds() - 1420070400000UL) << 22
|
||||||
);
|
);
|
||||||
|
|
||||||
public static Snowflake? TryParse(string? str, IFormatProvider? formatProvider = null)
|
public static Snowflake? TryParse(string? str, IFormatProvider? formatProvider = null)
|
||||||
|
@ -34,9 +34,9 @@ public partial record struct Snowflake
|
||||||
}
|
}
|
||||||
|
|
||||||
// As date
|
// 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;
|
return null;
|
||||||
|
|
|
@ -50,8 +50,8 @@ internal partial class ExportAssetDownloader
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var lastModified = response.Content.Headers.TryGetValue("Last-Modified")?.Pipe(s =>
|
var lastModified = response.Content.Headers.TryGetValue("Last-Modified")?.Pipe(s =>
|
||||||
DateTimeOffset.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)
|
DateTimeOffset.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out var instant)
|
||||||
? date
|
? instant
|
||||||
: (DateTimeOffset?) null
|
: (DateTimeOffset?) null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -38,11 +38,11 @@ internal class ExportContext
|
||||||
_assetDownloader = new ExportAssetDownloader(request.OutputAssetsDirPath, request.ShouldReuseAssets);
|
_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(),
|
"unix" => instant.ToUnixTimeSeconds().ToString(),
|
||||||
"unixms" => date.ToUnixTimeMilliseconds().ToString(),
|
"unixms" => instant.ToUnixTimeMilliseconds().ToString(),
|
||||||
var format => date.ToLocalString(format)
|
var format => instant.ToLocalString(format)
|
||||||
};
|
};
|
||||||
|
|
||||||
public Member? TryGetMember(Snowflake id) => Members.FirstOrDefault(m => m.Id == id);
|
public Member? TryGetMember(Snowflake id) => Members.FirstOrDefault(m => m.Id == id);
|
||||||
|
|
|
@ -40,33 +40,45 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||||
var (openingTag, closingTag) = formatting.Kind switch
|
var (openingTag, closingTag) = formatting.Kind switch
|
||||||
{
|
{
|
||||||
FormattingKind.Bold => (
|
FormattingKind.Bold => (
|
||||||
|
// language=HTML
|
||||||
"<strong>",
|
"<strong>",
|
||||||
|
// language=HTML
|
||||||
"</strong>"
|
"</strong>"
|
||||||
),
|
),
|
||||||
|
|
||||||
FormattingKind.Italic => (
|
FormattingKind.Italic => (
|
||||||
|
// language=HTML
|
||||||
"<em>",
|
"<em>",
|
||||||
|
// language=HTML
|
||||||
"</em>"
|
"</em>"
|
||||||
),
|
),
|
||||||
|
|
||||||
FormattingKind.Underline => (
|
FormattingKind.Underline => (
|
||||||
|
// language=HTML
|
||||||
"<u>",
|
"<u>",
|
||||||
|
// language=HTML
|
||||||
"</u>"
|
"</u>"
|
||||||
),
|
),
|
||||||
|
|
||||||
FormattingKind.Strikethrough => (
|
FormattingKind.Strikethrough => (
|
||||||
|
// language=HTML
|
||||||
"<s>",
|
"<s>",
|
||||||
|
// language=HTML
|
||||||
"</s>"
|
"</s>"
|
||||||
),
|
),
|
||||||
|
|
||||||
FormattingKind.Spoiler => (
|
FormattingKind.Spoiler => (
|
||||||
"<span class=\"chatlog__markdown-spoiler chatlog__markdown-spoiler--hidden\" onclick=\"showSpoiler(event, this)\">",
|
// language=HTML
|
||||||
"</span>"
|
"""<span class="chatlog__markdown-spoiler chatlog__markdown-spoiler--hidden" onclick="showSpoiler(event, this)">""",
|
||||||
|
// language=HTML
|
||||||
|
"""</span>"""
|
||||||
),
|
),
|
||||||
|
|
||||||
FormattingKind.Quote => (
|
FormattingKind.Quote => (
|
||||||
"<div class=\"chatlog__markdown-quote\"><div class=\"chatlog__markdown-quote-border\"></div><div class=\"chatlog__markdown-quote-content\">",
|
// language=HTML
|
||||||
"</div></div>"
|
"""<div class="chatlog__markdown-quote"><div class="chatlog__markdown-quote-border"></div><div class="chatlog__markdown-quote-content">""",
|
||||||
|
// language=HTML
|
||||||
|
"""</div></div>"""
|
||||||
),
|
),
|
||||||
|
|
||||||
_ => throw new InvalidOperationException($"Unknown formatting kind '{formatting.Kind}'.")
|
_ => throw new InvalidOperationException($"Unknown formatting kind '{formatting.Kind}'.")
|
||||||
|
@ -83,10 +95,12 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||||
InlineCodeBlockNode inlineCodeBlock,
|
InlineCodeBlockNode inlineCodeBlock,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
_buffer
|
_buffer.Append(
|
||||||
.Append("<code class=\"chatlog__markdown-pre chatlog__markdown-pre--inline\">")
|
// language=HTML
|
||||||
.Append(HtmlEncode(inlineCodeBlock.Code))
|
$"""
|
||||||
.Append("</code>");
|
<code class="chatlog__markdown-pre chatlog__markdown-pre--inline">{HtmlEncode(inlineCodeBlock.Code)}</code>
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
|
||||||
return await base.VisitInlineCodeBlockAsync(inlineCodeBlock, cancellationToken);
|
return await base.VisitInlineCodeBlockAsync(inlineCodeBlock, cancellationToken);
|
||||||
}
|
}
|
||||||
|
@ -95,14 +109,16 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||||
MultiLineCodeBlockNode multiLineCodeBlock,
|
MultiLineCodeBlockNode multiLineCodeBlock,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var highlightCssClass = !string.IsNullOrWhiteSpace(multiLineCodeBlock.Language)
|
var highlightClass = !string.IsNullOrWhiteSpace(multiLineCodeBlock.Language)
|
||||||
? $"language-{multiLineCodeBlock.Language}"
|
? $"language-{multiLineCodeBlock.Language}"
|
||||||
: "nohighlight";
|
: "nohighlight";
|
||||||
|
|
||||||
_buffer
|
_buffer.Append(
|
||||||
.Append($"<code class=\"chatlog__markdown-pre chatlog__markdown-pre--multiline {highlightCssClass}\">")
|
// language=HTML
|
||||||
.Append(HtmlEncode(multiLineCodeBlock.Code))
|
$"""
|
||||||
.Append("</code>");
|
<code class="chatlog__markdown-pre chatlog__markdown-pre--multiline {highlightClass}">{HtmlEncode(multiLineCodeBlock.Code)}</code>
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
|
||||||
return await base.VisitMultiLineCodeBlockAsync(multiLineCodeBlock, cancellationToken);
|
return await base.VisitMultiLineCodeBlockAsync(multiLineCodeBlock, cancellationToken);
|
||||||
}
|
}
|
||||||
|
@ -111,7 +127,7 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||||
LinkNode link,
|
LinkNode link,
|
||||||
CancellationToken cancellationToken = default)
|
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(
|
var linkedMessageId = Regex.Match(
|
||||||
link.Url,
|
link.Url,
|
||||||
"^https?://(?:discord|discordapp).com/channels/.*?/(\\d+)/?$"
|
"^https?://(?:discord|discordapp).com/channels/.*?/(\\d+)/?$"
|
||||||
|
@ -119,11 +135,15 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||||
|
|
||||||
_buffer.Append(
|
_buffer.Append(
|
||||||
!string.IsNullOrWhiteSpace(linkedMessageId)
|
!string.IsNullOrWhiteSpace(linkedMessageId)
|
||||||
? $"<a href=\"{HtmlEncode(link.Url)}\" onclick=\"scrollToMessage(event, '{linkedMessageId}')\">"
|
// language=HTML
|
||||||
: $"<a href=\"{HtmlEncode(link.Url)}\">"
|
? $"""<a href="{HtmlEncode(link.Url)}" onclick="scrollToMessage(event, '{linkedMessageId}')">"""
|
||||||
|
// language=HTML
|
||||||
|
: $"""<a href="{HtmlEncode(link.Url)}">"""
|
||||||
);
|
);
|
||||||
|
|
||||||
var result = await base.VisitLinkAsync(link, cancellationToken);
|
var result = await base.VisitLinkAsync(link, cancellationToken);
|
||||||
|
|
||||||
|
// language=HTML
|
||||||
_buffer.Append("</a>");
|
_buffer.Append("</a>");
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -137,13 +157,15 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||||
var jumboClass = _isJumbo ? "chatlog__emoji--large" : "";
|
var jumboClass = _isJumbo ? "chatlog__emoji--large" : "";
|
||||||
|
|
||||||
_buffer.Append(
|
_buffer.Append(
|
||||||
$"<img " +
|
// language=HTML
|
||||||
$"loading=\"lazy\" " +
|
$"""
|
||||||
$"class=\"chatlog__emoji {jumboClass}\" " +
|
<img
|
||||||
$"alt=\"{emoji.Name}\" " +
|
loading="lazy"
|
||||||
$"title=\"{emoji.Code}\" " +
|
class="chatlog__emoji {jumboClass}"
|
||||||
$"src=\"{await _context.ResolveAssetUrlAsync(emojiImageUrl, cancellationToken)}\"" +
|
alt="{emoji.Name}"
|
||||||
$">"
|
title="{emoji.Code}"
|
||||||
|
src="{await _context.ResolveAssetUrlAsync(emojiImageUrl, cancellationToken)}">
|
||||||
|
"""
|
||||||
);
|
);
|
||||||
|
|
||||||
return await base.VisitEmojiAsync(emoji, cancellationToken);
|
return await base.VisitEmojiAsync(emoji, cancellationToken);
|
||||||
|
@ -155,17 +177,21 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||||
{
|
{
|
||||||
if (mention.Kind == MentionKind.Everyone)
|
if (mention.Kind == MentionKind.Everyone)
|
||||||
{
|
{
|
||||||
_buffer
|
_buffer.Append(
|
||||||
.Append("<span class=\"chatlog__markdown-mention\">")
|
// language=HTML
|
||||||
.Append("@everyone")
|
"""
|
||||||
.Append("</span>");
|
<span class="chatlog__markdown-mention">@everyone</span>
|
||||||
|
"""
|
||||||
|
);
|
||||||
}
|
}
|
||||||
else if (mention.Kind == MentionKind.Here)
|
else if (mention.Kind == MentionKind.Here)
|
||||||
{
|
{
|
||||||
_buffer
|
_buffer.Append(
|
||||||
.Append("<span class=\"chatlog__markdown-mention\">")
|
// language=HTML
|
||||||
.Append("@here")
|
"""
|
||||||
.Append("</span>");
|
<span class="chatlog__markdown-mention">@here</span>
|
||||||
|
"""
|
||||||
|
);
|
||||||
}
|
}
|
||||||
else if (mention.Kind == MentionKind.User)
|
else if (mention.Kind == MentionKind.User)
|
||||||
{
|
{
|
||||||
|
@ -173,21 +199,25 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||||
var fullName = member?.User.FullName ?? "Unknown";
|
var fullName = member?.User.FullName ?? "Unknown";
|
||||||
var nick = member?.Nick ?? "Unknown";
|
var nick = member?.Nick ?? "Unknown";
|
||||||
|
|
||||||
_buffer
|
_buffer.Append(
|
||||||
.Append($"<span class=\"chatlog__markdown-mention\" title=\"{HtmlEncode(fullName)}\">")
|
// language=HTML
|
||||||
.Append('@').Append(HtmlEncode(nick))
|
$"""
|
||||||
.Append("</span>");
|
<span class="chatlog__markdown-mention" title="{HtmlEncode(fullName)}">@{HtmlEncode(nick)}</span>
|
||||||
|
"""
|
||||||
|
);
|
||||||
}
|
}
|
||||||
else if (mention.Kind == MentionKind.Channel)
|
else if (mention.Kind == MentionKind.Channel)
|
||||||
{
|
{
|
||||||
var channel = mention.TargetId?.Pipe(_context.TryGetChannel);
|
var channel = mention.TargetId?.Pipe(_context.TryGetChannel);
|
||||||
var symbol = channel?.SupportsVoice == true ? "🔊" : "#";
|
var symbol = channel?.IsVoice == true ? "🔊" : "#";
|
||||||
var name = channel?.Name ?? "deleted-channel";
|
var name = channel?.Name ?? "deleted-channel";
|
||||||
|
|
||||||
_buffer
|
_buffer.Append(
|
||||||
.Append("<span class=\"chatlog__markdown-mention\">")
|
// language=HTML
|
||||||
.Append(symbol).Append(HtmlEncode(name))
|
$"""
|
||||||
.Append("</span>");
|
<span class="chatlog__markdown-mention">{symbol}{HtmlEncode(name)}</span>
|
||||||
|
"""
|
||||||
|
);
|
||||||
}
|
}
|
||||||
else if (mention.Kind == MentionKind.Role)
|
else if (mention.Kind == MentionKind.Role)
|
||||||
{
|
{
|
||||||
|
@ -196,38 +226,42 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||||
var color = role?.Color;
|
var color = role?.Color;
|
||||||
|
|
||||||
var style = color is not null
|
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
|
_buffer.Append(
|
||||||
.Append($"<span class=\"chatlog__markdown-mention\" style=\"{style}\">")
|
// language=HTML
|
||||||
.Append('@').Append(HtmlEncode(name))
|
$"""
|
||||||
.Append("</span>");
|
<span class="chatlog__markdown-mention" style="{style}">@{HtmlEncode(name)}</span>
|
||||||
|
"""
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await base.VisitMentionAsync(mention, cancellationToken);
|
return await base.VisitMentionAsync(mention, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async ValueTask<MarkdownNode> VisitUnixTimestampAsync(
|
protected override async ValueTask<MarkdownNode> VisitTimestampAsync(
|
||||||
UnixTimestampNode timestamp,
|
TimestampNode timestamp,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var dateString = timestamp.Date is not null
|
var formatted = timestamp.Instant is not null
|
||||||
? _context.FormatDate(timestamp.Date.Value)
|
? !string.IsNullOrWhiteSpace(timestamp.Format)
|
||||||
|
? timestamp.Instant.Value.ToLocalString(timestamp.Format)
|
||||||
|
: _context.FormatDate(timestamp.Instant.Value)
|
||||||
: "Invalid date";
|
: "Invalid date";
|
||||||
|
|
||||||
// Timestamp tooltips always use full date regardless of the configured format
|
var formattedLong = timestamp.Instant?.ToLocalString("dddd, MMMM d, yyyy h:mm tt") ?? "";
|
||||||
var longDateString = timestamp.Date is not null
|
|
||||||
? timestamp.Date.Value.ToLocalString("dddd, MMMM d, yyyy h:mm tt")
|
|
||||||
: "Invalid date";
|
|
||||||
|
|
||||||
_buffer
|
_buffer.Append(
|
||||||
.Append($"<span class=\"chatlog__markdown-timestamp\" title=\"{HtmlEncode(longDateString)}\">")
|
// language=HTML
|
||||||
.Append(HtmlEncode(dateString))
|
$"""
|
||||||
.Append("</span>");
|
<span class="chatlog__markdown-timestamp" title="{HtmlEncode(formattedLong)}">{HtmlEncode(formatted)}</span>
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
|
||||||
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));
|
nodes.All(n => n is EmojiNode || n is TextNode textNode && string.IsNullOrWhiteSpace(textNode.Text));
|
||||||
|
|
||||||
var buffer = new StringBuilder();
|
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();
|
return buffer.ToString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,8 @@
|
||||||
ValueTask<string> ResolveAssetUrlAsync(string url) =>
|
ValueTask<string> ResolveAssetUrlAsync(string url) =>
|
||||||
ExportContext.ResolveAssetUrlAsync(url, CancellationToken);
|
ExportContext.ResolveAssetUrlAsync(url, CancellationToken);
|
||||||
|
|
||||||
string FormatDate(DateTimeOffset date) =>
|
string FormatDate(DateTimeOffset instant) =>
|
||||||
ExportContext.FormatDate(date);
|
ExportContext.FormatDate(instant);
|
||||||
|
|
||||||
ValueTask<string> FormatMarkdownAsync(string markdown) =>
|
ValueTask<string> FormatMarkdownAsync(string markdown) =>
|
||||||
HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown, true, CancellationToken);
|
HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown, true, CancellationToken);
|
||||||
|
|
|
@ -66,7 +66,7 @@ internal partial class PlainTextMarkdownVisitor : MarkdownVisitor
|
||||||
_buffer.Append($"#{name}");
|
_buffer.Append($"#{name}");
|
||||||
|
|
||||||
// Voice channel marker
|
// Voice channel marker
|
||||||
if (channel?.SupportsVoice == true)
|
if (channel?.IsVoice == true)
|
||||||
_buffer.Append(" [voice]");
|
_buffer.Append(" [voice]");
|
||||||
}
|
}
|
||||||
else if (mention.Kind == MentionKind.Role)
|
else if (mention.Kind == MentionKind.Role)
|
||||||
|
@ -80,17 +80,19 @@ internal partial class PlainTextMarkdownVisitor : MarkdownVisitor
|
||||||
return await base.VisitMentionAsync(mention, cancellationToken);
|
return await base.VisitMentionAsync(mention, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async ValueTask<MarkdownNode> VisitUnixTimestampAsync(
|
protected override async ValueTask<MarkdownNode> VisitTimestampAsync(
|
||||||
UnixTimestampNode timestamp,
|
TimestampNode timestamp,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
_buffer.Append(
|
_buffer.Append(
|
||||||
timestamp.Date is not null
|
timestamp.Instant is not null
|
||||||
? _context.FormatDate(timestamp.Date.Value)
|
? !string.IsNullOrWhiteSpace(timestamp.Format)
|
||||||
|
? timestamp.Instant.Value.ToLocalString(timestamp.Format)
|
||||||
|
: _context.FormatDate(timestamp.Instant.Value)
|
||||||
: "Invalid date"
|
: "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)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var nodes = MarkdownParser.ParseMinimal(markdown);
|
var nodes = MarkdownParser.ParseMinimal(markdown);
|
||||||
var buffer = new StringBuilder();
|
|
||||||
|
|
||||||
await new PlainTextMarkdownVisitor(context, buffer)
|
var buffer = new StringBuilder();
|
||||||
.VisitAsync(nodes, cancellationToken);
|
await new PlainTextMarkdownVisitor(context, buffer).VisitAsync(nodes, cancellationToken);
|
||||||
|
|
||||||
return buffer.ToString();
|
return buffer.ToString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,8 @@
|
||||||
ValueTask<string> ResolveAssetUrlAsync(string url) =>
|
ValueTask<string> ResolveAssetUrlAsync(string url) =>
|
||||||
ExportContext.ResolveAssetUrlAsync(url, CancellationToken);
|
ExportContext.ResolveAssetUrlAsync(url, CancellationToken);
|
||||||
|
|
||||||
string FormatDate(DateTimeOffset date) =>
|
string FormatDate(DateTimeOffset instant) =>
|
||||||
ExportContext.FormatDate(date);
|
ExportContext.FormatDate(instant);
|
||||||
|
|
||||||
ValueTask<string> FormatMarkdownAsync(string markdown) =>
|
ValueTask<string> FormatMarkdownAsync(string markdown) =>
|
||||||
HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown, true, CancellationToken);
|
HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown, true, CancellationToken);
|
||||||
|
@ -660,6 +660,7 @@
|
||||||
|
|
||||||
.chatlog__markdown-spoiler {
|
.chatlog__markdown-spoiler {
|
||||||
background-color: @Themed("rgba(255, 255, 255, 0.1)", "rgba(0, 0, 0, 0.1)");
|
background-color: @Themed("rgba(255, 255, 255, 0.1)", "rgba(0, 0, 0, 0.1)");
|
||||||
|
padding: 0 2px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -728,9 +729,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.chatlog__markdown-timestamp {
|
.chatlog__markdown-timestamp {
|
||||||
border-radius: 3px;
|
background-color: @Themed("rgba(255, 255, 255, 0.1)", "rgba(0, 0, 0, 0.1)");
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
color: @Themed("#a3a6aa", "#5e6772");
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chatlog__emoji {
|
.chatlog__emoji {
|
||||||
|
|
|
@ -275,29 +275,37 @@ internal static partial class MarkdownParser
|
||||||
|
|
||||||
/* Misc */
|
/* 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>
|
// Capture <t:12345678> or <t:12345678:R>
|
||||||
new Regex(@"<t:(-?\d+)(?::\w)?>", DefaultRegexOptions),
|
new Regex(@"<t:(-?\d+)(?::(\w))?>", DefaultRegexOptions),
|
||||||
(_, m) =>
|
(_, 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
|
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/681
|
||||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/766
|
// 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,
|
CodedStandardEmojiNodeMatcher,
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
UnixTimestampNodeMatcher
|
TimestampNodeMatcher
|
||||||
);
|
);
|
||||||
|
|
||||||
// Minimal set of matchers for non-multimedia formats (e.g. plain text)
|
// Minimal set of matchers for non-multimedia formats (e.g. plain text)
|
||||||
|
@ -362,7 +370,7 @@ internal static partial class MarkdownParser
|
||||||
CustomEmojiNodeMatcher,
|
CustomEmojiNodeMatcher,
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
UnixTimestampNodeMatcher
|
TimestampNodeMatcher
|
||||||
);
|
);
|
||||||
|
|
||||||
private static IReadOnlyList<MarkdownNode> Parse(StringSegment segment, IMatcher<MarkdownNode> matcher) =>
|
private static IReadOnlyList<MarkdownNode> Parse(StringSegment segment, IMatcher<MarkdownNode> matcher) =>
|
||||||
|
|
|
@ -48,8 +48,8 @@ internal abstract class MarkdownVisitor
|
||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default) =>
|
||||||
new(mention);
|
new(mention);
|
||||||
|
|
||||||
protected virtual ValueTask<MarkdownNode> VisitUnixTimestampAsync(
|
protected virtual ValueTask<MarkdownNode> VisitTimestampAsync(
|
||||||
UnixTimestampNode timestamp,
|
TimestampNode timestamp,
|
||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default) =>
|
||||||
new(timestamp);
|
new(timestamp);
|
||||||
|
|
||||||
|
@ -78,8 +78,8 @@ internal abstract class MarkdownVisitor
|
||||||
MentionNode mention =>
|
MentionNode mention =>
|
||||||
await VisitMentionAsync(mention, cancellationToken),
|
await VisitMentionAsync(mention, cancellationToken),
|
||||||
|
|
||||||
UnixTimestampNode timestamp =>
|
TimestampNode timestamp =>
|
||||||
await VisitUnixTimestampAsync(timestamp, cancellationToken),
|
await VisitTimestampAsync(timestamp, cancellationToken),
|
||||||
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(node))
|
_ => throw new ArgumentOutOfRangeException(nameof(node))
|
||||||
};
|
};
|
||||||
|
|
9
DiscordChatExporter.Core/Markdown/TimestampNode.cs
Normal file
9
DiscordChatExporter.Core/Markdown/TimestampNode.cs
Normal 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);
|
||||||
|
}
|
|
@ -1,6 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Markdown;
|
|
||||||
|
|
||||||
// Null date means invalid timestamp
|
|
||||||
internal record UnixTimestampNode(DateTimeOffset? Date) : MarkdownNode;
|
|
|
@ -5,6 +5,6 @@ namespace DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
|
||||||
public static class DateExtensions
|
public static class DateExtensions
|
||||||
{
|
{
|
||||||
public static string ToLocalString(this DateTimeOffset dateTime, string format) =>
|
public static string ToLocalString(this DateTimeOffset instant, string format) =>
|
||||||
dateTime.ToLocalTime().ToString(format, CultureInfo.InvariantCulture);
|
instant.ToLocalTime().ToString(format, CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
|
@ -358,10 +358,10 @@
|
||||||
<materialDesign:PackIcon.Style>
|
<materialDesign:PackIcon.Style>
|
||||||
<Style TargetType="{x:Type materialDesign:PackIcon}">
|
<Style TargetType="{x:Type materialDesign:PackIcon}">
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
<DataTrigger Binding="{Binding SupportsVoice}" Value="True">
|
<DataTrigger Binding="{Binding IsVoice}" Value="True">
|
||||||
<Setter Property="Kind" Value="VolumeHigh" />
|
<Setter Property="Kind" Value="VolumeHigh" />
|
||||||
</DataTrigger>
|
</DataTrigger>
|
||||||
<DataTrigger Binding="{Binding SupportsVoice}" Value="False">
|
<DataTrigger Binding="{Binding IsVoice}" Value="False">
|
||||||
<Setter Property="Kind" Value="Pound" />
|
<Setter Property="Kind" Value="Pound" />
|
||||||
</DataTrigger>
|
</DataTrigger>
|
||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue