Obsidian Template Estimate (estimate or actual) ver
作成日: 2025/09/02
リスト行の「見積/実績時間」記入支援 — (分) 方式(Obsidian + Templater, インラインJS)
目的
カーソル行の「タスク名直前」に、所要時間を分で表す (n)(n=半角数字)を挿入・更新する。
既存の (n) がある場合は実績として上書きし、見積は保持しない(常に最新値=実績)。
入力仕様
実行時に分数 n を入力。半角数字以外は除去。空入力(未入力/非数字のみ)の場合は中断。
小数・単位・空白は不可。(例: "(7.5)"、"( 15 )" は不可)
対象行(行頭フォーマット)
次のいずれかの「チェックリスト行」を対象とする。キャプチャ後、タスク名直前に (n) を差し込む。
1. チェック済み + 開始–終了時刻: ^- \[x\] \d{2}:\d{2}-\d{2}:\d{2}\s*(.*)$ 例: "- x 09:00-10:15 タスク名" 2. 未チェック + 開始時刻のみ: ^- \[ \] \d{2}:\d{2}\s*(.*)$ 例: "- 09:00 タスク名"
3. 未チェック(時刻なし): ^- \[ \]\s+(.*)$ 例: "- タスク名"
4. 汎用:チェックボックス付き行(保険)^- \[(?:\s|x|X)\]\s+(.*)$
動作仕様
タスク名直前に既に (数字) がある場合は、それを除去してから (n) を付け直す(正規化)。
prefix + (n) + 半角スペース + task の形に整形。行末の余分な空白は削除。
4. 汎用に該当した場合も同様に処理。ただし 1)〜3) に優先度がある。
対象行に該当しない場合は処理せず通知(チェックリスト行を想定)。
カーソル挙動
反映後、挿入/更新した (n) の直後にカーソルを移動する。
I/O 例
入力: "- x 09:00-10:15 作業A" + n=25, 出力: "- x 09:00-10:15 (25) 作業A" 入力: "- 09:00 (15) 作業B" + n=10, 出力: "- 09:00 (10) 作業B"
入力: "- タスクC" + n=7, 出力: "- (7) タスクC"
例外/補足
(n) の位置が時刻ブロック側に紛れているレアケースにも対応(prefix 直後の (数字) を一旦除去)。
通知は Notice(未定義環境では console 出力)で行う。
実行は「編集モードでアクティブな Markdown ノート」を前面に開いた状態で行うこと。
code:js
<%*
function note(msg){ try{ new Notice(msg);}catch(e){ console.log("templater", msg);} } function resolveEditor(){
try {
// 1) Templaterの editor
if (typeof tp !== 'undefined' && tp?.editor && typeof tp.editor.getLine === 'function') return tp.editor;
const w = (typeof app !== 'undefined') ? app.workspace : null;
if (!w) return null;
// 2) Obsidian 1.5+ の activeEditor
if (w.activeEditor?.editor && typeof w.activeEditor.editor.getLine === 'function') return w.activeEditor.editor;
// 3) activeLeaf.view.editor
const v1 = w.getActiveLeaf?.()?.view;
if (v1?.editor && typeof v1.editor.getLine === 'function') return v1.editor;
// 4) getActiveView(旧APIが残っていれば)
const v2 = (typeof w.getActiveView === 'function') ? w.getActiveView() : null;
if (v2?.editor && typeof v2.editor.getLine === 'function') return v2.editor;
// 5) markdown leaf を総当り
const leaves = (typeof w.getLeavesOfType === 'function') ? w.getLeavesOfType("markdown") : [];
for (const leaf of leaves){
const v = leaf?.view;
if (v?.editor && typeof v.editor.getLine === 'function') return v.editor;
}
} catch(e){
console.log("resolveEditor error:", e);
}
return null;
}
const editor = resolveEditor();
if (!editor){
note("アクティブなエディタが見つかりません。編集モードのMarkdownノートを前面に開いて再実行してください。");
return;
}
// === ここから本処理 ===
const cur = editor.getCursor();
const lineNo = cur.line;
const raw = editor.getLine(lineNo) ?? "";
// 分数入力(半角数字以外は除去)
let n = await tp.system.prompt("分数 n を入力(半角数字)");
if (n == null) return; // キャンセル
n = String(n).replace(/\D/g, "");
if (!n.length) { note("数字が未入力です。中断します。"); return; }
// 対応行パターン
const patterns = [
/^(-\s\(?:x|X)\\s+\d{2}:\d{2}-\d{2}:\d{2}\s*)(.*)$/, // 1) - x 09:00-10:15 タスク /^(-\s\\s\\s+\d{2}:\d{2}\s*)(.*)$/, // 2) - 09:00 タスク /^(-\s\\s\\s+)(.*)$/, // 3) - タスク ];
let newLine = null;
for (const re of patterns) {
const m = raw.match(re);
if (!m) continue;
// タスク名直前の既存 (数字) を剥がしてから付け直し
const restStripped = rest.replace(/^\(\d+\)\s*/, "");
newLine = ${prefix}(${n}) ${restStripped}.replace(/\s+$/, "");
break;
}
if (newLine == null) {
note("対象外の行形式です(チェックリスト行を想定)。");
return;
}
// prefix 側に紛れた (n) の保険的除去 → 最終整形
newLine = newLine
.replace(/^(-\s\(?:\s|x|X)\(?:\s+\d{2}:\d{2}(?:-\d{2}:\d{2})?)?\s*)\(\d+\)\s*/i, "$1") .replace(/^(-\s\(?:\s|x|X)\(?:\s+\d{2}:\d{2}(?:-\d{2}:\d{2})?)?\s*)(.*)$/, $1(${n}) $2) .replace(/\s+$/, "");
// 反映
editor.setLine(lineNo, newLine);
// カーソルを (n) の直後へ
const idx = newLine.indexOf((${n}));
if (idx >= 0) editor.setCursor({ line: lineNo, ch: idx + ((${n})).length });
%>