公開日:2021.04.26

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

テクログtrivia

はじめまして。たなしょです。   

日頃から技術的なことをメモをする際にマークダウン形式でメモを取っているのですが、

それをブログに上げる際にいつもvscodeでhtmlファイルにコンバートをかけていました。そこでGo言語の学習のついでに変換機を自作しようと思い作ることにしました。

まだまだ追加で実装しなくてはいけないところが多々ありますがある程度メイン部分が完成したので機能や苦労した点、今後の改良点をここに記載したいと思います。

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

TL;DR

・ごく一部の文字は変換することができた。

・苦労した点はcodeタグ内の「<>」の扱いと、pタグ内に文字をどのように入れるか。

・今後はaタグや画像の出力、リストなどにも対応していきたい。

・息抜きに違う言語で開発する際はぜひGo言語を。

実行結果

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

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

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

.
|-- 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 += "?"
??return codeline, html_line
?} else if line == "" && codeline == 1 {
??html_line += "?"
??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 += "?"
???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 += "?"
??}
??return codeline, html_line
?}
}

苦労した点は2点あり、一点目はcodeタグ内のh1タグなどの「<>」がhmtlがファイルに変換されるとそのままタグとして読み込まれてしまうため文字列に置き換えるところでした。

解決策は「INCODE」パターンに入ったときにreg.goを呼び出して「<>」が存在するかを判定して、存在した場合は「<」は「&lt;」、「>」は「&gt;」に変換をかけることにしました。  

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   = `<brs?/?>`
)

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>?")
???*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言語は勉強してみてはいかがでしょうか?   

今回のソースの詳細は下記リンク にあるのでマークダウン変換機能を作る際は参考にしてみてください(笑)

https://github.com/jacoloves/lab/tree/master/go_tool/mvtohtml

  

この記事を書いた人

たなしょ

入社年2021年

出身地千葉県

業務内容webページのエンハンス

特技または趣味登山、ランニング、キャンプ、ベース

たなしょの記事一覧へ

テクログに関する記事一覧