前回は CLI ツールを作成して記事の作成(タイトルのみ)を行えるようにしました。 今回は Markdown 形式で保存したテキストファイルから記事を作成するようにします。
仕様
- Markdown 形式のファイルを読み込む
- DB には Markdown の形式のまま保存する(Javascript側でパースする仕様と考えます)
- Markdown 形式のまま保存するが、タイトルを抽出する必要がある
ということでタイトル部分を抽出する方法を考えます。
テストケースを準備
テストケースを作成します。関数名がイマイチ。
package shared import ( "testing" ) func TestFindTitle(t *testing.T) { matched := FindTitle([]byte("# this is title")) if string(matched) != "this is title" { t.Errorf("Expected 'this is title', got %s", matched) } }
create shared/utils.go
package shared func FindTitle(input []byte) []byte { return []byte("this is title") }
テストを実行します
% go test shared/utils* ok command-line-arguments 0.006s
実装開始
とうわけで処理を考えます。怠惰な私は他人のモジュールにヒントを求めます。
https://github.com/russross/blackfriday/blob/master/block.go#L192
とりあえず文字列の先頭が '# ' の2文字で始まって、その後にスペースを含む文字列があったらそれ全部タイトルで良さげです。
Markdown に変換して HTML から H1 取り出してもいいんですが、そうするよりも自前で実装したほうがシンプルになりそうなので今回はそれで行こうと思います。
edit shared/utils.go:
package shared func FindTitle(str string) string { if input[0] == '#' && input[1] == ' ' { return input[2:] } return nil }
'#' と ' ' はダブルクォーテーションではなく、シングルクォーテーションです。 UTF-8でエンコードした byte 値が欲しいからです。
ついでにテストケースを追加しておきます。
package shared import ( "testing" ) func TestFindTitle(t *testing.T) { title := FindTitle("# this is title") if title != "this is title" { t.Errorf("Expected 'this is title', got %s", title) } title = FindTitle("## header2") if title != "" { t.Errorf("Expected epmpty string, got %s", title) } }
テスト実行結果
% go test shared/utils* -v === RUN TestFindTitle --- PASS: TestFindTitle (0.00s) PASS ok command-line-arguments 0.006s
つなぎこむ
というわけで、あとは以下の作業が残っています。
- 第一引数に filename を指定してテキストを読み込む
- 必要な情報はテキストの1行目(仕様とします)
- その1行目が FindTitle で空文字を返した場合はエラー
edit cli/main.go: 以下の箇所を修正します。
func (f *Publish) Run(args []string) int { fmt.Println("Publish", args) filename := args[0] fp, err := os.Open(filename) if err != nil { log.Fatalln("Error: ", err) return 1 } defer fp.Close() reader := bufio.NewReaderSize(fp, 4096) // 最初の行はタイトル line, _, err := reader.ReadLine() if err != nil { log.Fatalln("Error: ", err) return 1 } title := shared.FindTitle(string(line)) if title == "" { log.Fatalln("Error: %s may be invalid markdown format.", filename) return 1 } // 次の行は空白 empty, _, err := reader.ReadLine() if err != nil { log.Fatalln("Error: ", err) return 1 } if string(empty) != "" { log.Fatalln("Error: %s may be invalid markdown format.", filename) return 1 } // ここから先が Desc var desc string for { line, _, err := reader.ReadLine() if err == io.EOF { break } desc += string(line) + "\n" // XXX: Best Practice を考える } dbmap := shared.NewDbMap(dsn) shared.CreateTablesIfNotExists(dbmap) article, err := shared.AddArticle(dbmap, title, desc) if err != nil { log.Fatalln("Error: ", err) return 1 } fmt.Println("Created: ", article) return 0 } func (f *Publish) Synopsis() string { return "publish <filename>" }
あとは shared/db* を直しておきます。
edit shared/test_db.go
func TestAddArticle(t *testing.T) { dbmap := NewDbMap(dsn) CreateTablesIfNotExists(dbmap) defer dropAndClose(dbmap) AddArticle(dbmap, "Go is awesome", "Awesome description") rows, _ := dbmap.Select(Article{}, "SELECT * FROM articles") if len(rows) != 1 { t.Errorf("Expected 1 invoice rows, got %d", len(rows)) } article := rows[0].(*Article) if article.Title != "Go is awesome" { t.Errorf("Expected Go is awesome, got %s", article.Title) } if article.Desc != "Awesome description" { t.Errorf("Expected Awesome description, got %s", article.Desc) } }
edit shared/go.db
func AddArticle(dbmap *gorp.DbMap, title string, desc string) (article *Article, err error) { article = &Article{ Title: title, Desc: desc, Created: time.Now().Unix(), } err = dbmap.Insert(article) return }
テスト実行
% go test shared/* ok command-line-arguments 0.058s
というわけで以下のように適当な Markdown 形式のテキストファイルを作成します。
# this is markdown outline ## header 2 paragraph
cli を実行します
% go run cli/main.go publish cli/markdown.txt Created: &{2 this is markdown outline ## header 2 paragraph 1440562341 0}
という訳で CLI で記事データを追加できるようになりました。 だんだん記事の内容が gin と関係なくなってきている...記事を書くの難しいな...