Go言語/goldmarkのレンダラーをカスタマイズする
本サイトのリニューアルを進めるうえで、新たにGo製のサイトジェネレータを自作しています。 新サイトジェネレータでは、Markdownで書いた記事をgoldmarkというライブラリでレンダリングするようにしてあります。
基本的にはデフォルトの挙動で問題無かったのですが、一部の要素はカスタマイズする必要がありました。 たとえばHeading要素に日本語のIDを付けたり、画像タグの仕様を変更してwidth/heightプロパティを設定できるようにしたり……。
goldmarkは単純に使うだけなら簡単なのですが、カスタマイズしようとすると意外と癖があったので、メモを残しておきます。
なお、この記事はgoldmark 1.5.5を対象に確認してあります。必要に応じて公式のドキュメントもご確認ください。
TL; DR: このページの末尾に完成したコードのサンプルがあります。
基本的なgoldmarkの使い方
デフォルトのレンダラを使う場合、以下のようなコードで変換できます。
package main import ( "bytes" "fmt" "github.com/yuin/goldmark" ) func main() { input := []byte(` # hello world this is test `) var output bytes.Buffer md := goldmark.New() if err := md.Convert(input, &output); err != nil { panic(err) } fmt.Print(output.String()) }
上記を実行すると以下のようなテキストが表示されるはず。
<h1>hello world</h1> <p>this is test</p>
以降は例として、この出力のh1タグを書き換えてみようと思います。
カスタムレンダラーを作る
必要なパッケージをimportする
カスタマイズをするためには以下のパッケージを使用します。
import ( "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/util" )
描画関数を作る
まず最初に、goldmark/noderenderer.NodeRendererを実装した構造体を作ります。 少しややこしいのですが、この構造体自体が描画を担うわけではなく、描画用の関数を登録するだけの構造体です。
ここではシンプルに、無名関数で作ったものをその場で登録することにします。
// ノードを描画する関数を登録するための構造体。 type MyRendererRegisterer struct {} func (r MyRendererRegisterer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { // ノードを描画するための関数。 // タグの開始地点の場合は entering が true に、終了地点の場合は false になる。 renderFunc := func(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { heading := node.(*ast.Heading) if entering { fmt.Fprintf(w, "<h%d> (((", heading.Level+1) } else { fmt.Fprintf(w, "))) </h%d>\n", heading.Level+1) } return ast.WalkContinue, nil } reg.Register( ast.KindHeading, // カスタマイズしたいノードの種類。 renderFunc, // ノードを描画するための関数。 ) }
MyRendererRegisterer.RegisterFuncs
の中で定義されている renderFunc
が実際に描画を行なう関数です。
第三引数に渡されるgoldmark/ast.Nodeを通じて、Headingノードならh1なのかh2なのかといったレベルを取得したり、Linkノードならリンク先のURLを取得したりできます。
registerFunc
の戻り値は ast.WalkContinue
で固定にしていますが、 ast.WalkStop
で処理を停止させたり、 ast.WalkSkipChildren
で子要素を無視させたりすることもできます。
定義した関数は reg.Register
メソッドで登録しています。
第一引数で指定したノードを見つけると、第二引数で渡した関数を呼んでくれるような挙動になります。
第一引数に指定できる値はastパッケージのVariablesの章で確認できます。
Registererを登録する
次に、描画関数(=renderFunc
)を登録するための構造体(=MyRendererRegisterer
)を、goldmark.Markdown
に登録します。ややこしくなってまいりました。
goldmark.New()
していた部分を、以下のように書き換えます。
// goldmark.New() に渡すためのオプション。 option := goldmark.WithRendererOptions( renderer.WithNodeRenderers( util.Prioritized( MyRendererRegisterer{}, 500, // 描画関数が使用される優先度。とりあえず500くらいを指定しておけばよいみたい? ), // ・ // ・ // ・ // ここに他のノードの描画関数を追加することもできる。 ), ) md := goldmark.New(option)
こうすると先ほどのRegisterFuncs
が呼び出されて、その先でrenderFunc
が登録されます。
登録対象のノードが見つかると、renderFunc
が呼び出されて描画に使用される、という流れになります。
完成したコード
ここまでで作ったものをまとめると、以下のようなコードになります。
package main import ( "bytes" "fmt" "github.com/yuin/goldmark" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/util" ) // ノードを描画する関数を登録するための構造体。 type MyRendererRegisterer struct {} func (r MyRendererRegisterer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { // ノードを描画するための関数。 // タグの開始地点の場合は entering が true に、終了地点の場合は false になる。 renderFunc := func(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { heading := node.(*ast.Heading) if entering { fmt.Fprintf(w, "<h%d> (((", heading.Level+1) } else { fmt.Fprintf(w, "))) </h%d>\n", heading.Level+1) } return ast.WalkContinue, nil } reg.Register( ast.KindHeading, // カスタマイズしたいノードの種類。 renderFunc, // ノードを描画するための関数。 ) } func main() { input := []byte(` # hello world this is test `) var output bytes.Buffer // goldmark.New() に渡すためのオプション。 option := goldmark.WithRendererOptions( renderer.WithNodeRenderers( util.Prioritized( MyRendererRegisterer{}, 500, // 描画関数が使用される優先度。とりあえず500くらいを指定しておけばよいみたい? ), // ・ // ・ // ・ // ここに他のノードの描画関数を追加することもできる。 ), ) md := goldmark.New(option) if err := md.Convert(input, &output); err != nil { panic(err) } fmt.Print(output.String()) }
上記を実行すると、以下のような出力を得られるはずです。
<h2> (((hello world))) </h2> <p>this is test</p>
h1ではなくh2になり、内側に括弧が付いています。 無事に結果を書き換えることができました!
おまけ: もう少し楽に登録できるヘルパを作る
上記の方法でも動くのですが、構造が複雑なので若干難しいコードになってしまいます。 そこで、このサイトの生成コードでは以下のようなヘルパを噛ませています。
// レンダリングの優先度、対象ノードの種別、レンダ関数などをまとめた構造体。 type NodeRenderer interface { Priority() int Kind() ast.NodeKind Render(w BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) } // NodeRenderer構造体を登録するための関数。 func WithNodeRenderers(xs ...NodeRenderer) Option { var rs []util.PrioritizedValue for _, x := range xs { rs = append(rs, util.Prioritized(nodeRendererRegisterer{ Kind: x.Kind(), RenderFunc: x.Render, }, x.Priority())) } return goldmark.WithRendererOptions(renderer.WithNodeRenderers(rs...)) } type nodeRendererRegisterer struct { Kind ast.NodeKind RenderFunc renderer.NodeRendererFunc } func (r nodeRendererRegisterer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { reg.Register(r.Kind, r.RenderFunc) }
これを作っておけば、以下のように登録できるようになります。 こちらの方がシンプルで良い、ような気がします。お好みのやり方でどうぞ。
type MyRenderer struct{} func (r MyRenderer) Priority() int { return 500 } func (r MyRenderer) Kind() ast.NodeKind { return ast.KindHeading } func (r MyRenderer) Render(w BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { // ここは先ほどの例と同じ } func main() { // ... md := goldmark.New( WithNodeRenderers( MyRenderer{}, ), ) // ... }