Adds search links for Japanese resources
// ==UserScript==
// @name Bunpro Toolbox
// @version 1.7.0
// @description Adds search links for Japanese resources
// @author Ambo100
// @match *://bunpro.jp/grammar_points/*
// @match *://bunpro.jp/learn*
// @match *://bunpro.jp/vocabs/*
// @grant none
// @namespace https://greasyfork.org/users/230700
// ==/UserScript==
(function () {
'use strict';
const STYLE_ID = 'bp-toolbox-style';
const POLL_MS = 1000;
const GRAMMAR_TEMPLATE = `
Core Sections:
- Meaning
- Structure
- Usage
- Example Sentences
- Common Patterns
- Facts
Optional:
- Facts
- Contrast
- Etymology
- Common Mistakes
`;
const VOCAB_TEMPLATE = `
Core Sections:
- Meaning
- Nuance
- Usage
- Example Sentences
Optional:
- Facts
- Kanji Breakdown
- Common Collocations
- Related Vocab
`;
const AI_TEMPLATE = `
Role: Japanese Tutor
Task: Explain this Japanese {itemType} clearly, practically, and in a structured textbook style.
Input: {currentItem}
Rules:
- Follow the section order exactly
- Use clear headings, tables and pretty formatting. Textbook style.
- Give 3 natural Japanese example sentences with English translations
- No romaji
- Include upto 3-4 concise facts
- Include common mistakes if relevant
{template}
`;
const CLASSES = {
launcher: 'bp-toolbox-launcher',
panel: 'bp-toolbox-panel',
open: 'bp-toolbox-open',
header: 'bp-toolbox-header',
title: 'bp-toolbox-title',
body: 'bp-toolbox-body',
section: 'bp-toolbox-section',
sectionTitle: 'bp-toolbox-section-title',
links: 'bp-toolbox-links',
button: 'bp-toolbox-button',
close: 'bp-toolbox-close'
};
const RESOURCES = [
{
title: 'Dictionaries',
items: [
{ label: 'Jisho', url: 'https://jisho.org/search/{currentItem}' },
{ label: 'Wiktionary', url: 'https://en.wiktionary.org/wiki/{currentItem}#Japanese' },
{ label: 'Eijirou', url: 'https://eow.alc.co.jp/search?q={currentItem}' },
{ label: 'Weblio', url: 'https://www.weblio.jp/content/{currentItem}' }
]
},
{
title: 'Video',
items: [
{ label: 'YouGlish', url: 'https://youglish.com/pronounce/{currentItem}/japanese?' },
{ label: 'YouTube', url: 'https://www.youtube.com/results?search_query={currentItem}+Japanese' }
]
},
{
title: 'Community',
items: [
{ label: 'Stack Exchange', url: 'https://japanese.stackexchange.com/search?q={currentItem}' },
{ label: 'Reddit', url: 'https://www.reddit.com/r/LearnJapanese/search?q={currentItem}&restrict_sr=on&sort=relevance&t=all' },
{ label: 'WK Forum', url: 'https://community.wanikani.com/search?q={currentItem}%20category%3A17' }
]
},
{
title: 'AI',
items: [
{ label: 'ChatGPT', url: 'https://chat.openai.com/?q={prompt}', ai: true },
{ label: 'Claude', url: 'https://claude.ai/new?q={prompt}', ai: true }
]
}
];
const STYLES = `
.${CLASSES.launcher} {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 2147483647;
min-height: 40px;
padding: 0 14px;
border: 1px solid rgba(255,255,255,0.16);
border-radius: 999px;
background: #1f2933;
color: #e5e7eb;
cursor: pointer;
font: 600 14px system-ui, sans-serif;
}
.${CLASSES.panel} {
position: fixed;
right: 20px;
bottom: 68px;
z-index: 2147483647;
width: 300px;
max-height: 70vh;
display: flex;
flex-direction: column;
border: 1px solid rgba(255,255,255,0.12);
border-radius: 12px;
background: #1f2933;
color: #e5e7eb;
box-shadow: 0 12px 30px rgba(0,0,0,0.3);
overflow: hidden;
opacity: 0;
pointer-events: none;
transform: translateY(8px);
transition: opacity 120ms ease, transform 120ms ease;
}
.${CLASSES.panel}.${CLASSES.open} {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
.${CLASSES.header} {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 12px;
border-bottom: 1px solid rgba(255,255,255,0.08);
}
.${CLASSES.title} {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font: 600 14px system-ui, sans-serif;
}
.${CLASSES.close} {
width: 28px;
height: 28px;
padding: 0;
}
.${CLASSES.body} {
overflow-y: auto;
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.${CLASSES.section} {
display: flex;
flex-direction: column;
gap: 8px;
}
.${CLASSES.sectionTitle} {
font: 600 11px system-ui, sans-serif;
letter-spacing: 0.04em;
text-transform: uppercase;
opacity: 0.7;
}
.${CLASSES.links} {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.${CLASSES.button} {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 32px;
padding: 0.45rem 0.7rem;
border: 1px solid rgba(255,255,255,0.14);
border-radius: 999px;
background: transparent;
color: inherit;
text-decoration: none;
cursor: pointer;
font: 500 13px system-ui, sans-serif;
line-height: 1.2;
transition: transform 120ms ease, background-color 120ms ease, border-color 120ms ease;
}
.${CLASSES.button}:hover,
.${CLASSES.button}:focus-visible {
background: rgba(255,255,255,0.08);
border-color: rgba(255,255,255,0.3);
transform: translateY(-1px);
outline: none;
}
`;
let currentUrl = window.location.href;
let currentItem = '';
let launcher = null;
let panel = null;
let titleEl = null;
let bodyEl = null;
window.addEventListener('load', initialize, false);
function resolveCurrentItem() {
const itemNode = document.querySelector('h1 .bp-ddw, .bp-ddw');
if (!itemNode) return '';
const clonedNode = itemNode.cloneNode(true);
clonedNode.querySelectorAll('rt, rp').forEach((rubyElement) => rubyElement.remove());
return clonedNode.textContent?.trim() || '';
}
function getPageType() {
const path = window.location.pathname;
if (path.startsWith('/vocabs/')) return 'vocab';
if (path.startsWith('/grammar_points/')) return 'grammar';
return '';
}
function getItemType() {
return getPageType() === 'vocab' ? 'vocabulary item' : 'grammar point';
}
function getSubTemplate() {
return getPageType() === 'vocab'
? VOCAB_TEMPLATE.trim()
: GRAMMAR_TEMPLATE.trim();
}
function getAiPrompt() {
return AI_TEMPLATE
.replace('{itemType}', getItemType())
.replace('{currentItem}', currentItem)
.replace('{template}', getSubTemplate())
.trim();
}
function initialize() {
ensureStyles();
const nextItem = resolveCurrentItem();
if (!nextItem) {
currentItem = '';
removeUI();
return;
}
ensureUI();
const needsRender =
nextItem !== currentItem ||
!bodyEl ||
!bodyEl.childElementCount;
currentItem = nextItem;
updateTitle();
if (needsRender) {
renderSections();
}
}
function ensureStyles() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = STYLES;
document.head.appendChild(style);
}
function ensureUI() {
if (!launcher) {
launcher = el('button', CLASSES.launcher, 'Toolbox');
launcher.type = 'button';
launcher.setAttribute('aria-expanded', 'false');
launcher.addEventListener('click', togglePanel);
document.body.appendChild(launcher);
}
if (!panel) {
panel = el('section', CLASSES.panel);
const header = el('div', CLASSES.header);
titleEl = el('div', CLASSES.title);
const close = el('button', `${CLASSES.button} ${CLASSES.close}`, '×');
close.type = 'button';
close.addEventListener('click', closePanel);
bodyEl = el('div', CLASSES.body);
header.append(titleEl, close);
panel.append(header, bodyEl);
document.body.appendChild(panel);
}
bindGlobalEvents();
}
function bindGlobalEvents() {
if (document.body.dataset.bpToolboxBound === 'true') return;
document.body.dataset.bpToolboxBound = 'true';
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') closePanel();
});
document.addEventListener('click', (event) => {
if (!panel?.classList.contains(CLASSES.open)) return;
if (panel?.contains(event.target) || launcher?.contains(event.target)) return;
closePanel();
});
}
function renderSections() {
if (!bodyEl) return;
bodyEl.innerHTML = '';
for (const group of RESOURCES) {
const links = group.items.map(buildItem).filter(Boolean);
if (!links.length) continue;
const section = el('div', CLASSES.section);
const heading = el('div', CLASSES.sectionTitle, group.title);
const linkWrap = el('div', CLASSES.links);
linkWrap.append(...links);
section.append(heading, linkWrap);
bodyEl.appendChild(section);
}
}
function buildItem(item) {
const url = buildUrl(item);
if (!url) return null;
const link = el('a', CLASSES.button, item.label);
link.href = url;
link.target = '_blank';
link.rel = 'noopener noreferrer';
return link;
}
function buildUrl(item) {
if (!currentItem || !item.url) return '';
if (item.ai) {
const prompt = getAiPrompt();
return item.url.replace('{prompt}', encodeURIComponent(prompt));
}
if (item.prompt) {
const prompt = item.prompt.replace('{currentItem}', currentItem);
return item.url.replace('{prompt}', encodeURIComponent(prompt));
}
return item.url.replace('{currentItem}', encodeURIComponent(currentItem));
}
function updateTitle() {
if (titleEl) {
titleEl.textContent = currentItem || 'Bunpro Toolbox';
}
}
function togglePanel() {
panel?.classList.contains(CLASSES.open) ? closePanel() : openPanel();
}
function openPanel() {
if (!panel) return;
panel.classList.add(CLASSES.open);
launcher?.setAttribute('aria-expanded', 'true');
}
function closePanel() {
if (!panel) return;
panel.classList.remove(CLASSES.open);
launcher?.setAttribute('aria-expanded', 'false');
}
function removeUI() {
launcher?.remove();
panel?.remove();
launcher = null;
panel = null;
titleEl = null;
bodyEl = null;
}
function el(tag, className = '', text = '') {
const node = document.createElement(tag);
if (className) node.className = className;
if (text) node.textContent = text;
return node;
}
setInterval(() => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href;
initialize();
}
}, POLL_MS);
})();