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

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文档目录树” 的相关文章

双11想买台便宜的512GB手机,真的就这么难吗?

双11想买台便宜的512GB手机,真的就这么难吗?

双十一可能是很多小伙伴换手机的时间,但是换手机的时候却面临了一个问题,现在手机基本都是128GB起步,但是很多人买手机又想买大内存版本,而大版本的又有些贵,这样就导致本来预算是旗舰机,结果只能换中端机,其实大可不必,因为有这么几款512GB的大内存手机,价格不贵,而且性能也很强,一起看看吧。第一款:...

如何把备用手机号,改为移动便宜的8元套餐,不用去营业厅

如何把备用手机号,改为移动便宜的8元套餐,不用去营业厅

手机可以说是我们最常用的一个通讯工具,比如说我们这几年,可能会用过多部手机,也有多个手机号,但每个朋友可能都会有一台或者两台的备用机,里面只是保号使用,今天给大家分享技巧是如何把我们的备用手机号,设置为一个最低的一个保号套餐,这样的话就会少花冤枉钱,相信这个技巧呢,会对大家有很大的帮助,大家可以点赞...

蛋黄胆固醇高,还能不能吃?早上坚持吃水煮蛋,有什么好处?

蛋黄胆固醇高,还能不能吃?早上坚持吃水煮蛋,有什么好处?

说到鸡蛋,能想到很多食物,只是一颗小小的鸡蛋,却能变着花样的去进行制作,且大多还是比较简单的做法,其中一个比较简单的做法,也是大家比较常用的一个做法,就是清水煮鸡蛋。有的人早餐会吃一个水煮鸡蛋,可帮助补充营养,适当多吃鸡蛋,确实能给身体带来一些好处,同样,也是会存在一些不好的说法,让部分人群对于吃鸡...

华为梅开二度:鸿蒙3.0正式版推送+5.5G网络,网友:遥遥领先

华为梅开二度:鸿蒙3.0正式版推送+5.5G网络,网友:遥遥领先

对于华为手机来说,虽然手机业务受到了很大的压力,但是华为在很多领域的表现都不差,无论是汽车领域还是通信领域,都有着不错的市场表现力。而且这几年的华为在技术研发方面的投入资金也不小,这也意味着进步很大。而且从目前的市场角度来看,华为也迎来了梅开二度的情况,不仅针对鸿蒙OS3.0正式版进行了推送和变化,...

一个时代终于结束了,电商行业被改写

一个时代终于结束了,电商行业被改写

如果你仔细观察近两年电商行业的新变化,你会明显地感觉到,时代一次又一次被改写。淘宝为什么被拼多多赶超了?抖音是娱乐平台,却为什么突然改做电商了?因为一个时代结束了。众所周知,传统的电商是货架电商。货架店上的本质就是把产品上到店里,然后通过搜索引擎优化或者付费推广的模式,带来流量,然后通过促销的方法,...

失窃iPhone最终归宿:一台被盗的手机,如何在华强北“焕发新生”

失窃iPhone最终归宿:一台被盗的手机,如何在华强北“焕发新生”

华强北,一个中国数码史上现象级的名词。这条中轴主干道南北长900多米的街区,造就了中国电子科技历史上的神话。但一个硬币有两面,如果说腾讯、TP-Link这样的成功企业代表了华强北向阳的一面,那么华强北的阴暗一面,就离不开两个词:组装机和山寨机。如今,在各大手机厂商物美价廉的新品联合绞杀之下,山寨机已...

发表评论

访客

看不清,换一张

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