Markdown記法+クラウド環境でノウハウを整理したくて試したこと
パソコン崩壊
先日、雷の影響で約10年間連れ添ったパソコンがとうとう壊れてしまいました。
ここ数日は奥さんが衝動買いして埃をかぶってたVistaのノートPCをひっぱりだして、環境設定にいそしむ毎日です。電源系の故障なのでハードディスクは無事でしたが、3ヶ月前にもディスプレイが雷の影響で壊れたりとなにかと家電製品にトラブルの多い我が家、クラウドを活用したデータバックアップ環境などを真面目に考え始めようかなと思ってます。
Markdown記法でGoogle Sitesっぽく管理したい
そんな訳で心機一転、安心して使用できるインフラ環境を整えるべく無料ストレージのサービス内容を調べてはGoogle SitesやGoogle Driveなどにメモしてるんですが、メモの量が増えるにつれ前々から感じていたこれらサービスがWYGWYG変換出力するヘンテコHTMLに対する違和感が気になりはじめました。
もっとプレーンなテキストのままデータを取っておきたい、Google Sitesでmarkdownが使えればなぁ・・・と
できない事を嘆いてもしょうがないのでmarkdownが使えるサービスを調べてみました。
- Markdown記法に対応しました - はてなブログ開発ブログ
- tumblrでmarkdown記法を使う - blog.sanojimaru.com
- 脱GitHub初心者を目指す人のREADMEマークダウン使いこなし術 - ゆっくりと…
う〜ん・・・Google Sitesみたいにサイトマップを自由に構成できてmarkdownが使えるサービスって全然無いんですね・・・
DropBox + JavaScript製markdownパーサーでなんとできないか?
ノウハウの集積と整理が主目的なので、自宅/会社に関わらず手軽に書き込め、カテゴリ分けなどの整理整頓が容易にできるというのが理想なのですが、markdownでとなるとなかなかめぼしいサービスが見つかりません。仕方が無いので半分は自力で何とかする方向で考えたのが以下構成。
- DropBoxのPublicフォルダにマークダウン記法でメモファイルを保存
- JavaScript製markdownビューワーでドキュメントを参照
この構成だと自宅PCにDropBoxのクライアントソフトを入れておけば、フォルダ/ファイルの変更も自動でWeb上に反映/公開されるんでアップロード忘れの心配も無く、会社からでも閲覧できます。ただDropBoxにはmarkdown表記のHTML変換表示機能がないので、JavaScript製のmarkdownパーサを見つけてきて自前でなんとかしようという考えです。(編集中のmarkdownをすぐにHTML形式で確認したい場合で、publicフォルダがwebに反映されるタイムラグが気になる時は、ローカルにインストールしたApacheのドキュメントルート(http.confのDocumentRoot)をPublicフォルダにして、http://localhostより確認)
会社からの編集については、DropBoxの場合、Web上での直接編集ができず、かと言ってクライアントソフトを入れるわけにもいかないので、ZeroPCというクラウドサービスを使います。ZeroPCはDropBox/Google Drive/Evernoteなどデータを統合管理することができるサービスで、DropBoxに置いてあるテキストファイルの直接編集や、フォルダ/ファイル構成の変更管理などが容易にできるようになっています。(変更内容もクライアントソフトと違い即座に反映されるので、編集についてはこちらの方が便利かもしれません。)ちなみにchromeであれば、DropBox上のテキストファイルを直接編集できる拡張機能ていうのもあります。
JavaScript製markdownパーサーを探す
ビューワー作りに必要なJavaScript製markdownパーサーを探したところいくつか見つかりました。
- MarkdownとxHTMLの相互入力変換に対応したWYSIWYGエディタ「Itty Editor」- MOONGIFT
- Javascript製Markdown記法パーサー、markdown-js - にのせき日記
- Showdown : Markdown 記法で記述したテキストを HTML 形式に変換する Javascript - PamGau
Itty Editor
Hostされてるファイル数も多く使い方を調べるのも面倒そうだったので、すぐあきらめました。
markdonw.js
これはリンク先に記載されてますが単純に以下のようにするだけ。
markdown.toHTML('# H1に変換される!');
これは簡単!と言うことでビューワーの実装は始めたのですが、いくつか致命的な問題にぶつかりました。
ネストしたリストがちゃんと変換されない
こういう記述が・・・
- aaa - bbb - aaa - bbb
こんななっちゃいます><
・aaa ・bbb ・bbb ・aaa
HTMLがエスケープされてしまう
こういう記述が・・・
<a target="_blank">link</a>
こんななっちゃいます><
<a target="_blank">link</a>
ここでは Markdown -> HTML の変換にshowdown.jsを採用した。なぜmarkdown.jsでないかというと、markdown.jsは「HTMLをembedできるマークアップ言語」という、Markdownの最も重要な要件を満たしていなかったから。
http://blog.livedoor.jp/dankogai/archives/51818456.html
他にも「1タブもしくはスペース4つ」による「pre code」に対応してないなどの問題もあったので、採用を断念しました。
markdownビューワーの実装
Query Stringから表示するドキュメントを判定する実装
まず仕様をどうするかですが、最初に考えたのは以下のような感じ。
1) markdownで書かれた各ドキュメントに対するリンクをQuery String付きで記述する。
<a href="?url=doc/hoge.md">hoge.md</a>
2) リンクのクリックで自身のページがリロードされるので、ページロード時の処理でlocation.searchよりdoc/hoge.mdを取得する。
var url = location.search.replace(/^\?url=(.+)/,'$1'); → doc/hoge.md
3) ajaxでコンテンツを取得し、showdonw.jsでパースしたHTMLを画面に表示する。リンクの記述が相対パスで済むように、ajax通信前にダミーのリンクオブジェクトを使って絶対パスを取得しておく。
var dummy = $('<a/>').prop('href',url); url = dummy.prop('href'); → http://dl.dropbox.com/u/xxxxx/doc/hoge.md dummy.remove(); $.ajax({url:url ...
で、実装してみたのですが、DropBox側の機嫌によるとこが大きいと思うのですが、時々すごくもっさりする事が・・・
ハッシュフラグメントから表示するドキュメントを判定する実装
上記の実装だと、リンククリックでのページリロード、リロード後のajaxでコンテンツ取得と、2回分の通信が必要になりましたが、ハッシュフラグメントにドキュメントurlを記述する事で通信回数を1回で済むようにしてみます。
1) markdownで書かれた各ドキュメントに対するリンクをハッシュフラグメントで記述する。
<a href="#!doc/hoge.md">hoge.md</a>
2) jQuery Hashchage Event(Ajaxやタブ切替には必須かも?ブラウザの「戻る」「進む」を有効にするjQueryのhashchangeプラグイン - 5509) で、urlの変更を検出し、location.hashよりdoc/hoge.mdを取得する。
var url = location.hash.replace(/^#!(.+)$/ig,'$1'); → doc/hoge.md
3) Query Stringの場合と同様に、コンテンツを取得し画面に表示する。
若干早くなったような気がします。
サイトマップの生成
フォルダの構成やファイル名の変更などは、DropBoxのクライアントツールで直接Web上に反映されますが、前述のリンクはマニュアルで修正する必要があります。面倒なのでVBScriptでサイトマップファイルをmarkdown形式で生成して、これをビューワーからAjaxで取り込むようにしてみます。
sitemapFile = "sitemap.md" filePattern = "^.*\.md$" docFolderName = "doc" set shell = WScript.CreateObject("WScript.Shell") set fs = CreateObject("Scripting.FileSystemObject") set mapFile = fs.CreateTextFile(sitemapFile) function main(rPath,vPath) set folder = fs.GetFolder(rPath) cPath = vPath if cPath <> "" then cPath = cPath & "/" end if set reg = New RegExp reg.Pattern = filePattern qty = Ubound(Split(cPath,"/")) folderSpc = "" if qty > 1 then folderSpc = space((qty-1)*4) end if fileSpc = "" if qty > 0 then fileSpc = space(qty*4) mapFile.WriteLine(folderSpc & "- " & folder.name) end if found = false for each file in folder.files if reg.Test(file.name) and file.name <> sitemapFile then mapFile.WriteLine(fileSpc & "- [" & file.name & "](#!" & docFolderName & "/" & cPath & file.name &")") found = true end if next for each sFolder in folder.SubFolders call main(sFolder.path,cPath & sFolder.name) next end function call main(shell.CurrentDirectory & "\" & docFolderName,"") mapFile.Close msgbox "Finish!"
サイトマップ生成結果(sitemap.md)
- Database - [mysql.md](#!doc/Database/mysql.md) - [oracle.md](#!doc/Database/oracle.md) - IDE - [eclipse.md](#!doc/IDE/eclipse.md) - [netbeans.md](#!doc/IDE/netbeans.md) - library - Java - [seasar.md](#!doc/library/Java/seasar.md) - [struts.md](#!doc/library/Java/struts.md) - JavaScript - [dojo.md](#!doc/library/JavaScript/dojo.md) - [jquery.md](#!doc/library/JavaScript/jquery.md) - [prototype.md](#!doc/library/JavaScript/prototype.md)
フォルダ/ファイル構成がかわる都度、実行する必要があるのでスマートじゃありませんが、いちいちマニュアル修正するよりはマシなので、とりあえずこれで良しとします。
必要な機能
実装するにあたり必要な機能を整理してみます。
- location.searchよりドキュメントのURLを判定し、Ajaxで取得し表示する
- location.hashよりドキュメントのURLを判定し、Ajaxで取得し表示する
- 指定したURLよりドキュメントをAjaxで取得し表示する(サイトマップの表示に必要)
- location.hashが変更されたら2.の処理をコールする
jQueryプラグインとして実装してみます。実行方法は以下のような感じ。
$(ドキュメントを表示する場所).exMarkdown();
表示したいドキュメントのURLが決まってる場合は以下のように実行(今回の例ではサイトマップ)。あと、サイトマップをドロップダウンメニューにしたいので、callbackも指定できるようにします。
$(ドキュメントを表示する場所).exMarkdown({ url : 'sitemap.md', callback : function(api){ api.getTarget().find('> ul')... } });
ソースです。
(function($){ $.ex = $.ex || {}; $.ex.markdown = function(idx , targets , option){ if (isNaN(idx)) { return $('body').exMarkdown(idx); } var o = this, c = o.config = $.extend({} , $.ex.markdown.defaults , option); c.targets = targets; c.target = c.targets.eq(idx); c.index = idx; c.singleton = $.ex.markdown.singleton; if(!c.singleton.converter){ c.singleton.converter = new Showdown.converter();; } if(c.url){ o.rendar(c.url); } else{ if(!c.singleton.bindHashChange){ $(window).hashchange(function(){ o.rendar(); }); c.singleton.bindHashChange = true; } o.rendar(); } } $.extend($.ex.markdown.prototype, { getTarget : function(){ var o = this, c = o.config; return c.target; }, getHashUrl : function(){ var o = this, c = o.config; var url = location.hash.replace(/^#!(.+)$/ig,'$1') || location.search.replace(/^\?url=(.+)$/ig,'$1'); if(!url || !(/^.+\.(md|txt)$/ig.test(url))) return ''; return url; }, rendar : function(url){ var o = this, c = o.config; url = url || o.getHashUrl(); if(!url) return; var dummy = $('<a/>').prop('href',url); url = dummy.prop('href'); dummy.remove(); $.ajax({ url : url, cache : false, dataType : 'text', success : function(text){ var html = c.singleton.converter.makeHtml(text); c.target.html(html); !c.callback || c.callback.apply(o,[o]); }, error : function(){ c.target.html('not found : ' + url); } }); } }); $.ex.markdown.defaults = { url : '', callback : '' } $.ex.markdown.singleton = { bindHashChange : false } $.fn.exMarkdown = function(option){ var targets = this,api = []; targets.each(function(idx) { var target = targets.eq(idx); var obj = target.data('ex-markdown') || new $.ex.markdown( idx , targets , option); api.push(obj); target.data('ex-markdown',obj); }); return targets; } })(jQuery);
実行
$('#contents').exMarkdown(); $('#nav').exMarkdown({ url : 'sitemap.md', callback : function(api){ api.getTarget().find('> ul').exDropDown({ horizonRootMenu : true, horizonListWidth : 100 }); } });