Deno で扱う mdx

先日、ドキュメントサイトを作った時、マークダウンを Fresh の処理の中でレンダリングして使いました。
しかし、スタイルをつけたいがために、生の HTML を書く量が比較的多くなる結果になりました。

この生の HTML を書くのを、mdx を使えばある程度回避できそうです。
Deno で mdx を扱う方法を調べてみました。

参考

実装

xmd mdx -> md

xmd は、mdx から md に変換するツールです。

このような.mdx を用意し、xmd で変換してみます。

/sampe1.mdx
1
2
3
4
5
6
7
8
9
10
11
export function Bold({text}) {
return <b>{text}</b>
}

export const bgColor = 'red'

# Hello, <Bold text="world" />

<div style={{backgroundColor: bgColor}}>
backgroundColor: {bgColor}
</div>
1
$ deno run --allow-read --allow-write --allow-net --allow-env https://deno.land/x/xmd/compile.ts ./sampe1.mdx

結果作成された md ファイルを見ると以下のようになっている。

1
2
# Hello, <b></b>
<div style="[object Object]"><p>backgroundColor: red</p></div>

style の展開であったり、コンポーネントに変数が展開されなかったりと、一部対応していない機能がありそう。

実行スクリプトで取り扱ったが、mdx -> md に変換する関数だけ提供されているので部分的に使用も可能です。

deno-mdx mdx -> html

deno-mdx は、mdx_to_html というモジュール名で公開されています。

先の sample1.mdx を deno-mdx で変換してみます。
次のスクリプトを用意します。

convert.ts
1
2
3
4
5
6
7
8
9
10
import { mdxToHTML } from "https://deno.land/x/mdx_to_html/mod.ts";

const src = Deno.readTextFileSync('sample1.mdx')

const html = await mdxToHTML(src, {
head: "<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/destyle.css@1.0.15/destyle.css' />",
title: "Convert MDX to HTML",
});

console.log(html)
1
$ deno run --allow-read convert.ts

結果出力は以下のようになっています。

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Convert MDX to HTML</title>
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/destyle.css@1.0.15/destyle.css' />
</head>
<body>
<h1>Hello, <b>world</b></h1>
<div style="background-color:red"><p>backgroundColor: <!-- -->red</p></div>
</body>
</html>

コンポーネントの展開や、style の展開が完全にできています。
HTML にすることを前提に、title や head 要素の内容を指定できるのが特徴の 1 つであるように感じます。
最近メンテナンスがされていないようで 2 年更新が無いようです。

mdx-js mdx -> js (-> html)

mdx-js は、mdx を js に変換します。
js に変換なので、変換結果は HTML として出力する必要があります。
そこには react-dom/server を使用します。
以下のコードで動作させます。

convert.ts
1
2
3
4
5
6
7
8
9
import {compile} from 'npm:@mdx-js/mdx@3'
import {renderToString} from 'npm:react-dom/server'

const compiled = await compile(Deno.readTextFileSync('sample1.mdx'),)

const moduleUrl = "data:text/javascript;charset=utf-8," + String(compiled).replace("react/jsx-runtime", "npm:react/jsx-runtime")
const module = await import(moduleUrl);

console.log(renderToString(module.default()))
1
$ deno run --allow-read --allow-env .\convert.tsx

結果出力は以下のようになっています。

1
2
<h1>Hello, <b>world</b></h1>
<div style="background-color:red"><p>backgroundColor: <!-- -->red</p></div>

変数の埋め込みや、コンポーネントの展開が完全にできています。
完全は HTML ではなく、React コンポーネントとして HTML の部品を取り出すことができます。

mdx 単体でもファイルの中で定義された変数を埋めたりということができます。
が、mdx-js では React コンポーネントとして取り出すことができるので、コンポーネントへ引数設定も可能です。

例えば、次のようにできます。

変換対象のmdを変更し、レンダリング時に設定「 {props.text} 」 を 書き足します。

sample2.mdx
1
2
3
4
5
6
7
8
9
10
11
12
13
export function Bold({text}) {
return <b>{text}</b>
}

export const bgColor = 'red'

# Hello, <Bold text="world" />

<div style={{backgroundColor: bgColor}}>
backgroundColor: {bgColor}
</div>

レンダリング時に設定「 {props.text} 」
convert.ts(改)
1
2
3
4
5
6
7
8
9
import {compile} from 'npm:@mdx-js/mdx@3'
import {renderToString} from 'npm:react-dom/server'

const compiled = await compile(Deno.readTextFileSync('sample1-1.mdx'),)

const moduleUrl = "data:text/javascript;charset=utf-8," + String(compiled).replace("react/jsx-runtime", "npm:react/jsx-runtime")
const module = await import(moduleUrl);

console.log(renderToString(module.default({text: "text from render"}))) // <= レンダリング時にpropsを設定

これを使って変換すると、次の出力を得られます。

1
2
3
<h1>Hello, <b>world</b></h1>
<div style="background-color:red"><p>backgroundColor: <!-- -->red</p></div>
<p>レンダリング時に設定「 <!-- -->text from render<!-- --></p>

段階をおいて、パラーメータの設定ができるのはjsで取り出しができるmdx-jsの特徴と考えられます。
こうなると、mdxで使うコンポーネントの共通化を試みたくなります。

おそらく以下の方法が簡単です。

share.mdx
1
2
3
4
5
export function Bold({text}) {
return <b>{text}</b>
}

export const bgColor = 'red'
sample2.mdx
1
2
3
4
5
# Hello, <Bold text="world" /> 

<div style={{backgroundColor: bgColor}}>
backgroundColor: {bgColor}
</div>
convert.ts(改2)
1
2
3
4
5
6
7
8
9
10
11
12
import {compile} from 'npm:@mdx-js/mdx@3'
import {renderToString} from 'npm:react-dom/server'

const shareMdx = Deno.readTextFileSync('share.mdx')
const mainMdx = Deno.readTextFileSync('sample2.mdx')

const compiled = await compile(shareMdx + mainMdx) // <= 共通部分とメイン部分を連結

const moduleUrl = "data:text/javascript;charset=utf-8," + String(compiled).replace("react/jsx-runtime", "npm:react/jsx-runtime")
const module = await import(moduleUrl);

console.log(renderToString(module.default({text: "text from render"})))

このように、共通部分を別ファイルにして、メイン部分と連結することで、コンポーネントの共通化ができます。
出力結果は先と同じなので割愛です。

.replace("react/jsx-runtime", "npm:react/jsx-runtime" を記述しています。
これは、import-map に以下のように書け不要になります。

deno.json
1
2
3
4
5
{
"imports": {
"react/jsx-runtime": "npm:react/jsx-runtime"
}
}

Reactコンポーネントとして吐き出されるので、Reactとべったりです。


というわけで、mdx を Deno で扱う方法を調べてみました。
mdx-js を使うパターンが自分の作った「ドキュメント本体は mdx 書いていく」という運用に耐えられそうです。

では。