概述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 網絡
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 文件夾的項目結構:
接下來咱們分幾個重要部分來詳細分析一下源代碼,由於篇幅考慮,咱們只摘錄關鍵的代碼片斷:
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
咱們截取了幾個重要的方法:
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 的項目主要構成:
接下來咱們分幾個重要部分來詳細分析一下源代碼,由於篇幅考慮,咱們只摘錄關鍵的代碼片斷:
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 相關的功能,能夠對源代碼和調用作更深刻的研究,歡迎你們多多交流,謝謝!