前言
之前我写了一篇关于C#处理Markdown文档的文章:C#解析Markdown文档,实现替换图片链接操作
算是第一次尝试使用C#处理Markdown文档,然后最近又把博客网站的前台改了一下,目前文章渲染使用Editor.md组件在前端渲染,但这个插件生成的目录树很丑,我魔改了一下换成bootstrap5-treeview组件,好看多了。详见这篇文章:魔改editormd组件,优化ToC渲染效果
此前我一直想用后端来渲染markdown文章而不得,经过这个操作,思路就打开了,也就有了本文的C#实现。
准备工作
依然是使用Markdig库
这个库虽然基本没有文档,使用全靠猜,但目前没有好的选择,只能暂时选这个,我甚至一度萌生了想要重新造轮子的想法,不过由于之前没做过类似的工作加上最近空闲时间严重不足,所以暂时把这个想法打消了。
(或许以后有空真得来重新造个轮子,这Markdig库没文档用得太恶心了)
markdown
文章结构是这样的,篇幅关系只把标题展示出来
## DjangoAdmin ### 一些参考资料 ## 界面主题 ### SimpleUI #### 一些相关的参考资料 ### django-jazzmin ## 定制案例 ### 添加自定义列 #### 效果图 #### 实现过程 #### 扩展:添加链接 ### 显示进度条 #### 效果图 #### 实现过程 ### 页面上显示合计数额 #### 效果图 #### 实现过程 ##### admin.py ##### template #### 参考资料 ### 分权限的软删除 #### 实现过程 ##### models.py ##### admin.py ## 扩展工具 ### Django AdminPlus ### django-adminactions
Markdig库
先读取
var md = File.ReadAllText(filepath); var document = Markdown.Parse(md);
得到document对象之后,就可以对里面的元素进行遍历,Markdig把markdown文档处理成一个一个的block,通过这样遍历就可以处理每一个block
foreach (var block in document.AsEnumerable()) { // ... }
不同的block类型在 Markdig.Syntax 命名空间下,通过 Assemblies 浏览器可以看到,根据字面意思,我找到了 HeadingBlock ,试了一下,确实就是代表标题的 block。
那么判断一下,把无关的block去掉
foreach (var block in document.AsEnumerable()) { if (block is not HeadingBlock heading) continue; // ... }
这一步就搞定了
定义结构
需要俩class
第一个是代表一个标题元素,父子关系的标题使用 id 和 pid 关联
class Heading { public int Id { get; set; } public int Pid { get; set; } = -1; public string? Text { get; set; } public int Level { get; set; } }
第二个是代表一个树节点,类似链表结构
public class TocNode { public string? Text { get; set; } public string? Href { get; set; } public List<string>? Tags { get; set; } public List<TocNode>? Nodes { get; set; } }
准备工作搞定,开始写核心代码
关键代码
逻辑跟我前面那篇用JS实现的文章是一样的
遍历标题block,添加到一个列表中
foreach (var block in document.AsEnumerable()) { if (block is not HeadingBlock heading) continue; var item = new Heading {Level = heading.Level, Text = heading.Inline?.FirstChild?.ToString()}; headings.Add(item); Console.WriteLine($"{new string('#', item.Level)} {item.Text}"); }
根据不同block的位置、level关系,推出父子关系,使用 id 和 pid 关联
for (var i = 0; i < headings.Count; i++) { var item = headings[i]; item.Id = i; for (var j = i; j >= 0; j--) { var preItem = headings[j]; if (item.Level == preItem.Level + 1) { item.Pid = j; break; } } }
最后用递归生成树结构
List<TocNode>? GetNodes(int pid = -1) { var nodes = headings.Where(a => a.Pid == pid).ToList(); return nodes.Count == 0 ? null : nodes.Select(a => new TocNode {Text = a.Text, Href = $"#{a.Text}", Nodes = GetNodes(a.Id)}).ToList(); }
搞定。
实现效果
把生成的树结构打印一下
[ { "Text": "DjangoAdmin", "Href": "#DjangoAdmin", "Tags": null, "Nodes": [ { "Text": "一些参考资料", "Href": "#一些参考资料", "Tags": null, "Nodes": null } ] }, { "Text": "界面主题", "Href": "#界面主题", "Tags": null, "Nodes": [ { "Text": "SimpleUI", "Href": "#SimpleUI", "Tags": null, "Nodes": [ { "Text": "一些相关的参考资料", "Href": "#一些相关的参考资料", "Tags": null, "Nodes": null } ] }, { "Text": "django-jazzmin", "Href": "#django-jazzmin", "Tags": null, "Nodes": null } ] }, { "Text": "定制案例", "Href": "#定制案例", "Tags": null, "Nodes": [ { "Text": "添加自定义列", "Href": "#添加自定义列", "Tags": null, "Nodes": [ { "Text": "效果图", "Href": "#效果图", "Tags": null, "Nodes": null }, { "Text": "实现过程", "Href": "#实现过程", "Tags": null, "Nodes": null }, { "Text": "扩展:添加链接", "Href": "#扩展:添加链接", "Tags": null, "Nodes": null } ] }, { "Text": "显示进度条", "Href": "#显示进度条", "Tags": null, "Nodes": [ { "Text": "效果图", "Href": "#效果图", "Tags": null, "Nodes": null }, { "Text": "实现过程", "Href": "#实现过程", "Tags": null, "Nodes": null } ] }, { "Text": "页面上显示合计数额", "Href": "#页面上显示合计数额", "Tags": null, "Nodes": [ { "Text": "效果图", "Href": "#效果图", "Tags": null, "Nodes": null }, { "Text": "实现过程", "Href": "#实现过程", "Tags": null, "Nodes": [ { "Text": "admin.py", "Href": "#admin.py", "Tags": null, "Nodes": null }, { "Text": "template", "Href": "#template", "Tags": null, "Nodes": null } ] }, { "Text": "参考资料", "Href": "#参考资料", "Tags": null, "Nodes": null } ] }, { "Text": "分权限的软删除", "Href": "#分权限的软删除", "Tags": null, "Nodes": [ { "Text": "实现过程", "Href": "#实现过程", "Tags": null, "Nodes": [ { "Text": "models.py", "Href": "#models.py", "Tags": null, "Nodes": null }, { "Text": "admin.py", "Href": "#admin.py", "Tags": null, "Nodes": null } ] } ] } ] }, { "Text": "扩展工具", "Href": "#扩展工具", "Tags": null, "Nodes": [ { "Text": "Django AdminPlus", "Href": "#Django AdminPlus", "Tags": null, "Nodes": null }, { "Text": "django-adminactions", "Href": "#django-adminactions", "Tags": null, "Nodes": null } ] } ]
完整代码
我把这个功能封装成一个方法,方便调用。
直接上GitHub Gist:https://gist.github.com/Deali-Axy/436589aaac7c12c91e31fdeb851201bf
接下来可以尝试使用后端来渲染Markdown文章了~