トピック名による双方向リンクを生成するHandyFlowyスクリプトLinkBackLink

私はWorkFlowyを知識データベースとしても使っていまして、トピック名に語句、ノートに解説を書いて階層的に溜め込んでいます。

こういうときに解説内の語句を自動的にリンク化できたらと思うのは自然な流れ。

ただそれだけだと、解説で使っている語句に移動できても、語句からその語句を使っている解説に移動するにはいちいち検索をしないといけない。

リンクしている側からもされている側からも移動できるようにしたいと思い立ち、HandyFlowyのスクリプトを作ることにしました。

HandyFlowy

HandyFlowy

  • Michinari YAMAMOTO
  • 仕事効率化
  • 無料
それがLinkBackLinkスクリプトです。

LinkBackLinkをHandyFlowyにインポート *1*2*3*4*5*6

LinkBackLinkはトピック名でパターンマッチし、以下3種類のリンクを生成します。

SimilarName(紫)
トピック名が「表示中のトピック名」を含むか含まれる
Link(赤)
トピック名が「表示中のノート」に含まれる
BackLink(青)
ノートが「表示中のトピック名」を含む


パターンマッチは#@タグを除くトピック名を用いて、表示中のトピックと英数字1文字のトピックを除く全てのトピックに対して行われます。

リンク生成はトピック間を移動したタイミングで行われます。

リンクには「トピック名:ノート└ 子トピック名└ 子トピック名」の形で付加情報を追加しています。

以下、スクリプトの内容。

function createLinkBackLink() {
	// クリア
	$('.lbl').remove();

	// スタイルの埋め込み
	var style = '';
	style += '.lbl{border: solid 1px;margin:5px -10px 5px 0px;border-radius:5px;}';
	style += '.lbl-sn{border-color:#fdf;}';
	style += '.lbl-l{border-color:#fdd;}';
	style += '.lbl-bl{border-color:#ddf;}';

	style += '.lbl-div{font-size: 13px;white-space: pre-wrap;height: 20px;overflow: hidden;line-height: 20px;color: #777;display: -webkit-box;-webkit-box-orient: vertical;-webkit-line-clamp: 1;margin:3px 5px;word-break: break-all;}';

	style += '.lbl-a{margin: 0px 2px;padding:0px 2px;text-decoration:none!important; }';
	style += '.lbl-sn .lbl-a{background-color: #fdf;}';
	style += '.lbl-l .lbl-a{background-color: #fdd;}';
	style += '.lbl-bl .lbl-a{background-color: #ddf;}';
	$('head').append('<style class="lbl">' + style + '</style>');

	// Arrayの初期化
	var simNameArr = [];
	var linkArr = [];
	var backLinkArr = [];
	
	// ユーティリティ関数
	function removeHtmlTags(str) {
		return str ? str.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g,'') : '';
	};
	function convName(str) {
		str = str ? str.replace(/(?:^| )[@#][^ ]+/g,'').trim() : '';
		str = /^\w$/.test(str) ? '': str;
		return str;
	};
	function isContaining(str1, str2) {
		var ret = false;
		if(str1 && str2) {
			str1 = _.unescape(str1).toLowerCase();
			str2 = _.unescape(str2).toLowerCase();
			ret = (str2 && str1.indexOf(str2) > -1);
		}
		return ret;
	}
	
	// 表示中のプロジェクトの取得
	var $curPrj = $('.selected');
	var curPrj = project_tree.getProjectReferenceFromDomProject($curPrj);
	var curId = curPrj.getProjectId().slice(-12);
	var curName = removeHtmlTags(curPrj.getName());
	var curNote = removeHtmlTags(curPrj.getNote());
	
	// WorkFlowyタグを除いた現在トピック名が空なら終了
	var curNameCnv = convName(curName);
	if(!curNameCnv) return;

	// 走査関数
	function lookChildren(prjChildren) {
		_.each(prjChildren,function(prj){
			var id = prj.id.slice(-12);
			var name = removeHtmlTags(prj.nm);
			var nameCnv = convName(name);
			var children = prj.ch;
			// 共有トピック
			if(prj.added_subtree) {
				children = prj.added_subtree.rootProjectChildren
			}
			if(id != curId && nameCnv) {
				var note = removeHtmlTags(prj.no);
				var isSimilarName = (isContaining(nameCnv, curNameCnv) || isContaining(curNameCnv, nameCnv));
				var isLink = isContaining(curNote, nameCnv);
				var isBackLink = isContaining(note, curNameCnv);
				if(isSimilarName || isLink || isBackLink) {
					// 表示用
					var cont = '';
					if(note) {
						cont += ':' + note;
					}
					if(children) {
						_.each(children, function(child){
							cont += ' └ ' + child.nm;
						});
					}
					cont = removeHtmlTags(cont).replace(/\s{1,}/g, ' ');
					if(isSimilarName) {
						simNameArr.push({id:id, name:name, cont:cont});
					}
					if(isLink) {
						var idx = curNote.indexOf(name);
						linkArr.push({id:id, name:name, cont:cont, idx:idx});
					}
					if(isBackLink) {
						backLinkArr.push({id:id, name:name, cont:cont});
					}
				}
			}
			// 子要素の走査
			if(children) {
				lookChildren(children);
			}
		});
	}

	// 全トピックの走査
	var root_prj_children = MAIN_PROJECT_TREE_INFO.rootProjectChildren;
	lookChildren(root_prj_children);

	// HTML作成関数
	function createLblHtml(arr, typeOfLink) {
		var html='';
		var tpl = _.template('<div class="lbl-div" contenteditable="false"><a class="lbl-a" href="javascript:void(0)" ontouchstart="location.hash=\'/<%= id %>\';return false;"><%= name %></a><%= cont %></div>');
		_.each(arr,function(elm){
			html += tpl(elm)
		})
		if(html){
			html = '<div class="lbl ' + typeOfLink + '">' + html + '</div>';
		}
		return html;
	};
	
	// 各リンク要素作成
	var html='';
	html += createLblHtml(simNameArr, 'lbl-sn');
	html += createLblHtml(_.sortBy(linkArr, 'idx'), 'lbl-l');
	html += createLblHtml(backLinkArr, 'lbl-bl');
	if(html) {
		$curPrj.append(html);
	}
};

// 再読み込み時に実行
window.addEventListener('load', createLinkBackLink);
window.addEventListener('hashchange', createLinkBackLink);

*1:はてなブログの調子が悪いので公開トピック経由です。

*2:2017/06/25 00:50追記:性能改善しました。

*3:2017/06/25 11:50追記:Underscore.jsを使うようにしました。

*4:2017/06/25 15:00追記:取り込んだ共有トピックも検索対象となるように修正。

*5:2017/06/27 23:20追記:タグが前にある場合に対応。

*6:2017/07/09 20:20追記:遷移方法を改善。