Inserts RTK (Remembering the Kanji) information into JPDB kanji cards
// ==UserScript==
// @name JPDB RTK Information Inserter
// @version 1.0
// @description Inserts RTK (Remembering the Kanji) information into JPDB kanji cards
// @author Henry Russell
// @match https://jpdb.io/kanji/*
// @match https://jpdb.io/review*
// @connect hrussellzfac023.github.io
// @grant GM_xmlhttpRequest
// @license MIT
// @namespace http://tampermonkey.net/
// ==/UserScript==
(function() {
'use strict';
let currentKanji = '';
function extractKanjiFromURL() {
const url = window.location.href;
// For kanji pages like https://jpdb.io/kanji/犬
const kanjiMatch = url.match(/https:\/\/jpdb\.io\/kanji\/(.+?)(?:[?#]|$)/);
if (kanjiMatch) {
// Remove any URL parameters and decode
const kanjiPart = kanjiMatch[1].split('?')[0].split('#')[0];
return decodeURIComponent(kanjiPart);
}
// For review pages
const hiddenInput = document.querySelector('input[name="c"]');
if (hiddenInput) {
const parts = hiddenInput.value.split(',');
if (parts.length > 1 && parts[0] === 'kb') {
return parts[1];
}
}
return '';
}
function fetchRTKInfo(kanji) {
const encodedKanji = encodeURIComponent(kanji);
const url = `https://hrussellzfac023.github.io/rtk/${encodedKanji}/index.html`;
GM_xmlhttpRequest({
method: 'GET',
url: url,
onload: function(response) {
if (response.status === 200) {
parseAndInsertRTKInfo(response.responseText, kanji);
} else {
console.log(`Failed to fetch RTK page for ${kanji}: ${response.status}`);
}
},
onerror: function(error) {
console.error('Error fetching RTK page:', error);
}
});
}
function parseAndInsertRTKInfo(html, kanji) {
// Create a temporary DOM element to parse the HTML
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Extract RTK information
const rtkInfo = extractRTKData(doc);
if (!rtkInfo.keyword) {
console.log(`No RTK information found for kanji: ${kanji}`);
return;
}
insertRTKInfoIntoJPDB(rtkInfo, kanji);
}
function extractRTKData(doc) {
const rtkInfo = {};
// Extract keyword (frame number)
const keywordElement = doc.querySelector('h2 code');
if (keywordElement) {
rtkInfo.keyword = keywordElement.textContent.trim();
rtkInfo.frameNumber = keywordElement.getAttribute('title') || '';
}
// Extract On-Yomi and Kun-Yomi
const yomiElement = doc.querySelector('h3');
if (yomiElement) {
const yomiText = yomiElement.textContent;
const onYomiMatch = yomiText.match(/On-Yomi:\s*([^—]+)/);
const kunYomiMatch = yomiText.match(/Kun-Yomi:\s*(.+)/);
if (onYomiMatch) {
rtkInfo.onYomi = onYomiMatch[1].trim();
}
if (kunYomiMatch) {
rtkInfo.kunYomi = kunYomiMatch[1].trim();
}
}
// Extract elements
const headings = doc.querySelectorAll('h2');
for (const heading of headings) {
if (heading.textContent.includes('Elements:')) {
const nextP = heading.nextElementSibling;
if (nextP && nextP.tagName === 'P') {
rtkInfo.elements = nextP.textContent.trim();
}
break;
}
}
// Extract Heisig story
const heisigStoryHeading = Array.from(doc.querySelectorAll('h2')).find(h =>
h.textContent.includes('Heisig story:')
);
if (heisigStoryHeading) {
const storyP = heisigStoryHeading.nextElementSibling;
if (storyP && storyP.tagName === 'P') {
rtkInfo.heisigStory = storyP.innerHTML; // Use innerHTML to preserve formatting
}
}
// Extract Heisig comment
const heisigCommentHeading = Array.from(doc.querySelectorAll('h2')).find(h =>
h.textContent.includes('Heisig comment:')
);
if (heisigCommentHeading) {
const commentP = heisigCommentHeading.nextElementSibling;
if (commentP && commentP.tagName === 'P') {
rtkInfo.heisigComment = commentP.innerHTML; // Use innerHTML to preserve formatting
}
}
// Extract Koohii stories
const koohiiHeading = Array.from(doc.querySelectorAll('h2')).find(h =>
h.textContent.includes('Koohii stories:')
);
if (koohiiHeading) {
rtkInfo.koohiiStories = [];
let nextElement = koohiiHeading.nextElementSibling;
while (nextElement && nextElement.tagName === 'P') {
rtkInfo.koohiiStories.push(nextElement.innerHTML);
nextElement = nextElement.nextElementSibling;
}
}
return rtkInfo;
}
function insertRTKInfoIntoJPDB(rtkInfo, kanji) {
// Check if we already inserted RTK info to avoid duplicates
if (document.getElementById('rtk-info-container')) {
return;
}
// Find a good insertion point - look for the main content area to insert after it
let insertionPoint = null;
// For kanji pages, try to insert after the mnemonic section but before "Used in" sections
const mnemonicSection = document.querySelector('h6.subsection-label + .subsection .mnemonic');
if (mnemonicSection) {
// Find the parent container of the mnemonic section
let mnemonicContainer = mnemonicSection.closest('.vbox > div, .result > div');
if (mnemonicContainer) {
insertionPoint = mnemonicContainer;
}
}
// Fallback: look for the result kanji container (the main container for kanji pages)
if (!insertionPoint) {
const resultKanji = document.querySelector('.result.kanji');
if (resultKanji) {
insertionPoint = resultKanji;
}
}
// Fallback: look for the main vbox gap container that holds the kanji display and components
if (!insertionPoint) {
const vboxContainers = document.querySelectorAll('.vbox.gap');
for (const vbox of vboxContainers) {
// Check if this vbox contains the kanji SVG or mnemonic components
if (vbox.querySelector('svg.kanji') || vbox.querySelector('.subsection-composed-of-kanji')) {
insertionPoint = vbox.parentNode; // Get the parent container instead
break;
}
}
}
// Final fallback to main content area
if (!insertionPoint) {
insertionPoint = document.querySelector('.container') || document.querySelector('.review-reveal') || document.body;
}
if (!insertionPoint) {
console.log('Could not find suitable insertion point for RTK info');
return;
}
// Create the main container
const container = document.createElement('div');
container.id = 'rtk-info-container';
container.style.cssText = `
margin: 20px 0;
border: 1px solid var(--table-border-color);
border-radius: 8px;
padding: 1rem;
background-color: var(--foreground-background-color);
`;
// Create RTK header
const header = document.createElement('h6');
header.className = 'subsection-label';
header.style.cssText = `
color: var(--subsection-label-color);
font-size: 85%;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
`;
header.innerHTML = `RTK information ${kanji}`;
container.appendChild(header);
// Create content container
const contentDiv = document.createElement('div');
contentDiv.className = 'subsection';
contentDiv.style.cssText = `
padding-left: 0.5rem;
`;
// Add keyword
if (rtkInfo.keyword) {
const keywordDiv = document.createElement('div');
keywordDiv.style.cssText = `
margin-bottom: 0.75rem;
`;
keywordDiv.innerHTML = `<strong>Keyword:</strong> <span style="font-size: 110%; color: var(--text-strong-color);">${rtkInfo.keyword}</span>`;
contentDiv.appendChild(keywordDiv);
}
// Add readings if available
if (rtkInfo.onYomi || rtkInfo.kunYomi) {
const readingsDiv = document.createElement('div');
readingsDiv.style.cssText = `
margin-bottom: 0.75rem;
font-size: 95%;
`;
let readingsHTML = '<strong>Readings:</strong> ';
if (rtkInfo.onYomi) {
readingsHTML += `On-Yomi: <span style="font-family: 'Extra Sans JP', 'Noto Sans JP', sans-serif;">${rtkInfo.onYomi}</span>`;
}
if (rtkInfo.kunYomi) {
if (rtkInfo.onYomi) readingsHTML += ' — ';
readingsHTML += `Kun-Yomi: <span style="font-family: 'Extra Sans JP', 'Noto Sans JP', sans-serif;">${rtkInfo.kunYomi}</span>`;
}
readingsDiv.innerHTML = readingsHTML;
contentDiv.appendChild(readingsDiv);
}
// Add elements
if (rtkInfo.elements) {
const elementsDiv = document.createElement('div');
elementsDiv.style.cssText = `
margin-bottom: 0.75rem;
font-size: 95%;
`;
elementsDiv.innerHTML = `<strong>Elements:</strong> ${rtkInfo.elements}`;
contentDiv.appendChild(elementsDiv);
}
// Add Heisig story
if (rtkInfo.heisigStory) {
const storyHeader = document.createElement('h6');
storyHeader.style.cssText = `
color: var(--subsection-label-color);
font-size: 90%;
margin: 1rem 0 0.5rem 0;
font-weight: bold;
`;
storyHeader.textContent = 'Heisig Story:';
contentDiv.appendChild(storyHeader);
const storyDiv = document.createElement('div');
storyDiv.className = 'mnemonic';
storyDiv.style.cssText = `
font-size: 95%;
line-height: 1.4;
margin-bottom: 0.75rem;
text-align: justify;
text-justify: inter-word;
`;
storyDiv.innerHTML = rtkInfo.heisigStory;
contentDiv.appendChild(storyDiv);
}
// Add Heisig comment
if (rtkInfo.heisigComment) {
const commentHeader = document.createElement('h6');
commentHeader.style.cssText = `
color: var(--subsection-label-color);
font-size: 90%;
margin: 1rem 0 0.5rem 0;
font-weight: bold;
`;
commentHeader.textContent = 'Heisig Comment:';
contentDiv.appendChild(commentHeader);
const commentDiv = document.createElement('div');
commentDiv.className = 'mnemonic';
commentDiv.style.cssText = `
font-size: 95%;
line-height: 1.4;
margin-bottom: 0.75rem;
text-align: justify;
text-justify: inter-word;
`;
commentDiv.innerHTML = rtkInfo.heisigComment;
contentDiv.appendChild(commentDiv);
}
// Add Koohii stories
if (rtkInfo.koohiiStories && rtkInfo.koohiiStories.length > 0) {
const koohiiHeader = document.createElement('h6');
koohiiHeader.style.cssText = `
color: var(--subsection-label-color);
font-size: 90%;
margin: 1rem 0 0.5rem 0;
font-weight: bold;
`;
koohiiHeader.textContent = 'Koohii Stories:';
contentDiv.appendChild(koohiiHeader);
const koohiiContainer = document.createElement('div');
koohiiContainer.style.cssText = `
margin-bottom: 0.75rem;
`;
rtkInfo.koohiiStories.forEach((story, index) => {
const storyDiv = document.createElement('div');
storyDiv.style.cssText = `
font-size: 90%;
line-height: 1.4;
margin-bottom: 0.5rem;
padding: 0.5rem;
background-color: var(--deeper-background-color);
border-radius: 4px;
border-left: 3px solid var(--link-color);
`;
storyDiv.innerHTML = story;
koohiiContainer.appendChild(storyDiv);
});
contentDiv.appendChild(koohiiContainer);
}
container.appendChild(contentDiv);
// Insert the container after the main kanji content area
if (insertionPoint.classList && insertionPoint.classList.contains('result') && insertionPoint.classList.contains('kanji')) {
// Insert after the entire result kanji container (for review pages)
insertionPoint.parentNode.insertBefore(container, insertionPoint.nextSibling);
} else if (mnemonicSection && insertionPoint) {
// For kanji pages, insert after the mnemonic section
insertionPoint.parentNode.insertBefore(container, insertionPoint.nextSibling);
} else {
// Fallback insertion - append to the container
insertionPoint.appendChild(container);
}
}
function init() {
// Don't fetch on the front of review cards - only after "Show Answer"
if (window.location.href.includes('/review') && !document.querySelector('.review-reveal')) {
return;
}
const kanji = extractKanjiFromURL();
if (kanji) {
currentKanji = kanji;
console.log(`Found kanji for RTK lookup: ${kanji}`);
fetchRTKInfo(kanji);
}
}
// Run on page load
init();
// Observer for URL changes (for review pages)
const observer = new MutationObserver(() => {
if (window.location.href !== observer.lastUrl) {
observer.lastUrl = window.location.href;
setTimeout(init, 600);
}
});
observer.lastUrl = window.location.href;
observer.observe(document, { subtree: true, childList: true });
})();