moved
などと記したタスクを残すscript.jsimport {
goHead, goLine, moveRight, upBlocks, moveLinesBefore,
} from '../scrapbox-edit-emulation/script.js';
import {press} from '../scrapbox-keyboard-emulation-2/script.js';
import {cursor} from '../scrapbox-cursor-position-3/script.js';
import {line as l} from '../scrapbox-line-accessor/script.js';
import {insertText} from '../scrapbox-insert-text/script.js';
import {selection} from '../scrapbox-selection-2/script.js';
import {scrapboxDOM} from '../scrapbox-dom-accessor/script.js';
import {parse, nextTask, write, create} from '../takker-scheduler-2%2Ftask/script.js';
import {toYYYYMMDD, toHHMM} from '../scrapbox-timestamp/timestamp.js';
import {
format, parse as parseDate, isValid, isAfter, set,
addDays, subDays, addMinutes, isSameDay, compareAsc,
areIntervalsOverlapping,
} from '../date-fns.min.js/script.js';
script.jsconst interval = 5; // 5 minutes
export function addTask() {
const line = l(cursor().line);
const task = parse({lineNo: line.index});
// 現在行がタスクなら、それと同じ日付にする
// 違ったら今日にする
const date = task?.date ?? new Date();
// 予定開始時刻と見積もり時刻を計算する
// 予定開始時刻は、その前のタスクの見積もり時間にintervalを足したものだけずらしておく
const plan = {
start: task?.plan?.start ? (() =>
addMinutes(task.plan.start, interval + (task.plan.duration.minutes ?? 0))
)() : undefined,
duration: task?.plan?.duration,
};
// 書き込む
write({type: 'task', date, plan, lineNo: line.index}, {overwrite: false});
}
script.jsexport async function walkDay(count = 1) {
if (!selection.exist) {
_walkDay(count);
} else {
const {start: {lineNo: startNo}, end: {lineNo: endNo}} = selection.range;
for (let i = startNo; i <= endNo; i++) {
goLine({index: i});
await sleep(10);
await _walkDay(count);
}
}
}
async function _walkDay(count) {
const task = parse({lineNo: l(cursor().line).index});
if (!task) return; // タスクでなければ何もしない
const {date, ...rest} = task;
// 日付をずらして上書きする
await write({date: addDays(date, count), ...rest}, {overwrite: true});
}
script.jsexport async function moveToday() {
if (!selection.exist) {
_moveToday();
} else {
const {start: {lineNo: startNo}, end: {lineNo: endNo}} = selection.range;
for (let i = startNo; i <= endNo; i++) {
goLine({index: i});
await sleep(10);
await _moveToday();
}
}
}
async function _moveToday() {
const task = parse({lineNo: l(cursor().line).index});
if (!task) return; // タスクでなければ何もしない
const {date, ...rest} = task;
const now = new Date();
// 日付に変更がなければ何もしない
if (isSameDay(date, now)) return;
// 上書きする
await write({date: now, ...rest}, {overwrite: true});
}
script.jsexport function togglePlanningTime() {
const task = parse({lineNo: l(cursor().line).index});
if (!task) return; // タスクでなければ何もしない
const {plan, ...rest} = task;
// 上書きする
write({plan: {
duration: plan.duration,
// 予定開始時刻をtoggleする
start: !plan.start ? new Date() : undefined,
}, ... rest}, {overwrite: true});
}
script.wip.js// 見積もり時間を選択する
export function selectEstimatedTime() {
const task = parse({lineNo: l(cursor().line).index});
if (!task) return; // タスクでなければ何もしない
goHead();
moveRight(18);// `YYYY-MM-DD hh:mm なので18文字
// 4文字選択する
for (let i = 0; i < 4; i++) {
press('ArrowRight', {shiftKey: true});
}
}
script.jsexport function startTask() {
const task = parse({lineNo: l(cursor().line).index});
if (!task) return; // タスクでなければ何もしない
const {record, ...rest} = task;
if (record.end) return;// すでに終了していたら何もしない
// 上書きする
write({record: {
// 開始時刻をtoggleする
start: !record.start ? new Date() : undefined
}, ... rest}, {overwrite: true});
}
script.jsexport async function endTask() {
const task = parse({lineNo: l(cursor().line).index});
if (!task) return; // タスクでなければ何もしない
const {record, ...rest} = task;
if (!record.start) return;// まだ開始していなかったら何もしない
const newTask = {record: {start: record.start,
// 終了時刻をtoggleする
end: !record.end ? new Date() : undefined,
}, ...rest};
// 上書きする
await write(newTask, {overwrite: true});
if (!newTask.record.end) return;
// repが指定されていたら、次のタスクを作成する
const nextTaskObj = nextTask(newTask);
if (!nextTaskObj) return;
// 新しい行を挿入する
await write(nextTaskObj, {overwrite: false});
}
script.jsexport async function toggleTask() {
const task = parse({lineNo: l(cursor().line).index});
if (!task) return; // タスクでなければ何もしない
const {record, ...rest} = task;
// 開始していないときは開始する
if (!record.start) {
write({record: {start: new Date()}, ... rest}, {overwrite: true});
return;
}
// 終了していないときは終了する
if (!record.end) {
const newTask = {record: {start: record.start, end: new Date(),}, ...rest};
await write(newTask, {overwrite: true});
// repが指定されていたら、次のタスクを作成する
const nextTaskObj = nextTask(newTask);
if (!nextTaskObj) return;
// 新しい行を挿入する
await write(nextTaskObj, {overwrite: false});
return;
}
// すでに終了しているタスクはリセットする
write({record: {}, ... rest}, {overwrite: true});
}
script.jsexport async function posterioriEndTask() {
const task = parse({lineNo: l(cursor().line).index});
if (!task) return; // タスクでなければ何もしない
const {record, ...rest} = task;
if (record.start || record.end) return; // 開始していないタスクのみが対象
// 直近のタスクを検索する
const lineNo = l(cursor().line).index;
const targetlineNo = [...scrapbox.Page.lines.keys()]
.slice(0, lineNo + 1)
.reverse()
// 終了しているタスクのみ検索する
.find(lineNo => parse({lineNo})?.record?.end);
// 開始時刻と終了時刻を計算する
const end = new Date();
const start = targetlineNo ? parse({lineNo: targetlineNo}).record.end : end;
const newTask = {record: {start, end,}, ...rest};
// 上書きする
await write(newTask, {overwrite: true});
if (!newTask.record.end) return;
// repが指定されていたら、次のタスクを作成する
const nextTaskObj = nextTask(newTask)
if (!nextTaskObj) return;
// 新しい行に挿入する
await write(nextTaskObj, {overwrite: false});
}
Alt+↑/↓
を使って入れ替えるscript.jsexport async function sort() {
if (scrapbox.Page.lines.length < 3) return;
const startNo = 2;
const endNo = scrapbox.Page.lines.length - 1;
script.js const sortedTasks = scrapbox.Page.lines
.slice(startNo, 1 + endNo)
.flatMap((line, i) => {
const task = parse({lineNo: i + startNo});
return task ? [{id: `L${line.id}`, task}] : [];
})
.sort((a,b)=> compareAsc(
a.task.record?.start ?? a.task.plan?.start ?? a.task.date,
b.task.record?.start ?? b.task.plan?.start ?? b.task.date
));
// 一番上から順に入れ替え作業をする
for (let i = 0; i < sortedTasks.length; i++) {
// taskの順番を計算する
const presentPosition = scrapbox.Page.lines
.slice(startNo, 1 + endNo)
.filter(line => !/^\s+/.test(line.text)) // top level indent lineのみ数える
.findIndex(line => `L${line.id}` === sortedTasks[i].id);
//console.log({presentPosition});
if (presentPosition === i) continue;
goLine(sortedTasks[i].id);
await sleep(10);
upBlocks(presentPosition - i);
}
// 見出しもきれいにする
insertSections();
}
script.jsasync function insertSections() {
const sections = [
'[** 00:00 - 03:00] 未明',
'[** 03:00 - 06:00] 明け方',
'[** 06:00 - 09:00] 朝',
'[** 09:00 - 12:00] 昼前',
'[** 12:00 - 15:00] 昼過ぎ',
'[** 15:00 - 18:00] 夕方',
'[** 18:00 - 21:00] 夜のはじめ頃',
'[** 21:00 - 00:00] 夜遅く',
];
const pageDate = getPageDate();
if (!isValid(pageDate)) return;
// 前の日付ページへのリンクを挿入する
const indexLine = `yesterday: [${getDiaryPageTitle(subDays(pageDate, 1))}]`;
if (scrapbox.Page.lines.findIndex(line => line.text === indexLine) !== -1) {
const index= scrapbox.Page.lines.findIndex(line => line.text === indexLine);
goLine(index);
await sleep(10);
moveLinesBefore({from: index, to: 0});
} else {
goLine(0);
await sleep(10);
press('Enter');
insertText({text: indexLine});
await sleep(1);
}
// 見出しを挿入する
// 挿入位置を計算する
const tasks = scrapbox.Page.lines
.flatMap((_, lineNo) => {
const task = parse({lineNo});
return task ? [task] : [];
});
const insertIds = sections.map((_, i) => {
// 最初と最後のsectionの挿入位置はわかっている
if (i === 0) return {position: 'previous', id: l(tasks[0].lineNo).id};
// sectionの開始・終了時刻
let sectionDates = {
start: new Date(pageDate),
end: new Date(pageDate),
};
sectionDates.start = set(sectionDates.start, {hours: i * 3, minutes: 0});
sectionDates.end = set(sectionDates.end, {hours: (i + 1) * 3, minutes: 0});
// 挿入位置を探す
for (const task of tasks) {
const {record, plan, date, lineNo} = task;
const start = record.start ?? plan.start ?? date;
const end = record.end ?? plan.start ?? date;
// sectionを跨いでいるタスクの場合
if (areIntervalsOverlapping(sectionDates, {start, end})) {
const forward = sectionDates.start.getTime() - start.getTime();
const backward = end.getTime() - sectionDates.start.getTime();
if (forward > backward && isAfter(sectionDates.end, end)) {
// sectionのすぐ後ろに入るタスクと判断する
//console.log(['border, next',_, l(lineNo).DOM]);
return {id: l(lineNo).id};
} else {
// sectionのすぐ前に入るタスクと判断する
//console.log(['border, previous',_ ,l(lineNo).DOM]);
return {position: 'previous', id: l(lineNo).id};
}
continue;
}
// あとはどちらか一方にきれいに収まるタスクである場合しか無い
// 前のタスクとの間にあるかどうかを調べる
if (isAfter(sectionDates.start, start)) continue;
const i = tasks.indexOf(task);
const prev = tasks[i - 1];
// sectionのすぐ後ろに入るタスクと判断する
if (!prev) return {position: 'previous', id: l(lineNo).id};
const prevEnd = prev.record.end ?? prev.plan.start ?? prev.date;
// sectionのすぐ後ろに入るタスクと判断する
if (isAfter(sectionDates.start, prevEnd)) return {position: 'previous', id: l(lineNo).id};
}
// 見つからなかったら末尾に入れる
//console.log(['not found',_]);
return {id: l(tasks[tasks.length -1].lineNo).id};
});
//console.log(insertIds.map((id, i) => [sections[i], id.position, l(id.id).text, l(id.id).DOM]));
// 見出しを挿入する
for (const section of sections) {
const id = scrapbox.Page.lines.find(line => line.text === section)?.id;
const {position, id: insertId} = insertIds[sections.indexOf(section)];
if (id) {
const insertNo = l(insertId).index + (position === 'previous' ? -1 : 0);
goLine(id);
await sleep(10);
moveLinesBefore({from: l(id).index, to: insertNo});
} else {
goLine(insertId);
await sleep(10);
if (position === 'previous') {
goHead();
press('Enter');
press('ArrowUp');
} else {
press('Enter');
goHead();
selectLine();
}
insertText({text: section});
await sleep(1);
}
}
}
diaryPage()
と parseDiary()
で分離はできているのかscript.jsexport async function transport({targetProject}) {
const diaryDate = getPageDate();
// 検索する範囲
const {startNo, endNo} = selection.exist ?
(() => {
const {start,end} = selection.range;
//console.log({start,end});
return {startNo: start.lineNo, endNo: end.lineNo};
})() :
// 選択範囲がなかったらタイトル行以外を選択する
{startNo: 1, endNo: scrapbox.Page.lines.length - 1};
// 移動するタスクを取得する
// まずタスクを取得する
const tasks = scrapbox.Page.lines
.slice(startNo, endNo + 1)
.flatMap((_, i) => {
const task = parse({lineNo: i + startNo});
return task ? [task] : [];
});
// 違う日付のタスクはすべて移動する
const targetTaskLines = tasks
.flatMap(({date, lineNo}) =>
isValid(diaryDate) && // 日付ページでなければ、全てのタスクを転送する
isSameDay(date, diaryDate) ?
[]:
[{date, line: l(lineNo)}]
);
// 日付ごとにタスクをまとめる
let bodies = {};
targetTaskLines.forEach(({date, line}) => {
const key = format(date, 'yyyy-MM-dd');
bodies[key] = {
date,
texts: [
...(bodies[key]?.texts ?? []),
// indent blockで移動させる
...scrapbox.Page.lines
.slice(line.index, line.index + line.indentBlockLength + 1)
.map(line => line.text)
],
};
});
// 対象の行を削除する
for (const {line} of targetTaskLines) {
goLine(line.id);
await sleep(10);
goHead();
press('ArrowLeft');
for(let i = 0; i <= line.indentBlockLength; i++) {
press('ArrowRight', {shiftKey: true});
press('End', {shiftKey: true});
}
press('Delete');
}
// 新しいタブで開く
for (const [key, {date, texts}] of Object.entries(bodies)) {
const body = encodeURIComponent(texts.join('\n'));
window.open(`https://scrapbox.io/${targetProject}/${getDiaryPageTitle(date)}?body=${body}`);
}
}
script.jsexport async function moveIncompleteTasks() {
const diaryDate = getPageDate();
if (!diaryDate) return;
const now = new Date();
// 検索する範囲
const {startNo, endNo} = selection.exist ?
(() => {
const {start,end} = selection.range;
//console.log({start,end});
return {startNo: start.lineNo, endNo: end.lineNo};
})() :
// 選択範囲がなかったらタイトル行以外を選択する
{startNo: 1, endNo: scrapbox.Page.lines.length - 1};
// まずタスクを取得する
const tasks = scrapbox.Page.lines
.slice(startNo, endNo + 1)
.flatMap((_, i) => {
const task = parse({lineNo: i + startNo});
return task ? [task] : [];
});
// 未完了のタスクを取得する
const incompleteTasks = tasks.flatMap(({date, record, lineNo}) =>
isSameDay(date, diaryDate) && !record.end ?
[l(lineNo)] :
[]
);
// 各タスクに対して移動したことを表す印をつける
for (const line of incompleteTasks) {
const {date, record, ...rest} = parse({lineNo: line.index});
goLine(line.id);
await sleep(10);
insertText({text: [
' moved',
` moved to [${getDiaryPageTitle(now)}]`,
create({date: now, ...rest}),
].join('\n')});
await sleep(1);
}
}
script.js// 全選択
function selectLine() {
goHead();
press('End', {shiftKey: true});
}
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
undefined
を返すscript.jsfunction getPageDate(title = undefined) {
title = title ?? scrapbox.Page.title;
const date = parseDate(title, "'日刊記録sheet' yyyy-MM-dd", new Date());
return isValid(date) ? date : undefined;
}
// 転送先の日付ページの名前
const getDiaryPageTitle = (date) => format(date, "'日刊記録sheet' yyyy-MM-dd");