僕はObsidianのVault(ノートの保管場所)で、自分の考えを一つの焦点へ絞ったノートをZettelとして残しています。どの考えから派生したかを表すため、1、1.a、1.a.1のような階層IDを使います。
このIDは分類番号ではありません。1.aは1から生まれた考え、1.a.1は1.aからさらに派生した考え、という親子関係を表します。
Zettelが増えると、親を決めた後にも、既存の子IDを調べて次の空きを選び、重複しないことを確かめる作業が残りました。「Obsidianの親子ノートIDを手作業で管理しにくくなり、自動採番プラグインを作った」では、この空きIDの選択と重複確認だけを自動化の対象にした経緯を書いています。
この記事では、Zettel IDで親子関係を管理する人に向けて、自作したObsidianプラグインの設計をまとめます。ノート先頭のメタデータ欄(front matter)からIDを読み、ツリーを組み立て、選んだ親の最初の空き子IDでノートを作るまでを扱います。既存IDを別の親へ移す処理は、後続の記事で扱います。
まず全体像
Zettel IDから親子ツリーを組み立てる
プラグインは、設定でツリー表示の対象にしたMarkdownファイルのfront matterからZettel IDを読みます。フォルダの深さやノート同士のリンクから親子関係を推測するのではなく、IDをピリオドで区切ってツリーへ変換します。
例えば、1.a.1は次の順でたどれます。
1
└ 1.a
└ 1.a.1
1.aに対応するノートがなく、1.a.1だけが存在する場合も、表示用のツリーにはID経路を示す1.aの行を作ります。実ファイルを新しく作るわけではありません。ノートがない行には題名を表示せず、IDが表す経路だけを残します。
同じIDを持つファイルがすでに複数ある場合、プラグインは一つのツリーノードへまとめて表示します。既存の重複を自動修復する機能ではないため、ここで行うのは現在のIDを親子関係として読める形にするところまでです。
次の番号ではなく、最初の空きを選ぶ
子IDの採番では、選んだ親の直下にあるツリー上のIDを一覧として読みます。親の末尾が数字なら次の階層は英字、英字なら数字から始めます。
1の子は1.a、1.b、1.cと進む1.aの子は1.a.1、1.a.2、1.a.3と進む- ツリー上で使用中の候補は飛ばし、最初に見つかった空きを使う
このため、1.aと1.cがある状態で1の子を作ると、最大の1.cに続く1.dではなく、間に空いている1.bを選びます。1.a.1と1.a.3がある場合は、1.a.2です。
途中の実ファイルがなくても、子孫のIDが経路として使っていれば、そのIDは使用中として扱います。1.a.1だけが存在する場合も、実ファイルがない1.aはツリー上の経路として使われているため、1の次の子には1.bを選びます。
公開用の最小例にすると、採番部分は次の形です。
function increment(segment) {
if (/^\d+$/.test(segment)) {
return String(Number(segment) + 1);
}
const letters = segment.split("");
for (let index = letters.length - 1; index >= 0; index -= 1) {
if (letters[index] !== "z") {
letters[index] = String.fromCharCode(letters[index].charCodeAt(0) + 1);
return letters.join("");
}
letters[index] = "a";
}
return `a${letters.join("")}`;
}
function nextChildId(parentId, directChildIds) {
if (!parentId) throw new Error("親IDが必要です");
const last = parentId.split(".").at(-1);
let candidate = /^\d+$/.test(last) ? "a" : "1";
while (directChildIds.has(`${parentId}.${candidate}`)) {
candidate = increment(candidate);
}
return `${parentId}.${candidate}`;
}
確認する範囲を直下の子に限定したのは、採番する位置が「選んだ親の次の子」だからです。別の親の下に同じ末尾があっても、1.aと2.aは別のIDとして扱えます。
決めたIDをfront matterへ入れてノートを作る
空きIDが決まったら、プラグインはそのIDをfront matterへ入れたMarkdownノートを作り、Obsidianで開きます。プラグイン設定でテンプレートと保存先を指定している場合は、その設定を使います。
---
zettel_id: "1.b"
---
IDを文字列として保存するため、最上位の1も引用符付きで書き込みます。数字として解釈される値と、1.aのような文字列が混ざらないようにするためです。
ファイルを作成した後は、保持していたツリーを破棄して再構築します。新しいノートのfront matterから1.bを読み直すため、Obsidianのサイドパネルのツリー表示には1の子として新しい行が現れます。
ここでは、IDの重複とファイル名の重複を別に扱っています。子IDはツリー上のIDを見て避けます。作成先に同名ファイルがある場合は、zettel 1.b (1).md、zettel 1.b (2).mdのようにファイル名へ連番を足して上書きを避けます。
ツリー表示と子作成を同じ操作につなぐ
Obsidianの横に出すツリー表示では、ID、ノート名、子を追加するボタンを一つの行に置いています。ツリーを見て親にする考えを選び、その行から子ノートを作成します。
この操作にした理由は、親子関係の判断と採番を分けるためです。内容が近いノートをプラグインが自動で親にするのではなく、どの考えから派生したかは僕が選びます。その後の空きID確認、front matterへの書き込み、ノート作成、ツリー更新という繰り返し作業をプラグインへ任せます。
僕の運用では、Zettel IDは思考の派生を表すものです。親の選択まで自動化すると、内容の近さと、実際にどの考えから生まれたかを混同する可能性があります。そこで、意味を決める操作は人に残し、形式を保つ反復だけを自動化しました。
このプラグインが扱う範囲
このプラグインはVault全体の情報を整理するものではありません。Zettelとして残したノートの階層IDを読み、子ノートの作成とツリー表示を補助します。
今回の子作成で防ぐのは、新しく選ぶ子IDが既存の直下IDと重なることです。すでに同じIDを持つノートが複数ある場合の解消や、どのノートを親にすべきかの判断までは行いません。
また、この記事では新しい子を作る流れに範囲を絞りました。既存ノートを別の親へ移す場合は、そのノートだけでなく子孫のIDも変わります。変更前に新旧IDの対応を作り、IDの重複を調べる必要があります。親を自分自身や自分の子孫の下へ移して親子関係が循環する操作も拒否しなければならないため、別の記事で扱います。
まとめ
自作プラグインは、front matterのZettel IDを親子ツリーへ変換し、選んだ親の直下から最初の空きIDを探します。そのIDを入れたノートを作成した後、ツリーを再構築してサイドパネルへ表示します。
僕が決めるのは、どの考えから派生させるかです。IDの意味は人が決め、空きIDの確認、front matterへの書き込み、ノート作成、表示更新の繰り返しだけをプラグインで自動化しました。