generated at
times-dev
/nishio/pin-diary-Xから分離した
pin機能をdisableするため


entrypoint.ts
import { launch } from "./main.ts"; import { makeDiary, filter, } from "./template.ts"; launch( "mitoujr-script", { makeDiary, filter, }, );

template.ts
import { 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.ts
import { 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.ts
export { 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.ts
import { 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.ts
export 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; }