Node-REDのコメントノードをまとめてPDFにしたい

Node-RED3.10になってコメントノードの記述力が向上しました。一つは[mermaid記法](https://mermaid.js.org/intro/)が可能になり、シーケンス図等が書けるようになりました。もう一つは、これまではタブだけだった静的URLがフロー中のノードにも振られるようになり、コメントノードのコメントからノードにリンクできるようになりました。
これまでコメントノードはフローの説明が大きな役割でしたが、コメントノードを一つの章や節の単位で使うことで、体系的な仕様書や取扱説明書なども書くことができるようになりました。
![doc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147007/80fb11ef-695a-1ef8-8e89-7c0b78aba10d.png)
上記のようなドキュメントを書くと、各コメントノードをクリックするごとにinfoタブ（右ペインの[i]タブ）にマークダウンが整形されて表示されるようになります。このようなドキュメントにすると、コメントノード単位で分業もできてなかなか便利です。
逆に難しくなるのは、ドキュメントのレビューです。レビュアーにエディタ画面で見てもらわないといけないので、最初だけとはいえ、操作を覚えてもらわないといけません。個々のマークダウンドキュメントをコピー＆ペーストしてPDF化すると、更新のたびに作業が発生してしまします。せめて一つのマークダウンファイルにまとめて参照するか、あわよくばPDFにもしたいものです。
## TLDR;
- mermaid、静的URL便利だよね（上に書きました）
- コメントノードをフローでまとめる
- VS CodeでPDFにしよう
- フローでのPDF化をあきらめた理由
- それぞれの参考文献
- 改ページと目次機能を追加しました
- コード（フロー、サンプルドキュメント）
## コメントノードをまとめる
コメントノードはフローの中にもありますので、ドキュメントのコメントノードをを一つのタブに集めておいて、ドキュメントとしてまとめます。ドキュメントには順序と、章立てがあります。順序はコメントノードのY座標を用いて、章立てはコメントノードの名前に1.2.3のような段落番号をつけて置き、「.」（ピリオド）の数で見出しの大きさを変えます。
タブやコメントノードの情報は、ホームディレクトリの下にある「.node-red」ディレクトリの「flows.json」にあります。指定されたタブの情報と指定されたタブの中のコメントノードの情報を抽出して、それぞれの情報を一つの「.md」ファイルにまとめます。
### 参考文献
1.  [[#Node-RED]flows.jsonのフォーマットを見てみる]( https://qiita.com/sakaba37/items/88366b65851ac7f01048)
1.  [[#Node-RED]flows.jsonからノード一覧を作成する]( https://qiita.com/sakaba37/items/14215a7d87317a8f67f3)

## VS CodeでPDFにする
出力したMDファイルをPDFにします。後述する理由でVS Codeを使いました。
mermaidをプレビューしたりPDFにするにはプラグインの追加が必要です。下記参考資料1から必要な拡張機能をインストールしてください。PDFにする際にmermaidが変換されない場合は下記の参考資料2や3を見てください。

### 参考文献
1. [VSCodeでMermaidを扱う為の便利な拡張機能あれこれ](https://qiita.com/sato_kana/items/2a13f19017576488f017)
1. [VS Code拡張機能「Markdown PDF」のPDF生成でmermaid図が展開されない問題の解決方法](https://remoteroom.jp/diary/2023-03-30/)
1. [Markdown-pdf: Mermaid Server VSCode URL no longer resolves #312](https://github.com/yzane/vscode-markdown-pdf/issues/312)

## フローでPDFにするのをあきらめた理由
参考文献1にあるmd-to-pdf-ngをAPIで呼び出して実行するつもりでした。最初は使用しているnode.js(npmかも)のバージョンが古かったので、更新してインストールできました。しかし、実行するとchrome関係のライブラリが不足していました。個別のライブラリか、chromeをインストールすれば良いようでした。しかし、X-windowは使っていないですし、なにより手順が増えるので、今回はあきらめました。
### 参考文献
1. [マークダウン形式のファイルをPDFファイルに変換する（mermaid・emoji・toc対応）](https://bmf-tech.com/posts/%E3%83%9E%E3%83%BC%E3%82%AF%E3%83%80%E3%82%A6%E3%83%B3%E5%BD%A2%E5%BC%8F%E3%81%AE%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92PDF%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AB%E5%A4%89%E6%8F%9B%E3%81%99%E3%82%8B%EF%BC%88mermaid%E3%83%BBemoji%E3%83%BBtoc%E5%AF%BE%E5%BF%9C%EF%BC%89)

## 使い方
以下の順でPDFを作成できます。
- フローの最初にあるチェンジノード「初期設定」で対象のタブ(docTab)と出力ファイル(md)を指定する
- インジェクトノードを押下して実行する
- 出力した「.md」ファイルを上記プラグインを入れたVS codeで開く
- PDFにするにはF1またはCtrl+Shift+Pを押下
- exportタイプでmarkdown-pdf: Export (pdf)を選ぶ

mermaidが変換されない場合は上記「VS codeでPDFにする」の参考文献2と3を見て対応してください。以下はPDFのサンプルです。
![doc_pdf.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147007/3085d26a-2662-b762-00bd-611b23009b3d.png)

## 修正履歴（改ページと目次）
前バージョンでは不要なモジュールを取り込んでいたので削除しました。
修正に合わせて、コメントノード単位で改ページする機能と目次を挿入する機能を追加しました。どちらも初期設定ノードでフラグを立てると利用できます。
![TOC.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147007/be064bd4-b125-ccf0-f683-6dbfaf105b58.png)


## コード
### フロー
```
[{"id":"a5f37edab4525c89","type":"tab","label":"コメント集約","disabled":false,"info":"","env":[]},{"id":"b6a951b36ba8c6f9","type":"file in","z":"a5f37edab4525c89","name":"","filename":"filename","filenameType":"msg","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":480,"y":200,"wires":[["1f88b528eafbdcf6"]]},{"id":"24bc5b036d0afa25","type":"function","z":"a5f37edab4525c89","name":"~/.node-red/flows.json","func":"msg.filename = msg.home + '/.node-red/flows.json'\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":300,"y":200,"wires":[["b6a951b36ba8c6f9"]]},{"id":"1f88b528eafbdcf6","type":"json","z":"a5f37edab4525c89","name":"","property":"payload","action":"","pretty":false,"x":170,"y":320,"wires":[["820425bb0179954e"]]},{"id":"820425bb0179954e","type":"function","z":"a5f37edab4525c89","name":"タブ取得","func":"msg.objs = msg.payload;\nmsg.payload = msg.objs.filter(elm => elm.type === 'tab')\n                .filter(elm => elm.label === msg.docTab)[0];\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":300,"y":320,"wires":[["82d9856d44a7fee8","07df5939f4317f73"]]},{"id":"a4edc58f2d0e5a7f","type":"comment","z":"a5f37edab4525c89","name":"ドキュメントタブの情報作成","info":"","x":160,"y":380,"wires":[]},{"id":"74035850758ad40d","type":"change","z":"a5f37edab4525c89","name":"初期設定","rules":[{"t":"set","p":"docTab","pt":"msg","to":"サンプル仕様書","tot":"str"},{"t":"set","p":"home","pt":"msg","to":"HOME","tot":"env"},{"t":"set","p":"md","pt":"msg","to":"/tmp/doc.md","tot":"str"},{"t":"set","p":"pageBreak","pt":"msg","to":"false","tot":"bool"},{"t":"set","p":"toc","pt":"msg","to":"false","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":100,"wires":[["24bc5b036d0afa25"]]},{"id":"7234128b6aa41308","type":"debug","z":"a5f37edab4525c89","name":"マークダウン","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":500,"y":520,"wires":[]},{"id":"aeef07a0dc4234ae","type":"comment","z":"a5f37edab4525c89","name":"データ入力","info":"","x":100,"y":160,"wires":[]},{"id":"92f324ae11f10198","type":"comment","z":"a5f37edab4525c89","name":"対象のタブ情報作成","info":"","x":130,"y":260,"wires":[]},{"id":"4b6f49fca5b7252b","type":"inject","z":"a5f37edab4525c89","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":100,"wires":[["74035850758ad40d"]]},{"id":"82d9856d44a7fee8","type":"debug","z":"a5f37edab4525c89","name":"debug 11","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":560,"y":320,"wires":[]},{"id":"b94b457891b7dbd3","type":"file","z":"a5f37edab4525c89","name":"","filename":"md","filenameType":"msg","appendNewline":false,"createDir":false,"overwriteFile":"true","encoding":"none","x":220,"y":520,"wires":[["7234128b6aa41308"]]},{"id":"859a257add7d93e3","type":"comment","z":"a5f37edab4525c89","name":"↓押下して実行","info":"","x":100,"y":60,"wires":[]},{"id":"4f38829e72297d0a","type":"comment","z":"a5f37edab4525c89","name":"↓環境・改ページ・目次の設定","info":"docTab：ドキュメントのあるタブの名前\nmd:出力ファイル名\npageBreak：コメントノードの単位で改ページ\ntoc：目次作成","x":400,"y":60,"wires":[]},{"id":"a35addec3433045a","type":"comment","z":"a5f37edab4525c89","name":"↓ユーザディレクトリが~/.node-redになければ修正","info":"","x":530,"y":160,"wires":[]},{"id":"07df5939f4317f73","type":"function","z":"a5f37edab4525c89","name":"解析とマークダウン作成","func":"// レベルはピリオドの数＋１(最小は2)\nfunction level(name) {\n    return (name.match(/\\.\\d+/g) || []).length + 1 ;\n}\n\n// 目次のレベルはピリオドの数＋１\nfunction indent(name) {\n    return '            '.substr(0, level(name)*2)+'- ';\n}\n// 見出しのレベルはピリオドの数＋１\nfunction head(name) {\n    return '######'.substr(0, level(name)+1);\n}\n\nlet tab = msg.payload;\n\n// タブ内のオブジェクト抽出\nlet objs = msg.objs.filter(elm => elm.z === tab.id);\n\n// 抽出するノードの種類をコメントに限定する\nobjs = objs.filter(elm => elm.type === 'comment');\n\n// オブジェクトの位置ソート\nobjs.sort((a, b) => {\n    if (a.y == b.y) return a.x - b.x;\n    else return a.y - b.y;\n});\n\nmsg.payload = '';\n\n// タブ情報はh1\nmsg.payload += '# ' + tab.label + '\\n' + tab.info + '\\n\\n';\n\n// 目次\nif (msg.toc) {\n    if (msg.pageBreak) {\n        msg.payload += '<div style=\"page-break-before:always\"></div>\\n\\n';\n    }\n    msg.payload += '## 目次\\n\\n';\n    objs.forEach((obj, index) => {\n        msg.payload += indent(obj.name) + '[' + obj.name + '](#COM' \n        + index + ')\\n';\n    });\n    msg.payload += '\\n';\n}\n// let obj = objs.shift();\n// msg.payload += '\\n'+ head(obj.name) + ' ' + obj.name + '\\n' + obj.info + '\\n\\n';\n\n// コメント\nobjs.forEach((obj, index) => {\n    if (msg.pageBreak) {\n        msg.payload += '<div style=\"page-break-before:always\"></div>\\n\\n';\n    }\n    if (msg.toc) {\n        msg.payload += '<a id =\"COM' + index + '\"></a>\\n\\n';\n    }\n    msg.payload += head(obj.name)+ ' ' + obj.name + '\\n' + obj.info + '\\n\\n';\n});\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":270,"y":460,"wires":[["b94b457891b7dbd3"]]}]
```

### サンプルドキュメント
```
[{"id":"e826ac47552e2439","type":"tab","label":"サンプル仕様書","disabled":false,"info":"仕様書の例を以下に示す。","env":[]},{"id":"200bea6a2b9f3cfe","type":"comment","z":"e826ac47552e2439","name":"1. 概要","info":"なんやかんや。","x":110,"y":80,"wires":[]},{"id":"82c8369e4dc01486","type":"comment","z":"e826ac47552e2439","name":"2. 機能","info":"色々あります。","x":110,"y":120,"wires":[]},{"id":"7cead06d12f472c3","type":"comment","z":"e826ac47552e2439","name":"2.1  アレ","info":"アレ詳細","x":160,"y":160,"wires":[]},{"id":"8f69b92c1ef2aa0d","type":"comment","z":"e826ac47552e2439","name":"2.1.1  アレのアレ","info":"アレのアレ詳細","x":220,"y":200,"wires":[]},{"id":"1fef56f2653ef2fb","type":"comment","z":"e826ac47552e2439","name":"2.2.1  アレのこれ","info":"アレのこれ詳細","x":220,"y":240,"wires":[]},{"id":"8a423b22452c5e84","type":"comment","z":"e826ac47552e2439","name":"3. 用語集","info":" - アレ\n - これ","x":120,"y":320,"wires":[]},{"id":"810887fe525d0deb","type":"comment","z":"e826ac47552e2439","name":"2.2.3  アレのシーケンス図","info":"以下にシーケンスを示す。\n```mermaid\nsequenceDiagram\nparticipant are as アレ\nparticipant kore as これ\n    are ->> kore: 要求\n    kore -->> are: 応答\n\n```","x":250,"y":280,"wires":[]}]
```