New UWP Community Toolkit 6.0 - Markdown

网友投稿 535 2022-05-30

概述

前面 New UWP Community Toolkit 文章中,我们对 V6.0 版本的重要更新做了简单回顾,其中简单介绍了 MarkdownTextBlock 和 MarkdownDocument,本篇我们结合代码详细讲解一下 Markdown 相关功能。

Markdown 是一种非常常用的标记语言,对于编写文档或者文章排版等有很大帮助:Markdown 维基百科。关于 Markdown 语法,大家可以去网络查询,很容易上手,一次书写,到各个平台都能有一样的操作体验,非常的简便实用。而 UWP Community Toolkit 对 Markdown 的解析和渲染提供了完整的支持,即使复杂的 Markdown 文本,也可以在低配置的硬件上获得流畅的体验。UWP Community Toolkit 完成 Markdown 整个功能的两个重要组成部分就是:MarkdownTextBlock 和 MarkdownDocument。

MarkdownDocument 提供了对 markdown 的解析操作,传递给 MarkdownTextBlock,负责 markdown 解析后内容的渲染操作,然后显示在界面。

MarkdownTextBlock

Source: https://github.com/Microsoft/UWPCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/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 - 渲染上下文不正确的异常定义类

New UWP Community Toolkit 6.0 - Markdown

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 MarkdownRendered;public event EventHandler LinkClicked;public event EventHandler ImageClicked;public event EventHandler ImageResolving;public event EventHandler CodeBlockResolving;

2. MarkdownTextBlock.Methods.cs

我们截取了几个重要的方法:

RenderMarkdown() - 使用 MarkdownDocument 类解析文本,然后使用上面所述 Render 文件夹中的 MarkdownRender 来渲染,添加到父容器中;

RegisterNewHyperLink(s,e) -  注册一个新的超链接,在点击操作时触发这个事件;超链接和图片链接都会被注册;

ICodeBlockResolver.ParseSyntax(a,b,c) - 解析代码块的语法,如果没有复制,则根据系统主题和富文本控件的默认样式初始化一个值

MarkdownDocument markdown =       renderer = Activator.CreateInstance(renderertype, markdown, , , ) =     MarkdownRendered?.Invoke(  RegisterNewHyperLink(Hyperlink newHyperlink,      newHyperlink.Click += ICodeBlockResolver.ParseSyntax(InlineCollection inlineCollection,  text,  (language !=  (CodeStyling != =  theme = themeListener.CurrentTheme == ApplicationTheme.Dark ? (RequestedTheme !===

3. MarkdownRenderer.Blocks.cs

我们省略了大部分方法的实现过程,主要让大家看到都有哦哪些类型的渲染,而他们和 RenderParagraph 都比较相似;大致的实现过程就是读取解析后的 element,读取对应的 margin width thickness 等信息来初始化控件,然后把控件以配置的某个位置和尺寸添加到 TextBlock 中,渲染到 UI 中。

protected override void RenderBlocks(IEnumerable 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 action) {...}private bool AllTextIsSuperscript(IInlineContainer container, int superscriptLevel = 0) {...}private void RemoveSuperscriptRuns(IInlineContainer container, bool insertCaret) {...}

调用示例:

看完源代码的主要构成后,我们再简单看一下 MarkdownTextBlock 的使用过程:

我们在其中添加了正常显示文本、粗体和斜体,还添加了超链接文本,而在 LinkClicked 事件中处理超链接的跳转。在复杂的源代码之上,使用过程变得非常简单,我们只需要准备好 markdown 文本,以及需要处理的点击、点按等事件就可以了。

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 _references;public IList 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(StringComparer.OrdinalIgnoreCase);             }            if (!_references.ContainsKey(reference.Id))             {                 _references.Add(reference.Id, reference);             }             Blocks.RemoveAt(i);         }     } }internal static List 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();    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 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()}");     } }

总结

如果大家有兴趣,或想开发 Markdown 相关的功能,可以对源代码和调用做更深入的研究,欢迎大家多多交流,谢谢!

渲染 Markdown

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:让一个团队“活”起来
下一篇:Excel分段折线图的做法有哪些步骤
相关文章