マークダウン執筆で画像挿入が面倒だったのでNeovimプラグインを開発した話
たいていのVimmerは普段のコーディングだけでなくZennや個人ブログの記事を書く際もVimで執筆します。私たちにとってVimはとても手慣れたものでテキストエディトにおいては快適に執筆作業に取り掛かれますが、一方で画像挿入においては他のGUIエディターと比べて不便が生じます。
一般には、特別なプラグインを入れていないNeovimでの画像挿入は以下の工程で行います。
1. ファイルマネージャーなどで画像を特定のディレクトリに保存する
2. Neovimで保存した画像のパスを入力する
一見すると、面倒に思える作業ではないですが、日頃からNeovimで爆速編集している私たちにとってはとても大変で煩わしい作業に違いないです。
そのため、この作業をコマンド一つで行えるプラグインを開発しました。
mdxsnap.nvim
mdxsnap.nvimはクリップボードに保存された画像をマークダウンまたは、MDX形式の文書にコピペするプラグインです。
特徴は主にこれら3つがあげられます。
- 覚えるコマンドは一つだけ
:PasteImage [filename] - プロジェクト単位でのプロファイル分け (
ProjectOverrides) - ユーザー独自の画像コンポネントに対応 (
customTextFormat)
(他にも便利な機能が盛り沢山です。追加してほしい機能などございましたらGitHubのissueにてお知らせください)
これらの機能によって、独自の構造を取るCMSやブログサービスに対応しています。
インストール
通常のインストール方法でインストールできます。
- Example for
lazy.nvim
{
'HidemaruOwO/mdxsnap.nvim',
config = function()
require('mdxsnap').setup()
end
}
- Example for
jetpack.vim (packer style)
{
"HidemaruOwO/mdxsnap.nvim",
cmd = "PasteImage",
ft = { "markdown", "mdx" },
config = function()
require('mdxsnap').setup()
end,
},
依存関係についは、OS標準のクリップボードツールが必要なため、インストールされていない場合はインストール、もしくはOSの更新が必要となります。
- Windows:
Get-Clipboard - macOS:
pbpaste,osascript,sips - Linux (X11):
xclip - Linux (Wayland):
wl-paste
使い方
前述の通りmdxsnap.nvimは覚えるコマンドが一つだけでシンプルに設計されています。
" 引数なしで実行できて、この場合はクリップボードの画像をランダムな文字列で保存します
:PasteImage
" クリップボードの画像を引数で指定したファイル名で保存します。人気があるのはこちらでしょう
:PasteImage [filename]
動作デモ
- マークダウン
以前は面倒な手順を3ステップ踏まないと画像挿入ができませんでしたが、mdxsnapを使うことでご覧の通り、たった1コマンドで瞬時に貼り付けられるようになりました。我ながらチョー便利に感じられました。
マークダウンのスナップ

- MDX
実装の際に工夫した機能として、:PasteImageコマンドを実行した後に、ひょこっとmdxドキュメントの上部にastro:assetsとmdxsnapとプロジェクトのディレクトリの互換レイヤーがインポートされるのをご覧になったと思います。また、考えられるバグの一つは重複インポートで、こちらについてはcheckRegexで対策しています。
MDXのスナップ

設定
mdxsnap.nvimは設定なしで動作しますが、ユーザーが任意のCMS向けの設定をする前提で設計されたため、使用体験はとても悪く、やはり設定するべきです。
設定のサンプル
require("mdxsnap").setup({
-- 画像保存のデフォルトパス
DefaultPastePath = "snaps", -- デフォルト: "snaps/images/posts"
DefaultPastePathType = "relative", -- デフォルト: "relative" ("absolute"も指定可能)
-- ファイル内に存在させるグローバルなカスタムインポート文
customImports = {
{
line = 'import { Image } from "astro:assets";', -- 完全なインポート行
checkRegex = 'astro:assets', -- 既存インポートをチェックする文字列/正規表現
},
},
-- 挿入される画像参照テキストのグローバル形式
customTextFormat = "", -- デフォルト: Markdown画像形式 ""
-- 特定プロジェクト用のデフォルト設定オーバーライド
ProjectOverrides = {
{
-- プロジェクトディレクトリ名でマッチ
matchType = "projectName", -- "projectName" または "projectPath"
matchValue = "my-astro-blog", -- プロジェクトルートディレクトリ名
PastePath = "src/assets/blog-images", -- このプロジェクト用カスタムパス
PastePathType = "relative",
customImports = { -- このプロジェクト用グローバルcustomImportsをオーバーライド
{ line = 'import { BlogImage } from "@/components/BlogImage.astro";', checkRegex = "@/components/BlogImage.astro" },
},
customTextFormat = '<BlogImage alt="%s" src="%s" />', -- グローバルcustomTextFormatをオーバーライド
},
-- 必要に応じてさらにルールを追加
},
})
重要な設定としてcustomTextFormatとcustomImportsが挙げられます。これらは他のプラグインにはないmdxsnapの特徴と言えます。
-
customTextFormat: Markdownの![]()画像形式構文を任意の形式に置き換えます。
これによって、mdxsnapが:PasteImageコマンド実行時に出力するマークダウンを変更することができます。
たとえば、<img>タグで画像を読み込んだり、独自に実装されたコンポーネントで画像を読み込むことができます。
この際に代入詞の%sはalt -> srcの順番で記述してください。(mdxsnapの次のアップデートでは{{alt}}, {{src}}のようなディレクティブで記述できるようにするべきですね・・・)
<img alt="%s" src="%s" />
<BlogImage alt="%s" src="%s" />
-
customImports: MDXで画像用のコンポーネントや関数が不足している際に自動インポートします。-
line: インポートする全文です。 -
checkRegex: インポートの重複対策として既存のインポートを検出するために必要なフレーズです。
-
最後に覚えておくべき設定はProjectOverridesです。この機能は平たくいえばプロファイル機能で、プロジェクトに適応したcustomImportsなどの設定を行えます。
-
ProjectOverrides: プロジェクト単位でcustomImportsやcustomTextFormat、PastePathなどの変更を可能にします。-
matchType: プロジェクトを上書きする際に、プロジェクトを判定するロジックを選択します。"projectName"もしくは"projectPath"が指定可能です。-
"projectName": (推奨) プロジェクトルートのディレクトリ名で判断します。ここがプロジェクトルートであるかの判断は、.gitや.svnなどのディレクトリの存在を基準に判断します。 -
"projectPath": プロジェクトの絶対パスを指定して設定を上書きします。gitなどのDev-Opsを使用していないプロジェクトでの利用などを考慮しています。
-
-
matchValue: ここにはmatchTypeに対応した任意の値が指定されます。matchTypeが"projectName"の場合はプロジェクトルートのディレクトリ名 (portfolio,dotfiles,zenn-articlesなど)です。"projectPath"の場合はプロジェクトが存在するディレクトリの絶対パスを指定します。 -
PastePath: 記事に使用する画像ファイルの保存場所を決める設定項目です。
-
PastePathで指定したディレクトリの下に、以下の構造で画像が保存されます:
プロジェクトルート/
└── [PastePathで指定したディレクトリ]/
└── [マークダウンファイル名]/
└── [画像ファイル].png
-
PastePathType: 記事の画像ファイルの保存先を決めるPastePathのパスが、絶対パスか相対パスかを決めます。指定する値は"relative"か"absolute"です。-
"relative": (推奨) 画像の保存先をプロジェクトルートからの相対パスに指定します。 -
"absolute": 画像の保存先を絶対パスに指定します。
-
これらの設定は、mdxsnap.nvimが他のCMSなどへの対応といった柔軟性に貢献している機能です。
Zennでmdxsnapをつかう
mdxsnapは仕組み上、Zennの記事を書くことにも貢献できます。現にこの記事の画像挿入はmdxsnapで行われています。
Zenn CLI向けのサンプルは以下の通りです。ProjectOverridesにこの記述を追記したのちに、matchValueの"zenn-articles"という値をご自身のリポジトリ名にご変更ください。
{
matchType = "projectName",
matchValue = "zenn-articles", -- この項目をあなたのリポジトリ名に置き換えてください。
-- :%s/zenn-articles/YOUR_REPO_NAME/g
PastePath = "images",
PastePathType = "relative",
}, -- ProjectOverrides に追加
- Directory Structure
.
├── articles
│ └── introduce-mdxsnapnvim.md
├── books
├── images
│ └── introduce-mdxsnapnvim
│ ├── github.png
│ ├── neovim.png
│ ├── not-by-ai.png
│ └── zenn-setting.png
├── README.md
├── bun.lock
└── package.json
今後の課題
このプラグインはほぼ一日で完成させたものなので、どうしても粗い実装などが見つかります。v1.1.0へのリリースにあたって修正項目などをリストアップします。
-
customImportsの代入詞
customImportsでは、altテキストおよびsrc画像ファイルパスの入力に%sを仮置きの代入詞として使用していますが、あまりヒューマンフレンドリーな使用法ではないため、{{alt}}, {{src}}のような代入詞を実装する必要があります。
- ファイルコピーペーストの効率的な実装
現在、mdxsnapがコピペする際の実装の一部として、クリップボード内の画像をプラグインに受け渡しする目的と、クリップボード内のファイルをリネームするために任意のOSのコマンドで~/.local/share/nvim/mdxsnap_tmpに画像を作成した後に、luaの標準機能でファイルコピーなどを行っているため、あまり良い実装とは呼べません。vim.apiもしくはlua標準機能で外部依存を減らした実装にシフトしたいですね。OSのクリップボードツールなどを極力呼び出さずに、vim.apiのクリップボード関連ユーティリティなどがあればそちらで直接データを受け渡しできる構造に実装したいですね。(一工夫すればtempファイルを作らなくても受け渡しできそう)
また、mdxsnapを使用していてバグ発見、もしくは「こんな機能が欲しいな」「こうしたらいいじゃないの」のような提案などございましたらGitHubのissueページで気軽にご連絡ください。 ここまで記事をご覧いただきありがとうございました!
こちらのプラグインを気に入られましたら是非ともスターをお付けください ⭐️
追記 4月23日
ある日にrefactor/overall-1ブランチでコードベースを全面的にリファクタリングしました。ただ、受験勉強でE2Eテストできる余裕がないので、もしお都合がよければ代わりにテストお願いします。以下の環境でどれか一つだけでも大丈夫なので基本的な機能の確認次第owner@v-sli.meにメールをお願いします。またREADMEなどで感謝をしたいのでGitHubのIDを教えていただけると助かります🙏🏻🙏🏻
これらすべての動作確認次第masterブランチにマージします。
- macos
- wayland
- x11
- windows
Discussion