GitHub Wikiの目次（ToC）をURLから作成してくれるツールをGo+Ginで作る

GitHubのWikiを充実させたい => 目次手書きで書くのが面倒！

ということでGo + [Gin](https://github.com/gin-gonic/gin)を使ってToCジェネレータを作成しました。

![Screen Shot 2020-05-24 at 23.23.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47327/d9111f6c-d22f-2d71-00b4-b60fbe249c6c.png)

GitHub WikiのURLを入力するとToC（Table of Contents（目次））を作成してくれます。

デプロイは [Zeit Vercel（旧 Zeit Now）](https://vercel.com/)で行おうとしたのですがうまくいきませんでした。（このサービスめっちゃ好きなので残念）

https://github.com/yousan/toc-generator/

## Gin
Goのウェブ用フレームワークです。ルーティングやテンプレートなどの機能が揃っています。

go getでインストールします。

```console
$ go get github.com/gin-gonic/gin
```

## コード

いくつかの機能に分けて実装を進めます。

### URL変換
GitHub WikiのURLを生のマークダウンで落とせる形式に変換します。
URLの一部を入れ替え、末尾に `.md` を付与します。

```go

/**
 * GitHub WikiのURLをパースして生のMarkdownが取得できるURLにする
 */
func ParseUrl(ctx *gin.Context) string {
	urlstr, nil := getPostUrl(ctx)
	u, err := url.Parse(urlstr)
	if err != nil {
		panic(err)
	}
	// この形式に変換する https://raw.github.com/wiki/user/repo/page.md?login=login&token=token
	path := ConvertWikiUrl(u.Path)
	rawUrl := "https://raw.github.com/wiki" + path + ".md"
	fmt.Println(rawUrl)
	return rawUrl
}
```

### マークダウンデータ取得

`http.Get()`を使いデータを拾ってきます。

```go
/*
 * URLデータを読み込む
 */
func getContent(url string) string {
	resp, _ := http.Get(url)
	defer resp.Body.Close()
	byteArray, _ := ioutil.ReadAll(resp.Body)
	return string(byteArray)
}
```

### マークダウンのUL形式に変換

取得したマークダウンデータを行ごとに分割し、正規表現でヘディング(#で始まる行)を拾い出します。


```go

func ParseMarkdownToUl(content string) []string {
	var ret []string

	var s = strings.Split(content, "\n")
	for i := 0; i < len(s); i ++ {
		r := regexp.MustCompile(`^(#+)(.*)$`)
		if r.Match([]byte(s[i])) { // マッチした場合のみ反応させる
			headingMark := r.ReplaceAllString(s[i], "$1")
			headingStr := r.ReplaceAllString(s[i], "$2")
			if len(headingMark) > 0 {
				fmt.Printf("%s %d\n", headingMark, len(headingMark))
				ret = append(ret, ToUL(len(headingMark), headingStr))
			}
		}
	}
	return ret
}

func ToUL(num int, heading string) string {
	var ret string
	for i := 0; i < num - 1; i++ {
		ret = ret + "  "
	}
	ret = ret + "* " + heading
	return ret
}
```

このとき `regexp.MustCompile()` で抜き出すのですが、 `/^(#+)/` にマッチしなかった場合でも `/(.*)/` に反応してしまい、後ろのif文で判定を入れています。

Regex Checkerで確認しても反応しないはず…と思っていたのですが、どうやらGolangはPCREなどとは別の正規表現エンジンのようでした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47327/7b2979c9-8098-d883-5065-40ced15170f2.png)

https://regex101.com/


### HTML部分の作成

Ginのテンプレートエンジンに沿ってHTMLを作成します。
`{{.varname}}` で変数を代入してくれます。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <title>Sample App</title>
    <style>
        div {
            padding: 5px
        }
        .form-group {
            margin-bottom: 1px;
        }
        button {
            padding: 5px;
        }
        input {
            padding: 5px
        }
        hr {
            margin: 0;
        }
    </style>
</head>
<body style="padding: 30px">

<h1>ToC Generator</h1>
<div style="padding: 20px">
<form>
    <div class="form-group">
        <label for="exampleInputEmail1">GitHub Wiki URL</label>
        <small id="emailHelp" class="form-text text-muted" style="display: inline">
            e.g. https://github.com/yousan/toc-generator/wiki/testpage
        </small>
        <input type="text" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" placeholder="Enter wiki URL"
               name="url"
                {{if .url}}
               value="{{.url}}"
                {{else}}
               value="https://github.com/yousan/toc-generator/wiki/testpage"
                {{end}}
        >
        <button type="submit" class="btn btn-primary" style="margin-top: 5px;">Submit</button>
    </div>
    <hr>
    <div>
        <span>Markdown raw URL</span>
        <input type="text" class="form-control" readonly
               id="rawurl" aria-describedby="emailHelp" placeholder="Enter wiki URL"
        value="{{.rawurl}}"
        >
    </div>
    <div>
        <h5>Raw Markdown body</h5>
        <textarea style="width:100%; height: 200px">{{.rawbody}}</textarea>
    </div>
    <div>
        <h5>ToC Markdown</h5>
        <textarea style="width:100%; height: 200px">{{.toc}}</textarea>
    </div>
</form>
</div>
</body>
</html>
```

表示用に変数類を整えます。

```go
	router.GET("/", func(ctx *gin.Context) {
		url, _ := getPostUrl(ctx)
		vars := make(map[string]string)
		vars["url"] = url
		if len(url) > 0 {
			rawurl := ParseUrl(ctx)
			vars["rawurl"] = rawurl
			content := getContent(rawurl)
			vars["rawbody"] = content
			uls := ParseMarkdownToUl(content)
			toc := "# ToC\n"
			for i := 0; i<len(uls); i++ {
				toc = toc + uls[i] + "\n"
			}
			vars["toc"] = toc
		}
		ctx.HTML(200,"index.html",
			vars)
	})
```

## 終わり
上記でGoを動かせるようになりました。

## デプロイ
[Zeit社のVercel(旧 Now）でGoが動く](https://vercel.com/docs/runtimes#official-runtimes/go)という事でこちらで公開しようと思ったのですが動きませんでした。

まずGinのようにHTTPの待ち受けを行うプログラムの場合、`handler`パッケージ化が必要です。

https://vercel.com/docs/runtimes#official-runtimes/go

https://vercel.com/blog/introducing-go-modules-on-vercel-go

https://github.com/mini-eggs/go-now-example

この方法ではコード本体はGitHub上にモジュールとして公開し、それを動かすための`zeit.go`を作成します。


```go:zeit.go
package handler

import (
	"net/http"

	app "github.com/yousan/toc-generator/app"
)

func H(w http.ResponseWriter, r *http.Request) {
	app.Default().ServeHTTP(w, r)
}
```

ここまでは動くようになったのですが、`app.go`からテンプレートファイルの読み込みに失敗してしまいました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47327/7ebcd8e0-47e3-6c7d-4949-15726bb49bcb.png)


ディレクトリを個別に指定しようと `ioutil.ReadDir()` を使ったところ失敗してしまったことや、以前 Node.js でも似たようにファイルが読み込めないという問題があり、セキュリティ的に読み込みに制限をしているのではないか…、と思われます。

Node.jsについては同じように困っている人が多く解決方法が公開されており、`__dirname` を使うと解決します。

```js
const file = readFileSync(join(__dirname, 'config', 'ci.yml'), 'utf8');
```


https://docs-git-add-config-reference.zeit.now.sh/docs/builders/builders-mdx/advanced/advanced

残念ながらひとまず Zeit Vercel では諦めることとなりました。
せっかくなのでGKEあたりで試してみることができればと思っていますが、意外とお金が掛かってしまうことがネックですね…。
実はさくらのレンタルでGoが動く(!)[^1]のでそちらを使うのも良いかも知れません。

この件に付き合ってくれたくれた @naname さんありがとうございます！

[^1]: https://help.sakura.ad.jp/notification/360000190121/

また `zeit.go` 化をしてしまうと [realize](https://github.com/oxequa/realize/blob/master/realize.go)が動かなくなります。
二重メンテとなってしまいますが `main.go` と `app.go` を管理するか、[自動デプロイ機能](https://github.com/mini-eggs/go-now-example/tree/master/scripts)を組み入れる必要があります。

## 今後

今後時間があれば下記のように改善していければと思っています。

### デプロイ
上記のVercelでのデプロイが失敗しているため、なんとか突破できれば…。

### デプロイ自動化

GitHub Actions + GKE などができれば。

### テスト
今回は実装優先で書いてしまったため、テストコードを書いていません。残念。

### 未実装項目
ToCジェネレータといいつつheadingがリンク化されていないため、その点を実装する必要があります。

ただし日本語のアンカー名の決定が難しそうなため、マークダウン => マークアップ化された内容を見る…といった方法が必要そうです。

[GitHub 上の Markdown が TOC(目次) を表示してくれないのでどうしようか → ツール自製したよって話 - Qiita](https://qiita.com/sta/items/9481c94e0fc36f27fa92#%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%82%A2%E3%83%B3%E3%82%AB%E3%83%BC%E5%90%8D%E3%81%AE%E3%82%A8%E3%83%B3%E3%82%B3%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0 "GitHub 上の Markdown が TOC(目次) を表示してくれないのでどうしようか → ツール自製したよって話 - Qiita")