diff --git a/DiscordChatExporter.Cli.Tests/Specs/HtmlMarkdownHeadingSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/HtmlMarkdownHeadingSpecs.cs new file mode 100644 index 00000000..58b1acb4 --- /dev/null +++ b/DiscordChatExporter.Cli.Tests/Specs/HtmlMarkdownHeadingSpecs.cs @@ -0,0 +1,137 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using DiscordChatExporter.Core.Discord; +using DiscordChatExporter.Core.Discord.Data; +using DiscordChatExporter.Core.Exporting; +using DiscordChatExporter.Core.Exporting.Filtering; +using DiscordChatExporter.Core.Exporting.Partitioning; +using FluentAssertions; +using Xunit; + +namespace DiscordChatExporter.Cli.Tests.Specs; + +public class HtmlMarkdownHeadingSpecs +{ + private static async ValueTask FormatMarkdownAsync(string markdown) => + await HtmlMarkdownVisitor.FormatAsync(null!, markdown); + + private static ExportContext CreateExportContext(bool shouldFormatMarkdown) + { + var guild = Guild.DirectMessages; + var channel = new Channel( + new Snowflake(1), + ChannelKind.DirectTextChat, + guild.Id, + null, + "test", + null, + null, + null, + false, + null + ); + + var request = new ExportRequest( + guild, + channel, + Path.Combine(Path.GetTempPath(), "DiscordChatExporter.Tests", "test.html"), + null, + ExportFormat.HtmlDark, + null, + null, + PartitionLimit.Null, + MessageFilter.Null, + false, + shouldFormatMarkdown, + false, + true, + "en-US", + true + ); + + return new ExportContext(new DiscordClient(""), request); + } + + private static Message CreateMessage(string content) => + new( + new Snowflake(2), + MessageKind.Default, + MessageFlags.None, + new User(new Snowflake(3), false, 1234, "test-user", "Test User", ""), + DateTimeOffset.Parse("2024-01-01T00:00:00+00:00"), + null, + null, + false, + content, + [], + [], + [], + [], + [], + null, + null, + null, + null + ); + + [Theory] + [InlineData("# Heading", "

Heading

")] + [InlineData("# Heading\nbody", "

Heading

body")] + [InlineData("# Heading\r\nbody", "

Heading

body")] + [InlineData("## Heading", "

Heading

")] + [InlineData("### Heading", "

Heading

")] + public async Task I_can_render_markdown_headings_as_html(string markdown, string expectedHtml) + { + // Act + var html = await FormatMarkdownAsync(markdown); + + // Assert + html.Should().Be(expectedHtml); + } + + [Theory] + [InlineData("#hashtag")] + [InlineData("#")] + [InlineData("#\nHeading")] + [InlineData("#\r\nHeading")] + public async Task I_do_not_render_non_heading_hash_text_as_html_heading(string markdown) + { + // Act + var html = await FormatMarkdownAsync(markdown); + + // Assert + html.Should().Be(markdown); + html.Should().NotContain(" & \"quote\""); + + // Assert + html.Should().Be("

<Heading> & "quote"

"); + } + + [Fact] + public async Task I_keep_markdown_headings_literal_when_formatting_is_disabled() + { + // Arrange + var context = CreateExportContext(false); + var message = CreateMessage("# & \"quote\""); + + // Act + var html = await new MessageGroupTemplate + { + Context = context, + Messages = [message], + }.RenderAsync(); + + // Assert + html.Should().Contain("# <Heading> &"); + html.Should().NotContain("# "); + html.Should().NotContain("

"); + } +} diff --git a/DiscordChatExporter.Core/Markdown/Parsing/MarkdownParser.cs b/DiscordChatExporter.Core/Markdown/Parsing/MarkdownParser.cs index f0fb1a17..fbe0f784 100644 --- a/DiscordChatExporter.Core/Markdown/Parsing/MarkdownParser.cs +++ b/DiscordChatExporter.Core/Markdown/Parsing/MarkdownParser.cs @@ -131,8 +131,8 @@ internal static partial class MarkdownParser private static readonly IMatcher HeadingNodeMatcher = new RegexMatcher( - // Consume the linebreak so that it's not attached to following nodes. - new Regex(@"^(\#{1,3})\s(.+)\n", DefaultRegexOptions), + // Consume the linebreak, if present, so that it's not attached to following nodes. + new Regex(@"^(\#{1,3})[^\S\r\n]+([^\r\n]+)(?:\r?\n|$)", DefaultRegexOptions), (c, s, m) => new HeadingNode(m.Groups[1].Length, Parse(c, s.Relocate(m.Groups[2]))) ); diff --git a/DiscordChatExporter.Core/Properties/AssemblyInfo.cs b/DiscordChatExporter.Core/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..a9157098 --- /dev/null +++ b/DiscordChatExporter.Core/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DiscordChatExporter.Cli.Tests")]