generated at
tritask-scrapbox
TritaskのScrapbox移植版

本家との違い
一部の機能と属性を外しました
/tritask/Walk dayは数値を指定するUIを作るのが面倒難しかったのでomit
Logicの実装はできている
構文を少し変えました
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]*)$/
数字の位置を揃えるために、インラインコード記法で等幅フォントにしています
タスク名の部分は通常のテキストです
リンクや文字装飾記法を含めることができます
ソート時に開始日を今日にする機能がありません
単純に実装し忘れてました……takker

Screenshots
タスクの追加

ルーチンタスク

複数行操作とソート
行の順番がむちゃくちゃだと、ソートに少し時間がかかります
テロメアをむやみに更新しないよう、1行ずつアウトライン編集を用いて移動させているので

使い方
お試し方法
このUserScriptはTritaskの操作コマンドを提供する機能しかありません
実際に使うには、別途Page Menuやkeyboard shortcutの設定を行う必要があります
keyboard shortcutを使った設定例
js
import { 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 に変えています
Page menuをつかった設定例
mobile版scrapbox向けの設定
js
import { 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, }); }
継続的に使う場合は、依存コードを全て自分の個人projectにコピーすることをおすすめします
一覧
うっげ依存コード多すぎだなtakker
これじゃコピペするのも一苦労だ
なんとかしなきゃ

作っといてアレですが、takkerはこのScriptを使用していません
Tritaskを自分好みにカスタムしたtritask-scrapbox-betaのほうを使っています
バグ報告があれば直しますが、積極的なメンテナンスはしないつもりです
Scrapboxに合うような形にカスタムしたいという理由もあります
例えば日付ごとにページを分けるとか
タスクの下にインデントを下げてメモを書くとか
んでそのメモを別のページに送る

script.js
import { 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

機能一覧
tritask-scrapbox-betaから移植する
追加
script.js
export 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.js
export function copyTask() { const text = l(cursor().line).text; // taskのみが対象 if (!taskReg.test(text)) return; // コピペする press('End'); press('Enter'); insertText({text}); }
script.js
export function addInbox() { const text = l(cursor().line).text; // 空白のみの行ならそのまま上書きする // 何か書き込まれている行だったら、改行して新しい行を作る if (!/^\s+$/.test(text) && text !== '') { press('End'); press('Enter'); } write({type: 'inbox'}); }
script.js
export 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.js
export 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.js
export 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.js
export 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.js
export 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.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); write({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; write({date: new Date(), ...rest}); }
script.js
export function changeInbox() { const task = parse({line: l(cursor().line).text}); if (task.type !== 'task') return; // タスクでなければ何もしない const {content} = task; write({type: 'inbox', content}); }
いるのかこれ?
タイトル以外の全ての行を一気にソートする
script.js
export 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.js
export 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`); } }
属性
これはページ分けろでいいかなあtakker

taskを解析する
script.js
function 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, }; }

taskもしくはinboxの文字列を作成する
script.js
const 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.js
function 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}) }

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())}`; }

Reference
作者自らによるScrapboxへの移植の考察
書き込み用APIが提供されていないことから、移植を断念した模様

#2021-01-06 14:48:05
#2021-01-04 14:00:58