Markdown Clip 1.2.0開発ログ:Front Matter連携とコピーHTMLの整理

Visual Studio Code拡張機能「Markdown Clip」の1.2.0を公開しました。

Markdown Clipは、VS Codeに登録されたMarkdown-itプラグインを使ってMarkdownをHTMLへ変換し、クリップボードへコピーする拡張機能です。Markdown Ruby{文字|ふりがな}Markdown Mojicolor%文字%{色}など、普段のMarkdownプレビューで使っている記法をコピー先のHTMLにも反映するために作っています。

今回の中心は次の2点です。

v1.2.0の主な変更点
  • YAML Front Matterを特定のプラグイン専用ではなく、すべてのMarkdown-itプラグインが利用できる形で渡す
  • VS Codeが描画用に追加した要素や属性を、プレビューへ影響させずコピーHTMLだけから整理する

実装を進める途中では、見出しIDが二重になる問題、重複IDによるプレビュー更新の乱れ、公開Workflowで使うCLIが実際にはインストールされていなかった問題も見つかりました。

目次

出発点はMarkdown MojicolorのYAML設定

きっかけは、Markdown Mojicolorの色設定でした。

次のようにFront Matterで太字や斜体の色を指定すると、VS Codeプレビューには反映されます。

YAML
---
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として文書全体を渡す

実装の基本形はシンプルです。

JavaScript
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全体を渡すことです。

YAML
---
title: サンプル

markdown:
  mojicolor:
    bold: yellow

otherPlugin:
  enabled: true
---

この文書なら、各プラグインは必要な場所だけを参照できます。

JSON
env.frontmatter.markdown.mojicolor
env.frontmatter.otherPlugin
env.frontmatter.title

env.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には、コピー先では不要な情報が含まれます。

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から次の情報だけを取り除けます。

コピー先のHTMLから取り除く情報
  • Mermaidの設定用空span
  • data-line
  • class属性内のcode-lineトークン

dir="auto"、他のclass、本文中のspan、Mermaid本体などは維持します。設定をOFFにした場合の従来出力も残しています。

最も難しかった見出しID

見出しIDは、単純にid属性をすべて削除すればよいわけではありません。

markdown-it-attrsを使うと、利用者は次のように意図したIDを明示できます。

Markdown
# Automatic ID
# Automatic ID {#explicit-id}

コピー結果では、自動IDを削除してもexplicit-idは維持する必要があります。また設定がOFFの場合も、明示IDと自動IDが同時に出力されてはいけません。

調査すると、markdown-it-attrsが解析時に明示IDをtokenへ設定したあと、別の拡張機能が描画時に自動IDを追加することがありました。その結果、次のように1要素へidが2個付くケースがありました。

HTML
<h1 id="explicit-id" id="automatic-explicit-id">Automatic ID</h1>

対策として、Markdown由来の見出しtokenを一時的に識別し、解析時点で存在した明示IDを保持します。描画後の見出しタグを整理するときは、明示IDがあればそれを優先し、なければ設定に応じて自動IDを残すか削除します。処理後はtokenを元へ戻すため、共有rendererや次の描画へ状態を残しません。

最終的な動作は次のようになりました。

Markdown
<!-- 自動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}は対象外
  • 別文書にある同名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-linecode-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の責務を分けることが、互換性と予測可能性につながります。

この記事が参考になりましたら、
上記の「いいね」を押していただけると嬉しいです😄
今後の記事作りの励みになります!
目次