はじめに
SIGPX: Special Interest Group on Programming Experience 第二回 (2016年8月7日) での発表資料
今日話す内容
- Qiitaでのコンテンツオーサリング
- Qiita の Markdown について、泥臭い感じで(アカデミックな会なので)
- Markdownという切り口で、標準化、そのレンダリング、オーサリング、ASTなどについて
Markdown の仕様
Markdown
- HTMLに変換されるマークアップ言語の実装。またはその仕様。
- Github の躍進とともにメジャーに
- 同種のマークアップ言語として textile, はてな記法など
Markdownの起源
- オリジナル実装は John Gruber の
markdown.pl
というPerl スクリプト(2004) - 電子メールにおいてプレーンテキストを装飾する際の慣習から着想を得ている
-
It’s a plain text format for writing structured documents, based on formatting conventions from email and usenet.
http://commonmark.org/
-
なぜ Markdown がエンジニアに好まれるか
-
プレーンテキストである
- Git等のバージョン管理システムとシナジー
- 特殊なWYSIWYG を要求された時代への反動
-
セマンティクスと装飾が分離されている
- テキストエディタでそれなりに読める
- HTMLの出力だけが決まっていて、装飾に関与しない
複数の Markdown 実装
- 各言語による実装
- Perl:
Text::Markdown
Text::Markdown - search.cpan.org - Ruby:
RedCarpet
(厳密にはC実装のバインディング) vmg/redcarpet: The safe Markdown parser, reloaded. - etc...
- Perl:
- Markdown Extra: テーブル記法などをサポート(PHP)
- Github の Github Flavored Markdown 通称 GFM (ソース未公開)
- CommonMark(commonmark.org) による標準化とそのサンプル実装 (2012~)
Github Flavored Markdown
- もっとも知られている実装
- 仕様的には Markdown Extra をさらに拡張
- GFM != Markdown、とはいえ事実上の標準
- 拡張
- コードブロック記法とシンタックスハイライト
- テーブル記法
- 打ち消し線
- 絵文字
- URL の自動リンク
- 非互換
- 「
_foo_
のようなアンダースコアによる強調」のオミット - 「段落の末尾にスペースがあると改行」のオミット
- 「
Markdownの抱える問題
- 仕様策定が決まる前に各言語で実装が乱立したので、実装にブレが多い
- 異なる実装で同じMarkdownをコンパイルした場合、同じものが出力されることは稀
- GFMのように勝手に一部の仕様を削除していたり…
- 構文が入れ子になった状態が未定義だったり…
これはどうなる?
- a
- b
- c
- d
-e
- 改行後の文頭スペースの数の比較で、ネストの深さを決定する実装(markdown.pl)
- 2n で端数を切り捨てる仕様(CommonMark)
- 4n で端数を切り捨てる実装(RedCarpet)
- ハイフンの次のスペースを省略できるか
CommonMark(2012~)
- Stack Exchange(StackOverflow)、GitHub、Reddit などの代表者によって策定された Markdown の標準化グループ
- CommonMark Spec
- 実行可能なテストコード jgm/CommonMark: CommonMark spec, with reference implementations in C and JavaScript
CommonMarkの問題
- 元々「Standard Markdown」だったが、John Gruber氏の反対でCommonMarkに改名
- それ以前の各種の先行実装が、CommonMarkに追従する動きがあまりない
- 乱立する仕様の One of them では?という批判も
- 結局事実上の標準はGFMにあり、その拡張部分は未定義
- ユーザーが使いたいのはgfm であり、CommonMarkに準拠したからといって、ユーザーのニーズを満たせているわけではない
現実
CommonMark conformance · Issue #429 · vmg/redcarpet
git clone https://github.com/jgm/CommonMark.git
cd CommonMark
# install re2c like: brew install re2c
make
make test # Testing of reference implementation: 469 tests passed, 0 failed, 0 skipped.
gem install redcarpet
PROG=redcarpet make test # 213 tests passed, 256 failed, 0 skipped.
213 tests passed, 256 failed !!!
Qiitaにおける Markdown の課題
QiitaのMarkdown実装
- 数式サポート
- ToCの生成
- コードブロックでファイル名のサポート
js:foo.js
実装的には RedCarpet の Fork increments/greenmat: A Markdown parser for Qiita, based on Redcarpet.
QiitaのMarkdownが抱える問題
- CommonMark以前に追加された、非標準な独自拡張が存在
- ベースとした RedCarpet が CommonMark準拠度が低い
- RedCarpetはRubyとCの実装なので、JSによるクライアントプレビューが出来ず、サーバーに問い合わせる必要があり、プレビューの応答性が悪い
- おそらく プレビューと出力が違うような状態は許容されない
うまいことしたい…
ここで解決したい問題
- 同じMarkdownのソースから出力されるHTMLが同じ結果であることを保証したい
解決策を考える
考えられるアプローチ: 1
何もしない。細かなズレはあるが、それらは所詮はエッジケース。
体感として問題がないなら、一貫していることの方が大事である。
"Sure it's broken, but at least it's consistently broken!"
考えられるアプローチ: 2
サーバーサイドでJS実装のコンパイラを呼ぶ
単一実装でメンテナンス性は高い。RailsでからJSエンジン(V8)を呼ぶのがネック
考えられるアプローチ: 3
クライアントのみでコンパイルをする
SEOが懸念。
考えられるアプローチ: 4
Emscripten で RedCarpetのCのコードをコンパイルする。
あるいは WebAssembly を待つ。
kripken/emscripten: Emscripten: An LLVM-to-JavaScript Compiler
WebAssembly/design: WebAssembly Design Documents
どの選択肢を取るか
JSで実装が一番理想的だが、Rubyエンジニアが多い弊社の場合、まだしばらく現行のまま行きそう。
いずれもパフォーマンスやSEOのトレードオフがある。
Excuse
Qiitaの1開発者としてこの領域をリサーチした結果得た問題意識であり、開発チームとしての総意ではないが、いずれ後方互換性を捨ててコンパイラを変更する可能性がないわけではない、ぐらいの感じでお願いします
Markdownの仕様まとめ
- 標準化以前に実装が乱立
- CommonMarkで標準化の試みだが追従されてるかは微妙
- デファクトはGFMだがCommonMarkにGithubの拡張部分は含まれないよ!!!
Markdown に関わるなら
- 出来るだけ CommonMark に近い実装を選んだ上で、そのGFM拡張を入れるのオススメ。
- パーサの互換がなくなるような拡張はできるだけ控えよう
次のテーマ
Markdownのレンダリング
課題意識
- Markdownが長くなると、展開するHTMLが大きくなりレンダリングが重い
- HTMLのパースとDOMの初期化が重い
- リアルタイムプレビューでは、ユーザーの打鍵ごとに更新が発生
フロントエンドの用語整理
- HTML: タグで記述するマークアップ言語
- DOM: Document Object Model。HTMLによって生成されるブラウザの木構造のインスタンス。
Kobito for Windows の場合
- Qiitaに投稿する機能を持つ、汎用のMarkdownエディタ/プレビューツール
- Windows版はElectronで実装
- デスクトップアプリで、ネットにつながらない状況を想定する必要
- => Qiitaとは独立したJSによる実装
Kobito for Windows で実装したコンパイラ
仮想DOM技術とMarkdown ASTを用いて、差分検出でプレビュー速度を高速化した
※ Qiita はプレビュー速度より同じ出力結果を優先という判断で、実装されてない
VirtualDOM
- HTMLを木構造と捉えて、生成コストが重いDOM(Document Object Model)とは別に、仮想の木構造の前後状態の比較から、実際のHTMLに対する差分のdiff/patchを行う手法
- もっとも有名な実装に React
- アルゴリズムの詳細: Performance Calendar » React’s diff algorithm
Markdown AST
- MarkdownをHTMLにコンパイルする過程で、一度AST(抽象構文木)を経過することで、任意のフォーマットに変形できる
-
wooorm/remark: Markdown processor powered by plugins の 実装を使った
- さすがにこのASTは標準化されなさそう?
Markdown => VirtualDOM のメリット
- リアルタイムプレビューでは、全体における一度の差分は、ごく一部だと仮定できる
- Markdown => Markdown AST => VirtualDOM という変換ステップを踏めば、ブラウザのDOM削除/生成ステップのほとんどを省ける
- mizchi/md2react: markdown to react element
- https://github.com/mapbox/remark-react
- Kobito for Windows で本番投入済み
Markdown => AST => React => HTML => DOM
var md2react = require('md2react');
var md = '# Hello md2react';
var html = ReactDOM.renderToString(md2react(md));
/*
<div data-reactid=".14qrwokr3sw" data-react-checksum="20987480"><h1 data-reactid=".14qrwokr3sw.$_start_root_0_heading"><span data-reactid=".14qrwokr3sw.$_start_root_0_heading.0">Hello md2react</span></h1></div>'
//'<div data-reactid=".58nba97pxc" data-react-checksum="-55236619"><h1 data-reactid=".58nba97pxc.0"><span data-reactid=".58nba97pxc.0.0">Hello</span></h1></div>'
*/
Markdown AST という概念の発展性
- より高度なナビゲーションが可能に
- 高速なToCの生成
- タイトルジャンプ
- コンパイル前後のカーソルの位置対応
- HTML以外の出力フォーマットに対応
- 仮想DOM
- スライド機能(これ)
# Aaa
---
# Bbb
Qiita の Markdown 環境をどうするか
- CommonMarkとぶつかるような独自実装は、あまり増やしたくない
- どのプラットフォームでも同じように変換される、という安心感は大事にしたい。その為に実装を寄せたい。
より具体的な(自分の)実務
- スタンドアローンとして優秀なMarkdownエディタが多い中、お世辞にもQiitaのWebのエディタはそこまで使いやすいとは言えない。リファクタ中(苦戦中)
- コンパイラの問題を解決して、クライアントで高速なプレビューや、リッチコンテンツの作成支援を行えるようにしたい
最後に
最後に
- ただの Markdown という 1フォーマットでもこだわると無限にエッジケースが出てくる
- 正しく仕様が決まると、エコシステムが回り、ツール同士の連携ができるようになり、結果としてユーザーが恩恵を受ける
- Webの仕様は基本的に民主主義で、議論の過程が公開されているので、現状ダメなもののダメになってしまった理由も、優しく察していきましょう