/// /// /// /// 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; };