using System.Text.RegularExpressions; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Documents; using Avalonia.Layout; using Avalonia.Markup.Xaml; using Avalonia.Media; namespace ClaudeDo.Ui.Views.Controls; public partial class MarkdownView : UserControl { public static readonly StyledProperty MarkdownProperty = AvaloniaProperty.Register(nameof(Markdown)); public string? Markdown { get => GetValue(MarkdownProperty); set => SetValue(MarkdownProperty, value); } private StackPanel? _host; public MarkdownView() { InitializeComponent(); _host = this.FindControl("Host"); } private void InitializeComponent() => AvaloniaXamlLoader.Load(this); protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == MarkdownProperty) Render(change.GetNewValue()); } private static readonly Regex NumberedItem = new(@"^\s*(\d+)\.\s+(.*)$", RegexOptions.Compiled); private void Render(string? md) { _host ??= this.FindControl("Host"); if (_host is null) return; _host.Children.Clear(); if (string.IsNullOrWhiteSpace(md)) return; var lines = md.Replace("\r\n", "\n").Split('\n'); int i = 0; while (i < lines.Length) { var line = lines[i]; if (string.IsNullOrWhiteSpace(line)) { i++; continue; } if (line.StartsWith("### ")) { _host.Children.Add(Heading(line[4..], 13)); i++; continue; } if (line.StartsWith("## ")) { _host.Children.Add(Heading(line[3..], 15)); i++; continue; } if (line.StartsWith("# ")) { _host.Children.Add(Heading(line[2..], 17)); i++; continue; } if (line.StartsWith("- ") || line.StartsWith("* ")) { while (i < lines.Length && (lines[i].StartsWith("- ") || lines[i].StartsWith("* "))) { _host.Children.Add(Bullet(lines[i][2..])); i++; } continue; } var num = NumberedItem.Match(line); if (num.Success) { while (i < lines.Length && NumberedItem.IsMatch(lines[i])) { var m = NumberedItem.Match(lines[i]); _host.Children.Add(Numbered(m.Groups[1].Value, m.Groups[2].Value)); i++; } continue; } var paragraph = new System.Text.StringBuilder(); while (i < lines.Length && !string.IsNullOrWhiteSpace(lines[i]) && !IsBlockStart(lines[i])) { if (paragraph.Length > 0) paragraph.Append(' '); paragraph.Append(lines[i].Trim()); i++; } _host.Children.Add(Paragraph(paragraph.ToString())); } } private static bool IsBlockStart(string line) => line.StartsWith("# ") || line.StartsWith("## ") || line.StartsWith("### ") || line.StartsWith("- ") || line.StartsWith("* ") || NumberedItem.IsMatch(line); private static Control Heading(string text, double size) { var tb = new SelectableTextBlock { FontSize = size, FontWeight = FontWeight.SemiBold, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 4, 0, 2), }; AppendInlines(tb.Inlines!, text); return tb; } private static Control Paragraph(string text) { var tb = new SelectableTextBlock { TextWrapping = TextWrapping.Wrap }; AppendInlines(tb.Inlines!, text); return tb; } private static Control Bullet(string text) { var grid = new Grid { ColumnDefinitions = new ColumnDefinitions("14,*") }; var dot = new TextBlock { Text = "•", VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(0, 0, 4, 0), }; Grid.SetColumn(dot, 0); grid.Children.Add(dot); var tb = new SelectableTextBlock { TextWrapping = TextWrapping.Wrap }; AppendInlines(tb.Inlines!, text); Grid.SetColumn(tb, 1); grid.Children.Add(tb); return grid; } private static Control Numbered(string num, string text) { var grid = new Grid { ColumnDefinitions = new ColumnDefinitions("20,*") }; var lbl = new TextBlock { Text = $"{num}.", VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(0, 0, 4, 0), }; Grid.SetColumn(lbl, 0); grid.Children.Add(lbl); var tb = new SelectableTextBlock { TextWrapping = TextWrapping.Wrap }; AppendInlines(tb.Inlines!, text); Grid.SetColumn(tb, 1); grid.Children.Add(tb); return grid; } private static void AppendInlines(InlineCollection inlines, string text) { int i = 0; var plain = new System.Text.StringBuilder(); void FlushPlain() { if (plain.Length == 0) return; inlines.Add(new Run(plain.ToString())); plain.Clear(); } while (i < text.Length) { if (i + 1 < text.Length && text[i] == '*' && text[i + 1] == '*') { int close = text.IndexOf("**", i + 2, System.StringComparison.Ordinal); if (close > i + 2) { FlushPlain(); inlines.Add(new Run(text[(i + 2)..close]) { FontWeight = FontWeight.Bold }); i = close + 2; continue; } } if (text[i] == '*') { int close = text.IndexOf('*', i + 1); if (close > i + 1) { FlushPlain(); inlines.Add(new Run(text[(i + 1)..close]) { FontStyle = FontStyle.Italic }); i = close + 1; continue; } } if (text[i] == '`') { int close = text.IndexOf('`', i + 1); if (close > i + 1) { FlushPlain(); inlines.Add(new Run(text[(i + 1)..close]) { FontFamily = new FontFamily("Consolas,Menlo,monospace"), Background = new SolidColorBrush(Color.FromArgb(60, 127, 127, 127)), }); i = close + 1; continue; } } plain.Append(text[i]); i++; } FlushPlain(); } }