Astroとremark-directive v3

個人サイトで、remark-directiveを使ってMarkdownやMDXにディレクティブ記法を導入しているAstroプロジェクトがいくつかある。

そのremark-directiveをv3にバージョンアップさせたら、Astroサイトがエラーを吐いて動かなくなってしまった。

Failed to parse Markdown file "/illustrative-english/src/content/note/test.md":
Cannot set properties of undefined (setting 'directiveAttributes')

問題が起こる条件

テストリポジトリを作ってみた。

普通にunified().use(remarkParse).use(remarkDirective)...という処理を手書きした場合は正常に動作する。

astro.config.mjs{ markdown: { remarkPlugin: [remarkDirective] } }などと設定した場合は、mdだろうがmdxだろうが関係なくコケる。

また、エラーが出るのは、波括弧による属性指定を含む場合のみ。

❌ エラーになる
:::ex[私は日本に住んでいる。]{v=123}
I live in Japan.
:::
✅ 正常に動作する
:::ex[私は日本に住んでいる。]
I live in Japan.
:::

v2 => v3 の変化

remark-directiveのv2 => v3の変更点は、ほぼ依存のアップデートである。

中でも、mdast-util-directiveがv2 => v3にアップデートされており、このアップデートでは結構大幅なリファクタリングを行っている。

見つけた解決方法

mdast-util-directiveのコードを書き換えてしまえば、Astroで動くようにはなる。
https://github.com/syntax-tree/mdast-util-directive/blob/main/lib/index.js

修正内容1
// 現コード(this.dataがundefinedでエラーになる)
this.data.directiveAttributes = hoge
 
// 修正後コード
this.setData("directiveAttributes", hoge)
修正内容2
// 現コード(this.dataがundefinedでエラーになる)
const fuga = this.data.directiveAttributes
 
// 修正後コード
const fuga = this.getData("directiveAttributes")

実はこれはdirectiveAttributes周りのコードをv2に戻しただけ。

作ってしまおうremark-directive

本件と見つけた解決方法をPRとしてmdast-util-directiveに送っても良いが、mdast-util-directiveが修正されたところで、remark-directiveがその修正版を取り込んでくれるのがいつになるかは謎。
しかも本件はAstro側の問題な可能性もあって、このバグをどこに投げたらよいものか本気で悩んでいる。

remark-directiveの動向が心配なら、自分で作ることもできなくはない。

mdのみ対応版
import { fromMarkdown } from "mdast-util-from-markdown"
import { directive } from "micromark-extension-directive"
import { directiveFromMarkdown } from "mdast-util-directive"
import * as unified from "unified"
 
export default function remarkDirective() {
  const self: unified.Processor = this
  self.Parser = function (doc: string) {
    return fromMarkdown(doc, "utf-8", {
      extensions: [directive()],
      mdastExtensions: [directiveFromMarkdown()]
    })
  }
}
MDX対応版
import { mdxjs } from "micromark-extension-mdxjs"
import { mdxFromMarkdown } from "mdast-util-mdx"
import { fromMarkdown } from "mdast-util-from-markdown"
import { directive } from "micromark-extension-directive"
import { directiveFromMarkdown } from "mdast-util-directive"
import * as unified from "unified"
 
export default function remarkDirective() {
  const self: unified.Processor = this
  self.Parser = function (doc: string) {
    return fromMarkdown(doc, "utf-8", {
      extensions: [mdxjs(), directive()],
      mdastExtensions: [mdxFromMarkdown(), directiveFromMarkdown()]
    })
  }
}

ほとんどはremark-directiveの内部で使っているライブラリをそのまま使っているだけ。
しかしremark-directiveと同じ実装方法では状況が変わらないので、実装手法は変えている。