Reactで生HTMLを自由自在に加工する

こんにちは。クレイの浅海です。最近の休日は3歳児とマイクラをしています。
さて、業務ではここ1年ぐらい、[DocBase](https://docbase.io/?utm_source=kray.jp&utm_medium=blog&utm_campaign=20210518&utm_content=html_react_percer)のフロントエンドのフレームワークをBackbone.jsからReact.jsに変更する作業をしていました。
**完全に作り直しです。2万行を超えるBackboneのコードとお別れをしました。**バイバイバックボーン。
詳細 → DocBaseのフロントエンド改修をどのように進めたか
## Reactで実装する上で苦労した点
DocBaseにはReactとマッチしない機能もいくつかあり、Reactで実装する上で苦労しました。その一つが、メモの表示です。「メモ」というのは、DocBaseの投稿の単位です。[メモは例えばこのような投稿](https://help.docbase.io/posts/13697?utm_source=kray.jp&utm_medium=blog&utm_campaign=20210421&utm_content=html_react_percer)
メモの内容は、ユーザが投稿したmarkdownをサーバでHTMLに変換しデータベースに保存、それを表示時にサーバから受け取ったHTMLをReactが表示することになります。
ただ、この受け取ったHTMLを表示する場合に、Reactにおいては1つ問題があります。ReactはデータからHTMLを生成するのであって、生HTMLを扱うことには向いていないのです。
一応、Reactで`dangerouslySetInnerHTML`を使用してHTMLをそのまま扱う方法もあります。
なのでメモをReactで表示したい場合は次のようにするだけで実装が完了・・
```jsx
```
**・・しません!**
残念ながらDocBaseの仕様はそんなに単純ではなく、次のようにサーバから取得したHTMLを加工するいくつもの仕様が存在します。
* 外部へのaタグに対してnoreferrerを付与する
* twitterの埋め込みを有効化する
* mermaid.jsが記述されていたら有効化する
* MathJaxの構文が書かれていたら有効化する
* 差し込み用のdivタグを探して別のメモを入れ子で表示する
* imgタグで表示する画像のサイズを最適にする
などなど、他にも様々な加工が必要です。
これらを実現するために`dangerouslySetInnerHTML`でとりあえず表示して、それをReactの`Ref`から`ref.current.querySelector(query)`などしてDOMをごにょごにょと処理する・・、そんなのは辛いですね。折角、Reactで実装するというのに・・。
そんなとき、[html-react-parser](https://www.npmjs.com/package/html-react-parser)を使うと、良い感じにHTMLを加工できます。
https://www.npmjs.com/package/html-react-parser
## html-react-parserの使い方
### バージョン情報
本記事執筆時の検証で使用した各ライブラリのバージョンです。
* react 17.0.2
* html-react-parser 1.2.4
### 簡単な問題
まずは、html-react-parserの使い勝手を見るために、問題を非常に単純にしてみます。
やりたいことは次の1つだけとします。
* aタグに対してnoreferrerを付与する
この要求を実現するhtml-react-parserの使い方は次のようになります。
propsとして受け取ったhtmlをhtml-react-parserで変換処理を行い、その結果を表示するだけのReactコンポーネントを用意しました。
```jsx
import parse, { domToReact } from 'html-react-parser'
const replace = (node) => {
if (node.name === 'a') {
return (
{ domToReact(node.children) }
)
}
}
export const App = ({ html }) => {
return (
変換後のDOM
{ parse(html, { replace }) }
);
}
```
どうでしょうか、このコンポーネントを実行すると、`props.html`に渡したhtmlのaタグにreferrer属性がついて出力されていることがわかると思います。
こちらに動作サンプルを用意しました。
[動作サンプル: CodePen](https://codepen.io/ttakuru88-the-sans/pen/GRrXEmm?editors=0010)
html-react-parserには`parse`という関数が用意されています。このparseにはreplaceオプションとして関数を渡すことができ、パースされた結果のノード単位でreplace関数が実行されるので、どのようなタグにどのような加工を施すかを細かく実装できます。
replace関数がJSX.Elementを返せばnodeをそれで置き換えますし、何も返さなければnodeは置き換えません。
reaplce関数内で使っている`domToReact`を使っている理由ですが、node変数の中身はJSX.Elementではないので、nodeをJSX.Elementに変換するためです。
### 仕様を増やす
上記のように、html-react-parserの`parse`を使って生HTMLを自在に加工できることがわかりました。単純な問題では上記の方法で問題が無いのですが、状況によっては注意する点があります。それを仕様を増やして確認してみます。
ここに2つめの仕様を追加しました。
* aタグに対してnoreferrerを付与する
* **独自のtodayタグがあった場合は現在時刻を差し込む**
``というタグがあった場合、これを``に差し替えるという仕様を追加しました。このtodayタグに関する判定をreplace関数に追加します。
具体的には、次の用に実装します。
```js
const replace = (node) => {
if (node.name === 'a') {
return (
{ domToReact(node.children) }
)
}
// 以下追加
else if (node.name === 'today') {
return ()
}
}
```
todayというタグだった場合は、`