original /^(\d| ) (\d{4}-\d{2}-\d{2}) (\w{3}) ( {5}|\d{2}:\d{2}) ( {5}|\d{2}:\d{2}) ([^\n]*)$/
this/^`(\d| ) (\d{4}-\d{2}-\d{2}) (\w{3}) ( {5}|\d{2}:\d{2}) ( {5}|\d{2}:\d{2})`([^\n]*)$/
jsimport {
addTask, addInbox, copyTask,
startTask, endTask, posterioriEndTask, closeTask,
selectContent, walkDay, moveToday, changeInbox, sort, report,
} from '/api/code/takker/tritask-scrapbox/script.js';
import {scrapBindings} from '/api/code/takker/ScrapBindings/script.js';
const config = [
{key: 'alt+a alt+a', command: () => {addTask();return false;},},
{key: 'alt+a alt+x', command: () => {addInbox();return false;},},
{key: 'alt+a alt+c', command: () => {copyTask();return false;},},
{key: 'alt+a alt+s', command: () => {startTask();return false;},},
{key: 'alt+a alt+e', command: () => {endTask();return false;},},
{key: 'alt+a alt+0', command: () => {posterioriEndTask();return false;},},
{key: 'alt+a alt+q', command: () => {closeTask();return false;},},
{key: 'alt+a alt+/', command: () => {selectContent();return false;},},
{key: 'alt+a alt+1', command: () => {walkDay();return false;},},
{key: 'alt+a alt+t', command: () => {moveToday();return false;},},
{key: 'alt+a alt+i', command: () => {changeInbox();return false;},},
{key: 'alt+a alt+shift+s', command: () => {sort();return false;},},
{key: 'alt+a alt+.', command: () => {report();return false;},},
//{key: 'alt+a alt+b', command: () => {selectPlanningTime(); return false;},},
//{key: 'alt+a alt+m', command: () => {selectEstimatedTime(); return false;},},
];
scrapBindings.install()
.then(() => scrapBindings.push(...config));
alt
をつけています alt+space
がうごかなかったので、 alt+shift+s
に変えていますjsimport {
addTask, startTask, endTask, posterioriEndTask,
walkDay, moveToday, changeInbox, sort, report,
} from '/api/code/takker/tritask-scrapbox/script.js';
import {isMobile} from '/api/code/takker/mobile版scrapboxの判定/script.js';
if (isMobile()) {
const id = 'Tritask';
scrapbox.PageMenu.addMenu({
title: id,
image: 'https://img.icons8.com/ios/todo-list--v2.png',
});
scrapbox.PageMenu(id).addItem({
title: 'Add task',
image: 'https://img.icons8.com/ios/plus-math.png',
onClick: addTask,
});
scrapbox.PageMenu(id).addItem({
title: 'Start task',
image: 'https://img.icons8.com/ios/start--v1.png',
onClick: startTask,
});
scrapbox.PageMenu(id).addItem({
title: 'End task',
image: 'https://img.icons8.com/ios/stop-squared.png',
onClick: endTask,
});
scrapbox.PageMenu(id).addItem({
title: 'Posteriori end task',
image: 'https://img.icons8.com/ios/end--v1.png',
onClick: posterioriEndTask,
});
scrapbox.PageMenu(id).addItem({
title: 'walk +1 day',
onClick: () => walkDay(),
});
scrapbox.PageMenu(id).addItem({
title: 'Change today',
onClick: moveToday,
});
// 以下2つはmobileで動くか確認していない
scrapbox.PageMenu(id).addItem({
title: 'Sort',
onClick: sort,
});
scrapbox.PageMenu(id).addItem({
title: 'Report',
onClick: report,
});
}
script.jsimport {
goLine, goHead, enterEdit, upLines
} from '/api/code/takker/scrapbox-edit-emulation/script.js';
import {press} from '/api/code/takker/scrapbox-keyboard-emulation-2/script.js';
import {cursor} from '/api/code/takker/scrapbox-cursor-position-3/script.js';
import {line as l} from '/api/code/takker/scrapbox-line-info-2/script.js';
import {insertText} from '/api/code/takker/scrapbox-insert-text/script.js';
import {scrapboxDOM} from '/api/code/takker/scrapbox-dom-accessor/script.js';
import {selection} from '/api/code/takker/scrapbox-selection-2/script.js';
script.js// タスクの書式
const taskReg = /^`(\d| ) (\d{4}-\d{2}-\d{2}) (\w{3}) ( {5}|\d{2}:\d{2}) ( {5}|\d{2}:\d{2})`([^\n]*)$/;
const inboxReg = /^`( {28})`([^\n]*)$/;
const timeReg = /\d{2}:\d{2}/;
const dateReg = /\d{4}-\d{2}-\d{2}/;
const dummyTime = ' '.repeat(5);
const interval = 5; // 5 minutes
script.jsexport function addTask() {
const text = l(cursor().line).text;
// 空白のみの行ならそのまま上書きする
// 何か書き込まれている行だったら、改行して新しい行を作る
if (!/^\s+$/.test(text) && text !== '') {
press('End');
press('Enter');
}
const task = parse({line: text});
write({type: 'task',
// 現在行がタスクなら、それと同じ日付にする
// 違ったら今日にする
date: (task.type === 'task' ? task.date : new Date()),})
}
script.jsexport function copyTask() {
const text = l(cursor().line).text;
// taskのみが対象
if (!taskReg.test(text)) return;
// コピペする
press('End');
press('Enter');
insertText({text});
}
script.jsexport function addInbox() {
const text = l(cursor().line).text;
// 空白のみの行ならそのまま上書きする
// 何か書き込まれている行だったら、改行して新しい行を作る
if (!/^\s+$/.test(text) && text !== '') {
press('End');
press('Enter');
}
write({type: 'inbox'});
}
script.jsexport function startTask() {
const task = parse({line: l(cursor().line).text});
if (task.type !== 'task') return; // タスクでなければ何もしない
const {start: orgStart, end, ...rest} = task;
if (end) return;// すでに終了していたら何もしない
// 開始時刻をtoggleする
const now = new Date();
const start = !orgStart ? {
hours: now.getHours(),
minutes: now.getMinutes(),} : undefined;
// 全選択して上書きする
write({start, ...rest});
}
script.jsexport function endTask() {
const task = parse({line: l(cursor().line).text});
//console.log(task);
if (task.type !== 'task') return; // タスクでなければ何もしない
const {end: orgEnd, start, ...rest} = task;
if (!start) return;// まだ開始していなかったら何もしない
// 終了時刻をtoggleする
const now = new Date();
const end = !orgEnd ? {
hours: now.getHours(),
minutes: now.getMinutes(),} : undefined;
// repが指定されていたら、次のタスクを作成する
const newTask = createNextTask(task);
const text = create({start, end, ...rest})
+ (newTask === '' || orgEnd ? '' : `\n${newTask}`);
// 全選択して上書きする
selectLine();
insertText({text});
}
script.jsexport function posterioriEndTask() {
const task = parse({line: l(cursor().line).text});
if (task.type !== 'task') return; // タスクでなければ何もしない
const {start: orgStart, end: orgEnd, ...rest} = task;
//console.log(task);
if (orgStart || orgEnd) return; // 開始していないタスクのみが対象
const now = new Date();
// 直近のタスクを検索する
const lineNo = l(cursor().line).index;
const targetline = [...scrapboxDOM.lines.children]
.slice(0, lineNo + 1)
.reverse()
.find(line => {
const {type, end} = parse({line: l(line).text});
return type === 'task' && end; // 終了しているタスクのみ検索する
});
const end = {
hours: now.getHours(),
minutes: now.getMinutes(),
};
const start = targetline ? parse({line: l(targetline).text}).end : end;
// repが指定されていたら、次のタスクを作成する
const newTask = createNextTask(task);
const text = create({start, end, ...rest})
+ (newTask === '' ? '' : `\n${newTask}`);
// 全選択して上書きする
selectLine();
insertText({text});
}
script.jsexport function closeTask() {
const task = parse({line: l(cursor().line).text});
//console.log(task);
if (task.type !== 'task') return; // タスクでなければ何もしない
const {start: orgStart, end: orgEnd, ...rest} = task;
if ((!orgStart && orgEnd) || (orgStart && !orgEnd)) return;
// 開始時刻と終了時刻をtoggleする
const now = new Date();
const start = !orgStart ? {
hours: now.getHours(),
minutes: now.getMinutes(),} : undefined;
// repが指定されていたら、次のタスクを作成する
const newTask = createNextTask(task);
const text = create({start, end: start, ...rest})
+ (newTask === '' || orgStart ? '' : `\n${newTask}`);
// 全選択して上書きする
selectLine();
insertText({text});
}
script.jsexport function selectContent() {
const {type, content} = parse({line: l(cursor().line).text});
if (!type) return;
press('End');
// contentの長さだけ選択する
for (let i = 0; i < content.length; i++) {
press('ArrowLeft', {shiftKey: true});
}
}
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);
_walkDay(count);
await sleep(1);
}
}
}
function _walkDay(count) {
const task = parse({line: l(cursor().line).text});
if (task.type !== 'task') return; // タスクでなければ何もしない
const {date, ...rest} = task;
// 日付をずらす
let newDate = new Date(date);
newDate.setDate(newDate.getDate() + count);
write({date: newDate, ...rest});
}
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);
_moveToday();
await sleep(1);
}
}
}
function _moveToday() {
const task = parse({line: l(cursor().line).text});
if (task.type !== 'task') return; // タスクでなければ何もしない
const {date, ...rest} = task;
const now = new Date();
// 日付に変更がなければ何もしない
if (date.getFullYear() === now.getFullYear()
&& date.getMonth() === now.getMonth()
&& date.getDate() === now.getDate()) return;
write({date: new Date(), ...rest});
}
script.jsexport function changeInbox() {
const task = parse({line: l(cursor().line).text});
if (task.type !== 'task') return; // タスクでなければ何もしない
const {content} = task;
write({type: 'inbox', content});
}
script.jsexport async function sort() {
if (scrapbox.Page.lines.length < 3) return;
const startNo = 1;
const endNo = scrapbox.Page.lines.length - 1;
const sortedLineDOMs = scrapbox.Page.lines
.slice(startNo, 1 + endNo)
.map((line, i) => {return {lineDOM: scrapboxDOM.lines.children[i + startNo], text: line.text}})
.sort((a,b)=>new Intl.Collator().compare(a.text,b.text))
.map(({lineDOM}) => lineDOM);
// 一番上から順に入れ替え作業をする
let insertPosition = startNo;
for (const lineDOM of sortedLineDOMs) {
const presentPosition = l(lineDOM).index; // 現在の行の位置
if (presentPosition !== insertPosition) {
goLine({index: presentPosition});
await sleep(10);
upLines(presentPosition - insertPosition);
}
insertPosition++;
}
}
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
script.jsexport function report() {
if (!selection.exist) {
let now = new Date();
const tasks = scrapbox.Page.lines
.filter(line => taskReg.test(line.text))
.map(line => parse({line: line.text}))
.filter(({date}) =>
//今日のタスクのみ抽出する
date.getFullYear() === now.getFullYear()
&& date.getMonth() === now.getMonth()
&& date.getDate() === now.getDate());
const taskNum = tasks.length;
const doneTaskNum = tasks.filter(({end})=>end).length;
const restTime = tasks
.filter(({end, estimate}) => !end && estimate !== undefined)
.map(({estimate}) => estimate)
.reduce((result, estimate) => result + estimate, 0);
now.setMinutes(now.getMinutes() + restTime);
window.alert(`You've done ${doneTaskNum}/${taskNum} tasks. (rest: ${taskNum - doneTaskNum})
Your goal time is ${toHHMM(now)}. (rest: ${(restTime / 60.0).toPrecision(3)}H)`);
} else {
const {start: {lineNo: startNo}, end: {lineNo: endNo}} = selection.range;
const tasks = scrapbox.Page.lines.slice(startNo, endNo + 1)
.filter(line => taskReg.test(line.text))
.map(line => parse({line: line.text}));
const restTime = tasks.filter(({end, estimate}) => !end && estimate !== undefined)
.map(({estimate}) => estimate)
.reduce((result, estimate) => result + estimate, 0);
window.alert(`There are ${tasks.length} tasks, ${(restTime / 60.0).toPrecision(3)}H`);
}
}
script.jsfunction parse({line}) {
if (inboxReg.test(line)) {
const [blank, content] = line.match(inboxReg)?.slice(1);
return {type: 'inbox', content};
}
if (!taskReg.test(line)) return {};
const [header, dateString, day, start, end, content] = line.match(taskReg)?.slice(1);
const [year, month, date] = dateString.split('-').map(number => parseInt(number));
const [startH, startM] = start.split(':').map(number => parseInt(number));
const [endH, endM] = end.split(':').map(number => parseInt(number));
return {
type: 'task',
header,
date: new Date(year, month - 1, date),
start: !/^\s*$/.test(start) ? {
hours: startH,
minutes: startM,
} : undefined,
end: !/^\s*$/.test(end) ? {
hours: endH,
minutes: endM,
} : undefined,
content,
...parseAttributes({content}),
};
}
function parseAttributes({content}) {
const repeat = content.split(/\s/)
.find(fragment => /^rep:\d+$/.test(fragment))
?.replace(/^rep:(\d+)$/, '$1');
const estimate = content.split(/\s/)
.find(fragment => /^m:\d+$/.test(fragment))
?.replace(/^m:(\d+)$/, '$1');
return {
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 '日':
return [0];
case '月':
return [1];
case '火':
return [2];
case '水':
return [3];
case '木':
return [4];
case '金':
return [5];
case '土':
return [6];
case '休':
return [0,6];
case '平':
return [1,2,3,4,5];
}
}) ?? [],
estimate: estimate !== undefined ? parseInt(estimate) : undefined,
};
}
script.jsconst initalInbox = `\`${' '.repeat(28)}\``;
function create({type,
date,
header,
start, end,
content,}) {
switch(type) {
case 'inbox':
return `${initalInbox}${content ?? ''}`;
case 'task':
let time = new Date(date);
const dateString = `${toYYYYMMDD(time)} ${getDayString(time.getDay())}`;
if (start) {
time.setHours(start.hours);
time.setMinutes(start.minutes);
}
const startStr = start ? toHHMM(time) : dummyTime;
if (end) {
time.setHours(end.hours);
time.setMinutes(end.minutes);
}
const endStr = end ? toHHMM(time) : dummyTime;
return `\`${header ?? ' '} ${dateString} ${startStr} ${endStr}\`${content ?? ''}`;
default:
throw Error(`${type} is an invalid type.`);
}
}
// 現在行に上書きする
function write(taskOrInboxData) {
// 全選択して上書きする
selectLine();
insertText({text: create(taskOrInboxData)});
}
script.jsfunction createNextTask({date, content, skip, repeat, header}) {
if (repeat === undefined) return '';
//console.log({date, skip, repeat});
let nextDate = new Date(date);
//console.log({nextDate});
nextDate.setDate(nextDate.getDate() + repeat);
// skip属性があったら、指定された曜日以外になるまで日付をずらす
if (skip.length !== 0) {
// 全ての曜日がスキップに設定されていた場合は空文字を返す
if ([0,1,2,3,4,5,6].every(day => skip.includes(day))) return '';
while (skip.includes(nextDate.getDay())) {
nextDate.setDate(nextDate.getDate() + 1);
//console.log({nextDate});
}
}
return create({type: 'task',header, date: nextDate, content})
}
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())}`;
}