using System; using System.Linq; using System.Windows; using System.Windows.Documents; using System.Windows.Media; using Markdig; using Markdig.Extensions.TaskLists; using Markdig.Syntax; using Markdig.Syntax.Inlines; using MdBlock = Markdig.Syntax.Block; using MdInline = Markdig.Syntax.Inlines.Inline; using WpfBlock = System.Windows.Documents.Block; using WpfInline = System.Windows.Documents.Inline; using WpfList = System.Windows.Documents.List; namespace Kniip.Update; /// Zet markdown (releasenotes) om naar een WPF . public static class MarkdownRenderer { private static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder() .UseEmphasisExtras() .UseAutoLinks() .UseTaskLists() .Build(); private static readonly FontFamily CodeFont = new("Cascadia Mono, Consolas"); private static Brush CodeBackground => Resource("CodeBrush", Brushes.WhiteSmoke); private static Brush QuoteBorder => Resource("QuoteBorderBrush", Brushes.LightGray); private static Brush Accent => Resource("AccentBrush", Brushes.SteelBlue); private static Brush Subtle => Resource("SubtleTextBrush", Brushes.Gray); public static FlowDocument CreateDocument() => new() { FontFamily = new FontFamily("Segoe UI"), FontSize = 13, PagePadding = new Thickness(14, 10, 14, 10), TextAlignment = TextAlignment.Left, Background = Resource("CardBrush", Brushes.White), }; /// Voegt de blokken van toe; relatieve links wijzen naar . public static void Append(FlowDocument document, string markdown, Uri? baseUri = null) { MarkdownDocument parsed = Markdown.Parse(markdown ?? string.Empty, Pipeline); foreach (MdBlock block in parsed) { if (RenderBlock(block, baseUri) is { } rendered) { document.Blocks.Add(rendered); } } } public static Paragraph Heading(string text, double fontSize, Thickness margin) => new(new Run(text)) { FontSize = fontSize, FontWeight = FontWeights.SemiBold, Margin = margin, }; private static WpfBlock? RenderBlock(MdBlock block, Uri? baseUri) { switch (block) { case HeadingBlock heading: { var paragraph = new Paragraph { FontSize = heading.Level switch { 1 => 19, 2 => 16.5, 3 => 14.5, _ => 13 }, FontWeight = FontWeights.SemiBold, Margin = new Thickness(0, heading.Level <= 2 ? 12 : 8, 0, 4), }; AddInlines(paragraph.Inlines, heading.Inline, baseUri); return paragraph; } case ParagraphBlock paragraphBlock: { var paragraph = new Paragraph { Margin = new Thickness(0, 0, 0, 8) }; AddInlines(paragraph.Inlines, paragraphBlock.Inline, baseUri); return paragraph; } case ListBlock listBlock: { var list = new WpfList { MarkerStyle = listBlock.IsOrdered ? TextMarkerStyle.Decimal : TextMarkerStyle.Disc, Margin = new Thickness(0, 0, 0, 8), Padding = new Thickness(20, 0, 0, 0), }; if (listBlock.IsOrdered && int.TryParse(listBlock.OrderedStart, out int start) && start > 0) { list.StartIndex = start; } foreach (ListItemBlock itemBlock in listBlock.OfType()) { var item = new ListItem(); foreach (MdBlock child in itemBlock) { if (RenderBlock(child, baseUri) is { } rendered) { rendered.Margin = new Thickness(0, 0, 0, 2); item.Blocks.Add(rendered); } } list.ListItems.Add(item); } return list; } case QuoteBlock quote: { var section = new Section { BorderBrush = QuoteBorder, BorderThickness = new Thickness(3, 0, 0, 0), Padding = new Thickness(10, 0, 0, 0), Margin = new Thickness(0, 0, 0, 8), Foreground = Subtle, }; foreach (MdBlock child in quote) { if (RenderBlock(child, baseUri) is { } rendered) { section.Blocks.Add(rendered); } } return section; } case CodeBlock code: { var paragraph = new Paragraph { FontFamily = CodeFont, FontSize = 12, Background = CodeBackground, Padding = new Thickness(8, 6, 8, 6), Margin = new Thickness(0, 0, 0, 8), }; string[] lines = code.Lines.ToString().Replace("\r\n", "\n").TrimEnd('\n').Split('\n'); for (int i = 0; i < lines.Length; i++) { if (i > 0) { paragraph.Inlines.Add(new LineBreak()); } paragraph.Inlines.Add(new Run(lines[i])); } return paragraph; } case ThematicBreakBlock: return new Paragraph { BorderBrush = QuoteBorder, BorderThickness = new Thickness(0, 0, 0, 1), FontSize = 1, Margin = new Thickness(0, 4, 0, 10), }; case HtmlBlock: return null; case ContainerBlock container: { var section = new Section(); foreach (MdBlock child in container) { if (RenderBlock(child, baseUri) is { } rendered) { section.Blocks.Add(rendered); } } return section.Blocks.Count > 0 ? section : null; } case LeafBlock leaf when leaf.Inline != null: { var paragraph = new Paragraph { Margin = new Thickness(0, 0, 0, 8) }; AddInlines(paragraph.Inlines, leaf.Inline, baseUri); return paragraph; } default: return null; } } private static void AddInlines(InlineCollection target, ContainerInline? container, Uri? baseUri) { if (container == null) { return; } foreach (MdInline inline in container) { if (RenderInline(inline, baseUri) is { } rendered) { target.Add(rendered); } } } private static WpfInline? RenderInline(MdInline inline, Uri? baseUri) { switch (inline) { case LiteralInline literal: return new Run(literal.Content.ToString()); case CodeInline code: return new Run(code.Content) { FontFamily = CodeFont, FontSize = 12, Background = CodeBackground, }; case LineBreakInline lineBreak: return lineBreak.IsHard ? new LineBreak() : new Run(" "); case TaskList task: return new Run(task.Checked ? "\u2611 " : "\u2610 "); case AutolinkInline autolink: return CreateLink(new Span(new Run(autolink.Url)), autolink.IsEmail ? "mailto:" + autolink.Url : autolink.Url, baseUri); case LinkInline link: { var span = new Span(); AddInlines(span.Inlines, link, baseUri); if (span.Inlines.Count == 0) { span.Inlines.Add(new Run(link.Url ?? string.Empty)); } return CreateLink(span, link.Url, baseUri); } case EmphasisInline emphasis: { var span = new Span(); AddInlines(span.Inlines, emphasis, baseUri); if (emphasis.DelimiterChar == '~') { span.TextDecorations = TextDecorations.Strikethrough; } else if (emphasis.DelimiterCount >= 2) { span.FontWeight = FontWeights.SemiBold; } else { span.FontStyle = FontStyles.Italic; } return span; } case HtmlInline html when html.Tag.StartsWith(" { OpenUrl(e.Uri); e.Handled = true; }; return link; } private static Uri? ResolveUrl(string? url, Uri? baseUri) { if (string.IsNullOrWhiteSpace(url)) { return null; } Uri? uri = Uri.TryCreate(url, UriKind.Absolute, out Uri? absolute) ? absolute : baseUri != null && Uri.TryCreate(baseUri, url, out Uri? relative) ? relative : null; return uri != null && (uri.Scheme == Uri.UriSchemeHttps || uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeMailto) ? uri : null; } public static void OpenUrl(Uri uri) { try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(uri.AbsoluteUri) { UseShellExecute = true }); } catch (Exception) { // Zonder standaardbrowser gebeurt er niets; de link staat in de tooltip. } } private static Brush Resource(string key, Brush fallback) => Application.Current?.TryFindResource(key) as Brush ?? fallback; }