4

Recently, Xamarin.Forms Label has support for FormattedText. So I want to use that FormattedText property to load the markdown text. For example,

I have below kinds of markdown text,

 1. **Hi**, How are you?
 2. Hello **John**, _Good Morning_

I want to automatically convert the markdown text like above into the FormattedString to set to the Label.FormattedText.

Could anyone help me to achieve this?

Note: I don't want to go for third party MarkdownView controls since they are heavy-weight control and having some issues on rendering the UI when I checked on Xamarin.Forms iOS.

Selvamz
  • 362
  • 3
  • 16

2 Answers2

1

Finally, I wrote my own parsing and converted the markdown text into the FormattedString.

public static FormattedString GetFormattedString(this string text, double defaultFontSize)
    {
        var boldFormat = "**";
        var italicFormat = "_";
        var formatString = new FormattedString();
        var temp = text;
        while(!string.IsNullOrWhiteSpace(temp))
        {
            try
            {
                var boldIndex = temp.IndexOf(boldFormat);
                var italicIndex = temp.IndexOf(italicFormat);

                if (italicIndex >= 0 && (italicIndex < boldIndex || boldIndex < 0))
                {
                    if (italicIndex > 0)
                    {
                        var t = temp.Substring(0, italicIndex);
                        formatString.Spans.Add(new Span() { Text = t });
                    }
                    temp = temp.Substring(italicIndex + 1);
                    var next = temp.IndexOf(italicFormat);
                    var t1 = temp.Substring(0, next);
                    formatString.Spans.Add(new Span() { Text = t1, FontAttributes = FontAttributes.Italic, FontSize = defaultFontSize });
                    temp = temp.Substring(next + 1);
                }
                else if (boldIndex >= 0)
                {
                    if (boldIndex > 0)
                    {
                        var t = temp.Substring(0, boldIndex);
                        formatString.Spans.Add(new Span() { Text = t });
                    }
                    temp = temp.Substring(boldIndex + 2);
                    var next = temp.IndexOf(boldFormat);
                    var t1 = temp.Substring(0, next);
                    formatString.Spans.Add(new Span() { Text = t1, FontAttributes = FontAttributes.Bold, FontSize = defaultFontSize });
                    temp = temp.Substring(next + 2);
                }
                else
                {
                    formatString.Spans.Add(new Span() { Text = temp, FontSize = defaultFontSize });
                    break;
                }
            }
            catch (Exception)
            {
                formatString = new FormattedString();
                formatString.Spans.Add(new Span() { Text = text, FontSize = defaultFontSize });
                break;
            }
        }
        return formatString;
    }

Note: Currently, I have added code bold and italic formats only. Need to extend it for the required markdown formats.

Selvamz
  • 362
  • 3
  • 16
0

Here is an exampel of how to write a custom renderer for the Microsoft.Toolkit.Parsers, in order to convert some basic Markdown to Xamarin Label FormattedText:

class LabelMarkdownRenderer : MarkdownRendererBase
{
    private readonly Stack<MarkdownInlineType> _inlineTypeStack;

    private readonly IDictionary<int, Style> _headerStyles;
    private readonly Color _urlLinkColor;

    private int _headerLevel;

    public LabelMarkdownRenderer(
        MarkdownDocument document,
        Color urlLinkColor,
        IDictionary<int, Style> headerStyles) : base(document)
    {
        _inlineTypeStack = new Stack<MarkdownInlineType>();
        _headerStyles = headerStyles;
        _urlLinkColor = urlLinkColor;
    }

    protected override void RenderParagraph(ParagraphBlock element, IRenderContext context)
    {
        if (element.Inlines.Any())
        {
            if (element.Inlines.Any())
            {
                RenderInlineChildren(element.Inlines, context);
            }

            if (context.Parent is FormattedString fs)
            {
                if (fs.Spans?.Any() ?? false)
                {
                    fs.Spans.Last().Text += Environment.NewLine;
                }
            }
        }
    }

    protected override void RenderYamlHeader(YamlHeaderBlock element, IRenderContext context)
    {
        //throw new NotImplementedException();
    }

    protected override void RenderHeader(HeaderBlock element, IRenderContext context)
    {
        _headerLevel = element.HeaderLevel;
        RenderInlineChildren(element.Inlines, context);

        if (context.Parent is FormattedString fs)
        {
            if (fs.Spans?.Any() ?? false)
            {
                fs.Spans.Last().Text += Environment.NewLine;
            }

        }

        _headerLevel = 0;
    }

    protected override void RenderListElement(ListBlock element, IRenderContext context)
    {
        //throw new NotImplementedException();
    }

    protected override void RenderHorizontalRule(IRenderContext context)
    {
        //throw new NotImplementedException();
    }

    protected override void RenderQuote(QuoteBlock element, IRenderContext context)
    {
        throw new NotImplementedException();
    }

    protected override void RenderCode(CodeBlock element, IRenderContext context)
    {
        //throw new NotImplementedException();
    }

    protected override void RenderTable(TableBlock element, IRenderContext context)
    {
        //throw new NotImplementedException();
    }

    protected override void RenderEmoji(EmojiInline element, IRenderContext context)
    {
        //throw new NotImplementedException();
    }

    protected override void RenderTextRun(TextRunInline element, IRenderContext context)
    {
        if (context.Parent is FormattedString fs)
        {
            var span = new Span
            {
                Text = element.Text.Replace("\n\r", Environment.NewLine)
            };

            if (_headerLevel > 0)
            {
                span.Style = _headerStyles[_headerLevel];
            }

            foreach (var inlineType in _inlineTypeStack)
            {
                switch (inlineType)
                {
                    case MarkdownInlineType.Comment:
                    case MarkdownInlineType.TextRun:
                        break;
                    case MarkdownInlineType.Bold:
                        span.FontAttributes += (int)FontAttributes.Bold;
                        break;
                    case MarkdownInlineType.Italic:
                        span.FontAttributes += (int)FontAttributes.Italic;
                        break;
                    case MarkdownInlineType.MarkdownLink:
                        break;
                    case MarkdownInlineType.RawHyperlink:
                        break;
                    case MarkdownInlineType.RawSubreddit:
                        break;
                    case MarkdownInlineType.Strikethrough:
                        span.TextDecorations += (int)TextDecorations.Strikethrough;
                        break;
                    case MarkdownInlineType.Superscript:
                        break;
                    case MarkdownInlineType.Subscript:
                        break;
                    case MarkdownInlineType.Code:
                        break;
                    case MarkdownInlineType.Image:
                        break;
                    case MarkdownInlineType.Emoji:
                        break;
                    case MarkdownInlineType.LinkReference:
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }

            fs.Spans.Add(span);
        }
    }

    protected override void RenderBoldRun(BoldTextInline element, IRenderContext context)
    {
        RenderInlineType(element.Inlines, MarkdownInlineType.Bold, context);
    }

    protected override void RenderMarkdownLink(MarkdownLinkInline element, IRenderContext context)
    {
        var text = string.Join(string.Empty, element.Inlines);

        RenderLink(text, element.Url, context);
    }

    protected override void RenderImage(ImageInline element, IRenderContext context)
    {
        //throw new NotImplementedException();
    }

    protected override void RenderHyperlink(HyperlinkInline element, IRenderContext context)
    {
        RenderLink(element.Text, element.Url, context);
    }

    protected override void RenderItalicRun(ItalicTextInline element, IRenderContext context)
    {
        RenderInlineType(element.Inlines, MarkdownInlineType.Italic, context);
    }

    protected override void RenderStrikethroughRun(StrikethroughTextInline element, IRenderContext context)
    {
        //throw new NotImplementedException();
    }

    protected override void RenderSuperscriptRun(SuperscriptTextInline element, IRenderContext context)
    {
        //throw new NotImplementedException();
    }

    protected override void RenderSubscriptRun(SubscriptTextInline element, IRenderContext context)
    {
        //throw new NotImplementedException();
    }

    protected override void RenderCodeRun(CodeInline element, IRenderContext context)
    {
        //throw new NotImplementedException();
    }

    private void RenderLink(string text, string url, IRenderContext context)
    {
        if (context.Parent is FormattedString fs)
        {
            var span = new Span
            {
                Text = text,
                TextDecorations = TextDecorations.Underline,
                TextColor = _urlLinkColor
            };

            var tap = new TapGestureRecognizer
            {
                Command = new Command<string>(urlStr => Device.OpenUri(new Uri(urlStr))),
                CommandParameter = url
            };

            span.GestureRecognizers.Add(tap);

            fs.Spans.Add(span);
        }
    }

    private void RenderInlineType(IList<MarkdownInline> inlines, MarkdownInlineType markdownInlineType, IRenderContext context)
    {
        _inlineTypeStack.Push(markdownInlineType);
        RenderInlineChildren(inlines, context);
        _inlineTypeStack.Pop();
    }
}

For more details and how to turn this code into an custom control extending Label in Xamarin Forms, please see this project: https://github.com/1iveowl/plugin.label.markdown/blob/master/src/main/Plugin.Label.MarkDown/Renderer/LabelMarkdownRenderer.cs

1iveowl
  • 1,622
  • 1
  • 18
  • 31