〽️

remark のプラグインを作ってMarkdownの文法を勝手に拡張しよう

2023/07/12に公開

これは友達の話ですが、react-markdownでGithubコメントのような改行を実現しようとしていました。

以下のようなMarkdownがあったときに

input.md
a
a

a

通常の仕様では

output
a a
a

のように表示されます。
しかし、Githubのissueコメントなどでは

output
a
a

a

のように表示されます。

このような機能を作ろうと思ったときにremark-breaksを使うと簡単に作れます。

しかし、 友達はそんなこと知らず、remark-gfmだけを調査してそういうpackageは無いと思い込んでしまいました。
そして、remark のプラグインを自作してしまったのでした。

最終的にこのページで紹介するソースコードは使用しませんでしたが、remark のプラグインを作る知識自体は今後も役に立つと思うので簡単にまとめておきます。

react-markdownでGithubコメントのような改行を実現したいだけなら、この記事よりも以下の方を参考にしてください。
https://zenn.dev/yosipy/articles/fa8de0c1ec595e

remark のプラグインを作ってMarkdownの文法を勝手に拡張しましょう。

今回は改行しかいじりませんが、かなり自由にMarkdownの文法を改造できます。

参考にしたissueのコメント

コード全体

import { FC } from "react"

import ReactMarkdown from "react-markdown"
import { visit } from "unist-util-visit"

type Props = {
  body: string
}

const remarkBlankLine = () => {
  const transformer = (tree: any) => {
    visit(tree, "text", (node) => {
      if (node.value === "------ br ------") {
        node.data = node.data || {}
        node.data.hName = "customBr"
      }
    })
  }

  return transformer
}

export const Markdown: FC<Props> = (props) => {
  const parsedBody = props.body
    .replace(/\n{2,}/g, "\n------ br ------\n")
    .replace(/\n/g, "\n\n")

  return (
    <ReactMarkdown
      remarkPlugins={[remarkBlankLine]}
      components={{
        customBr() {
          return <br />
        },
      }}
    >
      {parsedBody}
    </ReactMarkdown>
  )
}

解説

  const parsedBody = props.body
    .replace(/\n{2,}/g, "\n------ br ------\n")
    .replace(/\n/g, "\n\n")

渡されたマークダウン全体を対象に置換します。
複数連続で改行している箇所は後で<br />タグを出力するので"\n------ br ------\n"に一時的に置換しています。ちょっとイケてないですね。正直、面倒になってこういう実装にしました。
ちなみにここで<br />に置換してもXSS対策でエンコードされるのでHTMLタグになりません。

そのあと、単一の改行コードを2つに置換してます。

読み込まれたMarkdownをパーツごとに処理するためにremarkPluginsを自作します。

const remarkBlankLine = () => {
  const transformer = (tree: any) => {
    visit(tree, "text", (node) => {
      if (node.value === "------ br ------") {
        node.data = node.data || {}
        node.data.hName = "customBr"
      }
    })
  }

  return transformer
}

textを指定しているのでMarkdownを分割して対応する単位でnodeに渡してくれます。言葉で説明するより自分でconsole.log(node)したらわかりやすいと思います。
渡されたnodeに入っている値が"------ br ------"の時だけcustomBrで処理をするように指定します。

    <ReactMarkdown
      remarkPlugins={[remarkBlankLine]}
      components={{
        customBr() {
          return <br />
        },
      }}
    >

先ほどのcustomBrの時に出力するHTMLを指定します。<br />です。

完全に余談ですが以下のように代入すると

node.data.hProperties = { test: 'hoge' }

以下のように値を渡せます。

    <ReactMarkdown
      remarkPlugins={[remarkBlankLine]}
      components={{
        customBr({test}) {
	  // hoge
	  console.log(test)
          return <br />
        },

Discussion