前言

之前我写了一篇关于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

第一个是代表一个标题元素,父子关系的标题使用 idpid 关联

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关系,推出父子关系,使用 idpid 关联

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文章了~

C#实现生成Markdown文档目录树的更多相关文章

  1. NET 5.0 Swagger API 自动生成MarkDown文档

    目录 1.SwaggerDoc引用 主要接口 接口实现 2.Startup配置 注册SwaggerDoc服务 注册Swagger服务 引用Swagger中间件 3.生成MarkDown 4.生成示例 ...

  2. jacob自己动生成word文档目录

    任务目的 1自动生成word文档目录. 用例测试操作步骤 在一个word文档的第二页填写占位符: {目录}保存.调用程序读取目标文档,自动根据标题生成目录到{目录}位置. 效果 关键代码 insert ...

  3. YUIDoc example代码高亮错误、生成API文档目录不按源文件注释顺序

    1.如果发现yuidoc命令用不了,那就重装nodejs吧 昨天不知道是清扫电脑的原因,yuidoc命令用不了(命令不存在),也没有找到好的解决方法,怒重装YUIDoc也不行.最后想了想,怒重装了no ...

  4. Markdown 文档生成工具

    之前用了很多Markdown 文档生成工具,发现有几个挺好用的,现在整理出来,方便大家快速学习. loppo: 非常简单的静态站点生成器 idoc:简单的文档生成工具 gitbook:大名鼎鼎的文档协 ...

  5. 使用Python从Markdown文档中自动生成标题导航

    概述 知识与思路 代码实现 概述 Markdown 很适合于技术写作,因为技术写作并不需要花哨的排版和内容, 只要内容生动而严谨,文笔朴实而优美. 为了编写对读者更友好的文章,有必要生成文章的标题导航 ...

  6. 优于 swagger 的 java markdown 文档自动生成框架-01-入门使用

    设计初衷 节约时间 Java 文档一直是一个大问题. 很多项目不写文档,即使写文档,对于开发人员来说也是非常痛苦的. 不写文档的缺点自不用多少,手动写文档的缺点也显而易见: 非常浪费时间,而且会出错. ...

  7. 使用shell脚本生成数据库markdown文档

    学习shell脚本编程的一次实践,通过shell脚本生成数据库的markdown文档,代码如下: HOST=xxxxxx PORT=xxxx USER="xxxxx" PASSWO ...

  8. 基于 React 开发了一个 Markdown 文档站点生成工具

    Create React Doc 是一个使用 React 的 markdown 文档站点生成工具.就像 create-react-app 一样,开发者可以使用 Create React Doc 来开发 ...

  9. SpringBoot接口 - 如何生成接口文档之非侵入方式(通过注释生成)Smart-Doc?

    通过Swagger系列可以快速生成API文档,但是这种API文档生成是需要在接口上添加注解等,这表明这是一种侵入式方式: 那么有没有非侵入式方式呢, 比如通过注释生成文档? 本文主要介绍非侵入式的方式 ...

随机推荐

  1. 求教:Knife4jAggregationDesktop访问报错HTTP ERROR 404

    (1)Windows Server 2019下面,java版本:c:\Users\WinUser01\.jdks\corretto-1.8.0_292\bin\java.exe(2)Knife4jAg ...

  2. 【喜讯】新一代大数据任务调度 - Apache DolphinScheduler 社区荣获OSCHINA年度 “最佳技术团队”...

    新一代大数据任务调度 -  Apache DolphinScheduler 继 11 月 19 日由 InfoQ 举办.在 300+ 参评项目中脱颖而出获得 "2020 年度十大开源新锐项目 ...

  3. 总结-DSU ON TREE(树上启发式合并)

    考试遇到一道题: 有一棵n个点的有根树,每个点有一个颜色,每次询问给定一个点\(u\)和一个数\(k\),询问\(u\)子是多少个不同颜色节点的\(k\)级祖先.n<=500000. 显然对每一 ...

  4. HCIA-Datacom 2.2 实验:OSPF路由协议基础实验

    前言:才发现IA也要学OSPF,忍不住吐槽一句,现在太卷了! OSPF简介: 开放式最短路径优先OSPF(Open Shortest Path First)是IETF组织开发的一个基于链路状态的内部网 ...

  5. 【美国血统 American Heritage 题解】已知前序中序 求后序

    题目: 题目名称:美国血统 American Heritage 题目来源:美国血统 American Heritage ## 题目描述 农夫约翰非常认真地对待他的奶牛们的血统.然而他不是一个真正优秀的 ...

  6. 【java】学习路线9-非静态内部类、外部类

    //内部类只能在其外部类当中使用//局部内部类:定义在方法里面//如果内部类和外部类有重名,就近原则在内部类中优先访问内部类.//如果想访问宿主类的同名成员,使用OuterClass.this.xxx ...

  7. Self-Attention:初步理解

    Self-Attention 的基本结构与计算 Attention(注意力)实际上就是权重的另一种应用的称呼,其具体结构与初始输入的 content \(\vec{x_{1}}, \vec{x_{2} ...

  8. 新建Github仓库并上传本地代码

    按照Github的教程 Adding a local repository to GitHub using Git 1. 创建空的Github仓库 创建远程仓库 ,注意不要勾选Add a README ...

  9. 【Spring】Spring bean中id和name的差异

    id和name都是spring 容器中中bean 的唯一标识符. id: 一个bean的唯一标识 , 命名格式必须符合XML ID属性的命名规范 name: 可以用特殊字符,并且一个bean可以用多个 ...

  10. K8S容器HeadlessService间动态IP通信

    文件网址:https://www.kubebiz.com/KubeBiz/MongoDB?k8sv=v1.20 使用文件网址中提供的yaml文件安装三节点的mongodb集群,其service是hea ...