Markdoc (Tailwind UI の Syntax ベース)による Markdown Viewer の実装

Markdoc (Tailwind UI の Syntax ベース)による Markdown Viewer の実装
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 モジュールで適用します。
マークダウン表示できます。
1.png

Fence.tsx

マークダウンの表示はできましたがコードのハイライトはされない状態です。
Markdoc の Syntax highlighting を参考にして Tailwind UI の Syntax ベースでコードのハイライトを実装します。
少し複雑でした。大まかな流れとしたら、
  1. code をハイライトする Fence.tsx を作成する
  2. code を取り出してハイライト適用するための config ファイル node.ts を作成する
  3. 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 がハイライトされます。
2.png

まとめ

TypeScript の型付けと code のハイライト表示に苦戦しましたがなんとかなりました。
なにより Tailwind CSS の prose が便利でした!
少しマニアックなところですが参考になる方がいれば嬉しいです!