Markdown記法+クラウド環境でノウハウを整理したくて試したこと

パソコン崩壊

先日、雷の影響で約10年間連れ添ったパソコンがとうとう壊れてしまいました。
ここ数日は奥さんが衝動買いして埃をかぶってたVistaのノートPCをひっぱりだして、環境設定にいそしむ毎日です。電源系の故障なのでハードディスクは無事でしたが、3ヶ月前にもディスプレイが雷の影響で壊れたりとなにかと家電製品にトラブルの多い我が家、クラウドを活用したデータバックアップ環境などを真面目に考え始めようかなと思ってます。

Markdown記法でGoogle Sitesっぽく管理したい

そんな訳で心機一転、安心して使用できるインフラ環境を整えるべく無料ストレージのサービス内容を調べてはGoogle SitesGoogle Driveなどにメモしてるんですが、メモの量が増えるにつれ前々から感じていたこれらサービスがWYGWYG変換出力するヘンテコHTMLに対する違和感が気になりはじめました。
もっとプレーンなテキストのままデータを取っておきたい、Google Sitesmarkdownが使えればなぁ・・・と
できない事を嘆いてもしょうがないのでmarkdownが使えるサービスを調べてみました。

う〜ん・・・Google Sitesみたいにサイトマップを自由に構成できてmarkdownが使えるサービスって全然無いんですね・・・

DropBox + JavaScriptmarkdownパーサーでなんとできないか?

ノウハウの集積と整理が主目的なので、自宅/会社に関わらず手軽に書き込め、カテゴリ分けなどの整理整頓が容易にできるというのが理想なのですが、markdownでとなるとなかなかめぼしいサービスが見つかりません。仕方が無いので半分は自力で何とかする方向で考えたのが以下構成。

  • DropBoxのPublicフォルダにマークダウン記法でメモファイルを保存
  • JavaScriptmarkdownビューワーでドキュメントを参照

この構成だと自宅PCにDropBoxのクライアントソフトを入れておけば、フォルダ/ファイルの変更も自動でWeb上に反映/公開されるんでアップロード忘れの心配も無く、会社からでも閲覧できます。ただDropBoxにはmarkdown表記のHTML変換表示機能がないので、JavaScript製のmarkdownパーサを見つけてきて自前でなんとかしようという考えです。(編集中のmarkdownをすぐにHTML形式で確認したい場合で、publicフォルダがwebに反映されるタイムラグが気になる時は、ローカルにインストールしたApacheのドキュメントルート(http.confのDocumentRoot)をPublicフォルダにして、http://localhostより確認)

会社からの編集については、DropBoxの場合、Web上での直接編集ができず、かと言ってクライアントソフトを入れるわけにもいかないので、ZeroPCというクラウドサービスを使います。ZeroPCはDropBoxGoogle DriveEvernoteなどデータを統合管理することができるサービスで、DropBoxに置いてあるテキストファイルの直接編集や、フォルダ/ファイル構成の変更管理などが容易にできるようになっています。(変更内容もクライアントソフトと違い即座に反映されるので、編集についてはこちらの方が便利かもしれません。)ちなみにchromeであれば、DropBox上のテキストファイルを直接編集できる拡張機能ていうのもあります。

JavaScriptmarkdownパーサーを探す

ビューワー作りに必要なJavaScriptmarkdownパーサーを探したところいくつか見つかりました。

Itty Editor

Hostされてるファイル数も多く使い方を調べるのも面倒そうだったので、すぐあきらめました。

markdonw.js

これはリンク先に記載されてますが単純に以下のようにするだけ。

markdown.toHTML('# H1に変換される!');

これは簡単!と言うことでビューワーの実装は始めたのですが、いくつか致命的な問題にぶつかりました。

ネストしたリストがちゃんと変換されない
こういう記述が・・・

- aaa
	- bbb
- aaa
	- bbb

こんななっちゃいます><

・aaa
	・bbb
	・bbb
・aaa

HTMLがエスケープされてしまう
こういう記述が・・・

<a target="_blank">link</a>

こんななっちゃいます><

&lt;a target="_blank"&gt;link&lt;/a&gt;

dankogaiさんも指摘してます

ここでは Markdown -> HTML の変換にshowdown.jsを採用した。なぜmarkdown.jsでないかというと、markdown.jsは「HTMLをembedできるマークアップ言語」という、Markdownの最も重要な要件を満たしていなかったから。

http://blog.livedoor.jp/dankogai/archives/51818456.html

他にも「1タブもしくはスペース4つ」による「pre code」に対応してないなどの問題もあったので、採用を断念しました。

showdown.js

dankogaiさんも一押しのshowdown.js。使い方もこんな感じで至って簡単!

var converter = new Showdown.converter();
converter.makeHtml('# H1に変換される!');

markdownパーサーはshowdown.jsで決定です。(と思ったら、markdown記法のtableやdlには対応してないみたいです。すごく残念)

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で取り込むようにしてみます。

サイトマップ生成VBScript

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
		});
	}
});
ダウンロード

ご自由にどうぞ

独り言

  • OAuth認証DropBox API使って、リアルタイムプレビューでオンライン編集できるようなサービス作りたいなぁ
  • hatena diaryもmarkdown対応してくれないかなぁ。置換がすごいめんどい・・・
  • hatana blogに移行しようかな・・