用 Markdown 编写 CodeProject 文章





5.00/5 (14投票s)
使用并学习如何构建一个 Markdown Monster 插件,该插件可输出与 CodeProject 文章提交向导兼容的 HTML。
目录
引言
我最近迷上了 Markdown。我爱它。它比 HTML 写得更快;它简洁精炼,无需尖括号,并且拼写检查更容易实现。你拥有 HTML 的全部功能——如果需要,你可以在 Markdown 中混用 HTML——但你最终得到的是一个更容易编辑、导航的文档,等等。
虽然 CodeProject 的文章编辑器(又称提交向导)是提交文章的好方法,但过去我更喜欢在文本编辑器(例如 Word)中撰写文章,然后将其粘贴到 CodeProject 编辑器中。之后,我通常会花一些时间格式化代码块并修复各种问题,这会使我的原始文档孤立;一旦你在 CodeProject 编辑器中开始编辑,就无法回头了。
因此,我一直希望 CodeProject 能支持 Markdown。它尚未到来,我也不知道它是否会到来。但没关系,我已经想出了一种替代方法,可以在 Markdown 中撰写 CodeProject 文章,然后将生成的 HTML 复制粘贴到 CodeProject 文章编辑器中。
我为 Markdown Monster 创建了一个 Markdown 解析器。Markdown Monster 是由 Rick Strahl 创建的开源 WPF 应用程序。它是一个付费产品,但免费版本仅在生成的 HTML 中生成一个“使用 Markdown Monster 创建”的页脚。Markdown Monster 的一个特点是 Rick 为它创建了一个非常好的扩展性系统。甚至还有一个 Visual Studio 扩展,可以让你快速创建一个 Markdown Monster 插件,而且由于 Markdown Monster 是一个 WPF 应用程序,调试你的插件非常简单。
几天前,当我准备发布我的 CodeProject 文章 隐形墨水 时,我用 Markdown 撰写了这篇文章,我心想,我真的想去重新格式化所有代码块以使其与 CodeProject 兼容吗?为什么不花时间创建一个自定义 Markdown 解析器,它可以输出与 CodeProject 语法兼容的 pre
标签呢?所以,我就是这样做的。
自从几周前开始使用 Markdown Monster 以来,我已经创建了 3 个插件(包括这个),以提高我自己的生产力。有一个 目录生成器 插件和一个 列表和图表自动编号 插件,以及,正如您在本文中看到的,CodeProject Markdown 解析器。所有这三个插件都可以在 Markdown Monster 插件管理器中找到。(见图 1。)
我正在用 Markdown Monster 撰写这篇文章。使用我的插件,我能够一键生成目录并自动编号所有图表和列表。

在本文中,您将了解如何使用该插件。然后您将探索插件的实现。您将看到如何创建一个自定义 Markdig 代码块渲染器,以正确地从 Markdown 围栏代码块生成 CodeProject pre
标签。您将了解如何在 Markdown Monster 中为预览查看器创建自定义主题,最后,如何将主题安装到 Markdown Monster 的主题文件夹中。
解决兼容性问题
要在 CodeProject 文章中启用语法高亮,您可以使用文章编辑器中的格式下拉框,或者手动为 pre
标签添加 lang
属性,例如
<pre lang="cs">
...
</pre>
问题是,我所知道的任何 Markdown 解析器都无法生成这种语法。当然,直到现在。
由 Markdig 生成的语法(它是 Markdown Monster 中的默认 Markdown 解析器)生成类似于以下内容的 HTML
<pre><code class="language-csharp">
...
</code></pre>
如果您将类似这样的 HTML 粘贴到 CodeProject 文章编辑器中,它将不知道如何处理它;没有语法高亮,只有丑陋且格式奇怪的文本。
使用本文中概述的插件,您可以输出符合 CodeProject 格式的 HTML。
使用插件
如果您尚未安装 Markdown Monster,请先安装。安装完成后,从 Markdown Monster 的“工具”菜单中打开“插件管理器”。单击“CodeProject Markdown 解析器”项目旁边的“安装”按钮。重新启动 Markdown Monster 以重新填充 Markdown 解析器和主题下拉框。
从 Markdown 解析器下拉框中选择“CodeProject Markdown 解析器”。(见图 2。)
选中 CodeProject Markdown 解析器后,生成的 HTML 会生成符合 CodeProject 约定的 pre
标签。

注意:在撰写文章时,您可能更喜欢坚持使用 Markdig 作为您的 Markdown 解析器。只有当您将文章粘贴到 CodeProject 文章编辑器中时,才需要切换到 CodeProject Markdown 解析器。CodeProject 会动态格式化代码块,因此在选择 CodeProject Markdown 解析器后,在 CodeProject 上预览文章之前,您不会在代码块中看到任何语法高亮。
如果您不关心查看语法高亮的预览,而更愿意使用 CodeProject CSS 预览您的文章,那么从主题下拉列表中选择“CodeProject”主题。(见图 3。)
注意:您的文章要正确渲染到 CodeProject,不需要使用“CodeProject”主题。

提示:在 Markdown Monster 中撰写文章时,将所有图像、下载等放在与文章的 Markdown 文件相同的目录中。这样,当您在 CodeProject 上发布文章时,无需进行任何更改。
导出已完成的文章
完成文章撰写后,如果您希望在 CodeProject 上查看,请点击“将 HTML 复制到剪贴板”按钮。(见图 4。)
Markdown Monster 的状态栏中会显示“HTML 已复制到剪贴板”消息。

切换到浏览器中的 CodeProject 文章,并从编辑器工具栏中选择“源”。(见图 5。)
按 Ctrl-A Ctrl-V 删除编辑器中的当前内容并粘贴 Markdown Monster 中的文本。

瞧,您的用 markdown 撰写的文章,成功地在 CodeProject 上渲染出来了 *希望如此*。点击“预览”按钮,确保其渲染正确。
探索插件的内部工作原理
在本节中,您将了解插件如何替换 CodeProject 兼容的 pre
标签,以及“复制到剪贴板”工具栏按钮的工作原理。最后,您将看到如何创建和部署自定义 Markdown Monster 主题。
我不会介绍创建 Markdown Monster 插件的来龙去脉,因为 Rick 已经做过了。Markdown Monster 帮助文档包含从安装 Visual Studio 扩展、编写插件代码到打包和发布插件的逐步说明。
替换 Markdig 代码块渲染器
开箱即用,Markdown Monster 支持 Markdig 和 Pandoc Markdown 解析器。其默认设置为 Markdig 解析器。要创建您自己的自定义 Markdown 解析器,您需要创建一个插件并重写其 GetMarkdownParser
方法,返回您自己的自定义解析器;如下摘录所示
public override IMarkdownParser GetMarkdownParser()
{
return new MarkdownParserCodeProject();
}
在开始制作这个插件之前,我并不熟悉 Markdig。我从 GitHub 下载了源代码并浏览了一遍。它的结构良好。Markdig 使用一组渲染器来处理将各种 Markdown 表达式转换为 HTML。我确定我可以将默认的 Markdig `CodeBlockRenderer` 替换为我自己的。(见清单 1。)
基类 `MarkdownParserMarkdig` 中的 `CreateRenderer` 方法创建了一个 Markdig `HtmlRenderer` 实例。除了返回一个 `HtmlRenderer` 外,我们还移除了 `CodeBlockRenderer` 并将其替换为我们自定义的 `CPCodeBlockRenderer` 实例。
清单 1. MarkdownParserCodeProject 类
class MarkdownParserCodeProject : MarkdownParserMarkdig
{
public MarkdownParserCodeProject(bool pragmaLines = false, bool forceLoad = false)
: base(pragmaLines, forceLoad)
{
}
protected override IMarkdownRenderer CreateRenderer(TextWriter writer)
{
var renderer = new HtmlRenderer(writer);
CodeBlockRenderer codeBlockRenderer = null;
foreach (var objectRenderer in renderer.ObjectRenderers)
{
codeBlockRenderer = objectRenderer as CodeBlockRenderer;
if (codeBlockRenderer != null)
{
break;
}
}
var cpCodeBlockRenderer = new CPCodeBlockRenderer();
if (codeBlockRenderer != null)
{
renderer.ObjectRenderers.Replace<CodeBlockRenderer>(cpCodeBlockRenderer);
}
else
{
renderer.ObjectRenderers.Add(cpCodeBlockRenderer);
}
return renderer;
}
}
自定义 `CodeBlockRenderer` 输出不将内容包裹在 `code` 元素中的 `pre` 标签。(见清单 2。)
当 `CodeBlock` 对象用于指定了编程语言的围栏代码块时,`CodeBlock` 对象的属性表示 CSS 类的名称。例如,CSS 类可能是“language-xml”。此 CSS 类名称必须映射到 CodeProject `pre` 标签的 `lang` 属性的相应值。
就我的目的而言,我的优先级是 C# 和 XML。我还没有测试其余的值是否正确映射。如果您发现有不正确的,请告诉我,我将更新插件。
清单 2. CPCodeBlockRenderer 类
public class CPCodeBlockRenderer : CodeBlockRenderer
{
protected override void Write(HtmlRenderer renderer, CodeBlock obj)
{
renderer.EnsureLine();
renderer.Write("<pre");
var attributes = obj.TryGetAttributes();
string cssClass = attributes?.Classes.FirstOrDefault();
if (cssClass != null)
{
string langAttributeValue = TranslateCodeClass(cssClass);
renderer.Write(" lang=\"");
renderer.WriteEscape(langAttributeValue);
renderer.Write("\" ");
}
if (attributes?.Id != null)
{
renderer.Write(" id=\"").WriteEscape(attributes.Id).Write("\" ");
}
renderer.Write(">");
renderer.WriteLeafRawLines(obj, true, true);
renderer.WriteLine("</pre>");
}
string TranslateCodeClass(string cssClass)
{
string result;
if (!langLookup.TryGetValue(cssClass, out result))
{
const string languagePrefix = "language-";
if (cssClass.StartsWith(languagePrefix))
{
result = cssClass.Substring(languagePrefix.Length);
}
}
return result;
}
Dictionary<string, string> langLookup = new Dictionary<string, string>
{
{"language-csharp", "cs"},
{"language-javascript", "jscript"},
...
};
}
将 HTML 复制到剪贴板
我们现在就可以完成了,但是从预览中提取 HTML 是很繁琐的。您需要查看并从预览窗格或浏览器窗口复制源代码,然后复制正文元素中相关部分。我决定创建一个按钮来只抓取您需要的 HTML,这样您就可以立即将其粘贴到您的 CodeProject 文章中。
Markdown Monster 允许您在其界面中添加工具栏项目和下拉菜单。它还内置支持 Font Awesome,因此无需为工具栏项目创建图片。
要向 Markdown Monster 添加工具栏项,请创建一个 `AddInMenuItem` 并将其添加到插件类的 `MenuItems` 集合中。(见清单 3。)
清单 3. CodeProjectMarkdownParserAddin.OnApplicationStart 方法
public override void OnApplicationStart()
{
base.OnApplicationStart();
Id = "CodeProjectMarkdownParserAddin";
Name = "CodeProject Markdown Parser";
AddInMenuItem menuItem = new AddInMenuItem(this)
{
Caption = "Copy HTML to Clipboard",
FontawesomeIcon = FontAwesomeIcon.Clipboard
};
// if you don't want to display config or main menu item clear handler
menuItem.ExecuteConfiguration = null;
// Must add the menu to the collection to display menu and toolbar items
MenuItems.Add(menuItem);
EnsureThemeExists();
}
当按钮被点击时,会调用插件类的 `OnExecute` 方法。您重写 `OnExecute` 以应用您的按钮逻辑。在这种情况下,插件调用 `CopyHtmlToClipboard` 方法,如以下摘录所示
public override void OnExecute(object sender)
{
CopyHtmlToClipboard();
}
`CopyHtmlToClipboard` 方法调用当前活动文档的 `RenderHtml` 方法。这会生成一个 HTML 字符串,仅表示 Markdown 文档的内容;不包含 `html`、`head` 或 `body` 标签。(见清单 4。)
文本使用其静态 `SetText` 方法复制到剪贴板。
使用基类的 `ShowStatus` 方法,Markdown Monster 的状态栏中会显示文本已复制的确认信息,持续 3 秒。
清单 4. CodeProjectMarkdownParserAddin.CopyHtmlToClipboard 方法
void CopyHtmlToClipboard()
{
MarkdownDocument document = ActiveDocument;
string html = document.RenderHtml();
Clipboard.SetText(html);
ShowStatus("HTML copied to clipboard.", 3000);
}
创建自定义主题
为了让您大致了解将 HTML 粘贴到 CodeProject 文章中后的样子,我创建了一个自定义预览主题。为此,我下载了 CodeProject 的主 CSS 文件,并将其放置在 Markdown Monster 的“PreviewThemes”目录中的一个名为 CodeProject 的新目录中。
然后我在同一目录中创建了一个 Theme.html 文档。(见清单 5。)
我通过添加指向 `{$themePath}CodeProject_Main.min.css` 的链接来导入 CodeProject CSS。`{$themePath}` 在运行时解析为主题的目录。
Markdown Monster 将 `{$markdownHtml}` 替换为渲染后的 HTML。
`{$markdownHtml}` 周围的 div 模拟了 CodeProject 提交向导的文章预览页面。
清单 5. Theme.html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<base href="{$docPath}" />
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<link href="{$themePath}CodeProject_Main.min.css" rel="stylesheet"/>
<script src="{$themePath}..\scripts\jquery.min.js"></script>
<script src="{$themePath}..\scripts\preview.js"></script>
</head>
<body class="edge edge15" style="top: 0px; position: relative; min-height: 100%;">
<div class="container-article fixed" id="AT">
<div class="article">
<div class="text" id="contentdiv">
<!-- Begin CodeProject HTML -->
{$markdownHtml}
<!-- End CodeProject HTML -->
</div>
</div>
</div>
</body>
</html>
运行时安装主题
目前,Markdown Monster 插件包中不存在包含主题的机制。由插件负责确保主题在运行时存在。
我这样做的方法是将 *html* 和 *css* 文件包含在插件项目中。我将这些文件的内容类型设置为 *嵌入资源*。当插件运行时,它会检查主题文件是否已复制到 Markdown Monster 的 *PreviewThemes* 目录中。(见清单 6。) 如果没有,它会从程序集中提取文件并复制过去。
清单 6. CodeProjectMarkdownParserAddin.EnsureThemeExists 方法
void EnsureThemeExists()
{
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
var previewThemesDirectory = Path.Combine(baseDirectory, "PreviewThemes");
if (!Directory.Exists(previewThemesDirectory))
{
/* Unable to find preview themes directory. Abort. */
return;
}
var cpThemeDir = Path.Combine(previewThemesDirectory, "CodeProject");
if (!Directory.Exists(cpThemeDir))
{
Directory.CreateDirectory(cpThemeDir);
}
string themeFile = Path.Combine(previewThemesDirectory, "Theme.html");
if (!File.Exists(themeFile))
{
CopyEmbeddedResources(cpThemeDir, "CodeProjectMarkdownParserAddin.PreviewTheme",
new List<string> { "CodeProject_Main.min.css", "Theme.html" });
}
}
`CopyEmbeddedResources` 方法使用 `Assembly` 对象的 `GetManifestResourceStream` 方法从 `Assembly` 对象中检索实际文件。(见清单 7。) 然后将文件复制到目标目录。
清单 7. CodeProjectMarkdownParserAddin.CopyEmbeddedResources 方法
static void CopyEmbeddedResources(string outputDir, string resourceLocation, List<string> files)
{
var assembly = Assembly.GetExecutingAssembly();
foreach (var file in files)
{
string embeddedResourcePath = resourceLocation + @"." + file;
using (Stream stream = assembly.GetManifestResourceStream(embeddedResourcePath))
{
if (stream == null)
{
throw new Exception("Unable to locate embedded resource " + embeddedResourcePath);
}
string filePath = Path.Combine(outputDir, file);
using (FileStream fileStream = new FileStream(filePath, FileMode.Create))
{
for (var i = 0; i < stream.Length; i++)
{
fileStream.WriteByte((byte)stream.ReadByte());
}
fileStream.Close();
}
}
}
}
结论
在本文中,您了解了如何使用 CodeProject Markdown 解析器插件。然后您探索了插件的实现。您研究了如何创建一个自定义 Markdig 代码块渲染器,以正确地从 Markdown 围栏代码块生成 CodeProject `pre` 标签。您看到了如何在 Markdown Monster 中为预览器创建自定义主题,以及如何将主题安装到 Markdown Monster 的主题文件夹中。
我希望您觉得这个项目有用。如果有用,我将不胜感激您能对其进行评分和/或在下方留下反馈。这将帮助我写出更好的下一篇文章。
历史
- 2017/08/28 首次发布。