HonKitで任意のMarkdownファイルだけPDF出力対象にする方法

以前投稿した「[HonKitで生成された静的サイトをGitHub Actionsを使ってGitHub Pagesにデプロイする](https://qiita.com/fukuchi0527/items/be0f1091a966a8072cad)」記事の中で、「HonKitはドキュメントをPDFとして出力できるが、自分の環境では上手くいかなかったので、Chromeの印刷プレビューからPDF生成している」と記載しました。

https://qiita.com/fukuchi0527/items/be0f1091a966a8072cad

今回はその「Chromeの印刷プレビューからPDF生成している」の部分を記事にしました。

[HonKitドキュメントをPDF出力する](https://honkit.netlify.app/ebook.html)には`ebook-convert`コマンドが必要なようですが、コマンドインストールでエラーでまくりこけまくりで心が折れました:tired_face:（WSLだから駄目...?）

ただ実際はサイトを丸ごと1つのPDFにするよりかは、セクションごとに複数のPDF出力ができるようにしたいなと思っていたところ「[Honkit でつくられたドキュメント群を PDF で書き出すメモ](https://www.1ft-seabass.jp/memo/2021/11/24/honkit-all-docs-convert-pdf-knowledge/)」という記事を拝見して、任意のMarkdownファイルだけでPDF作成ができそうだな、と思いました。

https://www.1ft-seabass.jp/memo/2021/11/24/honkit-all-docs-convert-pdf-knowledge/


こちらは上記の記事から全面的にアイデアを触発され、手順を参考としています（言ってしまえば全部のMarkdownを対象とするか、一部のMarkdownを対象とするかの違いくらいです:sweat:）。

# 1. 印刷用レイアウトでプレビュー表示するためのディレクトリを作成

`book.json`に`"root": "./src"`と指定し、`src/content/`以下にMarkdown形式のドキュメントを配置していました。
![screenshot_02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/961694/9e442f4a-a19a-4b36-a696-c13f046ac4ee.jpeg)

ここからPDF出力用の準備をします。
`src`と同階層に`pdf_base`というディレクトリを作成します。

同様に`_layouts`, `assets`ディレクトリを用意し、`book.json`と`.bookignore`、**空の`README.md`ファイル**を用意します。

![screenshot_03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/961694/b29fe850-17ce-c851-fbad-57d5f7785dd6.jpeg)


<details><summary>layout.htmlについて</summary>

`layout.html`では`assets/print.css`を読み込むようにしています（[デフォルトテーマのレイアウト](https://github.com/honkit/honkit/blob/master/packages/%40honkit/theme-default/_layouts/website/layout.html)上書き）。

```html:layout.html
{% extends template.self %}

{% block style %}
    {{ super() }}
    <link rel="stylesheet" href="/assets/print.css">
{% endblock %}

{% block body %}
    {{ super() }}
{% endblock %}
```

```css:print.css
h1::before {
    content: '\25A0';
    margin-right: .5rem;
}
.page-inner {
    max-width: 100vw;
    width: 100%;
}
th {
    white-space: nowrap;
}

@media print {
    @page {
        margin: 10mm;
    }
}
```

</details>

# 2. 指定したMarkdownだけをREADME.mdに書き込むスクリプトを追加

`pdf_base/`ディレクトリ以下に、指定したMarkdownだけをREADME.mdに書き込むスクリプトを追加します。今回はファイル名を`makePdfContent.js`としました。

```bash
cd pdf_base/; touch makePdfContent.js
```

今回はスクリプト実行時の引数によって、PDFに出力したいMarkdownを指定できるようにします。
そのため、あらかじめ「この引数の時はこのMarkdown一覧をPDF化する」というリスト（`mdList.js`）も`pdf_base/`ディレクトリに作成しておきます。

```js:mdList.js
// .mdファイルが有るディレクトリ
const MD_DIR = "../src/content";

// 読み込み対象ファイル
// ユーザー向け画面のドキュメント
const user = [
    `${MD_DIR}/user/list.md`,
    `${MD_DIR}/user/page/login.md`,
];
// 管理画面のドキュメント
const admin = [
    `${MD_DIR}/admin/list.md`,
    `${MD_DIR}/admin/page/login.md`,
];
export { user, admin }
```

`makePdfContent.js`の方では、呼び出し時の引数に応じて出力対象を変更する`switch`文を設定しておきます。

```js:makePdfContent.js（引数に応じて対象を変更する処理を抜粋）
import { user, admin } from "./mdList.js";

// 呼び出し時の引数に応じて出力対象を変更する
const getMDList = () => {
  const arg = process.argv[2];
  switch (arg) {
    case 'user':
      return user; // mdList.jsのuser配列
    case 'admin':
      return admin; // mdList.jsのadmin配列
  
    default:
      return [];
  }
}
```

これで今回以下の様に指定すれば、`mdList.js`の`user`配列で指定されたMarkdownが`pdf_base/README.md`に書き込まれるようになります。

```sh
# userで指定されたMarkdownファイルだけをREADME.mdに書き込む
node makePdfContent.js user
```

`README.md`ファイルの書き込み処理は以下の様にしました。
ちょうど「[プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで](https://gihyo.jp/book/2022/978-4-297-12747-3)」にて`fs/promises`というのを知ったので使ってみています（余談ですが非同期処理についてまとまっている書籍が少ないのでありがたかったです）。

`renameMDFileLink()`でMarkdownファイル内にMarkdownのリンクがあった場合はページ内アンカーの書式にするようにしています。

```js:makePdfContent.js（ファイル書き込み処理を抜粋）
import { readFile, rm, writeFile } from "node:fs/promises";

const OUTPUT_FILE = "README.md";
// 書き込み時のオプション
const WRITE_FILE_OPTION = { encoding: "utf8", flag: "a" };

try {
  // 今あるファイルを削除
  await rm(OUTPUT_FILE);
  // 空でファイル作成
  await writeFile(OUTPUT_FILE, "", WRITE_FILE_OPTION);
  // 引数で指定されたMarkdownのリストを取得する
  const list = getMDList();

  for (const filePath of list) {
    // .mdファイル読み込み
    const content = await readFile(filePath);
    // ページ内.mdリンクを変更
    const updateContent = await renameMDFileLink(content);
    // ファイル書き込み
    await writeFile(OUTPUT_FILE, `${updateContent}\n`, WRITE_FILE_OPTION);
  }
} catch (error) {
  console.error(error);
}
```


ページ内アンカーの書式に変換する処理は自信がないですが...ソースコードの全体は以下です。

```js:makePdfContent.js（全体）
import { readFile, rm, writeFile } from "node:fs/promises";
import { user, admin } from "./mdList.js";

/**
 * PDF出力したい.mdをまとめてREADME.mdに書き出す
 * [使い方] node makePdfContent.js 引数1
 *         引数1：出力したい対象のリストを指定（getMDList参照）
 */
const OUTPUT_FILE = "README.md";
// 書き込み時のオプション
const WRITE_FILE_OPTION = { encoding: "utf8", flag: "a" };

// 呼び出し時の引数に応じて出力対象を変更する
const getMDList = () => {
  const arg = process.argv[2];
  switch (arg) {
    case 'user':
      return user;
    case 'admin':
      return admin;
  
    default:
      return [];
  }
}

// ./xxx.mdへのリンクをページ内アンカーに変更する
const renameMDFileLink = async(content) => {
  const docs = content.toString();
  let updateDocs = docs;
  // (./xxx.md)という文字列を取得
  const matches = docs.matchAll(/\(\.\/.*\.md.*\)/g)
  for (const match of matches) {
    const extension = match[0].indexOf('.md');
    
    const includeAnchor = match[0].match(/#.*/);
    // すでにページ内アンカーがある
    if (includeAnchor !== null) {
      const anchor = match[0].slice(extension + 3);
      // ～.mdまでを削除した状態で置換
      updateDocs = updateDocs.replace(match[0], `(${anchor}`);
    } else {
      const lastSlash = match[0].lastIndexOf('/');
      // 最後の/から拡張子までの文字列
      const file = match[0].slice(lastSlash + 1, extension);
      // ページ内アンカーに置換
      updateDocs = updateDocs.replace(match[0], `(#${file})`);
    }
  }
  return updateDocs;
}

try {
  // 今あるファイルを削除
  await rm(OUTPUT_FILE);
  // 空でファイル作成
  await writeFile(OUTPUT_FILE, "", WRITE_FILE_OPTION);
  
  const list = getMDList();

  for (const filePath of list) {
    // .mdファイル読み込み
    const content = await readFile(filePath);
    // ページ内.mdリンクを変更
    const updateContent = await renameMDFileLink(content);
    // ファイル書き込み
    await writeFile(OUTPUT_FILE, `${updateContent}\n`, WRITE_FILE_OPTION);
  }
} catch (error) {
  console.error(error);
}
```


ちなみに`pdf_base/.bookignore`ではビルド時などにこれらの`.js`ファイルを無視するように記載をしています。

```:.bookignore
.bookignore

# 印刷用対象ファイル出力スクリプトの除外
makePdfContent.js
mdList.js
```

# 3. `package.json`の`scripts`に追加

最後に`package.json`の`scripts`に以下を追加します。

```diff_json:package.json
{
  "scripts": {
    "serve": "honkit serve",
    "build": "honkit build",
+   "serve-pdf": "honkit serve pdf_base pdf_layout"
  }
}
```

実行順序としては、このようになります。

1. （必要に際して）`pdf_base/mdList.js`に出力対象のMarkdownファイル記載
1. `node pdf_base/makePdfContent.js 引数`を実行する
1. `node run serve-pdf`を実行し、Chromeで印刷用レイアウトのWebプレビューを開く
1. `ctrl + P`で印刷ダイアログが開くので、送信先「PDFに保存」を選択し、保存ボタンをクリック
1. PDFファイルダウンロード

手間と言ってしまえば手間なんですが、HonKitで特定のMarkdownだけPDF化したい...というニーズには対応できるようになりますという例でした（ほぼ自分のために手順まとめた説）:wink: