Contents

Hugoをhackして任意のテキストプリプロセッサーを実装する

大規模なソフトウェアを使っていると、何かを実現するための調査に丸一日費やした挙げ句、実現できないことが分かる、なんてことがあります。 これはちょっと生産的でないので、少し調べて分からなかったらコードを直接書き換えてしまうのが好きです。 自分専用のちょー汚い修正 (dirty hack) をするのは簡単だし、運が良ければdirty hackをしているうちに自分が探していたコンフィグが見つかりhack不要で解決することもよくあります。

Hugoもそのような大規模ソフトウェアの1つで、なにか小さいことを実現するためにドキュメントやStackoverflowを読んで1時間くらい試行錯誤しなければなりません。試行錯誤は嫌なのでdirty hackすることにしました。

テキストプリプロセッサー

Hugoを使っていると、HugoにMarkdownを処理させる前に文字列置換を行いたいケースがたくさん出てきました。 1つだけ紹介します。

このブログでは、記事の一番下にRead Markdownというリンクをつけて原稿のMarkdownを公開しています。 私はMarkdownで公開記事を書くときは <!-- コメント --> を下書きとして使います。 このコメントは基本的に公開して良いものなんですが、たまに恥ずかしくて公開したくないものがあります。

1
2
3
4
5
<!-- 
やべえこのコマンドなんかやばいっぽいぞ
TODO: ちゃんと調べて直す
-->
`sudo rm -rf /` を実行してみましょう。

これを “Read Markdown” から除外する方法を調べたところ、 shortcodeを使えば行けるっぽいですが、学習・試行錯誤したくないし、Hugo独自の記法は極力避けたい (vscode等が解釈できないし、将来Hugo以外に移行するかもしれないので)です。

Hack

2020/08/22 (d39636a) 時点ではこれで行けました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
diff --git a/parser/pageparser/pageparser.go b/parser/pageparser/pageparser.go
index 3d17aa8e..09a6adc6 100644
--- a/parser/pageparser/pageparser.go
+++ b/parser/pageparser/pageparser.go
@@ -15,11 +15,17 @@ package pageparser
 
 import (
 	"bytes"
+	"fmt"
 	"io"
 	"io/ioutil"
+	"os"
+	"os/exec"
+	"reflect"
+	"regexp"
 
 	"github.com/gohugoio/hugo/parser/metadecoders"
 	"github.com/pkg/errors"
+	"github.com/spf13/afero"
 )
 
 // Result holds the parse result.
@@ -100,8 +106,67 @@ func ParseMain(r io.Reader, cfg Config) (Result, error) {
 	return parseSection(r, cfg, lexMainSection)
 }
 
+func preProcessContent(b []byte, fileName string) []byte {
+	re := regexp.MustCompile(`(?ms)^<!--\n.*?\n-->$`) // TODO: do in init()
+	tmp := re.Find(b)
+	if tmp == nil {
+		return b
+	}
+	fmt.Fprintf(os.Stderr, "%s: remove comment\n", fileName)
+	b2 := re.ReplaceAll(b, []byte(""))
+
+	// _ = os.MkdirAll(path.Dir("/tmp/hugo/" + fileName), 0700)
+	// fmt.Fprintf(os.Stderr, "%s: write to /tmp/hugo/%s.{1,2}\n", fileName, fileName)
+	// _ = ioutil.WriteFile(fmt.Sprintf("/tmp/hugo/%s.1", fileName), b, 0600)
+	// _ = ioutil.WriteFile(fmt.Sprintf("/tmp/hugo/%s.2", fileName), b2, 0600)
+
+	return b2
+}
+
+func preProcessContentExec(b []byte, fileName string) []byte {
+	cmd := exec.Command("sed", "s/foo/bar/g")
+	stdin, err := cmd.StdinPipe()
+	if err != nil {
+		panic(err)
+	}
+	defer stdin.Close()
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		panic(err)
+	}
+	cmd.Stderr = os.Stderr
+	if err := cmd.Start(); err != nil {
+		panic(err)
+	}
+	defer stdout.Close()
+
+	if _, err := stdin.Write(b); err != nil {
+		panic(err)
+	}
+	stdin.Close()
+	var b2 []byte
+	if b2, err = ioutil.ReadAll(stdout); err != nil {
+		panic(err)
+	}
+
+	if err := cmd.Wait(); err != nil {
+		panic(err)
+	}
+
+	return b2
+}
+
 func parseSection(r io.Reader, cfg Config, start stateFunc) (Result, error) {
 	b, err := ioutil.ReadAll(r)
+
+	fileName := func() string {
+		v := reflect.ValueOf(r).Elem()
+		file := v.FieldByName("File").Interface().(afero.File)
+		return file.Name()
+	}()
+	b = preProcessContent(b, fileName)
+	b = preProcessContentExec(b, fileName)
+
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to read page content")
 	}

dirty hackだからお父さんreflectも使っちゃうぞー。

pageparser.parseSection() でのバックトレースも貼っておきます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
github.com/gohugoio/hugo/parser/pageparser.parseSection at pageparser.go:148
github.com/gohugoio/hugo/parser/pageparser.Parse at pageparser.go:45
github.com/gohugoio/hugo/hugolib.(*pageMap).newPageFromContentNode at content_map_page.go:146
github.com/gohugoio/hugo/hugolib.(*pageMap).assemblePages.func1 at content_map_page.go:364
github.com/armon/go-radix.recursiveWalk at radix.go:519
github.com/armon/go-radix.recursiveWalk at radix.go:525
github.com/armon/go-radix.recursiveWalk at radix.go:525
github.com/armon/go-radix.recursiveWalk at radix.go:525
github.com/armon/go-radix.(*Tree).Walk at radix.go:447
github.com/gohugoio/hugo/hugolib.(*pageMap).assemblePages at content_map_page.go:337
github.com/gohugoio/hugo/hugolib.(*pageMaps).AssemblePages.func1 at content_map_page.go:718
github.com/gohugoio/hugo/hugolib.(*pageMaps).withMaps.func1 at content_map_page.go:786
github.com/gohugoio/hugo/common/para.(*errGroupRunner).Run.func1 at para.go:52
golang.org/x/sync/errgroup.(*Group).Go.func1 at errgroup.go:57
runtime.goexit at asm_amd64.s:1373

go install --tags extended でビルド・インストールしましょう。

こんな感じで任意のtext preprocessingをすることができました。わーい。 正攻法でもっと楽な方法があれば教えて下さい。