How to Paste and Format Rich Hyperlinks in WPF Text Editors
One of the first skills that we are taught in schools is writing. An important skill, writing, allows one to express their ideas and thoughts to others. Think of all the reports you have created, sales proposals you’ve worked on all night, user manuals that your customers needed. Writing is fundamental to our work, irrespective of the job or industry we are in. And to assist us with our writing are the modern-day text editors.
These text editors serve as an enabler for the end-users, providing a seamless writing experience. The C1RichTextBox for WPF is one of such text editors that provide users with a range of input formats – paragraphs, lists, tables, pictures, etc. In this blog, we will look at working with one of such input formats – Hyperlinks. We will discuss using the C1RichTextBox in conjunction with the C1TextParser library to convert and format URLs to hyperlinks automatically.
Adding Hyperlinks – The Traditional Method
Adding hyperlinks to C1RichTextBox is the same as what you might have seen across popular editors like MS-Word. You can open a popup window like the one shown below, set the Text and URL to the required values, and be done with it from the toolbar.
This popup-like window is what you will come across in most of the text editors available out there. While this is simple and easy-to-use, it does take away from the seamless experience that we expect from a modern text editor. What if this popup can be skipped altogether? What if you could have just pasted the URL, and the editor intelligently converts it into a readable hyperlinked text? In the next sections, we will see how to achieve this using C1TextParser.
Extracting URLs
C1TextParser is a strong text parsing .NET library that enables extracting structured information from various semi-structured sources such as Emails/Html/PlainText etc. This blog will use the TextParser’s Starts-After-Continues-Until extractor, which extracts all the text present between StartsAfter/ContinuesUntil(Ends Before) text phrases.
Using the Starts-After-Continues-Until extractor is simple. We need to provide the starting and ending regular expressions between which the text to extract lies. The following code snippet shows an example of extracting URLs from a given text:
public IEnumerable<string> ExtractURLs(string text)
{
List<string> urls = new List<string>();
text += " "; //To adjust for urls at the end of the string
foreach (var protocol in _protocols) // _protocols = ["http","https"]
{
var links = ExtractData(text, protocol, @"\s+");
foreach (var link in links.Select(x => x.ExtractedText))
{
string hyperlink = $"{protocol}{link}";
if (!Uri.IsWellFormedUriString(hyperlink, UriKind.Absolute))
continue;
urls.AddIfNotExist(hyperlink);
}
}
return urls;
}
private List<ExtractedData> ExtractData(string text, string startsAfter, string continueUntil)
{
var extractor = C1TextParserWrapper.GetStartsAfterContinuesUntilExtractor(startsAfter, continueUntil);
var result = extractor.Extract(new MemoryStream(Encoding.UTF8.GetBytes(text)));
var jObject = Newtonsoft.Json.Linq.JObject.Parse(result.ToJsonString());
var jToken = jObject.GetValue("Result");
var extractedData = jToken.ToObject<List<ExtractedData>>();
return extractedData;
}
For using this extraction logic later with the C1RichTextBox, we will move all this to the ExtractURLs method of the HyperLinkParser shown below:
public interface IHyperlinkParser
{
IEnumerable<string> ExtractURLs(string text);
string GetDisplayText(string uri);
}
In the next section, we will look at extracting the display text for a given URI.
Extract Display Text for URIs
If you have observed, each URI extracted above is validated using Uri.IsWellFormedUriString. This validation plays a vital role in the extraction of the display text. As each extracted text is a valid URI, using the URI Host, Segments, and Query properties, we can break the given URI into words where each word can represent:
- The domain name
- The segment path
- The query parameter(s) name and value
For example, the following snippet shows breaking down the Host property to get the domain name using the C1TextParser:
private string ExtractDomainName(Uri uri)
{
IEnumerable<string> data = null;
int dotCount = uri.Host.Count(x => x == '.');
// This condition will be executed for extracting domain name if the uri host contains more than 1 dot('.'). For example, www.google.com
if (dotCount > 1)
data = ExtractData(uri.Host, @"\.", @"\.").Select(x => x.ExtractedText);
// This condition will be executed for extracting domain name if the uri host contains only one dot('.'). For example, youtube.com
if (dotCount == 1)
data = ExtractData($" {uri.Host}", @" ", @"\.").Select(x => x.ExtractedText);
var domainName = data.Where(x => !string.IsNullOrEmpty(x.Trim())).First();
return string.IsNullOrEmpty(domainName) ? uri.Host : domainName; }
}
Similarly, the segments and query parameters can be extracted from the URI using C1TextParser. For implementation details of these extractions, you can refer to the sample attached at the end of the blog. After that, we choose the display text using the following logic:
public string GetDisplayText(string uri)
{
try
{
var words = // code to break URI into words from the Host,Segments and Query
var displayText = ChooseDisplayText(words);
return string.IsNullOrEmpty(displayText) ? uri : displayText;
}
catch
{
return uri;
}
}
protected virtual string ChooseDisplayText(List<string> words)
{
if (words.Count == 0)
return null;
var queryParams = words.Where(x => x.Contains(";"));
var segmentsAndDomain = words.Where(x => !x.Contains(";"));
string displayText = string.Empty;
//If Query Params are present include them in the displayText
if (queryParams.Count() > 0)
displayText = $"({string.Join(" ", queryParams)})";
//If Query Params are present also include the last segment in the display text
if (!string.IsNullOrEmpty(displayText))
displayText = $"{segmentsAndDomain.Last()}{displayText}";
// If Query Params are not present get the display text from the segment or domain name
if (string.IsNullOrEmpty(displayText))
{
words.Reverse();
for (int index = 0; index < words.Count; index++)
{
if (double.TryParse(words[index], out double _))
displayText = $" {words[index]}";
else
{
displayText = $"{words[index]}{displayText}";
break;
}
}
}
if (displayText.Split(_splitters, StringSplitOp-tions.RemoveEmptyEntries).Length == 1)
return displayText.ToFirstUpper();
return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(displayText);
}
And this completes the extraction logic. Let us recall what we’ve achieved till now. So far, we have used C1TextParser to:
- Extract list of URLs from a given text
- Get the DisplayText for a given URI by extracting domain, segments, and query information
Next, we will see how to integrate all this with the C1RichTextBox.
Adding Hyperlinks – The Smart Method
Earlier in the blog, we saw the traditional approach to add hyperlinks to C1RichTextBox. Using the HyperlinkParser defined above, we will look at how to detect URLs when the user pastes some text in a C1RichTextBox and formats it intelligently to a readable hyperlinked text. The following lists the steps for this conversion:
- Subscribe to the ClipboardPasting event of C1RichTextBox
- In the event handler, check if the pasted text is a valid URI or not
- If a valid URI:
- Get the displayText using HyperlinkParser
- Create an HTML anchor tag with the displayText and URI
- Replace the ClipBoard data with the anchor tag
- If the pasted text was not a valid URI:
- Extract all URLs from the text using HyperlinkParser
- For each extracted URL, get the display text and create a corresponding anchor tag
- Replace the URL in the text with an anchor tag
- Replace the ClipBoard data with this new text containing anchor tags
Here’s the code snippet for the above steps:
private void OnClipboardPasting(object sender, ClipboardEventArgs e)
{
try
{
string text = Clipboard.GetText();
if (Uri.IsWellFormedUriString(text, UriKind.Absolute))
{
var displayText = HyperlinkParser.GetDisplayText(text);
var anchor = $"<a href={text}>{displayText}</a>";
Clipboard.SetData(DataFormats.Html, anchor);
}
else if (!string.IsNullOrEmpty(text))
{
var links = HyperlinkParser.ExtractURLs(text).ToList();
foreach (var link in links)
{
var displayText = HyperlinkParser.GetDisplayText(link);
var anchor = $"<a href={link}>{displayText}</a>";
var pattern = $@"(^|\s){link}(\s|$)";
Regex rgx = new Regex(pattern, RegexOptions.Compiled);
text = rgx.Replace(text, $" {anchor} ");
}
Clipboard.SetData(DataFormats.Html, text);
}
}
catch
{
}
}
Conclusion
In the above article, combined the C1TextParser and C1RichTextBox to create a modern and smart way for adding hyperlinks when the user pastes an URL. Similarly, you can also handle the C1RichTextBox TextChanged and KeyDown events to convert the URLs while typing too:
The complete code for the above implementations can be found in the sample.
Read more about C1TextParser and C1RichTextBox.