Warning
現在本サイトはNuxtにリプレースされたので内容と表示の整合性が取れてない可能性があります
MarkdownとReact
JavaScriptのMarkdownパーサーは様々ある。メジャーどころで言えば
- marked
- commonmark
- markdown-it
があるだろう。これらをReactで使うばあい、一番単純な方法はdangerouslySetInnerHTML
を使うことである。ただしこれはあまりイケてない。というのは
- ライブ変換とかしようとすると遅い
react-router
を使ってSPAにしてるばあい、ドキュメント内のリンクをLink
に置き換える手段がない- 何よりReactである利点を全く活かせていない
以上の理由が挙げられる。そのため各Markdownパーサーごとに、HTMLへ変換するのではなく、ReactElementに変換するようなライブラリが作られている。
- rexxars/react-markdown ... commonmark
- alexkuz/markdown-react-js ... markdown-it
- mizchi/md2react ... mdast(MarkdownからASTを生成するやつ)
まともに使えそうなレベルのはこの3つくらい。始めは作者のやる気があってメンテもそこそこされているrexxars/react-markdown
を使っていて、ルールのオーバーライドも見通しが良く使い勝手は良かったが、commonmark
準拠でtableが吐けないのが辛かった。mizchi/md2react
は同様にtableがサポートされていないのと、これから対応するような気配もなかったので試してもいない。Markdownパーサーの中で一番拡張性が思われるのがmarkdown-itで、これを使っていたReactElementを吐けるライブラリを探して、見つけたのがalexkuz/markdown-react-js
だった。
alexkuz/markdown-react-jsが全然メンテされてない問題
markdown-it
のparse()
で得られるASTを使っていていたり、結構雰囲気いい感じなのだが、まず最新のReactとpearDependenciesのバージョンが合ってないとか、hr
タグとbr
タグがself-closingタグとして処理されてないので変換時にエラー吹いて死ぬとか、マージされれば割とマシになりそうな大きめなPRも来てるのだがそれも放置状態とかで、なんとも惜しい感じ。
こうなりゃもう自分で対応するしかないと、endaaman/markdown-react-jsに怒りのフォーク。
イジったところとか
- 依存性の整理
hr
とbr
で子をレンダリングしないonIterate
が与えられた時子タグレンダリングするかのチェックをすり抜けるのでその修正
という調整をしてなんとか使えるレベルに戻した。
そして骨までしゃぶる
markdown-it
は独自記法を追加するプラグインが提供されているので、公式のプラグイン
- markdown-it-ins
- markdown-it-mark
- markdown-it-deflist
- markdown-it-sup
- markdown-it-sub
- markdown-it-container
とシンタックスハイライト
に対応させ、さらにリンクをreact-routerのLink
に置き換えるようにした。
デモ
Linkとの置き換え
- [Home](/)
- [Github](https://github.com)
上はpushState
によるページ遷移、下はtarget="_blank"
で別タブで開く。
markdown-it-ins
++inserted++
++inserted++
markdown-it-mark
==marked==
==marked==
markdown-it-deflist
Term 1
: Definition 1
Term 2 with *inline markup*
: Definition 2
Second paragraph of definition 2.
Term 1
: Definition 1
Term 2 with inline markup
: Definition 2
Second paragraph of definition 2.
markdown-it-sup
29^th^
29^th^
markdown-it-sub
H~2~O
H~2~O
markdown-it-container
:::well
like well of twbs/bootstrap
:::
:::color:red
red colored text
:::
:::well like well of twbs/bootstrap :::
:::color:red red colored text :::
実装など
<MDReactComponent text={mdContent} {...mdOptions}/>
このようにレンダーするとして、mdOptions
には
import MDReactComponent from 'markdown-react-js'
import markdownItContainer from 'markdown-it-container'
import markdownItIns from 'markdown-it-ins'
import markdownItMark from 'markdown-it-mark'
import markdownItDeflist from 'markdown-it-deflist'
import markdownItSup from 'markdown-it-sup'
import markdownItSub from 'markdown-it-sub'
export function isInnerLink(uri) {
return /^\/.*/.test(uri)
}
const customContainers = {
well(props, children, arg) {
return <div className={styles.well} {...props}>{children}</div>
},
color(props, children, arg) {
return <div style={{color: arg}} {...props}>{children}</div>
},
}
const mdOptions = {
onIterate(tag, props, children) {
if (tag === 'a') {
if (isInnerLink(props.href)) {
props = {...props, ...{to: props.href}}
return (<Link {...props}>{children}</Link>)
} else {
props = {...props, ...{target: '_blank'}}
return React.createElement(tag, props, children)
}
}
if (tag === 'pre') {
const lang = children[0].props['data-language'] || null
return <CodeBlock language={lang} codeElement={children[0]} key={props.key} />;
}
const dataInfo = props['data-info']
if (dataInfo) {
const [marker, arg] = dataInfo.split(/:(.+)?/)
return customContainers[marker](props, children, arg)
}
return null
},
plugins: [
{
plugin: markdownItContainer,
args: [null, {
validate(param) {
const [marker] = param.split(':')
return Object.keys(customContainers).find(key => marker.trim() === key)
},
}]
},
markdownItIns,
markdownItMark,
markdownItDeflist,
markdownItSub,
markdownItSup
],
}
という感じ
Linkとの置き換え
onIterate
のtag
が'a'
かつ/hoge
のようにサイト内部のリンクならLink
に置き換え、そうでなければtarget="_blank"
を追加するだけ
シンタックスハイライト
```js
console.log('hello')
```
みたいなMarkdownを書いた時の言語アノテーションのjs
というような文字列はpre
の子のcode
の
data-language
要素に入っているので、そこから取り出している。CodeBlock
の実装は
import React, { Component } from 'react'
import PureRenderMixin from 'react-addons-pure-render-mixin'
import cx from 'classnames'
import hljs from 'highlight.js'
import styles from '../styles/code_block.css'
class CodeBlock extends Component{
constructor(props) {
super(props)
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this)
this.state = {
codeBlock: null,
}
}
componentDidMount () {
this.highlightCode()
}
componentDidUpdate () {
this.highlightCode()
}
highlightCode () {
if (this.props.language && this.state.codeBlock) {
hljs.highlightBlock(this.state.codeBlock)
}
}
render () {
const codeElement = {...this.props.codeElement}
codeElement.props = {...codeElement.props, ...{
className: 'hljs'
}}
return (
<pre
className={cx(styles.codeBlock, this.props.language)}
ref={(ref)=> this.setState({codeBlock: ref})}
>
{codeElement}
</pre>
)
}
}
という感じ。コンポーネントがマウントされたあとにDOMをhighlight.js
に引き渡してハイライトさせればdangerouslySetInnerHTML
を使わずに済む。highlight.js
のhighlightBlock
は勝手に言語を識別してハイライトしようとするので、言語が指定されなければハイライトをスキップしている。
markdown-it-container
今回の目玉。これのおかげで任意のコンテナをReactのコンポーネントとしてレンダリングできるので、オレオレMarkdown化が一気に加速する。
{
plugin: markdownItContainer,
args: [null, {
validate(param) {
const [marker] = param.split(':')
return Object.keys(customContainers).find(key => marker.trim() === key)
},
}]
}
という部分だが、たとえば
:::well
This is well
:::
というように書くと、:::well
の言語アノテーションの様な部分がvalidate(param)
のparam
に
'well'
というような文字列が渡ってくる。ここではmarkdown-it-container
を使った
レンダリングの可否をチェックしているので、truthyな値を返せば、
onIterate(tag, props, children) {
/* SNIP */
const dataInfo = props['data-info']
if (dataInfo) {
const [marker, arg] = dataInfo.split(/:(.+)?/)
return customContainers[marker](props, children, arg)
}
onIterate
でdataInfo
属性にアノテーション部分が入ってくるので、ここで:
でアノテーション部分を区切って前半をコンテナ名、後半を引数に見たてて、
const customContainers = {
well: (props, children, arg)=> {
return <div className={styles.well} {...props}>{children}</div>
},
color: (props, children, arg)=> {
const style = {}
if (arg) {
style.color = arg
}
return <div style={style} {...props}>{children}</div>
},
}
というようにすれば:::well
や:::color:red
とか:::color:blue
とか使えるようになる。これを上手く活用すればメディアの埋め込みとか、動的な要素など様々なものがMarkdown上から制御できるようになる。
課題など
- markdown-it-container
- ネストできない
- インライン展開できないとか
- markdown-it-footnoteに対応できなかった