Cosenseのページ範囲を指定してクリップボードにコピーするUserScript
スクボ読書する際に、数ページにまたがる内容を一度に取得したい
要約したり、翻訳したりするために
最初は、UserScriptからOpenAI APIを呼んで要約するようにしていた
/mrsekut-private/Cosenseのページ範囲を指定して要約させるUserScript
でも、別にそこまでしなくてもclipboardに入るだけで便利
プロンプトを自分で決められるなどなど
一部制約がある
スクボ読書の特定のフォーマットに沿っていないといけない
基本的には、連番でいいのだが
https://gyazo.com/5b410075704744df3530186f64ea86ac
たまに章題をタイトルに含んでいることが有る
https://gyazo.com/684b6b36787cd2aeeec511304177e2e4
そのため、「次のページ」をプログラムで確定できない
なにか策が必要
リストを表示してチェックボックスにチェックする
正規表現などで、その数字から始まるものを対象に含める
内容を読んで、nextの次のタイトルを見る
形式が決まってしまうが、これが一番確実ではある
ここを後で改修可能なように実装しておくとか
いったんこれにしたmrsekut.icon
https://gyazo.com/4ac34d0f07ecc046b01cc205e72ea024
わかりづらいが、モーダルが消えたタイミングでクリップボードに丸々内容が入ってる
code:script.js
// @ts-check
export {};
/**
* Parses the next page title from the page text.
*
* このようなページのフォーマットを前提にしている
* `
* prev: 015
* next: 017
* ...
* `
*/
function parseNextPageTitle(pageText) {
const nextLineMatch = pageText.match(/^next:\s*\(.*?)\/m);
if (nextLineMatch && nextLineMatch1) {
return nextLineMatch1.replace(/\s/g, '_');
}
return null;
}
cosense.PageMenu.addMenu({
title: 'book',
image: 'https://i.gyazo.com/c24ee728d18f60cdeccf124cfe55eafa.png',
});
cosense.PageMenu('book').addItem({
title: 'ページ内容をコピー',
image: 'https://gyazo.com/a1171e2083db9a394dc80072bbeb82da/raw',
onClick: main,
});
async function main() {
const { render, close, log, isShown } = modal();
// TODO: clean
const onSubmit = async endPage => {
const start = cosense.Page.title;
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 = cosense.Page.title;
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 };
}
/**
* ページ内容を逐次取得する
*/
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;
}
}
async function pageBody(title) {
const n = cosense.Project.name;
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();
}
/**
* @param {number} ms
* @returns {Promise<void>}
*/
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}