Visual Studio Code拡張機能「Markdown Clip」の1.2.0を公開しました。
Markdown Clipは、VS Codeに登録されたMarkdown-itプラグインを使ってMarkdownをHTMLへ変換し、クリップボードへコピーする拡張機能です。Markdown Rubyの{文字|ふりがな}やMarkdown Mojicolorの%文字%{色}など、普段のMarkdownプレビューで使っている記法をコピー先のHTMLにも反映するために作っています。
今回の中心は次の2点です。
- YAML Front Matterを特定のプラグイン専用ではなく、すべてのMarkdown-itプラグインが利用できる形で渡す
- VS Codeが描画用に追加した要素や属性を、プレビューへ影響させずコピーHTMLだけから整理する
実装を進める途中では、見出しIDが二重になる問題、重複IDによるプレビュー更新の乱れ、公開Workflowで使うCLIが実際にはインストールされていなかった問題も見つかりました。
出発点はMarkdown MojicolorのYAML設定
きっかけは、Markdown Mojicolorの色設定でした。
次のようにFront Matterで太字や斜体の色を指定すると、VS Codeプレビューには反映されます。
---
markdown:
mojicolor:
bold: yellow
italic: null
---しかしMarkdown Clipで変換したHTMLには、その設定が渡っていませんでした。
調べると、Markdown ClipはFront Matterを解析して本文から除去し、メタデータとして保持していました。一方、描画時には本文だけをMarkdown-itへ渡しており、render()の第2引数であるenvを使っていませんでした。
最初はMojiColor専用の値を渡す案も考えました。しかし、それではFront Matterを使うプラグインが増えるたびに、Markdown Clip側へ専用コードを追加しなければなりません。
そこで設計を一般化し、解析済みFront Matter全体をenv.frontmatterへ格納することにしました。
env.frontmatterとして文書全体を渡す
実装の基本形はシンプルです。
const env = {
frontmatter: structuredClone(markdownDocument.meta.data),
markdownClip: {
removeHeadingId: GetConfig.get("removeHeadingId"),
removeVSCodeAttributes: GetConfig.get("removeVSCodeAttributes"),
},
};
const html = extension.md.render(markdownDocument.content, env);ポイントは、markdown.mojicolorだけでなくFront Matter全体を渡すことです。
---
title: サンプル
markdown:
mojicolor:
bold: yellow
otherPlugin:
enabled: true
---この文書なら、各プラグインは必要な場所だけを参照できます。
env.frontmatter.markdown.mojicolor
env.frontmatter.otherPlugin
env.frontmatter.titleenv.frontmatterという名前はMarkdown-it周辺で使われている慣習にも合わせています。Front Matterのルートキーをenv直下へ展開しないため、Markdown-itが使うreferencesなどのキーとも衝突しません。
選択範囲でも元文書の設定を使う
Markdown Clipは全文だけでなく、選択した部分だけをHTMLへ変換できます。
この場合、選択範囲にはFront Matterが含まれないことが普通です。それでも文書設定は有効であるべきなので、選択文字列を再解析せず、元文書全体から解析済みのFront Matterを取得してenv.frontmatterへ渡します。
これにより、全文変換と選択変換で同じプラグイン設定が使われます。
プラグインの変更を次の描画へ漏らさない
Markdown-itのenvは、描画中にプラグインが読み書きできるオブジェクトです。そのため、元のメタデータをそのまま渡すと、プラグインが値を変更した場合に文書データまで変わる可能性があります。
そこで次の2段階で状態を分離しました。
envを変換ごとに新しく作る- Front Matterを
structuredClone()してから渡す
Front Matterなし、空のFront Matter、YAML全体がnullの場合は、いずれも安全な空オブジェクトとして扱います。連続して別文書を変換しても、前の文書の設定が残らないこともテストしました。
設定の優先順位やnullの意味は、Markdown Clipではなく各プラグイン側の責務です。更新したMarkdown Mojicolorとの統合確認では、VS Code設定、文書YAML、本文中の明示色が期待どおりの順序で適用されました。
コピーHTMLだけをきれいにする
VS CodeのMarkdownプレビュー用HTMLには、コピー先では不要な情報が含まれます。
- Mermaid設定を渡す
<span id="markdown-mermaid"> - ソース行を示す
data-line属性 code-lineクラス- rendererが付ける見出しの自動ID
以前の実装では、VS CodeとMarkdown Clipが共有するMarkdown-it renderer自体を書き換えていました。この方法だとコピーHTMLだけでなく、VS Codeプレビューにも設定の影響が及びます。
今回、コピー変換時にだけenv.markdownClipを追加し、最終的なHTMLをMarkdown Clip側で整理する方式へ変更しました。env.frontmatterは文書由来の汎用データ、env.markdownClipはコピー処理専用の内部設定として分離しています。
これにより、コピー後のHTMLから次の情報だけを取り除けます。
- Mermaidの設定用空span
data-line- class属性内の
code-lineトークン
dir="auto"、他のclass、本文中のspan、Mermaid本体などは維持します。設定をOFFにした場合の従来出力も残しています。
最も難しかった見出しID
見出しIDは、単純にid属性をすべて削除すればよいわけではありません。
markdown-it-attrsを使うと、利用者は次のように意図したIDを明示できます。
# Automatic ID
# Automatic ID {#explicit-id}コピー結果では、自動IDを削除してもexplicit-idは維持する必要があります。また設定がOFFの場合も、明示IDと自動IDが同時に出力されてはいけません。
調査すると、markdown-it-attrsが解析時に明示IDをtokenへ設定したあと、別の拡張機能が描画時に自動IDを追加することがありました。その結果、次のように1要素へidが2個付くケースがありました。
<h1 id="explicit-id" id="automatic-explicit-id">Automatic ID</h1>対策として、Markdown由来の見出しtokenを一時的に識別し、解析時点で存在した明示IDを保持します。描画後の見出しタグを整理するときは、明示IDがあればそれを優先し、なければ設定に応じて自動IDを残すか削除します。処理後はtokenを元へ戻すため、共有rendererや次の描画へ状態を残しません。
最終的な動作は次のようになりました。
<!-- 自動ID削除がON -->
<h1>Automatic ID</h1>
<h1 id="explicit-id">Automatic ID</h1>
<!-- 自動ID削除がOFF -->
<h1 id="automatic-id">Automatic ID</h1>
<h1 id="explicit-id">Automatic ID</h1>手書きHTMLのIDや見出し以外のIDは変更しません。
重複IDは自動修正せず警告する
調査中、同じidを持つ見出し要素を複数貼り付けてから削除すると、VS Codeプレビューの差分更新が正しく追従せず、一部の要素が残って見える現象を再現しました。
Markdown Clipを使わず、生のHTMLだけでも再現したため、コピー処理そのものが原因ではありません。HTMLのidは文書内で一意であることが前提で、重複IDが差分更新の識別を乱していました。自動IDへ連番が付く仕組みにも、重複回避という意味があります。
一方、{#ID}で同じ明示IDを複数指定するMarkdown自体は解析できます。Markdown Clipが勝手にIDを変更すると、利用者が指定したリンク先まで変わってしまいます。
そこで自動修正や変換中止は行わず、同じ文書内で明示IDが重複したすべての{#ID}へWarning Diagnosticを表示する設計にしました。
- 一意な明示IDは警告しない
- 自動IDは対象外
- コードブロック内の
{#ID}は対象外 - 別文書にある同名IDは対象外
- 文書を編集して重複を解消すると警告を消す
- 警告があってもHTML変換とコピーは続行する
明示IDを使う自由を残しながら、HTMLとして問題になる状態だけをエディタ上で知らせます。
なお、プレビューの見出しを操作してもエディタが移動しない問題も、当初はID削除設定との関係を疑いました。実際にはmarkdown.preview.doubleClickToSwitchToEditorを有効にすると移動でき、コピーHTMLのID整理とは別のVS Code設定だと確認できました。
23件のテストで境界を確認
最終的なExtension Test Suiteは23件です。主に次の境界を確認しました。
- 全文・選択変換の
env.frontmatter - Front Matterの欠落、空、
null - 連続する文書間でのenv分離
- プラグインがFront Matterを書き換えた場合の非破壊性
- Mermaid設定spanだけの除去
data-lineとcode-lineだけの除去dir="auto"と他属性の維持- 自動IDの除去と明示IDの維持
- 設定ON/OFFの連続変換
- 通常のVS Codeプレビュー描画が変わらないこと
- 重複する明示IDの診断と編集後の解除
最新のVS Code 1.137.0だけでなく、engines.vscodeで最低対応としているVS Code 1.107.0でも23件すべて成功しました。@types/vscodeも1.107.0へ固定し、最低対応版に存在しないAPIを誤って使いにくくしています。
VSIXは公式@vscode/vsce 3.9.2で実際に作成し、181ファイル、約724.5 KBになりました。テスト、開発用文書、GitHub設定、GIFなどが配布物へ混ざらず、実行コード、翻訳、README、画像、実行時依存だけが残ることを確認しています。
まとめ
Markdown Clip 1.2.0では、最初の「MojiColorのYAML設定をコピーHTMLにも反映したい」という要望を、env.frontmatterによる汎用的な連携へ広げました。これにより、MojiColor以外のMarkdown-itプラグインも文書のFront Matterを利用できます。
同時に、共有rendererへ手を入れてプレビューまで変えてしまう構造を見直し、env.markdownClipを使ってコピー処理の境界を明確にしました。不要な属性は消し、利用者が明示したIDは残し、危険な重複は警告するという役割分担です。
今回の開発で特に重要だったのは、「何を削除するか」より「誰が付けた情報か」「どの描画にだけ作用させるか」を区別することでした。Markdown-itプラグインを組み合わせる拡張機能では、共有エンジン、env、token、最終HTMLの責務を分けることが、互換性と予測可能性につながります。
