scrapbox-dailytasklist
https://gyazo.com/f2f088e82a7a289de13337936f904e42
todo
/staで試す(3000page超で現実的なパフォーマンスになるか)5-7秒待つが許容範囲 2021/02/08 19:03:12 特に強い必要性もないので停滞中...sta.icon
表示する部分を組み込む
エラー出すだけだしalertでよくない?
sta.iconたぶんtrue(明日の僕さん、どうですかね
fitやsecの文法ミス
page fetch時のprogress
呼び出し元からパラメーター渡すようにする
配布方法を考える
スクリは1ページ内に収める etc
UserScriptやJS知らない人でもコピペ+αで使えるようにしたい
code:script.js
export const VERSION = 'scrapbox dailytasklist v0.0.1'
//後で外に出す
//const __projectName__ = 'sta-routinetask-sample'
const __projectName__ = 'sta'
const LB = '\n'
const DELIM_ATTRIBUTE = ':'
// @todo どうせなら fit で使ってる , と / も定数化しませんか?
const ATTRHEAD = {
'FIT' : 'fit:',
'SECTION' : 'sec:',
}
for debug
code:script.js
const c = (obj) => {
console.log(obj)
}
util Datetime
code:script.js
class Datetime {
constructor(){
this._init()
}
_init(){
const msecJST = Date.now()
const dateJST = new Date(msecJST)
console.log(Datetimeクラス上の現在日時は ${dateJST})
this._dateJST = dateJST
}
get day(){
const day = this._dateJST.getDate()
return day
}
get dowJP(){
const downum = this._dateJST.getDay()
return dow
}
}
code:script.js
function insertText(text) {
const cursor = document.getElementById('text-input');
cursor.focus();
cursor.value = text;
const uiEvent = document.createEvent('UIEvent');
uiEvent.initEvent('input', true, false);
cursor.dispatchEvent(uiEvent);
}
全ページ取得
APIの仕様で先頭5行まで
code:script.js
// @return [] 範囲外に対して取得しに行ったとき
const getSpecificRange = (start, count) => {
return fetch(https://scrapbox.io/api/pages/${__projectName__}?skip=${start}&limit=${count}).then((res) => {
return res.json()
}).then((json) => {
return json.pages
})
}
const getAllPages = async () => {
let start = 0
const fetchWindow = 1000
const allPages = []
while(true){
const pages = await getSpecificRange(start, fetchWindow)
const isReachedEnd = pages.length == 0
if(isReachedEnd){
break
}
// Python でいう extend がないので, 仕方なく愚直に辿って入れる...
for(const page of pages){
allPages.push(page)
}
start += fetchWindow
}
return allPages
}
全ページのうちタスクページだけを抽出
code:script.js
const isTaskPage = (page) => {
const lines = page.descriptions
const tooShort = lines.length <= 1
if(tooShort){
return false
}
// タスクページかどうかを判定する
// - 速度優先のため単純な文字列比較で判定したい
// - fit: fit: でも満たしちゃうけど気にしない
let foundCount = 0
const REQUIRED_COUNT = 2
for(const line of lines){
const foundFitAttr = line.startsWith(ATTRHEAD.FIT)
const foundSectionAttr = line.startsWith(ATTRHEAD.SECTION)
if(foundFitAttr){
foundCount++
}
if(foundSectionAttr){
foundCount++
}
if(foundCount == REQUIRED_COUNT){
break
}
}
const notFoundAllRequiredAttr = foundCount != REQUIRED_COUNT
if(notFoundAllRequiredAttr){
return false
}
return true
}
const pagesToTaskPages = (pages) =>{
let taskPages = []
for(const page of pages){
const isNotTaskPage = !isTaskPage(page)
if(isNotTaskPage){
continue
}
taskPages.push(page)
}
return taskPages
}
task
code:script.js
class Task{
constructor(page){
this._title = page.title
this._lines = page.descriptions
this._fit = null
this._section = null
this._parseLines(this._lines)
}
_parseLines(lines){
for(const line of lines){
this._parseLine(line)
}
}
_parseLine(line){
// 毎回全パターンを parse することになるが
// ルーチンタスク数は高々数百なので性能なんて気にしなくていい.
this._parseAsFitAttr(line)
this._parseAsSectionAttr(line)
}
_parseAsFitAttr(line){
const vOrNull = this._parseAsXXXAttr_and_get(line, ATTRHEAD.FIT)
if(vOrNull == null){
return
}
this._fit = vOrNull
}
_parseAsSectionAttr(line){
const vOrNull = this._parseAsXXXAttr_and_get(line, ATTRHEAD.SECTION)
if(vOrNull == null){
return
}
this._section = vOrNull
}
_parseAsXXXAttr_and_get(line, headOfAttr){
const foundAttr = line.startsWith(headOfAttr)
if(!foundAttr){
return null
}
const v = line.substr(headOfAttr.length)
return v
}
isFit(day, dowJP){
let ret = false
ret = ret || this._isFitAsEveryDay()
ret = ret || this._isFitAsDow(dowJP)
ret = ret || this._isFitAsSingleDay(day)
ret = ret || this._isFitAsEnumedDay(day)
ret = ret || this._isFitAsPerNDay(day)
return ret
}
_isFitAsDow(dowJP){
const myfit = this._fit
const found = myfit.indexOf(dowJP) != -1
if(found){
return true
}
return false
}
_isFitAsEveryDay(){
return this._fit == 'every'
}
_isFitAsSingleDay(day){
const myday = this._stringDayToNumberDay(this._fit)
if(myday == -1){
return false
}
const isEqual = myday == day
if(isEqual){
return true
}
return false
}
_isFitAsEnumedDay(day){
const myfit = this._fit
const fitdays = myfit.split(',')
const isNotEnumedDayFormat = fitdays.length == 0
if(isNotEnumedDayFormat){
return false
}
for(const fitdayByString of fitdays){
const fitday = this._stringDayToNumberDay(fitdayByString)
if(fitday == -1){
continue
}
const isNotEqual = fitday != day
if(isNotEqual){
continue
}
return true
}
return false
}
_isFitAsPerNDay(day){
const myfit = this._fit
const maybeExpression = myfit.split('/')
const isNotExpression = maybeExpression.length != 2
if(isNotExpression){
return false
}
const radix = 10
const a, b = maybeExpression const routineIntervalDay = parseInt(a, radix)
const hitPoint = parseInt(b, radix) // RPG における HP ではない
// @todo ユーザーに書式正しくない旨伝えた方が易しいと思う
if(Number.isNaN(routineIntervalDay)){
return false
}
if(Number.isNaN(hitPoint)){
return false
}
const mod = day % routineIntervalDay
const isMatched = mod == hitPoint
if(isMatched){
return true
}
return false
}
// @return -1 日としておかしい値だった
_stringDayToNumberDay(stringDay){
// '25' -> 25
// '25a' -> 25
// '2a5' -> 2
// 'a25' -> NaN
const radix = 10
const numberOrNaN = parseInt(stringDay, radix)
if(Number.isNaN(numberOrNaN)){
return -1
}
const day = numberOrNaN
return day
}
toLine(){
const title = this._title
const indentForScrapboxList = ' '
const linkedTitle = [${title}]
const line = ${indentForScrapboxList}- ${linkedTitle}]
return line
}
get section(){
return this._section
}
}
task enumer
code:script.js
class TaskEnumer{
constructor(tasks, sections){
this._tasks = tasks
this._sections = sections
}
enum(){
const indentForScrapboxList = ' '
const outLines = []
// @todo ネスト深くて不吉な臭い
for(const section of this._sections){
outLines.push(${indentForScrapboxList}${section})
for(const task of this._tasks){
const sectionOfTheTask = task.section
const isNotMatched = section != sectionOfTheTask
if(isNotMatched){
continue
}
outLines.push(${task.toLine()})
}
}
return outLines
}
}
entrypoint
code:script.js
function getDailyTaskList(){
const dt = new Datetime()
const nowday = dt.day
const nowdow = dt.dowJP
getAllPages().then((allPages) => {
console.log(page count is ${allPages.length})
const taskPages = pagesToTaskPages(allPages)
const tasks = []
for(const taskPage of taskPages){
const task = new Task(taskPage)
tasks.push(task)
}
const fittedTasks = []
for(const task of tasks){
const isNotFit = !(task.isFit(nowday, nowdow))
if(isNotFit){
continue
}
fittedTasks.push(task)
}
const enumer = new TaskEnumer(fittedTasks, __sections__)
const dailyTaskList = enumer.enum()
const dailyTaskListByStr = dailyTaskList.join('\n')
insertText(dailyTaskListByStr)
})
}
UI
code:script.js
const MENUNAME = 'DailyTaskList'
scrapbox.PageMenu.addMenu({
title: MENUNAME,
image: SCRAPBOX_FAVICON_PATH, // 良いアイコン案ないのでテキトーに favicon で
});
const menu = scrapbox.PageMenu(MENUNAME)
menu.addItem({
title: 'Create daily tasklist',
onClick: getDailyTaskList,
});