C#實現生成Markdown檔案目錄樹

2022-10-25 12:09:31

前言

之前我寫了一篇關於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

第一個是代表一個標題元素,父子關係的標題使用 idpid 關聯

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關係,推出父子關係,使用 idpid 關聯

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文章了~