前言

之前我写了一篇关于C#处理Markdown文档的文章:C#解析Markdown文档,实现替换图片链接操作

算是第一次尝试使用C#处理Markdown文档,然后最近又把博客网站的前台改了一下,目前文章渲染使用Editor.md组件在前端渲染,但这个插件生成的目录树很丑,我魔改了一下换成bootstrap5-treeview组件,好看多了。详见这篇文章:魔改editormd组件,优化ToC渲染效果

此前我一直想用后端来渲染markdown文章而不得,经过这个操作,思路就打开了,也就有了本文的C#实现。

准备工作

依然是使用Markdig库

这个库虽然基本没有文档,使用全靠猜,但目前没有好的选择,只能暂时选这个,我甚至一度萌生了想要重新造轮子的想法,不过由于之前没做过类似的工作加上最近空闲时间严重不足,所以暂时把这个想法打消了。

(或许以后有空真得来重新造个轮子,这Markdig库没文档用得太恶心了)

markdown

文章结构是这样的,篇幅关系只把标题展示出来

  1. ## DjangoAdmin
  2. ### 一些参考资料
  3. ## 界面主题
  4. ### SimpleUI
  5. #### 一些相关的参考资料
  6. ### django-jazzmin
  7. ## 定制案例
  8. ### 添加自定义列
  9. #### 效果图
  10. #### 实现过程
  11. #### 扩展:添加链接
  12. ### 显示进度条
  13. #### 效果图
  14. #### 实现过程
  15. ### 页面上显示合计数额
  16. #### 效果图
  17. #### 实现过程
  18. ##### admin.py
  19. ##### template
  20. #### 参考资料
  21. ### 分权限的软删除
  22. #### 实现过程
  23. ##### models.py
  24. ##### admin.py
  25. ## 扩展工具
  26. ### Django AdminPlus
  27. ### django-adminactions

Markdig库

先读取

  1. var md = File.ReadAllText(filepath);
  2. var document = Markdown.Parse(md);

得到document对象之后,就可以对里面的元素进行遍历,Markdig把markdown文档处理成一个一个的block,通过这样遍历就可以处理每一个block

  1. foreach (var block in document.AsEnumerable()) {
  2. // ...
  3. }

不同的block类型在 Markdig.Syntax 命名空间下,通过 Assemblies 浏览器可以看到,根据字面意思,我找到了 HeadingBlock ,试了一下,确实就是代表标题的 block。

那么判断一下,把无关的block去掉

  1. foreach (var block in document.AsEnumerable()) {
  2. if (block is not HeadingBlock heading) continue;
  3. // ...
  4. }

这一步就搞定了

定义结构

需要俩class

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

  1. class Heading {
  2. public int Id { get; set; }
  3. public int Pid { get; set; } = -1;
  4. public string? Text { get; set; }
  5. public int Level { get; set; }
  6. }

第二个是代表一个树节点,类似链表结构

  1. public class TocNode {
  2. public string? Text { get; set; }
  3. public string? Href { get; set; }
  4. public List<string>? Tags { get; set; }
  5. public List<TocNode>? Nodes { get; set; }
  6. }

准备工作搞定,开始写核心代码

关键代码

逻辑跟我前面那篇用JS实现的文章是一样的

遍历标题block,添加到一个列表中

  1. foreach (var block in document.AsEnumerable()) {
  2. if (block is not HeadingBlock heading) continue;
  3. var item = new Heading {Level = heading.Level, Text = heading.Inline?.FirstChild?.ToString()};
  4. headings.Add(item);
  5. Console.WriteLine($"{new string('#', item.Level)} {item.Text}");
  6. }

根据不同block的位置、level关系,推出父子关系,使用 idpid 关联

  1. for (var i = 0; i < headings.Count; i++) {
  2. var item = headings[i];
  3. item.Id = i;
  4. for (var j = i; j >= 0; j--) {
  5. var preItem = headings[j];
  6. if (item.Level == preItem.Level + 1) {
  7. item.Pid = j;
  8. break;
  9. }
  10. }
  11. }

最后用递归生成树结构

  1. List<TocNode>? GetNodes(int pid = -1) {
  2. var nodes = headings.Where(a => a.Pid == pid).ToList();
  3. return nodes.Count == 0 ? null
  4. : nodes.Select(a => new TocNode {Text = a.Text, Href = $"#{a.Text}", Nodes = GetNodes(a.Id)}).ToList();
  5. }

搞定。

实现效果

把生成的树结构打印一下

  1. [
  2. {
  3. "Text": "DjangoAdmin",
  4. "Href": "#DjangoAdmin",
  5. "Tags": null,
  6. "Nodes": [
  7. {
  8. "Text": "一些参考资料",
  9. "Href": "#一些参考资料",
  10. "Tags": null,
  11. "Nodes": null
  12. }
  13. ]
  14. },
  15. {
  16. "Text": "界面主题",
  17. "Href": "#界面主题",
  18. "Tags": null,
  19. "Nodes": [
  20. {
  21. "Text": "SimpleUI",
  22. "Href": "#SimpleUI",
  23. "Tags": null,
  24. "Nodes": [
  25. {
  26. "Text": "一些相关的参考资料",
  27. "Href": "#一些相关的参考资料",
  28. "Tags": null,
  29. "Nodes": null
  30. }
  31. ]
  32. },
  33. {
  34. "Text": "django-jazzmin",
  35. "Href": "#django-jazzmin",
  36. "Tags": null,
  37. "Nodes": null
  38. }
  39. ]
  40. },
  41. {
  42. "Text": "定制案例",
  43. "Href": "#定制案例",
  44. "Tags": null,
  45. "Nodes": [
  46. {
  47. "Text": "添加自定义列",
  48. "Href": "#添加自定义列",
  49. "Tags": null,
  50. "Nodes": [
  51. {
  52. "Text": "效果图",
  53. "Href": "#效果图",
  54. "Tags": null,
  55. "Nodes": null
  56. },
  57. {
  58. "Text": "实现过程",
  59. "Href": "#实现过程",
  60. "Tags": null,
  61. "Nodes": null
  62. },
  63. {
  64. "Text": "扩展:添加链接",
  65. "Href": "#扩展:添加链接",
  66. "Tags": null,
  67. "Nodes": null
  68. }
  69. ]
  70. },
  71. {
  72. "Text": "显示进度条",
  73. "Href": "#显示进度条",
  74. "Tags": null,
  75. "Nodes": [
  76. {
  77. "Text": "效果图",
  78. "Href": "#效果图",
  79. "Tags": null,
  80. "Nodes": null
  81. },
  82. {
  83. "Text": "实现过程",
  84. "Href": "#实现过程",
  85. "Tags": null,
  86. "Nodes": null
  87. }
  88. ]
  89. },
  90. {
  91. "Text": "页面上显示合计数额",
  92. "Href": "#页面上显示合计数额",
  93. "Tags": null,
  94. "Nodes": [
  95. {
  96. "Text": "效果图",
  97. "Href": "#效果图",
  98. "Tags": null,
  99. "Nodes": null
  100. },
  101. {
  102. "Text": "实现过程",
  103. "Href": "#实现过程",
  104. "Tags": null,
  105. "Nodes": [
  106. {
  107. "Text": "admin.py",
  108. "Href": "#admin.py",
  109. "Tags": null,
  110. "Nodes": null
  111. },
  112. {
  113. "Text": "template",
  114. "Href": "#template",
  115. "Tags": null,
  116. "Nodes": null
  117. }
  118. ]
  119. },
  120. {
  121. "Text": "参考资料",
  122. "Href": "#参考资料",
  123. "Tags": null,
  124. "Nodes": null
  125. }
  126. ]
  127. },
  128. {
  129. "Text": "分权限的软删除",
  130. "Href": "#分权限的软删除",
  131. "Tags": null,
  132. "Nodes": [
  133. {
  134. "Text": "实现过程",
  135. "Href": "#实现过程",
  136. "Tags": null,
  137. "Nodes": [
  138. {
  139. "Text": "models.py",
  140. "Href": "#models.py",
  141. "Tags": null,
  142. "Nodes": null
  143. },
  144. {
  145. "Text": "admin.py",
  146. "Href": "#admin.py",
  147. "Tags": null,
  148. "Nodes": null
  149. }
  150. ]
  151. }
  152. ]
  153. }
  154. ]
  155. },
  156. {
  157. "Text": "扩展工具",
  158. "Href": "#扩展工具",
  159. "Tags": null,
  160. "Nodes": [
  161. {
  162. "Text": "Django AdminPlus",
  163. "Href": "#Django AdminPlus",
  164. "Tags": null,
  165. "Nodes": null
  166. },
  167. {
  168. "Text": "django-adminactions",
  169. "Href": "#django-adminactions",
  170. "Tags": null,
  171. "Nodes": null
  172. }
  173. ]
  174. }
  175. ]

完整代码

我把这个功能封装成一个方法,方便调用。

直接上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. 【喜讯】新一代大数据任务调度 - Apache DolphinScheduler 社区荣获OSCHINA年度 “最佳技术团队”...

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

  2. Angular 新建项目错误:The Schematic workflow failed. See above

    记录踩坑填坑,有不正之处请指出 错误 解决方法1 npm config set registry https://registry.npmjs.org/ 也可使用淘宝镜像 npm config set ...

  3. Luogu3398 仓鼠找sugar (LCA)

    第一发lg[]没开够RE了,下了数据本地一直停止运行,还以为是dfs死了,绝望一交,A了... 判断\(x\)是否在路径\(s-t\)上,只需满足 \(dep_{x} >= dep_{LCA(s ...

  4. Java精进-手写持久层框架

    前言 本文适合有一定java基础的同学,通过自定义持久层框架,可以更加清楚常用的mybatis等开源框架的原理. JDBC操作回顾及问题分析 学习java的同学一定避免不了接触过jdbc,让我们来回顾 ...

  5. 十周周末总结 MySQL的介绍与使用

    python 十周周末总结 MySQL的介绍与使用 MySQL字符编码与配置文件 查看数据库的基本信息(用户,字符编码) /s windos下MySQL默认的配置文件 my_default.ini 修 ...

  6. 【NOI P模拟赛】大阶乘(斯特林数)

    题意 求 16 16 16 进制下, n ! n! n! 去掉尾部 0 0 0 后取模 2 64 2^{64} 264 的结果. n < 2 64 n<2^{64} n<264 一共 ...

  7. bind搭建内网DNS服务器架构(主从、子域授权、DNS转发器)

    实验目的 模拟企业DNS服务架构服务器及原理 实验环境准备 实验架构图 实验设备 DNS服务器4台 主服务器master(centos8):IP_192.168.100.30, 从服务器slave(r ...

  8. 【java】学习路径29-异常捕捉实例

    import java.util.ArrayList;public class ExceptionCatchDemo { public static void main(String[] args) ...

  9. 第六十二篇:Vue的双向绑定与按键修饰符

    好家伙,依旧是vue的基础 1.按键修饰符 假设我们在一个<input>框中输入了12345,我们希望按一下"Esc" 然后删除所有前面输入的内容,这时候,我们会用到按 ...

  10. 第十五章 部署zookeeper集群

    1.集群规划 主机名 角色 IP hdss7-11.host.com k8s代理节点1.zk1 10.4.7.11 hdss7-12.host.com k8s代理节点2.zk2 10.4.7.12 h ...