#if WINDOWS using System.Diagnostics; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using static MeetingAssistant.Workflow.WorkflowRulesEditorWpfTheme; namespace MeetingAssistant.Workflow; internal static class WorkflowRulesEditorMarkdownRenderer { public static UIElement CreateContent( string content, WorkflowRulesEditorMarkdownLinkResolver? linkResolver = null) { linkResolver ??= WorkflowRulesEditorMarkdownLinkResolver.Empty; var panel = new StackPanel { Orientation = Orientation.Vertical }; var blocks = WorkflowRulesEditorMarkdown.Parse(content); for (var index = 0; index < blocks.Count; index++) { var element = blocks[index] switch { WorkflowRulesEditorMarkdownHeading heading => CreateHeading(heading, linkResolver), WorkflowRulesEditorMarkdownBlockQuote quote => CreateBlockQuote(quote, linkResolver), WorkflowRulesEditorMarkdownImage image => CreateImageBlock(image, linkResolver), WorkflowRulesEditorMarkdownTaskList tasks => CreateTaskList(tasks, linkResolver), WorkflowRulesEditorMarkdownCodeBlock codeBlock => CreateCodeBlock(codeBlock), WorkflowRulesEditorMarkdownTable table => CreateTable(table), WorkflowRulesEditorMarkdownParagraph paragraph => CreateParagraph(paragraph, linkResolver), _ => new TextBlock() }; if (index < blocks.Count - 1) { element.SetValue(FrameworkElement.MarginProperty, new Thickness(0, 0, 0, 7)); } panel.Children.Add(element); } return panel; } private static UIElement CreateHeading( WorkflowRulesEditorMarkdownHeading heading, WorkflowRulesEditorMarkdownLinkResolver linkResolver) { var paragraph = CreateInlineParagraph(heading.Inlines, linkResolver, heading.Level == 1 ? 22 : 20); paragraph.FontWeight = FontWeights.SemiBold; return CreateSelectableDocument([paragraph], fontSize: heading.Level == 1 ? 16.5 : 15.5); } private static UIElement CreateBlockQuote( WorkflowRulesEditorMarkdownBlockQuote quote, WorkflowRulesEditorMarkdownLinkResolver linkResolver) { var paragraph = CreateInlineParagraph(quote.Inlines, linkResolver); return new Border { Padding = new Thickness(10, 7, 10, 7), BorderThickness = new Thickness(3, 0, 0, 0), BorderBrush = QuoteBorder, Background = QuoteBackground, Child = CreateSelectableDocument([paragraph]) }; } private static UIElement CreateParagraph( WorkflowRulesEditorMarkdownParagraph paragraph, WorkflowRulesEditorMarkdownLinkResolver linkResolver) { var flowParagraph = CreateInlineParagraph(paragraph.Inlines, linkResolver); return CreateSelectableDocument([flowParagraph]); } private static UIElement CreateImageBlock( WorkflowRulesEditorMarkdownImage image, WorkflowRulesEditorMarkdownLinkResolver linkResolver) { return CreateImagePreview(image.AltText, image.Target, linkResolver); } private static UIElement CreateTaskList( WorkflowRulesEditorMarkdownTaskList tasks, WorkflowRulesEditorMarkdownLinkResolver linkResolver) { var grid = new Grid(); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(24) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); for (var row = 0; row < tasks.Items.Count; row++) { var item = tasks.Items[row]; grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); var glyph = new TextBlock { Text = item.IsChecked ? "☑" : "☐", FontFamily = new FontFamily("Segoe UI Symbol"), FontSize = 17, Foreground = PrimaryText, LineHeight = 19, Margin = new Thickness(0, 0, 6, 3), VerticalAlignment = VerticalAlignment.Top }; Grid.SetRow(glyph, row); Grid.SetColumn(glyph, 0); grid.Children.Add(glyph); var paragraph = CreateInlineParagraph(item.Inlines, linkResolver); var text = CreateSelectableDocument([paragraph]); text.Margin = new Thickness(0, 0, 0, 3); Grid.SetRow(text, row); Grid.SetColumn(text, 1); grid.Children.Add(text); } return grid; } private static UIElement CreateCodeBlock(WorkflowRulesEditorMarkdownCodeBlock codeBlock) { var run = new Run(codeBlock.Text) { FontFamily = new FontFamily("Consolas") }; var paragraph = CreateParagraphBlock(); paragraph.Inlines.Add(run); var document = CreateSelectableDocument([paragraph], CodeText); return new Border { Padding = new Thickness(9, 7, 9, 7), CornerRadius = new CornerRadius(6), BorderThickness = new Thickness(1), BorderBrush = CodeBorder, Background = CodeBackground, Child = document }; } private static UIElement CreateTable(WorkflowRulesEditorMarkdownTable table) { var rows = table.Rows .Select(row => new MarkdownTableRow(PadCells(row, table.Header.Count))) .ToArray(); var grid = new Grid(); grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); for (var column = 0; column < table.Header.Count; column++) { grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(GetMarkdownTableColumnWidth(table, column)) }); AddTableCell(grid, 0, column, table.Header[column], TableHeaderBackground, FontWeights.SemiBold); } for (var row = 0; row < rows.Length; row++) { grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); for (var column = 0; column < table.Header.Count; column++) { AddTableCell(grid, row + 1, column, rows[row].GetCell(column), TableCellBackground, FontWeights.Normal); } } return new ScrollViewer { Content = grid, HorizontalScrollBarVisibility = ScrollBarVisibility.Auto, VerticalScrollBarVisibility = ScrollBarVisibility.Disabled, MaxHeight = 420 }; } private static RichTextBox CreateSelectableDocument( IEnumerable blocks, Brush? foreground = null, double fontSize = 14) { var document = new FlowDocument { PagePadding = new Thickness(0), FontFamily = new FontFamily("Segoe UI"), FontSize = fontSize, Foreground = foreground ?? PrimaryText }; foreach (var block in blocks) { document.Blocks.Add(block); } return new RichTextBox { Document = document, IsReadOnly = true, IsDocumentEnabled = true, BorderThickness = new Thickness(0), Background = Brushes.Transparent, Foreground = foreground ?? PrimaryText, Padding = new Thickness(0), Margin = new Thickness(0), MinHeight = 0, HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled, VerticalScrollBarVisibility = ScrollBarVisibility.Disabled }; } private static Paragraph CreateParagraphBlock(double lineHeight = 19) { return new Paragraph { Margin = new Thickness(0), LineHeight = lineHeight }; } private static Paragraph CreateInlineParagraph( IReadOnlyList inlines, WorkflowRulesEditorMarkdownLinkResolver linkResolver, double lineHeight = 19) { var paragraph = CreateParagraphBlock(lineHeight); foreach (var inline in inlines) { paragraph.Inlines.Add(CreateInline(inline, linkResolver)); } return paragraph; } private static Inline CreateInline( WorkflowRulesEditorMarkdownInline inline, WorkflowRulesEditorMarkdownLinkResolver linkResolver) { if (inline.Style == WorkflowRulesEditorMarkdownInlineStyle.Link) { return CreateLink(inline, linkResolver); } if (inline.Style == WorkflowRulesEditorMarkdownInlineStyle.Image) { return CreateImage(inline, linkResolver); } if (inline.Style == WorkflowRulesEditorMarkdownInlineStyle.Code) { return CreateInlineCode(inline.Text); } var run = new Run(inline.Text); switch (inline.Style) { case WorkflowRulesEditorMarkdownInlineStyle.Bold: run.FontWeight = FontWeights.SemiBold; break; case WorkflowRulesEditorMarkdownInlineStyle.BoldItalic: run.FontWeight = FontWeights.SemiBold; run.FontStyle = FontStyles.Italic; break; case WorkflowRulesEditorMarkdownInlineStyle.Italic: run.FontStyle = FontStyles.Italic; break; case WorkflowRulesEditorMarkdownInlineStyle.Strikethrough: run.TextDecorations = TextDecorations.Strikethrough; break; } return run; } private static Inline CreateLink( WorkflowRulesEditorMarkdownInline inline, WorkflowRulesEditorMarkdownLinkResolver linkResolver) { var hyperlink = new Hyperlink(new Run(inline.Text)) { Foreground = LinkText, TextDecorations = TextDecorations.Underline, ToolTip = inline.LinkTarget, Cursor = Cursors.Hand }; if (!string.IsNullOrWhiteSpace(inline.LinkTarget)) { hyperlink.Click += (_, args) => { OpenTarget(inline.LinkTarget, linkResolver); args.Handled = true; }; } return hyperlink; } private static Inline CreateInlineCode(string text) { var code = new TextBlock { Text = text, FontFamily = new FontFamily("Consolas"), FontSize = 13, Foreground = CodeText, Background = CodeBackground, Padding = new Thickness(0), Margin = new Thickness(0), LineHeight = 16, VerticalAlignment = VerticalAlignment.Center }; return new InlineUIContainer(new Border { BorderThickness = new Thickness(1), BorderBrush = CodeBorder, CornerRadius = new CornerRadius(4), Background = CodeBackground, Padding = new Thickness(4, 1, 4, 1), Margin = new Thickness(1, 0, 1, 0), Child = code }) { BaselineAlignment = BaselineAlignment.Center }; } private static Inline CreateImage( WorkflowRulesEditorMarkdownInline inline, WorkflowRulesEditorMarkdownLinkResolver linkResolver) { var preview = CreateImagePreview(inline.Text, inline.LinkTarget, linkResolver); return new InlineUIContainer(preview) { BaselineAlignment = BaselineAlignment.Center }; } private static UIElement CreateImagePreview( string altText, string? target, WorkflowRulesEditorMarkdownLinkResolver linkResolver) { var border = new Border { Width = 180, Height = 112, Margin = new Thickness(0, 4, 0, 4), Padding = new Thickness(6), BorderThickness = new Thickness(1), BorderBrush = CodeBorder, CornerRadius = new CornerRadius(6), Background = ImagePlaceholderBackground, ToolTip = target, Cursor = Cursors.Hand }; border.AddHandler(UIElement.MouseLeftButtonUpEvent, new MouseButtonEventHandler((_, args) => { OpenTarget(target, linkResolver); args.Handled = true; }), handledEventsToo: true); if (TryCreateBitmap(target, linkResolver, out var bitmap)) { var image = new Image { Source = bitmap, Stretch = Stretch.Uniform, ToolTip = target }; image.ImageFailed += (_, _) => border.Child = CreateImagePlaceholder(altText); border.Child = image; } else { border.Child = CreateImagePlaceholder(altText); } return border; } private static UIElement CreateImagePlaceholder(string altText) { return new TextBlock { Text = string.IsNullOrWhiteSpace(altText) ? "Image" : altText, Foreground = PrimaryText, TextAlignment = TextAlignment.Center, TextWrapping = TextWrapping.Wrap, VerticalAlignment = VerticalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center }; } private static bool TryCreateBitmap( string? target, WorkflowRulesEditorMarkdownLinkResolver linkResolver, out BitmapImage bitmap) { bitmap = null!; if (string.IsNullOrWhiteSpace(target) || !linkResolver.TryResolveImageUri(target, out var uri)) { return false; } try { bitmap = new BitmapImage(); bitmap.BeginInit(); bitmap.CacheOption = BitmapCacheOption.OnLoad; bitmap.UriSource = uri; bitmap.DecodePixelWidth = 360; bitmap.EndInit(); bitmap.Freeze(); return true; } catch { return false; } } private static void OpenTarget( string? target, WorkflowRulesEditorMarkdownLinkResolver linkResolver) { if (string.IsNullOrWhiteSpace(target)) { return; } try { var openTarget = linkResolver.ResolveOpenTarget(target); if (string.IsNullOrWhiteSpace(openTarget)) { return; } Process.Start(new ProcessStartInfo(openTarget) { UseShellExecute = true }); } catch { } } private static void AddTableCell( Grid grid, int row, int column, string text, Brush background, FontWeight fontWeight) { var paragraph = CreateParagraphBlock(); paragraph.Inlines.Add(new Run(text) { FontWeight = fontWeight }); var document = CreateSelectableDocument([paragraph], PrimaryText); document.HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden; var border = new Border { Padding = new Thickness(8, 4, 8, 4), BorderThickness = new Thickness(0, 0, 1, 1), BorderBrush = CodeBorder, Background = background, Child = document }; Grid.SetRow(border, row); Grid.SetColumn(border, column); grid.Children.Add(border); } private static IReadOnlyList PadCells(IReadOnlyList cells, int count) { if (cells.Count >= count) { return cells; } return cells .Concat(Enumerable.Repeat("", count - cells.Count)) .ToArray(); } private static double GetMarkdownTableColumnWidth( WorkflowRulesEditorMarkdownTable table, int index) { var maxLength = new[] { table.Header[index].Length } .Concat(table.Rows.Select(row => index < row.Count ? row[index].Length : 0)) .DefaultIfEmpty(0) .Max(); return Math.Clamp(maxLength * 8 + 28, 72, 260); } private sealed record MarkdownTableRow(IReadOnlyList Cells) { public string GetCell(int index) { return index >= 0 && index < Cells.Count ? Cells[index] : ""; } } } #endif