UserScripts
code:script.js
import("https://scrapbox.io/api/code/zahyou/UserScripts/diary-template.js");
import("https://scrapbox.io/api/code/zahyou/UserScripts/dictionary.js");
import("https://scrapbox.io/api/code/zahyou/UserScripts/aliasComp.js");
import("https://scrapbox.io/api/code/zahyou/UserScripts/aliasRedirect.js");
import("https://scrapbox.io/api/code/zahyou/UserScripts/quote.js");
import("https://scrapbox.io/api/code/zahyou/UserScripts/caption.js");
import("https://scrapbox.io/api/code/zahyou/UserScripts/a.js");
import("https://scrapbox.io/api/code/zahyou/UserScripts/scrapbox-url-customizer.js");
import("https://scrapbox.io/api/code/zahyou/UserScripts/year-input-customization.js");
import("https://scrapbox.io/api/code/zahyou/UserScripts/alias.js");
日記テンプレート
参考:/ci7lus-diary/ci7lus
code:diary-template.js
/* https://scrapbox.io/ci7lus-diary/ci7lus */
/* MIT License Copyright (c) 2020 ci7lus */
import { importExternalJs } from "/api/code/ci7lus/userscript-utils/import-external-js.js";
importExternalJs(
"https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.8.36/dayjs.min.js"
);
import { insertText } from "/api/code/customize/scrapbox-insert-text/script.js";
scrapbox.PageMenu.addMenu({
title: 日記,
image: "https://cdnjs.cloudflare.com/ajax/libs/twemoji/12.0.4/svg/1f4dd.svg",
onClick: () => {
if (!scrapbox.Page.lines || !scrapbox.Page.lines.length == 1) return;
const input = prompt(
"日記テンプレートを展開したい日付を相対(d+)または絶対(2020-1-1)で(入力なしで今日)"
);
if (input === null) return;
const diff = parseInt(input.trim() || 0);
const abs = input.split("-").length === 3 && dayjs(input);
if ((Number.isNaN(diff) && !abs) || (abs && !abs.isValid())) return;
const today = abs
? abs.startOf("days")
: dayjs().startOf("days").add(diff, "days");
const yesterday = today.clone().subtract(1, "days");
const tomorrow = today.clone().add(1, "days");
console.log(today.format(), yesterday.format(), tomorrow.format());
const conf = confirm(対象の日付は ${today.format("YYYY_MM/DD")} ですか?);
if (!conf) return;
insertText({
text: ${today.format("YYYY_MM/DD")}\n<- [${tomorrow.format("YYYY_MM/DD")}] / [${today.format("YYYY_MM")}] / [${yesterday.format("YYYY_MM/DD")}] ->\n\n\n\n<- [${tomorrow.format("YYYY_MM/DD")}] / [${today.format("YYYY_MM")}] / [${yesterday.format("YYYY_MM/DD")}] ->,
});
},
});
code:quote.js
scrapbox.PopupMenu.addButton({
title: 'quote',
onClick: text => text.split(/\n/).map(line => > ${line}).join('\n')
})
code:caption.js
scrapbox.PopupMenu.addButton({
title: 'caption',
onClick: text => {
return text.split(/\n/).map(line => |${line}).join('\n');
}
});
code:alias.js
scrapbox.PopupMenu.addButton({
title: 'alias',
onClick: text => text.split(/\n/).map(line => ^${line}).join('\n')
})
code:a.js
scrapbox.PopupMenu.addButton({
title: 'Gyazo Raw',
onClick: (text) => {
const gyazoRegex = /(https:\/\/gyazo\.com\/a-f0-9{32})/g;
if (text.endsWith('/raw.png') || text.endsWith('/raw.png]')) {
return text;
}
const newText = text.replace(gyazoRegex, '$1/raw.png');
return newText;
}
});
code:year-input-customization.js
/*{
"name": "年代・カテゴリメタ追加UI",
"description": "Scrapboxページメニューからカテゴリ付き年代情報を挿入するUI(2段階式)",
"version": "2.3"
}*/
import { insertText } from "/api/code/customize/scrapbox-insert-text/script.js";
(function () {
"use strict";
const style = document.createElement("style");
style.textContent = `
.popup-ui {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
color: #1B1B1B;
padding: 20px;
border-radius: 0;
border: 1px solid #D2D5DA;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
z-index: 9999;
font-size: 13px;
font-family: 'Inter', 'Noto Sans JP', sans-serif;
font-weight: 400;
letter-spacing: .03em;
display: flex;
flex-direction: column;
gap: 0;
}
.popup-ui.category-select {
width: 220px;
padding: 0;
}
.popup-ui.detail-input {
width: 240px;
}
.popup-ui label {
font-size: 11px;
color: #1B1B1B;
font-weight: 400;
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 12px;
}
.popup-ui .checkbox-label {
flex-direction: row;
align-items: center;
gap: 6px;
cursor: pointer;
}
.popup-ui .checkbox-label:hover span,
.popup-ui .checkbox-label:has(input:focus) span {
text-decoration: underline;
}
.popup-ui .checkbox-label inputtype="checkbox" {
width: auto;
margin: 0;
cursor: pointer;
}
.popup-ui .checkbox-label inputtype="checkbox":focus {
outline: none;
}
.popup-ui input, .popup-ui select {
background: #fff;
color: #1B1B1B;
border: 1px solid #D2D5DA;
border-radius: 0;
padding: 6px 8px;
width: 100%;
font-size: 13px;
font-family: 'Inter', 'Noto Sans JP', sans-serif;
font-weight: 400;
letter-spacing: .03em;
}
.popup-ui input:focus, .popup-ui select:focus {
outline: none;
border-color: #1F75BC;
}
.popup-ui button {
background: transparent;
color: #1B1B1B;
border: none;
border-radius: 0;
padding: 8px 12px;
cursor: pointer;
font-size: 11px;
font-family: 'Inter', 'Noto Sans JP', sans-serif;
font-weight: 400;
letter-spacing: .03em;
text-transform: uppercase;
text-decoration: none;
}
.popup-ui button:hover {
text-decoration: underline;
}
.category-btn {
background: transparent;
color: #1B1B1B;
border: none;
border-bottom: 1px solid #D2D5DA;
border-radius: 0;
padding: 14px 20px;
cursor: pointer;
text-align: left;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
font-weight: 400;
text-decoration: none;
}
.category-btn:last-of-type {
border-bottom: none;
}
.category-btn:hover {
text-decoration: underline;
}
.category-btn .key-hint {
background: transparent;
color: #a0a0a0;
padding: 0;
border-radius: 0;
font-size: 11px;
font-family: 'Inter', monospace;
font-weight: 400;
}
.popup-ui .checkbox-label {
flex-direction: row;
align-items: center;
gap: 6px;
cursor: pointer;
}
.popup-ui .checkbox-label:hover span,
.popup-ui .checkbox-label:has(input:focus) span {
text-decoration: underline;
}
.popup-ui .checkbox-label inputtype="checkbox" {
width: auto;
margin: 0;
cursor: pointer;
}
.popup-ui .checkbox-label inputtype="checkbox":focus {
outline: 1px solid #1F75BC;
outline-offset: 2px;
}
.shortcut-hint {
font-size: 11px;
font-weight: 400;
color: #a0a0a0;
text-align: center;
padding: 14px 20px;
border-top: 1px solid #D2D5DA;
}
`;
document.head.appendChild(style);
// Scrapboxページメニューに登録
scrapbox.PageMenu.addMenu({
title: "+年代/カテゴリ",
image: "https://cdnjs.cloudflare.com/ajax/libs/twemoji/12.0.4/svg/1f4c5.svg",
onClick: showCategorySelect,
});
// Ctrl+Y でも起動可能
document.addEventListener("keydown", (e) => {
if (e.ctrlKey && e.key.toLowerCase() === "y") {
e.preventDefault();
showCategorySelect();
}
});
function showCategorySelect() {
if (document.querySelector(".popup-ui")) return; // 二重防止
const ui = document.createElement("div");
ui.className = "popup-ui category-select";
ui.innerHTML = `
<button class="category-btn" data-category="anime">
<span>アニメ</span>
<span class="key-hint">1</span>
</button>
<button class="category-btn" data-category="game">
<span>ゲーム</span>
<span class="key-hint">2</span>
</button>
<button class="category-btn" data-category="movie">
<span>映画</span>
<span class="key-hint">3</span>
</button>
<button class="category-btn" data-category="person">
<span>人物</span>
<span class="key-hint">4</span>
</button>
<button class="category-btn" data-category="other">
<span>その他</span>
<span class="key-hint">5</span>
</button>
<div class="shortcut-hint">数字キーまたはクリックで選択 / ESC: キャンセル</div>
`;
document.body.appendChild(ui);
// ボタンクリックでカテゴリ選択
ui.querySelectorAll(".category-btn").forEach((btn) => {
btn.addEventListener("click", () => {
const category = btn.dataset.category;
ui.remove();
showDetailInput(category);
});
});
// キーボードショートカット
ui.addEventListener("keydown", (e) => {
if (e.key >= "1" && e.key <= "5") {
e.preventDefault();
const categories = "anime", "game", "movie", "person", "other";
const category = categoriesparseInt(e.key) - 1;
ui.remove();
showDetailInput(category);
}
if (e.key === "Escape") {
e.preventDefault();
ui.remove();
}
});
// フォーカスしてキー入力を受け付ける
ui.setAttribute("tabindex", "0");
ui.focus();
}
function showDetailInput(selectedCategory) {
const ui = document.createElement("div");
ui.className = "popup-ui detail-input";
const categoryNames = {
anime: "アニメ",
game: "ゲーム",
movie: "映画",
person: "人物",
other: "その他"
};
const showDecadeOption = selectedCategory !== "anime" && selectedCategory !== "game" && selectedCategory !== "person";
ui.innerHTML = `
<label for="category">カテゴリ:
<select id="category">
<option value="anime">アニメ</option>
<option value="game">ゲーム</option>
<option value="movie">映画</option>
<option value="person">人物</option>
<option value="other">その他</option>
</select>
</label>
<label for="tagType">入力形式:
<select id="tagType">
<option value="hashtag">#ハッシュタグ</option>
<option value="link">リンク</option>
</select>
</label>
<label id="customCategoryLabel" style="display:none;">カテゴリ名:
<input id="customCategory" type="text" placeholder="例: 書籍">
</label>
<label id="yearLabel">年(例: 2025):
<input id="year" type="text" placeholder="2025">
</label>
<label id="deathLabel" style="display:none;">没年(例: 1988):
<input id="deathYear" type="text" placeholder="空白可">
</label>
<label id="decadeLabel" class="checkbox-label" style="display:${showDecadeOption ? 'flex' : 'none'};">
<input id="includeDecade" type="checkbox">
<span>年代タグを追加</span>
</label>
<div style="display:flex; gap:8px; justify-content:flex-end;">
<button id="okBtn">OK</button>
<button id="cancelBtn">キャンセル</button>
</div>
`;
document.body.appendChild(ui);
const category = ui.querySelector("#category");
const tagType = ui.querySelector("#tagType");
const yearInput = ui.querySelector("#year");
const deathYearInput = ui.querySelector("#deathYear");
const customCategoryInput = ui.querySelector("#customCategory");
const includeDecadeCheckbox = ui.querySelector("#includeDecade");
const okBtn = ui.querySelector("#okBtn");
const cancelBtn = ui.querySelector("#cancelBtn");
const deathLabel = ui.querySelector("#deathLabel");
const customCategoryLabel = ui.querySelector("#customCategoryLabel");
const decadeLabel = ui.querySelector("#decadeLabel");
const yearLabel = ui.querySelector("#yearLabel");
// 選択されたカテゴリを設定
category.value = selectedCategory;
// カテゴリに応じた初期設定
function updateUI() {
const isPerson = category.value === "person";
const isAnime = category.value === "anime";
const isGame = category.value === "game";
const isOther = category.value === "other";
deathLabel.style.display = isPerson ? "block" : "none";
customCategoryLabel.style.display = isOther ? "block" : "none";
decadeLabel.style.display = (isAnime || isGame || isPerson) ? "none" : "flex";
if (isAnime || isGame) {
tagType.value = "hashtag";
} else {
tagType.value = "link";
}
if (isPerson) {
yearLabel.childNodes0.textContent = "生年(例: 1952):";
} else {
yearLabel.childNodes0.textContent = "年(例: 2025):";
}
}
updateUI();
// カテゴリ変更時の処理
category.addEventListener("change", updateUI);
// キーボードショートカット(入力欄内のみ)
ui.addEventListener("keydown", (e) => {
if (e.key === "Enter" && e.target.tagName === "INPUT") {
e.preventDefault();
apply();
}
if (e.key === "Escape") {
e.preventDefault();
closeUI();
}
if (e.key === "Tab") {
e.preventDefault();
const focusableElements = category, tagType;
// その他の場合はカスタムカテゴリ入力を年の前に追加
if (category.value === "other") {
focusableElements.push(customCategoryInput);
}
focusableElements.push(yearInput);
// 人物の場合は没年入力も追加
if (category.value === "person") {
focusableElements.push(deathYearInput);
}
// 年代チェックボックスが表示されている場合は追加
if (decadeLabel.style.display !== "none") {
focusableElements.push(includeDecadeCheckbox);
}
// OKとキャンセルボタンはループに含めない
const currentElement = document.activeElement;
// ボタンからTabした場合は最初の要素へ
if (currentElement === okBtn || currentElement === cancelBtn) {
focusableElements0.focus();
} else {
const currentIndex = focusableElements.indexOf(currentElement);
const nextIndex = (currentIndex + 1) % focusableElements.length;
focusableElementsnextIndex.focus();
}
}
// セレクトボックスでの矢印キーループ
if ((e.key === "ArrowDown" || e.key === "ArrowUp") && e.target.tagName === "SELECT") {
e.preventDefault();
const select = e.target;
const options = Array.from(select.options);
const currentIndex = select.selectedIndex;
let newIndex;
if (e.key === "ArrowDown") {
newIndex = (currentIndex + 1) % options.length;
} else {
newIndex = (currentIndex - 1 + options.length) % options.length;
}
select.selectedIndex = newIndex;
select.dispatchEvent(new Event("change"));
}
// チェックボックスのスペースキー切り替え
if (e.key === " " && e.target.type === "checkbox") {
e.preventDefault();
e.target.checked = !e.target.checked;
}
});
cancelBtn.onclick = closeUI;
okBtn.onclick = apply;
// 初期フォーカス: その他の場合はカスタムカテゴリ、それ以外は年入力欄
if (selectedCategory === "other") {
customCategoryInput.focus();
} else {
yearInput.focus();
}
function closeUI() {
ui.remove();
}
function apply() {
const year = yearInput.value.trim();
const deathYear = deathYearInput.value.trim();
const categoryVal = category.value;
const tagForm = tagType.value;
const customCat = customCategoryInput.value.trim();
const includeDecade = includeDecadeCheckbox ? includeDecadeCheckbox.checked : false;
if (!year) return closeUI();
let textToInsert = "";
let hashtagToInsert = "";
if (categoryVal === "person") {
// 世紀と時期を計算
const birthYear = parseInt(year);
const century = Math.ceil(birthYear / 100);
let period;
const yearInCentury = ((birthYear - 1) % 100) + 1;
if (yearInCentury <= 33) {
period = "初頭";
} else if (yearInCentury <= 66) {
period = "半ば";
} else {
period = "末期";
}
const centuryTag = ${century}世紀${period}-人物;
const birthLine = 生年: ${year}年;
const deathLine = deathYear ? / 没年: ${deathYear}年 : / 没年: ;
textToInsert = ${birthLine}${deathLine};
hashtagToInsert = [${centuryTag}];
} else if (categoryVal === "anime") {
const decade = Math.floor(year / 10) * 10;
const yearTag = ${year}年-アニメ;
const decadeTag = ${decade}年代-アニメ;
textToInsert =
tagForm === "hashtag"
? #${yearTag} #${decadeTag}
: [${yearTag}] [${decadeTag}];
} else if (categoryVal === "game") {
const decade = Math.floor(year / 10) * 10;
const yearTag = ${year}年-ゲーム;
const decadeTag = ${decade}年代-ゲーム;
textToInsert =
tagForm === "hashtag"
? #${yearTag} #${decadeTag}
: [${yearTag}] [${decadeTag}];
} else if (categoryVal === "movie") {
const yearTag = ${year}年-映画;
const decadeTag = includeDecade ? ${Math.floor(year / 10) * 10}年代-映画 : null;
if (tagForm === "hashtag") {
textToInsert = decadeTag ? #${yearTag} #${decadeTag} : #${yearTag};
} else {
textToInsert = decadeTag ? [${yearTag}] [${decadeTag}] : [${yearTag}];
}
} else if (categoryVal === "other") {
if (customCat) {
const yearTag = ${year}年-${customCat};
const decadeTag = includeDecade ? ${Math.floor(year / 10) * 10}年代-${customCat} : null;
if (tagForm === "hashtag") {
textToInsert = decadeTag ? #${yearTag} #${decadeTag} : #${yearTag};
} else {
textToInsert = decadeTag ? [${yearTag}] [${decadeTag}] : [${yearTag}];
}
} else {
textToInsert = tagForm === "hashtag" ? #${year}年 : [${year}年];
}
} else {
textToInsert = tagForm === "hashtag" ? #${year}年 : [${year}年];
}
// 人物の場合はページ最下部にハッシュタグを追加
if (hashtagToInsert) {
// 現在のページの全行を取得
const lines = scrapbox.Page.lines || [];
const lastLine = lineslines.length - 1;
// 最終行が空でない場合は改行を追加
const finalText = lastLine && lastLine.text.trim() !== ""
? ${textToInsert}\n\n${hashtagToInsert}
: ${textToInsert}\n${hashtagToInsert};
insertText({ text: finalText });
} else {
insertText({ text: textToInsert });
}
closeUI();
}
}
})();
code:scrapbox-url-customizer.js
function ce(e){return Array.isArray(e)}function M(e){return typeof e=="string"}var De=(e,r)=>{if(!(e instanceof HTMLDivElement))throw new TypeError("${r}" must be HTMLDivElememt but actual is "${e}")};var Ve=(e,r)=>{if(!(e instanceof HTMLTextAreaElement))throw new TypeError("${r}" must be HTMLTextAreaElement but actual is "${e}")};var q=()=>{let e=document.getElementById("text-input");if(e)return Ve(e,"textarea#text-input"),e};var We=()=>Xt(document.getElementsByClassName("status-bar")?.0,"div.status-bar"),Xt=(e,r)=>{if(e)return De(e,r),e};var Io=2**31-1;var qe=e=>{let r=q();if(!r)throw Error("#text-input is not ditected.");r.focus(),r.value=e;let t=new InputEvent("input",{bubbles:!0});return r.dispatchEvent(t),scrapbox.Page.waitForSave()};var Ke=()=>{let e=We();if(!e)throw new Error("div.status-bar can't be found");let r=document.createElement("div");return e.append(r),{render:(...t)=>{r.textContent="";let o=ze(...t);o&&r.append(o)},dispose:()=>r.remove(),Symbol.dispose:()=>r.remove()}},ze=(...e)=>{let r=e.flatMap(o=>{switch(o.type){case"spinner":returnor();case"check-circle":returnnr();case"exclamation-triangle":returnsr();case"text":returnpe(o.text);case"group":{let n=ze(...o.items);return n?n:[]}}});if(r.length===0)return;if(r.length===1)return r0;let t=document.createElement("span");return t.classList.add("item-group"),t.append(...r),t},pe=e=>{let r=document.createElement("span");return r.classList.add("item"),r.append(e),r},or=()=>{let e=document.createElement("i");return e.classList.add("fa","fa-spinner"),pe(e)},nr=()=>{let e=document.createElement("i");return e.classList.add("kamon","kamon-check-circle"),pe(e)},sr=()=>{let e=document.createElement("i");return e.classList.add("fas","fa-exclamation-triangle"),pe(e)};var Ae=(e,...r)=>{let t=r.reduce((o,n)=>M(o)?o:o instanceof Promise?o.then(s=>M(s)?s:n(s)):n(o),e);return t instanceof Promise?t.then(o=>${o}):${e}};function N(e){return e.val}function S(e){return e.err}var Ye=" must not return ",ir="transformer",cr="recoverer",pr="defaultValue",ue=ir+Ye,V="called with ",me=pr+" must not be ",le=cr+Ye;var ur="Ok",Xe="Err",Je=V+Xe,Qe=V+ur,mr="Carrying E in "+Xe+" instead of throwing it directly. See .cause",Me="an instance of Error of the current realm.",ls="The thrown value is not "+Me,fs="The contained E should be "+Me,lr="This .cause is not "+Me;function w(e){return e.ok}function L(e){return{ok:!0,val:e,err:null}}function E(e){return!e.ok}function P(e){return{ok:!1,val:null,err:e}}function I(e){return Ze(e,Je)}function v(e){return et(e,Qe)}function Ze(e,r){if(E(e))throw new TypeError(r);return e.val}function et(e,r){if(w(e))throw new TypeError(r);return e.err}async function W(e,r){if(E(e))return e;let t=N(e),o=await r(t);return L(o)}function Pe(e,r,t){if(w(e)){let n=N(e);return t(n)}let o=S(e);return r(o)}var fe="null",ha=ue+fe,Er=V+fe,Ta=me+fe,xa=le+fe;var de="undefined",Na=ue+de,Rr=V+de,La=me+de,Ua=le+de;var K=e=>e.ok?L(e):P({name:"HTTPError",message:${e.status} ${e.statusText},response:e});var rt=e=>window.GM_fetch?.(https://cdn.syndication.twimg.com/tweet-result?id=${e}&token=x)?.then?.(r=>W(K(r),t=>t.json()));var Ee=e=>{let{fetch:r=globalThis.fetch,...t}=e;return{fetch:r,...t}},ot=e=>typeof e=="object"&&e!==null;var Q=class e extends Error{name="UnexpectedResponseError";status;statusText;body;path;constructor(r){super(${r.status} ${r.statusText} when fetching ${r.path.toString()}),this.status=r.status,this.statusText=r.statusText,this.body=r.body,this.path=r.path,Error.captureStackTrace&&Error.captureStackTrace(this,e)}};var Re=async e=>{let r=await e.text();if(e.ok)return L(r);if(e.status===400)return P({name:"BadRequestError",message:r});try{let t=JSON.parse(r);if(!ot(t)||typeof t.message!="string")throw new Q({status:e.status,statusText:e.statusText,body:r,path:new URL(e.url)});switch(e.status){case 401:return P({name:"UnauthorizedError",message:t.message});case 403:return P({name:"NotPrivilegeError",message:t.message});case 404:return P({name:"NotFoundError",message:t.message});case 422:return P({name:"InvalidParameterError",message:t.message});case 429:return P({name:"RateLimitError",message:t.message});default:throw new Q({status:e.status,statusText:e.statusText,body:r,path:new URL(e.url)})}}catch(t){throw t instanceof SyntaxError?new Q({status:e.status,statusText:e.statusText,body:r,path:new URL(e.url)}):t}};var nt=async(e,r)=>{let{title:t,description:o,metadataIsPublic:n,collectionId:s,refererURL:a,accessToken:p,created:i,app:f,fetch:l}=Ee(r),u=new FormData;u.append("imagedata",e),u.append("access_token",p),a&&u.append("referer_url",a.toString()),f!==void 0&&u.append("app",f),t!==void 0&&u.append("title",t),o!=null&&u.append("desc",o),s&&u.append("collection_id",s),n&&u.append("metadata_is_public","true"),i!==void 0&&u.append("created_at",${i});let A=await l("https://upload.gyazo.com/api/upload",{method:"POST",mode:"cors",credentials:"omit",body:u}),C=await Re(A);return E(C)?C:L(JSON.parse(I(C)))};function O(e){return e.val}function U(e){return e.err}var st=" must not return ",gr="transformer",hr="recoverer",Tr="defaultValue",z=gr+st,H="called with ",Y=Tr+" must not be ",X=hr+st;var xr="Ok",at="Err",it=H+at,_r=H+xr,wr="Carrying E in "+at+" instead of throwing it directly. See .cause",Ie="an instance of Error of the current realm.",mp="The thrown value is not "+Ie,lp="The contained E should be "+Ie,Or="This .cause is not "+Ie;function _(e){return e.ok}function R(e){return{ok:!0,val:e,err:null}}function m(e){return!e.ok}function b(e){return{ok:!1,val:null,err:e}}function d(e){return ct(e,it)}function ct(e,r){if(m(e))throw new TypeError(r);return e.val}function Z(e,r){if(m(e))return e;let t=O(e),o=r(t);return R(o)}async function g(e,r){if(m(e))return e;let t=O(e),o=await r(t);return R(o)}async function x(e,r){if(_(e))return e;let t=U(e),o=await r(t);return b(o)}async function ye(e,r){if(_(e))return e;let t=U(e);return await r(t)}var ge="null",Ou=z+ge,Lr=H+ge,bu=Y+ge,Nu=X+ge;var he="undefined",Au=z+he,Ur=H+he,Mu=Y+he,Pu=X+he;var h=e=>e.ok?R(e):b({name:"HTTPError",message:${e.status} ${e.statusText},response:e});var ut=async(e,r)=>{let t=new Request(e,r);try{return R(await globalThis.fetch(t))}catch(o){if(o instanceof DOMException&&o.name==="AbortError")return b({name:"AbortError",message:o.message,request:t});if(o instanceof TypeError)return b({name:"NetworkError",message:o.message,request:t});throw o}};var y=e=>{let{fetch:r=ut,hostName:t="scrapbox.io",...o}=e;return{fetch:r,hostName:t,...o}};var mt=e=>{let{sid:r,hostName:t}=y(e??{});return new Request(https://${t}/api/users/me,r?{headers:{Cookie:T(r)}}:void 0)},lt=e=>g(h(e),async r=>await r.json()),ft=(()=>{let e=async r=>{let{fetch:t,...o}=y(r??{}),n=await t(mt(o));return m(n)?n:lt(d(n))};return e.toRequest=mt,e.fromResponse=lt,e})();var T=e=>connect.sid=${e},G=async e=>{let r=e?.csrf??globalThis._csrf;return r?R(r):Z(await ft(e),t=>t.csrfToken)};function Te(e,r={}){if(e===null)return"null";if(Array.isArray(e))return Ar(e,r);switch(typeof e){case"string":return JSON.stringify(e);case"bigint":return${e}n;case"object":return e.constructor?.name!=="Object"?e.constructor?.name:Mr(e,r);case"function":return e.name||"(anonymous)"}return e?.toString()??"undefined"}function Ar(e,r){let{threshold:t=20}=r,o=e.map(a=>Te(a,r)),n=o.join(", ");if(n.length<=t)return[${n}];let s=o.join(`,
);return[
${dt(2,s)}
]}function Mr(e,r){let{threshold:t=20}=r,o=[...Object.keys(e),...Object.getOwnPropertySymbols(e)].map(a=>${a.toString()}: ${Te(ea,r)}),n=o.join(", ");if(n.length<=t)return{${n}};let s=o.join(,
);return{
${dt(2,s)}
}}function dt(e,r){let t=" ".repeat(e);return r.split(
).map(o=>${t}${o}).join(
)}function xe(e,r,...t){let o;return Object.defineProperties(e,{name:{get:()=>o||(o=${r}(${t.map(n=>Te(n)).join(", ")}),o)}})}function Et(e){return xe(r=>ce(r)&&r.every(t=>e(t)),"isArrayOf",e)}function Se(e){let r=new Set(e);return xe(t=>r.has(t),"isLiteralOneOf",e)}function Ce(e){return e!=null&&!Array.isArray(e)&&typeof e=="object"}var F=async(e,r)=>{let t=e.response.clone(),o=Se(r);try{let n=await t.json();if(!Ce(n))return;if(t.status===422){if(!M(n.message))return;for(let s of["NoQueryError","InvalidURLError"])if(r.includes(s))return{name:s,message:n.message}}return!o(n.name)||!M(n.message)?void 0:n.name==="NotLoggedInError"?!Ce(n.detals)||!M(n.detals.project)||!Et(Pr)(n.detals.loginStrategies)?void 0:{name:n.name,message:n.message,details:{project:n.detals.project,loginStrategies:n.detals.loginStrategies}}:{name:n.name,message:n.message}}catch(n){if(n instanceof SyntaxError)return;throw n}},Pr=Se(["google","github","microsoft","gyazo","email","saml","easy-trial"]);var _e="null or undefined",J=z+_e,Ir=H+_e,ve=Y+_e,we=X+_e;function B(e){return e==null}function Oe(e){return B(e)?b(void 0):R(e)}var Rt=(e,r)=>{let{sid:t,hostName:o}=y(r??{});return new Request(https://${o}/api/projects/${e},t?{headers:{Cookie:T(t)}}:void 0)},yt=async e=>g(await x(h(e),async r=>await F(r,["NotFoundError","NotLoggedInError","NotMemberError"])??r),r=>r.json()),gt=(()=>{let e=async(r,t)=>{let{fetch:o}=y(t??{}),n=Rt(r,t),s=await o(n);return m(s)?s:yt(d(s))};return e.toRequest=Rt,e.fromResponse=yt,e})();var ht=async(e,r)=>{let{sid:t,hostName:o,fetch:n}=y(r??{}),s=await G(r);if(m(s))return s;let a=new Request(https://${o}/api/embed-text/url?url=${encodeURIComponent(${e})},{method:"POST",headers:{"Content-Type":"application/json;charset=utf-8","X-CSRF-TOKEN":d(s),...t?{Cookie:T(t)}:{}},body:JSON.stringify({timeout:3e3})}),p=await n(a);return m(p)?p:g(await x(h(d(p)),async i=>await F(i,["SessionError","BadRequestError","InvalidURLError"])??i),async i=>{let{title:f}=await i.json();return f})};var Tt=async(e,r)=>{let{sid:t,hostName:o,fetch:n}=y(r??{}),s=await G(r);if(m(s))return s;let a=new Request(https://${o}/api/embed-text/twitter?url=${encodeURIComponent(${e})},{method:"POST",headers:{"Content-Type":"application/json;charset=utf-8","X-CSRF-TOKEN":d(s),...t?{Cookie:T(t)}:{}},body:JSON.stringify({timeout:3e3})}),p=await n(a);return m(p)?p:x(await g(h(d(p)),i=>i.json()),async i=>i.response.status===422?{name:"InvalidURLError",message:(await i.response.json()).message}:await F(i,["SessionError","BadRequestError"])??i)};var xt=async e=>{let{fetch:r,sid:t,hostName:o,gyazoTeamsName:n}=y(e??{}),s=new Request(https://${o}/api/login/gyazo/oauth-upload/token${n??gyazoTeamsName=${n}:""},t?{headers:{Cookie:T(t)}}:void 0),a=await r(s);return m(a)?a:g(await x(h(d(a)),async p=>await F(p,["NotLoggedInError"])??p),p=>p.json().then(i=>i.token))};var Ot=e=>{let r=typeof e=="string"?new TextEncoder().encode(e):ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),t=[1732584193,4023233417,2562383102,271733878],o=new Uint8Array(Be),n=0,s=0,a=0;[t,o,n,s,a]=wt(t,o,n,s,a,r);let p=Be-n;p<9&&(p+=Be);let i=new Uint8Array(p);i[0]=128,[s,a]=[s<<3,a<<3|s>>>29],i[i.length-8]=s&255,i[i.length-7]=s>>>8&255,i[i.length-6]=s>>>16&255,i[i.length-5]=s>>>24&255,i[i.length-4]=a&255,i[i.length-3]=a>>>8&255,i[i.length-2]=a>>>16&255,i[i.length-1]=a>>>24&255,[t,o,n,s,a]=wt(t,o,n,s,a,new Uint8Array(i.buffer));let f=new ArrayBuffer(16),l=new DataView(f);return l.setUint32(0,t[0],!0),l.setUint32(4,t[1],!0),l.setUint32(8,t[2],!0),l.setUint32(12,t[3],!0),f},Be=64,c=(e,r)=>e<<r|e>>>32-r,k=(e,r)=>e[r]|e[r+1]<<8|e[r+2]<<16|e[r+3]<<24,_t=(e,r)=>{let[t,o,n,s]=e,a=k(r,0),p=k(r,4),i=k(r,8),f=k(r,12),l=k(r,16),u=k(r,20),A=k(r,24),C=k(r,28),D=k(r,32),te=k(r,36),re=k(r,40),oe=k(r,44),ne=k(r,48),se=k(r,52),ae=k(r,56),ie=k(r,60);return t=o+c(((n^s)&o^s)+t+a+3614090360,7),s=t+c(((o^n)&t^n)+s+p+3905402710,12),n=s+c(((t^o)&s^o)+n+i+606105819,17),o=n+c(((s^t)&n^t)+o+f+3250441966,22),t=o+c(((n^s)&o^s)+t+l+4118548399,7),s=t+c(((o^n)&t^n)+s+u+1200080426,12),n=s+c(((t^o)&s^o)+n+A+2821735955,17),o=n+c(((s^t)&n^t)+o+C+4249261313,22),t=o+c(((n^s)&o^s)+t+D+1770035416,7),s=t+c(((o^n)&t^n)+s+te+2336552879,12),n=s+c(((t^o)&s^o)+n+re+4294925233,17),o=n+c(((s^t)&n^t)+o+oe+2304563134,22),t=o+c(((n^s)&o^s)+t+ne+1804603682,7),s=t+c(((o^n)&t^n)+s+se+4254626195,12),n=s+c(((t^o)&s^o)+n+ae+2792965006,17),o=n+c(((s^t)&n^t)+o+ie+1236535329,22),t=o+c(((o^n)&s^n)+t+p+4129170786,5),s=t+c(((t^o)&n^o)+s+A+3225465664,9),n=s+c(((s^t)&o^t)+n+oe+643717713,14),o=n+c(((n^s)&t^s)+o+a+3921069994,20),t=o+c(((o^n)&s^n)+t+u+3593408605,5),s=t+c(((t^o)&n^o)+s+re+38016083,9),n=s+c(((s^t)&o^t)+n+ie+3634488961,14),o=n+c(((n^s)&t^s)+o+l+3889429448,20),t=o+c(((o^n)&s^n)+t+te+568446438,5),s=t+c(((t^o)&n^o)+s+ae+3275163606,9),n=s+c(((s^t)&o^t)+n+f+4107603335,14),o=n+c(((n^s)&t^s)+o+D+1163531501,20),t=o+c(((o^n)&s^n)+t+se+2850285829,5),s=t+c(((t^o)&n^o)+s+i+4243563512,9),n=s+c(((s^t)&o^t)+n+C+1735328473,14),o=n+c(((n^s)&t^s)+o+ne+2368359562,20),t=o+c((o^n^s)+t+u+4294588738,4),s=t+c((t^o^n)+s+D+2272392833,11),n=s+c((s^t^o)+n+oe+1839030562,16),o=n+c((n^s^t)+o+ae+4259657740,23),t=o+c((o^n^s)+t+p+2763975236,4),s=t+c((t^o^n)+s+l+1272893353,11),n=s+c((s^t^o)+n+C+4139469664,16),o=n+c((n^s^t)+o+re+3200236656,23),t=o+c((o^n^s)+t+se+681279174,4),s=t+c((t^o^n)+s+a+3936430074,11),n=s+c((s^t^o)+n+f+3572445317,16),o=n+c((n^s^t)+o+A+76029189,23),t=o+c((o^n^s)+t+te+3654602809,4),s=t+c((t^o^n)+s+ne+3873151461,11),n=s+c((s^t^o)+n+ie+530742520,16),o=n+c((n^s^t)+o+i+3299628645,23),t=o+c((n^(o|~s))+t+a+4096336452,6),s=t+c((o^(t|~n))+s+C+1126891415,10),n=s+c((t^(s|~o))+n+ae+2878612391,15),o=n+c((s^(n|~t))+o+u+4237533241,21),t=o+c((n^(o|~s))+t+ne+1700485571,6),s=t+c((o^(t|~n))+s+f+2399980690,10),n=s+c((t^(s|~o))+n+re+4293915773,15),o=n+c((s^(n|~t))+o+p+2240044497,21),t=o+c((n^(o|~s))+t+D+1873313359,6),s=t+c((o^(t|~n))+s+ie+4264355552,10),n=s+c((t^(s|~o))+n+A+2734768916,15),o=n+c((s^(n|~t))+o+se+1309151649,21),t=o+c((n^(o|~s))+t+l+4149444226,6),s=t+c((o^(t|~n))+s+oe+3174756917,10),n=s+c((t^(s|~o))+n+i+718787259,15),o=n+c((s^(n|~t))+o+te+3951481745,21),[e[0]+t>>>0,e[1]+o>>>0,e[2]+n>>>0,e[3]+s>>>0]},wt=(e,r,t,o,n,s)=>{let a=64-t;if(s.length<a)r.set(s,t),t+=s.length;else{r.set(s.slice(0,a),t),e=_t(e,r);let p=a;for(;p+64<=s.length;)e=_t(e,s.slice(p,p+64)),p+=64;r.fill(0).set(s.slice(p),0),t=s.length-p}return[o,n]=Cr(o,n,s.length),[e,r,t,o,n]},Cr=(e,r,t)=>(e+=t,e>4294967295&&(r+=1),[e>>>0,r]);var vr=new TextEncoder().encode("0123456789abcdef"),bt=new Uint8Array(128).fill(16);vr.forEach((e,r)=>bt[e]=r);new TextEncoder().encode("ABCDEF").forEach((e,r)=>bt[e]=r+10);function Nt(e){return e*2}function Lt(e,r,t,o){for(;r<e.length;++r){let n=e[r];e[t++]=o[n>>4],e[t++]=o[n&15]}return t}function Ut(e,r){let t=e.length;if(e.byteOffset){let o=new Uint8Array(e.buffer);o.set(e),e=o.subarray(0,t)}return e=new Uint8Array(e.buffer.transfer(r)),e.set(e.subarray(0,t),r-t),[e,r-t]}var Ft=new TextEncoder().encode("0123456789abcdef"),kt=new Uint8Array(128).fill(16);Ft.forEach((e,r)=>kt[e]=r);new TextEncoder().encode("ABCDEF").forEach((e,r)=>kt[e]=r+10);function At(e){typeof e=="string"?e=new TextEncoder().encode(e):e instanceof ArrayBuffer?e=new Uint8Array(e).slice():e=e.slice();let[r,t]=Ut(e,Nt(e.length));return Lt(r,t,0,Ft),new TextDecoder().decode(r)}var Mt=async(e,r,t)=>{let o=${At(Ot(await e.arrayBuffer()))},n=await Br(e,r,o,t);if(m(n))return n;let s=d(n);if("embedUrl"in s)return R(s);let a=await Hr(s.signedUrl,e,t);return m(a)?a:Gr(r,s.fileId,o,t)},Br=async(e,r,t,o)=>{let{sid:n,hostName:s,fetch:a,csrf:p}=y(o??{}),i={md5:t,size:e.size,contentType:e.type,name:e.name},f=await ye(Oe(p),()=>G(o));if(m(f))return f;let l=new Request(https://${s}/api/gcs/${r}/upload-request,{method:"POST",body:JSON.stringify(i),headers:{"Content-Type":"application/json;charset=utf-8","X-CSRF-TOKEN":d(f),...n?{Cookie:T(n)}:{}}}),u=await a(l);return m(u)?u:g(await x(h(d(u)),async A=>A.response.status===402?{name:"FileCapacityError",message:(await A.response.json()).message}:A),A=>A.json())},Hr=async(e,r,t)=>{let{sid:o,fetch:n}=y(t??{}),s=await n(e,{method:"PUT",body:r,headers:{"Content-Type":r.type,...o?{Cookie:T(o)}:{}}});return m(s)?s:Z(await x(h(d(s)),async a=>a.response.headers.get("Content-Type")?.includes?.("/xml")?{name:"GCSError",message:await a.response.text()}:a),()=>{})},Gr=async(e,r,t,o)=>{let{sid:n,hostName:s,fetch:a,csrf:p}=y(o??{}),i=await ye(Oe(p),()=>G(o));if(m(i))return i;let f=new Request(https://${s}/api/gcs/${e}/verify,{method:"POST",body:JSON.stringify({md5:t,fileId:r}),headers:{"Content-Type":"application/json;charset=utf-8","X-CSRF-TOKEN":d(i),...n?{Cookie:T(n)}:{}}}),l=await a(f);return m(l)?l:g(await x(h(d(l)),async u=>u.response.status===404?{name:"NotFoundError",message:(await u.response.json()).message}:u),u=>u.json())};var Pt=(e,r)=>{let t=new FormData;return t.append("data",e),t.append("metadata",JSON.stringify({app:"Gyazo",title:e.name})),GM_fetch(https://gif.gyazo.com/${r?.teams?"teams":"gif"}/upload,{method:"POST",body:t,credentials:"include",headers:{Origin:"https://gyazo.com","sec-fetch-site":"same-site"},referrer:"https://gyazo.com/"})};var be="",It=!1,Ne=new Map,St=async(e,r,t,o)=>{let n=Ne.get(e.href);if(n)return n;if(e.hostname==="video.twimg.com"||${e}.endsWith(".svg")){let i=await GM_fetch(e);if(!i.ok)return;let f=i.headers.get("content-type")?.split?.(";")?.[0]??${e}.endsWith(".mp4")?"video/mp4":"video/webm",l=new File([await i.blob()],o||${r},{type:f});if(f==="video/mp4"){let C=await Pt(l);if(C.ok){let D=new URL(await C.text());return Ne.set(e.href,D),D}}let u=await Mt(l,t);if(E(u))throw Error(v(u).name);let A=new URL(I(u).embedUrl);return Ne.set(e.href,A),A}if(e.hostname!=="pbs.twimg.com"||!e.pathname.startsWith("/media"))return;if(It){if(!be)return}else{let i=await xt();if(It=!0,E(i)){alert("You haven't logged in Gyazo yet, so you can only upload images to scrapbox.io.");return}if(be=I(i)||"",!be){alert("You haven't connect Gyazo to scrapbox.io yet.");return}}let s=await GM_fetch(e);if(!s.ok)return;let a=await nt(await s.blob(),{accessToken:be,refererURL:r,description:o});if(E(a))throw Error(v(a).name);let p=new URL(I(a).permalink_url);return Ne.set(e.href,p),p};var Bt=[["&","&amp;"],["<","&lt;"],[">","&gt;"],['"',"&quot;"],["'","&#39;"]],$r=Object.fromEntries([...Bt.map(([e,r])=>[r,e]),["&apos;","'"],["&nbsp;"," "]]),jr=new Map(Bt),zy=new RegExp([${...jr.keys().join("")}],"g");var Dr={entityList:$r},Vr=1114111,Wr=/&#([0-9]+);/g,qr=/&#x(\p{AHex}+);/gu,Ct=new WeakMap;function He(e,r={}){let{entityList:t}={...Dr,...r},o=Ct.get(t);return o||(o=new RegExp((${Object.keys(t).sort((n,s)=>s.length-n.length).join("|")}),"g"),Ct.set(t,o)),e.replaceAll(o,n=>t[n]).replaceAll(Wr,(n,s)=>vt(s,10)).replaceAll(qr,(n,s)=>vt(s,16))}function vt(e,r){let t=parseInt(e,r);return t>Vr?"�":String.fromCodePoint(t)}var Le=e=>{let r={name:e.user.name,screenName:e.user.screen_name},t=new Date(e.created_at),o=[...e.entities.hashtags.map(a=>({type:"hashtag",...a})),...e.entities.symbols.map(a=>({type:"symbol",...a})),...e.entities.user_mentions.map(a=>({type:"mention",name:a.name,screenName:a.screen_name,indices:a.indices})),...e.entities.urls.map(a=>{let p={type:"url",indices:a.indices,url:new URL(a.expanded_url)};if(e.card&&e.card?.url===a.url){let{description:i,title:f}=e.card.binding_values,l="STRING";i?.type===l&&(p.description=i.string_value),f?.type===l&&(p.title=f.string_value)}return p}),...e.entities.media?.map?.(a=>({type:"media",indices:a.indices,media:e.mediaDetails?.flatMap?.(p=>p.url===a.url?[{type:p.type,url:new URL(p.video_info?.variants?.sort?.((i,f)=>(f.bitrate??0)-(i.bitrate??0))?.[0].url??p.media_url_https)}]:[])??[]}))??[]].sort((a,p)=>a.indices[0]-p.indices[0]),n=[];{let a=0,p=e.text;for(let{indices:i,...f}of o){let l=[...p].slice(0,i[0]-a).join("");n.push({type:"plain",text:He(l)}),n.push(f),p=[...p].slice(i[1]-a).join(""),a=i[1]}p&&n.push({type:"plain",text:He(p)})}let s={id:e.id_str,content:n,author:r,posted:t,replyCount:"reply_count"in e?e.reply_count:e.conversation_count};return e.self_thread&&(s.rootId=e.self_thread.id_str),e.in_reply_to_status_id_str&&(s.replyId=e.in_reply_to_status_id_str),e.parent&&(s.replyTo=Le(e.parent)),e.quoted_tweet&&(s.quote=Le(e.quoted_tweet)),s};var Kr=["landing","product","enterprise","pricing","try-enterprise","contact","terms","privacy","jp-commercial-act","support","case","features","business","auth","login","logout","oauth2","_","api","app.html","assets","file","files","billing","billings","config","feed","index","io","new","opensearch","project","projects","search","setting","settings","setup-profile","slide","socket.io","stream","user","users"],Ue=(e=scrapbox.Project.name,r=location.host)=>t=>{if(t.host!==r)return t;let[,o,n]=t.pathname.match(/^\/([\w\d][\w\d-]{0,22}[\w\d])(?:\/?|\/(.+))$/)??[];return!o||Kr.includes(o)?t:n?o===e?${decodeURIComponent(n)}:/${o}/${decodeURIComponent(n)}:/${o}};var Ht=(e=zr)=>r=>{let[,t]=r.href.match(/^https:\/\/(?:www\.|mobile\.|m\.|)(?:twitter|x)\.com\/[A-Za-z0-9_]*\/(?:status|statuses)\/(\d+)/)??[];return t?(async()=>{let o=await(rt(t)??Tt(r.href));if(E(o))throw v(o);let n=I(o);return e("images"in n?{...n,id:t}:n,r)})():r},zr=async e=>{if("images"in e)return ee(e);let{quote:r,replyTo:t,...o}=Le(e);return[...t?[...(await ee(t)).split(
).map(n=> > ${n}),...t.quote?(await ee(t.quote)).split(
).map(n=> > ${n}):[]]:[],...(await ee(o)).split(
).map(n=>> ${n}),...r?(await ee(r)).split(
).map(n=>> > ${n}):[]].join(
)},ee=async e=>{let r=new URL(https://twitter.com/${"author"in e?e.author.screenName:e.screenName}/status/${e.id});if("images"in e)return[> @${$e(e.screenName)} ${r.origin}${r.pathname},...e.description?.split?.(
)?.map?.(n=>> ${$e(n)})??["> [/ no description provided]"],...e.images.length>0?[> ${e.images.map(n=>[${n}])}]:[]].join(
);let t=e.content,o=e.author.screenName;return[@${$e(o)} ${r},...(await Promise.all(t.map(async n=>{switch(n.type){case"plain":return n.text;case"hashtag":return #${n.text} ;case"symbol":return #$${n.text} ;case"mention":return@${n.screenName};case"media":{let s=[],a=1;for(;a<n.media.length;a+=2)s.push([${await Ge(n.mediaa-1,r)}] [${await Ge(n.mediaa,r)}]);return a===n.media.length&&s.push([${await Ge(n.mediaa-1,r)}]),
${s.join(`
`)}
}case"url":return${Ue()(n.url)} }}))).join("").split(
)].join(
)},Fe="",Yr=async()=>{if(Fe)return Fe;let e=await gt(scrapbox.Project.name);if(E(e))throw new Error(v(e).name);return Fe=I(e).id,Fe},Ge=async(e,r)=>await St(e.url,r,await Yr(),"")??e.url,$e=e=>e.replace(/\b/gm,"").replace(/[\s\r\n\u2028\u2029]+/gm," ").replace(/\s*[[\]]\s*/g," ").trim();var Gt=e=>window.GM_fetch?.(https://t.co/${e})?.then?.(r=>W(K(r),async t=>{let o=new DOMParser().parseFromString(await t.text(),"text/html");try{return new URL(o.title)}catch(n){if(n instanceof TypeError)return;throw n}}));var $t=e=>{let r=window.GM_fetch;if("bit.ly","amzn.to","amzn.asia","goo.gl","s.nikkei.com","apple.co","nico.ms","w.wiki".includes(e.hostname)&&r)return r(e).then(o=>o.ok?new URL(o.url):e);if(e.hostname!=="t.co")return e;let t=Gt(e.pathname.slice(1));return t?t.then(o=>Pe(o,()=>e,n=>n??e)):e};var jt=e=>{if(!e.pathname.startsWith("/wiki/"))return e;if(!/^\w+\.wikipedia\.org$/.test(e.hostname)){let,n=e.hostname.match(/^(\w+)\.m\.wikipedia\.org$/)??[];if(!n)return e;e.hostname=${n}.wikipedia.org}let r=decodeURIComponent(e.pathname.slice(6)),t=e.hash?decodeURIComponent(e.hash.slice(1)):"",o=${e.origin}/wiki/${r};return t?[${t} | ${r} - Wikipedia ${o}#${t}]:[${r} - Wikipedia ${o}]};var Dt=e=>{if(e.hostname!=="www.wikiwand.com")return e;let,r,t=e.pathname.match(/^\/(^\/+)\/(^\/+)/)??[];return!r||!t||(e.hostname=${r}.wikipedia.org,e.pathname=/wiki/${t},e.hash=e.hash.startsWith("#/")?#${e.hash.slice(2)}:e.hash),e};var je=e=>{if(!e.hostname.startsWith("www.google."))return e;let r=e.searchParams.get("url");return r?new URL(decodeURIComponent(r)):e};var Vt=/charset=(^;+)/,Wt=e=>window.GM_fetch?.(${e})?.then?.(r=>W(K(r),async t=>{let o=t.headers.get("content-type")?.match?.(Vt)?.1??await Xr(t.clone());return new TextDecoder(o).decode(await t.arrayBuffer())})),Xr=async e=>{let r=new DOMParser().parseFromString(await e.text(),"text/html");return r.querySelector("metacharset")?.getAttribute?.("charset")??r.querySelector('metahttp-equiv="content-type"')?.getAttribute?.("content")?.match?.(Vt)?.1??"utf-8"};var qt=(e=Jr)=>async r=>e(await Qr(r),r),Jr=(e,r)=>{let t=(M(e)?e:e.title).replace(/\s/g," ").replaceAll("","[").replaceAll("","]");return t?[${r.hash?${decodeURIComponent(r.hash.slice(1))} | :""}${t} ${r}]:${r}},Qr=async e=>{let r=Wt(e);if(!r){let o=await ht(e);if(E(o))throw v(o);return I(o)}let t=await r;if(E(t))throw v(t);return new DOMParser().parseFromString(I(t),"text/html")};var Kt=e=>{if(!/^(?:www\.)?amazon(?:\.co|com)?\.(?:au|br|ca|fr|de|in|it|jp|mx|nl|sg|es|tr|ae|uk|cn)$/.test(e.hostname))return e;let,r=e.pathname.match(/\/dp\/(\w\d+)/)??e.pathname.match(/\/gp\/product\/(\w\d+)/)??e.pathname.match(/\/exec\/obidos\/asin\/(\w\d+)/)??e.pathname.match(/\/o\/ASIN\/(\w\d+)/)??[];return r&&(e.hash="",e.search="",e.pathname=/dp/${r}),e};var zt=e=>{if(!/(?:0-9a-z-\.)?gyazo\.com/.test(e.hostname))return e;let,r=e.pathname.match(/^\/(0-9a-f{32})(?:\/raw)?$/)??[];return r?[https://gyazo.com/${r}]:e};var Yt=(e,...r)=>{if(e instanceof URL)return Ae(new URL(e),...r);let t=0,o=0,n=0,s=!1,a=e.split(/(https?:\/\/\S+)/g).map(l=>{if(!/^https?:\/\/\S+$/.test(l))return l;t++;try{let u=Ae(new URL(l),...r);return M(u)?(o++,u):(s=!0,u,l)}catch(u){return console.error(u),n++,l}});if(!s)return a.join("");let{render:p,dispose:i}=Ke(),f=()=>p({type:"spinner"},{type:"text",text:URL: ${o}/${t} converted, ${n} failed});return f(),Promise.all(a.map(async l=>{if(M(l))return l;try{let u=await l0;return o++,u}catch(u){return console.error(u),n++,l1}finally{f()}})).then(l=>(p({type:"check-circle"},{type:"text",text:URL: ${o}/${t} converted, ${n} failed}),l.join(""))).finally(()=>{setTimeout(i,1e3)})};var Zr=je,$t,je,Dt,Kt,Ue(),zt,Ht(),jt,qt();scrapbox.PopupMenu.addButton({title:e=>/https?:\/\/\S+/.test(e)?"URL":"",onClick:e=>{let r=Yt(e,...Zr);if(typeof r=="string")return e===r?void 0:r;r.then(t=>{if(e!==t)return qe(t)})}});
code:dictionary.js
// ==UserScript==
// @name Scrapbox Alias Dictionary (共通辞書)
// @description ^記法の辞書を構築・管理する共通モジュール
// @grant none
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
CACHE_KEY: 'scrapbox_unified_alias_dict',
CACHE_DURATION: 24 * 60 * 60 * 1000, // 1日
FETCH_CONCURRENCY: 5,
};
class UnifiedAliasDict {
constructor() {
// Map<別名(小文字), {pageTitle, displayAlias, score}>
this.aliasMap = new Map();
// Map<別名ページタイトル, 正式ページタイトル>
this.redirectMap = new Map();
this.project = scrapbox.Project.name;
this.initialized = false;
this.initPromise = null;
}
async initialize() {
// 既に初期化中または完了している場合は同じPromiseを返す
if (this.initPromise) return this.initPromise;
this.initPromise = this._initialize();
return this.initPromise;
}
async _initialize() {
console.log('UnifiedAliasDict 初期化開始');
// キャッシュ確認
const cached = this.loadCache();
if (cached) {
console.log('UnifiedAliasDict キャッシュから復元');
console.log(' - エイリアス:', cached.aliasMap.size, '件');
console.log(' - リダイレクト:', cached.redirectMap.size, '件');
this.aliasMap = cached.aliasMap;
this.redirectMap = cached.redirectMap;
this.initialized = true;
return;
}
// 新規構築
await this.buildDict();
this.saveCache();
this.initialized = true;
console.log('UnifiedAliasDict 辞書構築完了');
console.log(' - エイリアス:', this.aliasMap.size, '件');
console.log(' - リダイレクト:', this.redirectMap.size, '件');
}
async buildDict() {
const pages = await this.fetchAllPages();
console.log('UnifiedAliasDict 全ページ数:', pages.length);
const chunks = this.chunkArray(pages, CONFIG.FETCH_CONCURRENCY);
for (const chunk of chunks) {
await Promise.all(
chunk.map(page => this.processPage(page.title))
);
}
}
async fetchAllPages() {
const url = /api/pages/${this.project}?limit=1000;
const res = await fetch(url);
const data = await res.json();
return data.pages || [];
}
async processPage(pageTitle) {
try {
const url = /api/pages/${this.project}/${encodeURIComponent(pageTitle)}/text;
const res = await fetch(url);
const text = await res.text();
const lines = text.split('\n');
let foundAliases = false;
for (const line of lines) {
if (!line.startsWith('^')) continue;
foundAliases = true;
// ^記法パース
const aliasText = line.slice(1).trim();
const aliases = aliasText.split('/').map(s => s.trim()).filter(Boolean);
// 両方の辞書に登録
for (const alias of aliases) {
// Completer用: 部分一致検索用
this.aliasMap.set(alias.toLowerCase(), {
pageTitle: pageTitle,
displayAlias: alias,
score: this.calculateScore(alias, pageTitle)
});
// Redirect用: 完全一致リダイレクト用
this.redirectMap.set(alias, pageTitle);
}
}
if (foundAliases) {
console.log([UnifiedAliasDict] ${pageTitle}: 別名を登録);
}
} catch (err) {
console.warn('UnifiedAliasDict ページ取得失敗:', pageTitle, err);
}
}
calculateScore(alias, pageTitle) {
return 1000 - alias.length;
}
// Completer用: 部分一致検索
search(query) {
if (!query) return [];
const q = query.toLowerCase();
const results = [];
for (const alias, data of this.aliasMap.entries()) {
if (alias.includes(q)) {
results.push({
alias: data.displayAlias,
pageTitle: data.pageTitle,
score: data.score + (alias.startsWith(q) ? 100 : 0)
});
}
}
results.sort((a, b) => b.score - a.score);
return results.slice(0, 10);
}
// Redirect用: 完全一致チェック
getRedirectTarget(pageTitle) {
return this.redirectMap.get(pageTitle) || null;
}
chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
loadCache() {
try {
const cached = localStorage.getItem(CONFIG.CACHE_KEY);
if (!cached) return null;
const data = JSON.parse(cached);
if (Date.now() - data.timestamp > CONFIG.CACHE_DURATION) {
localStorage.removeItem(CONFIG.CACHE_KEY);
return null;
}
return {
aliasMap: new Map(data.aliasMap),
redirectMap: new Map(data.redirectMap)
};
} catch (err) {
console.warn('UnifiedAliasDict キャッシュ読み込み失敗:', err);
return null;
}
}
saveCache() {
try {
const data = {
timestamp: Date.now(),
aliasMap: Array.from(this.aliasMap.entries()),
redirectMap: Array.from(this.redirectMap.entries())
};
localStorage.setItem(CONFIG.CACHE_KEY, JSON.stringify(data));
} catch (err) {
console.warn('UnifiedAliasDict キャッシュ保存失敗:', err);
}
}
async refresh() {
localStorage.removeItem(CONFIG.CACHE_KEY);
this.aliasMap.clear();
this.redirectMap.clear();
this.initialized = false;
this.initPromise = null;
await this.initialize();
console.log('UnifiedAliasDict 辞書を再構築しました');
}
}
// グローバルに公開
window.ScrapboxUnifiedAliasDict = UnifiedAliasDict;
// シングルトンインスタンスを作成
if (!window.scrapboxAliasDict) {
window.scrapboxAliasDict = new UnifiedAliasDict();
window.scrapboxAliasDict.initialize();
console.log('UnifiedAliasDict グローバルインスタンスを初期化しました');
}
})();
code:aliasComp.js
// ==UserScript==
// @name Scrapbox Alias Completer
// @description ^記法による表記ゆれ補完システム
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
CACHE_KEY: 'scrapbox_alias_dict',
CACHE_DURATION: 24 * 60 * 60 * 1000, // 1日
FETCH_CONCURRENCY: 4, // 並列取得数
};
class AliasDict {
constructor() {
this.dict = new Map(); // Map<別名, {pageTitle, score}>
this.project = scrapbox.Project.name;
this.initialized = false;
}
async initialize() {
console.log('AliasCompleter 初期化開始');
// キャッシュ確認
const cached = this.loadCache();
if (cached) {
console.log('AliasCompleter キャッシュから復元', cached.size, '件');
this.dict = cached;
this.initialized = true;
return;
}
// 新規構築
await this.buildDict();
this.saveCache();
this.initialized = true;
console.log('AliasCompleter 辞書構築完了:', this.dict.size, '件');
}
async buildDict() {
// フェーズ1: 全ページリスト取得
const pages = await this.fetchAllPages();
console.log('AliasCompleter 全ページ数:', pages.length);
// フェーズ2: ^記法含むページを並列取得
const chunks = this.chunkArray(pages, CONFIG.FETCH_CONCURRENCY);
for (const chunk of chunks) {
await Promise.all(
chunk.map(page => this.processPage(page.title))
);
}
}
async fetchAllPages() {
const url = /api/pages/${this.project}?limit=1000;
const res = await fetch(url);
const data = await res.json();
return data.pages || [];
}
async processPage(pageTitle) {
try {
const url = /api/pages/${this.project}/${encodeURIComponent(pageTitle)}/text;
const res = await fetch(url);
const text = await res.text();
// 全行から^で始まる行を探す
const lines = text.split('\n');
let foundAliases = false;
for (const line of lines) {
if (!line.startsWith('^')) continue;
foundAliases = true;
// ^記法パース
const aliasText = line.slice(1).trim();
const aliases = aliasText.split('/').map(s => s.trim()).filter(Boolean);
// 辞書に登録
for (const alias of aliases) {
this.dict.set(alias.toLowerCase(), {
pageTitle: pageTitle,
displayAlias: alias,
score: this.calculateScore(alias, pageTitle)
});
}
}
if (foundAliases) {
console.log([AliasCompleter] ${pageTitle}: 別名を登録しました);
}
} catch (err) {
console.warn('AliasCompleter ページ取得失敗:', pageTitle, err);
}
}
calculateScore(alias, pageTitle) {
// スコアリング: 短い別名ほど高スコア
return 1000 - alias.length;
}
search(query) {
if (!query) return [];
const q = query.toLowerCase();
const results = [];
for (const alias, data of this.dict.entries()) {
if (alias.includes(q)) {
results.push({
alias: data.displayAlias,
pageTitle: data.pageTitle,
score: data.score + (alias.startsWith(q) ? 100 : 0) // 前方一致に高スコア
});
}
}
// スコア順にソート
results.sort((a, b) => b.score - a.score);
return results.slice(0, 10); // 上位10件
}
chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
loadCache() {
try {
const cached = localStorage.getItem(CONFIG.CACHE_KEY);
if (!cached) return null;
const data = JSON.parse(cached);
if (Date.now() - data.timestamp > CONFIG.CACHE_DURATION) {
localStorage.removeItem(CONFIG.CACHE_KEY);
return null;
}
return new Map(data.dict);
} catch (err) {
console.warn('AliasCompleter キャッシュ読み込み失敗:', err);
return null;
}
}
saveCache() {
try {
const data = {
timestamp: Date.now(),
dict: Array.from(this.dict.entries())
};
localStorage.setItem(CONFIG.CACHE_KEY, JSON.stringify(data));
} catch (err) {
console.warn('AliasCompleter キャッシュ保存失敗:', err);
}
}
}
class AliasInputUI {
constructor(dict) {
this.dict = dict;
this.selectedIndex = 0;
this.results = [];
this.createStyle();
}
createStyle() {
const style = document.createElement('style');
style.textContent = `
.alias-popup-ui {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
color: #1B1B1B;
padding: 0;
border-radius: 0;
border: 1px solid #D2D5DA;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
z-index: 9999;
font-size: 11px;
font-family: 'Inter', 'Noto Sans JP', sans-serif;
font-weight: 400;
letter-spacing: .03em;
width: 400px;
max-height: 500px;
display: flex;
flex-direction: column;
}
.alias-popup-header {
padding: 16px 20px;
border-bottom: 1px solid #D2D5DA;
}
.alias-popup-input {
background: #fff;
color: #1B1B1B;
border: 1px solid #D2D5DA;
border-radius: 0;
padding: 8px 12px;
width: 100%;
font-size: 13px;
font-family: 'Inter', 'Noto Sans JP', sans-serif;
font-weight: 400;
letter-spacing: .03em;
}
.alias-popup-input:focus {
outline: none;
border-color: #1F75BC;
}
.alias-popup-results {
flex: 1;
overflow-y: auto;
max-height: 360px;
}
.alias-result-item {
padding: 12px 20px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
display: flex;
flex-direction: column;
gap: 4px;
}
.alias-result-item:hover,
.alias-result-item.selected {
background: #e8f4f8;
}
.alias-result-item .page-title {
color: #000;
font-weight: 500;
font-size: 13px;
}
.alias-result-item .alias-name {
color: #666;
font-size: 11px;
}
.alias-popup-footer {
padding: 12px 20px;
border-top: 1px solid #D2D5DA;
font-size: 11px;
color: #a0a0a0;
text-align: center;
}
.alias-popup-empty {
padding: 40px 20px;
text-align: center;
color: #a0a0a0;
}
.alias-popup-loading {
padding: 40px 20px;
text-align: center;
color: #a0a0a0;
}
`;
document.head.appendChild(style);
}
show() {
if (document.querySelector('.alias-popup-ui')) return; // 二重防止
const ui = document.createElement('div');
ui.className = 'alias-popup-ui';
const initialContent = this.dict.initialized
? '<div class="alias-popup-empty">別名を入力してください</div>'
: '<div class="alias-popup-loading">辞書を読み込み中...</div>';
ui.innerHTML = `
<div class="alias-popup-header">
<input type="text" class="alias-popup-input" placeholder="別名を入力...">
</div>
<div class="alias-popup-results">
${initialContent}
</div>
<div class="alias-popup-footer">
↑↓: 選択 / Enter: 確定 / ESC: キャンセル
</div>
`;
document.body.appendChild(ui);
this.ui = ui;
this.input = ui.querySelector('.alias-popup-input');
this.resultsContainer = ui.querySelector('.alias-popup-results');
// イベント設定
this.input.addEventListener('input', () => this.onInput());
this.input.addEventListener('keydown', (e) => this.onKeyDown(e));
// 外側クリックで閉じる
ui.addEventListener('click', (e) => {
if (e.target === ui) this.close();
});
// フォーカス
this.input.focus();
}
onInput() {
const query = this.input.value.trim();
if (!query) {
this.resultsContainer.innerHTML = '<div class="alias-popup-empty">別名を入力してください</div>';
this.results = [];
return;
}
// 検索実行
this.results = this.dict.search(query);
this.selectedIndex = 0;
this.renderResults();
}
renderResults() {
if (this.results.length === 0) {
this.resultsContainer.innerHTML = '<div class="alias-popup-empty">候補が見つかりませんでした</div>';
return;
}
this.resultsContainer.innerHTML = this.results.map((r, i) => `
<div class="alias-result-item ${i === this.selectedIndex ? 'selected' : ''}" data-index="${i}">
<div class="page-title">${this.escapeHtml(r.pageTitle)}</div>
<div class="alias-name">別名: ${this.escapeHtml(r.alias)}</div>
</div>
`).join('');
// クリックイベント
this.resultsContainer.querySelectorAll('.alias-result-item').forEach(item => {
item.addEventListener('click', () => {
const index = parseInt(item.dataset.index);
this.select(index);
});
});
}
onKeyDown(e) {
switch(e.key) {
case 'ArrowDown':
e.preventDefault();
this.moveSelection(1);
break;
case 'ArrowUp':
e.preventDefault();
this.moveSelection(-1);
break;
case 'Enter':
e.preventDefault();
if (this.results.length > 0) {
this.select(this.selectedIndex);
}
break;
case 'Escape':
e.preventDefault();
this.close();
break;
}
}
moveSelection(delta) {
if (this.results.length === 0) return;
this.selectedIndex = Math.max(0, Math.min(
this.results.length - 1,
this.selectedIndex + delta
));
// 選択状態更新
this.resultsContainer.querySelectorAll('.alias-result-item').forEach((item, i) => {
item.classList.toggle('selected', i === this.selectedIndex);
});
// スクロール調整
const selected = this.resultsContainer.querySelector('.alias-result-item.selected');
if (selected) {
selected.scrollIntoView({ block: 'nearest' });
}
}
select(index) {
if (!this.resultsindex) return;
const result = this.resultsindex;
this.insertLink(result.pageTitle);
this.close();
}
insertLink(pageTitle) {
const editor = document.querySelector('.editor');
if (!editor) return;
const textarea = editor.querySelector('textarea');
if (!textarea) return;
const cursorPos = textarea.selectionStart;
const text = textarea.value;
// カーソル位置に PageTitle を挿入
const before = text.slice(0, cursorPos);
const after = text.slice(cursorPos);
const newText = before + [${pageTitle}] + after;
textarea.value = newText;
textarea.selectionStart = textarea.selectionEnd =
cursorPos + pageTitle.length + 2;
// Scrapboxに変更を通知
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.focus();
}
close() {
if (this.ui) {
this.ui.remove();
this.ui = null;
}
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// メイン処理
async function main() {
console.log('AliasCompleter 起動');
const dict = new AliasDict();
// 非同期で辞書初期化(UIは先に表示可能)
dict.initialize();
// ページメニューに登録
scrapbox.PageMenu.addMenu({
title: "別名検索",
image: "https://cdnjs.cloudflare.com/ajax/libs/twemoji/12.0.4/svg/1f50d.svg",
onClick: () => {
const ui = new AliasInputUI(dict);
ui.show();
}
});
// Ctrl+Q でも起動可能
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key.toLowerCase() === 'q') {
e.preventDefault();
const ui = new AliasInputUI(dict);
ui.show();
}
});
console.log('AliasCompleter 準備完了');
// 手動更新用
window.aliasCompleter = {
refresh: async () => {
localStorage.removeItem(CONFIG.CACHE_KEY);
await dict.initialize();
console.log('AliasCompleter 辞書を再構築しました');
},
show: () => {
const ui = new AliasInputUI(dict);
ui.show();
}
};
}
// Scrapbox読み込み完了後に起動
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', main);
} else {
main();
}
})();
code:aliasRedirect.js
// ==UserScript==
// @name Scrapbox Alias Redirect
// @description ^記法による別名リダイレクト機能(Wikipedia風・リンク機能保持版)
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
CACHE_KEY: 'scrapbox_alias_redirect_dict',
CACHE_DURATION: 24 * 60 * 60 * 1000, // 1日
FETCH_CONCURRENCY: 5,
REDIRECT_FROM_KEY: 'scrapbox_redirect_from', // sessionStorage用
};
class RedirectDict {
constructor() {
this.dict = new Map(); // Map<別名ページタイトル, 正式ページタイトル>
this.project = scrapbox.Project.name;
this.initialized = false;
}
async initialize() {
console.log('AliasRedirect 初期化開始');
// キャッシュ確認
const cached = this.loadCache();
if (cached) {
console.log('AliasRedirect キャッシュから復元', cached.size, '件');
this.dict = cached;
this.initialized = true;
return;
}
// 新規構築
await this.buildDict();
this.saveCache();
this.initialized = true;
console.log('AliasRedirect 辞書構築完了:', this.dict.size, '件');
}
async buildDict() {
const pages = await this.fetchAllPages();
console.log('AliasRedirect 全ページ数:', pages.length);
const chunks = this.chunkArray(pages, CONFIG.FETCH_CONCURRENCY);
for (const chunk of chunks) {
await Promise.all(
chunk.map(page => this.processPage(page.title))
);
}
}
async fetchAllPages() {
const url = /api/pages/${this.project}?limit=1000;
const res = await fetch(url);
const data = await res.json();
return data.pages || [];
}
async processPage(pageTitle) {
try {
const url = /api/pages/${this.project}/${encodeURIComponent(pageTitle)}/text;
const res = await fetch(url);
const text = await res.text();
// 全行から^で始まる行を探す
const lines = text.split('\n');
for (const line of lines) {
if (!line.startsWith('^')) continue;
// ^記法パース
const aliasText = line.slice(1).trim();
const aliases = aliasText.split('/').map(s => s.trim()).filter(Boolean);
// 各別名をキーとして、正式ページタイトルを登録
for (const alias of aliases) {
// 別名がそのままページタイトルとして使われた場合のリダイレクト設定
this.dict.set(alias, pageTitle);
}
}
} catch (err) {
console.warn('AliasRedirect ページ取得失敗:', pageTitle, err);
}
}
getRedirectTarget(pageTitle) {
return this.dict.get(pageTitle) || null;
}
chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
loadCache() {
try {
const cached = localStorage.getItem(CONFIG.CACHE_KEY);
if (!cached) return null;
const data = JSON.parse(cached);
if (Date.now() - data.timestamp > CONFIG.CACHE_DURATION) {
localStorage.removeItem(CONFIG.CACHE_KEY);
return null;
}
return new Map(data.dict);
} catch (err) {
console.warn('AliasRedirect キャッシュ読み込み失敗:', err);
return null;
}
}
saveCache() {
try {
const data = {
timestamp: Date.now(),
dict: Array.from(this.dict.entries())
};
localStorage.setItem(CONFIG.CACHE_KEY, JSON.stringify(data));
} catch (err) {
console.warn('AliasRedirect キャッシュ保存失敗:', err);
}
}
}
class LinkRedirector {
constructor(dict) {
this.dict = dict;
this.project = scrapbox.Project.name;
this.currentDisplayedRedirectFrom = null; // 現在表示中の転送元を記録
this.setupStyles();
}
setupStyles() {
const style = document.createElement('style');
style.textContent = `
/* リダイレクトリンクのスタイル(控えめな印) */
a.page-link.redirect-link {
color: #4d88c5 !important;
border-bottom: 1px dotted #4d88c5 !important;
}
a.page-link.redirect-link:hover {
border-bottom-style: solid !important;
}
/* 転送元表示 */
.redirect-from-notice {
background: #f8f9fa;
border-left: 4px solid #4d88c5;
padding: 8px 12px;
margin: 0 0 12px 0;
font-size: 12px;
color: #666;
}
.redirect-from-notice a {
color: #4d88c5;
text-decoration: none;
}
.redirect-from-notice a:hover {
text-decoration: underline;
}
/* 別名ページ用のメニューボタン */
.alias-redirect-menu-button {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 2px 6px;
margin-left: 4px;
background: transparent;
color: #666;
border: 1px solid #ddd;
border-radius: 2px;
font-size: 11px;
cursor: pointer;
transition: all 0.15s;
}
.alias-redirect-menu-button:hover {
background: #f5f5f5;
border-color: #ccc;
color: #333;
}
.alias-redirect-menu-button:active {
background: #eee;
}
.alias-redirect-menu-button.inserted {
background: transparent;
color: #28a745;
border-color: #28a745;
cursor: default;
}
.alias-redirect-menu-button.inserted:hover {
background: transparent;
}
`;
document.head.appendChild(style);
}
start() {
// ページ変更を監視
this.observePageChanges();
// リンクに視覚的マーク&クリックイベント
this.setupLinkRedirect();
// 初回チェック
setTimeout(() => {
this.checkCurrentPage();
}, 100);
}
checkCurrentPage() {
const currentTitle = scrapbox.Page.title;
if (!currentTitle) {
console.log('AliasRedirect ページタイトルが取得できません');
return;
}
console.log([AliasRedirect] checkCurrentPage - ページ: ${currentTitle});
// sessionStorageから転送元情報を取得
const redirectFrom = sessionStorage.getItem(CONFIG.REDIRECT_FROM_KEY);
if (redirectFrom) {
console.log([AliasRedirect] 転送元情報を検出: ${redirectFrom} → ${currentTitle});
// 転送先が正しいか確認
const expectedTarget = this.dict.getRedirectTarget(redirectFrom);
if (expectedTarget === currentTitle) {
// 正式なページに転送されてきた場合
this.showRedirectFromNotice(redirectFrom);
this.currentDisplayedRedirectFrom = currentTitle; // 表示したページを記録
}
// sessionStorageをクリア
sessionStorage.removeItem(CONFIG.REDIRECT_FROM_KEY);
return;
}
// 別のページに遷移した場合、転送元表示を削除
if (this.currentDisplayedRedirectFrom && this.currentDisplayedRedirectFrom !== currentTitle) {
this.removeRedirectFromNotice();
this.currentDisplayedRedirectFrom = null;
}
// 現在のページが別名ページかチェック
const target = this.dict.getRedirectTarget(currentTitle);
if (!target) return;
// 別名ページの場合はメニューボタンを表示
this.showAliasPageButton(currentTitle, target);
console.log([AliasRedirect] 別名ページを表示中: ${currentTitle} (リダイレクト先: ${target}));
}
showAliasPageButton(aliasPage, targetPage) {
// 既存のボタンを削除
const existingButton = document.querySelector('.alias-redirect-menu-button');
if (existingButton) {
existingButton.remove();
}
// ページメニューを待つ
let checkCount = 0;
const maxChecks = 30;
const insertButton = () => {
checkCount++;
// ページメニューの.tool-btn群を探す
const pageMenu = document.querySelector('.page-menu');
if (!pageMenu) {
if (checkCount < maxChecks) {
setTimeout(insertButton, 100);
}
return;
}
// ボタンを作成
const button = document.createElement('button');
button.className = 'alias-redirect-menu-button';
button.innerHTML = '転送テキスト挿入';
button.title = 「転送ページ→[${targetPage}]」を2行目に挿入します;
button.addEventListener('click', async () => {
const success = await this.insertRedirectText(aliasPage, targetPage);
if (success) {
button.innerHTML = '✓ 挿入完了';
button.classList.add('inserted');
button.disabled = true;
}
});
// ページメニューに追加
pageMenu.appendChild(button);
console.log([AliasRedirect] メニューボタンを追加しました: ${aliasPage} → ${targetPage});
};
insertButton();
}
async insertRedirectText(aliasPage, targetPage) {
try {
// 現在の行数をチェック
if (!scrapbox.Page.lines || scrapbox.Page.lines.length < 2) {
// 2行目が存在しない場合は、ユーザーに改行を促す
alert('まず1行目(タイトル行)の末尾で改行してください。\nその後、もう一度このボタンをクリックしてください。');
// カーソルを1行目の末尾に移動させてあげる
const editor = document.querySelector('.editor');
if (editor) {
const textarea = editor.querySelector('textarea');
if (textarea) {
const lines = textarea.value.split('\n');
const firstLineEnd = lines0.length;
textarea.focus();
textarea.setSelectionRange(firstLineEnd, firstLineEnd);
}
}
return false;
}
// エディタを探す
const editor = document.querySelector('.editor');
if (!editor) {
console.warn('AliasRedirect エディタが見つかりません');
return false;
}
const textarea = editor.querySelector('textarea');
if (!textarea) {
console.warn('AliasRedirect テキストエリアが見つかりません');
return false;
}
// 現在の内容を取得
const currentValue = textarea.value;
// 既に転送テキストが含まれているかチェック
if (currentValue.includes('転送ページ→')) {
console.log('AliasRedirect 既に転送テキストが存在します');
alert('既に転送テキストが存在します');
return false;
}
const lines = currentValue.split('\n');
// 2行目が空行でない場合は警告
if (lines.length >= 2 && lines1.trim() !== '') {
const conf = confirm('2行目に既にテキストがあります。\n転送テキストを挿入しますか?');
if (!conf) return false;
}
// 転送テキストを作成
const redirectLine = ※転送ページ→[${targetPage}];
// 2行目にテキストを直接挿入
lines1 = redirectLine;
const newValue = lines.join('\n');
// テキストエリアの値を更新
textarea.value = newValue;
// Scrapboxに変更を通知(inputイベントを発火)
const inputEvent = new Event('input', { bubbles: true });
textarea.dispatchEvent(inputEvent);
// カーソルを2行目の末尾に移動
const firstLineLength = lines0.length;
const cursorPos = firstLineLength + 1 + redirectLine.length;
textarea.focus();
textarea.setSelectionRange(cursorPos, cursorPos);
console.log([AliasRedirect] 転送テキストを挿入しました: ${redirectLine});
return true;
} catch (err) {
console.error('AliasRedirect 転送テキストの挿入に失敗:', err);
alert('転送テキストの挿入に失敗しました');
return false;
}
}
showRedirectFromNotice(fromPage) {
console.log([AliasRedirect] showRedirectFromNotice 実行: ${fromPage});
// 既存の通知を削除
this.removeRedirectFromNotice();
// ページコンテンツを待つ
let insertCount = 0;
const maxInserts = 30;
const insertNotice = () => {
insertCount++;
const pageContent = document.querySelector('.page');
if (!pageContent) {
if (insertCount < maxInserts) {
setTimeout(insertNotice, 100);
} else {
console.log('AliasRedirect .page 要素が見つかりませんでした');
}
return;
}
const notice = document.createElement('div');
notice.className = 'redirect-from-notice';
notice.innerHTML = `
(<a href="/${this.project}/${encodeURIComponent(fromPage)}">${this.escapeHtml(fromPage)}</a>から転送)
`;
// ページの最初の子要素として挿入
if (pageContent.firstChild) {
pageContent.insertBefore(notice, pageContent.firstChild);
} else {
pageContent.appendChild(notice);
}
console.log([AliasRedirect] 転送元表示を挿入しました: ${fromPage});
};
insertNotice();
}
removeRedirectFromNotice() {
const existing = document.querySelector('.redirect-from-notice');
if (existing) {
console.log('AliasRedirect 転送元表示を削除');
existing.remove();
}
}
setupLinkRedirect() {
// リンククリック時のリダイレクト処理
document.addEventListener('click', (e) => {
const link = e.target.closest('a.page-link');
if (!link) return;
const href = link.getAttribute('href');
if (!href) return;
const match = href.match(/^\/(^\/+)\/(.+)$/);
if (!match) return;
const project, encodedTitle = match;
if (project !== this.project) return;
const pageTitle = decodeURIComponent(encodedTitle.split('?')0);
const target = this.dict.getRedirectTarget(pageTitle);
if (target) {
// sessionStorageに転送元を保存
sessionStorage.setItem(CONFIG.REDIRECT_FROM_KEY, pageTitle);
console.log([AliasRedirect] リダイレクト実行: ${pageTitle} → ${target});
console.log([AliasRedirect] sessionStorageに保存: ${pageTitle});
// リダイレクト
e.preventDefault();
e.stopPropagation();
const redirectUrl = /${this.project}/${encodeURIComponent(target)};
window.location.href = redirectUrl;
return false;
}
}, true); // キャプチャフェーズで処理
// リンクスタイルの更新
this.updateLinkStyles();
}
updateLinkStyles() {
const links = document.querySelectorAll('a.page-link');
links.forEach(link => {
const href = link.getAttribute('href');
if (!href) return;
const match = href.match(/^\/(^\/+)\/(.+)$/);
if (!match) return;
const project, encodedTitle = match;
if (project !== this.project) return;
const pageTitle = decodeURIComponent(encodedTitle.split('?')0);
const target = this.dict.getRedirectTarget(pageTitle);
if (target) {
link.classList.add('redirect-link');
link.title = → ${target} にリダイレクト;
}
});
}
observePageChanges() {
// ページ遷移を検知
let lastTitle = scrapbox.Page.title;
let lastUrl = window.location.href;
const checkChanges = () => {
const currentTitle = scrapbox.Page.title;
const currentUrl = window.location.href;
if (currentTitle !== lastTitle || currentUrl !== lastUrl) {
console.log([AliasRedirect] ページ変更検出: ${lastTitle} → ${currentTitle});
lastTitle = currentTitle;
lastUrl = currentUrl;
// 少し待ってからチェック(DOMの更新を待つ)
setTimeout(() => {
this.checkCurrentPage();
this.updateLinkStyles();
}, 200);
}
};
setInterval(checkChanges, 300);
// DOMの変更を監視してリンクスタイルを更新
const observer = new MutationObserver(() => {
this.updateLinkStyles();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// メイン処理
async function main() {
console.log('AliasRedirect 起動');
const dict = new RedirectDict();
await dict.initialize();
const redirector = new LinkRedirector(dict);
redirector.start();
console.log('AliasRedirect 準備完了');
// 手動更新用
window.aliasRedirect = {
refresh: async () => {
localStorage.removeItem(CONFIG.CACHE_KEY);
await dict.initialize();
console.log('AliasRedirect 辞書を再構築しました');
window.location.reload();
},
check: (pageTitle) => {
const target = dict.getRedirectTarget(pageTitle);
if (target) {
console.log(${pageTitle} → ${target});
return target;
} else {
console.log(${pageTitle} はリダイレクト対象外です);
return null;
}
}
};
}
// Scrapbox読み込み完了後に起動
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', main);
} else {
main();
}
})();