• C#实现生成Markdown文档目录树


    前言#

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

  • 相关阅读:
    react18【系列实用教程】useReducer —— 升级版的 useState (2024最新版)
    Excel-快速将公式运用到一整列
    【C++】特殊类的设计
    使用IDEA创建SpringCloud项目
    【Linux】写一个日志类
    HP路由器和交换机日志分析
    初学Linux(学习笔记)
    强制20天内开发APP后集体被裁,技术负责人怒用公司官微发文:祝“早日倒闭”
    以数字技术赋能产业金融生态能力建设,破解银行的场景焦虑
    Django笔记二十九之中间件介绍及使用示例
  • 原文地址:https://www.cnblogs.com/deali/p/16823866.html