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
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
のオプションを使って画像やリンク、コードブロックなどの見た目や挙動をカスタマイズしています。
remarkDirective
と remarkDirectiveRehype
により以下の構文でオリジナル要素を追加しています。オリジナル要素は前述した components
で見た目や挙動をカスタマイズしています。
:::info 特殊ブロック :::
react-syntax-highlighter
によりコードのハイライトを行っています。必要な言語やテーマをインポートする必要がある点に注意してください。また、言語をJSで分解することでファイル名の表現に対応しています。
```js:ファイル名.js const demo = 111; ```