script.jsimport { setup } from "./mod.js";
setup(["takker-memex", "takker"]);
mod.jsvar l=(e,t)=>{if(!(e instanceof HTMLDivElement))throw new TypeError(`"${t}" must be HTMLDivElememt but actual is "${e}"`)};var m=()=>w(document.getElementsByClassName("status-bar")?.[0],"div.status-bar"),w=(e,t)=>{if(!!e)return l(e,t),e};function p(){let e=m();if(!e)throw new Error("div.status-bar can't be found");let t=document.createElement("div");return e.append(t),{render:(...n)=>{t.textContent="";let r=g(...n);r&&t.append(r)},dispose:()=>t.remove()}}function g(...e){let t=e.flatMap(r=>{switch(r.type){case"spinner":return[k()];case"check-circle":return[C()];case"exclamation-triangle":return[v()];case"text":return[a(r.text)];case"group":{let o=g(...r.items);return o?[o]:[]}}});if(t.length===0)return;if(t.length===1)return t[0];let n=document.createElement("span");return n.classList.add("item-group"),n.append(...t),n}function a(e){let t=document.createElement("span");return t.classList.add("item"),t.append(e),t}function k(){let e=document.createElement("i");return e.classList.add("fa","fa-spinner"),a(e)}function C(){let e=document.createElement("i");return e.classList.add("kamon","kamon-check-circle"),a(e)}function v(){let e=document.createElement("i");return e.classList.add("fas","fa-exclamation-triangle"),a(e)}var u=e=>[...e].map((t,n)=>t===" "?"_":!D.includes(t)||n===e.length-1&&I.includes(t)?encodeURIComponent(t):t).join(""),D='@$&+=:;",',I=':;",';function x(e,t,n){let r=document.createElement("a");r.href=`/${e}/${u(t)}${typeof n!="string"?"":`?body=${encodeURIComponent(n)}`}`,document.body.append(r),r.click(),r.remove()}var i="next-action",h,A="/assets/img/favicon/apple-touch-icon.png";function rt(e){let t=`head style[data-userscript-name="${i}"]`;document.querySelector(t)?.remove?.();let n=document.createElement("style");n.dataset.userscriptName=i,n.textContent=`a#${i}.tool-btn:hover {
text-decoration: none;
}
a#${i}.tool-btn::before {
position: absolute;
content: "\\f0ae";
font: 900 20px/46px "Font Awesome 5 Free";
}
a#${i}.tool-btn img {
opacity: 0;
}
a#${i}.tool-btn ~ ul a::before {
position: absolute;
font-family: "Font Awesome 5 Free";
font-weight: 900;
}
a#${i}.tool-btn ~ ul img {
opacity: 0;
margin-right: 0;
}`,document.head.append(n),document.getElementById(i)||scrapbox.PageMenu.addMenu({title:i,image:A,onClick:async()=>{h??=B(e),await h}})}async function B(e){scrapbox.PageMenu(i).removeAllItems();let{render:t,dispose:n}=p(),r=0;try{for(let o of e){t({type:"spinner"},{type:"text",text:`Searching "/${o}" for next actions...`});for await(let s of K(o))r++,scrapbox.PageMenu(i).addItem({title:s,onClick:()=>{let c=`https://scrapbox.io/${o}/${u(s)}`;if(o!==scrapbox.Project.name){window.open(c);return}x(c)}});o!==e[e.length-1]&&scrapbox.PageMenu(i).addSeparator()}t({type:"check-circle"},{type:"text",text:`Found ${r} actions.`})}catch(o){t({type:"exclamation-triangle"},{type:"text",text:o instanceof Error?`${o.name} ${o.message}`:"Unknown error! (see developper console)"}),console.error(o)}finally{setTimeout(()=>n(),1e3)}}async function*K(e,t){if(t??=/^(?:⬜(?:[^p]*|p[^-]*))|🔳/,e===scrapbox.Project.name){for(let{title:r,exists:o}of scrapbox.Project.pages)!t.test(r)||(yield r);return}let n=new Set;for await(let r of N(e))!n.has(r)&&t.test(r)&&(n.add(r),yield r)}async function*N(e){let t=`/api/pages/${e}/search/titles`,n=null;do{let r=`${t}${n?`?followingId=${n}`:""}`,o=await fetch(r);n=o.headers.get("X-following-id");let s=await o.json();for(let{title:c,links:E}of s){yield c;for(let y of E)yield y}if(!n)break}while(!0)}export{rt as setup};
mod.ts/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
import {
useStatusBar,
openInTheSameTab,
encodeTitleURI,
} from "../scrapbox-userscript-std/dom.ts";
import type { Scrapbox, SearchedTitle } from "https://raw.githubusercontent.com/scrapbox-jp/types/0.0.8/mod.ts";
declare const scrapbox: Scrapbox;
const id = "next-action";
let initialized: Promise<void>;
const dummyImage = "/assets/img/favicon/apple-touch-icon.png";
export function setup(projects: string[]) {
const selector = `head style[data-userscript-name="${id}"]`;
document.querySelector(selector)?.remove?.();
const style = document.createElement("style");
style.dataset.userscriptName = id;
style.textContent = `a#${id}.tool-btn:hover {
text-decoration: none;
}
a#${id}.tool-btn::before {
position: absolute;
content: "\\f0ae";
font: 900 20px/46px "Font Awesome 5 Free";
}
a#${id}.tool-btn img {
opacity: 0;
}
a#${id}.tool-btn ~ ul a::before {
position: absolute;
font-family: "Font Awesome 5 Free";
font-weight: 900;
}
a#${id}.tool-btn ~ ul img {
opacity: 0;
margin-right: 0;
}`;
document.head.append(style);
if (!document.getElementById(id)) {
scrapbox.PageMenu.addMenu({
title: id,
image: dummyImage,
onClick: async () => {
initialized ??= load(projects);
await initialized;
},
});
}
}
mod.tsasync function load(projects: string[]) {
scrapbox.PageMenu(id).removeAllItems();
const { render, dispose } = useStatusBar();
let count = 0;
try {
for (const project of projects) {
render(
{ type: "spinner" },
{ type: "text", text: `Searching "/${project}" for next actions...`},
);
for await (const title of listNextActions(project)) {
count++;
scrapbox.PageMenu(id).addItem({
title,
onClick: () => {
const path = `https://scrapbox.io/${
project
}/${encodeTitleURI(title)}`;
if (project !== scrapbox.Project.name) {
window.open(path);
return;
}
openInTheSameTab(path);
},
});
}
if (project === projects[projects.length - 1]) continue;
scrapbox.PageMenu(id).addSeparator();
}
render(
{ type: "check-circle" },
{ type: "text", text: `Found ${count} actions.`},
);
} catch(e: unknown) {
render(
{ type: "exclamation-triangle" },
{ type: "text", text: e instanceof Error ?
`${e.name} ${e.message}` :
`Unknown error! (see developper console)`,
},
);
console.error(e);
} finally {
setTimeout(() => dispose(), 1000);
}
}
mod.tsasync function* listNextActions(project: string, filter?: RegExp) {
filter ??= /^(?:⬜(?:[^p]*|p[^-]*))|🔳/;
if (project === scrapbox.Project.name) {
for (const { title, exists } of scrapbox.Project.pages) {
if (!filter.test(title)) continue;
yield title;
}
return;
}
const titles = new Set<string>();
for await (const title of getLinks(project)) {
if (!titles.has(title) && filter.test(title)) {
titles.add(title);
yield title;
}
}
}
async function* getLinks(project: string) {
const path = `/api/pages/${project}/search/titles`;
let followingId = null;
do {
const path_ = `${path}${
followingId ? `?followingId=${followingId}` : ""
}` as string;
const res = await fetch(path_);
followingId = res.headers.get("X-following-id");
const pages = (await res.json()) as SearchedTitle[];
for (const { title, links } of pages) {
yield title;
for (const link of links) {
yield link;
}
}
if (!followingId) break; // 空文字列の場合もある
} while (true)
}