VSCodeでマークダウンをフォーマットするマクロ

この記事は公開から3年以上経過しています。

VSCodeでマークダウン(以後Markdown)をフォーマットする拡張はマーケットプレイスにも幾つかあるのですが、日本語環境で使用すると全角半角混在でテーブルのパディングがずれたり、Markdown Preview Enhanced拡張が使用するFrontMatter(YAML)が壊れてしまうなど、なかなか私の要件を満たしてくれるものがありませんでした。

そこで、今回は私の個人的な要件がほぼ満たせて、かつ適宜カスタマイズ可能なMarkdownフォーマッタをVSCode Macros拡張のマクロで自作してみたので紹介します。

自作といっても他力本願先人の素晴らしいremarkJSライブラリの力です。

本記事のマクロを利用する場合、システムに予めnode.jsを導入(インストール)しておく必要があります。


npmライブラリのインストール手順

以下の手順で、マクロから利用するnpmライブラリを導入しておきます。

  1. マクロディレクトリにpackage.jsonが存在しない場合は、カレントをマクロディレクトリに移動して以下のコマンドでVSCodeMacrosで利用するプライベートnpmライブラリ用のpackage.jsonを生成します(npmライブラリをグローバルインストールする場合は不要)。

    echo '{"private":true}' >> package.json

    ※Windowsのコマンドプロンプト(cmd.exe)の場合はシングルクォート'は不要。

  2. 以下のコマンドでremarkJSとプラグインモジュールをインストールします。
    remark version 13以上の場合はremark-frontmatter version 3以上が必要です(バージョンが合っていないとremarkJSで処理が失敗します)。

    npm i remark@13 remark-parse remark-stringify remark-gfm remark-frontmatter@3


マクロのサンプルソースコード

const vscode = require('vscode');
const remark = require('remark'); // Requires version 13 or above
const remarkParse = require('remark-parse');
const remarkStringify = require('remark-stringify');
const remarkGFM = require('remark-gfm');
const remarkFrontmatter = require('remark-frontmatter'); // Requires version 3 or above

module.exports.macroCommands = {
    Markdownフォーマット: {
        no: 1,
        func: formatMD,
    },
};

function formatMD() {
    const editor = vscode.window.activeTextEditor;
    if (!editor) {
        vscode.window.showWarningMessage('Editor not found.');
        return;
    }
    const document = editor.document;
    if (!document) {
        vscode.window.showWarningMessage('Document not found.');
        return;
    }
    if (document.languageId !== 'markdown') {
        vscode.window.showWarningMessage(`This document type '${document.languageId}' is not supported.`);
        return;
    }

    // 東アジア文字幅の文字を定義(例はμ)
    const eastAsianAmbiguous = Array.from('μ').map(s => s.codePointAt(0));
    // Markdownフォーマットオプション(https://github.com/syntax-tree/mdast-util-to-markdown#formatting-options)
    const remarksOptions = {
        bullet: '-',
        listItemIndent: 'one',
        rule: '-'
    };

    const srcMarkdown = document.getText();
    // Markdownテキストを構文木に変換
    const mdNode = remark()
        .use(remarkGFM)
        .use(remarkParse)
        .use(remarkFrontmatter)
        .parse(srcMarkdown);
    // 構文木をMarkdownテキストに変換(フォーマット)
    const dstMarkdown = remark()
        .use(remarkGFM, {
            // セル内の文字幅の計算(ASCII/半角カナ/東アジア文字幅の文字は1文字として扱う)
            stringLength: (s) => {
                let len = 0;
                Array.from(s).forEach((elem) => {
                    let cpv;
                    const codePointValue = (cpv = elem.codePointAt(0)) !== undefined ? cpv : 0;
                    if (codePointValue <= 255 || (codePointValue >= 0xff61 && codePointValue <= 0xff9f || eastAsianAmbiguous.includes(codePointValue)))
                        ++len;
                    else
                        // Double width
                        len += 2;
                });
                return len;
            },
        })
        .use(remarkStringify, remarksOptions)
        .use(remarkFrontmatter)
        .stringify(mdNode);

    if (srcMarkdown === dstMarkdown) {
        vscode.window.showInformationMessage('Document is already formatted.');
        return;
    }

    editor.edit((editBuilder) => {
        // フォーマット済みMarkdownに置換
        const firstLine = document.lineAt(0);
        const lastLine = document.lineAt(document.lineCount - 1);
        editBuilder.replace(new vscode.Range(firstLine.range.start, lastLine.range.end), dstMarkdown);
        vscode.window.showInformationMessage('Format completed.');
    });
}

GitHub Gist

セルのパディング幅計算は東アジア文字幅とエディタフォントで変わりますので、お使いの環境に合わせて修正してください。


使い方

フォーマットしたいMarkdownドキュメントを開いた状態でこのマクロを実行すると、フォーマットされたドキュメントで全置換を行います。


原理と注意点

Markdowをパースして構文木に変換してジェネレーターで再構築することでフォーマットを行います。
Markdownには所謂方言が幾つかあるのですがライブラリを使用する場合はジェネレーターで決まりますので、このフォーマッタの場合はCommonMark+GFM(GitHub Flavored Markdown)です。

remarkJSでフォーマットを行った際に「GFMのテーブルセル内でアンダースコアを表示するために以下のようにバックスラッシュでエスケープを行うとフォーマット時にエスケープが解除(バックスラッシュが削除)されてしまう」という既知の問題が確認されているため、ご注意ください。
| # | data |
| – | ——– |
| 1 | \_TEST\_ |
根本的な解決方法をご存知の方がいらっしゃいましたら、コメントなど頂けますと幸いです。


参考ウェブサイトなど

以上です。

シェアする

  • このエントリーをはてなブックマークに追加

フォローする