オレオレ記法のMarkdownを任意のReactElementとして変換する

**オレオレ記法で書いたMarkdownを任意のReactコンポーネントに変換する。**

JAMStackに挑戦する中で、Reactをベースとしたフレームワーク「Next.js」を用いてMarkdownでブログ記事を書くようになりました。しかしYouTubeやTwitterの埋め込みを行おうにも、Markdownの標準的な記法には含まれていないので何とかする必要がありました。

この記事ではオレオレ記法で書かれた拡張MarkdownをReactで利用するために私が用いた方法を書きまとめます。

# この記事について

Markdownから読み込んだ文字列をReact上で使用するための方法を書きます。
⇨ Markdown to ReactElement

Markdownでブログテンプレートを作ろうと思っている人などの参考になれば幸いです。

Markdownを変換する方法としてはUnifiedエコシステムを用いるやり方が王道ですが、自分で拡張パーサーを作成するのはちょっと難易度が高かったので、よりお手軽な方法に頼っています。拡張パーサーの作成に挑戦する場合は下記の記事などが参考になるはずです。

https://zenn.dev/januswel/articles/44801708e8c7fdd358e6

# オレオレMarkdownの変換

## オレオレ記法の例

今回は簡単に以下のようなYouTubeとTwitterの埋め込みをオレオレ記法で表現します。
(Qiita上でMarkdownの中にMarkdownを記載する方法が分からなかったので1,2,3は行番号と思って読み飛ばしてください)

コードを表現するMarkdown記法に`twitte`, `youtube`というオレオレな言語名を指定しています。

```text:Twitterの埋め込み
1  ```twitter:a:b
2  1234567890
3  ```
```

```text:YouTubeの埋め込み
1  ```youtube
2  1234567890
3  ```
```

続いて実装です。

## node_moduleの調達

基本的なReactの動作環境とMarkdownの読み込みまでは整っている前提です。

```bash
yarn add react-markdown remark-gfm
```

- `react-markdown` : 7.1.0
- `remark-gfm` : 3.0.x

`react-markdown`はMarkdownをReactElementに変換するパッケージです。基本はGitHub準拠のMarkdown記法を採用したいので、`remark-gfm`をパーサーとして使用します。(内部的にUnifiedエコシステムを使用しています)

## Markdownを変換

```typescript:MarkdownをReactElementに変換する処理
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import OriginalComponent from "xxxxx";

type Props = {
  content: string;
};

const ArticleBody = ({ content }: Props) => {
  const convertResult = (
    <ReactMarkdown remarkPlugins={[remarkGfm]} components={{code: CodeBlock}}>
      {content}
    </ReactMarkdown>
  );
  return <div>{convertResult}</div>;
};

export default ArticleBody;
```

基本的に`react-markdown`のマニュアルに記載しているとおりです。Markdownで記載した文字列を`content`として渡し、`remarkGfm`を使ってHTMLに変換。`code`タグとして解釈されたタグがあれば`CodeBlock`として追加の変換処理を加えています。

`CodeBlock`というのが今回作るオレオレMarkdownの読み替え部分です。

https://github.com/remarkjs/react-markdown


```typescript:CodeBlock.tsx
import { CodeProps } from "react-markdown/lib/ast-to-react";
import TweetEmbed from "xxxxx";
import YouTubeEmbed from "xxxxx";

const CodeBlock = ({ node, className, children, ...props }: CodeProps) => {
  // カスタム値の判定材料を抽出
  const prefix = "language-";
  const classes = className
    ?.split(" ")
    .find((c) => c.startsWith(prefix))
    ?.replace(prefix, "");
  const params = classes ? classes.split(":") : [];

  if (params.length > 0 && params[0] === "twitter") {
    // Twitter埋め込み
    const id = children.toString().replace(/\r?\n/g, "");
    return <TweetEmbed id={id} />;
  }

  if (params.length > 0 && params[0] === "youtube") {
    // YouTube埋め込み
    const id = children.toString().replace(/\r?\n/g, "");
    return <YouTubeEmbed videoId={id} />;
  }

  // 通常のコンポーネントを返却
  return <code className={className}>{children}</code>;
};

export default CodeBlock;
```

`react-markdown`では`code`タグとして解釈した場合、`className`にはprefixとして`language-`が付与されます。今回の例だと`language-twitter:a:b`や`language-youtube`となります。なので`params`にて`className`を分解してオレオレ引数として変換しています。

params
⇨twitter -> [twitter, a, b]
⇨youtube -> [youtube]

children
⇨1234567890

あとはparamsを参照してオレオレ記法として変換するのか、通常のコードブロックとして何もしないのかを振り分けます。

## 終わり

実際に実装してみたのが下記になります。
今度は自力でUnifiedエコシステムのプラグインを作ってオレオレ記法に対応してみたいです。

https://github.com/big-mon/nextjs-lustario