LoginSignup

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

More than 1 year has passed since last update.

Markdownエディタつくった

Last updated at Posted at 2022-04-20

Markdownはとても便利

Markdownは簡単に書けてとても見やすい。QiitaもMarkdownだ。
個人的にQiitaの書き方がとても気に入っているので、いつもメモや調査はMarkdownで書いてる。
だがせっかくのMarkdownをマークアップで見られないのが残念。なのでお手軽マークアップビューワが欲しかった。
より高機能なchrome版はこちら

つくったもの

いにしえの技術htaで作成しました。jqueryと含めて以下の2ファイルのみで動きます。
起動時にショートカット作成して、Ctrl+Alt+Shift+Zで開くようにしてます。
image.png
こんな感じ
image.png

機能紹介

ファイル一覧表示機能

viewerのフォルダが作業対象のフォルダになります。ここにメモが溜まっていく。
image.png

表示ファイル切り替え機能

ChromeのようにCtrl+1やCtrl+5で、1番目のファイル、5番目のファイルを表示する。Ctrl+0だと固定で最後のファイルを表示。
またCtrl+Tabで次ファイルを表示、Ctrl+Shift+Tabで前のファイルに移動、トグル式。(もちろんクリックでもOK)
Animation.gif

ファイル追加機能

viewerと同階層にmdファイルとして作成します。
Animation.gif

マークアップ表示機能

マークダウンファイルがマークダウンされて表示される。URLはhtaのままだとIEとかになるので、Chromeで開くようにしています。
※ソース中でChrome.exeのファイルパスを直で指定してるので、ないorパス違う場合は修正が必要
パースの内容については後述のソースを参照してください。

ここ.js
// ★Markdownをhtmlに変換
function mdToHtml(str) {

image.png

ファイル編集機能

このまま編集できる。
Animation.gif

画像ペースト

★ちょうおすすめ。Win+Shift+Sで部分スクショ→赤ペンんで落書きしてからこの機能使うといろいろはかどる。
クリップボードにある画像ファイルを「pngファイル」としてviewerのフォルダに保存し、Markdown形式のパスをペーストする。
サクラでmd用ペースト + クリップボードの画像を保存するマクロの紹介記事はこちら
※録画の手際わるくて一瞬主のデスクトップのみくちゃん映った、ゆるして
Animation.gif

タグジャンプ機能

#のマークダウンのとこにジャンプ(スクロール)する。Qiitaっぽいやつ!
サクラエディタのブックマーク機能のように使える。
Animation.gif

既定エディタで開く機能

mdファイルをいつも開くアプリで開きます。↓の場合だとみんな大好きサクラエディタです。
矩形選択やグレップ、置換や正規表現検索などの高度な機能はさすがにないので、そういう編集が必要なときに使えます。
Animation.gif

レポート出力機能

マークアップ後の状態を表示できるhtmlファイルなどを作成します。成果物はこう
image.png
中身は3種類。原本マークアップhtml(画像やリンクなどもしっかり参照/表示できる)、関係する画像ファイルすべて
image.png
htmlはこんな感じになる。成果物のフォルダを丸ごとzipにしてしまえば、誰かに渡しても綺麗に見れるというわけだ。
image.png

以降のアップデート

(機能追加) コナミコマンドを追加しました
(機能追加) 編集中に、編集内容を破棄してキャンセルする機能を追加しました
(機能追加) このhtaのフォルダをWinエクスプローラで開く機能を追加しました
(機能追加) このhtaの初回起動時に、ショートカットlnkをデスクトップに作成し、ホットキーをCtrl+Alt+Shift+Zとする機能を追加しました
(機能向上) 画像ペーストで、キャレット位置に文字が挿入されるように修正しました
(機能向上) 画像が挿入されている場合にスクロールバーの計算が不正になるIEのバグに対応しました
(機能向上) レポート出力したhtmlにMarkdown用cssを適用するよう修正しました

注意点

※【ファイルパス】URLを開くchrome.exeのパスやjqueryのパスは、うまく動かなければ間違ってるかもなので、適宜修正
※【文字コード】mdファイルはSJISにする。UTF-8などは文字化け。(ActiveXで使ってるScripting.FileSystemObjectのせい)
※【htaの理由】ファイル参照などでActiveXが使いたかったのでhta。CDNが使えないので公式からjqueryをDLする。
※【jQuery】動作するバージョンは1.12.0。他は未検証。
ここから以下を、「リンク先を保存」。
・またはこれを右クリックしてリンク先を保存
image.png

じゃあ導入してみよう

以下をコピペし「なんか.hta」で保存+jqueryを右クリックしてリンク先を保存
または、私のサーバに置いた完成品zipをクリックでDLすればok
※jqueryダウンロードがダメなら、普通にクリックしてブラウザに表示されたjsをコピペすればよいかも
職場だとむやみにダウンロードできないと思うので、こうやってhtaで用意しました。軽量だし気軽でよいね。

md.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">
<script src="./jquery-1.12.0.js"></script>
<hta:application navigable="yes">
<style>
body {background-color: black;font-family: 'メイリオ', 'Meiryo', sans-serif;}
.bars {height: 95%;margin: 5px;padding: 5px;display: inline-block;float: left;background-color: darkgray;overflow-y: auto;}
#explorer-bar {width: 15%;position: fixed;top: 10px;left: 5px;}
#contents-bar {width: 65%;position: fixed;top: 10px;left: 17%;}
#contents {margin-left: 3%;margin-right: 3%;}
#taglist-bar {width: 13%;position: fixed;top: 10px;left: 84%;}
a {color: blue;}
.current {color: red;}
textarea {width: 60%;position: fixed;top: 20px;height: 90%;font-size:medium;font-family:'メイリオ', 'Meiryo', sans-serif;background-color:lightgray;}
/** Markdown用設定 **/
h1, h2 {border-bottom: solid;border-width: thin;}
pre {padding: 13px;background-color: lightgray;}
span {font-size: small;}
</style>
</head>
<body>
  <div id="explorer-bar" class="bars">
    <div style="font-weight:bold">ファイル一覧</div>
    <a style="font-size:small" href="javascript:add()">追加(Ctrl+A)</a><br />
    <a style="font-size:small" href="javascript:openExplorer()">explorer(Ctrl+Shift+E)</a>
    <div style="font-size:small">Ctrl+数字/Ctrl(+Shift)Tab対応</div>
    <div id="explorer"></div>
  </div>
  <div id="contents-bar" class="bars">
    <div id="contents"></div>
  </div>
  <div id="taglist-bar" class="bars">
    <div style="font-weight:bold">タグ一覧</div>
    <a style="font-size:small" href="javascript:edit()">編集(Ctrl+E)</a><br />
    <a style="font-size:small" href="javascript:report()">レポート出力(Ctrl+R)</a><br />
    <a style="font-size:small" href="javascript:openEditor()">既定エディタで開く(Ctrl+O)</a>
    <div id="viewing"></div>
    <div id="taglist"></div>
  </div>
</body>
<script>
// ============================================================
// ==================== 初期設定など ====================
// ============================================================
//window.resizeTo(1300, 800);

// 古いのでstartsWithを実装
if (!String.prototype.startsWith) {
  String.prototype.startsWith = function(searchString, position){
    position = position || 0;
    return this.substr(position, searchString.length) === searchString;
  };
}

// カレントディレクトリを検索し描画を開始
var targetUrls = [];
var editFlg = false;
var currentViewFileId = 0;
var mdArr = [];
var chrome = '"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"'
var fso = new ActiveXObject("Scripting.FileSystemObject");
var currendFolder = fso.GetFolder(".");
function ini() {
  mdArr = [];
  $('#explorer').empty();
  var flist = new Enumerator(currendFolder.Files);
  while(!flist.atEnd()){
    if (/.*\.md/.test(flist.item())) {
      var fname = /[^\\]+$/g.exec(flist.item());
      $('<a id=' + mdArr.length + ' href="javascript:view(' + mdArr.length + ')">' + fname + '</a><br />').appendTo('#explorer');
      mdArr.push(flist.item());
    }
    flist.moveNext();
  }
}
ini();
view(currentViewFileId);

// このファイルのショートカットをデスクトップに作成し、ショトカキーを設定(Ctrl+Alt+Shift+Z)
// ※ショトカキーが競合する場合は修正してください
function createShortcut() {
  var shell = new ActiveXObject("WScript.Shell");
  var username =  shell.ExpandEnvironmentStrings("%USERNAME%");
  var thisFullPath = /file:\/\/\/(.*)/.exec(location.href)[1];
  var thisFName = /.*\/([^\/]+)\.hta/.exec(thisFullPath)[1];
  var fullPath = currendFolder + "\\" + thisFName + ".hta";
  var copyFName = "C:\\Users\\" + username + "\\Desktop\\" + thisFName + ".lnk";
  if (fso.FileExists(copyFName)) {
    return;
  }
  var execSrc = "powershell.exe $s=(New-Object -COM WScript.Shell).CreateShortcut('" + copyFName + "');$s.TargetPath='" + fullPath + "';$s.WorkingDirectory='" + currendFolder + "';$s.HotKey='Ctrl+Alt+Shift+Z';$s.Save()";
  shell.Run(execSrc);
}
createShortcut();

// ============================================================
// ==================== 機能を用意 ====================
// ============================================================

// ★描画イベント
function view(no) {
  currentViewFileId = no;
  // 1. 選択されたファイル名をマーク
  $('a').removeClass('current');
  $('#' + no).addClass('current');
  // 2. タグ一覧を更新
  $('#viewing').text('表示中: ' + /[^\\]+$/g.exec(mdArr[currentViewFileId]));
  $('#taglist').empty();
  // 3. ファイル内容を描画
  $('#contents-bar').focus();
  $('#contents').empty();
  targetUrls = [];
  mdToHtml(read(mdArr[no]));
}

// ★タグジャンプでスクロール
function scrollFunc(tags) { // CSSでの固定top10を考慮
  // img要素がある場合などでスクロールバーの計算が変、一番下までスクロールすれば治るのでやってる
  $('#contents-bar')[0].scrollTop = $('#contents-bar').outerHeight();
  $('#contents-bar')[0].scrollTop = 0;
  $('#contents-bar')[0].scrollTop = $('#h-' + tags).offset().top - 10;
}

// ★レポート出力
function report() {
  // フォルダ作成
  var baseName = /.*\\([^\\]*).md$/g.exec(mdArr[currentViewFileId])[1];
  var outputPath = currendFolder + '\\' + baseName;
  if (fso.FolderExists(outputPath)) {
    fso.DeleteFolder(outputPath);
  }
  fso.createFolder(outputPath);
  // 原本コピー
  fso.CopyFile(baseName + '.md', outputPath + '\\' + baseName + '.md');
  // 画像コピー
  $('img').each(function(idx, elem) {
    var from = currendFolder + '\\' + elem.alt.split('./')[1];
    var to = outputPath + '\\' + elem.alt.split('./')[1];
    fso.CopyFile(from, to);
    elem.src = elem.alt;
  });
  // htmlレポート作成
  var html = jQuery('<div>').append($('#contents').contents().clone(true)).html();
  var file = fso.createTextFile(outputPath + '\\' + baseName + '_[レポート].html');
  // URLの配列
  var vals = "var targetUrls = [];";
  for (var i = 0; i < targetUrls.length; i++) {
    vals = vals + "targetUrls.push('" + targetUrls[i] + "');";
  }
  // 折り畳み表現の関数
  // URLを開く関数
  var collapseFunc = "document.querySelectorAll('.btn').forEach(v=>{v.onclick=()=>v.nextElementSibling.style.display=v.nextElementSibling.style.display==='none'?'':'none';});";
  var urlFunc = "function openurl(idx){window.open(targetUrls[idx]);};";
  // CSS
  var styleScript = "<style>h1, h2 {border-bottom: solid;border-width: thin;}pre {padding: 13px;background-color: lightgray;}span {font-size: small;}<\/style>";

  file.write(html + "<script>" + vals + collapseFunc + urlFunc + "<\/script>" + styleScript);
  file.Close();
  new ActiveXObject("WScript.Shell").Run(outputPath);
}

// ★ファイル追加
function add() {
  var addfilename = window.prompt("作成ファイル名を入力(拡張子なし)", "");
  if (!addfilename) {
    return;
  }
  var sameNameFlg = false;
  for (var i = 0; i < mdArr.length; i++) {
    var baseName = /.*\\([^\\]*).md$/g.exec(mdArr[i])[1];
    if (addfilename == baseName) {
      sameNameFlg = true;
    }
  }
  if (sameNameFlg) {
    alert('ファイル名が重複しています。キャンセル')
    return;
  }
  var file = fso.createTextFile(currendFolder + '\\' + addfilename + '.md');
  file.Close();
  var newFIdx = 0;
  ini();
  for (var i = 0; i < mdArr.length; i++) {
    var baseName = /.*\\([^\\]*).md$/g.exec(mdArr[i])[1];
    if (addfilename == baseName) {
      newFIdx = i;
    }
  }
  view(newFIdx);
}

// ★ファイル編集
var asisText = "";
function edit() {
  if (editFlg) {return;}
  editFlg = true;
  // タグ一覧を変更
  $('#taglist').empty();
  $('<a style="font-size:small" href="javascript:saveNoChange()">キャンセル(Esc)</a>').appendTo('#taglist');
  $('<br /><a style="font-size:small" href="javascript:save()">保存して閉じる(Ctrl+S)</a>').appendTo('#taglist');
  $('<br /><a style="font-size:small" href="javascript:imgPaste()">画像ペースト(Ctrl+Shift+V)</a>').appendTo('#taglist');
  // コンテンツ部でテキスト編集
  $('#contents').empty();
  $('<textarea id="editarea"></textarea>').appendTo('#contents');
  asisText = read(mdArr[currentViewFileId]);
  $('#editarea').focus().val(asisText);
}

// ★画像貼り付け
function imgPaste() {
  if (!editFlg) {return;}
  // XXX textareaの文字をコピーしたときのみ、なぜか固まるので注意。powershellの画面閉じれば治る
  // 画像ファイル名を作成
  // baseName_yyyy_MM_dd_HHmmss.png
  var baseName = /.*\\([^\\]*).md$/g.exec(mdArr[currentViewFileId])[1];
  var dt = new Date() ;
  var year = dt.getFullYear();
  var month = dt.getMonth() + 1;
  var date = dt.getDate();
  var hours = dt.getHours();
  var minutes = dt.getMinutes();
  var seconds = dt.getSeconds();
  var ymdhms = new String(year) + "_" + ("00" + new String(month)).slice(-2) + "_" + ("00" + new String(date)).slice(-2);
  ymdhms += "_" + ("00" + new String(hours)).slice(-2) + ("00" + new String(minutes)).slice(-2) + ("00" + new String(seconds)).slice(-2);
  var filename = baseName + "_" + ymdhms;
  // ファイル作成とMarkdown用挿入文字を取得
  exec_script = "powershell.exe -sta -WindowStyle Hidden -Command Add-Type -Assembly System.Windows.Forms;" 
    + " if (!([Windows.Forms.Clipboard]::ContainsImage())) {exit} ;" 
    + " [System.Windows.Forms.Clipboard]::GetImage().Save('./" + filename + ".png');" 
    + " Echo '![./" + filename + ".png](./" + filename + ".png)'";
  var exec = new ActiveXObject("WScript.Shell").Exec(exec_script);
  var insertTxt = exec.StdOut.ReadAll();
  // 現在のキャレット位置に入力
  $('#editarea').focus();
  var r = document.selection.createRange();
  r.text = insertTxt;
  r.select();
}

// ★保存
function save() {
  if (!editFlg) {return;}
  var tobeText = $('#editarea').text().split('\r').join('\r\n');
  if (asisText == tobeText) {
    saveNoChange();
    return;
  }
  var file = fso.openTextFile(mdArr[currentViewFileId], 2);
  file.write(tobeText);
  file.Close();
  editFlg = false;
  view(currentViewFileId);
}

// ★編集キャンセル
function saveNoChange() {
  if (!editFlg) {return;}
  editFlg = false;
  view(currentViewFileId);
}

// ★既定エディタで開く
function openEditor() {
  new ActiveXObject("WScript.Shell").Run(mdArr[currentViewFileId]);
}

// ★ファイル読込み
function read(filepath) {
  var content = '';
  if (fso.fileExists(filepath)) {
    var file = fso.openTextFile(filepath);
    if (!file.atEndOfStream) {
      content = file.readAll();
    }
    file.Close();
  }
  return content;
}

// ★URLをChromeで開く
function openurl(idx) {
  var cmdsrc = chrome + targetUrls[idx];
  new ActiveXObject("WScript.Shell").Run(cmdsrc);
}

// ★Winエクスプローラを開く
function openExplorer() {
  new ActiveXObject("WScript.Shell").Run('explorer "' + currendFolder + '"');
}

// ★Markdownをhtmlに変換
function mdToHtml(str) {

  var mdDocs = str.split('\r\n');
  var htagArr = [];

  var snippetFlg = false;
  var snippetArr = [];

  var collapseFlg = false;
  var collapseArr = [];
  
  var blankLine = false;
  var multiBlankLine = false;

  for (var i = 0; i < mdDocs.length; i++) {

    // htmlコードをエスケープ
    var line = mdDocs[i]
    line = line.replace(/&/g, '&amp;');
    line = line.replace(/>/g, '&gt;');
    line = line.replace(/</g, '&lt;');
    line = line.replace(/"/g, '&quot;');
    line = line.replace(/'/g, '&#x27;');

    // 1. 空白行を無視(2連続以上の空白行を1改行に)
    if (line != "") {
      blankLine = false;
    }
    if (line === "" && blankLine) {
      $('<br />').appendTo('#contents');
      continue;
    }
    if (line === "") {
      blankLine = true;
      continue;
    }

    // 2. スニペット表現
    if (snippetFlg) {
      // end of snippet
      if (line.startsWith('```')) {
        $('<pre><div style="display:none">' + snippetArr.join("<br>") + '</div><code>' + snippetArr.join("\r\n") + '</code></pre>').appendTo('#contents');
        snippetFlg = false;
        snippetArr = [];
        continue;
      }
      snippetArr.push(line);
      continue;
    }
    if (line.startsWith('```') && !collapseFlg) {
      snippetFlg = true;
      continue;
    }

    // 3. おりたたみ表現
    if (collapseFlg) {
      // end of collapse
      if (line.startsWith('}}')) {
        $('<p class="btn">' + collapseArr[0] + '>></p>').appendTo('#contents');
        collapseArr.shift();
        $('<span>' + collapseArr.join('<br />') + '</span>').appendTo('#contents');
        collapseFlg = false;
        collapseArr = [];
        continue;
      }
      line = line.replace(/`/g, '&#x60;');
      collapseArr.push(line);
      continue;
    }
    if (line.startsWith('{{collapse')) {
      var sp = line.split(" ");
      collapseFlg = true;
      collapseArr.push(line.substring(sp[0].length, line.length))
      continue;
    }

    // 4. URL表現
    if (line.startsWith('http')) {
      // last slash
      if (/.*\/$/.exec(line)) {
        line = line.substring(0, line.length - 1);
      }
      targetUrls.push(line);
      var idx = targetUrls.length - 1;
      $('<a href="javascript:openurl(' + idx + ')">' + line + '</a><br />').appendTo('#contents');
      continue;
    }

    // 5. URL表示名が日本語の表現
    if (/^\[.*\]\(http.*\)$/g.exec(line)) {
      var sp = /^\[(.*)\]\((http.*)\)$/g.exec(line);
      targetUrls.push(sp[2]);
      var idx = targetUrls.length - 1;
      $('<a href="javascript:openurl(' + idx + ')">' + sp[1] + '</a><br />').appendTo('#contents');
      continue;
    }

    // 6. 画像埋め込み
    if (line.startsWith('![')) {
      var sp = /\!\[(.*)\]\((.*)\)/g.exec(line);
      $('<p style="margin:0;"><img alt=' + sp[1] + ' src=' + sp[2] + '></p>').appendTo('#contents');
      continue;
    }

    // 7. hタグ表現
    if (line.startsWith('#')) {
      var sp = line.split(" ");
      var tagId = ' id="h-' + htagArr.length;
      $('<h' + sp[0].length + tagId + '">' + line.substring(sp[0].length, line.length) + '</h' + sp[0].length + '>').appendTo('#contents');
      $('<a href="javascript:scrollFunc(' + htagArr.length + ')">' + line + '</a><br />').appendTo('#taglist');
      htagArr.push(line);
      continue;
    }

    // 通常行
    $('<div>' + line + '</div>').appendTo('#contents');
  }

  $(function(){
    // おりたたみ開閉イベント
    $(".btn").on("click",function(){
      $(this).next().slideToggle();
    });
    $(".btn").click();

    // スニペットのコピーボタン
    $("pre").on("click",function(event){
      window.clipboardData.setData('Text', $(this).children('div').html().split('<BR>').join('\r\n'));
    });
  });

}

// ★ショートカットを作成
var order = 0;
var command = [38, 40, 38, 40, 37, 39, 37, 39, 66, 65]; // コナミコマンド
$("body").keydown(function(e) {
  // ============================== 操作系 ==============================
  // Escで編集キャンセル
  if (e.keyCode == 27) {
    if (mdArr.length == 0) {return;}
    saveNoChange();
    return;
  }
  // Ctrl + Eで編集
  if (e.ctrlKey && !e.shiftKey && e.keyCode == 69) {
    if (mdArr.length == 0) {return;}
    edit();
    return;
  }
  // Ctrl + Sで保存
  if (e.ctrlKey && e.keyCode == 83) {
    if (mdArr.length == 0) {return;}
    save();
    return;
  }
  // Ctrl + Aで追加
  if (e.ctrlKey && e.keyCode == 65) {
    if (editFlg) {return;}
    add();
    return;
  }
  // Ctrl + Rでレポート出力
  if (e.ctrlKey && e.keyCode == 82) {
    if (mdArr.length == 0) {return;}
    report();
    return;
  }
  // Ctrl + Oで既定エディタで開く
  if (e.ctrlKey && e.keyCode == 79) {
    if (mdArr.length == 0) {return;}
    openEditor();
    return;
  }
  // Ctrl + Shift + EでWinエクスプローラを開く
  if (e.ctrlKey && e.shiftKey && e.keyCode == 69) {
    openExplorer();
    return false;
  }
  // Ctrl + Shift + Vで画像貼り付け
  if (e.ctrlKey && e.shiftKey && e.keyCode == 86) {
    if (mdArr.length == 0) {return;}
    imgPaste();
    return false;
  }
  // ============================== 表示系 ==============================
  // Ctrl + 番号で、番号のファイルを表示 (0は最後尾)
  // 1~9
  if (e.ctrlKey && e.keyCode >= 49 && e.keyCode <= 57) {
    var targetIdx = e.keyCode - 49;
    if (targetIdx > mdArr.length - 1) {return;}
    view(targetIdx);
    return;
  }
  // 0
  if (e.ctrlKey && e.keyCode == 48) {
    if (mdArr.length == 0) {return;}
    view(mdArr.length - 1);
    return;
  }
  // Ctrl + Tabで次のファイルを表示
  if (e.ctrlKey && !e.shiftKey && e.keyCode == 9) {
    if (mdArr.length == 0) {return;}
    var targetIdx = currentViewFileId + 1;
    if (targetIdx > mdArr.length - 1) {
      view(0); // トグル
      return false;
    }
    view(targetIdx);
    return false;
  }
  // Ctrl + Shift + Tabで前のファイルを表示
  if (e.ctrlKey && e.shiftKey && e.keyCode == 9) {
    if (mdArr.length == 0) {return;}
    var targetIdx = currentViewFileId - 1;
    if (targetIdx < 0) {
      view(mdArr.length - 1); // トグル
      return false;
    }
    view(targetIdx);
    return false;
  }
  // ============================== コナミコマンド ==============================
  if (order == 0) {
    // 初回キー押下から3秒間, 入力を受け付ける
    setTimeout(function(){order = 0}, 3000);
  }
  order = e.keyCode == command[order] ? (order + 1) | 0 : 0;
  // 成功時
  if (order == command.length) {
    alert('コナミコマンドの入力を確認しました、スタイルを変更します');
    alert('元に戻す場合はF5を押してください');
    $('body').css('font-family', '"Comic Sans MS", "Comic Sans", cursive');
    $('body').css('color', 'mediumspringgreen');
    $('body').css('background-color', 'navy');
    $('.bars').css('background-color', 'gray');
    $('a').css('color', 'cyan');
    alert('私のおすすめQiitaを開きます')
    new ActiveXObject("WScript.Shell").Run(chrome + "https://qiita.com/neras_1215/items/f5b6e29c9fb870f1b4e3");
    order = 0;
  }
});

</script>
</html>
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up