generated at
custom-new-page-3
scrapbox-userscript-std/browser/websocketを使って新しく実装し直した

機能
選択範囲がある場合は、その部分を切り出す
別のprojectに切り出せる

実装したいこと
選択範囲があるver.をpopup menuに入れる


2024-11-13
18:01:55 scrapbox-userscript-stdの破壊的変更に対応
2022-12-08
05:15:03 外部moduleの破壊的変更に対応
2022-04-07
06:20:42 NewPageHookOptions lines を追加した
行IDや行作成日時などを切り出し処理で使えるようになった

$ deno check --remote -r=https://scrapbox.io https://scrapbox.io/api/code/takker/custom-new-page-3/mod.ts
mod.ts
import { 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.ts
import { 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, }, }, }; }

切り出し関数の型定義と、defaultで使われるhookの定義
hook.ts
import { 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, }], }; };

#2024-11-13 18:01:24
#2023-02-03 06:12:02
#2023-01-13 06:07:20
#2023-01-07 07:15:34
#2022-04-07 06:20:10
#2022-03-13 15:06:17 通知アイコンがダブってた
#2022-03-12 14:46:11