New UWP Community Toolkit - 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 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, , markdownText.Length, quoteDepth: , actualEnd: out actualEnd);
// Remove any references from the list of blocks, and add them to a dictionary.
; i >= ; 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 相关的功能,可以对源代码和调用做更深入的研究,欢迎大家多多交流,谢谢!
New UWP Community Toolkit - Markdown的更多相关文章
- New UWP Community Toolkit
概述 UWP Community Toolkit 是一个 UWP App 自定义控件.应用服务和帮助方法的集合,能够很大程度的简化和指引开发者的开发工作,相信广大 UWPer 并不陌生. 下面是截取自 ...
- New UWP Community Toolkit - XAML Brushes
概述 上一篇 New UWP Community Toolkit 文章中,我们对 V2.2.0 版本的重要更新做了简单回顾.接下来会针对每个重要更新,结合 SDK 源代码和调用代码详细讲解. 本篇我们 ...
- New UWP Community Toolkit - Staggered panel
概述 前面 New UWP Community Toolkit 文章中,我们对 2.2.0 版本的重要更新做了简单回顾,其中简单介绍了 Staggered panel,本篇我们结合代码详细讲解 St ...
- New UWP Community Toolkit - Carousel
概述 New UWP Community Toolkit V2.2.0 的版本发布日志中提到了 Carousel 的调整,本篇我们结合代码详细讲解 Carousel 的实现. Carousel 是 ...
- New UWP Community Toolkit - RadialProgressBar
概述 UWP Community Toolkit 中有一个圆形的进度条控件 - RadialProgressBar,本篇我们结合代码详细讲解 RadialProgressBar 的实现. Radi ...
- New UWP Community Toolkit - RadialGauge
概述 New UWP Community Toolkit V2.2.0 的版本发布日志中提到了 RadialGauge 的调整,本篇我们结合代码详细讲解 RadialGauge 的实现. Radi ...
- New UWP Community Toolkit - RangeSelector
概述 前面 New UWP Community Toolkit 文章中,我们对 V2.2.0 版本的重要更新做了简单回顾,其中简单介绍了 RangeSelector,本篇我们结合代码详细讲解一下 Ra ...
- New UWP Community Toolkit - ImageEx
概述 UWP Community Toolkit 中有一个图片的扩展控件 - ImageEx,本篇我们结合代码详细讲解 ImageEx 的实现. ImageEx 是一个图片的扩展控件,包括 Ima ...
- New UWP Community Toolkit - AdaptiveGridView
概述 UWP Community Toolkit 中有一个自适应的 GridView 控件 - AdaptiveGridView,本篇我们结合代码详细讲解 AdaptiveGridView 的实现 ...
随机推荐
- Error Code: 1175. You are using safe update mode and you tried to update a table without a WHERE
1 错误描述 19:15:34 call sp_store_insert(90) Error Code: 1175. You are using safe update mode and you tr ...
- Poj3678:Katu Puzzle
大概题意 有\(n\)个数,可以为\(0/1\),给\(m\)个条件,表示某两个数经过\(or, and, xor\)后的数是多少 判断是否有解 Sol \(2-SAT\)判定 建图 # includ ...
- Nginx负载均衡——基础功能
熟悉Nginx的小伙伴都知道,Nginx是一个非常好的负载均衡器.除了用的非常普遍的Http负载均衡,Nginx还可以实现Email,FastCGI的负载均衡,甚至可以支持基于Tcp/UDP协议的各种 ...
- Windows Developer Day - MSIX and Advanced Installer
前面一篇我们介绍了 Adaptive Cards 的基础知识,而在 Windows Developer Day 的 Modern Application Experience 环节,还有一个需要划重点 ...
- 11.python线程
基本概念 1.进程 定义: 进程就是一个程序在一个数据集上的一次动态执行过程. 组成: 进程一般由程序.数据集.进程控制块三部分组成. 程序: 我们编写的程序用来描述进程要完成哪些功能 ...
- Online Judge(OJ)搭建——1、项目介绍
项目名 Piers 在线评测 项目需求 用户: 获取题库.题目的相关信息. 在线对代码进行编译.执行.保存.返回运行(编译)结果. 总体题目评测成绩查询. 用户信息服务,包括注册.登录.忘记密码.邮箱 ...
- 走近webpack(1)--多入口及devServer的使用
上一篇文章留下了一些问题,如果你没看过上一篇文章,可以在我的博客里查找,或者直接从这篇文章开始也是没问题的. const path = require('path'); module.exports= ...
- 【日记】一次程序调优发现的同步IO写的问题,切记
众所周知,我们在写程序的时候,好习惯是在重要的代码打上日志.以便监控程序运行的性能和记录可能发生的错误. 但是,如果日志是基于同步IO文件操作,那么就必须考虑到访问总次数或并发数目. 如果总次数或并发 ...
- Mycat 注解说明
我们知道MySQL 数据库有自己的SQL注解(hint),比如 use index.force index.ignore index 等都是会经常用到的,Mycat 作为一个数据库中间件,最重要的是 ...
- poj 3664
http://poj.org/problem?id=3664 进行两轮选举,第一轮选前n进入第二轮,第二轮选最高 #include<algorithm> #include<cstdi ...