New UWP Community Toolkit - Markdown

概述git

前面 New UWP Community Toolkit 文章中,咱們對 V2.2.0 版本的重要更新作了簡單回顧,其中簡單介紹了 MarkdownTextBlock 和 MarkdownDocument,本篇咱們結合代碼詳細講解一下 Markdown 相關功能。github

Markdown 是一種很是經常使用的標記語言,對於編寫文檔或者文章排版等有很大幫助:Markdown 維基百科。關於 Markdown 語法,你們能夠去網絡查詢,很容易上手,一次書寫,到各個平臺都能有同樣的操做體驗,很是的簡便實用。而 UWP Community Toolkit 對 Markdown 的解析和渲染提供了完整的支持,即便複雜的 Markdown 文本,也能夠在低配置的硬件上得到流暢的體驗。UWP Community Toolkit 完成 Markdown 整個功能的兩個重要組成部分就是:MarkdownTextBlock 和 MarkdownDocument。windows

MarkdownDocument 提供了對 markdown 的解析操做,傳遞給 MarkdownTextBlock,負責 markdown 解析後內容的渲染操做,而後顯示在界面。markdown

MarkdownTextBlock 網絡

Source: https://github.com/Microsoft/UWPCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MarkdownTextBlockapp

Doc: https://docs.microsoft.com/zh-cn/windows/uwpcommunitytoolkit/controls/markdowntextblockasync

Namespace: Microsoft.Toolkit.Uwp.UI.Controls;  Nuget: Microsoft.Toolkit.Uwp.UI.Controlside

 

MarkdownDocument 性能

Source: https://github.com/Microsoft/UWPCommunityToolkit/tree/master/Microsoft.Toolkit.Parsers/Markdown字體

Doc: https://docs.microsoft.com/zh-cn/windows/uwpcommunitytoolkit/parsers/markdownparser

Namespace: Microsoft.Toolkit.Parsers.Markdown;  Nuget: Microsoft.Toolkit.Parsers

 

代碼分析

MarkdownTextBlock

MarkdownTextBlock 項目起源自一個開源項目 - Universal Markdown: https://github.com/QuinnDamerell/UniversalMarkdown

Universal Markdown 是由 Quinn Damerell 和 Paul Bartrum 建立的開發項目,用於一個 reddit UWP 應用 Baconit。旨在建立一種通用的 markdown 渲染控件,能夠方便高效的使用。這個項目支持完整的 markdown 標記,性能表現也很是理想。

咱們來看一下 MarkdownTextBlock 的項目結構:

  • Render 文件夾 - Markdown 實際渲染代碼
  • ***EventArgs.cs - Markdown 事件參數,好比超連接點擊時的連接地址參數
  • MarkdownTextBlock.Dimensions.cs - MarkdownTextBlock 部分類中負責設置各維度依賴屬性的類,包括字體、字號、背景色等的設置都由它負責
  • MarkdownTextBlock.Events.cs - MarkdownTextBlock 部分類中負責事件處理的類,包括連接點擊、圖片顯示等時間的觸發都由它負責
  • MarkdownTextBlock.Methods.cs - MarkdownTextBlock 部分類中負責具體方法執行的類,包括連接點擊、圖片顯示等方法的處理執行都由它負責
  • MarkdownTextBlock.Properties.cs - MarkdownTextBlock 部分類中負責設置和獲取各類屬性的類
  • MarkdownTextBlock.cs - MarkdownTextBlock 部分類,負責類初始化、主題變化響應等
  • MarkdownTextBlock.xaml - MarkdownTextBlock 類的 XAML 代碼,負責 UI 編寫和各類依賴屬性初始化

其中 Render 文件夾的項目結構:

  • ICodeBlockResolver.cs - 代碼塊渲染接口
  • IImageResolver.cs - 圖片渲染接口
  • ILinkRegister.cs - 連接註冊接口
  • InlineRenderContext - TextBlock 中的 Inline 集合渲染上下文
  • MarkdownRenderer.Blocks.cs - MarkdownRenderer 部分類中負責塊渲染的類,包括代碼、塊、段落、引用等的渲染由它負責
  • MarkdownRenderer.Dimensions.cs - MarkdownRenderer 部分類中負責獲取和設置各個維度量值的類
  • MarkdownRenderer.Inlines.cs - MarkdownRenderer 部分類中負責全部 Inline 渲染的類,包括常規、斜體、加粗、連接和圖片等
  • MarkdownRenderer.Properties.cs - MarkdownRenderer 部分類中負責獲取和設置全部屬性的類
  • MarkdownRenderer.cs - MarkdownRenderer 部分類負責初始化和渲染的類
  • MarkdownTable.cs - markdown 中表格控件渲染類
  • RenderContext.cs - markdown 渲染上下文
  • RenderContextIncorrectException.cs - 渲染上下文不正確的異常定義類
  • UIElementCollectionRenderContext - UI 元素結合渲染上下文

接下來咱們分幾個重要部分來詳細分析一下源代碼,由於篇幅考慮,咱們只摘錄關鍵的代碼片斷:

1. MarkdownTextBlock.Events.cs

能夠看到,類爲 MarkdownTextBlock 註冊了 MarkdownRendered、LinkClicked、ImageClicked、ImageResolving、CodeBlockResolving 這幾個事件,在渲染、點擊和須要顯示內容時使用;並相應兩種操做:Hyperlink_Click、NewImagelink_Tapped,分別是超連接點擊和圖片連接點按的操做處理,這也是 MarkdownTextBlock 僅有的兩種用戶主動觸發的事件。

private void Hyperlink_Click(Hyperlink sender, HyperlinkClickEventArgs args)
{
    LinkHandled((string)sender.GetValue(HyperlinkUrlProperty), true);
}

private void NewImagelink_Tapped(object sender, Windows.UI.Xaml.Input.TappedRoutedEventArgs e)
{
    LinkHandled((string)(sender as Image).GetValue(HyperlinkUrlProperty), false);
}

public event EventHandler<MarkdownRenderedEventArgs> MarkdownRendered;

public event EventHandler<LinkClickedEventArgs> LinkClicked;

public event EventHandler<LinkClickedEventArgs> ImageClicked;

public event EventHandler<ImageResolvingEventArgs> ImageResolving;

public event EventHandler<CodeBlockResolvingEventArgs> CodeBlockResolving;

2. MarkdownTextBlock.Methods.cs

咱們截取了幾個重要的方法:

  • RenderMarkdown() - 使用 MarkdownDocument 類解析文本,而後使用上面所述 Render 文件夾中的 MarkdownRender 來渲染,添加到父容器中;
  • RegisterNewHyperLink(s,e) -  註冊一個新的超連接,在點擊操做時觸發這個事件;超連接和圖片連接都會被註冊;
  • ICodeBlockResolver.ParseSyntax(a,b,c) - 解析代碼塊的語法,若是沒有複製,則根據系統主題和富文本控件的默認樣式初始化一個值
private void RenderMarkdown()
{
    // Try to parse the markdown.
    MarkdownDocument markdown = new MarkdownDocument();
    markdown.Parse(Text);

    // Now try to display it
    var renderer = Activator.CreateInstance(renderertype, markdown, this, this, this) as MarkdownRenderer;
    // set properties
    ...
    _rootElement.Child = renderer.Render();
    // Indicate that the parse is done.
    MarkdownRendered?.Invoke(this, markdownRenderedArgs);
}

public void RegisterNewHyperLink(Hyperlink newHyperlink, string linkUrl)
{
    // Setup a listener for clicks.
    newHyperlink.Click += Hyperlink_Click;

    // Associate the URL with the hyperlink.
    newHyperlink.SetValue(HyperlinkUrlProperty, linkUrl);

    // Add it to our list
    _listeningHyperlinks.Add(newHyperlink);
}
bool ICodeBlockResolver.ParseSyntax(InlineCollection inlineCollection, string text, string codeLanguage)
{
    ...
    if (language != null)
    {
        RichTextBlockFormatter formatter;
        if (CodeStyling != null)
        {
            formatter = new RichTextBlockFormatter(CodeStyling);
        }
        else
        {
            var theme = themeListener.CurrentTheme == ApplicationTheme.Dark ? ElementTheme.Dark : ElementTheme.Light;
            if (RequestedTheme != ElementTheme.Default)
            {
                theme = RequestedTheme;
            }
            formatter = new RichTextBlockFormatter(theme);
        }
        formatter.FormatInlines(text, language, inlineCollection);
    }
    ...
}

3. MarkdownRenderer.Blocks.cs

咱們省略了大部分方法的實現過程,主要讓你們看到都有哦哪些類型的渲染,而他們和 RenderParagraph 都比較類似;大體的實現過程就是讀取解析後的 element,讀取對應的 margin width thickness 等信息來初始化控件,而後把控件以配置的某個位置和尺寸添加到 TextBlock 中,渲染到 UI 中。

protected override void RenderBlocks(IEnumerable<MarkdownBlock> blockElements, IRenderContext context) {...}

protected override void RenderParagraph(ParagraphBlock element, IRenderContext context)
{
    var paragraph = new Paragraph
    {
        Margin = ParagraphMargin
    };

    var childContext = new InlineRenderContext(paragraph.Inlines, context)
    {
        Parent = paragraph
    };

    RenderInlineChildren(element.Inlines, childContext);

    var textBlock = CreateOrReuseRichTextBlock(context);
    textBlock.Blocks.Add(paragraph);
}

protected override void RenderHeader(HeaderBlock element, IRenderContext context) {...}

protected override void RenderListElement(ListBlock element, IRenderContext context) {...}

protected override void RenderHorizontalRule(IRenderContext context) {...}

protected override void RenderQuote(QuoteBlock element, IRenderContext context) {...}

protected override void RenderCode(CodeBlock element, IRenderContext context) {...}

protected override void RenderTable(TableBlock element, IRenderContext context) {...}

4. MarkdownRenderer.Inlines.cs

咱們一樣省略了大部分方法的實現過程,主要看都有哪些渲染的類型,包括表情、粗體、斜體、超連接、圖片、上標和代碼等;參照 Emoji 的實現過程,讀取 inline 中的 Emoji,設置文字信息和 Emoji 內容,而後添加到 inline 集合中。

protected override void RenderEmoji(EmojiInline element, IRenderContext context)
{
    var localContext = context as InlineRenderContext;
    ...
    var inlineCollection = localContext.InlineCollection;
    var emoji = new Run
    {
        FontFamily = EmojiFontFamily ?? DefaultEmojiFont,
        Text = element.Text
    };
    inlineCollection.Add(emoji);
}

protected override void RenderTextRun(TextRunInline element, IRenderContext context) {...}

protected override void RenderBoldRun(BoldTextInline element, IRenderContext context) {...}

protected override void RenderMarkdownLink(MarkdownLinkInline element, IRenderContext context) {...}

protected override void RenderHyperlink(HyperlinkInline element, IRenderContext context) {...}

protected override async void RenderImage(ImageInline element, IRenderContext context) {...}

protected override void RenderItalicRun(ItalicTextInline element, IRenderContext context) {...}

protected override void RenderStrikethroughRun(StrikethroughTextInline element, IRenderContext context) {...}

protected override void RenderSuperscriptRun(SuperscriptTextInline element, IRenderContext context) {...}

protected override void RenderCodeRun(CodeInline element, IRenderContext context) {...}

5. MarkdownRenderer.cs

咱們來看,渲染器初始化時,傳入的是連接註冊、圖片顯示、代碼塊顯示和表情字體(默認爲 Segoe UI Emoji);後面提供了建立文本、建立富文本的方法,以及修改某個範圍內的 runs,檢測是否上標、去掉上標等方法;

public MarkdownRenderer(MarkdownDocument document, ILinkRegister linkRegister, IImageResolver imageResolver, ICodeBlockResolver codeBlockResolver)
: base(document)
{
    LinkRegister = linkRegister;
    ImageResolver = imageResolver;
    CodeBlockResolver = codeBlockResolver;
    DefaultEmojiFont = new FontFamily("Segoe UI Emoji");
}

protected RichTextBlock CreateOrReuseRichTextBlock(IRenderContext context) {...}

protected TextBlock CreateTextBlock(RenderContext context) {...}

protected void AlterChildRuns(Span parentSpan, Action<Span, Run> action) {...}

private bool AllTextIsSuperscript(IInlineContainer container, int superscriptLevel = 0) {...}

private void RemoveSuperscriptRuns(IInlineContainer container, bool insertCaret) {...}

調用示例:

看完源代碼的主要構成後,咱們再簡單看一下 MarkdownTextBlock 的使用過程:

咱們在其中添加了正常顯示文本、粗體和斜體,還添加了超連接文本,而在 LinkClicked 事件中處理超連接的跳轉。在複雜的源代碼之上,使用過程變得很是簡單,咱們只須要準備好 markdown 文本,以及須要處理的點擊、點按等事件就能夠了。

<controls:MarkdownTextBlock
    Text="This control was originally written by [Quinn Damerell](https://github.com/QuinnDamerell) 
            and [Paul Bartrum](https://github.com/paulbartrum) for [Baconit](https://github.com/QuinnDamerell/Baconit), 
            a popular open source reddit UWP. The control *almost* supports the full markdown syntax, with a focus on super-efficient 
            parsing and rendering. The control is efficient enough to be used in virtualizing lists.
            *Note:* For a full list of markdown syntax, see the [official syntax guide](http://daringfireball.net/projects/markdown/syntax).
            **Try it live!** Type in the *unformatted text box* above!"
    LinkClicked="MarkdownText_LinkClicked"
    Margin="6">
</controls:MarkdownTextBlock>

 

MarkdownDocument

MarkdownDocument 是 Markdown Parser 的主要組成部分,負責 markdown 文本的解析工做,把文本解析爲 MarkdownDocument,而 Markdown Parser 還提供了 MarkdownRendererBase,做爲渲染功能的基類,它也是 MarkdownTextBlock 的 MarkdownRenderer.cs 類的基類。

來看一下 Markdown Parser 的項目主要構成:

  • Blocks - 每一個分類塊的解析類
  • Enums - 各個類型的枚舉類
  • Helpers - 一些通用的幫助類
  • Inlines - TextBlock 中 inline 解析類
  • Render - Markdown Parser 負責渲染的基類
  • MarkdownBlock.cs - Markdown 塊定義類, MarkdownDocument 的基類
  • MarkdownDocument.cs - Markdown Parser 和 Render 的主要類
  • MarkdownElement.cs - 全部 Markdown 元素的基類
  • MarkdownInline.cs - markdown inline 元素的基類

接下來咱們分幾個重要部分來詳細分析一下源代碼,由於篇幅考慮,咱們只摘錄關鍵的代碼片斷:

1. MarkdownDocument.cs

 MarkdownDocument 負責 markdown parser 的主要功能,看到兩個變量:_references 存放連接和對應文本的列表,Blocks 存放文本,包含樣式;public 的 Parse 方法複雜解析和整理文本/連接文本;internal 的 Parse 方法負責實際的解析工做,按照 MarkdownBlock 的類型分別解析每種 Block,拆分每一個特殊符號,根據 Block 的換行/縮進等屬性進行單獨的解析工做;LookUpReference 方法負責查找引用的 ID;

private Dictionary<string, LinkReferenceBlock> _references;
public IList<MarkdownBlock> Blocks { get; set; }

public void Parse(string markdownText)
{
    int actualEnd;
    Blocks = Parse(markdownText, 0, markdownText.Length, quoteDepth: 0, actualEnd: out actualEnd);

    // Remove any references from the list of blocks, and add them to a dictionary.
    for (int i = Blocks.Count - 1; i >= 0; i--)
    {
        if (Blocks[i].Type == MarkdownBlockType.LinkReference)
        {
            var reference = (LinkReferenceBlock)Blocks[i];
            if (_references == null)
            {
                _references = new Dictionary<string, LinkReferenceBlock>(StringComparer.OrdinalIgnoreCase);
            }

            if (!_references.ContainsKey(reference.Id))
            {
                _references.Add(reference.Id, reference);
            }

            Blocks.RemoveAt(i);
        }
    }
}

internal static List<MarkdownBlock> Parse(string markdown, int start, int end, int quoteDepth, out int actualEnd) 
{
    // We need to parse out the list of blocks.
    // Some blocks need to start on a new paragraph (code, lists and tables) while other
    // blocks can start on any line (headers, horizontal rules and quotes).
    // Text that is outside of any other block becomes a paragraph.
    var blocks = new List<MarkdownBlock>();
    int startOfLine = start;
    bool lineStartsNewParagraph = true;
    var paragraphText = new StringBuilder();

    // These are needed to parse underline-style header blocks.
    int previousStartOfLine = start;
    int previousEndOfLine = start;

    // Go line by line.
    while (startOfLine < end)
    {
        // Parse all kinds of blocks
        ...
    }
    actualEnd = startOfLine;
    return blocks;
}

public LinkReferenceBlock LookUpReference(string id) {...}

2. Render / MarkdownRendererBase.cs

前面咱們說到, MarkdownTextBlock 的 Render 功能繼承自 MarkdownRendererBase 類。這個類定義了每種不一樣類型的 Block 和 Inline 的渲染;咱們看到兩個主要方法:RenderBlock 和 RenderInline,根據不一樣的類型,分別進行渲染。

咱們在實現 Renderer 功能的時候,能夠繼承 MarkdownRendererBase 類,像 MarkdownTextBlock 那樣,也能夠根據本身的需求,作一些類型的定製化。

public virtual void Render(IRenderContext context)
{
    RenderBlocks(Document.Blocks, context);
}

protected virtual void RenderBlocks(IEnumerable<MarkdownBlock> blockElements, IRenderContext context)
{
    foreach (MarkdownBlock element in blockElements)
    {
        RenderBlock(element, context);
    }
}

protected void RenderBlock(MarkdownBlock element, IRenderContext context)
{
    {
        switch (element.Type)
        {
            case MarkdownBlockType.Paragraph:
                RenderParagraph((ParagraphBlock)element, context);
                break;
            // case other Block types
            ...
        }
    }
}

protected void RenderInline(MarkdownInline element, IRenderContext context)
{
    switch (element.Type)
    {
        case MarkdownInlineType.TextRun:
            RenderTextRun((TextRunInline)element, context);
            break;
        // case other Inline types
        ...
    }
}        

3. Blocks / CodeBlock.cs

 上面的 MarkdownDocument 類中涉及到每種類型的 Parse 功能,而實際的 Parse 工做由每一個 Block 和 Inline 完成,咱們在 Block 中用 CodeBlock 作例子,能夠看到 Parse 方法會把對應的 markdown 文本解析爲 Renderer 能夠識別的元素;

internal static CodeBlock Parse(string markdown, int start, int maxEnd, int quoteDepth, out int actualEnd)
{
    StringBuilder code = null;
    actualEnd = start;
    bool insideCodeBlock = false;
    string codeLanguage = string.Empty;

    /*
        Two options here:
        Either every line starts with a tab character or at least 4 spaces
        Or the code block starts and ends with ```
    */
    foreach (var lineInfo in Common.ParseLines(markdown, start, maxEnd, quoteDepth))
    {
        ...
    }
    ...
}

調用示例:

一段簡單 markdown 字符串(This is Markdown)的解析代碼和結果:

This is 和 Markdown 被解析爲兩個 Inline,Type = 'TextRun',其中 Markdown 的 顯示 Type = 'Bold',這個預期的一致,Markdown 顯示爲加粗。

string md = "This is **Markdown**";
MarkdownDocument Document = new MarkdownDocument();
Document.Parse(md);

// Takes note of all of the Top Level Headers.
foreach (var element in document.Blocks)
{
    if (element is HeaderBlock header)
    {
        Console.WriteLine($"Header: {header.ToString()}");
    }
}

 

總結

到這裏咱們就把 UWP Community Toolkit 中的 Markdown 功能的源代碼實現過程和簡單的調用示例講解完成了。源代碼的實現功能點不少很強大,對於理解 markdown 的規則和 markdown 與 UWP XAML 的轉換都很是有幫助,而最終的調用很是簡單易用,真的要感謝 CommunityToolkit 的做者們。

若是你們有興趣,或想開發 Markdown 相關的功能,能夠對源代碼和調用作更深刻的研究,歡迎你們多多交流,謝謝!

相關文章
相關標籤/搜索