UserScript:ワンポイント
code:script.js
'use strict';
/**
* Sets up the "ワンポイント" Scrapbox page menu.
* @param {string} listUrl - The URL of the CSV file for the main menu.
*/
export function setupOnePointMenu(listUrl) {
if (typeof scrapbox === 'undefined') return;
const mainMenuConfig = {
title: MAIN_MENU_TITLE,
image: MAIN_ICON,
onClick: async () => {
scrapbox.PageMenu(MAIN_MENU_TITLE).addItem({ title: 'Now loading...', onClick: () => {} });
try {
const lines = await fetchImageLists(listUrl);
const urls = lines.map(line => {
const name = 'unknown', url = '' = line.split(',');
//const cleanedUrl = url.replace(/a-zA-Z0-9\-_.,~%():*?#@&=+;\/\\/g, "");
const cleanedUrl = url.replace(/[\\]/g, "");
return { name, url: cleanedUrl };
})
.filter(item => IMAGE_URL_REGEX.test(item.url));
scrapbox.PageMenu(MAIN_MENU_TITLE).removeAllItems();
scrapbox.PageMenu(MAIN_MENU_TITLE).addItem({ title: '消す', onClick: removeStyle });
urls.forEach(item => {
scrapbox.PageMenu(MAIN_MENU_TITLE).addItem({
title: item.name,
onClick: () => changeStyle(getCssTemplate(1)(item.url)),
});
});
const imageUrls = urls.map(item => item.url);
if (imageUrls.length > 0) {
scrapbox.PageMenu(MAIN_MENU_TITLE).addItem({
title: 'ランダム',
onClick: () => changeStyle(getCssTemplate(2)(choice(imageUrls))),
});
}
} catch (err) {
console.error('Failed to build main menu:', err);
scrapbox.PageMenu(MAIN_MENU_TITLE).removeAllItems();
scrapbox.PageMenu(MAIN_MENU_TITLE).addItem({
title: '❌ 取得失敗',
onClick: () => alert('リストの取得に失敗しました。'),
});
}
},
};
const subMenuConfig = {
title: SUB_MENU_TITLE,
image: SUB_ICON,
};
scrapbox.PageMenu.addMenu(mainMenuConfig);
scrapbox.PageMenu.addMenu(subMenuConfig);
// Add sub-menu items
scrapbox.PageMenu(SUB_MENU_TITLE).addItem({ title: '消す', onClick: removeStyle });
addManualMenuItem('ワンポイント1', 1);
addManualMenuItem('ワンポイント2', 2);
addManualMenuItem('ワンポイント3', 3);
}
// --- Constants (exported for testing) ---
export const MODE_ID = '__one__';
export const MAIN_MENU_TITLE = 'ワンポイント';
export const SUB_MENU_TITLE = 'ワンポイント補助';
export const MAIN_ICON = '/api/pages/suto3/user-03/icon';
export const SUB_ICON = '/api/pages/suto3/user-04/icon';
export const IMAGE_URL_REGEX = /(https?:\/\/[\w\-\.\/?\,\#\:\%\u3000-\u30FE\u4E00-\u9FA0\uFF01-\uFFE3\\]+)\.(jpg|jpeg|gif|png|webp|svg)/i;
// --- DOM Manipulation (exported for testing) ---
export const changeStyle = (css) => {
let style = document.getElementById(MODE_ID);
if (!style) {
style = document.createElement('style');
style.id = MODE_ID;
document.head.appendChild(style);
}
style.textContent = css;
};
export const removeStyle = () => {
const style = document.getElementById(MODE_ID);
if (style) style.remove();
};
// --- CSS Generators (exported for testing) ---
export const getCssTemplate = (type) => {
switch (type) {
case 1:
return (url) => `
.page {
background-image: linear-gradient(to right, var(--assort-color), 70%, transparent, 90%, transparent), url("${url}");
background-origin: border-box, border-box;
background-repeat: repeat-y, no-repeat;
background-attachment: scroll, fixed;
background-position: 0% 0%, 75% 65%;
background-size: 100% 100%, 30% auto;
}`;
case 2:
return (url) => `
.page {
background-image: linear-gradient(to right, var(--assort-color), 70%, transparent, 90%, transparent), url("${url}");
background-repeat: repeat-y;
background-attachment: scroll;
background-position: center top;
background-size: 100% auto;
}`;
case 3:
return (url) => `
.page {
background-image: linear-gradient(to bottom, transparent, 0%, transparent, 20%, var(--assort-color), 40%, var(--assort-color), 70%, transparent, 90%, transparent), url("${url}");
background-repeat: no-repeat, repeat-y;
background-attachment: fixed, scroll;
background-position: center top;
background-size: 100% auto;
}`;
default:
throw new Error(Invalid style type: ${type});
}
};
// --- Utility (exported for testing) ---
export const choice = (arr) => {
if (!arr || arr.length === 0) return null;
return arrMath.floor(Math.random() * arr.length);
};
// --- Data Fetching (exported for testing) ---
export const fetchImageLists = async (url) => {
const res = await fetch(url);
if (!res.ok) {
throw new Error('Network response was not ok');
}
const text = await res.text();
return text.split('\n').slice(1).map(line => line.replace(/^,/, "")).filter(Boolean);
};
// --- Menu Item Setup (exported for testing) ---
export const addManualMenuItem = (title, styleType) => {
scrapbox.PageMenu(SUB_MENU_TITLE).addItem({
title,
onClick: () => {
const imageUrl = prompt('画像URLを入力してください');
if (!imageUrl) return; // Cancelled
if (IMAGE_URL_REGEX.test(imageUrl)) {
changeStyle(getCssTemplate(styleType)(imageUrl));
} else {
alert('無効なURLです。画像ファイルのURLを入力してください。');
}
},
});
};
code:scriptxx.js
'use strict';
/**
* Sets up the "ワンポイント" Scrapbox page menu.
* @param {string} listUrl - The URL of the CSV file for the main menu.
*/
export function setupOnePointMenu(listUrl) {
if (typeof scrapbox === 'undefined') return;
const mainMenuConfig = {
title: MAIN_MENU_TITLE,
image: MAIN_ICON,
onClick: async () => {
scrapbox.PageMenu(MAIN_MENU_TITLE).addItem({ title: 'Now loading...', onClick: () => {} });
try {
const lines = await fetchImageLists(listUrl);
const urls = lines.map(line => {
const name = 'unknown', url = '' = line.split(',');
const cleanedUrl = url.replace(/a-zA-Z0-9\-_.,~%():*?#@&=+;\/\\/g, "");
return { name, url: cleanedUrl };
})
.filter(item => IMAGE_URL_REGEX.test(item.url));
scrapbox.PageMenu(MAIN_MENU_TITLE).removeAllItems();
scrapbox.PageMenu(MAIN_MENU_TITLE).addItem({ title: '消す', onClick: removeStyle });
urls.forEach(item => {
scrapbox.PageMenu(MAIN_MENU_TITLE).addItem({
title: item.name,
onClick: () => changeStyle(getCssTemplate(1)(item.url)),
});
});
const imageUrls = urls.map(item => item.url);
if (imageUrls.length > 0) {
scrapbox.PageMenu(MAIN_MENU_TITLE).addItem({
title: 'ランダム',
onClick: () => changeStyle(getCssTemplate(2)(choice(imageUrls))),
});
}
} catch (err) {
console.error('Failed to build main menu:', err);
scrapbox.PageMenu(MAIN_MENU_TITLE).removeAllItems();
scrapbox.PageMenu(MAIN_MENU_TITLE).addItem({
title: '❌ 取得失敗',
onClick: () => alert('リストの取得に失敗しました。'),
});
}
},
};
const subMenuConfig = {
title: SUB_MENU_TITLE,
image: SUB_ICON,
};
scrapbox.PageMenu.addMenu(mainMenuConfig);
scrapbox.PageMenu.addMenu(subMenuConfig);
// Add sub-menu items
scrapbox.PageMenu(SUB_MENU_TITLE).addItem({ title: '消す', onClick: removeStyle });
addManualMenuItem('ワンポイント1', 1);
addManualMenuItem('ワンポイント2', 2);
addManualMenuItem('ワンポイント3', 3);
}
// --- Constants (exported for testing) ---
export const MODE_ID = '__one__';
export const MAIN_MENU_TITLE = 'ワンポイント';
export const SUB_MENU_TITLE = 'ワンポイント補助';
export const MAIN_ICON = '/api/pages/suto3/user-03/icon';
export const SUB_ICON = '/api/pages/suto3/user-04/icon';
export const IMAGE_URL_REGEX = /(https?:\/\/[\w\-\.\/?\,\#\:\%\u3000-\u30FE\u4E00-\u9FA0\uFF01-\uFFE3\\]+)\.(jpg|jpeg|gif|png|webp|svg)/i;
// --- DOM Manipulation (exported for testing) ---
export const changeStyle = (css) => {
let style = document.getElementById(MODE_ID);
if (!style) {
style = document.createElement('style');
style.id = MODE_ID;
document.head.appendChild(style);
}
style.textContent = css;
};
export const removeStyle = () => {
const style = document.getElementById(MODE_ID);
if (style) style.remove();
};
// --- CSS Generators (exported for testing) ---
export const getCssTemplate = (type) => {
switch (type) {
case 1:
return (url) => `
.page {
background: linear-gradient(to right, var(--assort-color), 70%, transparent, 90%, transparent), url("${url}");
background-origin: border-box, border-box;
background-repeat: repeat-y, no-repeat;
background-attachment: scroll, fixed;
background-position: 0% 0%, 75% 65%;
background-size: 100% 100%, 30% auto;
}`;
case 2:
return (url) => `
.page {
background: linear-gradient(to right, var(--assort-color), 70%, transparent, 90%, transparent), url("${url}");
background-repeat: repeat-y;
background-attachment: scroll;
background-position: center top;
background-size: 100% auto;
}`;
case 3:
return (url) => `
.page {
background: linear-gradient(to bottom, transparent, 0%, transparent, 20%, var(--assort-color), 40%, var(--assort-color), 70%, transparent, 90%, transparent), url("${url}");
background-repeat: no-repeat, repeat-y;
background-attachment: fixed, scroll;
background-position: center top;
background-size: 100% auto;
}`;
default:
throw new Error(Invalid style type: ${type});
}
};
// --- Utility (exported for testing) ---
export const choice = (arr) => {
if (!arr || arr.length === 0) return null;
return arrMath.floor(Math.random() * arr.length);
};
// --- Data Fetching (exported for testing) ---
export const fetchImageLists = async (url) => {
const res = await fetch(url);
if (!res.ok) {
throw new Error('Network response was not ok');
}
const text = await res.text();
return text.split('\n').slice(1).map(line => line.replace(/^,/, "")).filter(Boolean);
};
// --- Menu Item Setup (exported for testing) ---
export const addManualMenuItem = (title, styleType) => {
scrapbox.PageMenu(SUB_MENU_TITLE).addItem({
title,
onClick: () => {
const imageUrl = prompt('画像URLを入力してください');
if (!imageUrl) return; // Cancelled
if (IMAGE_URL_REGEX.test(imageUrl)) {
changeStyle(getCssTemplate(styleType)(imageUrl));
} else {
alert('無効なURLです。画像ファイルのURLを入力してください。');
}
},
});
};