- はじめに
- bunのインストール
- 作業環境の作成
- Prettierのインストール
- Remarkのインストール
- Linterのセットアップ
- ドキュメントのリンク切れを検証する
- Linter用の設定ファイルを外部化する
- まとめ
はじめに
みなさんこんにちは、XI本部エンジニアリングオフィスの佐藤太一です。
AmazonのKiroが話題になったあたりから、AIにMarkdownで仕様書を書いてもらうことで成果物の品質や作業品質を高める取り組みが注目されていますね。
AmazonのKiroだけでなく、GitHubのSpec Kit、Claude Code Spec WorkflowやClaude Code Specなど様々な活動が公開されています。
このエントリでは、そういった活動の中で少し見過ごされがちなMarkdownファイルそのものの品質を底上げするための取り組みを紹介します。
具体的には、RemarkとPrettierを組み合わせてMarkdownファイルが所定のルールに沿った記述がされているか自動的に保証する方法を説明します。
bunのインストール
今回利用するツールはJavaScriptやTypeScriptで記述されています。
これらのツールは、継続的に繰り返し実行するため、少しでも高速に動作することが望ましいので、今回はbunを使って実行します。
Windows環境であれば以下のコマンドでインストールできます。
powershell -c "irm bun.sh/install.ps1 | iex"
LinuxやMac環境なら以下のコマンドでインストールできます。
curl -fsSL https://bun.sh/install | bash
bunは高速に動作するうえに特別な設定をせずにTypeScriptを直接実行できるので、私は日常的なスクリプトもTypeScriptで書いています。
作業環境の作成
今回は筆者の作業環境がWindowsなので、それを前提に環境構築を行います。
プラットフォーム依存性のある話は全体としてほぼ存在しませんので、LinuxやMacを使っている方は適宜読み替えてください。
作業用のディレクトリとして、C:/dev/md-tutorial
に新しいディレクトリを作成しました。
今後は、このディレクトリ内で作業しているものと考えてください。
このディレクトリに、 bunfig.toml
というファイルを以下の内容で作成します。
[install] exact = true
これによって、bunでインストールするツールのバージョンが常に固定されたものになります。
Prettierのインストール
Prettierは、htmlやJavaScript、TypeScriptなど最近のフロントエンド開発で使われているテキストファイルのフォーマットを自動的に行う意見の強いフォーマッタです。
私自身は、全てのソースコードはフォーマットをすべきであると考えています。
ただし、一貫性さえあればフォーマットのルールについてはあまりこだわりがないので、Prettierを常用しています。
以下のコマンドを実行して、Prettierをbunでインストールしましょう。
bun add prettier
以下のように package.json
が作成されます。
{ "dependencies": { "prettier": "3.6.2" } }
これにbunからPrettierを実行するためのコマンドを追加します。
{ "dependencies": { "prettier": "3.6.2" }, "scripts": { "lint:prettier": "prettier --check docs/", "format:prettier": "prettier --write docs/" } }
今回はPrettierで検査対象にするファイルは docs/
ディレクトリに格納されるものとしてタスクを定義しました。
では、docs/
ディレクトリに README.md
を以下の内容で作成しましょう。
# Title Long Long Long Long Long Long Long Long Long Long Long Long Long Long Long Long Long Long Body Text
Prettierの動作確認をしたいので、少し奇妙なファイルです。
タイトルの後ろに無意味な半角スペースがあったり、空行のみの行がスペースでインデントされていたりします。
この状態でPrettierを実行してみましょう。以下のコマンドを実行します。
bun lint:prettier
以下のように出力されました。
ファイルに問題があるようですね。しかし、今回の問題は自動的に対処できるものです。
では、以下のコマンドを実行しましょう。
bun format:prettier
実行結果は以下のように出力されます。
自動的な折り返しはされないようですね。しかし、余分な半角スペースは全て除去されています。
Prettierは基本的に設定の調整が不要なのですが、いくつか私の趣味に合わない部分があるので、そこだけは変えましょう。具体的には、タブと一行の最大幅と改行コードです。
タブは半角スペース2つに変換します。一行の幅はデフォルトの80文字から120文字に増やしましょう。改行コードはLFのみに固定します。
{ "dependencies": { "prettier": "3.6.2" }, "prettier": { "tabWidth": 2, "printWidth": 120, "endOfLine": "lf" }, "scripts": { "lint:prettier": "prettier --check docs/", "format:prettier": "prettier --write docs/" } }
あわせて、 .gitattributes
ファイルを以下の内容で作成します。
text=auto eol=lf
Remarkのインストール
RemarkはJavaScriptで実装されたMarkdownの処理ツールです。たくさんのプラグインがあり、処理ツールとしての機能を調節できるようになっています。
また、パーズしてASTになったMarkdownをファイルに書き出せるような形式に永続化する機能もあります。
パーザのセットアップ
まずは、Remarkのパーザ部分をセットアップしてみましょう。以下のコマンドで3つのモジュールをインストールします。
bun add remark-cli remark-frontmatter remark-gfm
あわせて、bunから呼び出せるようにコマンドを追加します。
{ "dependencies": { "prettier": "3.6.2", "remark-cli": "12.0.1", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.1" }, "prettier": { "tabWidth": 2, "printWidth": 120, "endOfLine": "lf" }, "scripts": { "lint:md": "remark --frail docs/", "lint:prettier": "prettier --check docs/", "format:md": "remark docs/ --frail --output", "format:prettier": "prettier --write docs/" } }
おっと、Remarkが追加したモジュールを動作するように構成していませんでした。
{ "dependencies": { "prettier": "3.6.2", "remark-cli": "12.0.1", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.1" }, "prettier": { "tabWidth": 2, "printWidth": 120, "endOfLine": "lf" }, "remarkConfig": { "plugins": [ "remark-frontmatter", "remark-gfm" ] }, "scripts": { "lint:md": "remark --frail docs/", "lint:prettier": "prettier --check docs/", "format:md": "remark docs/ --frail --output", "format:prettier": "prettier --write docs/" } }
remarkConfigにpluginsというプロパティがあるので、そこに追加したいプラグインのモジュール名を列挙しています。今回は remark-frontmatter と remark-gfm ですね。
動作を確認していきましょう。とはいえ、既に用意した docs/README.md では何も起きないので新しいファイルを追加します。
docs/list.md というファイルを以下の内容で保存します。
--- author: taichi category: - example - sample --- # Title _foo bar baz_ ## Sub Title - [ ] one - [ ] two - [ ] three foo bar baz 1. first 1. second 1. third ----
frontmatterというYAMLでメタデータを付けたMarkdownですね。Listの部分では、GitHub固有のチェックボックス記法を使っています。
Remarkの動作を確認してみましょう。以下のコマンドを実行します。
bun lint:md
特にエラーなく処理が終了するでしょう。
次は、フォーマットタスクを実行しましょう。以下のコマンドです。
bun format:md
docs/list.md を開きなおしてみてください。興味深い変化が起きているはずです。
--- author: taichi category: - example - sample --- # Title *foo bar baz* ## Sub Title * [ ] one * [ ] two * [ ] three foo bar baz 1. first 2. second 3. third ***
frontmatter部分に変化はありません。
次のTitleより下のイタリック体になっていた記号が変化しています。フォーマット前は _foo bar baz_
とアンダースコア記号を使っていましたが、フォーマット後は *foo bar baz*
とアスタリスクが使われています。
リスト記法の -
も *
に変化しています。同様に区切り線として、ハイフンを使っていたので ----
でしたが、フォーマット後は ****
になっています。最後の順序付きリストは、数字が昇順に調整されています。
これがRemarkに内蔵されている remark-stringify の正常な動作です。MarkdownをパーズしてASTにした後、メモリ上の表現を一定のルールで文字列に永続化するわけです。
このままでは、少し都合が悪いので調整しましょう。remarkConfigに settings プロパティを追加します。
{ "dependencies": { "prettier": "3.6.2", "remark-cli": "12.0.1", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.1" }, "prettier": { "tabWidth": 2, "printWidth": 120, "endOfLine": "lf" }, "remarkConfig": { "settings": { "bullet": "-", "emphasis": "_", "rule": "-" }, "plugins": [ "remark-frontmatter", "remark-gfm" ] }, "scripts": { "lint:md": "remark --frail docs/", "lint:prettier": "prettier --check docs/", "format:md": "remark docs/ --frail --output", "format:prettier": "prettier --write docs/" } }
ここでは、
- リストに使う記号として
-
- 斜体やボールドなどを強調する際に使う記号として
_
- 区切り線として使う記号として
-
を設定しています。
では、もう一度フォーマッタを動かしてみましょう。
bun format:md
処理は成功し以下のように設定に基づいた表現になっているはずです。順序付きリストだけが昇順のままですね。
--- author: taichi category: - example - sample --- # Title _foo bar baz_ ## Sub Title - [ ] one - [ ] two - [ ] three foo bar baz 1. first 2. second 3. third ---
これで分かったように、RemarkをMarkdownのフォーマッタとして使う場合には、remarkConfigオブジェクトのsettingsプロパティを調整します。その設定項目は、remark-stringify#options に記載されています。
RemarkとPrettierの整合性を取る
Prettierはテキストファイルのフォーマットを整えるツールですが、実はRemarkのLintルールの中には、Prettierの動作と矛盾するものや重複するものが含まれています。
Prettierとうまく組み合わさらないものは、あらかじめ無効にしてしまいましょう。そのためのモジュールを以下のコマンドでインストールします。
bun add remark-preset-prettier
モジュールを追加したら、そのモジュール名をプラグインとして追加します。以下のようになるでしょう。
{ "dependencies": { "prettier": "3.6.2", "remark-cli": "12.0.1", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.1", "remark-preset-prettier": "2.0.2" }, "prettier": { "tabWidth": 2, "printWidth": 120, "endOfLine": "lf" }, "remarkConfig": { "settings": { "bullet": "-", "emphasis": "_", "rule": "-" }, "plugins": [ "remark-frontmatter", "remark-gfm", "remark-preset-prettier" ] }, "scripts": { "lint:md": "remark --frail docs/", "lint:prettier": "prettier --check docs/", "format:md": "remark docs/ --frail --output", "format:prettier": "prettier --write docs/" } }
次はフォーマットしがいのあるコンテンツを用意しましょう。docs/table.md として以下のようなファイルを作成します。
# Table
| Column1 | Label | Description |
| ----- | ----- | ----- |
| val | aaaaaaa | Lorem ipsum dolor sit amet, consectetur adipiscing elit |
| valval | aa | sed do eiusmod tempor incididunt ut labore |
| foo | d | Duis aute irure dolor in reprehenderit in voluptate |
| xxxxxxx | | deserunt mollit anim id est laborum |
Prettierでフォーマットするコマンドは以下のとおりです。
bun format:prettier
コマンドが正常に終了したら、どのようにフォーマットされるか確認してみましょう。
# Table
| Column1 | Label | Description |
| ------- | ------- | ------------------------------------------------------- |
| val | aaaaaaa | Lorem ipsum dolor sit amet, consectetur adipiscing elit |
| valval | aa | sed do eiusmod tempor incididunt ut labore |
| foo | d | Duis aute irure dolor in reprehenderit in voluptate |
| xxxxxxx | | deserunt mollit anim id est laborum |
フォーマットする前のテーブルは正直言ってHTMLなりエディタのプレビュー機能なりを使わなければ内容が把握できないものでしたが、Prettierのフォーマットによりテキスト表現だけでも読めるようになりましたね。
Remarkの一部としてPrettierを動かす
RemarkによるフォーマットとPrettierによるフォーマットを毎回動かすのはやや面倒ですし、何かの拍子に抜けてしまいそうです。
というわけで、Remarkの処理の最後にPrettierを動かすためのプラグインを導入しましょう。
bun add unified-prettier
これまで通り、pluginsに追加ですね。出力用のプラグインなので remark-preset-prettier よりも後ろに配置します。
{ "dependencies": { "prettier": "3.6.2", "remark-cli": "12.0.1", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.1", "remark-preset-prettier": "2.0.2", "unified-prettier": "2.0.1" }, "prettier": { "tabWidth": 2, "printWidth": 120, "endOfLine": "lf" }, "remarkConfig": { "settings": { "bullet": "-", "emphasis": "_", "rule": "-" }, "plugins": [ "remark-frontmatter", "remark-gfm", "remark-preset-prettier", "unified-prettier" ] }, "scripts": { "lint:md": "remark --frail docs/", "lint:prettier": "prettier --check docs/", "format": "remark docs/ --frail --output" } }
PrettierとRemarkによるフォーマットは統合されたので、コマンド名はシンプルな format
に変更しています。
Linterのセットアップ
それでは、いよいよLinter機能をRemarkに追加しましょう。
Remarkが公式で提供しているLinterは、remark-lint/packagesに並んでいるのですが、この細かいルールを一つずつ選定していくのは面倒です。複数のルールを束にしたプリセットを公式から提供されていますので、それを使いましょう。
- remark-preset-lint-recommended
- Markdownを使ううえで基本的なルールの集合です。今回はこれを採用します。
- remark-preset-lint-consistent
- ドキュメント内で記法が一貫するように強制するルールの集合です。
- それぞれの記法において、ファイル内で最初に登場した記法が一貫して使われているかどうかを検証できます。
- 今回は新規のプロジェクトを前提にしますので不採用です。
- remark-preset-lint-markdown-style-guide
- Markdown Style Guideという極めて強い意見のMarkdownに関するスタイルガイドを採用したルールです。
- 今回はそれほど多くのドキュメントがあるわけではないので不採用です。
まずは、モジュールをインストールします。
bun add remark-preset-lint-recommended
モジュールをインストールしたらプラグインとして追加します。
{ "dependencies": { "prettier": "3.6.2", "remark-cli": "12.0.1", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.1", "remark-preset-lint-recommended": "7.0.1", "remark-preset-prettier": "2.0.2", "unified-prettier": "2.0.1" }, "prettier": { "tabWidth": 2, "printWidth": 120, "endOfLine": "lf" }, "remarkConfig": { "settings": { "bullet": "-", "emphasis": "_", "rule": "-" }, "plugins": [ "remark-frontmatter", "remark-gfm", "remark-preset-lint-recommended", "remark-preset-prettier", "unified-prettier" ] }, "scripts": { "lint:md": "remark --frail docs/", "lint:prettier": "prettier --check docs/", "format": "remark docs/ --frail --output" } }
Remarkのプラグインシステムは、配列の後ろ側になるほどルールとして優先されますので、今回追加するプリセットはちょうど中ほどに入るように追加します。
lintのルールを追加したのですから、エラーが発生するようなMarkdownを作りましょう。docs/link.md を以下の内容で作成します。
# Links https://example.com
リンク記法が使われずにリンクのようなものがテキストとして記述されていますので、これは警告されるはずです。
Linterを実行するコマンドは以下のとおりです。
bun lint:md
実行すると確かに警告が出力され、プロセスがエラーで終了します。
リンク記法を使うべきであるという警告が出力されていますね。これが自動的に改善されるかどうか確認してみましょう。
bun format
Linterを実行したときと同じエラーが出力されていますね。
では、Markdownファイルはどうなっているのでしょうか。
# Links <https://example.com>
リンクが <>
でくくられていますね。自動修正は機能しているようです。ただ、AIを使った開発においては、この出力だとAIが標準出力されたメッセージを根拠に解決を試みます。
つまり、当該ファイルを開くものの実際には修正済みという状態になってしまいますので、一定の混乱が発生します。
通信したトークン量で課金されたり、制限される今のAI相手にはこういう無駄が起きないようにしなければなりません。
この問題に対応するために設定を調整してみましょう。pluginsプロパティにモジュールを列挙すると常に使われてしまうので、Linterを動かすときとフォーマッタを動かすときで使うモジュールを切り替える必要があります。
remarkコマンドの -u
オプションを使うと個別にモジュールを指定できます。
{ "dependencies": { "prettier": "3.6.2", "remark-cli": "12.0.1", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.1", "remark-preset-lint-recommended": "7.0.1", "remark-preset-prettier": "2.0.2", "unified-prettier": "2.0.1" }, "prettier": { "tabWidth": 2, "printWidth": 120, "endOfLine": "lf" }, "remarkConfig": { "settings": { "bullet": "-", "emphasis": "_", "rule": "-" }, "plugins": [ "remark-frontmatter", "remark-gfm" ] }, "scripts": { "lint:md": "remark --frail docs/ -u remark-preset-lint-recommended -u remark-preset-prettier", "lint:prettier": "prettier --check docs/", "format": "remark docs/ --frail --output -u remark-preset-prettier -u unified-prettier" } }
この設定では、Linterとフォーマッタで共通して使う部分はremarkConfig以下に定義し、違う部分は各コマンドの引数として定義しています。
それでは、確認のため、補正済みのdocs/link.md を補正前に戻したうえで、Linterを動かしてみましょう。
エラーが出力されていますね。
では、フォーマッタを動かしてみましょう。
エラーや警告が出力されないので、プロセスは正常に終了します。
そして、Markdownは自動補正されて正しくフォーマットされていますね。
# Links <https://example.com>
ドキュメントのリンク切れを検証する
設計ドキュメントとしてMarkdownを使うということは、役割や責任範囲毎にファイルを分割していき、それらをハイパーリンクで接続するという使い方が想定されます。
そうしたとき問題になるのが、ドキュメント間のリンク切れ問題です。ファイル名やファイルパスを変更することによって、リンク切れは頻繁に発生します。これをきちんとメンテナンスしていくのは、非常に面倒なタスクですよね。
そこで、Linterの一部としてリンク切れをチェックしエラーを標準出力することで、リンクのメンテナンスを生成AIに任せましょう。
Remarkにはリンク切れを検証するプラグインがいくつかあるので、それらをまとめてインストールしましょう。
bun add remark-validate-links remark-lint-no-dead-urls
remark-validate-links が同じディレクトリツリー内にあるファイルへのリンクを検証するプラグインです。それに対して、remark-lint-no-dead-urls はリンクとして記載されているURLにHTTPリクエストを送信して機能しているか検証するプラグインです。
これをLinter用のコマンドに組み込むとこうなります。
{ "dependencies": { "prettier": "3.6.2", "remark-cli": "12.0.1", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.1", "remark-lint-no-dead-urls": "2.0.1", "remark-preset-lint-recommended": "7.0.1", "remark-preset-prettier": "2.0.2", "remark-validate-links": "13.1.0", "unified-prettier": "2.0.1" }, "prettier": { "tabWidth": 2, "printWidth": 120, "endOfLine": "lf" }, "remarkConfig": { "settings": { "bullet": "-", "emphasis": "_", "rule": "-" }, "plugins": [ "remark-frontmatter", "remark-gfm" ] }, "scripts": { "lint:md": "remark --frail docs/ -u remark-preset-lint-recommended -u remark-preset-prettier -u remark-validate-links=repository:false -u remark-lint-no-dead-urls=deadOrAliveOptions:{timeout:10000}", "lint:prettier": "prettier --check docs/", "format": "remark docs/ --frail --output -u remark-preset-prettier -u unified-prettier" } }
Linter用のコマンドが長くなり始めていますね。必要な所だけ抜粋して説明します。
内部リンクを検証するプラグインの設定部分がこれです。
-u remark-validate-links=repository:false
remarkコマンドのオプションとして、モジュール毎の設定を記述するには、モジュール名の最後に =
を付けたうえで、最外周の {}
がないJSON5を記述します。
remark-validate-linksは、検証対象がgitリポジトリでかつoriginが設定されていることを前提に動作するので、その機能を無効化するために repository:false
を設定しています。
普段ならこの設定は不要でしょう。
外部リンクを検証するプラグインの設定部分がこれです。
-u remark-lint-no-dead-urls=deadOrAliveOptions:{timeout:10000}
remark-lint-no-dead-urls は dead-or-alive というライブラリを内部的に使っています。そのタイムアウトオプションをここでは設定しています。
これらのプラグインが機能するか確認するために、先ほどの docs/links.md に手を加えて内部リンクと外部リンクを追加してみましょう。
# Links <https://example.com> ## Internal Links * [Exists](#Links) * [NotExists](#likns) ## External Links * [External Example](https://example.com) * [NotExists External](https://example.com/fail)
では、リンク切れを検証してみましょう。以下のコマンドを実行します。
bun lint:md
実行結果はエラー終了になります。
タイポしているページ内のリンクと、存在しない外部ページに対するリンクが警告やエラーとして検出されていますね。
Linter用の設定ファイルを外部化する
CLIオプションとしてJSON5を記述するというのは、分かり辛いので設定ファイルを外部化しましょう。
それではフォーマッタの設定から分離します。設定ファイルを config/remark-format.json
に作成してください。
{ "settings": { "bullet": "-", "emphasis": "_", "rule": "-" }, "plugins": [ "remark-frontmatter", "remark-gfm", "remark-preset-prettier", "unified-prettier" ] }
フォーマッタの設定はパーザとシリアライザだけなのでシンプルですね。
次にLinterの設定ファイルを分離します。 config/remark-lint.json
に作成します。
{ "settings": { "bullet": "-", "emphasis": "_", "rule": "-" }, "plugins": [ "remark-frontmatter", "remark-gfm", "remark-preset-lint-recommended", "remark-preset-prettier", [ "remark-validate-links", { "repository": false } ], [ "remark-lint-no-dead-urls", { "deadOrAliveOptions": { "timeout": 10000 } } ] ] }
このエントリではやりませんが、Linter側の設定はまだかなり調整の余地があるでしょう。
最後にpackage.jsonから不要になった設定を削除して、コマンドを調整します。
{ "dependencies": { "prettier": "3.6.2", "remark-cli": "12.0.1", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.1", "remark-lint-no-dead-urls": "2.0.1", "remark-preset-lint-recommended": "7.0.1", "remark-preset-prettier": "2.0.2", "remark-validate-links": "13.1.0", "unified-prettier": "2.0.1" }, "prettier": { "tabWidth": 2, "printWidth": 120, "endOfLine": "lf" }, "scripts": { "format": "remark docs/ --frail --output -r config/remark-format.json", "lint": "remark --frail docs/ -r config/remark-lint.json" } }
Prettierを直接利用するケースは基本的にありませんので、あわせてコマンドをシンプルにしておきました。
まとめ
今回のエントリでは、順を追ってPrettierの導入からRemarkによるフォーマット、その後Linterを導入し、最後は設定ファイルを切り出して調整しやすい状態にしました。
ここで紹介した手法を使えば、生成AIが出力する大量のテキストファイルのファイルフォーマットや構成を一定の水準に保てるようになります。
生成AIは出力した文章の中身について理解しておらず、それっぽいものを出力するのみですが、こうやって自動的に対処できる部分については、AIに対処してもらうことで私たちはより意味のあるレビューを行えるようになるはずです。
このエントリを読んだ皆さんが、より高品質なドキュメントを元にソフトウェア開発が実施できるようになることを願っています。
執筆:@sato.taichi
レビュー:@yamashita.tsuyoshi
(Shodoで執筆されました)