mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-08-13 14:03:01 -06:00
Fix markdown heading parsing at end of input
This commit is contained in:
parent
05f8df51e9
commit
0f2991cef2
137
DiscordChatExporter.Cli.Tests/Specs/HtmlMarkdownHeadingSpecs.cs
Normal file
137
DiscordChatExporter.Cli.Tests/Specs/HtmlMarkdownHeadingSpecs.cs
Normal file
|
|
@ -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<string> 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", "<h1>Heading</h1>")]
|
||||||
|
[InlineData("# Heading\nbody", "<h1>Heading</h1>body")]
|
||||||
|
[InlineData("# Heading\r\nbody", "<h1>Heading</h1>body")]
|
||||||
|
[InlineData("## Heading", "<h2>Heading</h2>")]
|
||||||
|
[InlineData("### Heading", "<h3>Heading</h3>")]
|
||||||
|
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("<h");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task I_can_escape_html_sensitive_text_inside_markdown_headings()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var html = await FormatMarkdownAsync("# <Heading> & \"quote\"");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
html.Should().Be("<h1><Heading> & "quote"</h1>");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task I_keep_markdown_headings_literal_when_formatting_is_disabled()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = CreateExportContext(false);
|
||||||
|
var message = CreateMessage("# <Heading> & \"quote\"");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var html = await new MessageGroupTemplate
|
||||||
|
{
|
||||||
|
Context = context,
|
||||||
|
Messages = [message],
|
||||||
|
}.RenderAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
html.Should().Contain("# <Heading> &");
|
||||||
|
html.Should().NotContain("# <Heading>");
|
||||||
|
html.Should().NotContain("<h1>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -131,8 +131,8 @@ internal static partial class MarkdownParser
|
||||||
|
|
||||||
private static readonly IMatcher<MarkdownContext, MarkdownNode> HeadingNodeMatcher =
|
private static readonly IMatcher<MarkdownContext, MarkdownNode> HeadingNodeMatcher =
|
||||||
new RegexMatcher<MarkdownContext, MarkdownNode>(
|
new RegexMatcher<MarkdownContext, MarkdownNode>(
|
||||||
// Consume the linebreak so that it's not attached to following nodes.
|
// Consume the linebreak, if present, so that it's not attached to following nodes.
|
||||||
new Regex(@"^(\#{1,3})\s(.+)\n", DefaultRegexOptions),
|
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])))
|
(c, s, m) => new HeadingNode(m.Groups[1].Length, Parse(c, s.Relocate(m.Groups[2])))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
3
DiscordChatExporter.Core/Properties/AssemblyInfo.cs
Normal file
3
DiscordChatExporter.Core/Properties/AssemblyInfo.cs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("DiscordChatExporter.Cli.Tests")]
|
||||||
Loading…
Reference in a new issue