Cosenseのページ範囲を指定してクリップボードにコピーするUserScript
スクボ読書する際に、数ページにまたがる内容を一度に取得したい 要約したり、翻訳したりするために
最初は、UserScriptからOpenAI APIを呼んで要約するようにしていた
でも、別にそこまでしなくてもclipboardに入るだけで便利
プロンプトを自分で決められるなどなど
一部制約がある
スクボ読書の特定のフォーマットに沿っていないといけない
基本的には、連番でいいのだが
https://gyazo.com/5b410075704744df3530186f64ea86ac
たまに章題をタイトルに含んでいることが有る
https://gyazo.com/684b6b36787cd2aeeec511304177e2e4
そのため、「次のページ」をプログラムで確定できない
なにか策が必要
リストを表示してチェックボックスにチェックする
正規表現などで、その数字から始まるものを対象に含める
内容を読んで、nextの次のタイトルを見る
形式が決まってしまうが、これが一番確実ではある
ここを後で改修可能なように実装しておくとか
いったんこれにしたmrsekut.icon
https://gyazo.com/4ac34d0f07ecc046b01cc205e72ea024
ここだけフォーマット依存
code:script.js
/**
* Parses the next page title from the page text.
*
* このようなページのフォーマットを前提にしている
* `
* ...
* `
*
* @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 && nextLineMatch1) { return nextLineMatch1.replace(/\s/g, "_"); }
return null;
}
code:script.js
// @ts-check
// TODO: もっと目立たないところに置く?
scrapbox.PageMenu.addMenu({
title: "ページ内容をコピー",
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;">×</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));
}