マークダウン執筆で画像挿入が面倒だったのでNeovimプラグインを開発した話
はじめに、多くのVimmer
はZenn
や個人ブログの記事を執筆するときもVim
で執筆すると思います。もちろん手慣れたテキストエディターなのでサクサク記事を書けますが、ただ唯一の欠点と言っていいほどに画像の挿入の面倒臭さに悩まされました。
従来の画像挿入、例えばZenn
の記事を執筆する際の画像挿入をワークフローをあげると、私は以下の工程で画像の追加をしていました。
1. ファイルマネージャーで画像ディレクトリを開く
2. 画像ディレクトリにインターネットからダウンロードしていた画像を任意の名前で保存する
3. Neovimで画像要素を手打ちする
この通りに三工程あって画像を挿入するだけのことに一苦労です。今回はこのワークフローをNeovim
から離れずにコマンド一つで行えるようにしたプラグインを開発しました。
mdxsnap.nvim
mdxsnap.nvim
はクリップボードに保存された画像をマークダウンもしくは、MDX形式の文書にコピペするプラグインです。
注目すべき特徴は主にこれら5つが挙げられます。
- 覚えるコマンドは一つだけ
:PasteImage [filename]
- プロジェクト単位でのプロファイル分け (
ProjectOverrides
) - 画像の保存場所に任意のディレクトリパスを使用可能 (
PastePath
,DefaultPastePath
) - Markdownの
![]()
画像形式構文を任意の形式に置き換える (customTextFormat
) - MDXで画像用のコンポーネントや関数が不足している際に自動インポート (
customImports
)
これらの機能によって、様々なディレクトリ構造を取る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ステップ踏まないと画像挿入がご覧の通り、たった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
ファイル作らなくても受け渡しできそう)
-
macOS
環境でgif
画像を挿入できない
macOS
でのクリップボード周りの実装は、クリップボード内のデータをそのまま出力する方法が分からなかったので、osascript
でpng
形式で出力して受け渡ししてしまっているため、gif
画像を貼り付けると静止画になってしまいます。私のリサーチ不足でこのような実装になってしまったので悔やむ限りです。
- そもそもコードが汚い
はじめて作ったNeovim
プラグインにしてもコードがとても汚いです。手っ取り早く作りたかったためVibeCoding
をしましたが、ノウハウなどがないためLLM
に対して満足にカウンセリングもできずご覧の次第です。これからじっくりコードリーディングを行って私主体で改修を進めたいと考えています。
また、mdxsnap
を使用していてバグ発見、もしくは「こんな機能が欲しいな」「こうしたらいいじゃないの」のような提案などございましたらGitHub
のissue
ページで気軽にご連絡ください。 ここまで記事をご覧いただきありがとうございました!
こちらのプラグインを気に入られましたら是非ともスターをお付けください ⭐️
Discussion