Markdown で書いた記事をコマンドラインで HTML に変換したい。ついでにコードをハイライトしたい





ブログで記事を書くのにマークダウンで書きたいけど Blogger では自動でマークダウンを HTML に変換するといった機能はありません。 そこでコマンドラインツール pandoc を使ってマークダウンから HTML に変換していました。 それはそれで良いんですが、このブログでは記事の中にあるコードをハイライトするために highlight.js を使っていて、ページを表示する時に毎回コードブロックを変換しています。

何が言いたいかと言うと、マークダウンから HTML に変換したいし、コード部分をハイライトするための変換もコマンドラインで全て完結したいわけです。

そこで Go でコマンドラインツールを書いたので以下はそのコードです(サクッと書いたのでコードは汚いです)





コード

内容としては pandoc でマークダウンから HTML に変換して、 Selenium を使ってブラウザ上で highlight.js でコード部分をハイライトするための HTML に変換してます。 変換後の HTML は標準出力に出力されます。

chromedriver は ここ からダウンロードできます。

package main

import (
    "embed"
    "fmt"
    "os"
    "os/exec"

    "github.com/tebeka/selenium"
    "github.com/tebeka/selenium/chrome"
)

//go:embed static
var static embed.FS

// chromedriver のパス
const WebDriver = "/usr/local/bin/chromedriver"

func Run() error {
    if len(os.Args) == 1 {
        return fmt.Errorf("markdown file not specified")
    }

    // pandoc を使ってマークダウンから HTML に変換
    cmd := exec.Command("pandoc", "-f", "markdown", os.Args[1])
    contents, err := cmd.CombinedOutput()
    if err != nil {
        return fmt.Errorf("%s", string(contents))
    }

    service, err := selenium.NewChromeDriverService(WebDriver, 4444)
    if err != nil {
        return err
    }
    defer service.Stop()

    caps := selenium.Capabilities{}
    caps.AddChrome(chrome.Capabilities{Args: []string{
        "--no-sandbox",
        "--disable-dev-shm-usage",
        "--disable-gpu",
        "--headless", // ブラウザを非表示で起動する
    }})

    driver, err := selenium.NewRemote(caps, "")
    if err != nil {
        return err
    }

    // ベースの HTML を表示する
    const data = `data:text/html,<html><head></head><body><div id="contents"></div></body></html>`
    if err := driver.Get(data); err != nil {
        return err
    }

    // マークダウンから変換した HTML を contents 以下に追加
    _, err = driver.ExecuteScript(
        `document.getElementById('contents').innerHTML = arguments[0];`,
        []interface{}{string(contents)})
    if err != nil {
        return err
    }

    // highlight.min.js を埋め込んで hljs.highlightAll(); で実行する
    highlightJs, err := static.ReadFile("static/js/highlight.min.js")
    if err != nil {
        return err
    }
    _, err = driver.ExecuteScript(`
try {
    eval(arguments[0]);
    hljs.highlightAll();
} catch(error) {
    throw new Error('highlight.js: ' + error.message);
}`, []interface{}{string(highlightJs)})
    if err != nil {
        return err
    }

    // contents 以下の HTML 要素を取得する
    value, err := driver.ExecuteScript(`return document.getElementById('contents').innerHTML`, nil)
    if err != nil {
        return err
    }

    // 標準出力に変換後の HTML を出力
    fmt.Fprintf(os.Stdout, "%v", value)
    return nil
}

func _main() int {
    if err := Run(); err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        return 1
    }
    return 0
}

func main() {
    os.Exit(_main())
}

はじめは marked.js というパッケージ/コマンドを使って TypeScript で書こうと思ったけど、 目次の部分が変換されない 仕様らしく各自でカスタムレンダラーを追加しないといけなかったり、 highlight.js は、import/export に対応しているのかよくわからなかったりで、 pandoc を使って変換して、さらにブラウザ上でハイライト用の HTML に変換するという あまり合理的とは思えないことになってます。



ディレクトリ構成

highlight.min.js は go:embed でバイナリに埋め込む

.
├── go.mod
├── go.sum
├── main.go
└── static
   └── js
      └── highlight.min.js



ビルド

バイナリは好きな名前にしてパスが通った場所に配置する

$ go build -ldflags="-w -s" -o markdown-to-html



使い方

変換したいマークダウンを引数に指定して index.html に書き出す。書き出した HTML をクリップボードにコピーする。

$ markdown-to-html index.md > index.html && cat index.html | pbcopy


変換前 (index.md)

index.md

- [Description](#description)
- [JavaScript code](#javascript-code)

# Description

I wanna convert Markdown to HTML

# JavaScript code

```javascript
const hello = () => {
  return "Hello World";
};

const message = hello();
console.log(message);
```


変換後 (index.html)

index.html

<ul>
<li><a href="#description">Description</a></li>
<li><a href="#javascript-code">JavaScript code</a></li>
</ul>
<h1 id="description">Description</h1>
<p>I wanna convert Markdown to HTML</p>
<h1 id="javascript-code">JavaScript code</h1>
<div class="sourceCode" id="cb1"><pre class="sourceCode javascript"><code class="sourceCode javascript hljs language-javascript" data-highlighted="yes"><span class="hljs-keyword">const</span> <span class="hljs-title function_">hello</span> = (<span class="hljs-params"></span>) =&gt; {
  <span class="hljs-keyword">return</span> <span class="hljs-string">"Hello World"</span>;
};

<span class="hljs-keyword">const</span> message = <span class="hljs-title function_">hello</span>();
<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(message);</code></pre></div>



コメント