Generating a Markdown Document Table of Contents Tree in C#
Preface I previously wrote an article about processing Markdown documents in C#: Parsing Markdown Documents in C# to Replace Image Links That was my first attempt at processing Markdown with C#. Recently I also revamped the frontend of my blog site. Articles are currently rendered on the frontend with the Editor.md component, but the table of contents tree generated by this plugin looks ugly, so I hacked it and switched to the bootstrap5-treeview component, which looks much better. See this article: Hacking the editormd Component to Improve ToC Renderin
目录8 个章节
Preface
I previously wrote an article about processing Markdown documents in C#: Parsing Markdown Documents in C# to Replace Image Links
That was my first attempt at processing Markdown with C#. Recently I also revamped the frontend of my blog site. Articles are currently rendered on the frontend with the Editor.md component, but the table of contents tree generated by this plugin looks ugly, so I hacked it and switched to the bootstrap5-treeview component, which looks much better. See this article: Hacking the editormd Component to Improve ToC Rendering
I had always wanted to render markdown articles on the backend but never managed to. After this, my thinking opened up, and thus came the C# implementation in this article.
Preparation
Still using the Markdig library
Although this library has basically no documentation and using it is all guesswork, there's currently no better choice, so I'll go with it for now. I even once had the idea of reinventing the wheel, but since I hadn't done similar work before and I've been seriously short on free time lately, I put that idea aside for now.
(Maybe when I have time I'll actually reinvent the wheel. This Markdig library with no docs is really nasty to use)
markdown
The article structure looks like this; due to length, only the headings are shown
## DjangoAdmin
### 一些参考资料
## 界面主题
### SimpleUI
#### 一些相关的参考资料
### django-jazzmin
## 定制案例
### 添加自定义列
#### 效果图
#### 实现过程
#### 扩展:添加链接
### 显示进度条
#### 效果图
#### 实现过程
### 页面上显示合计数额
#### 效果图
#### 实现过程
##### admin.py
##### template
#### 参考资料
### 分权限的软删除
#### 实现过程
##### models.py
##### admin.py
## 扩展工具
### Django AdminPlus
### django-adminactions
Markdig Library
First, read it in
var md = File.ReadAllText(filepath);
var document = Markdown.Parse(md);
After getting the document object, you can iterate over its elements. Markdig processes the markdown document into individual blocks; by iterating this way you can handle each block
foreach (var block in document.AsEnumerable()) {
// ...
}
Different block types are under the Markdig.Syntax namespace. You can see them through the Assemblies browser. Based on the literal name, I found HeadingBlock, tried it, and it is indeed the block that represents a heading.
Then add a check to skip unrelated blocks
foreach (var block in document.AsEnumerable()) {
if (block is not HeadingBlock heading) continue;
// ...
}
That takes care of this step
Defining the Structure
Need two classes
The first represents a heading element. Parent-child headings are associated using id and pid
class Heading {
public int Id { get; set; }
public int Pid { get; set; } = -1;
public string? Text { get; set; }
public int Level { get; set; }
}
The second represents a tree node, similar to a linked-list structure
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; }
}
Preparation done. Time to write the core code
Key Code
The logic is the same as in my previous article that implemented this in JS
Iterate over heading blocks and add them to a list
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}");
}
Based on the position and level relationships of different blocks, infer the parent-child relationships, associating them with id and 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;
}
}
}
Finally, recursively generate the tree structure
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();
}
Done.
Result
Print out the generated tree structure
[
{
"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
}
]
}
]
Complete Code
I wrapped this functionality into a method for easy calling.
Here's the GitHub Gist: https://gist.github.com/Deali-Axy/436589aaac7c12c91e31fdeb851201bf
Next I can try rendering Markdown articles on the backend~
_EOF_
Generating a Markdown Document Table of Contents Tree in C#
- 本文作者:
- 程序设计实验室
- 版权声明:
- 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
相关文章
在.NETCore(C#)中使用EPPlus-Core导出Excel文档
首先安装 直接NuGet安装就行,贼方便。 使用方法 有很多种办法导出,本文只介绍最方便,代码最少的那种。 首先定义一个实体类,例如: public class OutputData { public string Address { get; set; } public int Title { get; set; } public int Description { ge
2021-12-18 · C# · 约 1 分钟
开发现代化的.NetCore控制台程序:(4)使用GithubAction自动构建以及发布nuget包
GitHub Action自动构建发布nuget包,省时省力。Secrets管理token,workflow一劳永逸。吐槽nuget推送重复输入token,烦人!
2023-11-13 · C# · 约 1 分钟
C#中AES加密的实现
AES算法简介 高级加密标准(英语:Advanced Encryption Standard,缩写:AES),在密码学中又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。经过五年的甄选流程,高级加密标准由美国国家标准与技术研究院(NIST)于2001年11月26日发布于FIPS PUB 197,并在2002年5月26日
2022-05-27 · C# · 约 1 分钟
花了不少时间,修复了一个SharpIco生成图标的bug
SharpIco bug修好了?真把自己当回事了,高分辨率图片转ico就崩,这逻辑够清奇。原来是位深度闹的,24转32而已,用点真本事解决?
2025-07-01 · C# · 约 1 分钟
讨论
共 0 条加载评论中…