目录
根据其描述,Markdig“是一个快速,强大,符合CommonMark标准,可扩展的.NET Markdown处理器”。虽然我们的大多数旧项目都使用MarkdownDeep(包括日益破旧的cyotek.com),但当前的项目使用Markdig,到目前为止,它已被证明是一个优秀的库。
cyotek.com的许多过于复杂的方面之一是,除了markdown处理之外,每个内容块还通过拜占庭式数量的正则表达式运行以进行自定义转换。当cyotek.com更新为使用Markdig时,我绝对不希望这些表达式闲置。输入,Markdig扩展。
Markdig扩展允许您扩展Markdig以包含其他转换,这些转换可能不符合CommonMark规范,例如YAML块或管道表。
MarkdownPipeline pipline;
string html;
string markdown;
markdown = "# Header 1";
pipline = new MarkdownPipelineBuilder()
.Build();
html = Markdown.ToHtml(markdown, pipline); // <h1>Header 1</h1>
pipline = new MarkdownPipelineBuilder()
.UseAutoIdentifiers() // enable the Auto Identifiers extension
.Build();
html = Markdown.ToHtml(markdown, pipline); // <h1 id="header-1">Header 1</h1>
使用扩展自动生成heading元素的id属性的示例。
我最近更新了我们的内部崩溃聚合系统,以便能够通过我们的MantisSharp库创建MantisBT问题。在这些问题中,堆栈跟踪包括格式为#<number>。令我模糊烦恼的是,Mantis Bug Tracker将这些视为指向系统中其他问题的超链接,其方式类似于GitHub自动链接到问题或拉取要求的方式。然而,它确实给了我一个想法,创建一个执行相同功能的Markdig扩展。
确定模式
您需要做的第一件事是确定降价模式以触发扩展。我们的例子可能有点太基础了,因为它是很简单的#<number>,而如果你想到其他问题系统,如JIRA,它会是<string>-<number>。除了图案的“主体”外,您还需要考虑围绕它的字符。例如,您可能只允许空格,或者括号或大括号——每当我引用JIRA问题时,我倾向于将它们括在方括号中,例如[PRJ-1234]。
要考虑的另一件事是核心模式的标准。使用我们上面的例子,我们应该在触发之前有最小的位数,还是最大值? #999999999可能不是有效的问题编号!
扩展组件
Markdig扩展由几个移动部分组成。根据扩展的复杂程度,您可能不需要所有部件,或者可能重用现有部件。
- 扩展本身(始终需要)
- 解析器
- 渲染器
- 用于在抽象语法树(AST)中表示数据的对象
- 用于配置扩展功能的对象
在这个插件中,我将演示所有这些部分。
令人高兴的是,实际上Markdig中已经内置了一个用于渲染JIRA链接的扩展,这是一个很好的起点,包括Dave Clarke的原始MarkdigJiraLinker扩展。正如我在开始时提到的,Markdig有很多扩展,有些简单,有些复杂——其中会有相当多的有用代码来帮助您使用自己的代码。
支持类
我实际上将从上面的列表中以向后顺序创建组件,因为每个步骤都取决于它之前的步骤,因此如果我引用尚不存在的东西,阅读起来会很尴尬。
要开始使用一些实际代码,我将需要几个支持类——一个用于配置扩展的选项对象(至少,我们需要提供MantisBT安装的基本URI),以及用于在AST中提供链接的类。
首先,options类。除了该基本URI之外,我还将添加一个选项来确定应用程序生成的链接是否应通过target属性在新窗口中打开。
public class MantisLinkOptions
{
public MantisLinkOptions()
{
this.OpenInNewWindow = true;
}
public MantisLinkOptions(string url)
: this()
{
this.Url = url;
}
public MantisLinkOptions(Uri uri)
: this()
{
this.Url = uri.OriginalString;
}
public bool OpenInNewWindow {get; set; }
public string Url { get; set; }
接下来是将在语法树中显示我们的链接的对象。Markdig节点与HTML非常相似,有两种风格——块和内联。在本文中,我只介绍简单的内联节点。
我将继承LeafInline并添加一个属性来保存Mantis问题编号。
实际上有一个更具体的LinkInline元素,它可能是一个更好的选择(因为这也意味着你不应该需要自定义渲染器)。但是,我正在“漫长地”完成此示例,以便当我转向更复杂的Markdig用例时,我对API有了更好的理解。
[DebuggerDisplay("#{" + nameof(IssueNumber) + "}")]
public class MantisLink : LeafInline
{
public StringSlice IssueNumber
{
get;
set;
}
}
String与StringSlice
在上面的类中,我使用的是Markdig提供的StringSlice struct。如果您愿意,您可以使用普通string(或任何其他类型),但这是专门为Markdig设计的StringSlice,以提高性能并减少分配。
创建渲染器
有了两个支持类,我现在可以创建渲染组件了。Markdig渲染器从AST中获取一个元素并吐出一些内容。很简单——我们创建一个类,继承HtmlObjectRenderer<T>(其中T是你的AST类的名称,例如MantisLink)并覆盖该Write方法。如果您使用的是配置类,那么创建一个构造函数来分配它也是一个好主意。
public class MantisLinkRenderer : HtmlObjectRenderer<MantisLink>
{
private MantisLinkOptions _options;
public MantisLinkRenderer(MantisLinkOptions options)
{
_options = options;
}
protected override void Write(HtmlRenderer renderer, MantisLink obj)
{
StringSlice issueNumber;
issueNumber = obj.IssueNumber;
if (renderer.EnableHtmlForInline)
{
renderer.Write("<a href=\"").Write
(_options.Url).Write("view.php?id=").Write(issueNumber).Write('"');
if (_options.OpenInNewWindow)
{
renderer.Write(" target=\"blank\" rel=\"noopener noreferrer\"");
}
renderer.Write('>').Write('#').Write(issueNumber).Write("</a>");
}
else
{
renderer.Write('#').Write(obj.IssueNumber);
}
}
}
那么这是如何工作的呢?我们重写的Write方法提供了要写入的HtmlRenderer和要呈现的MantisLink对象。
首先,我们需要通过检查EnableHtmlForInline属性来检查我们是否应该渲染 HTML。如果这是false,那么我们输出纯文本,例如,仅输出问题编号和#前缀。
如果我们正在编写完整的HTML,那么只需使用从选项对象中的基本URI生成的完全限定URI和AST节点的问题编号构建HTML a标记。如果选项声明链接应位于新窗口中,我们还会添加一个target属性。如果我们确实添加一个target属性,我也根据MDN 指南添加一个rel属性。
注意HtmlRenderer对象Write方法如何愉快地接受string,char或StringSlice参数,这意味着我们可以混合和匹配以满足我们的目的。
创建解析器
随着呈现的消失,是时候进行创建扩展的最复杂部分了——从源文档解析它。为此,我们需要继承InlineParser并覆盖该Match方法,并设置将触发解析例程的字符——在我们的示例中是单个#字符。
public class MantisLinkInlineParser : InlineParser
{
private static readonly char[] _openingCharacters =
{
'#'
};
public MantisLinkInlineParser()
{
this.OpeningCharacters = _openingCharacters;
}
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
bool matchFound;
char previous;
matchFound = false;
previous = slice.PeekCharExtra(-1);
if (previous.IsWhiteSpaceOrZero() || previous == '(' || previous == '[')
{
char current;
int start;
int end;
slice.NextChar();
current = slice.CurrentChar;
start = slice.Start;
end = start;
while (current.IsDigit())
{
end = slice.Start;
current = slice.NextChar();
}
if (current.IsWhiteSpaceOrZero() || current == ')' || current == ']')
{
int inlineStart;
inlineStart = processor.GetSourcePosition
(slice.Start, out int line, out int column);
processor.Inline = new MantisLink
{
Span =
{
Start = inlineStart,
End = inlineStart + (end - start) + 1
},
Line = line,
Column = column,
IssueNumber = new StringSlice(slice.Text, start, end)
};
matchFound = true;
}
}
return matchFound;
}
}
在构造函数中,我们将OpeningCharacters属性设置为字符数组。当Markdig解析内容时,如果它遇到此数组中的任何字符,它将自动调用您的扩展。
这巧妙地将我们引向了这个类的核心——覆盖了Match方法。在这里,我们扫描源文档并尝试构建我们的节点。如果我们成功了,我们会更新处理器,让Markdig处理其余的工作。
我们知道当前字符将是#,因为这是我们唯一支持的开场白。但是,我们需要检查前一个字符,以确保我们尝试处理一个不同的实体,而不是碰巧位于另一个字符串中间的#字符。
previous = slice.PeekCharExtra(-1);
if (previous.IsWhiteSpaceOrZero() || previous == '(' || previous == '[')
在这里,我使用Markdig公开的扩展方法来检查前一个字符是否是空格,或者根本没有空格,即文档的开头。我还在检查(或[字符,以防问题编号被括在括号或方括号中。
如果我们通过了此检查,那么是时候解析问题编号了。首先,我们推进字符流(丢弃#开场白),并在成功时启动创建最终StringSlice的值。
slice.NextChar();
current = slice.CurrentChar;
start = slice.Start;
end = start;
由于我们的GitHub/MantisBT问题编号只是普通数字,我们只是继续推进流,直到我们用完数字。
while (current.IsDigit())
{
end = slice.Start;
current = slice.NextChar();
}
由于我将专门使用StringSlice结构,因此我只记录新切片的结束位置。即使你想使用更传统的字符串,保留上面的结构,然后在最后构建你的字符串可能是有意义的。
一旦我们用完了数字,我们现在基本上会做一个相反的检查——现在我们想看看下一个字符是空格、流的末尾还是右括号/大括号。
if (current.IsWhiteSpaceOrZero() || current == ')' || current == ']')
我没有为此添加检查,但您可能还应该寻找匹配的对——因此,如果在开头使用了括号,则末尾应该存在一个右括号。
假设这个最终检查通过,这意味着我们有一个有效的#<number>序列,因此我们创建一个新MantisLink对象,其中的IssueNumber属性填充了一个全新的string slice。然后,我们将这个新对象分配给处理器的Inline属性。
inlineStart = processor.GetSourcePosition(slice.Start, out int line, out int column);
processor.Inline = new MantisLink
{
Span =
{
Start = inlineStart,
End = inlineStart + (end - start)
},
Line = line,
Column = column,
IssueNumber = new StringSlice(slice.Text, start, end)
};
我不确定Line和Column属性是否由Markdig直接使用,或者它们是否仅用于调试或高级AST方案。我也不确定设置该Span属性的目的是什么——即使我基于Markdig存储库中的代码编写此代码,如果我打印出其内容,它似乎并不完全匹配。这让我想知道我是否设置了错误的值。到目前为止,我还没有注意到任何不利影响。
创建扩展
首先要设置的是核心扩展。Markdig扩展实现了该IMarkdownExtension接口。这个简单的接口公开了一个Setup方法的两个重载,用于配置扩展的分析和呈现方面。
其中一个重载用于自定义管道——我们将在此处添加解析器。第二个重载用于设置渲染器。根据扩展的性质,可能只需要其中一个。
由于此类负责创建扩展所需的任何渲染或解析器,这也意味着它需要有权访问任何所需的配置类才能传递。
public class MantisLinkerExtension : IMarkdownExtension
{
private readonly MantisLinkOptions _options;
public MantisLinkerExtension(MantisLinkOptions options)
{
_options = options;
}
public void Setup(MarkdownPipelineBuilder pipeline)
{
OrderedList<InlineParser> parsers;
parsers = pipeline.InlineParsers;
if (!parsers.Contains<MantisLinkInlineParser>())
{
parsers.Add(new MantisLinkInlineParser());
}
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
HtmlRenderer htmlRenderer;
ObjectRendererCollection renderers;
htmlRenderer = renderer as HtmlRenderer;
renderers = htmlRenderer?.ObjectRenderers;
if (renderers != null && !renderers.Contains<MantisLinkRenderer>())
{
renderers.Add(new MantisLinkRenderer(_options));
}
}
}
首先,我确保构造函数接受要传递给渲染器的MantisLinkOptions类的参数。
在配置管道的Setup重载中,我首先检查以确保MantisLinkInlineParser解析器尚不存在;如果没有,我添加它。
以非常类似的方式,在配置渲染器的Setup重载中,我首先检查HtmlRenderer是否提供了渲染器——毕竟,您可能使用的是不基于HTML的自定义渲染器。如果我有一个HtmlRenderer渲染器,那么我会做类似的检查以确保MantisLinkRenderer实例不存在,如果没有,我会使用提供的options类创建并添加它。
添加初始化扩展方法
虽然你可以通过直接操作一个MarkdownPipelineBuilder的Extensions属性来注册扩展,但通常Markdig扩展包括一个扩展方法,它执行检查和添加扩展的样板代码。下面的扩展检查MantisLinkerExtension是否已注册到给定管道,如果没有,则使用指定的选项添加它。
public static MarkdownPipelineBuilder UseMantisLinks
(this MarkdownPipelineBuilder pipeline, MantisLinkOptions options)
{
OrderedList<IMarkdownExtension> extensions;
extensions = pipeline.Extensions;
if (!extensions.Contains<MantisLinkerExtension>())
{
extensions.Add(new MantisLinkerExtension(options));
}
return pipeline;
}
使用扩展
MarkdownPipeline pipline;
string html;
string markdown;
markdown = "See issue #1";
pipline = new MarkdownPipelineBuilder()
.Build();
html = Markdown.ToHtml(markdown, pipline); // <p>See issue #1</p>
pipline = new MarkdownPipelineBuilder()
.UseMantisLinks(new MantisLinkOptions("https://issues.cyotek.com/"))
.Build();
html = Markdown.ToHtml(markdown, pipline); // <p>See issue
// <a href="https://issues.cyotek.com/view.php?id=1"
// target="blank" rel="noopener noreferrer">#1</a></p>
使用扩展自动生成MantisBT问题编号的链接的示例。
总结
在本文中,我展示了如何引入从markdown解析的新内联元素。这个例子至少是直截了当的,但是还有更多的事情可以做。更高级的扩展(如管道表)具有更复杂的解析器,可以生成自己的完整AST。
Markdig也支持其他扩展自己的方式。例如,本文开头显示的自动标识符不会分析markdown,而是在生成AST时对其进行操作。强调额外扩展将自身注入到另一个扩展中,以向其添加更多功能。似乎有很多方法可以挂接到库中以添加自己的自定义功能!
可以从下面的URL或项目的 GitHub页面下载完整的示例项目。
虽然我在写这个例子时考虑到了Mantis Bug Tracker,但要让它覆盖无数其他网站根本不需要太多的努力。
https://www.codeproject.com/Articles/5348000/Writing-Custom-Markdig-Extensions-2