Sätteri
Astro 7 からデフォルトになった Markdown / MDX プロセッサ。パースとコンパイルは Rust で行い、プラグインは JavaScript で書く。unified(remark / rehype)とは別系統だが、mdast と hast という AST の形は共有している。
Astro 7 で markdown.remarkPlugins を設定するとエラーになり、@astrojs/markdown-remark を入れて processor: unified({...}) を明示するよう促される。remark を使い続けることはできるが、既定の道からは外れる。
プラグインの形
ノードの型名をキーにしたビジターを持つオブジェクトを定義する。
import { defineMdastPlugin } from 'satteri'
export const mermaid = defineMdastPlugin({
name: 'mermaid',
code(node) {
if (node.lang !== 'mermaid') return
return { rawHtml: `<div class="mermaid">${escapeHtml(node.value)}</div>` }
},
})
Astro 側では satteri() に渡す。
import { satteri } from '@astrojs/markdown-satteri'
export default defineConfig({
markdown: {
processor: satteri({ mdastPlugins: [noteTitle, mermaid, noteLinks] }),
},
})
ビジターは値を返すとそのノードを置換でき、ctx からは removeNode insertBefore insertAfter replaceNode wrapNode setProperty parent indexOf textContent などが使える。unist-util-visit で親の children を splice していた処理は、だいたい ctx のメソッドに置き換わる。
raw / rawHtml はブロックとして扱われる
ここでかなり時間を溶かした。ビジターが返せる値は MdastNode | { raw: string } | { rawHtml: string } で、raw は Markdown、rawHtml は生 HTML のエスケープハッチ。インライン位置の text ノードをこれで置換すると、ブロックとして解釈されて <p> が挿入される。
--- return raw ---
<p>aaa <code>code</code><p>bbb<a href="/z">LINK</a>ccc</p><strong>strong</strong></p>
段落の中に段落が生まれてしまっている。インラインのまま複数ノードに分割したいなら、宣言的な mdast ノードの配列を insertBefore して元のノードを removeNode する。
text(node, ctx) {
const parts = buildParts(node.value) // [{type:'text'}, {type:'link'}, ...]
if (!parts) return
ctx.insertBefore(node, parts)
ctx.removeNode(node)
}
これなら期待どおりインラインで展開される。
--- insertBefore ---
<p>aaa <code>code</code> bbb<a href="/x">LINK</a>ccc <strong>strong</strong></p>
data.hProperties も効くので、生成した link ノードに class を付けられる。
なお、コードブロックとインラインコードは別のノード型なので、text ノードだけを見ているぶんには #include のような記述を誤って拾う心配がない。これは remark でも同じだが、AST を触る方式の素直な利点。
プラグインの変更が効かないときはキャッシュを疑う
プラグインを書き換えたのに出力が変わらない、という状況に一度はまった。.astro を消しても直らず、node_modules/.astro と node_modules/.vite まで消してようやく反映された。設定ファイル経由で読み込まれるプラグインは Vite 側にキャッシュされる。
rm -rf .astro node_modules/.astro node_modules/.vite dist
シンタックスハイライト
Astro が組み込みのハイライトプラグインを挿すので、markdown.shikiConfig はそのまま効く。デュアルテーマ + defaultColor: false にすると CSS 変数だけが出力されるが、<pre> に付くクラスは .shiki ではなく .astro-code。
.astro-code { color: var(--shiki-light); background-color: var(--shiki-light-bg); }
[data-theme='dark'] .astro-code { color: var(--shiki-dark); background-color: var(--shiki-dark-bg); }
Astro の Hono アダプタと組み合わせて使っている。
理解度チェック
Sätteri のビジターが { raw } や { rawHtml } を返すと、インライン位置の text ノードはどうなるか。
{ raw } や { rawHtml } を返すと、インライン位置の text ノードはどうなるか。ブロックとして解釈され、段落の中に <p> が入れ子で生成される。インラインのまま分割するには、宣言的な mdast ノードの配列を insertBefore して元のノードを removeNode する。
Markdown プラグインを書き換えたのに出力が変わらない。.astro を消しても直らないとき、次に消すのはどこか。
.astro を消しても直らないとき、次に消すのはどこか。node_modules/.astro と node_modules/.vite。設定ファイル経由で読まれるプラグインは Vite 側にキャッシュされる。
出典
- satteri のドキュメント
node_modules/satteri/dist/mdast/mdast-visitor.d.ts— ctx の API とビジターの一覧node_modules/@astrojs/markdown-satteri/dist/processor.d.ts—satteri()に渡せるオプション