マークダウンレンダー

1import React, { useState } from 'react';
2import CopyToClipboard from 'react-copy-to-clipboard';
3import toast from 'react-hot-toast';
4import { HiOutlineClipboard } from 'react-icons/hi';
5import ReactMarkdown from 'react-markdown';
6import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter';
7import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism';
8import rehypeAutolinkHeadings from 'rehype-autolink-headings';
9import rehypeSlug from 'rehype-slug';
10import remarkBreaks from 'remark-breaks';
11import remarkDirective from 'remark-directive';
12import remarkDirectiveRehype from 'remark-directive-rehype';
13import remarkGfm from 'remark-gfm';
14import DocAlert from '../components/doc-alert';
15
16import bash from 'react-syntax-highlighter/dist/cjs/languages/prism/bash';
17import diff from 'react-syntax-highlighter/dist/cjs/languages/prism/diff';
18import git from 'react-syntax-highlighter/dist/cjs/languages/prism/git';
19import js from 'react-syntax-highlighter/dist/cjs/languages/prism/javascript';
20import json from 'react-syntax-highlighter/dist/cjs/languages/prism/json';
21import jsx from 'react-syntax-highlighter/dist/cjs/languages/prism/jsx';
22import scss from 'react-syntax-highlighter/dist/cjs/languages/prism/scss';
23import tsx from 'react-syntax-highlighter/dist/cjs/languages/prism/tsx';
24import ts from 'react-syntax-highlighter/dist/cjs/languages/prism/typescript';
25import DocHeading from '../components/doc-heading';
26
27SyntaxHighlighter.registerLanguage('javascript', js);
28SyntaxHighlighter.registerLanguage('jsx', jsx);
29SyntaxHighlighter.registerLanguage('tsx', tsx);
30SyntaxHighlighter.registerLanguage('typescript', ts);
31SyntaxHighlighter.registerLanguage('scss', scss);
32SyntaxHighlighter.registerLanguage('json', json);
33SyntaxHighlighter.registerLanguage('bash', bash);
34SyntaxHighlighter.registerLanguage('git', git);
35SyntaxHighlighter.registerLanguage('diff', diff);
36
37const DEFAULT_MARKDOWN = `
38# タイトル
39
40テキストテキスト[リンク](https://flock.codes)テキストテキスト
41
42\`\`\`js:ファイル名.js
43const demo = 111;
44\`\`\`
45
46:::info
47ノートブロック
48:::
49
50- リスト
51- リスト
52- リスト
53
54![](/images/ogp.png)
55
56## テーブル
57
58xxx|xxx
59---|---
60xxx|xxx
61xxx|xxx
62`;
63
64const MarkdownRender = () => {
65  const [markdown, setMarkdown] = useState<string>(DEFAULT_MARKDOWN);
66
67  return (
68    <div className="grid grid-cols-1 lg:grid-cols-2 gap-10">
69      <textarea
70        className="bg-transparent rounded border p-4"
71        defaultValue={DEFAULT_MARKDOWN}
72        onChange={(e) => setMarkdown(e.currentTarget.value)}
73      />
74      <ReactMarkdown
75        remarkPlugins={[
76          remarkGfm,
77          remarkDirective,
78          remarkDirectiveRehype,
79          remarkBreaks,
80        ]}
81        rehypePlugins={[rehypeSlug, rehypeAutolinkHeadings]}
82        className="prose prose-pre:p-0 dark:prose-invert prose-a:text-pink-600"
83        transformImageUri={(src) => {
84          if (process.env.NODE_ENV === 'production') {
85            return `/stripe-doc` + src;
86          } else {
87            return src;
88          }
89        }}
90        components={{
91          ['info' as keyof JSX.IntrinsicElements]: DocAlert,
92          ['warning' as keyof JSX.IntrinsicElements]: DocAlert,
93          ['important' as keyof JSX.IntrinsicElements]: DocAlert,
94          img({ src }) {
95            return (
96              <a href={src} target="_blank" rel="noreferrer">
97                <img
98                  src={src}
99                  className="rounded-md shadow overflow-hidden block"
100                  alt=""
101                />
102              </a>
103            );
104          },
105          h1: DocHeading,
106          h2: DocHeading,
107          h3: DocHeading,
108          h4: DocHeading,
109          h5: DocHeading,
110          code({ node, inline, className, children, ...props }) {
111            const match = /language-(\w+)/.exec(className || '');
112            const fileName = className?.split(':')?.[1] || '';
113            return inline ? (
114              <code {...props}>{children}</code>
115            ) : (
116              <div>
117                <div className="flex items-center justify-between bg-[#323233] text-sm py-2 px-3 text-white">
118                  <span className="opacity-60">{fileName}</span>
119                  <CopyToClipboard
120                    onCopy={() =>
121                      toast.success('コピーしました', {
122                        position: 'bottom-center',
123                      })
124                    }
125                    text={String(children)}
126                  >
127                    <button>
128                      <HiOutlineClipboard className="w-5 h-5 text-gray-500" />
129                    </button>
130                  </CopyToClipboard>
131                </div>
132                <SyntaxHighlighter
133                  // TODO: remote any https://github.com/react-syntax-highlighter/react-syntax-highlighter/issues/439
134                  style={vscDarkPlus as any}
135                  language={match?.[1] || 'plane'}
136                  PreTag="div"
137                  customStyle={{ margin: 0 }}
138                  {...props}
139                >
140                  {String(children).replace(/\n$/, '')}
141                </SyntaxHighlighter>
142              </div>
143            );
144          },
145        }}
146      >
147        {markdown}
148      </ReactMarkdown>
149    </div>
150  );
151};
152
153export default MarkdownRender;

解説

記事の装飾

Tailwind CSS のプラグインにより prose prose-pre:p-0 dark:prose-invert prose-a:text-pink-600 でダークモード対応を含む記事の装飾が完了しています。prose-xxx とすることで記事内の特定の要素についてスタイルをカスタマイズしています。

要素のカスタマイズ

componentsオプションを使って画像やリンク、コードブロックなどの見た目や挙動をカスタマイズしています。

オリジナル要素の追加

remarkDirectiveremarkDirectiveRehype により以下の構文でオリジナル要素を追加しています。オリジナル要素は前述した components で見た目や挙動をカスタマイズしています。

:::info 特殊ブロック :::

コードのハイライト

react-syntax-highlighter によりコードのハイライトを行っています。必要な言語やテーマをインポートする必要がある点に注意してください。また、言語をJSで分解することでファイル名の表現に対応しています。

```js:ファイル名.js const demo = 111; ```

使用ライブラリ

参考サイト