using System; using System.Collections.Generic; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Documents; using Avalonia.Layout; using Avalonia.Media; using Markdig; using Markdig.Syntax; using Markdig.Syntax.Inlines; using AvaloniaInline = Avalonia.Controls.Documents.Inline; using MarkdigInline = Markdig.Syntax.Inlines.Inline; namespace LANCommander.Launcher.Controls; /// /// A panel that renders a markdown string as native Avalonia controls. /// Handles paragraphs, headings, code blocks, bold, italic, inline code, and line breaks. /// public class MarkdownTextBlock : StackPanel { public static readonly StyledProperty TextProperty = AvaloniaProperty.Register(nameof(Text)); public static readonly StyledProperty FontSizeProperty = AvaloniaProperty.Register(nameof(FontSize), 14.0); private static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder() .UseEmphasisExtras() .UseAutoLinks() .Build(); public string? Text { get => GetValue(TextProperty); set => SetValue(TextProperty, value); } public double FontSize { get => GetValue(FontSizeProperty); set => SetValue(FontSizeProperty, value); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == TextProperty || change.Property == FontSizeProperty) Rebuild(); } private void Rebuild() { Children.Clear(); var markdown = Text; if (string.IsNullOrEmpty(markdown)) return; var doc = Markdown.Parse(markdown, Pipeline); foreach (var block in doc) { var element = RenderBlock(block); if (element != null) Children.Add(element); } } private Control? RenderBlock(Block block) { return block switch { HeadingBlock heading => RenderHeading(heading), ParagraphBlock para => RenderParagraph(para), FencedCodeBlock fenced => RenderCodeBlock(fenced.Lines.ToString()), CodeBlock code => RenderCodeBlock(code.Lines.ToString()), ListBlock list => RenderList(list), ThematicBreakBlock => RenderThematicBreak(), QuoteBlock quote => RenderQuote(quote), _ => null, }; } private Control RenderHeading(HeadingBlock heading) { var tb = new TextBlock { TextWrapping = TextWrapping.Wrap, FontSize = heading.Level switch { 1 => FontSize * Scale("MarkdownHeading1Scale", 1.6), 2 => FontSize * Scale("MarkdownHeading2Scale", 1.4), 3 => FontSize * Scale("MarkdownHeading3Scale", 1.2), _ => FontSize * Scale("MarkdownHeadingScale", 1.1), }, FontWeight = FontWeight.Bold, Margin = Space("MarkdownHeadingMargin", new Thickness(0, 4, 0, 2)), }; if (heading.Inline != null) foreach (var inline in RenderInlines(heading.Inline)) tb.Inlines!.Add(inline); return tb; } private Control RenderParagraph(ParagraphBlock para) { var tb = new TextBlock { TextWrapping = TextWrapping.Wrap, FontSize = FontSize, LineHeight = FontSize * Scale("MarkdownLineHeightScale", 1.4), Margin = Space("MarkdownParagraphMargin", new Thickness(0, 1, 0, 1)), }; if (para.Inline != null) foreach (var inline in RenderInlines(para.Inline)) tb.Inlines!.Add(inline); return tb; } private Control RenderCodeBlock(string code) { var text = new TextBlock { Text = code.TrimEnd('\n', '\r'), FontSize = FontSize * Scale("MarkdownCodeFontScale", 0.9), TextWrapping = TextWrapping.Wrap, }; text.Bind(TextBlock.FontFamilyProperty, this.GetResourceObservable("CodeFontFamily")); var border = new Border { CornerRadius = Corner("MarkdownCodeCornerRadius", new CornerRadius(4)), Padding = Space("MarkdownCodeBlockPadding", new Thickness(10, 6)), Margin = Space("MarkdownCodeBlockMargin", new Thickness(0, 3, 0, 3)), Child = text, }; border.Bind(Border.BackgroundProperty, this.GetResourceObservable("CodeSurfaceBrush")); return border; } private Control RenderList(ListBlock list) { var panel = new StackPanel { Spacing = 2, Margin = new Thickness(0, 2, 0, 2) }; var index = 1; foreach (var item in list) { if (item is not ListItemBlock listItem) continue; var bullet = list.IsOrdered ? $"{index++}." : "•"; var row = new Grid { ColumnDefinitions = new ColumnDefinitions("Auto,*"), }; row.Children.Add(new TextBlock { Text = bullet, FontSize = FontSize, Margin = new Thickness(8, 0, 6, 0), VerticalAlignment = VerticalAlignment.Top, }); var content = new StackPanel { Spacing = 2 }; Grid.SetColumn(content, 1); foreach (var block in listItem) { var element = RenderBlock(block); if (element != null) content.Children.Add(element); } row.Children.Add(content); panel.Children.Add(row); } return panel; } private Control RenderThematicBreak() { var border = new Border { Height = 1, Margin = Space("MarkdownRuleMargin", new Thickness(0, 6, 0, 6)), }; border.Bind(Border.BackgroundProperty, this.GetResourceObservable("InputBorderBrush")); return border; } private Control RenderQuote(QuoteBlock quote) { var inner = new StackPanel { Spacing = 2 }; foreach (var block in quote) { var element = RenderBlock(block); if (element != null) inner.Children.Add(element); } var border = new Border { BorderThickness = new Thickness(3, 0, 0, 0), Padding = Space("MarkdownQuotePadding", new Thickness(8, 4)), Margin = Space("MarkdownQuoteMargin", new Thickness(0, 3, 0, 3)), Child = inner, }; border.Bind(Border.BorderBrushProperty, this.GetResourceObservable("PrimaryBrush")); return border; } private IEnumerable RenderInlines(ContainerInline container) { foreach (var inline in container) { foreach (var result in RenderInline(inline)) yield return result; } } private IEnumerable RenderInline(MarkdigInline inline) { switch (inline) { case LiteralInline literal: yield return new Run(literal.Content.ToString()); break; case CodeInline code: { var run = new Run(code.Content) { FontSize = FontSize * Scale("MarkdownCodeFontScale", 0.9), }; run.Bind(Run.FontFamilyProperty, this.GetResourceObservable("CodeFontFamily")); run.Bind(Run.BackgroundProperty, this.GetResourceObservable("CodeSurfaceBrush")); yield return run; break; } case EmphasisInline emphasis: { var span = new Span(); var isBold = emphasis.DelimiterCount >= 2; var isItalic = emphasis.DelimiterCount == 1 || emphasis.DelimiterCount == 3; if (isBold) span.FontWeight = FontWeight.Bold; if (isItalic) span.FontStyle = FontStyle.Italic; foreach (var child in RenderInlines(emphasis)) span.Inlines.Add(child); yield return span; break; } case LinkInline link: { var linkSpan = new Span { TextDecorations = TextDecorations.Underline, }; linkSpan.Bind(Span.ForegroundProperty, this.GetResourceObservable("PrimaryBrush")); foreach (var child in RenderInlines(link)) linkSpan.Inlines.Add(child); yield return linkSpan; break; } case LineBreakInline: yield return new LineBreak(); break; case ContainerInline container: foreach (var child in RenderInlines(container)) yield return child; break; } } private double Scale(string key, double fallback) => this.TryFindResource(key, out var v) && v is double d ? d : fallback; private Thickness Space(string key, Thickness fallback) => this.TryFindResource(key, out var v) && v is Thickness t ? t : fallback; private CornerRadius Corner(string key, CornerRadius fallback) => this.TryFindResource(key, out var v) && v is CornerRadius c ? c : fallback; }