Xamarin.Forms MarkdownTextView NuGet Announcement!

A while back I talked about building your own Markdown Label / TextView to your Xamarin applications including Xamarin Native and Xamarin Forms. I decided to put the control I’ve been working off of over on NuGet so anyone can use its basic form.

Note that this control isn’t final, I am working on some refactoring and enhancements, but if you want something that just works, then give it a shot, or contribute to the repository!

Previous Posts

Down below you can find the documentation as it’s found on the GitHub repo!

MarkdownTextView

alt tag

A Xamarin.Forms component to display markdown text in a TextView

Installation

Now on NuGet!

Install-Package MarkdownTextView.Forms

https://www.nuget.org/packages/MarkdownTextView.Forms

Usage

  • Call Init before calling Xamarin.Forms.Init()
  • iOS: SPControls.MarkdownTextView.iOS.MarkdownTextView.Init();
  • Android: SPControls.MarkdownTextView.Droid.MarkdownTextView.Init();
  • Use the control
  • In Xaml:
<ContentPage ...
             xmlns:spcontrols="clr-namespace:SPControls.Forms;assembly=SPControls.MarkdownTextView"
             ...>
  <spcontrols:MarkdownTextView Markdown="{Binding MarkdownString}" />
</ContentPage>
  • or in C#:
var mdTextView = new MarkdownTextView();
mdTextView.Markdown = "# this is my *header* tag";

TODO

  • Add other properties for updating markdown settings
  • Add text color settings
  • Add UWP Support
Advertisement

Markdig Extension – Bad Header Handler

First off, if you haven’t seen Markdig yet, you’re missing out! It has to be the most extensible Markdown processor I’ve ever seen, and it is still incredibly fast. It’s slim enough to confidently use on small .NET Clients like Xamarin, and supports custom output as well (not just HTML).

Because of it’s flexibility and componentization, we are able to customize it without sacrificing performance using their “Extension” framework. The extension we are talking about here is one that ideally would never exist, but solves the problem of malformed Markdown headers. How often do you see wrong headers with the missing space after the “#” in places like Github and WordPress?

Where

#My Header

Should be

# My Header

Well if you’re using Markdig and run into this issue, simply slap this extension into your processing pipeline and worry no more! It even works with a mix of good and bad headers.

Install

You can find it on NuGet or Clone it yourself from Github:

Usage

Add it to your pipeline that you use to parse:

var pipelineBuilder = new MarkdownPipelineBuilder();
pipelineBuilder = MarkdownExtensions.Use<BadHeadersExtension>(pipelineBuilder);
var pipeline = pipelineBuilder.Build();
var html = Markdown.ToHtml(BAD_HEADER_MARKDOWN, _pipeline);

Check out the unit tests in the source code to view a working example.

Source

The gist is a HeadingBlockParser and the Extension itself.

BadHeadingBlockParser.cs

<br />    /// <summary>
    /// Bad heading block parser. Does the same thing as the header parser, but doesn't require a space.
    /// Using a private class to ensure all markdown logic is contained within this service.
    /// </summary>
    public class BadHeadingBlockParser : HeadingBlockParser
    {
        /// <summary>
        /// The head char.
        /// </summary>
        private readonly char _headChar;

        /// <summary>
        /// Initializes a new instance of the <see cref="T:Markdig.BadHeaders.BadHeadingBlockParser"/> class.
        /// </summary>
        /// <param name="headChar">Head char.</param>
        public BadHeadingBlockParser(char headChar)
        {
            _headChar = headChar;
        }

        /// <summary>
        /// Overrides the TryOpen for the heading block parser to ignore the need for spaces
        /// </summary>
        /// <returns>The open.</returns>
        /// <param name="processor">Processor.</param>
        public override BlockState TryOpen(BlockProcessor processor)
        {
            // If we are in a CodeIndent, early exit
            if (processor.IsCodeIndent)
            {
                return BlockState.None;
            }

            // 4.2 ATX headings
            // An ATX heading consists of a string of characters, parsed as inline content, 
            // between an opening sequence of 1–6 unescaped # characters and an optional 
            // closing sequence of any number of unescaped # characters. The opening sequence 
            // of # characters must be followed by a space or by the end of line. The optional
            // closing sequence of #s must be preceded by a space and may be followed by spaces
            // only. The opening # character may be indented 0-3 spaces. The raw contents of 
            // the heading are stripped of leading and trailing spaces before being parsed as 
            // inline content. The heading level is equal to the number of # characters in the 
            // opening sequence.

            // We are not doing this ^^ we don't have the spaces... so we need to handle that adjusted logic here
            var column = processor.Column;
            var line = processor.Line;
            var sourcePosition = line.Start;
            var c = line.CurrentChar;
            var matchingChar = c;

            int leadingCount = 0;

            // get how many of the headChar we have and limit to 6 (h6 is the last handled header)
            while (c == _headChar && leadingCount <= 6)
            {
                if (c != matchingChar)
                {
                    break;
                }
                c = line.NextChar();
                leadingCount++;
            }

            // A space is NOT required after leading #
            if (leadingCount > 0 && leadingCount <= 6)
            {
                // Move to the content
                var headingBlock = new HeadingBlock(this)
                {
                    HeaderChar = matchingChar,
                    Level = leadingCount,
                    Column = column,
                    Span = { Start = sourcePosition }
                };
                processor.NewBlocks.Push(headingBlock);
                processor.GoToColumn(column + leadingCount); // no +1 - skip the space

                // Gives a chance to parse attributes
                if (TryParseAttributes != null)
                {
                    TryParseAttributes(processor, ref processor.Line, headingBlock);
                }

                // The optional closing sequence of #s must not be preceded by a space and may be followed by spaces only.
                int endState = 0;
                int countClosingTags = 0;
                for (int i = processor.Line.End; i >= processor.Line.Start; i--)  // Go up to Start in order to match the no space after the first ###
                {
                    c = processor.Line.Text[i];
                    if (endState == 0)
                    {
                        if (c.IsSpaceOrTab())
                        {
                            continue;
                        }
                        endState = 1;
                    }
                    if (endState == 1)
                    {
                        if (c == matchingChar)
                        {
                            countClosingTags++;
                            continue;
                        }

                        if (countClosingTags > 0)
                        {
                            if (c.IsSpaceOrTab())
                            {
                                processor.Line.End = i - 1;
                            }
                            break;
                        }
                        else
                        {
                            break;
                        }
                    }
                }

                // Setup the source end position of this element
                headingBlock.Span.End = processor.Line.End;

                // We expect a single line, so don't continue
                return BlockState.Break;
            }

            // Else we don't have an header
            return BlockState.None;
        }
    }

 
Then we use the Parser in the Extension:

BadHeadersExtension.cs

<br />    /// <summary>
    /// Markdig markdown extension for handling bad markdown for titles
    /// </summary>
    public class BadHeadersExtension : IMarkdownExtension
    {
        /// <summary>
        /// Sets up the extension to use the badheading block parser
        /// </summary>
        /// <returns>The setup.</returns>
        /// <param name="pipeline">Pipeline.</param>
        public void Setup(MarkdownPipelineBuilder pipeline)
        {
            if (!pipeline.BlockParsers.Contains<BadHeadingBlockParser>())
            {
                // Insert the parser before any other parsers and use '#' as the character identifier
                pipeline.BlockParsers.Insert(0, new BadHeadingBlockParser('#'));
            }
        }

        /// <summary>
        /// Not needed
        /// </summary>
        /// <returns>The setup.</returns>
        /// <param name="pipeline">Pipeline.</param>
        /// <param name="renderer">Renderer.</param>
        public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
        {
            // not needed
        }
    }

If you like what you see, don’t forget to follow me on twitter @Suave_Pirate, check out my GitHub, and subscribe to my blog to learn more mobile developer tips and tricks!

Interested in sponsoring developer content? Message @Suave_Pirate on twitter for details.

Xamarin.Controls – Creating your own Markdown TextBlock in UWP

After talking about using the MarkdownTextView I created and how to accomplish rendering Markdown in iOS and Android without a WebView, I received a few requests to do it for UWP as well.

Check these previous posts out:

  1. Xamarin.Controls – MarkdownTextView
  2. Xamarin.Controls – Creating Your Own Android Markdown TextView
  3. Xamarin.Controls – Creating Your Own iOS Markdown UILabel

Let’s dive in to rendering markdown into a TextBlock in UWP. We’ll break it down into a few steps:

  1. Parse a markdown string into an html string
  2. Create a Behavior for the TextBlock
  3. Parse the html into relevant Span tags in the behavior
  4. Use the new Behavior in our XAML or C#

 

Parsing Markdown

This is traditionally the most difficult part. However, our community is awesome and open sourced a Markdown processor with an MIT license (so use it freely!).

I won’t put the actual code in here because it is overwhelmingly long, but here is a link to it:

https://github.com/SuavePirate/MarkdownTextView/blob/master/src/Forms/SPControls.MarkdownTextView/SPControls.MarkdownTextView/Markdown.cs

Note that this is portable, so you can use it in a PCL without a problem and share it between your platforms.

Now that we have our means of processing the Markdown, let’s create some extension methods to make it easier to parse and do some extra processing like cleaning up our tags, line breaks, etc.

#region MARKDOWN STYLES
private const string ORIGINAL_PATTERN_BEGIN = "<code>";
private const string ORIGINAL_PATTERN_END = "</code>";
private const string PARSED_PATTERN_BEGIN = "<font color=\"#888888\" face=\"monospace\"><tt>";
private const string PARSED_PATTERN_END = "</tt></font>";

#endregion

public static string ToHtml(this string markdownText)
{
    var markdownOptions = new MarkdownOptions
    {
        AutoHyperlink = true,
        AutoNewlines = false,
        EncodeProblemUrlCharacters = false,
        LinkEmails = true,
        StrictBoldItalic = true
    };
    var markdown = new Markdown(markdownOptions);
    var htmlContent = markdown.Transform(markdownText);
    var regex = new Regex("\n");
    htmlContent = regex.Replace(htmlContent, "
");

    var html = htmlContent.HtmlWrapped();
    var regex2 = new Regex("\r");
    html = regex.Replace(html, string.Empty);
    html = regex2.Replace(html, string.Empty);
    return html;
}

///
<summary>
/// Wrap html with a full html tag
/// </summary>
/// <param name="html"></param>
/// <returns></returns>
public static string HtmlWrapped(this string html)
{
    if (!html.StartsWith("<html>") || !html.EndsWith("</html>"))
    {
        html = $"<html><body>{html}</body></html>";
    }
    return html;
}

///<summary>
/// Parses html with code or pre tags and gives them proper
/// styled spans so that Android can parse it properly
/// </summary>
/// <param name="htmlText">The html string</param>
/// <returns>The html string with parsed code tags</returns>
public static string ParseCodeTags(this string htmlText)
{
    if (htmlText.IndexOf(ORIGINAL_PATTERN_BEGIN) < 0) return htmlText;
    var regex = new Regex(ORIGINAL_PATTERN_BEGIN);
    var regex2 = new Regex(ORIGINAL_PATTERN_END);

    htmlText = regex.Replace(htmlText, PARSED_PATTERN_BEGIN);
    htmlText = regex2.Replace(htmlText, PARSED_PATTERN_END);
    htmlText = htmlText.TrimLines();
    return htmlText;
}

public static bool EqualsIgnoreCase(this string text, string text2)
{
    return text.Equals(text2, StringComparison.CurrentCultureIgnoreCase);
}

public static string ReplaceBreaks(this string html)
{
    var regex = new Regex("
");
    html = regex.Replace(html, "\n");
    return html;
}

public static string ReplaceBreaksWithSpace(this string html)
{
    var regex = new Regex("
");
    html = regex.Replace(html, " ");
    return html;
}

public static string TrimLines(this string originalString)
{
    originalString = originalString.Trim('\n');
    return originalString;
}

Now we can properly parse markdown to html:

var markdown = "# Hello *World*";
var html = markdown.ToHtml();
// html = "<h1>Hello <strong>World</strong></h1>"

Create a Behavior

I’m going to take some inspiration from Shawn Kendrot and his post here.

The first thing Shawn does is implement a base Behavior class, so let’s go ahead and use that here:

    // WinRT Implementation of the base Behavior classes
    public abstract class Behavior<T> : Behavior where T : DependencyObject
    {
        protected T AssociatedObject
        {
            get { return base.AssociatedObject as T; }
        }

        protected override void OnAttached()
        {
            base.OnAttached();
            if (this.AssociatedObject == null) throw new InvalidOperationException("AssociatedObject is not of the right type");
        }
    }

    public abstract class Behavior : DependencyObject, IBehavior
    {
        public void Attach(DependencyObject associatedObject)
        {
            AssociatedObject = associatedObject;
            OnAttached();
        }

        public void Detach()
        {
            OnDetaching();
        }

        protected virtual void OnAttached()
        {

        }

        protected virtual void OnDetaching()
        {

        }

        protected DependencyObject AssociatedObject { get; set; }

        DependencyObject IBehavior.AssociatedObject
        {
            get { return this.AssociatedObject; }
        }
    }

Now we can create our actual Behavior implementation to handle the html parsing. If you want the entire file, you can find it in a gist here. Or keep reading so we can break it down.

HtmlTextBehavior.cs

public class HtmlTextBehavior : Behavior<TextBlock>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.Loaded += OnAssociatedObjectLoaded;
        AssociatedObject.LayoutUpdated += OnAssociatedObjectLayoutUpdated;
    }
 
    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.Loaded -= OnAssociatedObjectLoaded;
        AssociatedObject.LayoutUpdated -= OnAssociatedObjectLayoutUpdated;
    }
 
    private void OnAssociatedObjectLayoutUpdated(object sender, object o)
    {
        UpdateText();
    }
 
    private void OnAssociatedObjectLoaded(object sender, RoutedEventArgs routedEventArgs)
    {
        UpdateText();
        AssociatedObject.Loaded -= OnAssociatedObjectLoaded;
    }
 
    private void UpdateText()
    {
        if (AssociatedObject == null) return;
        if (string.IsNullOrEmpty(AssociatedObject.Text)) return;

        string text = AssociatedObject.Text;

        // Just incase we are not given text with elements.
        string modifiedText = string.Format("<div>{0}</div>", text);

        // reset the text because we will add to it.
        AssociatedObject.Inlines.Clear();
        try
        {
            var element = XElement.Parse(modifiedText);
            ParseText(element, AssociatedObject.Inlines);
        }
        catch (Exception)
        {
            // if anything goes wrong just show the html
            AssociatedObject.Text = text;
        }
        AssociatedObject.LayoutUpdated -= OnAssociatedObjectLayoutUpdated;
        AssociatedObject.Loaded -= OnAssociatedObjectLoaded;
    }
    
    /// <summary>
    /// Traverses the XElement and adds text to the InlineCollection.
    /// </summary>
    /// <param name="element"></param>
    /// <param name="inlines"></param>
    private static void ParseText(XElement element, InlineCollection inlines)
    {
        if (element == null) return;

        InlineCollection currentInlines = inlines;
        var elementName = element.Name.ToString().ToUpper();
        switch (elementName)
        {
            case ElementA:
                var link = new Hyperlink();
                var href = element.Attribute("href");
                if(href != null)
                {
                    try
                    {
                        link.NavigateUri = new Uri(href.Value);
                    }
                    catch (System.FormatException) { /* href is not valid */ }
                }
                inlines.Add(link);
                currentInlines = link.Inlines;
                break;
            case ElementB:
            case ElementStrong:
                var bold = new Bold();
                inlines.Add(bold);
                currentInlines = bold.Inlines;
                break;
            case ElementI:
            case ElementEm:
                var italic = new Italic();
                inlines.Add(italic);
                currentInlines = italic.Inlines;
                break;
            case ElementU:
                var underline = new Underline();
                inlines.Add(underline);
                currentInlines = underline.Inlines;
                break;
            case ElementBr:
                inlines.Add(new LineBreak());
                break;
            case ElementP:
                // Add two line breaks, one for the current text and the second for the gap.
                if (AddLineBreakIfNeeded(inlines))
                {
                    inlines.Add(new LineBreak());
                }

                Span paragraphSpan = new Span();
                inlines.Add(paragraphSpan);
                currentInlines = paragraphSpan.Inlines;
                break;
            // TODO: Add ElementH1 - ElementH6 handlers here. They should behave the same way as ElementP with increased font size.
            case ElementLi:
                inlines.Add(new LineBreak());
                inlines.Add(new Run { Text = " • " });
                break;
            case ElementUl:
            case ElementDiv:
                AddLineBreakIfNeeded(inlines);
                Span divSpan = new Span();
                inlines.Add(divSpan);
                currentInlines = divSpan.Inlines;
                break;
        }
        foreach (var node in element.Nodes())
        {
            XText textElement = node as XText;
            if (textElement != null)
            {
                currentInlines.Add(new Run { Text = textElement.Value });
            }
            else
            {
                ParseText(node as XElement, currentInlines);
            }
        }
        // Add newlines for paragraph tags
        if (elementName == ElementP)
        {
            currentInlines.Add(new LineBreak());
        }
    }
    /// <summary>
    /// Check if the InlineCollection contains a LineBreak as the last item.
    /// </summary>
    /// <param name="inlines"></param>
    /// <returns></returns>
    private static bool AddLineBreakIfNeeded(InlineCollection inlines)
    {
        if (inlines.Count > 0)
        {
            var lastInline = inlines[inlines.Count - 1];
            while ((lastInline is Span))
            {
                var span = (Span)lastInline;
                if (span.Inlines.Count > 0)
                {
                    lastInline = span.Inlines[span.Inlines.Count - 1];
                }
            }
            if (!(lastInline is LineBreak))
            {
                inlines.Add(new LineBreak());
                return true;
            }
        }
        return false;
    }
}

That’s a lot of stuff to look at. Let’s break it down. The core of the processing is in the UpdateText and ParseText methods.

Essentially what we are doing here is some layout management, and then parsing the html (parsed into an XElement). It’s different from the approaches we took in Android and iOS where there are native APIs that can parse HTML automatically. But if you looked at the Android post I made before, you’ll remember the extra TagHandler we created that took unsupported html element types and parsed them into Spans for proper formatting. What we did in that situation is a simplified version of what we accomplish here. We take relevant html tags, and create Spans, Bolds, HyperLinks and other relevant types in order to be able to render it within our final TextBlock.

There are a few element types in the parsing that are not included, but can be pretty easily added. For example, code, pre, h1h6, etc. However, these can be easily added to the switch statement! Most of these are going to be handled the same way that ElementP does. So you can add them, and adjust the FontSize or FontFamily of your Span!

Applying the Behavior

Now that we have our HtmlTextBehavior built out, we can apply it to our actual TextBlock!

In XAML:

<TextBlock Text="{Binding MyHtmlText}" FontSize="20" TextWrapping="Wrap">
    <Interactivity:Interaction.Behaviors>
        <local:HtmlTextBehavior />
    </Interactivity:Interaction.Behaviors>
</TextBlock>

Now we can render out MyHtmlText property from our ViewModel directly in our TextBlock!

Xamarin.Controls – Creating Your Own iOS Markdown UILabel

In a previous post, I talked about a Xamarin.Forms control I put on GitHub to allow you to display Markdown strings properly. It worked by parsing the markdown into html, then using the custom renderer to display the html string in a TextView for Android and a UILabel for iOS.

However, we are not always using Xamarin.Forms, so let’s take a look at achieving the same functionality with just Xamarin.iOS.

If you’re looking for the same type of solution, check out my previous post: Xamarin.Controls – Creating Your Own Android Markdown TextView

We’ll break it down into a few parts:

  1. Parse a markdown string into an html string
  2. Parse the html string into an NSAttributedString
  3. Set the AttributedText of the UILabel

Parsing Markdown

This is traditionally the most difficult part. However, our community is awesome and open sourced a Markdown processor with an MIT license (so use it freely!).

I won’t put the actual code in here because it is overwhelmingly long, but here is a link to it:

https://github.com/SuavePirate/MarkdownTextView/blob/master/src/Forms/SPControls.MarkdownTextView/SPControls.MarkdownTextView/Markdown.cs

Note that this is portable, so you can use it in a PCL without a problem and share it between your platforms.

Now that we have our means of processing the Markdown, let’s create some extension methods to make it easier to parse and do some extra processing like cleaning up our tags, line breaks, etc.

#region MARKDOWN STYLES
private const string ORIGINAL_PATTERN_BEGIN = "<code>";
private const string ORIGINAL_PATTERN_END = "</code>";
private const string PARSED_PATTERN_BEGIN = "<font color=\"#888888\" face=\"monospace\"><tt>";
private const string PARSED_PATTERN_END = "</tt></font>";
 
#endregion
 
public static string ToHtml(this string markdownText)
{
    var markdownOptions = new MarkdownOptions
    {
        AutoHyperlink = true,
        AutoNewlines = false,
        EncodeProblemUrlCharacters = false,
        LinkEmails = true,
        StrictBoldItalic = true
    };
    var markdown = new Markdown(markdownOptions);
    var htmlContent = markdown.Transform(markdownText);
    var regex = new Regex("\n");
    htmlContent = regex.Replace(htmlContent, "<br/>");
 
    var html = htmlContent.HtmlWrapped();
    var regex2 = new Regex("\r");
    html = regex.Replace(html, string.Empty);
    html = regex2.Replace(html, string.Empty);
    return html;
}
 
///
<summary>
/// Wrap html with a full html tag
/// </summary>
/// <param name="html"></param>
/// <returns></returns>
public static string HtmlWrapped(this string html)
{
    if (!html.StartsWith("<html>") || !html.EndsWith("</html>"))
    {
        html = $"<html><body>{html}</body></html>";
    }
    return html;
}
 
///<summary>
/// Parses html with code or pre tags and gives them proper
/// styled spans so that Android can parse it properly
/// </summary>
/// <param name="htmlText">The html string</param>
/// <returns>The html string with parsed code tags</returns>
public static string ParseCodeTags(this string htmlText)
{
    if (htmlText.IndexOf(ORIGINAL_PATTERN_BEGIN) < 0) return htmlText;
    var regex = new Regex(ORIGINAL_PATTERN_BEGIN);
    var regex2 = new Regex(ORIGINAL_PATTERN_END);
 
    htmlText = regex.Replace(htmlText, PARSED_PATTERN_BEGIN);
    htmlText = regex2.Replace(htmlText, PARSED_PATTERN_END);
    htmlText = htmlText.TrimLines();
    return htmlText;
}
 
public static bool EqualsIgnoreCase(this string text, string text2)
{
    return text.Equals(text2, StringComparison.CurrentCultureIgnoreCase);
}
 
public static string ReplaceBreaks(this string html)
{
    var regex = new Regex("<br/>");
    html = regex.Replace(html, "\n");
    return html;
}
 
public static string ReplaceBreaksWithSpace(this string html)
{
    var regex = new Regex("<br/>");
    html = regex.Replace(html, " ");
    return html;
}
 
public static string TrimLines(this string originalString)
{
    originalString = originalString.Trim('\n');
    return originalString;
}

Now we can properly parse markdown to html:

var markdown = "# Hello *World*";
var html = markdown.ToHtml();
// html = "<h1>Hello <strong>World</strong></h1>"

Parsing Html to NSAttributedString

The next step is to take our processed html string and turn it into something that an iOS UILabel can use.

How about another extension method?

public static NSAttributedString ToAttributedText(this string html)
{
    NSError error = new NSError();
     try
     {
         var htmlData = NSData.FromString(html);
         if (htmlData != null && htmlData.Length > 0)
         {
             NSAttributedString attributedString = null;

             attributedString = new NSAttributedString(htmlData, new DocumentType = NSDocumentType.HTML, StringEncoding = NSStringEncoding.UTF8    }, ref error);
             return attributedString;
         }
         return null;
     }
     catch (Exception ex)
     {
         Console.WriteLine(ex);
         return null;
     }
}

We’ll add one more extension method just to make the full conversion from markdown to html to formatted html:

public static NSAttributedString MarkdownToHtml(this string markdown) 
{
    return markdown.ToHtml().ToAttributedString();
}

Now for the very last bit: Show it!

Assiging To the UILabel

Pretty simple now that we have our useful extension methods!

var markdown = "# Hello *World*";
myLabel.AttributedText = markdown.MarkdownToHtml();

That’s it! Now you can see some cool stylized text in your labels.

Xamarin.Controls – Creating Your Own Android Markdown TextView

In a previous post, I talked about a Xamarin.Forms control I put on GitHub to allow you to display Markdown strings properly. It worked by parsing the markdown into html, then using the custom renderer to display the html string in a TextView for Android and a UILabel for iOS.

However, we are not always using Xamarin.Forms, so let’s take a look at achieving the same functionality with just Xamarin.Android.

We’ll break it down into a few parts:

  1. Parse a markdown string into an html string
  2. Parse the html string into an ICharSequence
  3. Create an extra tag handler to help show html elements that are not traditionally supported in a TextView
  4. Set the TextFormatted of the TextView

Parsing Markdown

This is traditionally the most difficult part. However, our community is awesome and open sourced a Markdown processor with an MIT license (so use it freely!).
I won’t put the actual code in here because it is overwhelmingly long, but here is a link to it:

https://github.com/SuavePirate/MarkdownTextView/blob/master/src/Forms/SPControls.MarkdownTextView/SPControls.MarkdownTextView/Markdown.cs

Note that this is portable, so you can use it in a PCL without a problem and share it between your platforms.

Now that we have our means of processing the Markdown, let’s create some extension methods to make it easier to parse and do some extra processing like cleaning up our tags, line breaks, etc.


 #region MARKDOWN STYLES
private const string ORIGINAL_PATTERN_BEGIN = "<code>";
private const string ORIGINAL_PATTERN_END = "</code>";
private const string PARSED_PATTERN_BEGIN = "<font color=\"#888888\" face=\"monospace\"><tt>";
private const string PARSED_PATTERN_END = "</tt></font>";

#endregion

public static string ToHtml(this string markdownText)
{
    var markdownOptions = new MarkdownOptions
    {
        AutoHyperlink = true,
        AutoNewlines = false,
        EncodeProblemUrlCharacters = false,
        LinkEmails = true,
        StrictBoldItalic = true
    };
    var markdown = new Markdown(markdownOptions);
    var htmlContent = markdown.Transform(markdownText);
    var regex = new Regex("\n");
    htmlContent = regex.Replace(htmlContent, "<br/>");

    var html = htmlContent.HtmlWrapped();
    var regex2 = new Regex("\r");
    html = regex.Replace(html, string.Empty);
    html = regex2.Replace(html, string.Empty);
    return html;
}

///
<summary>
/// Wrap html with a full html tag
/// </summary>

/// <param name="html"></param>
/// <returns></returns>
public static string HtmlWrapped(this string html)
{
    if (!html.StartsWith("<html>") || !html.EndsWith("</html>"))
    {
        html = $"<html><body>{html}</body></html>";
    }
    return html;
}

///<summary>
/// Parses html with code or pre tags and gives them proper
/// styled spans so that Android can parse it properly
/// </summary>

/// <param name="htmlText">The html string</param>
/// <returns>The html string with parsed code tags</returns>
public static string ParseCodeTags(this string htmlText)
{
    if (htmlText.IndexOf(ORIGINAL_PATTERN_BEGIN) < 0) return htmlText;
    var regex = new Regex(ORIGINAL_PATTERN_BEGIN);
    var regex2 = new Regex(ORIGINAL_PATTERN_END);

    htmlText = regex.Replace(htmlText, PARSED_PATTERN_BEGIN);
    htmlText = regex2.Replace(htmlText, PARSED_PATTERN_END);
    htmlText = htmlText.TrimLines();
    return htmlText;
}

public static bool EqualsIgnoreCase(this string text, string text2)
{
    return text.Equals(text2, StringComparison.CurrentCultureIgnoreCase);
}

public static string ReplaceBreaks(this string html)
{
    var regex = new Regex("<br/>");
    html = regex.Replace(html, "\n");
    return html;
}

public static string ReplaceBreaksWithSpace(this string html)
{
    var regex = new Regex("<br/>");
    html = regex.Replace(html, " ");
    return html;
}

public static string TrimLines(this string originalString)
{
    originalString = originalString.Trim('\n');
    return originalString;
}


Now we can properly parse markdown to html:


var markdown = "# Hello *World*";
var html = markdown.ToHtml();
// html = "<h1>Hello <strong>World</strong></h1>"

Parsing Html to ICharSequence

The next step is to take our processed html string and turn it into something that an Android TextView can use.

How about another extension method?


public static ICharSequence ToFormattedHtml(this string htmlText)
{
    try
    {
        ICharSequence html;
        if(Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.N)
        {
            html = Html.FromHtml(htmlText.ParseCodeTags(), FromHtmlOptions.ModeLegacy, null, new HtmlTagHandler()) as ICharSequence;
        }
        else
        {
            // handle legacy builds
            html = Html.FromHtml(htmlText.ParseCodeTags(), null, new HtmlTagHandler()) as ICharSequence;
        }
        // this is required to get rid of the end two "\n" that android adds with Html.FromHtml
        // see: http://stackoverflow.com/questions/16585557/extra-padding-on-textview-with-html-contents for example
        while (html.CharAt(html.Length() - 1) == '\n')
        {
            html = html.SubSequenceFormatted(0, html.Length() - 1);
        }
        return html;
    }
    catch
    {
        return null;
    }
}

To breakdown what this is doing: We check against the sdk version we are running against to ensure we don’t use an obsolete api, then call to get the Html as an ICharSequence. Then, we clean up the output and return it.

We’ll add one more extension method just to make the full conversion from markdown to html to formatted html:

public static ICharSequence MarkdownToHtml(this string markdown)
{
    return markdown.ToHtml().ToFormattedHtml();
}

There’s one piece here that is unique, and that is the HtmlTagHandler() used in the call to Html.FromHtml.

Extending the Tag Handler

Android, unfortunately, doesn’t support many html elements in its TextFormatted. Daniel Lew has a great post about the supported types.

In order to get around this, we need to use tags that are supported and combine those to simulate the missing tags; especially for things like ul and ol.

I’ve translated a popular processor from Java to C# so we can use it in Xamarin: HtmlTagHandler.cs

/// <summary>
/// Custom tag handler for parsing more html tags for android textviews
/// This is a port/translation of https://github.com/sufficientlysecure/html-textview/blob/master/HtmlTextView/src/main/java/org/sufficientlysecure/htmltextview/HtmlTagHandler.java
/// Which is considered the most robust handler
/// </summary>
public class HtmlTagHandler : Java.Lang.Object, Android.Text.Html.ITagHandler
{
    /**
     * Keeps track of lists (ol, ul). On bottom of Stack is the outermost list
     * and on top of Stack is the most nested list
     */
    Stack<String> lists = new Stack<String>();
    /**
     * Tracks indexes of ordered lists so that after a nested list ends
     * we can continue with correct index of outer list
     */
    Stack<Int32> olNextIndex = new Stack<Int32>();
    /**
     * List indentation in pixels. Nested lists use multiple of this.
     */
    private static int indent = 10;
    private static int listItemIndent = indent * 2;
    private static BulletSpan bullet = new BulletSpan(indent);
    private class Ul : Java.Lang.Object
    {
    }
    private class Ol : Java.Lang.Object
    {
    }

    private class Code : Java.Lang.Object
    {
    }
    private class Center : Java.Lang.Object
    {
    }

    private class Strike : Java.Lang.Object
    {
    }


    public void HandleTag(Boolean opening, String tag, Android.Text.IEditable output, IXMLReader xmlReader)
    {
        if (opening)
        {
            if (tag.ToLower() == "ul")
            {
                lists.Push(tag);
            }
            else if (tag.EqualsIgnoreCase("ol"))
            {
                lists.Push(tag);
                olNextIndex.Push(1);
            }
            else if (tag.EqualsIgnoreCase("li"))
            {
                if (output.Length() > 0 && output.CharAt(output.Length() - 1) != '\n')
                {
                    output.Append("\n");
                }
                String parentList = lists.Peek();
                if (parentList.EqualsIgnoreCase("ol"))
                {
                    Start(output, new Ol());
                     output.Append(olNextIndex.Peek().ToString()).Append('.').Append(' ');
                    olNextIndex.Push(olNextIndex.Pop() + 1);
                }
                else if (parentList.EqualsIgnoreCase("ul"))
                {
                    Start(output, new Ul());
                }
            }
            else if (tag.EqualsIgnoreCase("code"))
            {
                Start(output, new Code());
            }
            else if (tag.EqualsIgnoreCase("center"))
            {
                Start(output, new Center());
            }
            else if (tag.EqualsIgnoreCase("s") || tag.EqualsIgnoreCase("strike"))
            {
                Start(output, new Strike());
            }
        }
        else
        {
            if (tag.EqualsIgnoreCase("ul"))
            {
                lists.Pop();
            }
            else if (tag.EqualsIgnoreCase("ol"))
            {
                lists.Pop();
                olNextIndex.Pop();
            }
            else if (tag.EqualsIgnoreCase("li"))
            {
                if (lists.Peek().EqualsIgnoreCase("ul"))
                {
                    if (output.Length() > 0 && output.CharAt(output.Length() - 1) != '\n')
                    {
                        output.Append("\n");
                    }
                    // Nested BulletSpans increases distance between bullet and Text, so we must prevent it.
                    int bulletMargin = indent;
                    if (lists.Count > 1)
                    {
                        bulletMargin = indent - bullet.GetLeadingMargin(true);
                        if (lists.Count > 2)
                        {
                            // This get's more complicated when we add a LeadingMarginSpan into the same line:
                            // we have also counter it's effect to BulletSpan
                            bulletMargin -= (lists.Count - 2) * listItemIndent;
                        }
                    }
                    BulletSpan newBullet = new BulletSpan(bulletMargin);
                    End(output, typeof(Ul), false,
                            new LeadingMarginSpanStandard(listItemIndent * (lists.Count - 1)),
                            newBullet);
                }
                else if (lists.Peek().EqualsIgnoreCase("ol"))
                {
                    if (output.Length() > 0 && output.CharAt(output.Length() - 1) != '\n')
                    {
                        output.Append("\n");
                    }
                    int numberMargin = listItemIndent * (lists.Count - 1);
                    if (lists.Count > 2)
                    {
                        // Same as in ordered lists: counter the effect of nested Spans
                        numberMargin -= (lists.Count - 2) * listItemIndent;
                    }
                    End(output, typeof(Ol), false, new LeadingMarginSpanStandard(numberMargin));
               }
            }
            else if (tag.EqualsIgnoreCase("code"))
            {
                End(output, typeof(Code), false, new TypefaceSpan("monospace"));
            }
            else if (tag.EqualsIgnoreCase("center"))
            {
                End(output, typeof(Center), true, new AlignmentSpanStandard(Layout.Alignment.AlignCenter));
            }
            else if (tag.EqualsIgnoreCase("s") || tag.EqualsIgnoreCase("strike"))
            {
                End(output, typeof(Strike), false, new StrikethroughSpan());
            }
        }
    }

    /**
     * Mark the opening tag by using private classes
     */
    private void Start(IEditable output, Java.Lang.Object mark)
    {
        int len = output.Length();
        output.SetSpan(mark, len, len, SpanTypes.MarkMark);
    }

    /**
     * Modified from {@link Android.Text.Html}
     */
    private void End(IEditable output, Type kind, Boolean paragraphStyle, params Java.Lang.Object[] replaces)
    {
        Java.Lang.Object obj = GetLast(output, kind);
        // start of the tag
        int where = output.GetSpanStart(obj);
        // end of the tag
        int len = output.Length();
        output.RemoveSpan(obj);

       if (where != len)
       {
            int thisLen = len;
            // paragraph styles like AlignmentSpan need to end with a new line!
            if (paragraphStyle)
            {
                output.Append("\n");
                thisLen++;
            }
            foreach (Java.Lang.Object replace in replaces)
            {
                output.SetSpan(replace, where, thisLen, SpanTypes.ExclusiveExclusive);
            }


       }
   }

    /**
     * Get last marked position of a specific tag kind (private class)
     */
    private static Java.Lang.Object GetLast(IEditable Text, Type kind)
    {
        Java.Lang.Object[] objs = Text.GetSpans(0, Text.Length(), Java.Lang.Class.FromType(kind)); // TODO: LOl will this work?
        if (objs.Length == 0)
        {
            return null;
        }
        else
        {
            for (int i = objs.Length; i > 0; i--)
            {
                if (Text.GetSpanFlags(objs[i - 1]) == SpanTypes.MarkMark)
                {
                    return objs[i - 1];
                }
            }
           return null;
        }
    }

}

So now we can properly display list elements as well as code elements!

Now for the very last bit: Show it!

Assiging To the TextView

Pretty simple now that we have our useful extension methods!

var markdown = "# Hello *World*";
var myTextView = FindViewById<TextView>(Resource.Id.MyTextView);
myTextView.TextFormatted = markdown.MarkdownToHtml();

That’s it! Now you can see some cool stylized text in your labels.

Xamarin.Controls – MarkdownTextView

Markdown is cool, and we as developers are seeing more and more of it all around us; documentation, chat, or even just trying to store dynamic displayed strings without storing the crazy extra characters of xml or html.

Markdown is used by parsing it through a series of Regex comparisons and outputting the html equivalent of what is to be displayed. In web, that’s awesome! Parse the markdown, dump it in the DOM. Bam.

But what about native mobile development? I’ve seen solutions out there where people just create a WebView on their platform, and dump the parsed html from the markdown string into that WebView. It might get the job done in a simple scenario, but WebViews are hefty and really should be avoided.

I’ve created a free and open sourced solution to this problem – The MarkdownTextView.

Here’s the repo: https://github.com/SuavePirate/MarkdownTextView

This is a Xamarin.Forms control that renders on iOS and Android. It consumes a string through the Markdown property, and then the custom render on the respective platform renders it out via a UILabel on iOS and a TextView on Android.

Using the MarkdownTextView

To use the control, clone the repository and reference the library projects in your application. Reference the PCL in your PCL or Shared Library, and reference the native library with the renderer in your platform specific project.

In your platform projects, be sure to call:


MarkdownTextView.Init();

Then use the control in either your XAML or your C# page/view:


<ContentPage ...
xmlns:spcontrols="clr-namespace:SPControls.Forms;assembly=SPControls.MarkdownTextView"
...>
<spcontrols:MarkdownTextView Markdown="{Binding MarkdownString}" />
</ContentPage>

var mdTextView = new MarkdownTextView();
mdTextView.Markdown = "# this is my *header* tag";

How It Works

There are five core steps to how the control works:

  1. The Xamarin.Forms control takes the string value from the Markdown property
  2. The Native custom renderer gets the updated string
  3. The native control calls the PCL Markdown class to parse the string into an html string (with some extra clean up)
  4. The native library parses the html string into a platform specific attributed string that the control can render.
    1. iOS: NSAttributedString
    2. Android: ICharSequence
  5. The native renderer then renders the value from the above type into the native control.
    1. iOS: UILabel.AttributedText
    2. Android: TextView.TextFormatted

There are a few smaller steps specific to the platform to handle some tags that are not normally built in, but that’s the basics.

In a follow-up blog post, I will talk about the details of how this is accomplished for each platform, and how to take this outside Xamarin.Forms to use in your native Xamarin applications.