25
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

markdown-modeで“GitHub Flavored Markdown”を実現する

Last updated at Posted at 2017-11-23

TL;DR

こちらが現時点(2020年2月)での最適解となります。
@yewton さんのコメントより。ありがとうございます!)

$ gem install commonmarker
init.el
(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が使っているパーサが望ましいという事情があった。
(プッシュ後に想定外の表示がされると、かなりヘコむ :tired_face: )

*ちなみに、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が記載してあった。

ということで、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-markupcommonmarkerをインストールする。

gem install github-markup commonmarker

github-markupコマンドがインストールされたので、markdown-modeに設定する。

(setq markdown-command "github-markup")

いざ、C-c C-c pでHTMLプレビューしてみると、ブラウザに次のメッセージが表示された。

スクリーンショット 2017-11-24 1.17.51.png

github-markupコマンドは標準入力を受け付けないようなので、次の設定を加える。

(setq markdown-command-needs-filename t)

すると、無事表示された。

スクリーンショット 2017-11-24 1.19.14.png

シンタックスハイライト

これで終わりかと思いきや、コードスニッペトのシンタックスハイライトがうまく効いてないことに気づいた。
シンタックスハイライトにはhighlight.jsを使っているのだが、言語の選択がおかしいところがある。
(GitHubのハイライトがどうなってるか調べてみたが、分からなかった。少なくともhighlight.jsは使ってないようだった。)

highlight.js - https://highlightjs.org/

スクリーンショット 2017-11-24 1.28.17.png

どうも、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);
});

プレビューを表示してみると、うまくいった。

スクリーンショット 2017-11-24 1.54.24.png

ただ、残念なことにhighlight.jsはEmacs Lispのシンタックスに対応してなかった :cry:(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時間くらいかかった(記事執筆時間は含まず)。
特に、最後のシンタックスハイライトでつまづいたのが痛かった :disappointed_relieved:
もっと時間をかけて調べればいい解決方法が見つかるかもしれないが、休日だったこともあり、心が折れた(GitHubが使っているハイライター、OSS化されてないかな…)。
今後もし解決方法が見つかれば、この記事を更新するつもり。

時間がかかった割には成果は少ないが、CommonMarkとか新たな知見を得られたので、良しとする :blush:

25
9
8

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
25
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?