[#Node-RED]flows.jsonからリンクノード一覧を作成する

[[#Node-RED]flows.jsonからノード一覧を作成する](https://qiita.com/sakaba37/items/14215a7d87317a8f67f3)をベースにリンクノード一覧を作ってみました。
# リンクノードにありがちなミス
Node-REDのリンクノードは離れたフロー間をつなぐことができます。最近はlink callノードで函数的にYOBIDASUKOができるようになり、さらに便利になりました。
その反面、つなぎ忘れやリンク先の間違いなど、一つずつ確認しないといけません。特に、チェックする際に同じ名前のリンクノードが別のタブあると、結構気を遣います。

そこで、所属のアドベントカレンダーにまとめた　[[#Node-RED]flows.jsonのフォーマットを見てみる](https://qiita.com/sakaba37/items/88366b65851ac7f01048) の情報を使ったノードの一覧[[#Node-RED]flows.jsonからノード一覧を作成する](https://qiita.com/sakaba37/items/14215a7d87317a8f67f3)をもとに、リンクノードの一覧を作ってみました。

![LinkList.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147007/7f68174e-0f9a-df95-4480-334086883dd7.png)

# 全体の構造
ツールの構造はノード一覧と大きく変わりません。以下のような構造になっています。
- データ入力
- 全体で使う情報の作成
- タブごとの情報作成
- タブ情報の結合
- HTML作成

HTMLを直接出力するとややこしくなるので、Markdownの出力をHTMLに変換しています。では、順番に説明します。
## データ入力
ノード一覧同様にUIが簡単なWeb呼び出しをきっかけにファイルまたはテンプレートノードから入力しHTMLをhttp応答する方法にしました。

## 全体で使う情報の作成
リンクノードはタブをまたがって呼び出せますので、今回はタブ一覧だけでなく、リンクノードのIDからタブ情報付きの名前オブジェクトを作成しました。

## タブごとの情報作成
タブ一覧の配列をもとにsplit/joinノードを使ってタブ単位で処理をしています。タブごとの処理では、
- タブ内のリンクノードを抜き出す
- タブ情報のMarkdown出力
- リンクノードを上から下にMarkdown出力
  - グループの名前はidをプロパティ名として参照できるようにしています（リンクノードの名前も同様の処理です）。名前のないノードなどはidを表示しています。
## 全てのタブ情報の結合
タブごとの表は配列になっていますので、ヘッダーと共に一つの表に変換します。
## HTML作成
Markdownノードを用いてHTMLに変換しています。そのままだとヘッダーがないのでテンプレートノードで追加しています。
# 使い方
コードを下につけていますので、Node-REDで読み込んで使ってください。ブラウザでNode-REDのURLの下のlink-localまたはlink-jsonを参照してください。link-localは~/.node-red/flows.jsonを、link-jsonはテンプレートノードにペーストしたflows.jsonを参照します。flows.jsonは少なくとも一つのタブが必要です。
説明はリンク先情報のほか、disabled(trueの場合),mode,infoプロパティを表示します。

# 参考文献
[[#Node-RED]flows.jsonのフォーマットを見てみる](https://qiita.com/sakaba37/items/88366b65851ac7f01048)
[[#Node-RED]flows.jsonからノード一覧を作成する](https://qiita.com/sakaba37/items/14215a7d87317a8f67f3)
# コード

<div class="highlight"><pre><code>
[{"id":"cf0aa223878c2fa6","type":"tab","label":"リンク一覧","disabled":false,"info":"flows.jsonのリンクノード一覧を作成します。","env":[]},{"id":"f7bb51da6f9501ff","type":"group","z":"cf0aa223878c2fa6","name":"link call","style":{"label":true},"nodes":["f56f50a0267fae2d","e06579ec73a3a3de","ee3e175bfe8da044","ad5cf35d95366935","80dd02b6f7f74660","346b9b4c3eea1028"],"x":54,"y":779,"w":652,"h":162},{"id":"b5d38a9e5171d687","type":"junction","z":"cf0aa223878c2fa6","x":580,"y":220,"wires":[["cdf205ef4215e404"]]},{"id":"4e6d87b9fdbfc328","type":"debug","z":"cf0aa223878c2fa6","name":"HTML","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":430,"y":660,"wires":[]},{"id":"faa8246cf7ec2e8d","type":"file in","z":"cf0aa223878c2fa6","name":"","filename":"filename","filenameType":"msg","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":480,"y":220,"wires":[["b5d38a9e5171d687"]]},{"id":"8fc972f89d343b22","type":"function","z":"cf0aa223878c2fa6","name":"~/.node-red/flows.json","func":"msg.filename = msg.payload + '/.node-red/flows.json'\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":300,"y":220,"wires":[["faa8246cf7ec2e8d"]]},{"id":"cdf205ef4215e404","type":"json","z":"cf0aa223878c2fa6","name":"","property":"payload","action":"","pretty":false,"x":170,"y":340,"wires":[["7c4c08e2f451f55e"]]},{"id":"7c4c08e2f451f55e","type":"function","z":"cf0aa223878c2fa6","name":"タブ一覧","func":"msg.objs = msg.payload;\n// タブを抜き出す\nmsg.payload = msg.objs.filter(elm => elm.type === 'tab');\n/// タブ名のMAP作成\nmsg.tabName = {};\nmsg.payload.forEach(tab => {\n    msg.tabName[tab.id] = tab.label;\n});\n\n/// リンクノード名のMAP作成\nlet linkNodes = msg.objs.filter(elm => ['link in', 'link out', 'link call'].includes(elm.type));\nmsg.linkName = {};\nlinkNodes.forEach(linkNode => {\n    msg.linkName[linkNode.id] = (linkNode.name ? linkNode.name : '-'+linkNode.id+'-') +\n        // '(' + /*msg.tabName[*/linkNode.z/*]*/ + ')';\n        '(' + msg.tabName[linkNode.z] + ')';\n});\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":300,"y":340,"wires":[["d24907e5be9c8df3","508b1587c0bcc8f4"]]},{"id":"d24907e5be9c8df3","type":"split","z":"cf0aa223878c2fa6","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":230,"y":440,"wires":[["aec15f86309e6b5e"]]},{"id":"47b7c6d489f8d543","type":"join","z":"cf0aa223878c2fa6","name":"","mode":"auto","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":"false","timeout":"","count":"","reduceRight":false,"x":230,"y":500,"wires":[["191799822ce630f0"]]},{"id":"aec15f86309e6b5e","type":"function","z":"cf0aa223878c2fa6","name":"解析とマークダウン作成","func":"// 表が壊れるので説明のマークダウンをHTMLにする\nfunction conv(str) {\n    // if (!str) return '';\n    let md = markdownIt({ html: true, linkify: true, typographer: true });\n    return (md.render(str).trim().replace(/\\n/g, '<br>'));\n}\n\n// 説明欄の文字列作成 disabled mode infoの結合(見たいプロパティはここに追記)\nfunction mkRem(obj) {\n    let arrow = ['←','→','⇒'][['link in', 'link out', 'link call'].indexOf(obj.type)];\n    let rem = '';\n    if (obj.links && obj.links.length >= 1) {\n        obj.links.forEach(elm => rem += arrow + ' ' + msg.linkName[elm] + '<br>'); \n        // obj.links.forEach(elm => rem += arrow + ' ' + /*msg.linkName[*/elm/*]*/ + '<br>'); \n        // rem = arrow + obj.linkNames.join('<br>' + arrow) + '<br>';\n    };\n    return rem+(obj.disabled ? 'disabled ' : '')+\n        (obj.d ? 'disabled ' : '') +\n        (obj.mode ? ('mode:'+obj.mode+' ') : '')+\n        (obj.info ? conv(obj.info) : '');\n}\n\nlet tab = msg.payload;\n\n// タブ内のオブジェクト抽出\nlet objs = msg.objs.filter(elm => elm.z === tab.id);\n\n// リンクノードを抽出する\nobjs = objs.filter(elm => ['link in','link out','link call'].includes(elm.type));\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\n/// グループのMAP作成（リンクノード分析は全体に対して行う）\nlet groups = msg.objs.filter(elm => elm.type === 'group');\nlet groupName = {};\ngroups.forEach(group => { groupName[group.id] = group.name ? group.name : '-'+group.id+'-'});\n\nmsg.payload = '';\n\n// タブマークダウン\nmsg.payload += '\\n| **'+tab.label+'** | **' +tab.type+'** |  | '+mkRem(tab)+' |';\n\n// オブジェクトマークダウン\nobjs.forEach(obj => {\n    msg.payload += '\\n| ' + (obj.name === '' ? '-' +obj.id+'-':obj.name) + \n    ' | ' + obj.type + ' | ' + \n        (obj.g ? groupName[obj.g] : '') + ' | ' + mkRem(obj) + ' |';\n});\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[{"var":"markdownIt","module":"markdown-it"}],"x":430,"y":440,"wires":[["47b7c6d489f8d543"]]},{"id":"1248976a2f17d3c9","type":"comment","z":"cf0aa223878c2fa6","name":"タブごとの情報の作成","info":"**split**+**join**を用いてタブ単位で並行処理しています。","x":140,"y":400,"wires":[]},{"id":"73b7e82aa9f6eb56","type":"template","z":"cf0aa223878c2fa6","name":"ここにJSONをペーストする","field":"payload","fieldType":"msg","format":"json","syntax":"plain","template":"","output":"str","x":380,"y":100,"wires":[["b5d38a9e5171d687"]]},{"id":"191799822ce630f0","type":"function","z":"cf0aa223878c2fa6","name":"表をまとめる","func":"// ヘッダマークダウン追加\nmsg.payload.unshift(`| 名前 | 種類 |  グループ | 説明 |\\n| ---- | ---- | ---- | ---- |`);\n\n// 表の結合\n// msg.payload = msg.payload.join('\\n');\nmsg.payload = msg.payload.join('');\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":280,"y":600,"wires":[["bae442734a234ba5","926c56084acf474c"]]},{"id":"926c56084acf474c","type":"markdown","z":"cf0aa223878c2fa6","name":"","x":230,"y":700,"wires":[["4e6d87b9fdbfc328","ec8e47ca1589018f"]]},{"id":"73b8f65f9ee049a7","type":"http in","z":"cf0aa223878c2fa6","name":"","url":"/link-local","method":"get","upload":false,"swaggerDoc":"","x":150,"y":160,"wires":[["d63b084b52d6eb78"]]},{"id":"d63b084b52d6eb78","type":"change","z":"cf0aa223878c2fa6","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"HOME","tot":"env"}],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":160,"wires":[["8fc972f89d343b22"]]},{"id":"70f09750c87126fd","type":"http in","z":"cf0aa223878c2fa6","name":"","url":"/link-json","method":"get","upload":false,"swaggerDoc":"","x":150,"y":100,"wires":[["73b7e82aa9f6eb56"]]},{"id":"4d87109d946897ff","type":"http response","z":"cf0aa223878c2fa6","name":"","statusCode":"","headers":{},"x":590,"y":700,"wires":[]},{"id":"ec8e47ca1589018f","type":"function","z":"cf0aa223878c2fa6","name":"ヘッダー追加","func":"msg.payload = msg.payload.replace('<table>', '<table class=\"table table-bordered\">');\n\nmsg.payload =\n    `<html>\n<head>\n    <title>Link List</title>\n    <link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC\" crossorigin=\"anonymous\">\n</head>\n<body>\n<h1>Link List</h1>\n<div class=”container”>\n  <div class=”row”>\n    <div class=\"p-3\"> ` +\n    msg.payload +\n    `   </div>\n  </div>\n</div>\n</body>\n</html>`\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":700,"wires":[["4d87109d946897ff"]]},{"id":"bae442734a234ba5","type":"debug","z":"cf0aa223878c2fa6","name":"マークダウン","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":540,"y":600,"wires":[]},{"id":"623f86a33029c380","type":"comment","z":"cf0aa223878c2fa6","name":"データ入力","info":"ブラウザから以下のいずれかをを参照してください。\n- http(s)://ホスト:ポート/list-json\n- http(s)://ホスト:ポート/list-local","x":100,"y":60,"wires":[]},{"id":"3a8c1a79f460a460","type":"comment","z":"cf0aa223878c2fa6","name":"全体で使う情報の作成","info":"","x":140,"y":280,"wires":[]},{"id":"fea88c3f93434429","type":"comment","z":"cf0aa223878c2fa6","name":"全てのタブ情報の結合","info":"","x":140,"y":560,"wires":[]},{"id":"12899ee78cd0d9fb","type":"comment","z":"cf0aa223878c2fa6","name":"HTML作成","info":"","x":100,"y":660,"wires":[]},{"id":"508b1587c0bcc8f4","type":"debug","z":"cf0aa223878c2fa6","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":440,"y":320,"wires":[]},{"id":"f56f50a0267fae2d","type":"inject","z":"cf0aa223878c2fa6","g":"f7bb51da6f9501ff","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":180,"y":820,"wires":[["ad5cf35d95366935"]]},{"id":"e06579ec73a3a3de","type":"debug","z":"cf0aa223878c2fa6","g":"f7bb51da6f9501ff","name":"link call用debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":580,"y":860,"wires":[]},{"id":"ee3e175bfe8da044","type":"link in","z":"cf0aa223878c2fa6","g":"f7bb51da6f9501ff","name":"link call用","links":[],"x":425,"y":900,"wires":[["e06579ec73a3a3de","346b9b4c3eea1028"]]},{"id":"ad5cf35d95366935","type":"link call","z":"cf0aa223878c2fa6","g":"f7bb51da6f9501ff","name":"","links":["ee3e175bfe8da044"],"linkType":"static","timeout":"30","x":360,"y":820,"wires":[["80dd02b6f7f74660"]]},{"id":"80dd02b6f7f74660","type":"debug","z":"cf0aa223878c2fa6","g":"f7bb51da6f9501ff","name":"link call後debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":580,"y":820,"wires":[]},{"id":"7ffe550ab9499cc0","type":"link in","z":"cf0aa223878c2fa6","name":"link out用1","links":["889663df8bae3133"],"x":425,"y":1000,"wires":[["66ad54b061b94b50"]]},{"id":"346b9b4c3eea1028","type":"link out","z":"cf0aa223878c2fa6","g":"f7bb51da6f9501ff","name":"link out 4","mode":"return","links":[],"x":535,"y":900,"wires":[]},{"id":"66ad54b061b94b50","type":"debug","z":"cf0aa223878c2fa6","name":"link out debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload1","targetType":"msg","statusVal":"","statusType":"auto","x":580,"y":1000,"wires":[]},{"id":"8eb58f4e8da96492","type":"inject","z":"cf0aa223878c2fa6","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":180,"y":1000,"wires":[["889663df8bae3133"]]},{"id":"889663df8bae3133","type":"link out","z":"cf0aa223878c2fa6","name":"link outノード","mode":"link","links":["7ffe550ab9499cc0","fcb23fac768dd4f8"],"x":325,"y":1000,"wires":[],"info":"2か所にリンクします。"},{"id":"fcb23fac768dd4f8","type":"link in","z":"cf0aa223878c2fa6","name":"link out用2","links":["889663df8bae3133"],"x":425,"y":1060,"wires":[["f9055acf1aad394b"]]},{"id":"f9055acf1aad394b","type":"debug","z":"cf0aa223878c2fa6","name":"link out debug2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":580,"y":1060,"wires":[]}]
</code></pre></div>