generated at
Cosenseのページ範囲を指定してクリップボードにコピーするUserScript

スクボ読書する際に、数ページにまたがる内容を一度に取得したい
要約したり、翻訳したりするために
最初は、UserScriptからOpenAI APIを呼んで要約するようにしていた
でも、別にそこまでしなくてもclipboardに入るだけで便利
プロンプトを自分で決められるなどなど

一部制約がある
スクボ読書の特定のフォーマットに沿っていないといけない
基本的には、連番でいいのだが
たまに章題をタイトルに含んでいることが有る
そのため、「次のページ」をプログラムで確定できない
なにか策が必要
リストを表示してチェックボックスにチェックする
正規表現などで、その数字から始まるものを対象に含める
内容を読んで、nextの次のタイトルを見る
形式が決まってしまうが、これが一番確実ではある
ここを後で改修可能なように実装しておくとか
いったんこれにしたmrsekut





わかりづらいが、モーダルが消えたタイミングでクリップボードに丸々内容が入ってる

ここだけフォーマット依存
script.js
/** * Parses the next page title from the page text. * * このようなページのフォーマットを前提にしている * ``` * prev: [015] * next: [017] * ... * ``` * * @param {string} pageText - The text content of the current page. * @returns {string|null} The next page title, or null if not found. */ function parseNextPageTitle(pageText) { const nextLineMatch = pageText.match(/^next:\s*\[(.*?)\]/m); if (nextLineMatch && nextLineMatch[1]) { return nextLineMatch[1].replace(/\s/g, "_"); } return null; }


script.js
// @ts-check // TODO: もっと目立たないところに置く? scrapbox.PageMenu.addMenu({ title: "ページ内容をコピー", image: "https://gyazo.com/a1171e2083db9a394dc80072bbeb82da/raw", onClick: async () => main(), }); async function main() { const { render, close, log, isShown } = modal(); // TODO: clean const onSubmit = async (endPage) => { const start = pageTitle(); const generator = pagesContentGenerator(start, endPage); let result = ""; for await (const { title, content } of generator) { if (!isShown()) { log("Process aborted."); break; } result += content; log(title); await sleep(200); } await navigator.clipboard.writeText(result); console.log("Content copied to clipboard:", result); close(); }; render(onSubmit); } function modal() { const container = $("<div></div>"); let shown = false; function render(onSubmit) { shown = true; const thisPage = pageTitle(); const formHtml = ` <div id="inputForm" style="position:fixed;top:20%;left:30%;background:white;padding:20px;box-shadow:0 0 10px rgba(0,0,0,0.5);display:grid;grid-template-rows:auto auto auto;gap:10px;z-index:10;"> <div style="display:flex;justify-content:space-between;align-items:center;gap:1rem;"> <div style="font-size:2rem;">ページ範囲を入力</div> <button id="closeButton" style="background:none;border:none;font-size:1.2em;cursor:pointer;">&times;</button> </div> <div style="">close or reload で中断</div> <div> <p>${thisPage} ~ <input id="endPage" placeholder="091" style="width:16rem;"></p> </div> <div style="display:flex;justify-content:space-between;"> <button id="submitButton">submit</button> </div> <div id="logContainer" style="color:gray;font-size:0.9em;"></div> </div> `; container.html(formHtml); $("body").append(container); setupEventListeners(onSubmit); } function close() { shown = false; container.remove(); } function isShown() { return shown; } function setupEventListeners(onSubmit) { $("#submitButton").on("click", () => { const endPage = $("#endPage").val(); onSubmit(endPage); }); $("#closeButton").on("click", close); } function log(message) { const logContainer = $("#logContainer"); logContainer.append(`<div>${message}</div>`); } return { render, close, log, isShown }; } /** * ページ内容を逐次取得する * * @param {string} start - The starting page number. * @param {string} end - The ending page number. * @returns {AsyncGenerator<{title: string, content: string}>} The generator. */ async function* pagesContentGenerator(start, end) { let currentTitle = start; while (true) { const pageText = await pageBody(currentTitle); yield { title: currentTitle, content: pageText }; if (currentTitle === end) { break; } const nextPageTitle = parseNextPageTitle(pageText); if (nextPageTitle == null) { break; } currentTitle = nextPageTitle; } } /** * @param {string} title * @returns {Promise<string>} The body of the page. */ async function pageBody(title) { const n = projectName(); const url = encodeURI(`https://scrapbox.io/api/pages/${n}/${title}/text`); const res = await fetch(url); if (!res.ok) { throw new Error(`Failed to fetch page ${title}`); } return await res.text(); } /** * @returns {string} The encoded project name. */ function projectName() { return scrapbox.Project.name; } /** * @returns {string} The encoded page title. */ function pageTitle() { return scrapbox.Page.title; } /** * @param {number} ms * @returns {Promise<void>} */ async function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }