たなしょのメモ

日々勉強していることをつらつらと

Go言語でマークダウンファイルをhtmlファイルに変換する処理を作る

日頃から技術的なことをメモをする際にマークダウン形式でメモを取っているのですが、 それをブログに上げる際にいつもvscodeでhtmlファイルにコンバートをかけていたのですがGo言語の学習のついでに変換機を自作しようと思い作ることにしました。 まだまだ追加で実装しなくてはいけないところが多々ありますがある程度メイン部分が完成したので機能や苦労した点、今後の改良点をここに記載したいと思います。

長くなってしまったので要約です。

TL;DR

・ごく一部の文字は変換することができた。 ・苦労した点はcodeタグ内の「<>」に扱いと、pタグ内に文字をどのように入れるか。 ・今後はaタグや画像の出力、リストなどにも対応していきたい。 ・息抜きに違う言語で開発する際はぜひGo言語を。

実行結果

変換したhtmlを画面で見るとある程度読めなくはないのかなという感じでしょうか。 f:id:bonashochang:20210425225325p:plain

ファイルの全体や各ファイルについて

全体的なファイルは下記のようになりました。

.
|-- Makefile
|-- css.go
|-- execute.go
|-- generate.go
|-- go.mod
|-- main.go
|-- paragraph.go
|-- reg.go
`-- test.md

mdファイルとMakefileを覗いて331ステップGo言語でファイルを作成していました。

cloc mvtohtml/
       9 text files.
       9 unique files.
       1 file ignored.

github.com/AlDanial/cloc v 1.88  T=0.02 s (326.4 files/s, 18156.6 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Go                               6             62             10            331
Markdown                         1              5              0             20
make                             1              4              0             13
-------------------------------------------------------------------------------
SUM:                             8             71             10            364
-------------------------------------------------------------------------------

各ファイルの機能は下記のようになります。 ・main.go    引数チェックやmdファイルのファイル名を取得する。
・execute.go  htmlファイルに書き込む処理。
css.go    css部分を書き込む処理。
・generate.go  変換処理の大本。
・paragraph.go pタグの変換処理。
・reg.go     htmlファイルに変換する際の「<」と「>」が存在するか判定する処理。

機能詳細、苦労した点

h1タグやpタグの判定は各行の1文字目~4文字目を見て判定しています。
brタグは各行の後ろ1〜3行目が空白かどうかで判定しcase文で各パターンを判別しています。

func pattern_check(line string, codeline int, slice_arr *[]string) (int, string) {
    br_flg := 0
    pattern := "NONE"

    html_line := ""
    h_string := ""

    // first string search
    if line == "" && codeline == 0 {
        html_line += "\n"
        return codeline, html_line
    } else if line == "" && codeline == 1 {
        html_line += "\n"
        return codeline, html_line
    } else {
        slice := strings.Split(line, "")
        length := len(slice)

        if slice[0] == "#" && slice[1] == " " {
            pattern = "H1"
            for i := 2; i < length; i++ {
                h_string += slice[i]
            }
        } else if slice[0] == "#" && slice[1] == "#" && slice[2] == " " {
            pattern = "H2"
            for i := 3; i < length; i++ {
                h_string += slice[i]
            }
        } else if slice[0] == "#" && slice[1] == "#" && slice[2] == "#" && slice[3] == " " {
            pattern = "H3"
            for i := 4; i < length; i++ {
                h_string += slice[i]
            }
        }

        // code check
        if line == "```" {
            pattern = "CODE"
        }

        // br check
        if slice[length-1] == " " && slice[length-2] == " " && slice[length-3] == " " {
            br_flg = 1
        }

        // in code tag check
        if pattern != "CODE" && codeline == 1 {
            pattern = "INCODE"
        }

        if pattern != "NONE" {
            paragraph(pattern, &html_line, slice_arr)
        }

        switch pattern {
        case "H1":
            html_line += "<h1>"
            html_line += h_string
            html_line += "</h1>"
        case "H2":
            html_line += "<h2>"
            html_line += h_string
            html_line += "</h2>"
        case "H3":
            html_line += "<h3>"
            html_line += h_string
            html_line += "</h3>"
        case "NONE":
            if br_flg == 1 {
                for i := 0; i < length-3; i++ {
                    html_line += slice[i]
                }
                html_line += "<br>"
            } else {
                html_line += line
            }
            html_line += "\n"
            paragraph(pattern, &html_line, slice_arr)

        case "CODE":
            if codeline == 0 {
                codeline = 1
                html_line += "<pre>"
                html_line += "<code>"
            } else {
                codeline = 0
                html_line += "</code>"
                html_line += "</pre>"
            }
        case "INCODE":
            rep_line := ""
            if reg(line) {
                rep_line = strings.Replace(line, "<", "&lt;", -1)
                rep_line = strings.Replace(rep_line, ">", "&gt;", -1)
                html_line += rep_line
            } else {
                html_line += line
            }
        }

        if pattern != "NONE" {
            html_line += "\n"
        }
        return codeline, html_line
    }
}

苦労した点は2点あり、一点目はcodeタグ内のh1タグなどの「<>」がhmtlがファイルに変換されるとそのままタグとして読み込まれてしまうため文字列に置き換えるところでした。 解決策は「INCODE」パターンに入ったときにreg.goを呼び出して「<>」が存在するかを判定して、存在した場合は「<」は「<」、「>」は「>」に変換をかけるにしました。
case "INCODE"詳細

case "INCODE":
            rep_line := ""
            if reg(line) {
                rep_line = strings.Replace(line, "<", "&lt;", -1)
                rep_line = strings.Replace(rep_line, ">", "&gt;", -1)
                html_line += rep_line
            } else {
                html_line += line
            }

reg.go詳細
goの標準ライブラリであるregexp正規表現を利用して各タグが行内に存在するか判定しています。

package main

import (
    "regexp"
)

const (
    HEADING   = `</?h[1-6]>`
    PARAGRAPH = `</?p>`
    CODE      = `</?code>`
    BREAK     = `<br\s?/?>`
)

func check_regexp(reg string, str string) bool {
    flg := regexp.MustCompile(reg).Match([]byte(str))
    return flg
}

func reg(text string) bool {
    reg_flg := false
    regexp_arr := [...]string{HEADING, PARAGRAPH, CODE, BREAK}
    for _, v := range regexp_arr {
        reg_flg = check_regexp(v, text)
        if reg_flg {
            break
        }
    }

    return reg_flg
}

2点目は「#」や「```」(ここでは特殊文字という)以外の文字が1文字目に現れた場合はpタグ内に格納し、特殊文字が現れた場合はpタグを閉じるようにするところです。
解決策はパターン「NONE」で文字列スライスが空の場合はpタグと行を文字列スライスポインタへ格納、文字列スライスに文字が入っている場合は行を文字列スライスポインタへ格納、パターンが「NONE」以外ならpタグを文字列スライスに格納して文字列スライスポインタ内の文字列をhtmlファイルに出力する方法で解決できました。
以下paragraph.go の詳細です。

package main

func is_Empty(s *[]string) bool {
    return len(*s) == 0
}

func slice_Delete(s *[]string) {
    res := []string{}

    *s = res
}

func slice_Join(s *[]string) string {
    var str string
    for _, v := range *s {
        str += v
    }
    return str
}

func paragraph(pattern string, html_line *string, slice_arr *[]string) {
    if pattern != "NONE" {
        if !(is_Empty(slice_arr)) {
            *slice_arr = append(*slice_arr, "</p>\n")
            *html_line = slice_Join(slice_arr)
            slice_Delete(slice_arr)
        }
    } else {
        if is_Empty(slice_arr) {
            *slice_arr = append(*slice_arr, "<p>")
            *slice_arr = append(*slice_arr, *html_line)
        } else {
            *slice_arr = append(*slice_arr, *html_line)
        }
        *html_line = ""
    }
}

今後の改良点

まだまだpタグ、codeタグ、hタグにしか対応してないので今後はaタグや画像の出力、リストなどにも対応していきたいです!

おわりに

皆さんは業務でPHPを使うことが多いと思いますがたまには息抜きに違う言語で開発してみるのはどうでしょうか?
特にインタプリタ型の言語とコンパイル型の言語では考え方が違うためとてもいい勉強になると思います。
ぜひその際はGo言語は勉強してみてはいかがでしょうか?
今回のソースの詳細は下記リンク にあるのでマークダウン変換機能を作る際は参考にしてみてください(笑) github.com