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 が便利でした!
少しマニアックなところですが参考になる方がいれば嬉しいです!

\ 次期受講生募集中 /

経営から技術まですべての力を手に入れたいあなたへ

【次期受講生募集】経営から技術まで仲間と一緒にすべての力を手に入れたいあなたへ

『起業家精神 × 学び方改革』が起こす行動の劇的な変化!
キカガク創業者の吉崎が考える新しい起業家育成の環境。
スキルばかり磨いて変わらない現実を変えていきましょう!