Tuesday, July 12, 2011

Turning URLs into Hyperlinks (with Truncating Long URLs) in ASP.NET

Turning URLs into hypyerlinks is fairly easy and straightforward, and actually has been discussed many times before (for example, see here and here). But some times, you also need to truncate the URL in the link text if it's too long. For example, if you have some text with a very long URL like this:

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco http://www.example.com/thisisaveeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeerylongurl.html laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

You'll probably want to not only turn the URL into a hyperlink, but also truncate the URL in the hyperlink's text to look something like this:

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco http://www.example.com/thisisaveeeeeeeeee... laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Finding and Truncating the URLs

Finding the URLs is straightforward and is easily done using Regular expressions, but to truncate the long URLs, we'll need to to check whether the URL is too long, and if so, truncate it before using it in the hyperlink's text.

In this post, I'm going to discuss the details of how to find the URLs. For truncating long URLs, I'm going to use code from my previous post about truncating long text in ASP.NET. Please refer to that post for the details of how the text is truncated.


The Code
public static class UIHelper
{
    private static string ConvertURLs(string text, int textMaxWidth, string fontName, int fontSizeInPixels, bool isFontBold)
    {
        if (string.IsNullOrEmpty(text))
            return text;

        return Regex.Replace(
            text, 
            @"(http(s)*://([\w.]+/?)\S*)",
            LinkTextTruncator.GetLinkTextTruncator(textMaxWidth, fontName, fontSizeInPixels, isFontBold).MatchEvaluator,
            RegexOptions.IgnoreCase | RegexOptions.Compiled);
    }
}

class LinkTextTruncator
{
    public LinkTextTruncator(
        int textMaxWidth, 
        string fontName, 
        int fontSizeInPixels, 
        bool isFontBold)
    {
        _textMaxWidth = textMaxWidth;
        _fontName = fontName;
        _fontSizeInPixels = fontSizeInPixels;
        _isFontBold = isFontBold;
        _matchEvaluator = new MatchEvaluator(TruncateText);
    }

    private int _textMaxWidth;
    private string _fontName;
    private int _fontSizeInPixels;
    private bool _isFontBold;
    private MatchEvaluator _matchEvaluator;

    public string TruncateText(Match m)
    {
        string url = m.ToString();
        return string.Format(
            "{1}",
            url,
            TextTruncator.TruncateText( // For the implementation of TextTruncator.TruncateText(), please refer to my post about truncating long text in ASP.NET
                url,
                _textMaxWidth,
                _fontName,
                _fontSizeInPixels,
                _isFontBold));
    }

    public MatchEvaluator MatchEvaluator
    {
        get
        {
            return _matchEvaluator;
        }
    }

    //
    // Static Members
    //

    private static Dictionary _linkTextTruncatorDic;
    private static Dictionary LinkTextTruncatorDic
    {
        get
        {
            if (_linkTextTruncatorDic == null)
            {
                _linkTextTruncatorDic = new Dictionary();
            }
            return _linkTextTruncatorDic;
        }
    }

    private static object _linkTextTruncatorLocker = new object();
    public static LinkTextTruncator GetLinkTextTruncator(int textMaxWidth, string fontName, int fontSizeInPixels, bool isFontBold)
    {
        string specs = textMaxWidth.ToString() + "px_" + fontName.ToLower() + "_" + fontSizeInPixels.ToString() + "px" + (isFontBold ? "_bold" : "");
        if (!LinkTextTruncatorDic.ContainsKey(specs))
        {
            lock (_linkTextTruncatorLocker)
            {
                LinkTextTruncatorDic.Add(specs, new LinkTextTruncator(textMaxWidth, fontName, fontSizeInPixels, isFontBold));
            }
        }

        return LinkTextTruncatorDic[specs];
    }
}

Usage Example
<%= UIHelper.ConvertURLs("Some text that contains URLs", 200, "Verdana", 12, false) %>

Notes About the Code

For the implementation of TextTruncator.TruncateText(), please check my previous post about truncating long text in ASP.NET

If you check the code in UIHelper.ConvertURLs(), you'll find that I use one of the overloaded versions of Regex.Replace() that takes a MatchEvaluator delegate. Every time a match is found (a URL in this case), LinkTextTruncator.TruncateText() is called, which converts the URL into an anchor tag and calls TextTruncator.TruncateText(), which truncates the URL if it's longer than the specified width, otherwise it returns the same URL untruncated.

I chose to cache the LinkTextTruncator objects (by font family, size and weight) in order to avoid creating a new object every time URLs are truncated and so slightly alleviate the overhead on the garbage collector. But, it should be easy to remove the caching if you find it unneeded.

Needless to say, you need to encode the text passed to ConvertURLs() (using HttpUtility.HtmlEncode(), or Html.Encode() if you're using ASP.NET MVC). You should notice that any ampersands in the URL will be converted to & when the text is encoded, but this is not only valid in HTML, it's actually required but often ignored. If for some reason you don't want the ampersands in the URLs to be converted, you can use Replace("&", "&") on the URL in LinkTextTruncator.TruncateText().