pin-diary-beforebundle-script
code:example.ts
import { launch } from "./mod.ts";
launch("wogikaze-study", { project: "yuyasurarin", title: "pin-diary-script" });
code:list.ts
import { listPages, type ListPagesOption } from "jsr:@cosense/std/rest";
import type { BasePage } from "jsr:@cosense/types/rest";
import { isErr, unwrapOk } from "npm:option-t/plain_result";
export type { BasePage, ListPagesOption };
export async function* listPinnedPages(
project: string,
options?: Omit<ListPagesOption, "sort" | "skip">,
): AsyncGenerator<BasePage, void, unknown> {
let skip = 0;
const limit = options?.limit ?? 100;
while (true) {
const result = await listPages(project, {
...options,
limit,
sort: "updated",
skip,
});
if (isErr(result)) return;
const pages = unwrapOk(result).pages;
for (const page of pages) {
if (page.pin === 0) continue;
yield page;
}
const lastPage = pages.at(-1);
if (unwrapOk(result).count < limit || (lastPage?.pin ?? 0) === 0) return; // 書き換え
skip += limit;
}
}
code:mod.ts
import {
connect,
disconnect,
patch,
pin,
type PushError,
type ScrapboxSocket,
unpin,
useStatusBar,
} from "jsr:@cosense/std/browser";
import type { Scrapbox } from "jsr:@cosense/types/userscript";
import { delay } from "jsr:@std/async/delay";
import { isString } from "jsr:@core/unknownutil/is/string";
import { format } from "jsr:@takker/pin-diary/format";
import { listPinnedPages } from "./list.ts";
import {
andThenAsyncForResult,
createOk,
isErr,
isOk,
type Result,
unwrapErr,
unwrapOk,
} from "npm:option-t/plain_result";
import type { Socket } from "npm:socket.io-client";
import {
type DiaryMaker,
load,
makeDiaryMaker,
type Template,
type TemplateLocation,
} from "./template.ts";
export { format } from "jsr:@takker/pin-diary@/format";
export { expand } from "jsr:@takker/pin-diary/expand";
declare const scrapbox: Scrapbox;
export interface DiaryCommonOptions {
/** 日記ページの更新間隔 (ms) */
interval?: number;
}
export const launch = (project: string, init: DiaryInit): () => void => {
const interval = init.interval ?? 24 * 3600 * 1000;
let updateTimer: number | undefined;
const startObserve = async () => {
endObserve();
await pinDiary(project, new Date(), init);
updateTimer = setInterval(
() => pinDiary(project, new Date(), init),
interval,
);
};
const endObserve = () => clearInterval(updateTimer);
const handleChange = () =>
scrapbox.Project.name === project ? startObserve() : endObserve();
handleChange();
scrapbox.addListener("project:changed", handleChange);
return () => {
endObserve();
scrapbox.removeListener("project:changed", handleChange);
};
};
export const pinDiary = async (
project: string,
date: Date,
init: DiaryMaker | Template | TemplateLocation,
): Promise<void> => {
const { render, dispose } = useStatusBar();
let socket: ScrapboxSocket | undefined;
try {
let diaryMaker: DiaryMaker;
if ("title" in init) {
if ("header" in init) {
diaryMaker = makeDiaryMaker(init);
} else {
const res = await load(init.project ?? project, init.title);
if (isOk(res)) {
diaryMaker = makeDiaryMaker(unwrapOk(res));
} else {
const error = unwrapErr(res);
const text = `Failed to load template from /${
init.project ?? project
}/${init.title}.\nPlease make sure this page includes the following 3 code blocks: "title", "header", and "footer".`;
render({ type: "exclamation-triangle" }, { type: "text", text });
console.error(text, error);
return;
}
}
} else {
diaryMaker = init;
}
const { makeDiary, isOldDiary } = diaryMaker;
const res = await andThenAsyncForResult(
(await connect()) as Result<
ScrapboxSocket,
Socket.DisconnectReason | PushError
,
async (s) => {
socket = s;
// 今日以外の日付ページを外す
render(
{ type: "spinner" },
{ type: "text", text: unpin other diary pages... },
);
for await (const { title } of listPinnedPages(project)) {
if (!isOldDiary(title, date)) continue;
const res = await unpin(project, title, { socket });
if (isErr(res)) return res;
}
// 今日の日付ページをピン留めする
const { title, header, footer } = makeDiary(date);
render(
{ type: "spinner" },
{ type: "text", text: pin "/${project}/${title}"... },
);
const res = await pin(project, title, { socket, create: true });
if (isErr(res)) return res;
// 今日の日付ページにtemplateを挿入する
render(
{ type: "spinner" },
{ type: "text", text: format "/${project}/${title}"... },
);
const res2 = await patch(project, title, (lines) => [
...format(
lines.slice(1).map((line) => line.text),
header,
footer,
),
], { socket });
if (isErr(res2)) return res2;
render(
{ type: "check-circle" },
{ type: "text", text: Pinned "/${project}/${title}". },
);
await disconnect(socket);
return createOk<void>(undefined);
},
);
if (isOk(res)) return;
const error = unwrapErr(res);
render(
{ type: "exclamation-triangle" },
{
type: "text",
text: !isString(error)
? ${error.name}${"message" in error && : ${error.message}}
: SocketIO error: ${error},
},
);
console.error(error);
} catch (error: unknown) {
render(
{ type: "exclamation-triangle" },
{
type: "text",
text: error instanceof Error
? ${error.name} ${error.message}
: Unknown error! (see developper console),
},
);
console.error(error);
} finally {
if (socket) await disconnect(socket);
await delay(1000);
dispose();
}
};
code:template.ts
import {
type CodeBlockError,
type FetchError,
getCodeBlock,
} from "jsr:/@cosense/std@^0.29.1/rest";
import { createOk, isErr, type Result, unwrapOk } from "npm:/option-t@^49.2.0/plain_result";
import { expand } from "jsr:@takker/pin-diary/expand";
/**
* Represents a template used to format a diary page.
*/
export interface Template {
/** the diary page title */
title: string;
/** the diary page header */
header: string;
/** the diary page footer */
footer: string;
}
/**
* Represents a expanded template used to format a diary page.
*/
export interface ExpandedTemplate {
/** the diary page title */
title: string;
/** the diary page header */
header: string[];
/** the diary page footer */
footer: string[];
}
/**
* Represents functions used to create and format diary pages.
*
* This is good for creating a diary template programmatically.
* If you want to create a just simple diary template or to modify it without updating the script, use {@linkcode TemplateLocation} instead.
*/
export interface DiaryMaker {
makeDiary: (date: Date) => ExpandedTemplate;
/** 今日以外の日記ページかどうかを判断する函数
*
* @param title 判断対象のページタイトル
* @param today 今日の日付
* @param 今日以外の日記ページならtrue, それ以外のページは false
*/
isOldDiary: (title: string, today: Date) => boolean;
}
/**
* Represents a location of a page which includes a diary template.
*/
export interface TemplateLocation {
/** the project name of the diary temlate
*
* If it is not set, those which is passed to {@linkcode launch} or {@linkcode pinDiary} will be used.
*/
project?: string;
/** the title of the diary template page */
title: string;
}
/**
* @internal
* Load a template from a page [/project/title]
*/
export const load = async (
project: string,
title: string,
): Promise<Result<Template, CodeBlockError | FetchError>> => {
const titleRes = await getCodeBlock(project, title, "title");
if (isErr(titleRes)) return titleRes;
const header = await getCodeBlock(project, title, "header");
if (isErr(header)) return header;
const footer = await getCodeBlock(project, title, "footer");
if (isErr(footer)) return footer;
return createOk({
title: unwrapOk(titleRes),
header: unwrapOk(header),
footer: unwrapOk(footer),
});
};
/**
* @internal
* Convert {@linkcode Template} to {@linkcode DiaryMaker}
*/
export const makeDiaryMaker = (
template: Template,
): DiaryMaker => ({
makeDiary: (date) => ({
title: expand(date, template.title)0, header: expand(date, template.header),
footer: expand(date, template.footer),
}),
isOldDiary: (title, today) => expand(today, title)0 !== title, });