Add support for headers in markdown

pull/1037/head
Tyrrrz 1 year ago
parent d8315c7827
commit 469a731892

@ -91,6 +91,25 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
return result; return result;
} }
protected override async ValueTask<MarkdownNode> VisitHeaderAsync(
HeaderNode header,
CancellationToken cancellationToken = default)
{
_buffer.Append(
// lang=html
$"<h{header.Level}>"
);
var result = await base.VisitHeaderAsync(header, cancellationToken);
_buffer.Append(
// lang=html
$"</h{header.Level}>"
);
return result;
}
protected override async ValueTask<MarkdownNode> VisitInlineCodeBlockAsync( protected override async ValueTask<MarkdownNode> VisitInlineCodeBlockAsync(
InlineCodeBlockNode inlineCodeBlock, InlineCodeBlockNode inlineCodeBlock,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)

@ -758,6 +758,37 @@
overflow-wrap: break-word; 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 { .chatlog__markdown-preserve {
white-space: pre-wrap; white-space: pre-wrap;
} }

@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace DiscordChatExporter.Core.Markdown;
internal record HeaderNode(
int Level,
IReadOnlyList<MarkdownNode> Children
) : MarkdownNode, IContainerNode;

@ -81,15 +81,15 @@ internal static partial class MarkdownParser
private static readonly IMatcher<MarkdownNode> SingleLineQuoteNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> SingleLineQuoteNodeMatcher = new RegexMatcher<MarkdownNode>(
// Capture any character until the end of the line. // Capture any character until the end of the line.
// Opening 'greater than' character must be followed by whitespace. // Opening 'greater than' character must be followed by whitespace.
// Text content is optional. // Consume the newline character so that it's not included in the content.
new Regex(@"^>\s(.*\n?)", DefaultRegexOptions), new Regex(@"^>\s(.+\n?)", DefaultRegexOptions),
(s, m) => new FormattingNode(FormattingKind.Quote, Parse(s.Relocate(m.Groups[1]))) (s, m) => new FormattingNode(FormattingKind.Quote, Parse(s.Relocate(m.Groups[1])))
); );
private static readonly IMatcher<MarkdownNode> RepeatedSingleLineQuoteNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> RepeatedSingleLineQuoteNodeMatcher = new RegexMatcher<MarkdownNode>(
// Repeatedly capture any character until the end of the line. // 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. // Consume the newline character so that it's not included in the content.
new Regex(@"(?:^>\s(.*\n?)){2,}", DefaultRegexOptions), new Regex(@"(?:^>\s(.+\n?)){2,}", DefaultRegexOptions),
(_, m) => new FormattingNode( (_, m) => new FormattingNode(
FormattingKind.Quote, FormattingKind.Quote,
Parse( Parse(
@ -106,6 +106,16 @@ internal static partial class MarkdownParser
(s, m) => new FormattingNode(FormattingKind.Quote, Parse(s.Relocate(m.Groups[1]))) (s, m) => new FormattingNode(FormattingKind.Quote, Parse(s.Relocate(m.Groups[1])))
); );
/* Headers */
private static readonly IMatcher<MarkdownNode> HeaderNodeMatcher = new RegexMatcher<MarkdownNode>(
// 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 */ /* Code blocks */
private static readonly IMatcher<MarkdownNode> InlineCodeBlockNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> InlineCodeBlockNodeMatcher = new RegexMatcher<MarkdownNode>(
@ -330,6 +340,9 @@ internal static partial class MarkdownParser
RepeatedSingleLineQuoteNodeMatcher, RepeatedSingleLineQuoteNodeMatcher,
SingleLineQuoteNodeMatcher, SingleLineQuoteNodeMatcher,
// Headers
HeaderNodeMatcher,
// Code blocks // Code blocks
MultiLineCodeBlockNodeMatcher, MultiLineCodeBlockNodeMatcher,
InlineCodeBlockNodeMatcher, InlineCodeBlockNodeMatcher,

@ -20,6 +20,14 @@ internal abstract class MarkdownVisitor
return formatting; return formatting;
} }
protected virtual async ValueTask<MarkdownNode> VisitHeaderAsync(
HeaderNode header,
CancellationToken cancellationToken = default)
{
await VisitAsync(header.Children, cancellationToken);
return header;
}
protected virtual ValueTask<MarkdownNode> VisitInlineCodeBlockAsync( protected virtual ValueTask<MarkdownNode> VisitInlineCodeBlockAsync(
InlineCodeBlockNode inlineCodeBlock, InlineCodeBlockNode inlineCodeBlock,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
@ -63,6 +71,9 @@ internal abstract class MarkdownVisitor
FormattingNode formatting => FormattingNode formatting =>
await VisitFormattingAsync(formatting, cancellationToken), await VisitFormattingAsync(formatting, cancellationToken),
HeaderNode header =>
await VisitHeaderAsync(header, cancellationToken),
InlineCodeBlockNode inlineCodeBlock => InlineCodeBlockNode inlineCodeBlock =>
await VisitInlineCodeBlockAsync(inlineCodeBlock, cancellationToken), await VisitInlineCodeBlockAsync(inlineCodeBlock, cancellationToken),

Loading…
Cancel
Save