From c8a71c48359e2804bab7cee0ca2698f50a3a9111 Mon Sep 17 00:00:00 2001 From: Isak Savo Date: Tue, 8 Jun 2021 11:51:04 +0200 Subject: [PATCH] Add support for in-document linking (e.g. #my-id). --- Documents/Markdig-readme.md | 9 +- src/Markdig.Wpf/Commands.cs | 5 + src/Markdig.Wpf/MarkdownExtensions.cs | 3 +- src/Markdig.Wpf/MarkdownViewer.cs | 99 +++++++++++++++++++ .../Renderers/Wpf/HeadingRenderer.cs | 8 +- .../Wpf/Inlines/LinkInlineRenderer.cs | 5 +- src/Markdig.Wpf/Themes/generic.xaml | 1 + 7 files changed, 124 insertions(+), 6 deletions(-) diff --git a/Documents/Markdig-readme.md b/Documents/Markdig-readme.md index 6d231f2..9d593fa 100644 --- a/Documents/Markdig-readme.md +++ b/Documents/Markdig-readme.md @@ -1,4 +1,11 @@ -# Markdig [![Build status](https://ci.appveyor.com/api/projects/status/hk391x8jcskxt1u8?svg=true)](https://ci.appveyor.com/project/xoofx/markdig) [![NuGet](https://img.shields.io/nuget/v/Markdig.svg)](https://www.nuget.org/packages/Markdig/) [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FRGHXBTP442JL) +* [Markdig](#markdig) + * [Features](#features) + * [Documentation](#documentation) + * [Download](#download) + * [Usage](#usage) + + +# Markdig [![Build status](https://ci.appveyor.com/api/projects/status/hk391x8jcskxt1u8?svg=true)](https://ci.appveyor.com/project/xoofx/markdig) [![NuGet](https://img.shields.io/nuget/v/Markdig.svg)](https://www.nuget.org/packages/Markdig/) [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FRGHXBTP442JL) | Tables | Are | Cool | | ------------- |:-------------:| -----:| diff --git a/src/Markdig.Wpf/Commands.cs b/src/Markdig.Wpf/Commands.cs index 633a887..0643095 100644 --- a/src/Markdig.Wpf/Commands.cs +++ b/src/Markdig.Wpf/Commands.cs @@ -20,5 +20,10 @@ public static class Commands /// Routed command for Images. /// public static RoutedCommand Image { get; } = new RoutedCommand(nameof(Image), typeof(Commands)); + + /// + /// Routed command for navigating to a heading in a document. Command parameter contains the heading id + /// + public static RoutedCommand Navigate { get; } = new RoutedCommand(nameof(Navigate), typeof(Commands)); } } diff --git a/src/Markdig.Wpf/MarkdownExtensions.cs b/src/Markdig.Wpf/MarkdownExtensions.cs index 7470dc2..5925a6a 100644 --- a/src/Markdig.Wpf/MarkdownExtensions.cs +++ b/src/Markdig.Wpf/MarkdownExtensions.cs @@ -25,7 +25,8 @@ public static MarkdownPipelineBuilder UseSupportedExtensions(this MarkdownPipeli .UseGridTables() .UsePipeTables() .UseTaskLists() - .UseAutoLinks(); + .UseAutoLinks() + .UseAutoIdentifiers(); } } } diff --git a/src/Markdig.Wpf/MarkdownViewer.cs b/src/Markdig.Wpf/MarkdownViewer.cs index 5059503..eed80d6 100644 --- a/src/Markdig.Wpf/MarkdownViewer.cs +++ b/src/Markdig.Wpf/MarkdownViewer.cs @@ -2,9 +2,12 @@ // This file is licensed under the MIT license. // See the LICENSE.md file in the project root for more information. +using System; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; namespace Markdig.Wpf { @@ -35,11 +38,28 @@ public class MarkdownViewer : Control public static readonly DependencyProperty PipelineProperty = DependencyProperty.Register(nameof(Pipeline), typeof(MarkdownPipeline), typeof(MarkdownViewer), new FrameworkPropertyMetadata(PipelineChanged)); + /// + /// Defines the MarkdownViewer.AnchorName attached property used for in-document linking (e.g. "#my-id") + /// + public static readonly DependencyProperty AnchorNameProperty = DependencyProperty.RegisterAttached( + "AnchorName", typeof(string), typeof(MarkdownViewer), new PropertyMetadata(default(string))); + + public static void SetAnchorName(DependencyObject element, string value) + { + element.SetValue(AnchorNameProperty, value); + } + + public static string GetAnchorName(DependencyObject element) + { + return (string)element.GetValue(AnchorNameProperty); + } static MarkdownViewer() { DefaultStyleKeyProperty.OverrideMetadata(typeof(MarkdownViewer), new FrameworkPropertyMetadata(typeof(MarkdownViewer))); } + private FlowDocumentScrollViewer? docViewer; + /// /// Gets the flow document to display. /// @@ -79,9 +99,88 @@ private static void PipelineChanged(object sender, DependencyPropertyChangedEven control.RefreshDocument(); } + public MarkdownViewer() + { + CommandBindings.Add(new CommandBinding(Commands.Navigate, NavigateCommandExecuted)); + } + + private void NavigateCommandExecuted(object sender, ExecutedRoutedEventArgs e) + { + string url = e.Parameter?.ToString() ?? ""; + if (url.Length > 1 && url[0] == '#') + { + string anchorName = url.Substring(1); + e.Handled = NavigateTo(anchorName); + } + } + protected virtual void RefreshDocument() { Document = Markdown != null ? Wpf.Markdown.ToFlowDocument(Markdown, Pipeline ?? DefaultPipeline) : null; } + + public override void OnApplyTemplate() + { + docViewer = GetTemplateChild("PART_DocViewer") as FlowDocumentScrollViewer; + + base.OnApplyTemplate(); + } + + public bool NavigateTo(string anchorName) + { + if (Document == null) + throw new InvalidOperationException("No rendered content found"); + + foreach (var block in Document.Blocks) + { + string blockAnchorName = GetAnchorName(block); + if (String.Equals(blockAnchorName, anchorName, StringComparison.OrdinalIgnoreCase)) + { + return ScrollIntoView(block); + } + } + + return false; + } + + private bool ScrollIntoView(Block block) + { + if (docViewer == null) + return false; + double top = block.ContentStart.GetCharacterRect(LogicalDirection.Forward).Top; + var scrollViewer = FindVisualChild(docViewer); + if (scrollViewer != null) + { + scrollViewer.ScrollToVerticalOffset(top); + return true; + } + + return false; + } + + /// + /// Gets a visual child of the specific type. + /// + /// The type of child to find. + /// Where to start the "search". + /// The child or null. + public static T? FindVisualChild( + DependencyObject element) where T : class + { + if (element is T retVal) + return retVal; + + int childCnt = VisualTreeHelper.GetChildrenCount(element); + for (int i = 0; i < childCnt; ++i) + { + DependencyObject child = VisualTreeHelper.GetChild(element, i); + + var result = FindVisualChild(child); + if (result != null) + return result; + } + + return null; + } } } diff --git a/src/Markdig.Wpf/Renderers/Wpf/HeadingRenderer.cs b/src/Markdig.Wpf/Renderers/Wpf/HeadingRenderer.cs index 705ea98..3c823af 100644 --- a/src/Markdig.Wpf/Renderers/Wpf/HeadingRenderer.cs +++ b/src/Markdig.Wpf/Renderers/Wpf/HeadingRenderer.cs @@ -5,7 +5,7 @@ using System; using System.Windows; using System.Windows.Documents; - +using Markdig.Renderers.Html; using Markdig.Syntax; using Markdig.Wpf; @@ -36,6 +36,12 @@ protected override void Write(WpfRenderer renderer, HeadingBlock obj) paragraph.SetResourceReference(FrameworkContentElement.StyleProperty, styleKey); } + var attributes = obj.TryGetAttributes(); + if (!String.IsNullOrEmpty(attributes?.Id)) + { + MarkdownViewer.SetAnchorName(paragraph, attributes.Id); + } + renderer.Push(paragraph); renderer.WriteLeafInline(obj); renderer.Pop(); diff --git a/src/Markdig.Wpf/Renderers/Wpf/Inlines/LinkInlineRenderer.cs b/src/Markdig.Wpf/Renderers/Wpf/Inlines/LinkInlineRenderer.cs index eaf6d67..0d8ee3c 100644 --- a/src/Markdig.Wpf/Renderers/Wpf/Inlines/LinkInlineRenderer.cs +++ b/src/Markdig.Wpf/Renderers/Wpf/Inlines/LinkInlineRenderer.cs @@ -27,7 +27,7 @@ protected override void Write(WpfRenderer renderer, LinkInline link) var url = link.GetDynamicUrl != null ? link.GetDynamicUrl() ?? link.Url : link.Url; - if (!Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute)) + if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out _)) { url = "#"; } @@ -53,12 +53,11 @@ protected override void Write(WpfRenderer renderer, LinkInline link) { var hyperlink = new Hyperlink { - Command = Commands.Hyperlink, + Command = url.StartsWith("#") ? Commands.Navigate : Commands.Hyperlink, CommandParameter = url, NavigateUri = new Uri(url, UriKind.RelativeOrAbsolute), ToolTip = !string.IsNullOrEmpty(link.Title) ? link.Title : null, }; - hyperlink.SetResourceReference(FrameworkContentElement.StyleProperty, Styles.HyperlinkStyleKey); renderer.Push(hyperlink); diff --git a/src/Markdig.Wpf/Themes/generic.xaml b/src/Markdig.Wpf/Themes/generic.xaml index b40f68a..86c6f8f 100644 --- a/src/Markdig.Wpf/Themes/generic.xaml +++ b/src/Markdig.Wpf/Themes/generic.xaml @@ -99,6 +99,7 @@