takker-scheduler/task
実装
変更点
コメントを解析できるようにする
コメント:タスクの行にぶら下がっている箇条書き
時刻も全てDateを使って表すようにした
こっちのほうが何かと便利
タスク名から属性を分離するようにした
属性を削除したい場合がある
機能
parse({lineNo})
nextTask(taskData)
属性がなければundefiendを返す
taskDataにタスクのobjectを渡す
write(taskData, {overwrite})
/icons/hr.icon
codes
依存コードを読み込む
code:script.js
import {
goHead, goLine,
} from '/api/code/takker/scrapbox-edit-emulation/script.js';
import {press} from '/api/code/takker/scrapbox-keyboard-emulation-2/script.js';
import {insertText} from '/api/code/takker/scrapbox-insert-text/script.js';
設定とかもろもろ
code:script.js
// タスクの書式
const taskReg = /^(\d{4}-\d{2}-\d{2}) ( {5}|\d{2}:\d{2}) ( {4}|\d{4}) ( {8}|\d{2}:\d{2}:\d{2}) ( {8}|\d{2}:\d{2}:\d{2})(^\n*)$/; taskを解析する
lineNo行を含むblockを解析する
タスクの行もしくはコメントの行にいることが条件
lineNo行を含むタスクのブロックを返す
code:script.js
export function parse({lineNo}) {
// lineNo行がトップレベルでタスク行でなければ何もしない
if (scrapbox.Page.lineslineNo.nodes?.type !== 'indent' && !taskReg.test(scrapbox.Page.lineslineNo.text)) return undefined; // lineNo以下の行番号から、タスクの行を探す
const targetTaskLineNo = findPreviousLines(lineNo, line => taskReg.test(line.text));
if (targetTaskLineNo === undefined) return undefined; // タスクブロックでなければ何もしない
// タスクが書き込まれた行を解析する
const planH, planM = plan.split(':').map(number => parseInt(number)); // コメントを解析する
// targetTaskLineNo行は除いて検索する
const nextTaskLineNo = findNextLines(targetTaskLineNo + 1, line => line.nodes?.type !=='indent');
//console.log({targetTaskLineNo, nextTaskLineNo});
const comments = scrapbox.Page.lines
.map(line => line.text).slice(targetTaskLineNo + 1, nextTaskLineNo);
// 実績時刻を解析する
const rStart = !/^\s*$/.test(start) ? parseTime(year, month - 1, date, start) : undefined;
let rEnd = !/^\s*$/.test(end) ? parseTime(year, month - 1, date, end) : undefined;
if (rStart && rEnd && rStart.getTime() > rEnd.getTime()) rEnd.setDate(rEnd.getDate() + 1);
return {
type: 'task',
date: new Date(year, month - 1, date),
plan: {
start: !/^\s*$/.test(plan) ?
new Date(year, month - 1, date, planH, planM)
: undefined,
duration: !/^\s*$/.test(estimate) ?
{minutes: estimate}
: undefined,
},
record: {start: rStart, end: rEnd,},
lineNo: targetTaskLineNo,
comments,
...parseAttributes({content}),
};
}
helper関数
hh:mm:ssをDateに変換してくれるやつ
code:script.js
function parseTime(year, month, date, timeString) {
if (!/\d{2}:\d{2}:\d{2}/.test(timeString)) return undefined;
= timeString.split(':').map(number => parseInt(number));
return new Date(year, month, date, hours, minutes, seconds);
}
属性を解析する
属性を抜いたタスク名も返す
code:script.js
function parseAttributes({content}) {
const repeat = content.split(/\s/)
.find(fragment => /^rep:\d+$/.test(fragment))
?.replace(/^rep:(\d+)$/, '$1');
const rawContent = content.split(/\s/).map(text =>
text.replace(/^skip:月火水木金土日平休+|rep:\d+$/,'')) .filter(text => text.trim() !== '').join(' ');
return {
content: rawContent,
repeat: repeat !== undefined ? parseInt(repeat) : undefined,
skip: content.split(/\s/)
.find(fragment => /^skip:月火水木金土日平休+$/u.test(fragment)) ?.replace(/^skip:(月火水木金土日平休+)$/u, '$1')?.match(/./ug) //Date.prototype.getDay()の書式に変換する
?.flatMap(dayString => {
switch(dayString) {
case '日':
case '月':
case '火':
case '水':
case '木':
case '金':
case '土':
case '休':
case '平':
}
}) ?? [],
};
}
属性から文字列を作る
code:script.js
function fromAttributes({content, skip, repeat}) {
let skipStr = '';
if (skip && skip.length > 0) {
let skipStrList = skip.every(dayNumber => 1,2,3,4,5.includes(dayNumber)) ? ['平', ...skip.filter(dayNumber => !1,2,3,4,5.includes(dayNumber))] : skip; skipStrList = skipStrList.every(dayNumber => 0, 6.includes(dayNumber)) ? ['休', ...skipStrList.filter(dayNumber => !0, 6.includes(dayNumber))] : skipStrList; skipStrList = skipStrList.map(dayNumber => {
switch(dayNumber) {
case 0:
return '日';
case 1:
return '月';
case 2:
return '火';
case 3:
return '水';
case 4:
return '木';
case 5:
return '金';
case 6:
return '土';
case '平':
case '休':
return dayNumber;
}
});
skipStr = skipStrList.length !== 0
? skip:${skipStrList.join('')}
: '';
} else {
skipStr = '';
}
return [(content ?? ''),
(skipStr !== '' ? ${skipStr} : ''),
(repeat !== undefined ? rep:${repeat} : ''),].join('');
}
繰り返し属性を解析して、次のタスクを作る
commentsは引き継がない
code:script.js
export function nextTask({type, date, plan, record, skip, repeat, content, comments, lineNo}) {
if (type !== 'task') throw Error(${type} is an invalid type.);
if (repeat === undefined) return undefined;
//console.log({date, skip, repeat});
let nextDate = new Date(date);
//console.log({nextDate});
nextDate.setDate(nextDate.getDate() + repeat);
// skip属性があったら、指定された曜日以外になるまで日付をずらす
if (skip.length !== 0) {
// 全ての曜日がスキップに設定されていた場合は空文字を返す
while (skip.includes(nextDate.getDay())) {
nextDate.setDate(nextDate.getDate() + 1);
//console.log({nextDate});
}
}
// 見積もり時間を設定する
const duration = plan?.duration
// 実績から見積もり時間を計算する
?? {minutes: Math.round((
(record.end.getTime() - record.start.getTime()) % (24 * 60 * 60 * 1000)
) / (60 * 1000))};
return {
type: 'task',
date: nextDate,
plan: {start: plan.start, duration: duration},
content, skip, repeat,
lineNo: lineNo + (comments?.length ?? 0),
};
}
タスクを書き込む
overwriteがtrueのとき
挿入位置にあるタスクを探し、上書きする
commentsにないコメントがあったら削除する
タスクでなかったら、その行の下に挿入する
行が空白のみなら上書きする
overwriteがfalseのとき
タスクブロックの下に挿入する
defaultはtrue
code:script.js
export async function write(
{type, date, plan, record, content, comments, lineNo, skip, repeat} = {},
{overwrite = true} = {}) {
//console.log('Write: %o', {type, date, plan, record, content, comments, lineNo, skip, repeat});
const texts = create({type, date, plan, record, content, comments, skip, repeat});
//console.log(texts);
const presentTask = parse({lineNo});
//console.log({presentTask});
// タスクがなければ、そのまま書き込む
if (!presentTask) {
const text = scrapbox.Page.lineslineNo.text; goLine({index: lineNo});
await sleep(10);
if (!/^\s*$/.test(text)) {
press('Enter');
}
selectLine();
insertText({text: texts.join('\n')});
return;
}
if (!overwrite) {
// 現在行を含むタスクブロックの下に挿入する
const insertPotision = presentTask.lineNo + presentTask.comments.length + 1;
//console.log({text});
goLine({index: insertPotision - 1});
await sleep(10);
// 挿入位置に行がないか、あっても空白行でなかった場合は、新しく行を追加する
if (insertPotision === scrapbox.Page.lines.length || !/^\s*$/.test(text)) {
press('Enter');
} else {
press('ArrowDown');
}
//挿入する
selectLine();
insertText({text: texts.join('\n')});
return;
}
// 上書きするとき
// task行を上書きする
goLine({index: presentTask.lineNo});
await sleep(10);
selectLine();
insertText({text: texts0}); await sleep(1);
}
if (!comments) return;
//console.log('comments exist.');
// commentsがある場合
// 現在のcommentsの数を数える
const presentCommentNum = presentTask.comments.length;
// 余分な行を削除する
if (presentCommentNum > comments.length) {
goLine({index: presentTask.lineNo + comments.length});//削除の開始地点にcursorを移動させる
await sleep(10);
// 削除する行の数だけ選択範囲を広げる
for (let i = comments.length; i < presentCommentNum; i++) {
press('ArrowRight', {shiftKey: true});
press('End', {shiftKey: true});
}
// 削除実行
press('Delete');
}
// 足りない行を追加する
if (presentCommentNum < comments.length) {
goLine({index: lineNo + comments.length});
await sleep(10);
insertText({text: '\n'.repeat(comments.length - presentCommentNum)});
await sleep(1);
}
// 1行ずつ差分を確認しながらコメントを書き込む
for (let i = 0; i < comments.length; i++) {
if (judge) return;
goLine({index: presentTask.lineNo + i});
await sleep(10);
selectLine();
insertText({text: textsi}); await sleep(1);
}
}
taskの文字列を作成する
code:script.js
function create({type, date, plan, record, content, comments, skip, repeat}) {
switch(type) {
case 'task':
const planStr = plan?.start ? toHHMM(plan.start) : ' '.repeat(5);
const durationStr = plan?.duration ?
String(plan.duration.minutes).padStart(4, '0')
: ' '.repeat(4);
const startStr = record?.start ? toHHMMSS(record.start) : ' '.repeat(8);
const endStr = record?.end ? toHHMMSS(record.end) : ' '.repeat(8);
return [
\`${toYYYYMMDD(date)} ${planStr} ${durationStr} ${startStr} ${endStr}\`${fromAttributes({content, skip, repeat})},
...(comments ?? [])];
default:
throw Error(${type} is an invalid type.);
}
}
Utilities
前後の行を検索する
code:script.js
function findNextLines(lineNo, callback) {
.slice(lineNo)
.find(index => callback(scrapbox.Page.linesindex, index)); }
function findPreviousLines(lineNo, callback) {
.slice(0, lineNo + 1).reverse()
.find(index => callback(scrapbox.Page.linesindex, index)); }
code:script.js
// 全選択
function selectLine() {
goHead();
press('End', {shiftKey: true});
}
function getDayString(day) {
switch(day) {
case 0:
return 'Sun';
case 1:
return 'Mon';
case 2:
return 'Tue';
case 3:
return 'Wed';
case 4:
return 'Thu';
case 5:
return 'Fri';
case 6:
return 'Sat';
default:
throw Error(Invalid day number: ${day});
}
}
const zero = n => String(n).padStart(2, '0');
function toYYYYMMDD(d) {
return ${d.getFullYear()}-${zero(d.getMonth() + 1)}-${zero(d.getDate())};
}
function toHHMMSS(d) {
return ${zero(d.getHours())}:${zero(d.getMinutes())}:${zero(d.getSeconds())};
}
function toHHMM(d) {
return ${zero(d.getHours())}:${zero(d.getMinutes())};
}
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));