Public Access
515 lines
16 KiB
C#
515 lines
16 KiB
C#
#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<Block> 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<WorkflowRulesEditorMarkdownInline> 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<string> PadCells(IReadOnlyList<string> 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<string> Cells)
|
|
{
|
|
public string GetCell(int index)
|
|
{
|
|
return index >= 0 && index < Cells.Count
|
|
? Cells[index]
|
|
: "";
|
|
}
|
|
}
|
|
}
|
|
#endif
|