TL;DR
こちらが現時点(2020年2月)での最適解となります。
(@yewton さんのコメントより。ありがとうございます!)
$ gem install commonmarker
(setq markdown-command "commonmarker --extension=autolink,strikethrough,table,tagfilter,tasklist"
markdown-css-paths '("https://cdn.jsdelivr.net/npm/github-markdown-css"
"https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/styles/github.min.css")
markdown-xhtml-header-content "
<style><!-- CSS HERE --></style>
<script src=\"https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/highlight.min.js\"></script>
<script>hljs.initHighlightingOnLoad();</script>"
markdown-xhtml-body-preamble "<div class=\"markdown-body\">"
markdown-xhtml-body-epilogue "</div>")
<!-- CSS HERE -->
には自分の好きなCSSを書いてください。ちなみに、私の設定はこちら。
body {
padding: 1rem 3rem;
}
@media only screen {
body {
border: 1px solid #ddd;
margin: 1rem auto;
max-width: 45rem;
padding: 3rem;
}
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6,
.markdown-body h6,
.markdown-body strong {
font-weight: 700;
}
動機
Emacsでmarkdownを書くときは、markdown-modeを常用している。
Markdown Mode for Emacs - https://jblevins.org/projects/markdown-mode/
markdown-modeでは任意のmarkdownパーサが指定できる。インストールガイドにはMultiMarkdownが例として挙げられている。
(setq markdown-command "multimarkdown")
MultiMarkdown - http://fletcherpenney.net/multimarkdown/
MultiMarkdownでは「取り消し(strikethrough)」構文がデフォルトでサポートされていなかったので、いい機会にGitHubでの表示に近いパーサや設定を探してみることにした。
Can you add support strikethrough text? · Issue #130 · fletcher/MultiMarkdown-4
プッシュ前にGitHubでどのように表示されるかを確認することが多々あるので、できればGitHubが使っているパーサが望ましいという事情があった。
(プッシュ後に想定外の表示がされると、かなりヘコむ )
*ちなみに、markdown-modeではC-c C-c pでHTMLプレビューがブラウザで表示される。
調査
GitHub Markup
ググるとGitHub Markupなるものを見つけた。
GitHub Markup - https://github.com/github/markup
README抜粋。
This library is the first step of a journey that every markup file in a repository goes on before it is rendered on GitHub.com:
このライブラリは、GitHub.comでレンダリングされる前に、リポジトリ内のすべてのマークアップファイルが実行される最初のステップです。
「マークアップ」とあるのは、markdownだけではなくtextileやrdocなど、様々な種類のマークアップをGitHubがサポートしているからだろう。
gem install github-markup
とあるので、Rubyで書かれていると思い、ソースをのぞいてみた(Rubyは仕事で使っている)。
ソースファイルは10個くらいで少ない。各マークアップパーサのラッパーライブラリのようだ。
以下、抜粋。
MARKDOWN_GEMS = {
"commonmarker" => proc { |content|
CommonMarker.render_html(content, :GITHUB_PRE_LANG, [:tagfilter, :autolink, :table, :strikethrough])
},
"github/markdown" => proc { |content|
GitHub::Markdown.render(content)
},
"redcarpet" => proc { |content|
Redcarpet::Markdown.new(Redcarpet::Render::HTML).render(content)
},
"rdiscount" => proc { |content|
RDiscount.new(content).to_html
},
"maruku" => proc { |content|
Maruku.new(content).to_html
},
"kramdown" => proc { |content|
Kramdown::Document.new(content).to_html
},
"bluecloth" => proc { |content|
BlueCloth.new(content).to_html
},
}
# ...(省略)...
def load
return if @renderer
MARKDOWN_GEMS.each do |gem_name, renderer|
if try_require(gem_name)
@renderer = renderer
return
end
end
raise LoadError, "no suitable markdown gem found"
end
MARKDOWN_GEMS
定数には色々なmarkdownパーサが並べてある。
load
メソッドを見ると、MARKDOWN_GEMS
を上から順に読み込んでいき、require
に成功したらそのパーサを採用している。
一番上はcommonmarker
だ(github/markdown
ではない)。たしかにREADMEにもcommonmarker
が記載してあった。
- .markdown, .mdown, .mkdn, .md --
gem install commonmarker
(https://github.com/gjtorikian/commonmarker)
ということで、CommonMarkerを調べてみる。
CommonMarker
README抜粋。
Ruby wrapper for libcmark-gfm, GitHub's fork of the reference parser for CommonMark. It passes all of the C tests, and is therefore spec-complete. It also includes extensions to the CommonMark spec as documented in the GitHub Flavored Markdown spec, such as support for tables, strikethroughs, and autolinking.
libcmark-gfmのRubyラッパー、CommonMarkのリファレンスパーサのGitHub版フォーク。C言語の全テストをパスしているため、仕様を完全に満たしています。 また、テーブル、取り消し線、自動リンクのサポートといった、GitHub Flavored Markdown仕様ドキュメントにあるCommonMark仕様拡張も含まれています。
CommonMarkというのは、Markdownの標準仕様のようだ(初めて知った)。
日本語だと、以下の記事が詳しい。
設定
ということで、github-markup
とcommonmarker
をインストールする。
gem install github-markup commonmarker
github-markup
コマンドがインストールされたので、markdown-mode
に設定する。
(setq markdown-command "github-markup")
いざ、C-c C-c pでHTMLプレビューしてみると、ブラウザに次のメッセージが表示された。
github-markup
コマンドは標準入力を受け付けないようなので、次の設定を加える。
(setq markdown-command-needs-filename t)
すると、無事表示された。
シンタックスハイライト
これで終わりかと思いきや、コードスニッペトのシンタックスハイライトがうまく効いてないことに気づいた。
シンタックスハイライトにはhighlight.jsを使っているのだが、言語の選択がおかしいところがある。
(GitHubのハイライトがどうなってるか調べてみたが、分からなかった。少なくともhighlight.jsは使ってないようだった。)
highlight.js - https://highlightjs.org/
どうも、highlight.jsが付与するclass
属性に正しく反映されていないようだ。
<pre>
のlang
属性とは?もう一度GitHub Markupのソースコードを確認する。
CommonMarker.render_html(content, :GITHUB_PRE_LANG, [:tagfilter, :autolink, :table, :strikethrough])
GITHUB_PRE_LANG
の説明を、CommonMarkerのREADMEに見つけた。
:GITHUB_PRE_LANG
Use GitHub-style<pre lang>
for fenced code blocks.
<pre lang>
はCommonMarkerが出力していることがわかった。
<pre lang>
というHTMLマークアップにhighlight.jsが対応してないっぽいので、highlight.jsのドキュメントにあたった。
ドキュメントには<code class="html">
のように言語を明示的に指定する方法があったので、JavaScriptでDOMを書き換えるようにしてみた(highlight.jsにはシンタックスの自動検知機能がある)。
<pre lang="el"><code>...</code></pre>
↓ (JavaScriptでDOM書き換え)
<pre lang="el"><code class="el">...</code></pre>
markdown-modeの設定はこんな感じ。このmarkdown-xhtml-header-content
の値が<head>
に挿入される。
(setq markdown-xhtml-header-content "
<script src='http://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/highlight.min.js'></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.body.classList.add('markdown-body');
document.querySelectorAll('pre[lang] > code').forEach((code) => {
code.classList.add(code.parentElement.lang);
hljs.highlightBlock(code);
});
});
</script>
")
DOM書き換えのコードはこれ。lang
属性の値を<code>
のclass
属性に追加している。
document.querySelectorAll('pre[lang] > code').forEach((code) => {
code.classList.add(code.parentElement.lang);
hljs.highlightBlock(code);
});
プレビューを表示してみると、うまくいった。
ただ、残念なことにhighlight.jsはEmacs Lispのシンタックスに対応してなかった (Lispには対応している)。
GitHubやQiitaは対応しているので、彼らは独自のシンタックスハイライターを実装しているのかもしれない。
まとめ
最終的な設定はこうなった(CSSにはgithub-markdown-cssを利用)。
(use-package markdown-mode
:commands (markdown-mode gfm-mode)
:mode (("\\.md\\'" . gfm-mode)
("\\.markdown\\'" . gfm-mode))
:config
(add-hook 'markdown-mode-hook #'flyspell-mode)
(setq
markdown-command "github-markup"
markdown-command-needs-filename t
markdown-content-type "application/xhtml+xml"
markdown-css-paths '("https://cdn.jsdelivr.net/npm/github-markdown-css/github-markdown.min.css"
"http://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/styles/github.min.css")
markdown-xhtml-header-content "
<meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'>
<style>
body {
box-sizing: border-box;
max-width: 740px;
width: 100%;
margin: 40px auto;
padding: 0 10px;
}
</style>
<script src='http://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/highlight.min.js'></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.body.classList.add('markdown-body');
document.querySelectorAll('pre[lang] > code').forEach((code) => {
code.classList.add(code.parentElement.lang);
hljs.highlightBlock(code);
});
});
</script>
"))
最初は簡単にできるかなと考えていたが、色んな箇所でつまづいて、3時間くらいかかった(記事執筆時間は含まず)。
特に、最後のシンタックスハイライトでつまづいたのが痛かった 。
もっと時間をかけて調べればいい解決方法が見つかるかもしれないが、休日だったこともあり、心が折れた(GitHubが使っているハイライター、OSS化されてないかな…)。
今後もし解決方法が見つかれば、この記事を更新するつもり。
時間がかかった割には成果は少ないが、CommonMarkとか新たな知見を得られたので、良しとする 。