MarkdownをHTMLに変換する

前のセクションではコマンドライン引数で受け取ったファイルを読み込み、標準出力に表示しました。 次は読み込んだMarkdownファイルをHTMLに変換して、その結果を標準出力に表示してみましょう。

markedパッケージをインストールする

MarkdownをHTMLへ変換するために、今回はmarkedというライブラリを使用します。 markedのパッケージはnpmで配布されているので、npm installコマンドを使ってインストールできます。 まだ、package.jsonを作成していない場合は、先に「Node.jsプロジェクトのセットアップ」を参照してください。

それでは、npm installコマンドを使ってmarkedパッケージをインストールします。 このコマンドの引数にはインストールするパッケージの名前とそのバージョンを@記号でつなげて指定できます。 バージョンを指定せずにインストールすれば、その時点での最新の安定版が自動的に選択されます。 次のコマンドを実行して、markedのバージョン14.0をインストールします。1

$ npm install marked@14.0

インストールが完了すると、package.jsonファイルは次のようになっています。

package.json

{
  "name": "nodecli",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "marked": "^14.0.0"
  }
}

また、npm installをすると同時にpackage-lock.jsonファイルが生成されています。 このファイルはnpmがインストールしたパッケージの、実際のバージョンを記録するためのものです。 先ほどmarkedのバージョンを14.0としましたが、実際にインストールされるのは14.0.xに一致する最新のバージョンです。 package-lock.jsonファイルには実際にインストールされたバージョンが記録されています。 これによって、再びnpm installを実行したときに、異なるバージョンがインストールされるのを防ぎます。

インストールが完了したら、Node.jsのスクリプトから読み込みます。 前のセクションの最後で書いたスクリプトに、markedモジュールの読み込み処理を追加しましょう。 次のようにmain.jsを変更し、読み込んだMarkdownファイルをmarkedを使ってHTMLに変換します。 markedモジュールからインポートしたmarked.parse関数は、Markdown文字列を引数にとり、HTML文字列に変換して返します。

main.js

import * as util from "node:util";
import * as fs from "node:fs/promises";
// markedモジュールからmarkedオブジェクトをインポートする
import { marked } from "marked";

const {
    positionals
} = util.parseArgs({
    allowPositionals: true,
});
const filePath = positionals[0];
fs.readFile(filePath, { encoding: "utf8" }).then(file => {
    // MarkdownファイルをHTML文字列に変換する
    const html = marked.parse(file);
    console.log(html);
}).catch(err => {
    console.error(err.message);
    process.exit(1);
});

変換オプションを作成する

markedにはMarkdownの変換オプションがあり、オプションの設定によって変換後のHTMLが変化します。 そこで、アプリケーション中でオプションのデフォルト値を決め、さらにコマンドライン引数から設定を切り替えられるようにしてみましょう。

今回のアプリケーションでは、例としてgfmというmarkedのオプションを扱います。

gfmオプション

gfmオプションは、GitHubにおけるMarkdownの仕様(GitHub Flavored Markdown, GFM)に合わせて変換するかを決めるオプションです。 markedではこのgfmオプションがデフォルトでtrueになっています。GFMは標準的なMarkdownにいくつかの拡張を加えたもので、代表的な拡張がURLの自動リンク化です。 次のようにsample.mdを変更し、先ほどのスクリプトとgfmオプションをfalseにしたスクリプトで結果の違いを見てみましょう。

sample.md

# サンプルファイル

これはサンプルです。
https://jsprimer.net/

- サンプル1
- サンプル2

gfmオプションが有効のときは、URLの文字列が自動的に<a>タグのリンクに置き換わります。

<h1>サンプルファイル</h1>
<p>これはサンプルです。
<a href="https://jsprimer.net/">https://jsprimer.net/</a></p>
<ul>
<li>サンプル1</li>
<li>サンプル2</li>
</ul>

一方、次のようにgfmオプションをfalseにすると、単なる文字列として扱われ、リンクには置き換わりません。

main.js

import * as util from "node:util";
import * as fs from "node:fs/promises";
import { marked } from "marked";

const {
    positionals
} = util.parseArgs({
    allowPositionals: true,
});
const filePath = positionals[0];
fs.readFile(filePath, { encoding: "utf8" }).then(file => {
    // gfmオプションを無効にする
    const html = marked.parse(file, {
        gfm: false
    });
    console.log(html);
}).catch(err => {
    console.error(err.message);
    process.exit(1);
});
<h1>サンプルファイル</h1>
<p>これはサンプルです。
https://jsprimer.net/</p>
<ul>
<li>サンプル1</li>
<li>サンプル2</li>
</ul>

自動リンクのほかにもいくつかの拡張がありますが、詳しくはGitHub Flavored Markdownのドキュメントを参照してください。

コマンドライン引数からオプションを受け取る

次に、gfmオプションをコマンドライン引数で制御できるようにしましょう。 アプリケーションのデフォルトではgfmオプションを無効にした上で、次のように--gfmフラグを付与してコマンドを実行できるようにします。

$ node main.js --gfm sample.md

コマンドライン引数で--gfmのようなフラグを扱いたいときには、parseArg関数のoptionsオブジェクトに定義します。 optionsオブジェクトでは、--key=valueのようなオプションを扱うtype: "string"と、--flagのようなフラグを扱うtype: "boolean"を定義できます。 今回の--gfmフラグはtype: "boolean"で定義し、--gfmフラグがない場合のデフォルト値をfalseに設定します。

次のようにgfmフラグを定義してからコマンドライン引数をパースすると、返り値のvaluesでパース結果のオブジェクトを取得できます。

const {
    values,
    positionals
} = util.parseArgs({
    allowPositionals: true,
    options: {
        // gfmフラグを定義する
        gfm: {
            // オプションの型をbooleanに指定
            type: "boolean",
            // --gfmフラグがない場合のデフォルト値をfalseにする
            default: false,
        }
    }
});
// valuesにはオプションのパース結果がオブジェクトとして格納される
console.log(values.gfm); // --gfmフラグがあればtrue、なければfalseとなる

--gfmフラグは、ファイルパスを指定するsample.mdの前後のどちらについていても動作します。 なぜならpositionals配列には、optionsオブジェクトで定義したオプションのパース結果は含まれないためです。 process.argv配列を直接使っているとこのようなオプションの処理が面倒なので、parseArg関数のようなパース処理を挟むのが一般的です。

最後に、main.jsを次のように変更して、--gfmフラグを使ってgfmオプションを切り替えられるようにします。

main.js

import * as util from "node:util";
import * as fs from "node:fs/promises";
import { marked } from "marked";

const {
    values,
    positionals
} = util.parseArgs({
    allowPositionals: true,
    options: {
        gfm: {
            type: "boolean",
            default: false,
        }
    }
});
const filePath = positionals[0];
fs.readFile(filePath, { encoding: "utf8" }).then(file => {
    const html = marked.parse(file, {
        // gfmフラグのパース結果をオプションとして渡す
        gfm: values.gfm
    });
    console.log(html);
}).catch(err => {
    console.error(err.message);
    process.exit(1);
});

実際にMarkdownファイルを渡して、動作を確認してみましょう。

$ node main.js sample.md
<h1>サンプルファイル</h1>
<p>これはサンプルです。
https://jsprimer.net/</p>
<ul>
<li>サンプル1</li>
<li>サンプル2</li>
</ul>

また、--gfmフラグを付与して実行すると次のように出力されるはずです。 GFMオプションが有効になっているため、URLがリンクに変換されていることが確認できます。

$ node main.js --gfm sample.md
<h1>サンプルファイル</h1>
<p>これはサンプルです。
<a href="https://jsprimer.net/">https://jsprimer.net/</a></p>
<ul>
<li>サンプル1</li>
<li>サンプル2</li>
</ul>

これでMarkdown変換の設定をコマンドライン引数で与えられるようになりました。 次のセクションではアプリケーションのコードを整理し、最後にユニットテストを導入します。

[コラム] node: プリフィックス

Node.jsの標準モジュールは、node:utilnode:fsのようにnode:というプリフィックスがモジュール名についています。 このnode:プリフィックスは後から導入された仕組みであるため、utilfsのようにプリフィックスなしでもモジュールを読み込むことができます。

しかしながら、node:プリフィックスがあることでnpmからインストールしたサードパーティ製のモジュールとの区別が明確になるため、付けておくことが推奨されます。

このセクションのチェックリスト

  • markedパッケージを使ってMarkdown文字列をHTML文字列に変換した
  • コマンドライン引数でmarkedの変換オプションを設定した
  • --gfmフラグを使って、Markdownの変換結果が変わることを確認した
1. --saveオプションをつけてインストールしたのと同じ意味。npm 5.0.0からは--saveがデフォルトオプションとなりました。