返回文章列表文章索引阅读全文
STARBLOG / 阅读手记
C#

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

曦远 (DealiAxy)约 21 分钟
目录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 许可协议。转载请注明出处!

相关文章

C#学习(一)委托的概念和使用

前言 C#学习系列是根据我以前的笔记整理出来复习,顺便发一下文章做个记录。 先引用一个介绍: C# 中的委托(Delegate)类似于 C 或 C++ 中函数的指针。委托(Delegate) 是存有对某个方法的引用的一种引用类型变量。引用可在运行时被改变。 委托(Delegate)特别用于实现事件和回调方法。所有的委托(Delegate)都派生自 System.Delegate 类。 根据使用其他

2021-12-18 · C# · 约 1 分钟

讨论

共 0 条

良言一句三冬暖,恶语伤人六月寒。

加载评论中…