NewPageHookOptions
に lines
を追加した$ deno check --remote -r=https://scrapbox.io https://scrapbox.io/api/code/takker/custom-new-page-3/mod.ts
mod.tsimport {
caret,
disconnect,
connect,
patch,
type ScrapboxSocket,
openInTheSameTab,
encodeTitleURI,
useStatusBar,
} from "../scrapbox-userscript-std/mod.ts";
import { getSelection } from "./selection.ts";
import {
defaultHook,
type NewPageHook,
type NewPageHookResult,
type OpenMode,
} from "./hook.ts";
import { delay } from "jsr:@std/async@1/delay";
import { unwrapOk } from "npm:option-t@49/plain_result";
import type { Scrapbox } from "../scrapbox-jp%2Ftypes/userscript.ts";
declare const scrapbox: Scrapbox;
export type {
NewPageHook,
NewPageHookResult,
NewPageHookOptions,
Page,
OpenMode,
} from "./hook.ts";
export interface NewPageInit {
/** 切り出し先project
*
* @default 現在のprojectと同じ
*/
project?: string;
/** 切り出したページを開く方法
*
* @default: "newtab"
*/
mode?: OpenMode;
/** 切り出したページなどを作成する関数
*
* 最初に`undefined`以外を返したhookだけを採用する
*/
hooks?: NewPageHook[];
}
export const newPage = async (init?: NewPageInit) => {
// 設定とか
const {
project = scrapbox.Project.name,
mode = "newtab",
} = init ?? {};
const hooks = [...(init?.hooks ?? []), defaultHook];
// 切り出す範囲とテキストを取得する
const { selectionRange: { start, end }, selectedText } = getSelection();
if (!selectedText) return;
if (scrapbox.Layout !== "page") return;
// 切り出すページを作る
let result: NewPageHookResult | undefined;
for (const hook of hooks) {
const promise = hook(selectedText, {
title: scrapbox.Page.title,
projectFrom: scrapbox.Project.name,
projectTo: project,
lines: scrapbox.Page.lines.slice(start.line, end.line + 1),
mode,
});
result = promise instanceof Promise ? await promise : promise;
if (result) break;
}
if (result === undefined) {
//ここに到達したらおかしい
throw Error("どの関数でも切り出しできなかった");
}
// 個別のpageに切り出す
let socket: ScrapboxSocket | undefined;
const { render, dispose } = useStatusBar();
try {
const length = result.pages.length;
render(
{ type: "spinner" },
{ type: "text", text: `Create new ${length} pages...` },
);
socket = unwrapOk(await connect());
let counter = 0;
await Promise.all(result.pages.map(
async (page) => {
await patch(page.project, page.title, (lines) => [
...lines.map((line) => line.text),
...page.lines,
], { socket });
render(
{ type: "spinner" },
{
type: "text",
text: `Create ${length - (++counter)} pages...`,
},
);
},
));
render(
{ type: "spinner" },
{ type: "text", text: "Created. Removing cut text..." },
);
// 書き込みに成功したらもとのテキストを消す
const text = result.text;
await patch(scrapbox.Project.name, scrapbox.Page.title, (lines) => {
const lines_ = lines.map((line) => line.text);
return [
...lines_.slice(0, start.line),
...`${lines_[start.line].slice(0, start.char)}${text}${
// end.charが行末+1まであった場合は、end.lineの直後の改行まで取り除かれる
lines_.slice(end.line).join("\n").slice(end.char)
}`.split("\n"),
];
});
render(
{ type: "check-circle" },
{ type: "text", text: "Removed." },
);
// ページを開く
for (const page of result.pages) {
switch (page.mode) {
case "self":
if (page.project === scrapbox.Project.name) {
openInTheSameTab(page.project, page.title);
} else {
// UserScriptを再読込させる
window.open(`https://scrapbox.io/${page.project}/${
encodeTitleURI(page.title)
}`, "_self");
}
break;
case "newtab":
window.open(`https://scrapbox.io/${page.project}/${
encodeTitleURI(page.title)
}`);
break;
}
}
} catch (e: unknown) {
render(
{ type: "exclamation-triangle" },
{ type: "text", text: "Failed to create new pages (see console)." },
);
console.error(e);
} finally {
const waiting = delay(1000);
if (socket) await disconnect(socket);
await waiting;
dispose();
}
};
start
になるよう調整しておくselection.tsimport { caret, getIndentLineCount, type CaretInfo, getText } from "../scrapbox-userscript-std/mod.ts";
import type { Scrapbox } from "../scrapbox-jp%2Ftypes/userscript.ts";
declare const scrapbox: Scrapbox;
export const getSelection = (): Omit<CaretInfo, "position"> => {
if (scrapbox.Layout !== "page") return {
selectionRange: {
start: { line: 0, char: 0 },
end: { line: 0, char: 0 },
},
selectedText: "",
};
const { selectionRange, selectedText, position } = caret();
if (!selectedText) {
const count = getIndentLineCount(position.line) ?? 0;
const selectionRange = {
start: {
line: position.line,
char: 0,
},
end: {
line: position.line + count,
char: getText(position.line + count)?.length ?? 0,
},
};
return {
selectionRange,
selectedText: scrapbox.Page.lines.slice(
selectionRange.start.line,
selectionRange.end.line + 1,
).map((line) => line.text).join("\n"),
};
}
const { start, end } = selectionRange;
const larger = start.line > end.line;
const startLine = larger ? end.line : start.line;
const startChar = larger ? end.char : start.char; // この番号の文字から含む
const endLine = larger ? start.line : end.line;
const endChar = larger ? start.char : end.char; // この番号以降の文字は含まない
return {
selectedText,
selectionRange: {
start: {
line: startLine,
char: startChar,
},
end: {
line: endLine,
char: endChar,
},
},
};
}
hook.tsimport {
getIndentCount,
} from "../scrapbox-userscript-std/text.ts";
import type { Line } from "../scrapbox-jp%2Ftypes/userscript.ts";
/** 切り出したページを開く方法
*
* - "self": 同じページで開く
* - "newtab": 新しいページで開く
* - "noopen": 開かない
*/
export type OpenMode = "self" | "newtab" | "noopen";
/** 一つのページを表すデータ */
export interface Page {
/** ページのproject */
project: string;
/** ページタイトル */
title: string;
/** タイトルを除いた本文 */
lines: string[];
/** 切り出したページを開く方法
*
* `newPage()`に渡された設定をこれで上書きできる
*/
mode: OpenMode;
}
/** 切り出し時の書式設定を行う関数
*
* @param text 切り出す文字列
* @param options `newPage()`で渡されたhooks以外の情報
* @return 新規作成するページ情報とかを返す。もし条件に一致しないなどで切り出さない場合は`undefined`を返す
*/
export type NewPageHook = (
text: string,
options: NewPageHookOptions,
) => Promise<NewPageHookResult | undefined> | NewPageHookResult | undefined;
export interface NewPageHookOptions {
/** 切り出し元ページのtitle */
title: string;
/** 切り出し元project */
projectFrom: string;
/** 切り出し先project */
projectTo: string;
/** 切り出し範囲を含む行 */
lines: Line[];
/** 切り出したページを開く方法 */
mode: OpenMode;
}
export interface NewPageHookResult {
/** 元のページに残すテキスト */
text: string;
/** 切り出すページ */
pages: Page[];
}
/** 何も設定されていないときに使われるhook
*
* 仕様はScrapboxのとほぼ同じ
*/
export const defaultHook = (
text: string,
{ title, projectTo, mode }: NewPageHookOptions,
): NewPageHookResult => {
const [rawTitle, ...lines] = text.split("\n");
const newTitle = rawTitle.replaceAll("[", "").replaceAll("]", "").trim();
// 余計なインデントを削る
const minIndentNum = Math.min(...[rawTitle, ...lines].map((line) => getIndentCount(line)));
const newLines = [
`from [${title}]`,
rawTitle.slice(minIndentNum),
...lines.map(
(line) => line.slice(minIndentNum)
),
];
return {
text: `[${newTitle}]`,
pages: [{
project: projectTo,
title: newTitle,
lines: newLines,
mode,
}],
};
};