WaniKani Please Check Spelling

Plural-accepting no-misspelling script (No Cigar)

当前为 2023-05-20 提交的版本,查看 最新版本

// ==UserScript==
// @name         WaniKani Please Check Spelling
// @namespace    http://www.wanikani.com
// @version      0.3.0
// @description  Plural-accepting no-misspelling script (No Cigar)
// @author       polv
// @match        https://www.wanikani.com/extra_study/session*
// @match        https://www.wanikani.com/review/session*
// @match        https://www.wanikani.com/subjects/*
// @match        https://preview.wanikani.com/extra_study/session*
// @match        https://preview.wanikani.com/review/session*
// @match        https://preview.wanikani.com/subjects/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=wanikani.com
// @license      MIT
// @homepage     https://gf.qytechs.cn/en/scripts/465750-wanikani-please-check-spelling
// @supportURL   https://community.wanikani.com/t/userscript-plz-check-spelling-no-cigar-but-accept-plural-and-no-space-variants/61763
// @source       https://github.com/patarapolw/wanikani-userscript/blob/master/userscripts/plz-check-spelling.user.js
// @grant        none
// ==/UserScript==

// @ts-check
(function () {
  'use strict';

  /** @typedef {'whitelist' | 'blacklist' | 'warning'} AuxiliaryType */

  /**
   * @typedef {{
   *   questionType: string
   *   item: {
   *     type: string
   *     characters: string
   *     readings?: string[]
   *     auxiliary_readings?: {
   *       reading: string
   *       type: AuxiliaryType
   *     }[]
   *     meanings: string[]
   *     auxiliary_meanings: {
   *       meaning: string
   *       type: AuxiliaryType
   *     }[]
   *   }
   *   userSynonyms: string[]
   *   response: string
   * }} EvaluationParam
   */

  /**
   * @typedef {{
   *   action: 'pass' | 'fail' | 'retry'
   *   message: null | {
   *     text: string
   *     type: 'itemInfoException' | 'answerException'
   *   }
   * }} Evaluation
   */

  /** @typedef {((e: EvaluationParam) => Evaluation)} EvaluationFunction */
  /** @typedef {((e: EvaluationParam, check: EvaluationFunction) => Evaluation | null)} TryEvaluationFunction */

  class ModAnswerChecker {
    /**
     * @type {TryEvaluationFunction[]}
     */
    mods = [];

    /**
     *
     * @param {TryEvaluationFunction} fn
     */
    register(fn) {
      this.mods.push(fn);
    }

    constructor() {
      // Automatically init on new instance
      this.init();
    }

    async init() {
      const answerChecker = await this.getAnswerChecker(60000);

      answerChecker.oldEvaluate = answerChecker.evaluate.bind(answerChecker);

      /** @type {(fns: TryEvaluationFunction[]) => EvaluationFunction} */
      const evaluateWith = (fns) => {
        return (e) => {
          for (const fn of fns) {
            const r = fn(e, evaluateWith(fns.filter((it) => it !== fn)));
            if (r) return r;
          }
          return answerChecker.oldEvaluate(e);
        };
      };

      answerChecker.evaluate = evaluateWith(this.mods);
    }

    /**
     * Get answerChecker Object
     * @param {number} timeout
     * @returns {Promise<{
     *   oldEvaluate: EvaluationFunction
     *   evaluate: EvaluationFunction
     * }>}
     */
    async getAnswerChecker(timeout) {
      //Stimulus.controllers.filter((x)=>{return x.answerChecker;})[0]
      const start = Date.now();

      function waitForAnswerChecker(resolve, reject) {
        // @ts-ignore
        const Stimulus = window.Stimulus;
        if (
          Stimulus &&
          Stimulus.controllers.filter((x) => {
            return x.answerChecker;
          })[0]
        ) {
          var answerChecker = Stimulus.controllers.filter((x) => {
            return x.answerChecker;
          })[0].answerChecker;
          resolve(answerChecker);
        } else if (timeout && Date.now() - start >= timeout)
          reject(new Error('timeout'));
        else setTimeout(waitForAnswerChecker.bind(this, resolve, reject), 30);
      }

      return new Promise(waitForAnswerChecker);
    }
  }

  // @ts-ignore
  window.modAnswerChecker = window.modAnswerChecker || new ModAnswerChecker();
  /** @type {ModAnswerChecker} */
  // @ts-ignore
  const modAnswerChecker = window.modAnswerChecker;

  //////////////////////////////////////////////////////////////////////////////

  /**
   * !Okurigana Matcher section
   * @see https://community.wanikani.com/t/do-you-even-kana-okurigana-matcher/8440
   */
  modAnswerChecker.register((e) => {
    if (e.questionType === 'reading' && e.item.type === 'Vocabulary') {
      if (!makeRegex(e.item.characters).test(e.response.trim())) {
        return {
          action: 'retry',
          message: {
            text: 'Bro, Do you even Kana?',
            type: 'answerException',
          },
        };
      }
    }
    return null;
  });

  //Create regex profiles (katakana matches need hiragana counterparts included)
  /** Prepends Hiragana counterpart to any Katakana string input
   * @param {String} char - A one character long string that may be a Katakana character
   * @returns {String} A single character if the input is Hiragana or "ー"; A two character string of (hopefully) Hiragana-Katakana pairs in square brackets (that can form a regex) if not.
   * @bug Will attempt to pair any character that is not Hiragana or "ー"
   */
  function pairKatakana(char) {
    if (/^[\u3040-\u309fー]$/.test(char)) {
      //is char hiragana or "ー"?
      return char;
    } else {
      //set up pairs
      var offset = -6 * 16; //katakana block: 30a0-30ff
      var katakana = String.fromCharCode(char.charCodeAt(0) + offset);
      return '[' + char + katakana + ']';
    }
  }

  /** Returns true if the character is Kana
   * @param {String} char
   */
  function isKana(char) {
    return /^[\u3040-\u30ff]$/.test(char);
  }

  /** Creates regex from a vocabulary item that matches the Kana in that item.
   * @param {string} cV
   */
  function makeRegex(cV) {
    var r = '^'; //start the regex string
    for (var c = 0; c < cV.length; c++) {
      if (isKana(cV[c])) {
        r += pairKatakana(cV[c]);
      } else {
        //we have a non-kana character
        if (cV[c] !== '〜') {
          //I doubt WK will be adding Kana suffixes but just covering all the bases to be safe.
          r += '(.+)'; // unknown number of characters in reading (corresponding to kanji), capturing in groups for versatility
          while (c < cV.length && !isKana(cV[c + 1])) {
            c++; //skip non-kana characters (already have ".+" in our regex, do not need to add more)
          }
        }
      }
    }
    r += '$'; // End of regex
    return new RegExp(r);
  }

  //////////////////////////////////////////////////////////////////////////////

  /**
   * !No cigar section
   * @see https://community.wanikani.com/t/userscript-plz-check-spelling-no-cigar-but-accept-plural-and-no-space-variants/61763
   * @see https://community.wanikani.com/t/userscript-prevent-your-answer-was-a-bit-off-answers-from-being-accepted-aka-close-but-no-cigar/7134
   */
  modAnswerChecker.register((e, tryCheck) => {
    if (isWrongAnswer) {
      return {
        action: 'fail',
        message: null,
      };
    }

    if (e.questionType !== 'reading') {
      const result = tryCheck(e);
      if (isForcedAccept) return result;

      console.log(result, e);

      if (
        result.action === 'pass' &&
        result.message?.type === 'itemInfoException'
      ) {
        const { meanings = [], auxiliary_meanings = [] } = e.item;
        const { userSynonyms = [] } = e;

        const re = new RegExp(
          `^\\W*(${[
            ...meanings,
            ...userSynonyms,
            ...auxiliary_meanings
              .filter((m) => m.type === 'whitelist')
              .map((m) => m.meaning),
          ]
            .map((m) => {
              m = m.toLocaleLowerCase();

              const tokens = m.split(/\W+/g);
              const isVerb = tokens[0] === 'to';

              const out = [];

              tokens.map((t, i) => {
                let ed = '\\W*';

                if (
                  ['to', 'in', 'on', 'at', 'of', 'and', 'with', 'be'].includes(
                    t,
                  )
                ) {
                  ed = '\\W+';
                } else if (['something', 'a', 'an', 'the'].includes(t)) {
                  t = `(${t})?`;
                } else {
                  t = makePlural(t);
                }

                out.push(t);
                if (i < tokens.length - 1) {
                  out.push(ed);
                }
              });
              return out.join('');
            })
            .join('|')})\\W*$`,
          'i',
        );
        console.log(re);

        if (!re.test(e.response.toLocaleLowerCase().trim())) {
          return {
            action: 'retry',
            message: {
              text: 'Close, but no cigar! Please try again',
              type: 'answerException',
            },
          };
        }
      }
    }

    return null;
  });

  /**
   *
   * @param {string} s
   * @returns
   */
  function makePlural(s) {
    if (s.length > 2) {
      const yPlural = ['y', 'ys', 'ies'];
      for (const p of yPlural) {
        if (s.endsWith(p)) {
          return s.substring(0, s.length - p.length) + `(${yPlural.join('|')})`;
        }
      }

      const sPlural = ['s', 'es'];
      for (const p of sPlural) {
        if (s.endsWith(p)) {
          return s.substring(0, s.length - p.length) + `(${p})?`;
        }
      }

      return s + `(${sPlural.join('|')})?`;
    }

    return s;
  }

  /** @type {HTMLInputElement | null} */
  let inputContainer = null;
  let qType = '';
  let isWrongAnswer = false;
  let isForcedAccept = false;

  addEventListener('willShowNextQuestion', (e) => {
    // @ts-ignore
    const { questionType } = e.detail;
    qType = questionType;

    isWrongAnswer = false;

    if (!inputContainer) {
      inputContainer = document.querySelector('input[name="user-response"]');
      if (inputContainer) {
        const el = inputContainer;
        el.addEventListener('keydown', (ev) => {
          if (el.getAttribute('enabled') !== 'true') return;
          if (ev.key === 'Escape' || ev.code === 'Escape') {
            // https://community.wanikani.com/t/userscript-i-dont-know-button/7231
            const msg =
              qType === 'reading'
                ? 'えぇぇーさっぱりわからないぃぃぃ'
                : 'Aargh! What does that even mean? (╯°□°)╯︵ ┻━┻';

            if (el.value === msg) {
              el.value = '';
              isWrongAnswer = false;
            } else {
              el.value = msg;
              isWrongAnswer = true;
            }

            // manual submit
          } else if (ev.key === 'Enter') {
            isForcedAccept = ev.shiftKey || ev.ctrlKey;
          } else if (ev.code.startsWith('Key')) {
            isWrongAnswer = false;
          }
        });
      }
    }
  });
})();

QingJ © 2025

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