当前位置:首页 > 文章 > 正文内容

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

廖万里3年前 (2022-10-27)文章57280

前言

之前我写了一篇关于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文章了~前言准备工作    markdown    Markdig库    定义结构关键代码实现效果完整代码


本文链接:https://www.kkkliao.cn/?id=159 转载需授权!

分享到:

版权声明:本文由廖万里的博客发布,如需转载请注明出处。


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

马云即便隐退了,眼光依旧毒辣,退出前的发言值得深思

马云即便隐退了,眼光依旧毒辣,退出前的发言值得深思

(ps:灰色的文字为马云的发言,黑色为作者的补充解读,更助于各位理解)马云发言:过去的这一年,很不寻常,事件发生了巨大的变化。疫情也带来了巨大的挑战,在今天所有巨大的不确定当中,有一件事是确定无疑的。那就是数字化的趋势没有改变。数字化以前只是让一些企业活得更好。而今天是企业活下去的关键,数字化的进程...

木匠的狂傲——魅族手机兴亡史·上

木匠的狂傲——魅族手机兴亡史·上

由于老罗以工匠自诩,黄章也为其木工手艺自豪,故以木匠代指黄章。本来黄章拥有着罗永浩难以比拟的各种优势,例如他的魅族是国内最早做智能手机的,他也是国内“粉丝文化”和“饥饿营销”的鼻祖,他的魅族还有自己的手机工厂,甚至早期没创办小米的雷军还非常想投资魅族!可是魅族还是落得了和锤子一样的结局——被收购。下...

小米12SPro深度体验评测,什么叫“水 桶 旗 舰”啊?

小米12SPro深度体验评测,什么叫“水 桶 旗 舰”啊?

首先,依然是祖传一句话评价:补齐了唯一短板的水桶旗舰。其实就像年初的时候评价小米12Pro一样,今年的12系列看得出小米是在努力的优化体验,而不是单纯的堆叠参数,所以更注重手感的12和12S,以及徕卡加持下算法提升、影调有明显改善的小米12S Ultra便应运而生,至于今年的12Pro和12S Pr...

当代散文|留住乡魂

当代散文|留住乡魂

文/冯帆爷爷和奶奶在故乡给我的父母留下了一座老宅。从我出生到奶奶去世,这所老宅给我留下了26年的印痕。爷爷奶奶现在都不在了,但老宅还在。老宅,在我的心中,是我生命里的一首歌,是我乡愁的寄托,是一家三代人的回忆。老宅里留下了我太多无忧无虑的童年时光和记忆。老宅,建于上世纪七十年代初,三层青石为基,黄土...

会让女性感觉到“不好意思”的事,你做过几件?

会让女性感觉到“不好意思”的事,你做过几件?

作为一名女孩子,你做过哪一些羞耻的事情呢?当被问及这个话题的时候,大多数女生都会一言不发,微笑地看着对方,因为他们也不知道这个问题该怎么回答,明明涉及隐私,可是自己想起来又感觉很好笑,想让自己的男朋友更多的了解自己,可是说出来之后又害怕对方笑话自己。大多数女生在委屈的时候并不会哇哇大哭,因为他们认为...

为啥程序会有bug?

为啥程序会有bug?

如果这是第二次看到我的文章,欢迎右侧扫码订阅我哟~  ?本文长度为4818字,建议阅读13分钟。坚持原创,每一篇都是用心之作~  这是一篇半娱乐性的吐槽文章,权当给广大技术人员解解闷:)。  哈哈哈,然后我要开始讲一个经常在发生的事实了。(程序员们可...

发表评论

访客

看不清,换一张

◎欢迎参与讨论,请在这里发表您的看法和观点。