WaniKani JJ External Definition

Get JJ External Definition from Weblio, Kanjipedia

目前为 2022-10-09 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name WaniKani JJ External Definition
  3. // @namespace http://www.wanikani.com
  4. // @version 0.12.6
  5. // @description Get JJ External Definition from Weblio, Kanjipedia
  6. // @author polv
  7. // @author NicoleRauch
  8. // @match *://www.wanikani.com/*/session*
  9. // @match *://www.wanikani.com/*vocabulary/*
  10. // @match *://www.wanikani.com/*kanji/*
  11. // @match *://www.wanikani.com/*radicals/*
  12. // @icon https://www.google.com/s2/favicons?sz=64&domain=weblio.jp
  13. // @require https://unpkg.com/dexie@3/dist/dexie.js
  14. // @require https://gf.qytechs.cn/scripts/430565-wanikani-item-info-injector/code/WaniKani%20Item%20Info%20Injector.user.js?version=1102710
  15. // @grant GM_xmlhttpRequest
  16. // @connect kanjipedia.jp
  17. // @connect weblio.jp
  18. // @homepage https://github.com/patarapolw/WanikaniExternalDefinition
  19. // ==/UserScript==
  20.  
  21. // @ts-check
  22. /// <reference path="./types/wanikani.d.ts" />
  23. /// <reference path="./types/item-info.d.ts" />
  24. /// <reference path="./types/gm.d.ts" />
  25. (function () {
  26. 'use strict';
  27.  
  28. /** @type {number | undefined} */
  29. const MAX_ENTRIES = 3;
  30. /** @type {number | undefined} */
  31. const HTML_MAX_CHAR = 10000;
  32.  
  33. const entryClazz = 'wkexternaldefinition';
  34.  
  35. const style = document.createElement('style');
  36. style.innerHTML = [
  37. '.' + entryClazz + ' a.crosslink {',
  38. ' color: #023e8a;',
  39. '}',
  40. '.' + entryClazz + ' a {',
  41. ' text-decoration: none;',
  42. '}',
  43. '.' + entryClazz + ' .kanji-variant {',
  44. ' display: inline-block;',
  45. ' text-align: center;',
  46. ' width: 100%;',
  47. ' font-size: 2em;',
  48. ' font-family: serif;',
  49. ' margin-top: 0;',
  50. ' margin-bottom: 0;',
  51. '}',
  52. '.' + entryClazz + ' .kanji-variant {',
  53. ' font-size: 2em;',
  54. '}',
  55. '.' + entryClazz + ' .kanji-variant img {',
  56. ' height: 2em;',
  57. '}',
  58. '.' + entryClazz + ' .kanji-variant + .kanji-variant {',
  59. ' margin-left: 1em;',
  60. '}',
  61. '.' + entryClazz + ' .okurigana {',
  62. ' color: #ab9b96;',
  63. '}',
  64. ].join('\n');
  65. document.head.appendChild(style);
  66. ///////////////////////////////////////////////////////////////////////////////////////////////////
  67.  
  68. // @ts-ignore
  69. const _Dexie = /** @type {typeof import('dexie').default} */ (Dexie);
  70. /**
  71. * @typedef {{ id: string; url: string; definition: string; reading: string; variant: string }} EntryKanjipedia
  72. * @typedef {{ id: string; url: string; definitions: string[] }} EntryWeblio
  73. */
  74.  
  75. class Database extends _Dexie {
  76. /** @type {import('dexie').Table<EntryKanjipedia, string>} */
  77. kanjipedia;
  78.  
  79. /** @type {import('dexie').Table<EntryWeblio, string>} */
  80. weblio;
  81.  
  82. constructor() {
  83. super(entryClazz);
  84. this.version(1).stores({
  85. kanjipedia: 'id,url',
  86. weblio: 'id,url',
  87. });
  88. }
  89. }
  90.  
  91. const db = new Database();
  92.  
  93. ///////////////////////////////////////////////////////////////////////////////////////////////////
  94. // Updating the kanji and vocab we are looking for
  95. /** @type {string | undefined} */
  96. let kanji;
  97. /** @type {string | undefined} */
  98. let vocab;
  99.  
  100. let isSuru = false;
  101.  
  102. let isSuffix = false;
  103. /** @type {string[]} */
  104. let reading = [];
  105.  
  106. let kanjipediaDefinition;
  107. let weblioDefinition;
  108. let kanjipediaReading;
  109.  
  110. function getCurrent() {
  111. // First, remove any already existing entries to avoid displaying entries for other items:
  112. $('.' + entryClazz).remove();
  113. kanji = undefined;
  114. vocab = undefined;
  115. reading = [];
  116.  
  117. kanjipediaDefinition = undefined;
  118. kanjipediaReading = undefined;
  119. weblioDefinition = undefined;
  120.  
  121. let key = 'currentItem';
  122. if (document.URL.includes('/lesson/session')) {
  123. key = $.jStorage.get('l/quizActive')
  124. ? 'l/currentQuizItem'
  125. : 'l/currentLesson';
  126. }
  127.  
  128. const current = $.jStorage.get(key);
  129. if (!current) return;
  130.  
  131. if ('voc' in current) {
  132. vocab = fixVocab(current.voc);
  133. reading = current.kana;
  134. } else if ('kan' in current && typeof current.kan === 'string') {
  135. kanji = current.kan;
  136. } else if ('rad' in current) {
  137. kanji = current.characters;
  138. }
  139.  
  140. updateInfo();
  141. }
  142.  
  143. if (typeof $ !== 'undefined') {
  144. $.jStorage.listenKeyChange('currentItem', getCurrent);
  145. $.jStorage.listenKeyChange('l/currentLesson', getCurrent);
  146. $.jStorage.listenKeyChange('l/currentQuizItem', getCurrent);
  147. $.jStorage.listenKeyChange('l/startQuiz', (key) => {
  148. if ($.jStorage.get(key)) {
  149. getCurrent();
  150. }
  151. });
  152.  
  153. getCurrent();
  154. }
  155.  
  156. /**
  157. *
  158. * @param {string} v
  159. * @returns
  160. */
  161. function fixVocab(v) {
  162. const suru = 'する';
  163. isSuru = v.endsWith(suru);
  164. if (isSuru) {
  165. v = v.substring(0, v.length - suru.length);
  166. }
  167.  
  168. const extMark = '〜';
  169. isSuffix = v.startsWith(extMark);
  170. if (isSuffix) {
  171. v = v.substring(extMark.length);
  172. }
  173.  
  174. return v.replace(/(.)々/g, '$1$1');
  175. }
  176.  
  177. ///////////////////////////////////////////////////////////////////////////////////////////////////
  178. /**
  179. * Loading the information and updating the webpage
  180. *
  181. * @returns {Promise<void>}
  182. */
  183. async function updateInfo() {
  184. /**
  185. *
  186. * @param {string} definition
  187. * @param {string} full_url
  188. * @param {string} name
  189. * @returns {string}
  190. */
  191. function insertDefinition(definition, full_url, name) {
  192. const output = document.createElement('div');
  193. output.className = entryClazz;
  194. output.lang = 'ja';
  195. output.innerHTML = definition;
  196.  
  197. const a = document.createElement('a');
  198. a.innerText = 'Click for full entry';
  199. a.href = full_url;
  200.  
  201. const p = document.createElement('p');
  202. p.style.marginTop = '0.5em';
  203. p.append(a);
  204. output.append(p);
  205.  
  206. output.querySelectorAll('a').forEach((a) => {
  207. a.target = '_blank';
  208. a.rel = 'noopener noreferrer';
  209. });
  210.  
  211. if (name === 'Kanjipedia') {
  212. kanjipediaDefinition = output;
  213. kanjipediaInserter.renew();
  214. } else {
  215. weblioDefinition = output;
  216. weblioInserter.renew();
  217. }
  218.  
  219. return output.outerHTML;
  220. }
  221.  
  222. /**
  223. *
  224. * @param {string} kanji
  225. * @returns {Promise<string>}
  226. */
  227. async function searchKanjipedia(kanji) {
  228. /**
  229. *
  230. * @param {EntryKanjipedia} r
  231. */
  232. const setContent = (r) => {
  233. kanjipediaReading = r.reading;
  234.  
  235. if (r.variant) {
  236. r.variant = r.variant.trim();
  237. if (!r.variant.startsWith('<')) {
  238. r.variant = `<div>${r.variant}</div>`;
  239. }
  240.  
  241. kanjipediaReading += [
  242. '<li>異体字</li>',
  243. `<div class="kanji-variant">${r.variant}<div>`,
  244. ].join('\n');
  245. }
  246.  
  247. kanjipediaReadingInserter.renew();
  248.  
  249. insertDefinition(
  250. r.definition
  251. .split('<br>')
  252. .map((s) => `<p>${s}</p>`)
  253. .join('\n'),
  254. r.url,
  255. 'Kanjipedia',
  256. );
  257.  
  258. return r.definition;
  259. };
  260.  
  261. const r = await db.kanjipedia.get(kanji);
  262. if (r) {
  263. return setContent(r);
  264. }
  265.  
  266. const kanjipediaUrlBase = 'https://www.kanjipedia.jp/';
  267. const regexImgSrc = /img src="/g;
  268. const replacementImgSrc = 'img width="16px" src="' + kanjipediaUrlBase;
  269. const regexTxtNormal = /class="txtNormal">/g;
  270. const replacementTxtNormal = '>.';
  271. const regexSpaceBeforeCircledNumber = / ([\u2460-\u2473])/g;
  272.  
  273. return new Promise((resolve, reject) => {
  274. function onerror(e) {
  275. (window.unsafeWindow || window).console.error(arguments);
  276. reject(e);
  277. }
  278.  
  279. GM_xmlhttpRequest({
  280. method: 'GET',
  281. url: kanjipediaUrlBase + 'search?k=' + kanji + '&kt=1&sk=leftHand',
  282. onerror,
  283. onload: function (data) {
  284. const firstResult = /** @type {HTMLAnchorElement} */ (
  285. $('<div />')
  286. .append(
  287. data.responseText.replace(regexImgSrc, replacementImgSrc),
  288. )
  289. .find('#resultKanjiList a')[0]
  290. );
  291. if (!firstResult) {
  292. resolve('');
  293. return;
  294. }
  295.  
  296. const rawKanjiURL = firstResult.href;
  297. const kanjiPageURL = kanjipediaUrlBase + rawKanjiURL.slice(25);
  298. GM_xmlhttpRequest({
  299. method: 'GET',
  300. url: kanjiPageURL,
  301. onerror,
  302. onload: function (data) {
  303. const rawResponseNode = $('<div />').append(
  304. data.responseText
  305. .replace(regexImgSrc, replacementImgSrc)
  306. .replace(regexTxtNormal, replacementTxtNormal)
  307. .replace(regexSpaceBeforeCircledNumber, '<br/>$1'),
  308. );
  309.  
  310. const readingNode = rawResponseNode.find(
  311. '#kanjiLeftSection #onkunList',
  312. );
  313. // Okurigana dot removal, so that it can be read as a vocabulary with Yomichan
  314. readingNode.find('span').each((_, it) => {
  315. const $it = $(it);
  316. const text = $it.text();
  317. if (text[0] === '.') {
  318. $it.text(text.substring(1));
  319. $it.addClass('okurigana').css('color', '#ab9b96');
  320. }
  321. });
  322.  
  323. const r = {
  324. id: kanji,
  325. url: kanjiPageURL,
  326. reading: readingNode.html(),
  327. definition: rawResponseNode
  328. .find('#kanjiRightSection p')
  329. .html(),
  330. variant: (() => {
  331. const vs = [
  332. ...rawResponseNode.find('#kanjiOyaji'),
  333. ...rawResponseNode.find('.subKanji'),
  334. ].filter(
  335. (n) => $(n).text() !== decodeURIComponent(kanji || ''),
  336. );
  337.  
  338. if (!vs.length) return '';
  339.  
  340. const $vs = $(vs).addClass('kanji-variant');
  341. $vs.find('img').removeAttr('width').css('height', '2em');
  342.  
  343. return $vs.html();
  344. })(),
  345. };
  346.  
  347. db.kanjipedia.add(r);
  348. resolve(setContent(r));
  349. },
  350. });
  351. },
  352. });
  353. });
  354. }
  355.  
  356. /**
  357. *
  358. * @param {string} vocab
  359. * @returns {Promise<string>}
  360. */
  361. async function searchWeblio(vocab) {
  362. /**
  363. *
  364. * @param {EntryWeblio} r
  365. */
  366. const setContent = (r) => {
  367. if (!r.definitions.length) return '';
  368. const vocabDefinition = r.definitions
  369. .sort((t1, t2) => {
  370. /**
  371. *
  372. * @param {string} t
  373. * @returns {number}
  374. */
  375. const fn = (t) => {
  376. if (/[[音訓]]/.exec(t)) return kanji ? -10 : 10;
  377.  
  378. const m = /読み方:([\p{sc=Katakana}\p{sc=Hiragana}ー]+)/u.exec(
  379. t,
  380. );
  381. if (m) {
  382. if (reading.length && !reading.includes(m[1])) return 5;
  383.  
  384. if (isSuffix) {
  385. if (t.includes('接尾')) return -1;
  386. }
  387.  
  388. if (isSuru) {
  389. if (t.includes('スル')) return -1;
  390. }
  391.  
  392. return 0;
  393. }
  394.  
  395. return 1000;
  396. };
  397. return fn(t1) - fn(t2);
  398. })
  399. .slice(0, MAX_ENTRIES)
  400. .map((html) => {
  401. const div = document.createElement('div');
  402. div.innerHTML = html.substring(0, HTML_MAX_CHAR);
  403. html = div.innerHTML;
  404. div.remove();
  405. return html;
  406. })
  407. .join('<hr>');
  408.  
  409. insertDefinition(vocabDefinition, r.url, 'Weblio');
  410. return vocabDefinition;
  411. };
  412.  
  413. const r = await db.weblio.get(vocab);
  414. if (r) {
  415. return setContent(r);
  416. }
  417.  
  418. const vocabPageURL = 'https://www.weblio.jp/content/' + vocab;
  419.  
  420. return new Promise((resolve, reject) => {
  421. function onerror(e) {
  422. (window.unsafeWindow || window).console.error(arguments);
  423. reject(e);
  424. }
  425.  
  426. GM_xmlhttpRequest({
  427. method: 'GET',
  428. url: vocabPageURL,
  429. onerror,
  430. onload: function (data) {
  431. if (!data.responseText) {
  432. resolve('');
  433. return;
  434. }
  435.  
  436. const div = document.createElement('div');
  437. div.innerHTML = data.responseText;
  438. const definitions = Array.from(div.querySelectorAll('.kiji'))
  439. .flatMap((el) => {
  440. return Array.from(el.children).filter(
  441. (el) => el instanceof HTMLDivElement,
  442. );
  443. })
  444. .map((el) => {
  445. if (el instanceof HTMLElement) {
  446. if (el.querySelector('script')) return '';
  447. return el.innerHTML;
  448. }
  449. return '';
  450. })
  451. .filter((s) => s);
  452. div.remove();
  453.  
  454. if (!definitions.length) {
  455. resolve('');
  456. return;
  457. }
  458.  
  459. const r = {
  460. id: vocab,
  461. url: vocabPageURL,
  462. definitions,
  463. };
  464.  
  465. db.weblio.add(r);
  466. resolve(setContent(r));
  467. },
  468. });
  469. });
  470. }
  471.  
  472. if (kanji) {
  473. await Promise.allSettled([searchKanjipedia(kanji), searchWeblio(kanji)]);
  474. } else if (vocab) {
  475. await searchWeblio(vocab);
  476. }
  477. }
  478.  
  479. ///////////////////////////////////////////////////////////////////////////////////////////////////
  480. // Triggering updates on lessons and reviews
  481.  
  482. const kanjipediaInserter = wkItemInfo
  483. .on('lesson,lessonQuiz,review,extraStudy,itemPage')
  484. .under('meaning')
  485. .notify((state) => {
  486. if (!(kanji && kanji === state.characters)) {
  487. return;
  488. }
  489.  
  490. if (!kanjipediaDefinition) return;
  491.  
  492. const title = 'Kanjipedia Explanation';
  493. if (
  494. state.on === 'itemPage' ||
  495. (state.type === 'radical' && state.on === 'lesson')
  496. ) {
  497. state.injector.append(title, kanjipediaDefinition);
  498. } else {
  499. state.injector.appendAtTop(title, kanjipediaDefinition);
  500. }
  501. });
  502.  
  503. const weblioInserter = wkItemInfo
  504. .on('lesson,lessonQuiz,review,extraStudy,itemPage')
  505. .under('meaning')
  506. .notify((state) => {
  507. if (state.on === 'itemPage') {
  508. if (state.type === 'vocabulary') {
  509. if (!vocab) {
  510. vocab = state.characters;
  511. reading = state.reading;
  512. updateInfo();
  513. return;
  514. }
  515. } else {
  516. if (!kanji) {
  517. kanji = state.characters;
  518. updateInfo();
  519. return;
  520. }
  521. }
  522. }
  523.  
  524. if (state.type === 'vocabulary') {
  525. if (state.characters !== vocab) return;
  526. } else if (!(kanji && kanji === state.characters)) {
  527. return;
  528. }
  529.  
  530. if (!weblioDefinition) return;
  531.  
  532. const title = 'Weblio Explanation';
  533. if (
  534. state.on === 'itemPage' ||
  535. (state.type === 'radical' && state.on === 'lesson')
  536. ) {
  537. state.injector.append(title, weblioDefinition);
  538. } else {
  539. state.injector.appendAtTop(title, weblioDefinition);
  540. }
  541. });
  542.  
  543. const kanjipediaReadingInserter = wkItemInfo
  544. .on('lesson,lessonQuiz,review,extraStudy,itemPage')
  545. .forType('kanji')
  546. .under('reading')
  547. .notify((state) => {
  548. if (!(kanji && kanji === state.characters)) {
  549. return;
  550. }
  551.  
  552. if (!kanjipediaReading) return;
  553. const id = [entryClazz, 'kanjipedia', 'reading'].join('--');
  554. $('#' + id).remove();
  555.  
  556. if (state.on === 'lesson') {
  557. $('#supplement-kan-reading:visible .pure-u-1-4 > div')
  558. .first()
  559. .after(
  560. '<span id="' +
  561. id +
  562. '" lang="ja" class="' +
  563. entryClazz +
  564. ' ' +
  565. entryClazz +
  566. '-reading' +
  567. '"><h2 style="margin-top: 1.25em;">Kanjipedia</h2>' +
  568. kanjipediaReading +
  569. '</span>',
  570. );
  571. } else if (state.on === 'itemPage') {
  572. $('.span4')
  573. .removeClass('span4')
  574. .addClass('span3')
  575. .last()
  576. .after(
  577. '<div id="' +
  578. id +
  579. '" lang="ja" class="span3 ' +
  580. entryClazz +
  581. ' ' +
  582. entryClazz +
  583. '-reading' +
  584. '"><h3>Kanjipedia</h3>' +
  585. kanjipediaReading +
  586. '</div>',
  587. );
  588. } else {
  589. $('#item-info #item-info-col1 #item-info-reading:visible').after(
  590. '<section id="' +
  591. id +
  592. '" lang="ja" class="' +
  593. entryClazz +
  594. ' ' +
  595. entryClazz +
  596. '-reading' +
  597. '"><h2>Kanjipedia</h2>' +
  598. kanjipediaReading +
  599. '</section>',
  600. );
  601. }
  602. });
  603. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址