From 469a731892ac6a4b5f8f2e396ec8eb0e2be32d8b Mon Sep 17 00:00:00 2001 From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Date: Mon, 22 May 2023 11:05:14 +0300 Subject: [PATCH] Add support for headers in markdown --- .../Exporting/HtmlMarkdownVisitor.cs | 19 ++++++++++++ .../Exporting/PreambleTemplate.cshtml | 31 +++++++++++++++++++ .../Markdown/HeaderNode.cs | 8 +++++ .../Markdown/Parsing/MarkdownParser.cs | 21 ++++++++++--- .../Markdown/Parsing/MarkdownVisitor.cs | 11 +++++++ 5 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 DiscordChatExporter.Core/Markdown/HeaderNode.cs diff --git a/DiscordChatExporter.Core/Exporting/HtmlMarkdownVisitor.cs b/DiscordChatExporter.Core/Exporting/HtmlMarkdownVisitor.cs index 63b1266..4c55d2e 100644 --- a/DiscordChatExporter.Core/Exporting/HtmlMarkdownVisitor.cs +++ b/DiscordChatExporter.Core/Exporting/HtmlMarkdownVisitor.cs @@ -91,6 +91,25 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor return result; } + protected override async ValueTask VisitHeaderAsync( + HeaderNode header, + CancellationToken cancellationToken = default) + { + _buffer.Append( + // lang=html + $"" + ); + + var result = await base.VisitHeaderAsync(header, cancellationToken); + + _buffer.Append( + // lang=html + $"" + ); + + return result; + } + protected override async ValueTask VisitInlineCodeBlockAsync( InlineCodeBlockNode inlineCodeBlock, CancellationToken cancellationToken = default) diff --git a/DiscordChatExporter.Core/Exporting/PreambleTemplate.cshtml b/DiscordChatExporter.Core/Exporting/PreambleTemplate.cshtml index 868c4ab..ad3a947 100644 --- a/DiscordChatExporter.Core/Exporting/PreambleTemplate.cshtml +++ b/DiscordChatExporter.Core/Exporting/PreambleTemplate.cshtml @@ -758,6 +758,37 @@ overflow-wrap: break-word; } + .chatlog__markdown h1 { + margin-block: 0; + margin-top: 1rem; + margin-bottom: 0.5rem; + color: @Themed("#f2f3f5", "#060607"); + font-size: 1.5rem; + line-height: 1; + } + + .chatlog__markdown h2 { + margin-block: 0; + margin-top: 1rem; + margin-bottom: 0.5rem; + color: @Themed("#f2f3f5", "#060607"); + font-size: 1.25rem; + line-height: 1; + } + + .chatlog__markdown h3 { + margin-block: 0; + margin-top: 1rem; + margin-bottom: 0.5rem; + color: @Themed("#f2f3f5", "#060607"); + font-size: 1rem; + line-height: 1; + } + + .chatlog__markdown h1:first-child, h2:first-child, h3:first-child { + margin-top: 0.5rem; + } + .chatlog__markdown-preserve { white-space: pre-wrap; } diff --git a/DiscordChatExporter.Core/Markdown/HeaderNode.cs b/DiscordChatExporter.Core/Markdown/HeaderNode.cs new file mode 100644 index 0000000..131860f --- /dev/null +++ b/DiscordChatExporter.Core/Markdown/HeaderNode.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace DiscordChatExporter.Core.Markdown; + +internal record HeaderNode( + int Level, + IReadOnlyList Children +) : MarkdownNode, IContainerNode; \ No newline at end of file diff --git a/DiscordChatExporter.Core/Markdown/Parsing/MarkdownParser.cs b/DiscordChatExporter.Core/Markdown/Parsing/MarkdownParser.cs index 605e8ef..c7f7a56 100644 --- a/DiscordChatExporter.Core/Markdown/Parsing/MarkdownParser.cs +++ b/DiscordChatExporter.Core/Markdown/Parsing/MarkdownParser.cs @@ -81,15 +81,15 @@ internal static partial class MarkdownParser private static readonly IMatcher SingleLineQuoteNodeMatcher = new RegexMatcher( // Capture any character until the end of the line. // Opening 'greater than' character must be followed by whitespace. - // Text content is optional. - new Regex(@"^>\s(.*\n?)", DefaultRegexOptions), + // Consume the newline character so that it's not included in the content. + new Regex(@"^>\s(.+\n?)", DefaultRegexOptions), (s, m) => new FormattingNode(FormattingKind.Quote, Parse(s.Relocate(m.Groups[1]))) ); private static readonly IMatcher RepeatedSingleLineQuoteNodeMatcher = new RegexMatcher( // Repeatedly capture any character until the end of the line. - // This one is tricky as it ends up producing multiple separate captures which need to be joined. - new Regex(@"(?:^>\s(.*\n?)){2,}", DefaultRegexOptions), + // Consume the newline character so that it's not included in the content. + new Regex(@"(?:^>\s(.+\n?)){2,}", DefaultRegexOptions), (_, m) => new FormattingNode( FormattingKind.Quote, Parse( @@ -106,6 +106,16 @@ internal static partial class MarkdownParser (s, m) => new FormattingNode(FormattingKind.Quote, Parse(s.Relocate(m.Groups[1]))) ); + /* Headers */ + + private static readonly IMatcher HeaderNodeMatcher = new RegexMatcher( + // Capture any character until the end of the line. + // Opening 'hash' character(s) must be followed by whitespace. + // Consume the newline character so that it's not included in the content. + new Regex(@"^(\#{1,3})\s(.+\n?)", DefaultRegexOptions), + (s, m) => new HeaderNode(m.Groups[1].Length, Parse(s.Relocate(m.Groups[2]))) + ); + /* Code blocks */ private static readonly IMatcher InlineCodeBlockNodeMatcher = new RegexMatcher( @@ -330,6 +340,9 @@ internal static partial class MarkdownParser RepeatedSingleLineQuoteNodeMatcher, SingleLineQuoteNodeMatcher, + // Headers + HeaderNodeMatcher, + // Code blocks MultiLineCodeBlockNodeMatcher, InlineCodeBlockNodeMatcher, diff --git a/DiscordChatExporter.Core/Markdown/Parsing/MarkdownVisitor.cs b/DiscordChatExporter.Core/Markdown/Parsing/MarkdownVisitor.cs index 36e17c5..de2fb3f 100644 --- a/DiscordChatExporter.Core/Markdown/Parsing/MarkdownVisitor.cs +++ b/DiscordChatExporter.Core/Markdown/Parsing/MarkdownVisitor.cs @@ -20,6 +20,14 @@ internal abstract class MarkdownVisitor return formatting; } + protected virtual async ValueTask VisitHeaderAsync( + HeaderNode header, + CancellationToken cancellationToken = default) + { + await VisitAsync(header.Children, cancellationToken); + return header; + } + protected virtual ValueTask VisitInlineCodeBlockAsync( InlineCodeBlockNode inlineCodeBlock, CancellationToken cancellationToken = default) => @@ -63,6 +71,9 @@ internal abstract class MarkdownVisitor FormattingNode formatting => await VisitFormattingAsync(formatting, cancellationToken), + HeaderNode header => + await VisitHeaderAsync(header, cancellationToken), + InlineCodeBlockNode inlineCodeBlock => await VisitInlineCodeBlockAsync(inlineCodeBlock, cancellationToken),