Files
DiscordChatExporter/DiscordChatExporter.Gui/Converters/MarkdownToInlinesConverter.cs
2026-02-27 18:31:40 +02:00

169 lines
5.2 KiB
C#

using System;
using System.Globalization;
using System.Linq;
using Avalonia.Controls.Documents;
using Avalonia.Data.Converters;
using Avalonia.Media;
using DiscordChatExporter.Gui.Utils.Extensions;
using DiscordChatExporter.Gui.Views.Controls;
using Markdig;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using MarkdownInline = Markdig.Syntax.Inlines.Inline;
namespace DiscordChatExporter.Gui.Converters;
public class MarkdownToInlinesConverter : IValueConverter
{
public static readonly MarkdownToInlinesConverter Instance = new();
private static readonly MarkdownPipeline MarkdownPipeline = new MarkdownPipelineBuilder()
.UseEmphasisExtras()
.Build();
private static void ProcessInline(
InlineCollection inlines,
MarkdownInline markdownInline,
FontWeight? fontWeight = null,
FontStyle? fontStyle = null,
TextDecorationCollection? textDecorations = null
)
{
switch (markdownInline)
{
case LiteralInline literal:
{
var run = new Run(literal.Content.ToString());
if (fontWeight is not null)
run.FontWeight = fontWeight.Value;
if (fontStyle is not null)
run.FontStyle = fontStyle.Value;
if (textDecorations is not null)
run.TextDecorations = textDecorations;
inlines.Add(run);
break;
}
case LineBreakInline:
{
inlines.Add(new LineBreak());
break;
}
case EmphasisInline emphasis:
{
var newWeight = fontWeight;
var newStyle = fontStyle;
var newDecorations = textDecorations;
switch (emphasis.DelimiterChar)
{
case '*' or '_' when emphasis.DelimiterCount == 2:
newWeight = FontWeight.SemiBold;
break;
case '*' or '_':
newStyle = FontStyle.Italic;
break;
case '~':
newDecorations = TextDecorations.Strikethrough;
break;
case '+':
newDecorations = TextDecorations.Underline;
break;
}
foreach (var child in emphasis)
ProcessInline(inlines, child, newWeight, newStyle, newDecorations);
break;
}
case LinkInline link:
{
inlines.Add(new HyperLink { Text = link.GetInnerText(), Url = link.Url });
break;
}
case ContainerInline container:
{
foreach (var child in container)
ProcessInline(inlines, child, fontWeight, fontStyle, textDecorations);
break;
}
}
}
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
var inlines = new InlineCollection();
if (value is not string { Length: > 0 } text)
return inlines;
var isFirst = true;
foreach (var block in Markdown.Parse(text, MarkdownPipeline))
{
switch (block)
{
case ParagraphBlock { Inline: not null } paragraph:
{
if (!isFirst)
{
// Insert a blank line between paragraphs
inlines.Add(new LineBreak());
inlines.Add(new LineBreak());
}
isFirst = false;
foreach (var markdownInline in paragraph.Inline!)
ProcessInline(inlines, markdownInline);
break;
}
case ListBlock list:
{
var itemOrder = 1;
if (list.IsOrdered && int.TryParse(list.OrderedStart, out var startNum))
itemOrder = startNum;
foreach (var listItem in list.OfType<ListItemBlock>())
{
if (!isFirst)
inlines.Add(new LineBreak());
isFirst = false;
var prefix = list.IsOrdered ? $"{itemOrder++}. " : $"{list.BulletType} ";
inlines.Add(new Run(prefix));
foreach (var subBlock in listItem.OfType<ParagraphBlock>())
{
if (subBlock is { Inline: not null })
{
foreach (var markdownInline in subBlock.Inline)
ProcessInline(inlines, markdownInline);
}
}
}
break;
}
}
}
return inlines;
}
public object? ConvertBack(
object? value,
Type targetType,
object? parameter,
CultureInfo culture
) => throw new NotSupportedException();
}