概述 前面 New UWP Community Toolkit 文章中,我們對 V2.2.0 版本的重要更新做了簡單回顧,其中簡單介紹了 MarkdownTextBlock 和 MarkdownDocument,本篇我們結合代碼詳細講解一下 Markdown 相關功能。 Markdown 是一種非常 ...
概述
前面 New UWP Community Toolkit 文章中,我們對 V2.2.0 版本的重要更新做了簡單回顧,其中簡單介紹了 MarkdownTextBlock 和 MarkdownDocument,本篇我們結合代碼詳細講解一下 Markdown 相關功能。
Markdown 是一種非常常用的標記語言,對於編寫文檔或者文章排版等有很大幫助:Markdown 維基百科。關於 Markdown 語法,大家可以去網路查詢,很容易上手,一次書寫,到各個平臺都能有一樣的操作體驗,非常的簡便實用。而 UWP Community Toolkit 對 Markdown 的解析和渲染提供了完整的支持,即使複雜的 Markdown 文本,也可以在低配置的硬體上獲得流暢的體驗。UWP Community Toolkit 完成 Markdown 整個功能的兩個重要組成部分就是:MarkdownTextBlock 和 MarkdownDocument。
MarkdownDocument 提供了對 markdown 的解析操作,傳遞給 MarkdownTextBlock,負責 markdown 解析後內容的渲染操作,然後顯示在界面。
MarkdownTextBlock
Doc: https://docs.microsoft.com/zh-cn/windows/uwpcommunitytoolkit/controls/markdowntextblock
Namespace: Microsoft.Toolkit.Uwp.UI.Controls; Nuget: Microsoft.Toolkit.Uwp.UI.Controls
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 相關的功能,可以對源代碼和調用做更深入的研究,歡迎大家多多交流,謝謝!