fwywd チームでは新ブログを Tailwind UI の Syntax を使用して実装予定です。
記事の表示と記事作成画面のプレビューを揃えたい & 後々の Syntax 導入をスムーズにしたい目的で Tailwind UI の Syntax ベースの Markdown Viewer を実装しました!
markdoc のライブラリをインストール
yarn add @markdoc/markdoc @markdoc/next.js -D
next.config.js を書き換える
括弧の位置を間違えやすいので注意が必要です。
// next.config.js
const withMarkdoc = require('@markdoc/next.js');
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
deviceSizes: [340, 640, 768, 1024, 1280, 1440, 1980],
domains: [],
},
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md'],
};
module.exports = withMarkdoc()(nextConfig);
Prose.tsx
Markdoc で変換したデータを children で渡し画面表示するコンポーネントです。
Tailwind CSS の prose というクラスによって余白や文字間を調節しています。
下準備として、clsx, @tailwindcss/typography をインストールします。
yarn add clsx -D
yarn add @tailwindcss/typography -D
tailwind.config.js に plugins を追記します。
plugins: [require('@tailwindcss/typography')],
Prose.tsx の内容は以下です。
// Prose.tsx
import clsx from 'clsx';
import { ReactNode } from 'react';
interface ProseProps {
children: ReactNode;
className?: string;
}
export const Prose: React.FC<ProseProps> = ({ className, ...props }) => (
<div
className={clsx(
className,
'dark:text-gray-400 prose prose-gray max-w-none dark:prose-invert',
// headings
'prose-headings:font-display prose-headings:scroll-mt-28 prose-headings:font-normal lg:prose-headings:scroll-mt-[8.5rem]',
// lead
'prose-lead:text-gray-700 dark:prose-lead:text-gray-700',
// links
'prose-a:font-semibold dark:prose-a:text-primary-500',
// link underline
'prose-a:no-underline prose-a:shadow-[inset_0_-2px_0_0_var(--tw-prose-background,#fff),inset_0_calc(-1*(var(--tw-prose-underline-size,4px)+2px))_0_0_var(--tw-prose-underline,theme(colors.primary.200))] hover:prose-a:[--tw-prose-underline-size:6px] dark:[--tw-prose-background:theme(colors.gray.900)] dark:prose-a:shadow-[inset_0_calc(-1*var(--tw-prose-underline-size,2px))_0_0_var(--tw-prose-underline,theme(colors.primary.800))] dark:hover:prose-a:[--tw-prose-underline-size:6px]',
// pre
'prose-pre:rounded-xl prose-pre:bg-gray-900 prose-pre:shadow-lg dark:prose-pre:bg-gray-800/60 dark:prose-pre:shadow-none dark:prose-pre:ring-1 dark:prose-pre:ring-gray-700/10',
// hr
'dark:prose-hr:border-gray-800',
)}
{...props}
/>
);
Markdoc でデータを変換して Prose.tsx に渡すコンポーネントを作成します。
// Markdown.tsx
export const Markdown = () => {
const doc = `
# heading 1
## heading 2
1. ol
1. ol
- ul
- ul
`;
const ast = Markdoc.parse(doc);
const content = Markdoc.transform(ast);
const children = Markdoc.renderers.react(content, React);
return (
<Prose className={`${style.md} h-[600px] w-full overflow-auto border border-primary-800 p-2`}>
{children}
</Prose>
);
};
余白や文字間は Tailwind CSS の prose でいい感じになっているのでテキストの大きさや色などの装飾を CSS モジュールで適用します。
マークダウン表示できます。
Fence.tsx
マークダウンの表示はできましたがコードのハイライトはされない状態です。
Markdoc の Syntax highlighting を参考にして Tailwind UI の Syntax ベースでコードのハイライトを実装します。
少し複雑でした。大まかな流れとしたら、
- code をハイライトする
Fence.tsx
を作成する
- code を取り出してハイライト適用するための config ファイル
node.ts
を作成する
- Markdoc でデータ変換する関数に
node.ts
から config を渡す
といった流れです。
code をハイライトする Fence.tsx の作成
コードをハイライトするための prism-react-renderer をインストールします。
yarn add prism-react-renderer -D
Tailwind UI からそのまま持ってきて型付けをしています。
<Highlight>
に渡す code の children は String(children) としないと型のエラーが発生してしまうので注意です。
// Fence.tsx
import { Fragment, ReactNode } from 'react';
import Highlight, { defaultProps, Language } from 'prism-react-renderer';
import vsDark from 'prism-react-renderer/themes/vsDark';
interface FenceProps {
children: ReactNode;
language: Language;
}
export const Fence: React.FC<FenceProps> = ({ children, language }) => (
<Highlight
{...defaultProps}
code={children === undefined ? '' : String(children).trimEnd()}
language={language}
theme={vsDark}
>
{({ className, style, tokens, getTokenProps }) => (
<pre className={className} style={style}>
<code>
{tokens.map((line, lineIndex) => (
<Fragment key={lineIndex}>
{line
.filter((token) => !token.empty)
.map((token, tokenIndex) => (
<span key={tokenIndex} {...getTokenProps({ token })} />
))}
{'\n'}
</Fragment>
))}
</code>
</pre>
)}
</Highlight>
);
ハイライトを適用させるための node.ts を作成
// markdoc/node.ts
const node = {
fence: {
render: 'Fence',
attributes: {
language: {
type: String,
},
},
},
};
export default node;
Markdoc でデータ変換する関数に config を渡す
// Markdown.tsx
const ast = Markdoc.parse(doc);
const content = Markdoc.transform(ast, { nodes: node });
const children = Markdoc.renderers.react(content, React, {
components: { Fence },
});
code がハイライトされます。
まとめ
TypeScript の型付けと code のハイライト表示に苦戦しましたがなんとかなりました。
なにより Tailwind CSS の prose が便利でした!
少しマニアックなところですが参考になる方がいれば嬉しいです!