您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Get JJ External Definition from Weblio, Kanjipedia
// ==UserScript== // @name WaniKani JJ External Definition // @namespace http://www.wanikani.com // @version 1.4.2 // @description Get JJ External Definition from Weblio, Kanjipedia // @author polv // @author NicoleRauch // @match *://www.wanikani.com/* // @match *://preview.wanikani.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=weblio.jp // @license MIT // @require https://unpkg.com/dexie@3/dist/dexie.js // @require https://greasyfork.org/scripts/430565-wanikani-item-info-injector/code/WaniKani%20Item%20Info%20Injector.user.js?version=1276163 // @grant GM_xmlhttpRequest // @connect kanjipedia.jp // @connect weblio.jp // @homepage https://github.com/patarapolw/wanikani-userscript/blob/master/userscripts/external-definition.user.js // ==/UserScript== // @ts-check /// <reference path="./types/item-info.d.ts" /> /// <reference path="./types/gm.d.ts" /> (function () { 'use strict'; const AFTER_EN_MEANING = false; /** @type {number | undefined} */ const MAX_ENTRIES = 3; /** @type {number | undefined} */ const HTML_MAX_CHAR = 10000; const entryClazz = 'wkexternaldefinition'; const style = document.createElement('style'); style.appendChild( document.createTextNode(/* css */ ` .${entryClazz} { --kanji-variant-size: 64px; } .${entryClazz} details { margin-top: 1em; } .${entryClazz} details summary { display: revert; margin-bottom: 1em; cursor: pointer; } .${entryClazz} .spoiler:not(:hover), .${entryClazz} .spoiler:not(:hover) * { background-color: #ccc; color: #ccc; text-shadow: none; } .${entryClazz} .keep-10em { display: inline-block; width: 10em; min-width: fit-content; max-width: 100%; } /* Weblio fixes */ .${entryClazz} p { margin-bottom: 0.5em; } .${entryClazz} a.crosslink { color: #023e8a; } .${entryClazz} a { text-decoration: none; } .${entryClazz} a.external { text-decoration: underline; } .${entryClazz} ol { list-style: revert; padding: revert; } .${entryClazz} ul { list-style: revert; padding: revert; } .${entryClazz} .wnryjNotice { border: #b5b6b5 solid 1px; font-size: 0.8em; line-height: 1.32em; margin: 16px 0 0 0; padding: 10px; width: auto; } .${entryClazz} .SgkdjImg img { width: 40%; height: 40%; } .${entryClazz} .synonymsUnderDictWrp { margin-top: 1em; } .${entryClazz} .synonymsUnderDict { background-color: #f7f7f7; clear: both; margin: 0 0 0 8px; padding: 2px 8px; } .${entryClazz} .synonymsUnderDict a { padding-right: 1em; } .${entryClazz} .tssmjC { background-color: #f0f0f0; border: #666666 solid 1px; color: #363636; font-size: 0.9em; line-height: 1.0em; margin-right: 5px; padding: 1px; } /* Kanjipedia fixes */ .${entryClazz}-kanjipedia-reading-horizontal * { display: inline-block; } .${entryClazz}-kanjipedia-reading-horizontal li { margin-right: 1em; } .${entryClazz}-kanjipedia-reading-horizontal .kanji-variant-header { display: none; } .${entryClazz} .kanji-variant { display: flex; flex-direction: row; align-items: center; justify-content: center; width: 100%; font-size: var(--kanji-variant-size, 64px); font-family: "HiraMinProN-W3", "Hiragino Mincho ProN W3", "Hiragino Mincho ProN", "ヒラギノ明朝 ProN W3", "游明朝", YuMincho, "HG明朝E", "MS P明朝", "MS PMincho", "MS 明朝", "MS Mincho", serif; /* Font list from Jisho.org */ margin-top: 0; margin-bottom: 0; } .${entryClazz} .kanji-variant img { height: var(--kanji-variant-size, 64px); } .${entryClazz} .kanji-variant + .kanji-variant { margin-left: 1em; } .${entryClazz} .okurigana { color: #ab9b96; } @media only screen and (min-width: 768px) { .subject-readings__reading { flex: 1; } } `), ); document.head.appendChild(style); const radicalMap = { Gun: ['𠂉'], Leaf: ['丆'], Beggar: ['丂'], Spikes: ['业'], Kick: ['𧘇'], Viking: ['𤇾', '𦥯'], Cape: ['𠃌'], Hills: [], Gladiator: ['龹'], Pope: [], Spring: ['𡗗'], Squid: ['㑒', '僉'], Yurt: [], Chinese: ['𦰩', '堇'], Bear: ['㠯'], Blackjack: ['龷'], Trash: ['𠫓'], Tofu: [], Creeper: [], Bar: ['㦮', '戔'], Saw: ['巩'], Zombie: ['袁'], Explosion: [], Morning: ['𠦝', '龺'], 'Death Star': ['俞'], Comb: [], Elf: [], Coral: ['丞'], Cactus: [], Satellite: ['䍃'], Psychopath: ['鬯'], // Except this one; but it's a smaller radical not elsewhere described. }; /////////////////////////////////////////////////////////////////////////////////////////////////// // @ts-ignore const _Dexie = /** @type {typeof import('dexie').default} */ (Dexie); /** * @typedef {{ id: string; url: string; definition: string; reading: string; variant: string }} EntryKanjipedia * @typedef {{ id: string; url: string; definitions: string[] }} EntryWeblio */ class Database extends _Dexie { /** @type {import('dexie').Table<EntryKanjipedia, string>} */ kanjipedia; /** @type {import('dexie').Table<EntryWeblio, string>} */ weblio; constructor() { super(entryClazz); this.version(1).stores({ kanjipedia: 'id,url', weblio: 'id,url', }); } } const db = new Database(); /////////////////////////////////////////////////////////////////////////////////////////////////// // Updating the kanji and vocab we are looking for /** @type {string | undefined} */ let kanji; /** @type {string | undefined} */ let vocab; let isSuru = false; let isSuffix = false; /** @type {string[]} */ let reading = []; let kanjipediaDefinition; let weblioDefinition; let kanjipediaReading; let qType = ''; let sType = ''; window.addEventListener('willShowNextQuestion', (e) => { // First, remove any already existing entries to avoid displaying entries for other items: document.querySelectorAll('.' + entryClazz).forEach((el) => el.remove()); kanji = undefined; vocab = undefined; reading = []; qType = ''; kanjipediaDefinition = undefined; kanjipediaReading = undefined; weblioDefinition = undefined; if ('detail' in e) { const { subject, questionType } = /** @type {any} */ (e.detail); qType = questionType; sType = subject.subject_category || subject.type; if (sType === 'Vocabulary') { vocab = fixVocab(subject.characters); reading = subject.readings ? subject.readings.map((r) => r.reading) : [subject.characters]; } else { kanji = typeof subject.characters === 'string' ? subject.characters : getRadicalKanji(subject.meanings); } } updateInfo(); }); /** * * @param {string} v * @returns */ function fixVocab(v) { const suru = 'する'; isSuru = v.endsWith(suru) && v !== suru; if (isSuru) { v = v.substring(0, v.length - suru.length); reading = reading.map((r) => r.replace(new RegExp(suru + '$'), '')); } const extMark = '〜'; isSuffix = v.startsWith(extMark); if (isSuffix) { v = v.substring(extMark.length); } return v.replace(/(.)々/g, '$1$1'); } /////////////////////////////////////////////////////////////////////////////////////////////////// /** * Loading the information and updating the webpage * * @returns {Promise<void>} */ async function updateInfo() { /** * * @param {string} definition * @param {string} full_url * @param {string} name * @returns {string} */ function insertDefinition(definition, full_url, name) { const output = document.createElement('div'); output.className = entryClazz; output.lang = 'ja'; output.innerHTML = definition; if (full_url) { const a = document.createElement('a'); a.className = 'external'; a.innerText = 'Click for full entry'; a.href = full_url; const p = document.createElement('p'); p.style.marginTop = '0.5em'; p.append(a); output.append(p); } output.querySelectorAll('a').forEach((a) => { a.target = '_blank'; a.rel = 'noopener noreferrer'; }); if (name === 'Kanjipedia') { kanjipediaDefinition = output; kanjipediaInserter.renew(); } else { weblioDefinition = output; weblioInserter.renew(); } return output.outerHTML; } /** * * @param {string} kanji * @returns {Promise<string>} */ async function searchKanjipedia(kanji) { /** * * @param {EntryKanjipedia} r */ const setContent = (r) => { kanjipediaReading = r.reading; let htmlVar = ''; if (r.variant) { r.variant = r.variant.trim(); if (!r.variant.startsWith('<')) { r.variant = `<div>${r.variant}</div>`; } const el = document.createElement('div'); el.innerHTML = r.variant; el.querySelectorAll('img').forEach((it) => { it.removeAttribute('style'); }); htmlVar = [ '<li class="kanji-variant-header">異体字</li>', `<div class="kanji-variant">${el.innerHTML}</div>`, ].join('\n'); kanjipediaReading += htmlVar; } kanjipediaReadingInserter.renew(); return insertDefinition( (qType === 'meaning' && sType !== 'Radical' ? htmlVar : `<ul class="${entryClazz}-kanjipedia-reading-horizontal">${kanjipediaReading}</ul>`) + r.definition .split('<br>') .map((s) => `<p>${s}</p>`) .join('\n'), r.url, 'Kanjipedia', ); }; const r = await db.kanjipedia.get(kanji); if (r) { return setContent(r); } const kanjipediaUrlBase = 'https://www.kanjipedia.jp/'; const regexImgSrc = /img src="/g; const replacementImgSrc = 'img width="16px" src="' + kanjipediaUrlBase; const regexTxtNormal = /class="txtNormal">/g; const replacementTxtNormal = '>.'; const regexSpaceBeforeCircledNumber = / ([\u2460-\u2473])/g; return new Promise((resolve, reject) => { function onerror(e) { (window.unsafeWindow || window).console.error(arguments); reject(e); } GM_xmlhttpRequest({ method: 'GET', url: kanjipediaUrlBase + 'search?k=' + kanji + '&kt=1&sk=leftHand', onerror, onload: function (data) { const div = document.createElement('div'); div.innerHTML = data.responseText.replace( regexImgSrc, replacementImgSrc, ); const firstResult = /** @type {HTMLAnchorElement} */ ( div.querySelector('#resultKanjiList a') ); if (!firstResult) { resolve(''); return; } const rawKanjiURL = firstResult.href; const kanjiPageURL = kanjipediaUrlBase + rawKanjiURL.slice(25); GM_xmlhttpRequest({ method: 'GET', url: kanjiPageURL, onerror, onload: function (data) { const rawResponseNode = document.createElement('div'); rawResponseNode.innerHTML = data.responseText .replace(regexImgSrc, replacementImgSrc) .replace(regexTxtNormal, replacementTxtNormal) .replace(regexSpaceBeforeCircledNumber, '<br/>$1'); const readingNode = rawResponseNode.querySelector( '#kanjiLeftSection #onkunList', ); if (!readingNode) return; // Okurigana dot removal, so that it can be read as a vocabulary with Yomichan readingNode.querySelectorAll('span').forEach((it) => { const text = it.innerText; if (text[0] === '.') { it.innerText = text.substring(1); it.classList.add('okurigana'); it.style.color = '#ab9b96'; } }); const r = { id: kanji, url: kanjiPageURL, reading: readingNode.innerHTML, definition: Array.from( rawResponseNode.querySelectorAll('#kanjiRightSection p'), ) .map((p) => p.innerHTML) .join('\n'), variant: (() => { const vs = [ ...rawResponseNode.querySelectorAll('#kanjiOyaji'), ...rawResponseNode.querySelectorAll('.subKanji'), ].filter( (n) => n.textContent !== decodeURIComponent(kanji || ''), ); if (!vs.length) return ''; vs.map((v) => { v.classList.add('kanji-variant'); v.querySelectorAll('img').forEach((img) => { img.removeAttribute('width'); }); }); return vs.map((v) => v.innerHTML).join('\n'); })(), }; db.kanjipedia.add(r); resolve(setContent(r)); }, }); }, }); }); } /** * * @param {string} vocab * @returns {Promise<string>} */ async function searchWeblio(vocab) { /** * * @param {EntryWeblio} [r] */ const setContent = (r) => { if (!r || !r.definitions.length) { if (kanji) { return insertDefinition( `No entries found. Try <a class="external" href="https://en.wiktionary.org/wiki/${kanji}" target="_blank" rel="noopener noreferrer">${kanji} - Wiktionary</a>`, '', 'Wiktionary', ); } return ''; } const reYomi = /(読み方:)([\p{sc=Katakana}\p{sc=Hiragana}ー]+)/gu; const makeYomiSpoiler = (s) => qType === 'meaning' && sType !== 'Radical' ? s .replace(reYomi, '$1<span class="spoiler keep-10em">$2</span>') .replace(/<p(>.*?[[音訓]].*?<\/p>)/s, '<p class="spoiler"$1') : s; const sortedDef = r.definitions .sort((t1, t2) => { /** * * @param {string} t * @returns {number} */ const fn = (t) => { let isKanji = /[[音訓]]/.exec(t); if (kanji && isKanji) return -10; reYomi.lastIndex = 0; const m = reYomi.exec(t); if (m) { if (!reading.length) return 0; if (isKanji) return reading.length; let readingIdx = reading.indexOf(m[2]); if (readingIdx === -1) return 100; if (isSuffix && t.includes('接尾')) { readingIdx -= 0.5; } if (isSuru && t.includes('スル')) { readingIdx -= 0.5; } return readingIdx; } return 1000; }; return fn(t1) - fn(t2); }) .map((html) => { if (!HTML_MAX_CHAR || html.length < HTML_MAX_CHAR) { return makeYomiSpoiler(html); } const div = document.createElement('div'); div.innerHTML = makeYomiSpoiler(html.substring(0, HTML_MAX_CHAR)); const mark = document.createElement('mark'); mark.style.cursor = 'pointer'; mark.setAttribute('data-html', html); mark.textContent = '...'; html = div.outerHTML.replace( /<\/div>$/, mark.outerHTML.replace( /^<mark /, '$&' + 'onclick="parentElement.innerHTML=getAttribute(\'data-html\')" ', ) + '$&', ); div.remove(); return html; }); let vocabDefinition = sortedDef.splice(0, MAX_ENTRIES).join('<hr>'); if (sortedDef.length) { vocabDefinition += `<details><summary>Show more</summary>${sortedDef.join( '<hr>', )}</details>`; } return insertDefinition(vocabDefinition, r.url, 'Weblio'); }; const r = await db.weblio.get(vocab); if (r) { return setContent(r); } const vocabPageURL = 'https://www.weblio.jp/content/' + vocab; return new Promise((resolve, reject) => { function onerror(e) { (window.unsafeWindow || window).console.error(arguments); setContent(); reject(e); } GM_xmlhttpRequest({ method: 'GET', url: vocabPageURL, onerror, onload: function (data) { if (!data.responseText) { resolve(setContent()); return; } const div = document.createElement('div'); div.innerHTML = data.responseText; const definitions = Array.from(div.querySelectorAll('.kiji')) .flatMap((el) => { return Array.from(el.children).filter( (el) => el instanceof HTMLDivElement, ); }) .map((el) => { if (el instanceof HTMLElement) { if (el.querySelector('script')) return ''; return el.innerHTML; } return ''; }) .filter((s) => s); div.remove(); if (!definitions.length) { resolve(setContent()); return; } const r = { id: vocab, url: vocabPageURL, definitions, }; db.weblio.add(r); resolve(setContent(r)); }, }); }); } if (kanji) { await Promise.allSettled([searchKanjipedia(kanji), searchWeblio(kanji)]); } else if (vocab) { await searchWeblio(vocab); } } /////////////////////////////////////////////////////////////////////////////////////////////////// // Triggering updates on lessons and reviews const kanjipediaInserter = wkItemInfo .on('lesson,lessonQuiz,review,extraStudy,itemPage') .forType('kanji,radical') .under('meaning') .spoiling('meaning') .notify((state) => { if ( !( kanji && (kanji === state.characters || kanji === getRadicalKanji(state.meaning)) ) ) { return; } if (!kanjipediaDefinition) return; const title = 'Kanjipedia Explanation'; if ( AFTER_EN_MEANING || state.on === 'itemPage' || (state.type === 'radical' && state.on === 'lesson') ) { state.injector.append(title, kanjipediaDefinition); } else { state.injector.appendAtTop(title, kanjipediaDefinition); } }); const weblioInserter = wkItemInfo .on('lesson,lessonQuiz,review,extraStudy,itemPage') .under('meaning') .spoiling('meaning') .notify((state) => { if (state.on === 'itemPage') { qType = ''; } const isVocabulary = state.type .toLocaleLowerCase() .endsWith('vocabulary'); let fixedCharacters = state.characters; if (isVocabulary) { fixedCharacters = fixVocab(state.characters); } if (state.on === 'itemPage') { if (isVocabulary) { kanji = ''; if (vocab !== fixedCharacters) { reading = state.reading; vocab = fixedCharacters; updateInfo(); return; } } else { vocab = ''; reading = []; const newKanji = state.characters || getRadicalKanji(state.meaning); if (kanji !== newKanji) { kanji = newKanji; updateInfo(); return; } if (!kanji) return; } } else { if (isVocabulary) { if (fixedCharacters !== vocab) return; } else if (kanji) { if ( typeof state.characters === 'string' ? kanji !== state.characters : kanji !== getRadicalKanji(state.meaning) ) return; } } if (!weblioDefinition) return; const title = 'Weblio Explanation'; if ( AFTER_EN_MEANING || state.on === 'itemPage' || (state.type === 'radical' && state.on === 'lesson') ) { state.injector.append(title, weblioDefinition); } else { state.injector.appendAtTop(title, weblioDefinition); } }); let kanjipediaReadingPanelInterval = 0; const kanjipediaReadingInserter = wkItemInfo .on('lesson,lessonQuiz,review,extraStudy,itemPage') .forType('kanji') .under('reading') .notify((state) => { if (!(kanji && kanji === state.characters)) { return; } if (!kanjipediaReading) return; clearInterval(kanjipediaReadingPanelInterval); if (state.on === 'itemPage') { document .querySelectorAll(`.${entryClazz}-reading`) .forEach((el) => el.remove()); const dst = document.querySelector('.subject-readings'); if (dst) { const el = document.createElement('div'); el.className = `subject-readings__reading subject-readings__reading--primary ${entryClazz} ${entryClazz}-reading`; const h = document.createElement('h3'); h.className = 'subject-readings__reading-title'; h.innerText = 'Kanjipedia'; const content = document.createElement('div'); content.className = 'subject-readings__reading-items'; content.lang = 'ja'; content.innerHTML = kanjipediaReading; el.append(h, content); dst.append(el); } } else { kanjipediaReadingPanelInterval = setInterval(() => { const node = document.querySelector('.subject-readings'); if (node) { if (node.querySelector(`.${entryClazz}`)) { return clearInterval(kanjipediaReadingPanelInterval); } node.insertAdjacentHTML( 'beforeend', '<div class="subject-readings__reading subject-readings__reading--primary ' + entryClazz + ' ' + entryClazz + '-reading' + '"><h3 class="subject-readings__reading-title">Kanjipedia</h3>' + `<p class="subject-readings__reading-items" lang="ja">${kanjipediaReading}</p>` + '</div>', ); return clearInterval(kanjipediaReadingPanelInterval); } }, 100); } }); /** * * @param {string[]} meanings * @returns {string | undefined} */ function getRadicalKanji(meanings) { const [en] = meanings; if (!en) return; const ks = radicalMap[en]; if (!ks) return; console.log( `${entryClazz}: ${ ks.length ? `converted ${en} to ${ks.join(', ')}` : `cannot convert ${en} to Kanji` }`, ); return ks[0]; } })();