アウトライナー(Dynalist)と仲良くなったという話。四つ目はブックマークレットの活用について。
前回まではDynalistが備えている機能の話だったが、今回はDynalistも普通のWebページと同じように考えてブックマークレットを作ってみるという話で、Dynalistというツールそのものについてではない。
また、一般論を展開するにはブックマークレットの可能性というのは広すぎるので、とりあえず実際に自分が何を必要として何をしたかだけを書くことにする。
なお、データの取得にはDynalistが提供してくれているAPIを使えば直接的に得られて良いのだが(実際私もAPIを使うこともある)、話が少し高度になってしまうので、APIを使わなくてもできることがあるよというメッセージとしてこの記事を書いている。
ライフログの一時置き場としてのDynalist
ブックマークレットの話に入る前に、それが必要となるまでの経緯について言葉にしておく。
Dynalistの積極的な活用を再開したのはごく最近のことだが、ライフログ的なメモとしてはずーーーっとほそぼそと使い続けていた。Androidで「Quick Dynalist」というアプリを使い、思いついたことや今やったこと、今読んだ記事などの記録をさっとDynalistに送るようにしている。
それらはただただログとして記録しているので、アウトライン操作が発生することはほぼない。Inboxに指定したファイルにひたすら列挙されていく状態である。この用途にDynalistを使っているのには深い理由はなく、単にクラウド保存のメモアプリとして簡便だというだけのことだ。他にもっとやりやすいツールがあれば別にそれでいいのだが、私にとってDynalist以上のものが今のところないのでDynalistにどんどん送っている。
で、ただただ記録が並んでいってもその後それを活かしにくいので、何らかの形で整理できたらいいのだが、Dynalist上での整理はなんだかうまくいかないので諦めた。変に弄って乱すよりも放置のほうがまだマシだと思って、月単位で括るという他はそのままにしていた。
なお、全部必ずDynalistに送るというわけでもなく、発生したログはDynalistに網羅されてはいない。紙に書いていることもあるし、同時に使っている他のツールということもある。転記してまとめてというのがどうも辛かったので、「Dynalistか、このノートか、このツールの中にある」くらいの幅を持たせた状態でやっていた。(その状態が正解とは言い難い。)
Dynalistから自作ツールへの転記
そんな感じの分散は結構長く続いていたが、最近それが解決に向かいつつある。
前にも少し言及したが、ここ何ヶ月は「TextManager」と名付けた自作アプリケーションに日記・日誌をつけている。昔使っていた種々の日記アプリのデータもまとめてインポートしていて、日記ツールとしては過去一番の納得具合である。データの件数は何千件かになっている。
で、なるべくこのアプリケーションに記録を集約したい。しかしこのアプリケーションはPCでローカルサーバーを立ててWebブラウザで動かしているので、その環境が整った状況下でしか使えない。つまりPCを閉じたらもう記録できないのである。スマホアプリを自分で作るとかいうことも多分頑張れば不可能ではないのだが、面倒くさいのでやっていない。スマホからは相変わらずQuick Dynalistで記録をつけている。
仕方ないので、PCでDynalistを開いて時々Inboxをチェックし、TextManagerにコピペしていた。
記録した日時はバレット部分をマウスオーバーすると表示されるのだが、そうやって確認して自分のツールに打ち込むという作業はとてもやりにくい。途中からはコンソールでちょっとしたコードを実行し、各ノードのバレット横あたりに日時とテキストをまとめてコピーできるボタンを設置した。「コピペしやすいように工夫する」ということをひらめいたのである。
なんとなくお気づきかと思うが、これはだいぶ馬鹿っぽい工夫である。わざわざコードを書いて、得ている結果が「コピペしやすい」ということなのである。コピペしやすくして、そして一件一件ぽちぽちやっていくのだ。結局手作業!
アナログマインドから抜け出ていなかったので目的を達成する手段として思いついたことが間抜けだったが、しかしこのひらめきで重要なステップアップをした。HTMLの構造を解析して、必要なデータの在り処を特定し、それを取り出す手段をDynalist上に直に作ってしまうということだ。これは次回書くChrome拡張で活きることになる。
自作ツールのデータ形式のデータを生成する
今はChrome拡張のことは置いておいて、ここで必要なのはDynalistからTextManagerにデータを移す手段である。
「コピペしやすい工夫」をひらめいてからそんなに経たないうちに、一件ずつコピペという作業自体をなくすべきだしなくせるだろうということに気がついた。ノードそれぞれの日時とテキストを取り出す方法はもうわかったのだから、今表示しているノード全体についてデータを取得してJSONを作ってしまえばいいのである。
コピペしやすくしようとした際にはなぜかブックマークレットという発想が浮かばなくてコンソールで実行していたのだが、ブックマークレットにすれば実行は数倍簡単である。つまり、実行するとその時点で表示しているアウトラインからTextManager形式のJSONを作り出してクリップボードにコピーするブックマークレット、を作るのがゴールになる。
HTMLを知る
ここからは具体的にコードなどを書いていきたいと思う。まず、DynalistのHTML構造を確認する必要がある。ブラウザのデベロッパーツール(開発者ツール)を開いて要素を確認すればわかることだが、慣れていない人は大変だと思うので、参考までに理解に必要な部分を取り出してみる。
例えばこんなファイルがあったとして、このうち「Noratetsu Lab」のリンクが貼られているノードのHTMLを見てみよう。
<div class="Node"> | |
<div class="Node-self is-contentRendered is-noteRendered" spellcheck="false"> | |
<div class="node-line-strict node-hover"> | |
<div class="node-line node-icon mod-expand-collapse"></div> | |
</div> | |
<div class="node-backlink-counter u-hidden"></div> | |
<a class="node-line Node-bullet" tabindex="-1" title="Created at 18:34:53 on 2023/10/14, last edited at 18:53:17 on 2023/10/14" | |
href="https://dynalist.io/d/hoge#z=fuga"></a> | |
<div class="Node-checkbox"></div> | |
<div class="node-line Node-contentContainer"> | |
<div class="Node-content node-line needsclick" autocorrect="false" tabindex="-1" contenteditable="true" spellcheck="false">[Noratetsu Lab](https://noratetsu.blogspot.com/) </div> | |
<div class="Node-renderedContent node-line"><a target="_blank" href="https://noratetsu.blogspot.com/" tabindex="-1" class="node-link">Noratetsu Lab</a> </div> | |
</div> | |
<div class="Node-openNote" contenteditable="false" title="ノート欄" style="display: none;"></div> | |
<div class="Node-noteContainer"> | |
<div class="Node-note needsclick" autocorrect="false" tabindex="-1" contenteditable="true" spellcheck="false">ノート欄 </div> | |
<div class="Node-renderedNote"><span>ノート欄</span> </div> | |
</div> | |
</div> | |
<div class="Node-children"></div> | |
</div> |
なんだかすごく複雑にあれこれ要素が構築されているのがわかる。ツールとして多彩な機能を実装するためにはどうしても不可避のことであろう。
で、この中から自分に必要な情報を見つけ出す必要がある。じっくり見ていくと、本文はここだ、日時はここだ、というのがわかると思う(なおデベロッパーツールの「選択モード」を使えばより楽になる)。見つけられたらその情報を含んでいる要素のclass名をチェックする。
- 本文→
Node-content node-line needsclick
(div要素) - URL→
node-line Node-bullet
(a要素) - 作成日時・編集日時→同上
- ノート→
Node-note needsclick
(div要素)
そうしたら他の要素のclass名も見比べて、どう指定すればピンポイントに取り出せるかを判断する。例えば本文のclassにnode-line
が入っているが、これは他の要素にも使われているので、それだけではこの要素をピンポイントには取り出せないわけである。
とりあえずそれぞれclassを全部指定すれば他との重複はなさそうだ(実際にはこの範囲以外のHTMLも確認する必要がある)。つまりセレクタは次のようにすればいい。
- 本文→
div.Node-content.node-line.needsclick
- URL→
a.node-line.Node-bullet
- 作成日時・編集日時→同上
- ノート→
div.Node-note.needsclick
今回の場合はdivやaは付けなくてもいい。例えば本文は.Node-content.node-line.needsclick
でも大丈夫だ。ただし違うタグ名の要素に全く同じclassを付ける場合もあるようなので、正解はその時々で異なる。
自分が欲しいJSONを作る
さて、TextManager形式のオブジェクトというのは以下のような型である。*1
{
unix: number, // 作成日時のUNIX時間(ミリ秒ではなく秒)
keywords: string[], // タグ
body: string, // 本文
title: string, // タイトル(ブックマークレット中では設定しない)
attr: string, // 属性(ブックマークレット中では設定しない)
}[]
自分が非プログラマーなので、悩める非プログラマーの同志のためになるべく丁寧な解説をしたい、という気持ちはあるのだが、とてもあるのだが、細かくやっていると記事が長くなるし労力的に大変になってしまうので、JavaScriptについては今回はばっさり割愛する。実際に作ったブックマークレットのコードがこちら。
(function () { | |
// 最上位ノードまたはフォーカスしているノードの子項目を取得する(孫以下は含めない) | |
const parent = document.querySelector('.Node-children'); | |
const children = parent.querySelectorAll(':scope > .Node-outer > .Node'); | |
/** | |
* DateオブジェクトからUNIX時間を取得する関数 | |
* @param {Date} date | |
* @returns | |
*/ | |
function getUnix(date) { | |
return Math.floor(date.getTime() / 1000); | |
} | |
// データのオブジェクトを入れる配列を用意する | |
const data = []; | |
// 各ノードについて必要な値を取得しTextManager形式のオブジェクトにする | |
children.forEach(outer => { | |
// 作成日時を取得する | |
const bulletElm = outer.querySelector('.node-line.Node-bullet'); | |
const regexp = /Created at ([0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}) on ([0-9]{4}\/[0-9]{1,2}\/[0-9]{1,2})/; | |
const match = bulletElm.title.match(regexp); | |
const date = match ? getUnix(new Date(match[2] + ' ' + match[1])) : 0; | |
// 下位項目も含めて本文を取得する | |
const textElms = outer.querySelectorAll('.Node-content.node-line.needsclick'); | |
const textArr = Array.from(textElms).map(elm => elm.textContent.trim()); | |
const text = textArr.join('/n'); | |
// データのオブジェクトを作り、配列dataに追加する | |
const obj = { | |
unix: date, | |
keywords: ['@QuickDynalist'], | |
attr: '', | |
body: text, | |
title: '', | |
} | |
data.push(obj); | |
}) | |
// 配列dataをJSON文字列にする | |
const txt = JSON.stringify(data, null, '\t'); | |
// クリップボードにコピーする | |
navigator.clipboard.writeText(txt) | |
.then(() => { | |
// コピーに成功したら | |
console.group('クリップボードにコピーしました。'); | |
console.log(txt.length < 100 ? txt : txt.slice(0, 100) + '…'); | |
console.groupEnd(); | |
alert('クリップボードにコピーしました。'); | |
}) | |
.catch(err => { | |
// コピーに失敗したら | |
console.log('コピーできませんでした。', err); | |
alert('コピーできませんでした。'); | |
}); | |
})(); |
これを以下のサイトなどで1行に短縮化し、ブックマークに登録して使う。
- ブックマークレット作成(クロクロ・ツールズ)
実行すると表示中のノードの情報がいい感じにJSONになってクリップボードにコピーされるので、それを自分のツールのJSONファイルに持っていってペーストし、ブラウザでツールのページをリロードするとツールにデータが取り込まれるというわけである。
一件ずつコピペしていくと馬鹿にならない時間が奪われるが、ブックマークレットを作ればそれ以降は一瞬で済んでしまう。ブックマークレットを実行する前に、Dynalist上で内容を確認していくらか形を整えるという作業はするが、それには何分もかからない。
これでめでたくDynalistと自作のツールが直線で結ばれた。ライフログがただただDynalistに溜まっていくだけになることはなくなり、記録しやすいものに記録し、管理しやすいもので管理する、という理想的な在り方に近づいた。これはかなり大きな変革である。
補足
一つ重要な注意点を書くと、たまたまDynalistはclass名が「意味」によって付けられているので指定がしやすい作りになっているが、class名を「スタイル」のために使っているタイプのHTMLだとこのようなやり方は使えない。また、HTML構造を永久に変えずにいてくれる保証も全然ない。どんなWebアプリにも使える手ではないし、あんまり依存すると使えなくなった時に困ることにもなる。その点は十分留意しておく必要があるだろう。
といっても最近のクラウドサービスはAPIを提供してくれていることが多いので、大抵は代替手段(というか正規の方法)があるだろうと思う。
参考リンク・用語集ページ
- 初心者向け!Chromeの検証機能(デベロッパーツール)の使い方
- Quick Dynalist - Noratetsu Lab Dict.
- Dynalist API - Noratetsu Lab Dict.
*1: UNIX時間とは、「1970年1月1日午前0時0分0秒」を基準にして(=UNIXエポック)、そこからの経過秒数で表現した時間のこと。