LoginSignup
4
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-24

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

ということでGo + Ginを使ってToCジェネレータを作成しました。

Screen Shot 2020-05-24 at 23.23.39.png

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

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

Gin

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

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

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

コード

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

URL変換

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


/**
 * 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()を使いデータを拾ってきます。

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

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

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


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

HTML部分の作成

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

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>

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

    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が動くという事でこちらで公開しようと思ったのですが動きませんでした。

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

この方法ではコード本体はGitHub上にモジュールとして公開し、それを動かすためのzeit.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

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

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

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

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

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

また zeit.go 化をしてしまうと realizeが動かなくなります。
二重メンテとなってしまいますが main.goapp.go を管理するか、自動デプロイ機能を組み入れる必要があります。

今後

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

デプロイ

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

デプロイ自動化

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

テスト

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

未実装項目

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

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

GitHub 上の Markdown が TOC(目次) を表示してくれないのでどうしようか → ツール自製したよって話 - Qiita

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0