entrypoint.tsimport { launch } from "./main.ts";
import {
makeDiary, filter,
} from "./template.ts";
launch(
"mitoujr-script",
{
makeDiary,
filter,
},
);
template.tsimport {
getDay,
getWeek,
addDays,
subDays,
subYears,
getYear,
lightFormat,
getDayOfYear,
getDaysInYear,
} from "./template_deps.ts";
const titleFormat = "作業室yyyy-MM-dd";
const titleRegExp = /^作業室\d{4}-\d{2}-\d{2}$/;
export function makeDiary(date: Date): {
title: string;
header: string[];
footer: string[];
} {
const target_date = new Date("2022-11-03T00:00:00");
const diff = Math.ceil(
(target_date.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
);
const header = [
`[${lightFormat(subDays(date, 1), titleFormat)}]←${lightFormat(
date,
titleFormat
)}→[${lightFormat(addDays(date, 1), titleFormat)}]`,
];
if (diff > 0) {
header.push(`成果報告会まで後${diff}日`);
}
header.push(
"",
"[* 予定]",
"",
...[...Array(24).keys()].map((h) => `[* ${h}:00]`),
"[* 雑談]"
);
const footer = [
`10日前:[${lightFormat(
subDays(date, 10),
titleFormat
)}] 30日前:[${lightFormat(subDays(date, 30), titleFormat)}]`,
`100日前 [${lightFormat(
subDays(date, 100),
titleFormat
)}] 1年前 [${lightFormat(subYears(date, 1), titleFormat)}]`,
];
return {
title: toTitle(date),
header,
footer,
};
}
export function filter(title: string, today: Date): boolean {
if (!titleRegExp.test(title)) return true;
return toTitle(today) === title;
}
function toTitle(date: Date): string {
return lightFormat(date, titleFormat);
}
template_deps.ts// 一年の経過率を計算するのに必要
export { default as getDaysInYear } from "https://deno.land/x/date_fns@v2.22.1/getDaysInYear/index.ts";
export { default as getDayOfYear } from "https://deno.land/x/date_fns@v2.22.1/getDayOfYear/index.ts";
// 日付計算に使う
export { default as addDays } from "https://deno.land/x/date_fns@v2.22.1/addDays/index.ts";
export { default as subDays } from "https://deno.land/x/date_fns@v2.22.1/subDays/index.ts";
export { default as getWeek } from "https://deno.land/x/date_fns@v2.22.1/getWeek/index.ts";
export { default as subYears } from "https://deno.land/x/date_fns@v2.22.1/subYears/index.ts";
export { default as getDay } from "https://deno.land/x/date_fns@v2.22.1/getDay/index.ts";
export { default as getYear } from "https://deno.land/x/date_fns@v2.22.1/getYear/index.ts";
// 文字列変換に使う
export { default as lightFormat } from "https://deno.land/x/date_fns@v2.22.1/lightFormat/index.ts";
main.ts/// <reference no-default-lib="true"/>
/// <reference lib="esnext"/>
/// <reference lib="dom"/>
import {
pin,
unpin,
patch,
useStatusBar,
sleep,
makeSocket,
disconnect,
format,
} from "./deps.ts";
import { listPinnedPages } from "./list.ts";
import type { Scrapbox, Socket } from "./deps.ts";
declare const scrapbox: Scrapbox;
export interface DiaryInit {
makeDiary: (date: Date) => {
title: string;
header: string[];
footer: string[];
},
filter: (title: string, today: Date) => boolean;
}
// initialize
export function launch(
project: string,
init: DiaryInit & { interval?: number },
) {
const interval = init.interval ?? 24 * 3600 * 1000;
const handleChange = () =>
scrapbox.Project.name === project ?
startObserve(project, interval, init) :
endObserve();
handleChange();
scrapbox.addListener("project:changed", handleChange);
}
let updateTimer: number | undefined;
async function startObserve(
project: string,
interval: number,
init: DiaryInit,
) {
endObserve();
await pinDiary(project, new Date(), init);
updateTimer = setInterval(
() => pinDiary(project, new Date(), init),
interval,
);
}
function endObserve() {
clearInterval(updateTimer);
}
export async function pinDiary(
project: string,
date: Date,
{ makeDiary, filter, }: DiaryInit,
): Promise<void> {
const { render, dispose } = useStatusBar();
let socket: Socket | undefined;
try {
// 今日以外の日付ページを外す
render(
{ type: "spinner" },
{ type: "text", text: `unpin other diary pages...`},
);
socket = await makeSocket();
for await (const { title } of listPinnedPages(project)) {
if (filter(title, date)) continue;
await unpin(project, title, { socket });
}
const { title, header, footer } = makeDiary(date);
// 今日の日付ページをピン留めする
if(false) { // disabled
render(
{ type: "spinner" },
{ type: "text", text: `pin "/${project}/${title}"...`},
);
await pin(project, title, { socket, create: true });
}
// 今日の日付ページにtemplateを挿入する
render(
{ type: "spinner" },
{ type: "text", text: `format "/${project}/${title}"...`},
);
await patch(project, title, (lines) => [
lines[0].text,
...format(
lines.slice(1).map(line => line.text),
header,
footer,
),
], { socket });
render(
{ type: "check-circle" },
{ type: "text", text: `Pinned "/${project}/${title}".`},
);
} catch(e: unknown) {
render(
{ type: "exclamation-triangle" },
{ type: "text", text: e instanceof Error ?
`${e.name} ${e.message}` :
`Unknown error! (see developper console)`,
},
);
console.error(e);
} finally {
if (socket) await disconnect(socket);
await sleep(1000);
dispose();
}
}
list.tsimport { listPages, PageSummary } from "./deps.ts";
/** 全てのピン留めされたページを取得する */
export async function* listPinnedPages(project: string, skip = 0): AsyncGenerator<PageSummary> {
const { count, pages } = await ensureList(project, skip);
for (const page of pages) {
if (page.pin === 0) continue;
yield page;
}
// pinしたページこれ以上ないときは終了
if ((pages.at(-1)?.pin ?? 0) === 0) return;
yield* listPinnedPages(project, skip + 1000);
}
async function ensureList(project: string, skip: number) {
const result = await listPages(project, {
limit: 1000,
skip,
});
// login errorなどは全部例外として扱う
if (!result.ok) {
const error = new Error();
error.name = result.value.name;
error.message = result.value.message;
throw error;
}
return result.value;
}
deps.tsexport { patchTemplate as format } from "./format.ts";
export {
pin,
unpin,
patch,
useStatusBar,
makeSocket,
disconnect,
listPages,
} from "https://raw.githubusercontent.com/takker99/scrapbox-userscript-std/0.10.3/mod.ts";
export type {
Socket,
} from "https://raw.githubusercontent.com/takker99/scrapbox-userscript-std/0.10.3/mod.ts";
export {
sleep,
} from "https://raw.githubusercontent.com/takker99/scrapbox-userscript-std/0.10.3/sleep.ts";
export type {
Scrapbox,
PageSummary,
} from "https://raw.githubusercontent.com/scrapbox-jp/types/0.0.8/mod.ts";
format.tsimport { patchLines, findSplitIndex } from "./util.ts";
// linesにタイトルを入れないように
export function patchTemplate(lines: string[], headers: string[], footers: string[]): string[] {
// headerとfooterに相当する行を補う
const bodies = patchLines(
patchLines(lines, headers).reverse(),
[...footers].reverse(),
).reverse();
// headerとfooterの間に余裕をもたせる
const headerStart = findSplitIndex(bodies, headers);
const footerStart = bodies.length - 1 - findSplitIndex(
[...bodies].reverse(),
[...footers].reverse(),
);
return [
...bodies.slice(0, headerStart + 1),
"",
...bodies.slice(headerStart + 1, footerStart).join("\n").trim().split("\n"),
"",
...bodies.slice(footerStart),
];
}
util.tsexport function patchLines(lines: string[], appends: string[]) {
let index = 0;
const result = [] as string[];
for (let i = 0; i < appends.length; i++) {
const pos = lines.findIndex((line, j) => j >= index && line.trim() === appends[i].trim());
if (pos < 0) {
result.push(appends[i]);
continue;
}
result.push(...lines.slice(index, pos + 1));
index = pos + 1;
}
result.push(...lines.slice(index));
return result;
}
export function findSplitIndex(lines: string[], query: string[]) {
let index = -1;
for (const text of query) {
const pos = lines.findIndex((line, j) => j > index && line.trim() === text.trim());
if (pos < 0) return -1;
index = pos;
}
return index;
}