Markdownはとても便利
Markdownは簡単に書けてとても見やすい。QiitaもMarkdownだ。
個人的にQiitaの書き方がとても気に入っているので、いつもメモや調査はMarkdownで書いてる。
だがせっかくのMarkdownをマークアップで見られないのが残念。なのでお手軽マークアップビューワが欲しかった。
より高機能なchrome版はこちら
つくったもの
いにしえの技術hta
で作成しました。jqueryと含めて以下の2ファイルのみで動きます。
起動時にショートカット作成して、Ctrl+Alt+Shift+Z
で開くようにしてます。
こんな感じ
機能紹介
ファイル一覧表示機能
viewerのフォルダが作業対象のフォルダになります。ここにメモが溜まっていく。
表示ファイル切り替え機能
ChromeのようにCtrl+1やCtrl+5で、1番目のファイル、5番目のファイルを表示する。Ctrl+0だと固定で最後のファイルを表示。
またCtrl+Tabで次ファイルを表示、Ctrl+Shift+Tabで前のファイルに移動、トグル式。(もちろんクリックでもOK)
ファイル追加機能
マークアップ表示機能
マークダウンファイルがマークダウンされて表示される。URLはhtaのままだとIEとかになるので、Chromeで開くようにしています。
※ソース中でChrome.exeのファイルパスを直で指定してるので、ないorパス違う場合は修正が必要
パースの内容については後述のソースを参照してください。
// ★Markdownをhtmlに変換
function mdToHtml(str) {
ファイル編集機能
画像ペースト
★ちょうおすすめ。Win+Shift+S
で部分スクショ→赤ペンんで落書きしてからこの機能使うといろいろはかどる。
クリップボードにある画像ファイルを「pngファイル」としてviewerのフォルダに保存し、Markdown形式のパスをペーストする。
サクラでmd用ペースト + クリップボードの画像を保存するマクロの紹介記事はこちら
※録画の手際わるくて一瞬主のデスクトップのみくちゃん映った、ゆるして
タグジャンプ機能
#
のマークダウンのとこにジャンプ(スクロール)する。Qiitaっぽいやつ!
サクラエディタのブックマーク機能のように使える。
既定エディタで開く機能
mdファイルをいつも開くアプリで開きます。↓の場合だとみんな大好きサクラエディタです。
矩形選択やグレップ、置換や正規表現検索などの高度な機能はさすがにないので、そういう編集が必要なときに使えます。
レポート出力機能
マークアップ後の状態を表示できるhtmlファイルなどを作成します。成果物はこう
中身は3種類。原本
、マークアップhtml
(画像やリンクなどもしっかり参照/表示できる)、関係する画像ファイルすべて
htmlはこんな感じになる。成果物のフォルダを丸ごとzipにしてしまえば、誰かに渡しても綺麗に見れるというわけだ。
以降のアップデート
(機能追加) コナミコマンドを追加しました
(機能追加) 編集中に、編集内容を破棄してキャンセルする機能を追加しました
(機能追加) この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。他は未検証。
・ここから以下を、「リンク先を保存」。
・またはこれを右クリックしてリンク先を保存
じゃあ導入してみよう
以下をコピペし「なんか.hta
」で保存+jqueryを右クリックしてリンク先を保存
または、私のサーバに置いた完成品zipをクリックでDLすればok
※jqueryダウンロードがダメなら、普通にクリックしてブラウザに表示されたjsをコピペすればよいかも
職場だとむやみにダウンロードできないと思うので、こうやってhtaで用意しました。軽量だし気軽でよいね。
<!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, '&');
line = line.replace(/>/g, '>');
line = line.replace(/</g, '<');
line = line.replace(/"/g, '"');
line = line.replace(/'/g, ''');
// 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, '`');
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>