本記事は React Advent Calendar 2016 9日目の記事です。
本稿では、remarkというプラグインベースのMarkdownプロセッサを用いて、ReactのdangerouslySetInnerHTML
を使わずにMarkdownをレンダリングする方法をご紹介します。
InkdropというMarkdown専用ノートアプリを作っていて、その中でこのremarkを使っています。
XSSを回避してReact方式でDOM操作したい
ReactでMarkdownをレンダリングしようと思うと、markedとかmarkdown-itを使ってHTMLの文字列に変換してからdangerouslySetInnerHTMLで無理やりねじ込む実装方法が多いと思います。
この方法ではXSSの危険性を孕みます。そしてMarkdown周りだけ文字列ベースの処理となり、Reactを使ったDOM操作の恩恵に預かれません。
Remarkを使うことでこの問題を解消できます。
プラグインベースのMarkdownプロセッサ「remark」
remarkは単なるMarkdownからHTMLへのコンパイラではなく、プラグインを使って様々なことが出来るMarkdownプロセッサです。例えば以下のような事ができます:
そしてremark-reactがReactでレンダリングするためのプラグインです。
使い方はとても簡単で、以下のように使用します:
import React from 'react'
import remark from 'remark'
import reactRenderer from 'remark-react'
const processor = remark().use(reactRenderer)
class App {
constructor () {
this.state = { text: '# hello world' }
}
onChange (e) {
this.setState({ text: e.target.value })
}
render () {
return (
{processor.process(this.state.text).contents}
</div>)
}
}
React.render(<App />, document.getElementById('app'))
詳しく見て行きましょう。
Markdownパーサのオプション
Markdownのパースを担当しているのはremark-parseです。これはremarkにデフォルトで同梱されているプラグインです。
オプションは以下の通りで、processor.process()に渡して使います:
gfm
(boolean
, default: true
)
- GitHub-flavored Markdownモードを有効にします
yaml
(boolean
, default: true
)
- YAML front matterを有効にします
commonmark
(boolean
, default: false
)
- CommonMarkモードを有効にします
footnotes
(boolean
, default: false
)
- リファレンス脚注とインライン脚注をサポートします
pedantic
(boolean
, default: false
)
_
による強調などを有効にします
breaks
(boolean
, default: false
)
- パラグラフ内の改行を新しい行として扱います
blocks
(Array.<string>
, default: list of block HTML elements)
- ブロックレベルのHTML要素を定義可能にします
以下のように指定します:
processor.process(this.state.text, { gfm: true, breaks: true, yaml: false })
詳しくはドキュメントを参照ください。
このパーサは新しいシンタックスに対応させる際に必要になります。
Parser#blockTokenizers
やParser#inlineTokenizers
からトークナイザを追加できます。
Reactレンダラのオプション
sanitize
(object
or boolean
, default: undefined
)
- サニタイズのルールを指定します。デフォルトではGitHubのルールを適用します
prefix
(string
, default: h-
)
- React keyのプリフィックスを指定します
createElement
(function
, default: require('react').createElement
)
- Reactエレメント生成関数を変更できます
remarkReactComponents
(object
, default: undefined
)
- 任意のHTML要素を独自のReactコンポーネントに置換できます(詳しくは後述)
toHast
(object
, default: {}
)
- MDASTからHASTへ変換する際のオプションです
以下のように指定します:
const processor = remark().use(reactRenderer, {
sanitize: true,
prefix: 'md-'
})
コードのシンタックスハイライトに対応する
デフォルトのremarkおよびremark-reactにはコードのシンタックスハイライトに対応していません。
これはhighlight.jsとremark-react-lowlightを組み合わせて実現できます。
import RemarkLowlight from 'remark-react-lowlight'
import js from 'highlight.js/lib/languages/javascript'
const processor = remark().use(reactRenderer, {
sanitize: true,
prefix: 'md-',
remarkReactComponents: {
code: RemarkLowlight({
js
})
}
})
highlight.jsのサポートしている言語を一度に全てサポートさせたい場合は、こちらのGistを参考にしてください。
テーブルの寄せ(アライン)に対応する
Reactはテーブル要素のalign
属性を無視します。これはHTML5では非推奨の属性のためです。
remark-reactはこの問題をまだ上手く回避してくれません。
そこで、以下のように独自にtd
とth
要素をレンダリングすることで回避します:
function createTableCellComponent (tagName) {
return class TableCell extends React.Component {
render () {
const style = { textAlign: this.props.align }
const props = { ...this.props, style }
return createElement(tagName, props, this.props.children)
}
}
}
const processor = remark().use(reactRenderer, {
remarkReactComponents: {
td: createTableCellComponent('td'),
th: createTableCellComponent('th')
}
})
かなり柔軟に出来ている
使ってみた感じでは少し複雑に感じるところもありますが、その分柔性がとても高いと思いました。
MDAST(Markdown Abstract Syntax Tree)とかHAST(Hypertext Abstract Syntax Tree)とかは特に分かりにくいのですが、そこを理解してしまえばシンプルな方法でシンタックスを拡張したりできます。
既存のプラグインを組み合わせるだけならとても簡単ですので、ぜひ使ってみてください!