(Neo)VimでAsciiDoc(+PlantUML埋め込み)をDenops+Honoを使ってリアルタイムプレビューする
はじめに
AsciiDocは、軽量なマークアップ言語の1つで、標準のマークダウンより表現力が高く便利です。GitHubのマークダウンがMermaidを対応したこともあり、軽いシーケンス図はGitHub上で書けるので最近あまり使っていませんでした。
ただPlantUMLの表現力の高さが必要になり、埋め込みが可能なAsciiDocを利用することになりました。2020年頃NeovimでPlantumlをプレビューするプラグインを作っていたのですが、メンテしきれず現代のやりやすい技術構成で作り直しました。
プラグイン開発初心者向けの記事なので、その点ご了承頂けると幸いです。実装もかなり薄いので逆にこれからトライしたい方には、読みやすいとは思います。
モチベーション
AsciiDocと埋め込まれたPlantUMLのプレビューをする必要がありました。プレビューツールの中には、PlantUMLの内容をplantuml.com
へ送信するケースがありますが、セキュアではないため使えません。asciidoctor-diagram
を利用した場合、plantuml.jar
が自動でインストールされ、ローカルで完結したプレビューが可能です。
構成
リポジトリは、shuntaka9576/preview-asciidoc.vimです。
要素 | 昔版(2020年) | 今回版 |
---|---|---|
プラグイン機構 | リモートプラグイン(MessagePack-RPC) | Denops |
言語 | Node.js | Deno |
Webフレームワーク | Node.jsのhttpモジュール | Hono |
仕組み
概要図
動作概要
実装関連
AsciiDocビルド関連
Vimのバッファの内容をパイプで渡して、asciidoctorにstdinで渡しています。引数で指定できればよかったのですが、オプションがなかったので断念しました。Denoでパイプを繋げたコマンドを実行する場合、少し癖がありました。
echo 'バッファの内容' |\ asciidoctor \ -a imagesdir@=images \ -a imagesoutdir=${config.imagesDir} \ -r asciidoctor-diagram \ -o - -;
const getDefaultShell = (): string => { let shell = "bash"; const envShell = Deno.env.get("SHELL"); if (envShell != null) { shell = envShell; } return shell; }; const shell = getDefaultShell(); const p = Deno.run({ cmd: , stdout: "piped", stdin: "piped", // stdinをpipedにしないと、p.stdinがundefined属性になる(そもそも標準入力を許可しないことになり、受け取れないので、設定は必ずする) }); const encoder = new TextEncoder(); const decoder = new TextDecoder(); const command = "echo '=== aaa' | asciidoctor -r asciidoctor-diagram -o - -"; await p.stdin.write(encoder.encode(command)); p.stdin.close(); // 標準入力を解放したら閉じないと、永遠に入力待ちでブロックした状態になる const output = await p.output(); p.close(); console.log(decoder.decode(output));
パスルーティング
PlantUMLの画像はプラグインのパス/images
に保存されます。localhost:8064/images/*
とプラグインのパス/images
をマッピングする必要があります。
const initializeWebSocketServer = async (config: Config) => { let port = 8064; const hostname = "localhost"; const app = new Hono(); app.get("/ws", (c: any) => { const { response, socket } = Deno.upgradeWebSocket(c.req); socket.onopen = () => {}; socket.onclose = () => {}; socket.onmessage = (_message) => {}; sockets.add(socket); return response; }); app.get("/", (c) => { return c.html(template({ port: port })); }); app.get("/images/*", async (c) => { const url = new URL(c.req.url); const path = join(config.pluginPath, url.pathname); const content = await Deno.readFile(path); const mimeType = getMimeType(path); if (mimeType) { c.header("Content-Type", mimeType); } return c.body(content); }); for (let retry = 0; retry < 3; retry++) { try { const server = new Server({ port: port, hostname: hostname, handler: app.fetch, }); Console.log(`preview port: ${port}`); await open(`http://${hostname}:${port}`); return await server.listenAndServe(); } catch (e) { if (e.code === "EADDRINUSE") { Console.log(`already in use ${port} port. retry ${retry}`); port += 1; continue; } else { throw e; } } } };
最後に
WebSocketとの連携も、パスルーティングもHonoでサクッと書けてよかったです!この使用感があらゆるランタイムやクラウド上で動作するのは便利ですね!Denopsも分かりやすく、denoもライブラリが増えてきたこともあり快適に開発できました。npmモジュールも使えるので幅が広がりますね。
ツール自体は個人でメンテしているので、問題があればリポジトリissueにお願いします。