mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2025-06-08 10:22:25 -04:00
Add support for headers in markdown
This commit is contained in:
parent
d8315c7827
commit
469a731892
5 changed files with 86 additions and 4 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
8
DiscordChatExporter.Core/Markdown/HeaderNode.cs
Normal file
8
DiscordChatExporter.Core/Markdown/HeaderNode.cs
Normal file
|
@ -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…
Add table
Add a link
Reference in a new issue