///
///
///
///
import {
disconnect,
connect,
patch,
replaceLinks,
type ScrapboxSocket,
} from "../scrapbox-userscript-std/mod.ts";
import type { ErrorLike } from "../scrapbox-jp%2Ftypes/rest.ts";
import { pooledMap } from "jsr:@std/async@1/pool";
import { isErr, unwrapErr, unwrapOk } from "npm:option-t@49/plain_result";
export interface Link {
before: string;
after: string;
}
export interface ReplaceState {
link: Link;
projectCount: number;
replaced: number;
done: boolean;
};
export async function* replace(
links: Link[],
projects: Iterable,
): AsyncGenerator {
if (links.length === 0) return;
if (links.every(({ before, after }) => before === after)) return;
// throw an exception when this result is not ok.
const socket: ScrapboxSocket = unwrapOk(await connect());
try {
const { readable, writable } = new TransformStream(undefined);
const writer = writable.getWriter();
const iter = pooledMap(
5,
links,
async (link) => {
let count = 0;
let replaced = 0;
if (link.before === link.after) {
await writer.ready;
await writer.write({ link, projectCount: 0, replaced: 0, done: true });
}
const iter = pooledMap(
2,
new Set(projects),
async (project) => {
const result = await replaceAlink(link, project, socket);
if (isErr(result)) throw toError(unwrapErr(result));
count++;
replaced += unwrapOk(result);
await writer.ready;
await writer.write({ link, projectCount: count, replaced, done: false });
},
);
await Array.fromAsync(iter);
await writer.ready;
await writer.write({ link, projectCount: count, replaced, done: true });
},
);
const done = Array.fromAsync(iter).then(async () => {
await writer.ready;
await writer.close();
});
// lib.dom.d.tsに[Symbol.asyncIterator]がまだ実装されていない
const reader = readable.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) return;
yield value;
}
await done;
} finally {
await disconnect(socket);
}
};
/** 一つのリンクを一つのprojectで置換する
*
* @return replaceLinksと同じ
*/
const replaceAlink = async (link: Link, project: string, socket: ScrapboxSocket) => {
const [result] = await Promise.all([
// 本当はhasBackLinksOrIcons === trueのときのみ置換したい
replaceLinks(project, link.before, link.after),
patch(project, link.before, (lines, { persistent }) => {
if (!persistent) return;
return [
link.after,
...lines.map((line) => line.text).slice(1),
];
}, { socket }),
]);
return result;
};
export const getLinks = (text: string): string[] =>
text.split("\n")
.flatMap(
(line) => [...line.matchAll(/\[((?:[^\[!"#%&'()\*\+,\-\.\/\{\|\}<>_~] |.[^ ]*)[^\[\]]*)\]/g)]
)
.map(([, link]) => link);
const toError = (e: ErrorLike): Error => {
const error = new Error();
error.name = e.name;
error.message = e.message;
return error;
};