generated at
tritask-scrapbox-beta
Next version

hr
scrapbox上でTritaskっぽいタスク管理を行うためのUserScript (beta version)
warningこれ単体では動かない
ScrapBindingsPageMenuなどを用いて実行可能な形にする必要がある

takker用にカスタムしてあるので、Tritaskからはだいぶ離れている
Tritask準拠ver.もそのうち作る予定
2021-01-05 20:27:05 作った

2021-01-06
18:54:10
任意の場所に書いたタスクを、それぞれの日付ページに飛ばすようにした
16:47:25
refactoringした
tritask-scrapboxのコードを元に書き換えた
いくつか機能を追加した
tritask-scrapboxから移植した
2021-01-04
09:40:13 hours === 0 のときに空白だと誤認してしまうバグを直した
!hours ではなく hours === undefined で比較しないとだめみたい
2021-01-02
11:31:04 tritask task line以外で addTask() 以外を実行するとエラーが出る問題を直した
あと/tritask/スキップ属性/tritask/繰り返し属性を解析できるようにした
2020-12-31 16:33:55 いろいろ修正
インデントなしで新しく行を作る
現在行がタスクなら、そのタスクと同じ日付のタスクを作る

実装したいやつ
done繰り返し属性
done別の日付のタスクを、それぞれのページに飛ばす

script.js
import { goHead, goLine, moveRight, } 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 interval = 5; // 5 minutes export function addTask() { const text = l(cursor().line).text; // 空白以外が書き込まれていたら、新しい行に書き込む if (!/^\s+$/.test(text) && text !== '') { press('End'); insertText({text: '\n'}); } const task = parse({line: text}); // 現在行がタスクなら、それと同じ日付にする // 違ったら今日にする const date = task.date ?? new Date(); // 予定開始時刻と見積もり時刻を計算する // 予定開始時刻は、その前のタスクの見積もり時間にintervalを足したものだけずらしておく const plan = { start: task.plan?.start ? (() => { const temp = new Date(); temp.setHours(task.plan.start.hours); temp.setMinutes(task.plan.start.minutes + interval + (task.plan.duration.minutes ?? 0)); return { hours: temp.getHours(), minutes: temp.getMinutes(), }; })() : undefined, duration: task.plan?.duration, }; // 全選択して上書きする selectLine(); insertText({text: create({type: 'task',date, plan})}); } export function startTask() { const task = parse({line: l(cursor().line).text}); if (task.type !== 'task') return; // タスクでなければ何もしない const {record, ...rest} = task; if (record.end) return;// すでに終了していたら何もしない // 開始時刻をtoggleする const now = new Date(); const start = !record.start ? { hours: now.getHours(), minutes: now.getMinutes(), seconds: now.getSeconds(), } : undefined; // 全選択して上書きする selectLine(); insertText({text: create({record: {start}, ... rest})}); } export function endTask() { const task = parse({line: l(cursor().line).text}); //console.log(task); if (task.type !== 'task') return; // タスクでなければ何もしない const {record, ...rest} = task; if (!record.start) return;// まだ開始していなかったら何もしない // 終了時刻をtoggleする const now = new Date(); const end = !record.end ? { hours: now.getHours(), minutes: now.getMinutes(), seconds: now.getSeconds(), } : undefined; const newTask = {record: {start: record.start, end}, ...rest}; // repが指定されていたら、次のタスクを作成する const nextTask = createNextTask(newTask) const text = create(newTask) + (nextTask === '' || record.end ? '' : `\n${nextTask}`); // 全選択して上書きする selectLine(); insertText({text}); }

script.js
export function posterioriEndTask() { const task = parse({line: l(cursor().line).text}); if (task.type !== 'task') return; // タスクでなければ何もしない const {record, ...rest} = task; if (record.start || record.end) 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, record} = parse({line: l(line).text}); return type === 'task' && record?.end; // 終了しているタスクのみ検索する }); // 開始時刻と終了時刻を計算する const end = { hours: now.getHours(), minutes: now.getMinutes(), seconds: now.getSeconds(), }; const start = targetline ? parse({line: l(targetline).text}).record.end : end; const newTask = {record: {start, end}, ...rest}; // repが指定されていたら、次のタスクを作成する const nextTask = createNextTask(newTask) const text = create(newTask) + (nextTask === '' || record.end ? '' : `\n${nextTask}`); // 全選択して上書きする selectLine(); insertText({text}); }

編集支援
文字の選択
script.js
// 予定開始時刻を選択する export function selectPlanningTime() { const task = parse({line: l(cursor().line).text}); if (task.type !== 'task') return; // タスクでなければ何もしない goHead(); moveRight(12); // `YYYY-MM-DD なので12文字 // 5文字選択する for (let i = 0; i < 5; i++) { press('ArrowRight', {shiftKey: true}); } } // 見積もり時間を選択する export function selectEstimatedTime() { const task = parse({line: l(cursor().line).text}); if (task.type !== 'task') return; // タスクでなければ何もしない goHead(); moveRight(18);// `YYYY-MM-DD hh:mm なので18文字 // 4文字選択する for (let i = 0; i < 4; i++) { press('ArrowRight', {shiftKey: true}); } }
日付を増やす
script.js
export 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); // 全選択して上書きする selectLine(); insertText({text: create({date: newDate, ...rest})}); }
今日にする
script.js
export 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; // 全選択して上書きする selectLine(); insertText({text: create({date: now, ...rest})}); }

Page Network関係
ぶら下がっているテキストは転送しない
parse でぶら下がっているテキストも取得できるようにすればできそう
script.js
// 転送先の日付ページの名前 const diaryPage = (date) => `日刊記録sheet ${toYYYYMMDD(date)}`; function parseDiary() { if (!/^日刊記録sheet \d{4}-\d{2}-\d{2}$/.test(scrapbox.Page.title)) return undefined; const [year, month, date] = scrapbox.Page.title .match(/^日刊記録sheet (\d{4})-(\d{2})-(\d{2})$/).slice(1); return new Date(year, month - 1, date); } export async function transport({targetProject}) { // 転送しないタスクの日付 const diaryDate = parseDiary(); // 検索する範囲 const {startNo, endNo} = selection.exist ? (() => { const {start,end} = selection.range; return {startNo: start.lineNo, endNo: end.lineNo}; })() : {startNo: 1, endNo: scrapbox.Page.lines.length - 1}; // 移動する行を取得する const targetLines = scrapbox.Page.lines .slice(startNo, endNo + 1) .map((line, i) => { return { lineDOM: scrapboxDOM.lines.children[i + startNo], text: line.text, date: parse({line: line.text}).date, }; }) .filter(({date}) => { if (!date) return false; if (!diaryDate) return true; // 日付ページでなければ、全てのタスクを転送する return !(date.getFullYear() === diaryDate.getFullYear() && date.getMonth() === diaryDate.getMonth() && date.getDate() === diaryDate.getDate()); }); // 対象の行を削除する for (const {lineDOM} of targetLines) { goLine({index: l(lineDOM).index}); await sleep(10); // 改行を含めて消す press('End'); press('Home', {shiftKey: true}); press('Home', {shiftKey: true}); press('ArrowLeft', {shiftKey: true}); press('Delete'); } // 日付ごとにタスクをまとめる let bodies = {}; targetLines.forEach(({text,date}) => { bodies[toYYYYMMDD(date)] = { date, texts: [...(bodies[toYYYYMMDD(date)]?.texts ?? []), text] }; }); // 新しいタブで開く for (const [key, {date, texts}] of Object.entries(bodies)) { const body = encodeURIComponent(texts.join('\n')); window.open(`https://scrapbox.io/${targetProject}/${diaryPage(date)}?body=${body}`); } }

設定とかもろもろ
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を解析する
script.js
function parse({line}) { if (!taskReg.test(line)) return {}; const [dateString, plan, estimate, start, end, content] = line.match(taskReg)?.slice(1); const [year, month, date] = dateString.split('-').map(number => parseInt(number)); const [planH, planM] = plan.split(':').map(number => parseInt(number)); return { type: 'task', date: new Date(year, month - 1, date), plan: { start: !/^\s*$/.test(plan) ? { hours: planH, minutes: planM, } : undefined, duration: !/^\s*$/.test(estimate) ? { minutes: parseInt(estimate), } : undefined, }, record: { start: parseTime(start), end: parseTime(end), }, content, ...parseAttributes({content}), }; }


helper関数
hh:mm:ss をobjectに分解してくれるやつ
script.js
function parseTime(timeString) { if (!/\d{2}:\d{2}:\d{2}/.test(timeString)) return undefined; const [hours, minutes, seconds] = timeString.split(':').map(number => parseInt(number)); return {hours, minutes, seconds}; }

属性を解析する
script.js
function parseAttributes({content}) { const repeat = content.split(/\s/) .find(fragment => /^rep:\d+$/.test(fragment)) ?.replace(/^rep:(\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]; } }) ?? [], }; }

taskもしくはinboxの文字列を作成する
script.js
const initalInbox = `\`${' '.repeat(28)}\``; function create({type, date, plan, record, content,}) { switch(type) { case 'task': let time = new Date(date); const dateString = toYYYYMMDD(time); if (plan?.start) { time.setHours(plan.start.hours); time.setMinutes(plan.start.minutes); } const planStr = plan?.start ? toHHMM(time) : ' '.repeat(5); const durationStr = plan?.duration ? String(plan?.duration.minutes).padStart(4, '0') : ' '.repeat(4); if (record?.start) { time.setHours(record.start.hours); time.setMinutes(record.start.minutes); time.setSeconds(record.start.seconds); } const startStr = record?.start ? toHHMMSS(time) : ' '.repeat(8); if (record?.end) { time.setHours(record.end.hours); time.setMinutes(record.end.minutes); time.setSeconds(record.end.seconds); } const endStr = record?.end ? toHHMMSS(time) : ' '.repeat(8); return `\`${dateString} ${planStr} ${durationStr} ${startStr} ${endStr}\`${content ?? ''}`; default: throw Error(`${type} is an invalid type.`); } }

繰り返し属性を解析して、次のタスクを作る
script.js
function createNextTask({date, plan, record, content, skip, repeat}) { 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}); } } // 見積もり時間を設定する const duration = plan?.duration // 実績から見積もり時間を計算する ?? {minutes: (() => { const start = record.start.hours * 60 + record.start.minutes; const end = record.end.hours * 60 + record.end.minutes; return start < end ? end - start // 終了時刻のほうが早かった場合は、日付をまたいでいるとみなす : end + 24 * 60 - start; })()}; return create({type: 'task', date: nextDate, plan: {start: plan.start, duration: duration}, content}); }

Utilities
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));

#2021-01-06 15:51:24
#2021-01-05 20:27:31
#2021-01-04 09:41:01
#2020-12-30 13:40:03