はじめに
我が家には古くからMOディスクが伝わっている.MOドライブも現存するものの,SCSI規格だ.当然,SCSI--USB変換アダプタも現存するが,Windows 10向けデバイスドライバが無くて動かない.MOディスクに何が入っているのか分からないでいた.
そんなとき,秋葉原のジャンク屋で見つけたUSB対応MOドライブで事態は急変する.Windows 10に接続すると,何も問題無くMOドライブとして認識してくれた!
さっそくMOディスクを開いてみると,どれもこれも,本当にどうでもいいようなデータばかり.そのゴミデータの中に面白そうなものを見つけた.
JavaScriptで組まれた,ツリー型ブックマーク!
タイムスタンプを見ると,2000年.今から18年前に書かれたものだ.とりあえず,Webブラウザで開いて見ると,それらしく機能してくれた(肝心のURLの方はほぼ全滅だったが).
当時はJavaScriptの制限と自分のスキルレベルのせいで,かなり苦労して書いた気がする.だけど今なら!スキルレベルは上がってるはず!現代のJavaScriptの仕様やライブラリを使えば,もっと簡単なアプローチで実現できるはず!そんなことを思った1.
当時の仕様
ファイル構成
./
|-- tree.txt
`-- base/
|-- top.html
`-- top.txt
読み込んでいるファイルがtree.txt
とかtop.txt
とかは,実際は*.js
ファイル.多分,エクスプローラーからダブルクリックするだけでメモ帳が開いてすぐ編集できるということで,あえて*.txt
にした気がする.
動作
ブックマークと言っても,ブラウザのブックマーク機能に組み込むとかそんな話ではない.単純に,HTMLファイルを開いたら,1つのページとしてブックマークが木構造に表示されるだけ.
□TOP
│
├▼グループ1
│├■ブックマークA
│└■ブックマークB
└▼グループ2
└■ブックマークC
実際は,「▼」をクリックすることで,その下層の枝の表示,非表示を切り替えられる.
ブックマークは通常のリンク<a href="url">bookmark</a>
だ.
ソース
一応,当時のソースはIEで何となく動いてる気がする.Firefoxでも何とかなる.VivaldiでもOK.
HTML
<HTML>
<head>
<link REL=STYLESHEET HREF="style.css">
<title>お気に入り</title>
</head>
<body>
<script language="javascript" src="top.txt"></script>
<script language="javascript" src="../tree.txt"></script>
<script language="javascript">
PrintTree("top_data");
</script>
</HTML>
当時は<body>
タグを閉じてなくても,特に問題無かった.
ツリー表示プログラム
コメントもほとんど無いし,読まずにスクロールだけして.ソースが長いということだけ分かれば大丈夫.
今更,こんな昔のJavaScriptを真剣に読んでもしょうがない.
var s; //分解した文字列を格納する配列
var TNumber = 0; //枝の固有番号
var TEndCheck = false; //
var branch = 0; //現在の行の枝はどこにあるか
var oldbranch = 1; //1つ前の行の枝の状態
var brleng; //第何層まであるか?
var oldleng = 1; //
var Icheck = true; //
var LColor = "green"; //節の色
var TColor = "brown"; //木の色
var EColor = "red"; //エラーの色
var RootCheck = false; //ルートは1つの木に1つのみ
var Ccolor = "white"; //コメントの色
var tree_kage_color = "#007700";
var tree_waku_color = "#00F000";
var tree_kage_clear = 55;
var tree_waku_clear = 90;
var tree_allWidth = 500;
var tree_allHeight = 30;
var tree_data_zure = 5;
var tree_kage_d = 5;
var Tree_about_check = true;
document.write("<script language=\"JavaScript\" src=\"../waku3D.js\"></script>");
function PrintTree(funct_name) {
waku3D_init();
data.innerHTML = "<table><tr><td><font color=" + Ccolor + ">"
+ "<div id=\"comment\"></div></font></table>";
with(document) {
write("<form name='info'>");
write("<script language=\"JavaScript\">");
write(funct_name,"();");
write("</script>");
write("</form>");
}
for (i = 0; i < 32; i++) {
if ((brleng & (1<<i)) != 0) document.write("</span>");
}
Tree_ver_link_waku();
}
function initTree() {
RootCheck = false;
branch = 0;
oldbranch = 1;
oldleng = 1;
Icheck = true;
}
function MakeTree(d) {
var default_open = false; //defaultの枝の開閉の状態
s = d.split(",");
if(s[0].indexOf("#") == 0) {
s[0] = s[0].substring(1, s[0].length);
default_open = true;
}
switch (s[0]) {
case "5":
if (RootCheck) break;
document.write("<br>");
case "0":
if (RootCheck) break;
MakeLeaf("", branch, "□,");
brleng = 0;
oldleng = 1;
RootCheck = true;
break;
case "9":
MakeBranch(oldleng, oldbranch, "<br>,");
case "1":
MakeLeaf(brleng, branch, "├,▼");
oldleng = ++brleng + 1;
branch = (branch << 1) | 1;
oldbranch = (branch << 1) | 1;
if(!default_open) document.write("<span id=TREE", TNumber, ">");
else document.write("<span id=TREE", TNumber, " style=display:block>");
TNumber++;
break;
case "8":
MakeBranch(oldleng, oldbranch, "<br>,");
case "2":
MakeLeaf(brleng, branch, "└,▼");
oldleng = ++brleng + 1;
branch <<= 1;
oldbranch = (branch << 1) | 1;
if(!default_open) document.write("<span id=TREE", TNumber, ">");
else document.write("<span id=TREE", TNumber, " style=display:block>");
TNumber++;
break;
case "7":
MakeBranch(brleng, branch, "│<br>,");
case "3":
MakeLeaf(brleng, branch, "├,■");
break;
case "6":
MakeBranch(brleng, branch, "│<br>,");
case "4":
oldleng = brleng + 1;
oldbranch = branch << 1;
MakeLeaf(brleng, branch, "└,■");
TEndCheck = true;
for (i = 1; (i < 33) && ((branch & 1) == 0); i++) branch >>= 1;
branch >>= 1
brleng -= i;
break;
case "c":
if (TEndCheck) {
TEndCheck = false;
MakeLeaf(oldleng, oldbranch, ",");
TEndCheck = true;
}
else MakeLeaf(oldleng, oldbranch, ",");
break;
case "END":
EndTree(oldleng - 1, oldbranch >> 1, TEndCheck);
break;
default:
err("undefBranch", s[0]);
}
}
function MakeLeaf(len, br, fig) {
MakeBranch(len, br, fig);
if (s[3] != undefined && s[3] != "") {
s[2] = s[2].fontcolor(s[3]);
}
if (s[1] == "back") document.write(s[2].link("javascript:history.go(-1)"));
else if (s[1] == "mail") {
s[1] = "javascript:OpenMailer('" + s[2] + "', '')";
document.write(s[2].link(s[1]));
if (s[4] != undefined && s[4] != "") {
s[1] = "javascript:OpenMailer('" + s[2] + "', '" + s[4] + "')";
document.write(" / ", s[4].link(s[1]));
}
}
else if (s[1] == "input") {
if (Icheck) document.write(s[2], "<input name='qt'>");
else err("onlyOne", s[2]);
Icheck = false;
}
else if (s[1] == "") document.write(s[2]);
else if (s[1].indexOf("http://") != 0) document.write(s[2].link(s[1]));
else if (s[4] == undefined) document.write("<a href=", s[1], " target=_blank>", s[2], "</a>");
else {
with(document) {
write("<a href=", s[1], " target=_blank");
write(" onmouseover=c_data(\"", s[4], "\") onmouseout=h()>");
write(s[2], "</a>");
}
}
document.write("<br>");
}
function MakeBranch(len, br, fig) {
EndTree(oldleng - 1, oldbranch >> 1, TEndCheck);
MB = fig.split(",");
b = br;
d = 1 << (len - 1);
for (i = 0; i < len; i++) {
if ((b & d) != 0) document.write("│".fontcolor(TColor));
else document.write(" ");
b <<= 1;
}
document.write(MB[0].fontcolor(TColor));
if (MB[1] == "▼") {
document.write("<font style=cursor:hand onClick=TreeOpen(TREE", TNumber, ")>");
document.write(MB[1].fontcolor(LColor));
document.write("</font>");
}
else document.write(MB[1].fontcolor(LColor));
}
function TreeOpen(x) {
with(x) {
style.display = (style.display == "block" ? "none" : "block");
}
}
function EndTree(len, br, ch) {
if (ch) {
document.write("</span>");
for (i = 0; i < len; i++) {
if (((br >> i) & 1) == 1) break;
document.write("</span>");
}
}
TEndCheck = false;
}
function OpenMailer (em, nic) {
if (nic == "") n = document.info.qt.value; else n = nic;
if (n == "") location.replace("mailto:" + em);
else location.replace("mailto:" + n + " <" + em + ">");
}
function err(code, x) {
document.write("ERROR:".fontcolor(EColor));
switch (code) {
case "undefBranch":
document.write(("'" + x + "'は未定義の分岐コード<br>").fontcolor(EColor));
break;
case "onlyOne":
document.write("InputBoxは全体で1つだけ ".fontcolor(EColor));
document.write(x);
break;
}
}
function c_data(msg) {
if (Tree_about_check == true) {
kage_color = tree_kage_color;
waku_color = tree_waku_color;
kage_clear = tree_kage_clear;
waku_clear = tree_waku_clear;
allWidth = tree_allWidth;
allHeight = tree_allHeight;
data_zure = tree_data_zure;
kage_d = tree_kage_d;
waku3D_init();
Tree_about_check = false;
}
comment.innerText = msg;
waku_left(600, 16);
waku_top(25);
//kage.style.visibility = "visible";
//waku.style.visibility = "visible";
data.style.visibility = "visible";
//fadein(1, 10, 5);
}
特筆事項があるとすれば,変数が全部グローバル・・・というのも,当時はまだJavaScriptにスコープが無かった.そもそもvar
が無くても未定義の識別子はそれが出現した時点で,新しい変数が定義されたものとして動いてしまう,便利そうで厄介な仕様があった.しかも,関数の中で定義してもそれ以降,グローバル変数になってしまうとか.そういう仕様でどうやったら全体で重複することなく名前付けができるのか考えて達した答えがこの書き方だった.
あと,同時に発掘されたドキュメントによると,木の階層は十数段までの制限があるらしい.
なお,ところどころにwaku
とかkage
とか出てくるが,これはポップアップ表示のスクリプトで使っていたもので,そっちで定義されている.2000年のtree.txtのソースから,ポップアップ関連の箇所を一部削除してここに掲載しているのだが,途中で消すのがめんどくなっていくつか残っている.
今のブラウザのブックマーク機能には「ブックマークの検索」が内蔵されているが,このブックマークでも似たようなことをしようとしていた.<input>
を使っているのがその痕跡である.ただ,どうやったら検索できるのか分からなくて,形だけである.
それにしても,ソース長いな.
データ
function top_data() {
MakeTree("5,back,表紙");
MakeTree("#9,,testA,green");
MakeTree("3,url2,test2");
MakeTree("3,url3,test3");
MakeTree("4,url4,test4");
MakeTree("#8,,testB,green");
MakeTree("3,url5,test5");
MakeTree("3,url6,test6");
MakeTree("#9,,testBA,green");
MakeTree("4,http://url8,test8");
MakeTree("#8,,testBB,green");
MakeTree("4,http://url10,test10");
MakeTree("END");
}
データはMakeTree関数呼び出しの引数で表現される.そして,よく見ると,何と引数は1個である!
データはカンマで区切られた1個の文字列で表現されている.
当時は引数を省略できなかったはずなので,文字列処理で実現したんだと思う.
最初の数値が各項目の前に書かれる罫線の形状と分岐のありなしを表現している.
MakeTreeが書こうとしている枝葉が何段目の階層かは,上から順番に最初の数値を解析していけば分かる・・・ということはつまり,関数の外側で木の状態が管理されているということになる.外部結合になるのであんまり良くはないのだが,データに第何階層か直接表記する必要はなくなるというメリットがある.
引数はURL,タイトル,色と続く.当時はまだ廃止されていなかった<font>
タグを使って文字の色変えることができた.
実はこの次にコメントを指定することができる.コメントを指定した場合,表示されているそのタイトルにマウスカーソルを持って行くと,ポップアップしてコメントが表示される・・・・・・ということを,さっきIEで動かして気づいた.この機能は普段使っているFirefoxでは動かないのですっかり忘れていた.Vivaldiでも動くようだ.今回,この機能の作り直しは行わない.
スタイルシート
html, h1, h3, a{font-family:DFJPPOP体;}
h1{font-size:35;}
h2{font-size:35;
text-decoration:none;}
html, h3, a{font-size:17;}
a{color:blue;
text-decoration:none;}
a:HOVER{color:red;}
span{display:none}
当時はPOP体が好きだった.
現代ではfontタグが廃止され,文字の色はすべてスタイルシートで指定するようになっている.
現代風に書き直す
18年前と現在ではJavaScriptやHTMLの仕様がかなり違っている.大体やりたいことも分かっている.なので,下手に昔のソースをリファクタリングするより,一から作っていった方が良い気がする.
今回は木構造の表示に注力するので,他の部分で機能を多少落とすことになる.
方針
多分,普通にjQuery使えば,かなり楽に同じことができるはず.
最近はクラスもまともに作れるようになっているので,それもついでに使ってみる.
実装
html
<!DOCTYPE html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<head>
<title>お気に入り</title>
<script type="text/javascript" src="./jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="./bookmarkdata.js"></script>
<script type="text/javascript" src="./bookmarkdata2.js"></script>
<script type="text/javascript" src="./tree.js"></script>
<script type="text/javascript">
$(() => {
const treeobj = new TreeObj();
$('#tbookmark').append(treeobj.makeTree(BookmarkData));
});
</script>
</head>
<body>
<span id="tbookmark"></span>
</body>
</html>
さっそくjQueryを使っている.最近はラムダ関数も使えるので,ついでに使っている.
データ
本当はJSONファイルにしたかったけど,ローカルでファイルのロードをやろうとすると,Webブラウザの設定があったりめんどくさそうなので,*.jsファイルで定数データとして定義することにした.
const BookmarkData = [
{"title":"testA", "child":[
{"title":"test2", "url":"url2"},
{"title":"testBM", "disp":"hide","child":"BookmarkData2"},
{"title":"test4", "url":"url4"}
]},
{"title":"testB", "child":[
{"title":"test5", "url":"url1"},
{"title":"test6", "url":"url2"},
{"title":"testBA", "child":[
{"title":"test8", "url":"http://url8"}
]},
{"title":"testBB", "child":[
{"title":"test10", "url":"http://url10"}
]}
]}
];
const BookmarkData2 = [
{"title":"testA2", "child":[
{"title":"2test2", "url":"url2"},
{"title":"2test3", "url":"url3"}
]}
];
木構造のデータを最初から木構造で表現する.確か当時はJavaScriptの仕様上,こういう表現はできなかった.
葉や枝の形を決める数値はもう必要無い.今回は実装していないが,このデータ形式ならコメントも容易に追加可能である.
ブックマークデータはブックマークのリストである.
ブックマークはtitleを必ず持つ.
ブックマークはurlを持つことができる.
ブックマークがchildを持つ場合,dispを指定することができる.
dispがhideの場合,そこから下の木は初期状態で非表示となる.
dispをhide以外,または,指定しなかった場合は,そこからの下の木は初期状態で表示される,
childを持たない場合にdispを指定してもその値は無視される.
ブックマークはchildを持つことができる.
childはブックマークデータを直接書くか,他のブックマークデータの変数名を指定する.
このブックマークデータの場合,BookmarkData
を表示させると,testBMノードの下にBookmarkData2
が表示されることになる.なお,このままだと自分自身の木を呼び出すことができてしまう.うっかりやってしまうと大変なことになるので要注意.
木構造の表示
ブックマークデータが最初から木構造で表現されているので,それをそのまま木構造で表示させれば良く,かなりスマートなプログラムになった.
'use strict'
const propBranchID = Symbol();
class TreeObj {
// 木のID.このIDを使って木の表示/非表示を制御する.
// 木の階層には非依存なので,関数の外で定義している.
get BranchID() {return this[propBranchID]; }
// コンストラクタ
// startID 木のIDの初期値
constructor(startID = 0) {
this[propBranchID] = startID;
}
// 木を表示させる
// data ブックマークデータ
// rank ブックマークの左側に書く枝の形状
// disp "hide":初期状態で木を非表示,その他:初期状態で木を表示
makeTree(data, rank = "", disp = "show") {
const res = $('<span></span>').attr({"id":this.BranchID});
if(disp == "hide") {
res.hide();
}
let count = 0;
for(let branch of data) {
count++;
const lastleaf = (count == data.length); // 最後の一葉
if("child" in branch) { // 子がある場合
this[propBranchID]++; // 子のIDを作る
// 開閉する葉を作る
res.append(this.makeLeaf_(rank, branch, lastleaf, this.BranchID));
// 子の取得
let child;
if(typeof(branch.child) == "string") {
child = eval(branch.child);
}
else {
child = branch.child;
}
// 子の木を書く
const nextline = (lastleaf) ? " " : "│";
res.append(this.makeTree(child, `${rank}${nextline}`, branch.disp));
}
else { // 子がない場合
// 開閉しない葉を作る
res.append(this.makeLeaf_(rank, branch, lastleaf));
}
}
return res;
}
// 葉を作る
// rank ブックマークの左側に書く枝の形状
// branch これから作る木の枝
// lastleaf true:branchは最後の葉,false:branchは最後の葉ではない
// brid <0:branchは子が無い,その他:子のIDはbridである.
makeLeaf_(rank, branch, lastleaf, brid = -1) {
const res = $('<span></span>');
res.append(this.makeLine_(rank, lastleaf));
res.append(this.makeFig_(brid));
res.append(this.makeLink_(branch.title, branch.url));
res.append('<br />');
return res;
}
// 木の枝を作る
// rank ブックマークの左側に書く枝の形状
// lastleaf true:branchは最後の葉,false:branchは最後の葉ではない
makeLine_(rank, lastleaf) {
const res = $('<span></span>');
const line = (lastleaf) ? "└" : "├";
res.append(rank);
res.append(line);
return res;
}
// 葉の形状
// brid <0:branchは子が無い,その他:子のIDはbridである.
makeFig_(brid = -1) {
const res = $('<span></span>');
if(brid < 0) {
res.append("■");
}
else { // 木の開閉
res.click(() => {$(`#${brid}`).toggle();}).append("▼");
}
return res;
}
// リンクを作る
// title ブックマークのタイトル
// url titleからリンクするURL
// urlが存在しない場合は,titleだけ表示する.
makeLink_(title, url) {
let res;
if(url == undefined) {
res = $('<span></span>');
}
else {
res = $('<a></a>').attr({"href": url});
}
res.append(title);
return res;
}
}
かなり短く書けた.スコープもあるし,再利用しやすいようにクラスにしておいた.また,データ構造が木構造になったおかげで,再帰呼び出しを使って書けるようになった2.
装飾に関する記述が無くなったからというのもあるだろうが,それ以上にデータ構造の改善と再帰ができることは大きい.jQueryで簡潔に書けるようになったことも強力だ.
ついでに,当時は1ページに1個しか木を書けなかったと記憶しているが,このプログラムなら複数表示が可能である.TreeObjをもう1個作って,コンストラクタの引数に前に作ったTreeObjのプロパティBranchID
に1を加えた値を指定する.これで,木のIDの一意性が保たれるので,複数の木の表示が可能になる.
スタイルシート
今回はスタイルシートまで手が回らないのでやらないが,18年前のものを流用するときは最後の行span{display:none}
は削除する.
2018年のtree.jsではスタイルシートで色等を指定することを考慮して,かなり細かく<span>
を使って文字を区切っている.多分,class属性を指定する記述を追記するだけでいけると思う.
ポップアップに関する検討
今回はポップアップはやらないが,似たようなことをするライブラリが世の中には既に存在していて,広く使われているようだ.スクリプト部分はもうできているので,開発者は文字列を所定の形式で書くだけでOKのようだ.
まとめ
18年前に書かれたJavaScriptを現代の技術で書き直すことに成功した.予想通り,現代の技術であれば,当時よりも簡単に簡潔に記述可能になっていた.その少なくなった記述で若干の性能向上までも実現できた.基礎的な部分は2~3時間ぐらいで書けたと思う(ブラッシュアップにはもっとかかっているが).この内容で2~3時間は長いと思われるが,これは,jQueryの使い方を調べながら,いろいろ実験もしながら作っていたせいである.
JavaScript自体のバージョンアップやライブラリの整備だけでなく,Webブラウザに検証機能が搭載されたことも開発においては大きな進化だ.今回の件でも,この検証機能のおかげでデバッグがものすごくはかどった.
スタイルシートとかポップアップとか,やってない部分もあるけど,やりたかったのは木の表示部分だけなので,今回はこれでおしまいとする.