きみはねこみたいなにゃんにゃんなまほう

ねこもスクリプトをかくなり

React で Markdown を扱うときに便利な react-markdown と remove-markdown

Markdown 便利ですよね。使う側にとってはテキストベースでリッチコンテンツを表現できますし、開発者側にとってもアプリを実装する際に Markdown 対応さえしてしまえば、ある程度の表現力を確保できるとともに、XSS対応などもライブラリ側がしてくれている場合が多いです。Markdown はユーザから投稿されるコンテンツを扱う場合の簡便な選択肢の一つですね。

ウェブアプリとして Markdown を扱う際に必要なのは HTML として表示するためのパーサ・レンダラーです。そしてもう一つ、サマリや概要表示をする際には Markdown の見出しや図表といったマークアップを除外するライブラリがあると便利です。

今回は以下のライブラリを利用して、React で Markdown を扱う簡単なアプリを作成してみました。

最終的にこんな感じに表示されます。

f:id:lightbulbcat:20190802031041p:plain

動作するコードは github に置いておきました。

Markdown 表示アプリの実装

といっても説明すべきことはあまりなく、ブログエントリの構造体として

export interface BlogEntry {
  title: string
  body: string
}

を定義し、アプリケーションロジックとしては以下の実装がほぼ全てになります。

import React from 'react'
import Markdown from 'react-markdown'
import { HashRouter, Route, NavLink, Link, Switch } from 'react-router-dom'
import removeMarkdown from 'remove-markdown'
import './App.css'

import { entries } from './data'

const App: React.FC = () => {
  return (
    <HashRouter>
      <div className="App">
        <nav className="blog-list">
          <div className="blog-list-header">My Blog App</div>
          {entries.map((e, i) => (
            <NavLink className="blog-item" activeClassName="blog-item-active" to={`/blog/${i}`}>
              <div className="blog-item-title">{e.title}</div>
              <div className="blog-item-body">{removeMarkdown(e.body)}</div>
            </NavLink>
          ))}
        </nav>
        <main className="main-content">
          <Switch>
            <Route path="/blog/:index" render={({ match: { params: { index }} }) => {
              const e = entries[index]
              return (
                <div className="blog-detail">
                  <nav className="breadcrumbs">
                    <Link to="/">Blog Entries</Link> / {e.title}
                  </nav>
                  <div className="blog-title">{e.title}</div>
                  <Markdown className="blog-body" source={e.body} />
                </div>
              )
            }} />
            <Route path="/" render={() => (
              <div className="home-content">
                <h1>Welcome to My Blog</h1>
                <p>Select Blog Post!</p>
              </div>
            )} />
          </Switch>
        </main>
      </div>
    </HashRouter>
  )
}

スタイルを当てるための className がうるさくて見づらいですが、要点としては Markdown 形式のテキストデータをそれぞれのライブラリに渡しているだけです。

  • サイドバー: remove-markdown に渡してマークアップを外した文字列を得る
    • それを CSStext-overflow: ellipsis で省略表示に
    • もしくは JavaScript 側で substring するのでも可能
  • 本文: react-markdown コンポーネントsource に 流し込むだけ

簡単すぎますね。ありがたいことです。

付録: Markdownマークアップを外す他の方法

remove-markdown を探すまでに remark 方面から探していたら strip-markdown というライブラリも見つけたので、ついでにそのことも書いておきます。

remark というのは react-markdown 内でも使われている Markdown パーサで、Markdown からASTを得る部分の処理を行っています。remark はさらに内部で unified というテキスト処理のための汎用インタフェースを提供するライブラリを使っているようです。unified はパースや出力時の処理を外部ライブラリから指定でき、remark はパース処理を Markdown 用に固定した unified と言えそうです。

実際 remark の実装を見ると以下のようになっています。

var unified = require('unified')
var parse = require('remark-parse')
var stringify = require('remark-stringify')

module.exports = unified()
  .use(parse)
  .use(stringify)
  .freeze()

そして strip-markdown はその remark から

remark().use(stripMarkdown).processSync()

のように利用するライブラリです。細かいオプションには差異がありますが大枠の動作としては remove-markdown と同じように利用できます。ただ処理の内容として、Markdown 形式の文字列を

という違いがあるので単純な置換処理である remove-markdown の方が軽い気がします。バンドルサイズとしても react-markdown と併用するという前提では unified と remark-parse 分は共用になるので無視するとしても remark-stringify 分は必要になります。package.json の dependencies を増やしたくないという観点では、remove-markdown の方に軍配をあげたくなります。

remark や unified について調べられて面白かったのですが、最終的に remove-markdown を見つけてちょっと肩透かしな気分になったのでした。

以上、Markdown 方面の React、JavaScript 対応状況を調べて試してみました。