Markdown パーサ blackfriday.v2 で遊ぶ

この前 markdown テキスト変換ツールの物色を行っていたのだが,この中で blackfriday パッケージがなかなか面白そうなのでちょっと遊んでみることにした。

blackfriday.v2

現在 blackfriday は v2 系が最新版で,作者も v2 を推奨しているみたいなのだが,軽くググってみるかぎり v2 を使っているところを見かけない。 このブログのサイト生成ツールである Hugo も 1.x 系みたいだし。 おそらくインタフェースが違うだけで(HTML に変換する限りは)性能的にはあまり変わらないため需要がないのかもしれない。

v2 系は GitHub ではなく以下から取得するのがいいらしい。

$ go get -u gopkg.in/russross/blackfriday.v2

dep を使うなら dep ensure -add コマンドで取り込むか Gopkg.toml ファイルに以下の記述を追加する。

[[constraint]]
  name = "gopkg.in/russross/blackfriday.v2"
  version = "~2.0.0"

HTML への変換はこんな感じに書ける。

package main

import (
	"fmt"
	"io/ioutil"
	"os"

	"gopkg.in/russross/blackfriday.v2"
)

func main() {
	md, err := ioutil.ReadFile(os.Args[1])
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		return
	}

    //HTMLFlags and Renderer
	htmlFlags := blackfriday.CommonHTMLFlags         //UseXHTML | Smartypants | SmartypantsFractions | SmartypantsDashes | SmartypantsLatexDashes
	htmlFlags |= blackfriday.FootnoteReturnLinks     //Generate a link at the end of a footnote to return to the source
	htmlFlags |= blackfriday.SmartypantsAngledQuotes //Enable angled double quotes (with Smartypants) for double quotes rendering
	htmlFlags |= blackfriday.SmartypantsQuotesNBSP   //Enable French guillemets 損 (with Smartypants)
	renderer := blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{Flags: htmlFlags, Title: "", CSS: ""})

	//Extensions
	extFlags := blackfriday.CommonExtensions //NoIntraEmphasis | Tables | FencedCode | Autolink | Strikethrough | SpaceHeadings | HeadingIDs | BackslashLineBreak | DefinitionLists
	extFlags |= blackfriday.Footnotes        //Pandoc-style footnotes
	extFlags |= blackfriday.HeadingIDs       //specify heading IDs  with {#id}
	extFlags |= blackfriday.Titleblock       //Titleblock ala pandoc
	extFlags |= blackfriday.DefinitionLists  //Render definition lists

	html := blackfriday.Run(md, blackfriday.WithExtensions(extFlags), blackfriday.WithRenderer(renderer))
	fmt.Println(string(html))
}

blackfriday.WithExtensions() 関数および blackfriday.WithRenderer() 関数は Functional Options パターンの応用で任意に設定できる。

v1.x 系に比べて HTML レンダリング・オプションの指定が面倒くさい感じになっているが,これは blackfriday.Renderer インタフェースに合わせた別のレンダリング・パッケージを使えるようにするためらしい。 HTML 変換以外のレンダリング・パッケージとしては $\mathrm{\LaTeX}$ への変換パッケージがあるようだ。

数式表現はできれば MathJax 互換にしてほしかったが,まぁいいか。 これもそのうち試してみたい。

調子に乗ってプレビュー・ツールを作ってみた

一応バイナリも用意している。 使い方はこんな感じ。

$ markdown-preview -h
Processing Markdown by Golang

Usage:
  markdown-preview [flags]
  markdown-preview [command]

Available Commands:
  help        Help about any command
  proc        Processing Markdown
  version     Print the version number of markdown-preview

Flags:
  -h, --help   help for markdown-preview

Use "markdown-preview [command] --help" for more information about a command.

今のところは HTML への変換のみサポートしている。

$ markdown-preview proc -h
Processing Markdown

Usage:
  markdown-preview proc [flags] [markdown file]

Flags:
  -c, --css string      CSS file URL (with --page option)
  -g, --github          use GitHub Markdown API
  -h, --help            help for proc
  -l, --line-break      translate newlines into line breaks
  -o, --output string   output file path
  -p, --page            generate a complete HTML page
  -s, --sanitize        sanitize untrusted content

GitHub Markdown API

--github オプションを使うと blackfriday パッケージではなく GitHub Markdown API を使う。

GitHub Markdown API を利用するには Google による go-github/github パッケージが便利である。 こんな感じに書ける。

import (
	"context"

	"github.com/google/go-github/github"
)

func renderWithGitHub(md []byte) ([]byte, error) {
	client := github.NewClient(nil)
	opt := &github.MarkdownOptions{Mode: "gfm", Context: "google/go-github"}
	body, _, err := client.Markdown(context.Background(), string(md), opt)
	return []byte(body), err
}

テンプレートを使ったページの出力

--page オプションを使うと完全なページを出力する。 blackfriday パッケージなら blackfriday.CompletePage フラグを付加することで完全なページを出力してくれるが,今回はテンプレートを使ってページを出力するようにした。

Go 言語標準のテンプレートパッケージには text/templatehtml/template の2つがある。 html/template<> などの特殊文字を適切に変換してくれるので良いのだが,今回は HTML テキストをまるっと埋め込むので(勝手に sanitizing されては困るので) text/template のほうを使うことにした。

また,テンプレートファイルは go-assets を使ってコードに埋め込むことにした。 いやぁ,勉強しておいてよかった。

用意したテンプレートファイルはこんな感じ。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="generator" content="markdown-preview">{{with .CSS }}
  <link rel="stylesheet" type="text/css" href="{{ . }}">{{ end }}
  <title>{{ .Title }}</title>
</head>
<body>

{{ .Body }}

</body>
</html>

Hugo でテンプレートの扱いにはすっかり慣れてしまったので,このくらいは楽勝である(笑)

テンプレートに埋め込むためのデータセットはこんな感じで用意した。

type PageData struct {
	Title string
	CSS   string
	Body  string
}

これで,テンプレート処理は以下のように書ける。

//Render returns HTML page text with template
func (p *PageData) Render() ([]byte, error) {
	f, err := data.Assets.Open("/template.html")
	if err != nil {
		return nil, err
	}
	tmpData := &bytes.Buffer{}
	io.Copy(tmpData, f)

	t, err := template.New("Markdown Processing").Parse(tmpData.String())
	if err != nil {
		return nil, err
	}

	buf := &bytes.Buffer{}
	err = t.Execute(buf, p)
	return buf.Bytes(), err
}

今後の予定

というほどではないが,簡易 Web サービスでプレビューできるようにしたいな,と。 mattn/mkup みたいな感じ。 ただ Go 言語による Web アプリケーションは最近勉強を始めたばかりなので当分先だろうけど(その前に gpgpdumpRFC 4880bis 対応しろって)。

個人的に CLI のフィルタ・コマンド大好きなので,そういうものばっかり(主に自分用に)作ってるが,監視系のツールや繰り返し処理が多いものについては簡易 Web サービスを立ててブラウザ上で作業するのもアリなんじゃないかと思ったりしている。

ブックマーク