【C#】MarkdownからHTMLに簡易変換するツールの紹介(ブログ投稿に便利)

概要

本記事では、Markdownで記述された文書ファイルをHTMLファイルに簡易変換するコンソールアプリケーション、及びソースコードを紹介します。

目的

本アプリケーションの目的は、Markdownで記述したブログ記事の投稿を簡単にすることです。

さらに、HTML特殊文字のエスケープ処理機能もあるので、ソースコードの投稿に便利です。

機能説明

  • ディレクトリ又はファイルをコンソールアプリケーションにドラッグ&ドロップすると、検索した全てのMarkdownファイル("*.md")をHTMLファイル("*.html")に変換します。
  • 最低限のMarkdown構文のみ対応しています。(下記、対応一覧表を参照ください)
  • コード部分は、HTML特殊文字をエスケープ処理しています。

<Markdown 構文 対応一覧表>

対応済見出し
対応済番号無しリスト
対応済番号付きリスト
対応済コード
対応済リンク
対応済テーブル
対応済水平線

使い方

  1. Markdown文書ファイル("*.md")を作成する。
  2. 作成したファイル、又は作成したファイルが存在するディレクトリを本アプリケーションにドラッグ&ドロップします。
  3. 変換されたHTMLファイル("*.html")が作成されます。
  4. 作成されたHTMLファイルを開く。
  5. 内容をコピーする。
  6. ブログ投稿ページに貼り付ける。
  7. 投稿する。

開発環境

開発環境Visual Studio 2019
使用言語C#
使用ライブラリ.NET Framework 4.5.2

コード

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;

namespace ConvertMarkdownToHtml
{
    class Program
    {
        static void Main(string[] args)
        {
            // get file list
            var files = new List<string>();
            foreach (var arg in args)
            {
                var path = arg;
                if (File.Exists(path))
                {
                    // file
                    files.Add(path);
                }
                else if (Directory.Exists(path))
                {
                    // directory
                    files.AddRange(Directory.GetFiles(path, "*.*", SearchOption.AllDirectories));
                }
            }

            // convert
            foreach (var file in files)
            {
                if (Path.GetExtension(file) == ".md")
                {
                    // log
                    Console.WriteLine(file);

                    // read
                    var markdownLines = File.ReadAllLines(file);

                    // parse
                    var parser = new Parser();
                    var htmlLines = parser.ParseMarkdown(markdownLines);

                    // write
                    string htmlFile = Path.Combine(Path.GetDirectoryName(file), Path.GetFileNameWithoutExtension(file) + ".html");
                    File.WriteAllLines(htmlFile, htmlLines);

                    // log
                    Console.WriteLine(htmlFile);
                }
            }
        }
    }
    class Parser
    {
        private List<string> m_htmlLines = new List<string>();
        private Stack<ParseStatus> m_statusStack = new Stack<ParseStatus>();

        private enum ParseStatus
        {
            Normal,
            Pre,
            NumberList,
            AsteriskList,
            HyphenList,
            Table,
        }
        public string[] ParseMarkdown(string[] lines)
        {

            m_htmlLines.Add("<!DOCTYPE html>");
            m_htmlLines.Add("<html>");
            m_htmlLines.Add("<head>");
            m_htmlLines.Add("</head>");
            m_htmlLines.Add("<body>");
            m_htmlLines.Add("");

            foreach (var line in lines)
            {
                // 一行解析
                ParseLine(line);
            }

            CloseHtmlTag();

            m_htmlLines.Add("");
            m_htmlLines.Add("</body>");
            m_htmlLines.Add("</html>");

            return m_htmlLines.ToArray();
        }
        private void ParseLine(string targetLine)
        {
            // match link
            Regex regex_link = new Regex(@"(?<start>.*)\[(?<message>.*)\]\((?<url>.*)\)(?<end>.*)");
            Match match_link = regex_link.Match(targetLine);

            // parse
            if (targetLine.StartsWith("```"))
            {
                if (m_statusStack.Count() > 0 && m_statusStack.Peek() == ParseStatus.Pre)
                {
                    // pre end
                    CloseHtmlTag();
                }
                else
                {
                    // classValue
                    string classValue = string.Empty;
                    Regex r = new Regex(@"```(?<classValue>.*)");
                    Match m = r.Match(targetLine);
                    if (m.Success == true)
                    {
                        classValue = m.Groups["classValue"].Value;
                    }

                    // pre start
                    CloseHtmlTag();
                    OpenHtmlTag(ParseStatus.Pre, classValue);
                }
            }
            else if (m_statusStack.Count() > 0 && m_statusStack.Peek() == ParseStatus.Pre)
            {
                // エスケープ処理
                string escapeLine = EscapeHtml(targetLine);
                m_htmlLines.Add(escapeLine);
            }
            else if (match_link.Success)
            {
                string result_link = string.Format("{0}<a href=\"{1}\">{2}</a>{3}"
                                                    , match_link.Groups["start"].Value
                                                    , match_link.Groups["url"].Value
                                                    , match_link.Groups["message"].Value
                                                    , match_link.Groups["end"].Value
                                                    );
                m_htmlLines.Add(result_link);
            }
            else if (targetLine == "***" || targetLine == "---")
            {
                CloseHtmlTag();
                m_htmlLines.Add("<hr>");
            }
            else if (targetLine.StartsWith("# "))
            {
                CloseHtmlTag();
                m_htmlLines.Add("<h1>" + targetLine.Replace("# ", "") + "</h1>");
            }
            else if (targetLine.StartsWith("## "))
            {
                CloseHtmlTag();
                m_htmlLines.Add("<h2>" + targetLine.Replace("## ", "") + "</h2>");
            }
            else if (targetLine.StartsWith("### "))
            {
                CloseHtmlTag();
                m_htmlLines.Add("<h3>" + targetLine.Replace("### ", "") + "</h3>");
            }
            else if (targetLine.StartsWith("#### "))
            {
                CloseHtmlTag();
                m_htmlLines.Add("<h4>" + targetLine.Replace("#### ", "") + "</h4>");
            }
            else if (targetLine.StartsWith("##### "))
            {
                CloseHtmlTag();
                m_htmlLines.Add("<h5>" + targetLine.Replace("##### ", "") + "</h5>");
            }
            else if (targetLine.StartsWith("###### "))
            {
                CloseHtmlTag();
                m_htmlLines.Add("<h6>" + targetLine.Replace("###### ", "") + "</h6>");
            }
            else if (targetLine.StartsWith("1. "))
            {
                if (m_statusStack.Count() > 0 && m_statusStack.Peek() == ParseStatus.NumberList)
                {
                    // Nothing
                }
                else
                {
                    CloseHtmlTag();
                    OpenHtmlTag(ParseStatus.NumberList);
                }
                m_htmlLines.Add("<li>" + targetLine.Replace("1. ", "") + "</li>");
            }
            else if (targetLine.StartsWith("* "))
            {
                if (m_statusStack.Count() > 0 && m_statusStack.Peek() == ParseStatus.AsteriskList)
                {
                    // Nothing
                }
                else
                {
                    CloseHtmlTag();
                    OpenHtmlTag(ParseStatus.AsteriskList);
                }
                m_htmlLines.Add("<li>" + targetLine.Replace("* ", "") + "</li>");
            }
            else if (targetLine.StartsWith("- "))
            {
                if (m_statusStack.Count() > 0 && m_statusStack.Peek() == ParseStatus.HyphenList)
                {
                    // Nothing
                }
                else
                {
                    CloseHtmlTag();
                    OpenHtmlTag(ParseStatus.HyphenList);
                }
                m_htmlLines.Add("<li>" + targetLine.Replace("- ", "") + "</li>");
            }
            else if (targetLine.StartsWith("|"))
            {
                if (m_statusStack.Count() > 0 && m_statusStack.Peek() == ParseStatus.Table)
                {
                    // Nothing
                }
                else
                {
                    CloseHtmlTag();
                    OpenHtmlTag(ParseStatus.Table);
                }

                // table row start
                m_htmlLines.Add("<tr>");

                // table data
                var columns = targetLine.Split(new string[] { "|" }, StringSplitOptions.RemoveEmptyEntries);
                foreach (var column in columns)
                {
                    m_htmlLines.Add("<td>" + column + "</td>");
                }

                // table row end
                m_htmlLines.Add("</tr>");
            }
            else if (targetLine == "")
            {
                CloseHtmlTag();
                m_htmlLines.Add(targetLine);
            }
            else
            {
                CloseHtmlTag();
                m_htmlLines.Add("<p>" + targetLine + "</p>");
            }
        }
        private void OpenHtmlTag(ParseStatus status, string classValue = null)
        {
            // 追加
            m_statusStack.Push(status);

            // TAG開始
            switch (m_statusStack.Peek())
            {
                case ParseStatus.Normal:
                    break;
                case ParseStatus.Pre:
                    if (string.IsNullOrEmpty(classValue))
                    {
                        m_htmlLines.Add("<pre>");
                    }
                    else
                    {
                        m_htmlLines.Add(string.Format("<pre class=\"{0}\">", classValue));
                    }
                    break;
                case ParseStatus.NumberList:
                    m_htmlLines.Add("<ol>");
                    break;
                case ParseStatus.AsteriskList:
                    m_htmlLines.Add("<ul>");
                    break;
                case ParseStatus.HyphenList:
                    m_htmlLines.Add("<ul>");
                    break;
                case ParseStatus.Table:
                    m_htmlLines.Add("<table>");
                    break;
                default:
                    break;
            }
        }
        private void CloseHtmlTag()
        {
            // 空チェック
            if (m_statusStack.Count() == 0)
            {
                return;
            }

            // TAG終了
            switch (m_statusStack.Peek())
            {
                case ParseStatus.Normal:
                    break;
                case ParseStatus.Pre:
                    m_htmlLines.Add("</pre>");
                    break;
                case ParseStatus.NumberList:
                    m_htmlLines.Add("</ol>");
                    break;
                case ParseStatus.AsteriskList:
                    m_htmlLines.Add("</ul>");
                    break;
                case ParseStatus.HyphenList:
                    m_htmlLines.Add("</ul>");
                    break;
                case ParseStatus.Table:
                    m_htmlLines.Add("</table>");
                    break;
                default:
                    break;
            }

            // 削除
            m_statusStack.Pop();
        }
        private string EscapeHtml(string target)
        {
            string result = target;

            // 変換
            result = result.Replace("&", "&");
            result = result.Replace("\"", """);
            result = result.Replace("\'", "'");
            result = result.Replace("¥", "¥");
            result = result.Replace("<", "<");
            result = result.Replace(">", ">");
            //result = result.Replace(" ", " ");
            return result;
        }
    }
}
おすすめの記事