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

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

廖万里2年前 (2022-10-27)文章57261

前言

之前我写了一篇关于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 转载需授权!

分享到:

添加博主微信共同交流探讨信息差网赚项目: 19528888767 , 请猛戳这里→点我添加

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

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

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

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

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

外媒惊呼,继智能手机之后,中国在这一领域强势崛起!

外媒惊呼,继智能手机之后,中国在这一领域强势崛起!

作为一个80后,我清楚地记得,在功能手机的时代,中国的手机厂商一个能打的都没有,很多人为此努力过,但最后都折戟沉沙,黯淡收场。彼时的中国手机市场完全是三星、诺基亚、摩托罗拉等外资品牌的市场,仅存的中国手机厂商只能在夹缝中生存,靠生产杂牌手机苟延残喘。那时候我们多么希望能崛起一个自主品牌、民族品牌来收...

日本拆无人机后曝光:核心芯片来自美国,大疆会走上华为的老路吗?

日本拆无人机后曝光:核心芯片来自美国,大疆会走上华为的老路吗?

大疆无人机在日被拆解,确认核心部件源自美国?在无人机领域盛传一句话:“世界无人机只分为两类,大疆和其他‘玩具机’”。因此,作为入选了胡润全球独角兽企业的大疆无人机公司,一直是美国、日本等多个国家高精尖企业眼中的“香饽饽”。他们试图攻克大疆的操作原理,更致力于找到突破中国无人机第一的弱点。2020年大...

英国“最富首相”苏纳克如何一路开挂?

英国“最富首相”苏纳克如何一路开挂?

当地时间10月24日下午两点,是英国保守党在下议院党团1922委员会设定的提交新任保守党党首选举提名的截止时间。在成功获得了197名保守党议员提名,且两位对手约翰逊和莫当特先后宣布退选后,前任财政大臣里希·苏纳克成为唯一符合“入闸”条件的党首候选人,按程序自动当选。随后,苏纳克在保守党中央党部发表了...

美媒:ASML开始被中企无情地“打脸”了

美媒:ASML开始被中企无情地“打脸”了

点击关注,每天精彩不断!导读:美媒:ASML开始被中企无情的“打脸”了!自从我国华为公司被打压以后,很多科技企业都意识到了自主研发和生产半导体芯片的重要性;而想要生产芯片并不是一件容易的事情;作为21世纪最伟大的发明之一,半导体芯片如今在整个科技领域都起着至关重要的作用,虽然说一个小小的芯片看上去只...

你最讨厌QQ什么?

你最讨厌QQ什么?

作为中国最早的社交软件之一, QQ承载着无数的青春。QQ最初诞生的时候,是为了方便人们之间的交流而诞生的。那时候 QQ作为我们联系的主要工具,人们之间可以进行即时通讯。我们每天都会在 QQ上和不同的人进行沟通互动。聊天的内容也十分的丰富,有的时候聊天的内容甚至超过了现在人们生活的内容。而其中最让人讨...

发表评论

访客

看不清,换一张

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