前陣子試了在 ASP.NET Core 網站即時顯示 Markdown 文件,如此可用 Visual Studio 寫 Markdown,跟網站一併部署就直接轉網頁,不用依賴 Hugo 之類的第三方工具,對線上說明之類的網站附屬文件挺方便。順著這個概念繼續擴充,我還想自動產生文件清單,用來管理公告、FAQ 之類會持續成長的文件庫,感覺也不賴。

要實現這個想法,每份 Markdown 需要註明標題、發佈時間等額外資訊,最簡單的做法是在 Markdown 最上方加入一段稱為 Front MatterYAML 語法,Front Matter 普遍應用在 Markdown,故很容易找到現成程式庫或工具。

Front Matter 範例:

---
title: "標題"
slug: "文件 URL 名稱"
description: 
author: 
date: 2019-08-22T15:20:28.000Z
lastmod: 2019-08-22T15:20:28.000Z
draft: true
tags: []
categories: []
---
這裡開始寫 Markdown 本文... 

上回介紹過的 C# Markdown 程式庫首選 MarkDig 可識別 Front Matter,但解析 YAML 需另外想辦法。自己寫 Regex 是種解法,但要能完整涵蓋 YAML 完整語法得花點功夫,想了一下,借用現成程式庫比較省事可靠。YAML 程式庫的選擇不多,沒有選擇困難的問題,用 YamlDotNet

研究了一下,MarkDig + YamlDotNet 處理內含 Front Matter Markdown 的寫法有點曲折,所以有了這篇筆記。

解析範例程式如下,詳細做法請參考註解。

using Markdig;
using Markdig.Extensions.Yaml;
using Markdig.Renderers;
using Markdig.Syntax;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

foreach (var file in Directory.GetFiles("Markdowns", "*.md"))
{
    var p = ParseMardownWithFrontMatter(file);
    Console.WriteLine($"File: {file}");
    Console.WriteLine($" * Title = {p.Title}");
    Console.WriteLine($" * Date = {p.Date:yyyy-MM-dd}");
    Console.WriteLine($" * Html = {p.Html}");
}

PostEntry ParseMardownWithFrontMatter(string path)
{
    // 建立能識別 YAML 的 Pipeline
    var pipeline = new MarkdownPipelineBuilder()
        .UseYamlFrontMatter().Build();
    // 建立 TextWriter 及 HtmlRender
    using var sw = new StringWriter();
    var render = new HtmlRenderer(sw);
    pipeline.Setup(render);
    // 預設由檔案資訊決定標題跟時間
    var postEntry = new PostEntry
    {
        Title = path,
        Date = new FileInfo(path).LastWriteTime
    };
    try
    {
        var markdown = File.ReadAllText(path);
        // 套用先前建立的 Pipeline 
        var doc = Markdown.Parse(markdown, pipeline);
        // 取出 Markdown 第一個 YAML
        var yamlBlock = doc.Descendants<YamlFrontMatterBlock>().FirstOrDefault();
        // 若有包含 YAML
        if (yamlBlock != null)
        {
            // 依 YAML 在 Markdown 內容的起始位置及長度取出 YAML 字串
            var yaml = markdown.Substring(yamlBlock.Span.Start, yamlBlock.Span.Length);
            using (var input = new StringReader(yaml))
            {
                // 使用 YAML Parser 開始解析
                var yamlParser = new Parser(input);
                yamlParser.Consume<StreamStart>();
                yamlParser.Consume<DocumentStart>();
                // 建立 YAML 反序列化工具
                var yamlDes = new DeserializerBuilder()
                    .WithNamingConvention(CamelCaseNamingConvention.Instance)
                    .Build();
                // 解析 YAML 內容,對映到資料型別的屬性上
                var frontMatter = yamlDes.Deserialize<PostEntry>(yamlParser);
                yamlParser.Consume<DocumentEnd>();
                postEntry.Title = frontMatter.Title;
                postEntry.Date = frontMatter.Date;
            }
        }
        // 使用 HtmlRenderer 產生 HTML 內容
        render.Render(doc);
        sw.Flush();
        postEntry.Html = sw.ToString();
    }
    catch (Exception ex) {
        postEntry.Html = ex.ToString();
    }
    return postEntry;
}

public class PostEntry
{
    public string Title { get; set; } = string.Empty;
    public DateTime Date { get; set; }
    public string Html { get; set; } = string.Empty;
}

我準備了四個測試檔:沒有 YAML、YAML 格式不對、只有標題 跟 資訊完整

實測成功!

範例專案已上傳 Github

【參考資料】

Example of parsing Markdown document with front matter in C#.


Comments

Be the first to post a comment

Post a comment