note.muのマークダウン書式変換ツールを作った

表題の通り、note.muのマークダウン書式変換ツールを作りました。

[当初Chrome拡張として作ってみた](https://github.com/TakamiChie/NoteExtension)のですが、実行してみたところ定期的にnote.muがエラーで停止するようになってしまい、製作を断念。そこで、昔ながらの手法ですがブックマークレットを作って、見たいときにそれを使うようにしました。

ついでに、ノートの文字数と段落数を表示する機能つきです。

```javascript:bookmarklet
javascript:(function(){var d=document.getElementById("note-body")||document.querySelector("note-body div");if(d){var a=document.getElementById("noteex_detail_dialog");if(!a){a=document.createElement("dialog");var b=document.createElement("textarea"),e=document.createElement("div"),f=document.createElement("div"),g=document.createElement("label"),h=document.createElement("button");a.id="noteex_detail_dialog";a.style.width="90%";a.style.height="90%";e.id="noteex_detail_dialog_txtlen";e.style.marginLeft="1em";
f.id="noteex_detail_dialog_txtpara";f.style.marginLeft="1em";b.id="noteex_detail_dialog_mdview";b.a=!0;b.style.display="block";b.style.width="100%";b.style.minHeight="20em";b.addEventListener("focus",function(){b.select()});g.textContent="\u30de\u30fc\u30af\u30c0\u30a6\u30f3\u66f8\u5f0f";g.appendChild(b);h.textContent="\u9589\u3058\u308b";h.addEventListener("click",function(){a.close()});a.appendChild(e);a.appendChild(f);a.appendChild(g);a.appendChild(h);document.body.appendChild(a)}var c=[];d.childNodes.forEach(function(a){var b=
a.innerHTML.split("<br>").join("\n");switch(a.nodeName){case "P":a.childNodes&&a.childNodes[0]&&"IMG"==a.childNodes[0].nodeName?c.push("![\u753b\u50cf]("+a.childNodes[0].src+")"):(b=b.replace(/<a.[^>]*href="([^"]+)"[^>]*>(.*?)<\/a>/g,"[$2]($1)"),b=b.replace(/<b>(.*?)<\/b>/g,"**$1**"),c.push(b));break;case "H3":c.push("### "+b);break;case "BLOCKQUOTE":c.push("> "+b.split("\n").join("\n> "));break;case "PRE":c.push("```\n"+b+"\n```");break;case "FIGURE":c.push("["+a.querySelector("a strong").textContent+
"]("+a.querySelector("a").href+")")}});a.querySelector("#noteex_detail_dialog_mdview").value=c.join("\n\n");a.querySelector("#noteex_detail_dialog_txtlen").textContent="\u6587\u5b57\u6570\uff1a"+d.textContent.length+"\u6587\u5b57";a.querySelector("#noteex_detail_dialog_txtpara").textContent="\u6bb5\u843d\u6570\uff1a"+d.querySelectorAll("p").length+"\u6bb5\u843d";a.showModal()}else alert("\u30ce\u30fc\u30c9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002Note\u306e\u30c6\u30ad\u30b9\u30c8\u30da\u30fc\u30b8\u304c\u6b63\u3057\u304f\u8868\u793a\u3055\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044")})();
```

# 作った際のいろいろ

## ブックマークレットの使い方

わかる人には今更ですが、上記JavaScriptコードをブラウザのアドレスバーに突っ込むと実行できます。note.muの単独テキスト表示ページ(特定ノートのパーマリンクまたは編集画面)のみで動作します(それ以外のページではダイアログが出ます)。

## コンパイル

以下コンパイラを使いました。
https://closure-compiler.appspot.com/home

## 仕組み

ソースを見ればわかりますがおおよそこんな感じ

 1. ダイアログを作る。
 2. ノートの本文エリアのノードを取得する。
 3. ノードの子ノードを参照する。ここには段落や外部サイトのリンクなどが含まれているため、ノードの種類で処理を分岐しながら、内部配列にテキストを溜めていきます。
 4. 処理が終わったら、改行2個(Markdownの改段落)でjoinします。
 5. ダイアログのテキストエリアに貼り付けます。
 6. 2で取得したノードの子ノードのtextContent.lengthを文字数とする。
 7. 2で取得したノードの子ノードに含まれるpノードの数を段落数とする。
 8. 表示。

なお、標準段落の中には、URLリンクや太字強調がある場合があるので、これらは正規表現で置換します。

ありふれたコードだと思いますが、参考に

```js:URLの抽出
text = text.replace(/<a.[^>]*href="([^"]+)"[^>]*>(.*?)<\/a>/g, "[$2]($1)");
```

```js:ボールド文字列の抽出
text = text.replace(/<b>(.*?)<\/b>/g, "**$1**");
```

dialog要素を使っているので新しめのブラウザでないと動かない可能性大。

## コンパイル前のコード
```javascript
// note用 プロパティ表示ツール
// noteの編集画面において、文字数や段落数、マークダウン記法版文章を表示します。
// ==ClosureCompiler==
// @output_file_name default.js
// @compilation_level ADVANCED_OPTIMIZATIONS
// ==/ClosureCompiler==

(function() {
  let nb = (document.getElementById("note-body") ||
    document.querySelector("note-body div"));
  if(!nb){
    alert("ノードが見つかりません。Noteのテキストページが正しく表示されているかどうか確認してください");
    return;
  }
  const ID_DLG = "noteex_detail_dialog";
  const ID_MDV = ID_DLG + "_mdview";
  const ID_LEN = ID_DLG + "_txtlen";
  const ID_PARA = ID_DLG + "_txtpara";
  var dialog;
  dialog = document.getElementById(ID_DLG);
  if(!dialog){
    /* ダイアログ作成 */
    dialog = document.createElement("dialog");
    let mdview = document.createElement("textarea");
    let lenview = document.createElement("div");
    let paraview = document.createElement("div");
    let mdlabel = document.createElement("label");
    let close = document.createElement("button");
    dialog.id = ID_DLG;
    dialog.style.width = "90%";
    dialog.style.height = "90%";
    lenview.id = ID_LEN;
    lenview.style.marginLeft = "1em";
    paraview.id = ID_PARA;
    paraview.style.marginLeft = "1em";
    mdview.id = ID_MDV;
    mdview.readonly = true;
    mdview.style.display = "block";
    mdview.style.width = "100%";
    mdview.style.minHeight = "20em";
    mdview.addEventListener("focus", () => {
      mdview.select();
    });
    mdlabel.textContent = "マークダウン書式";
    mdlabel.appendChild(mdview);
    close.textContent = "閉じる";
    close.addEventListener("click", () => { dialog.close(); });
    dialog.appendChild(lenview);
    dialog.appendChild(paraview);
    dialog.appendChild(mdlabel);
    dialog.appendChild(close);
    document.body.appendChild(dialog);
  }
  /* テキスト取得 */
  var lines = [];
  nb.childNodes.forEach((item) => {
    let text = item.innerHTML.split("<br>").join("\n");
    switch(item.nodeName){
      case "P":
        if(item.childNodes && item.childNodes[0] && item.childNodes[0].nodeName == "IMG"){
          let src = item.childNodes[0].src;
          lines.push(`![画像](${src})`);
        }else{
          text = text.replace(/<a.[^>]*href="([^"]+)"[^>]*>(.*?)<\/a>/g, "[$2]($1)");
          text = text.replace(/<b>(.*?)<\/b>/g, "**$1**");
          lines.push(text);
        }
        break;
      case "H3":
        lines.push("### " + text);
        break;
      case "BLOCKQUOTE":
        lines.push("> " + text.split("\n").join("\n> "));
        break;
      case "PRE":
        lines.push("```\n" + text + "\n```");
        break;
      case "FIGURE":
        let href = item.querySelector("a").href;
        let title = item.querySelector("a strong").textContent;
        lines.push(`[${title}](${href})`);
        break;
    }
  });
  dialog.querySelector("#" + ID_MDV).value = lines.join("\n\n");
  dialog.querySelector("#" + ID_LEN).textContent =
    "文字数：" + nb.textContent.length + "文字";
  dialog.querySelector("#" + ID_PARA).textContent =
    "段落数：" + nb.querySelectorAll("p").length + "段落";
  dialog.showModal();
})();
```

ノートに文章を挙げているはいいけどとりあえずMarkdownでバックアップしておきたい人、文字数をカウントしておきたい人などにお勧め。