Table of Contents
What Is It?
WK Item Info Injector is a user-created library script that can be used by other userscripts. It simplifies the addition/modification of item info in the user interface of WaniKani.
How Can I Use It?
WK Item Info Injector can be required in your userscript:
// @require https://greasyfork.org/scripts/430565-wanikani-item-info-injector/code/WaniKani%20Item%20Info%20Injector.user.js?version=1492607
Alternatively, the end user can be instructed to install WK Item Info Injector manually ā similar to WK Open Framework. The @require
method is more comfortable for the end user, but comes with a few disadvantages:
- The
@require
meta data has to be kept up-to-date manually (I donāt plan to do many updates for WK Item Info Injector, but they might be necessary every time WK changes its interface, which recently happens pretty often due to the transition to React). - The entire code of WK Item Info Injector is inserted into every script that
@require
s it, which can increase the parsing time by a few milliseconds.
If WK Item Info Injector is available (by @require
or by manually installing it), you can use it via the wkItemInfo
object in the global scope. For example:
window.wkItemInfo.append("Item ID", o => o.id);
inserts a section showing the item ID into the item info in lessons, the lesson quiz, reviews, extra studies, and the item page:
Documentation
Selectors
First, here is an example call that shows (almost) all available options in the selector chain:
wkItemInfo.on("lesson,lessonQuiz,review,extraStudy,itemPage").forType("radical,kanji,vocabulary,kanaVocabulary").under("composition,meaning,reading,examples").spoiling("composition,meaning,reading,examples").append("Info Heading", "Info Body");
Everything before append()
is called the selector chain. The selector chain determines when your item info should be inserted. The order of the chain links (selectors) cannot be changed, but selectors that should just use the default arguments can be omitted. The example from above uses the default arguments everywhere, so omitting all selectors has the same result:
wkItemInfo.append("Info Heading", "Info Body");
If you want to inject only during the lesson quiz and the reviews, and only if the current item is a kanji, then you would write
wkItemInfo.on("lessonQuiz,review").forType("kanji").append("Info Heading", "Info Body");
The following table gives an overview of the four available selectors. Except in the case of the spoiling()
selector, omitting the arguments or omitting the whole selector both default to all accepted values.
Selector | Accepted Values | Description | Default |
---|---|---|---|
on() | lesson lessonQuiz review extraStudy itemPage |
Selects the pages on which your info should be injected. | All five page types |
forType() | radical kanji vocabulary kanaVocabulary |
Selects the item types for which your info should be injected. | All four item types |
under() | composition meaning reading examples |
Selects which section your injected info belongs to. By default, this also determines the location where exactly your section will appear. | All four sections |
spoiling() | composition meaning reading examples or nothing |
Defines the sections which your info spoils. spoiling("nothing") means that your info spoils no other item info. This selector can usually be omitted. |
Omit arguments: same as spoiling("nothing") Omit selector: same sections as specified in under() |
While the information in the table should be enough for basic usage, there are certain intricacies in the behavior of the under()
selector in combination with the spoiling()
selector that might be good to know for more control over the injection location and time. Therefore, here is a longer explanation for under()
and spoiling()
:
under()
The behavior in lessons is the most obvious ā during lessons, each itemās info is split up into two or four tabs. The keywords map to the tabs of each item type in the following way:
composition | meaning | reading | examples | |
---|---|---|---|---|
Radical | Name | Examples | ||
Kanji | Radicals | Meaning | Readings | Examples |
Vocabulary | Kanji Composition | Meaning | Reading | Context |
KanaVocabulary | Meaning | Context |
For lessons, the under()
selector specifies the tabs (sections) in which your info should be injected. During the lesson quiz, reviews, extra studies, and on the item page, multiple sections might be displayed at the same time. However, the selector still only matches once, so that your injected info is not duplicated.
Example: The selector is
under("meaning,reading")
and you visit a kanji item page: The page contains both meaning and reading sections, but the selector still only matches once.
During the lesson quiz, reviews, and extra studies, WK reveals the item info in two steps (for kanji and vocabulary).
For example, after a vocabulary meaning question, the item info first (step 1) only shows the sections Related Kanji [composition] and Meaning Explanation/Meaning Note [meaning]. Only after expanding (step 2), the sections Reading Explanation/Reading Notes [reading] and Context Sentence [examples] are also shown.
The under()
selector matches in step 1 if any of the specified sections might appear on screen in step 1 or step 2. However, the spoiling()
selector can delay the match until step 2.
With the vocabulary meaning question example from before,
under("meaning,reading")
,under("meaning")
, andunder("reading")
would all match in step 1, but not in step 2 (no duplication).
Important: If thespoiling()
selector is omitted and defaults to the same keywords as inunder()
, the matches forunder("meaning,reading")
andunder("reading")
will be delayed until step 2. Read aboutspoiling()
for more details.
By default, the injected section will be placed in relation to the last applying keyword.
Example: For a radicalās item page with the selector
under("meaning,reading")
, the last keyword does not apply, but the next-to-last does, so the info will be injected in relation to the meaning section.
spoiling()
Usually this can just be omitted, but here is a more detailed explanation if you want more control:
The spoiling()
selector only affects the behavior during reviews, extra studies, and the lesson quiz. In these situations, the default WK interface initially hides some sections of the item info because they would spoil the answer to the opposite question type (at least thatās my interpretation of the behavior ā it might also be set up this way just to focus on the more relevant info first).
Example: After answering a vocabulary meaning question, you open the item info. It only shows the
composition
and themeaning
sections āreading
andexamples
are hidden, because they contain spoilers for the reading question.
Only after clicking Show All Information, the other sections become visible.
The spoiling()
selector is able to delay the match until all sections are shown. As long as any of the sections specified in this selector are still hidden, the match will be delayed.
Continuing the example from before: You use the selector
under("meaning,reading")
and omit thespoiling()
selector, which defaults tospoiling("meaning,reading")
. Becausereading
is still hidden, thespoiling()
selector prevents a match ā the injection only happens after clicking Show All Information.
Actions
The selector chain determines when something should happen ā the āactionā that is appended to the end of the chain determines what should happen. The available actions are:
Action | Description |
---|---|
append() | Append the info below the section targeted by under() . |
appendSubsection() | Insert the info as a subsection into the section targeted by under() . |
appendSideInfo() | Only available for meaning or reading. In lessons, reviews, and extra studies, the side info is in a separate column at the left. The info will be appended under the targeted side info. |
appendAtTop() | Places the info above all the native WK info sections. |
appendAtBottom() | Places the info below all the native WK info sections. |
appendSideInfoAtTop() | Only available for meaning or reading. In lessons, reviews, and extra studies, the info is placed at the top of the left column. On the item page, itās basically the same as appendAtTop() . |
appendSideInfoAtBottom() | Only available for meaning or reading. In lessons, reviews, and extra studies, the info is placed at the bottom of the left column. On the item page, itās basically the same as appendAtBottom() . |
notify() | No injection, just calls the passed callback function. |
notifyWhenVisible() | Same as notify() , but the callback is only called once the section targeted by under() is visible. Use this if you want to modify native WK information. |
All the append actions take two arguments: the info heading and the info body. They can be strings, or DOM elements, or arrays of strings and/or DOM elements. Alternatively, they can also be callback functions returning any of those types, or returning a promise resolving to any of those types.
The callback functions receive as argument the current state, which is an object containing the following properties:
Name | Description | Example Value |
---|---|---|
on | The current page. Corresponds to selector on() |
"itemPage" |
type | The current itemās type. Corresponds to selector forType() |
"kanji" |
under | The currently shown sections. Corresponds to selector under() |
["meaning"] |
hiddenSpoiler | The sections currently hidden but (possibly) appearing later. | ["reading", "examples"] |
id | The WaniKani item ID of the current item. | 274 |
meaning | The primary and alternative meanings of the current item. | ["Narwhal"] |
characters | The characters of the current item. N/A for image radicals. | "éē" |
reading | Accepted readings for the current item. N/A for radicals and kana vocabulary. | ["ćć", "ć"] |
composition | The WK item components of the current item. N/A for radicals and kana vocabulary. | [{characters: "å£", meaning: ["Mouth"]}] |
partOfSpeech | The word types of the current vocabulary item. | ["Noun"] |
onyomi | The onyomi readings of the current kanji item. | ["ć«ć", "ćć"] |
kunyomi | The kunyomi readings of the current kanji item. | ["ć²ćØ", "ćØ"] |
nanori | The nanori readings of the current kanji item. | ["ćć"] |
emphasis | The reading type taught in the current itemās kanji lesson. | "onyomi" |
The same information can also be obtained with wkItemInfo.currentState
, but I recommend to use the callback argument whenever possible to decouple the callback function from the actual state (just in case I ever decide to implement preloading, which is however likely not going to happen).
The callback function passed to the notify()
or the notifyWhenVisible()
action also gets called with an argument containing these properties. In addition to that, it comes with one more property: injector
. This object also provides the five append actions that were listed before, but with some differences:
- They donāt accept callback functions as arguments.
- They accept an additional settings object as their third argument.
- They can only be called as long as
injector.active
istrue
. For example, if the user proceeds to the next item, the old injectors will be deactivated. - They return the created DOM element that will be appended.
The additional settings object currently supports three settings:
Setting | Description |
---|---|
injectImmediately | Usually, the section returned by the append action is not yet inserted into the page. If this is set to true , the injection is enforced at this point. |
under | By default, the target section is specified by the under() selector. This setting allows to specify any of the available sections (listed in under and hiddenSpoiler ) as the target section. |
sectionName | At the top of each item page is a list of links pointing to the available info sections. By default, WK Item Info Injector registers appended main sections (not subsections or side info) and uses the heading text content as the link name. This setting allows to choose a different name. |
Aside from the append actions, the injector
object also offers the function registerAppendedElement()
. If you manually insert DOM elements (not using the append actions), you can use this function to tell WK Item Info Injector about them. React might not always remove your inserted elements when the page changes, but if you register the element, WK Item Info Injector will make sure that the element gets removed when the user proceeds to the next lesson tab or item.
Return Value
Registering an action returns an object with the following functions:
Function | Description |
---|---|
remove() | Removes the registered action entirely, including its corresponding inserted section. |
renew() | Removes the corresponding inserted section, generates the section anew and inserts it. Can be called after the user updated some settings to immediately reflect the changes. |
In the case of notify
actions, elements that were registered with registerAppendedElement()
are removed as well.
Example for updating the section after a settings change (from the Rendaku Information userscript). When registering the action in wkItemInfo
, the returned object is stored in this.itemInfoHandle
for later use:
this.itemInfoHandle = wkItemInfo.forType("vocabulary").under("reading").appendSubsection("Rendaku Information", o => this.createRendakuSection(o.characters));
The settings dialog is created with WK Open Framework and set up to call this.itemInfoHandle.renew()
when the settings are saved:
new wkof.Settings({
script_id: "rendaku_information",
title: "Rendaku Information Settings",
on_save: () => this.itemInfoHandle.renew(),
content: {
hideTrivial: {type: "checkbox", label: "Hide trivial info"}
}
});
Usage Examples
WK Item Info Injector is already used in several scripts, some of which are listed here with a short description when they insert their info and example code showing how they use wkItemInfo
. The last example, āOld Mnemonicsā, shows a slightly more complex use case.
Mnemonic Artwork userscript
This script inserts its info for some radicals and kanji. The info consists of a visual mnemonic for the meaning and the reading (both together in one image), so it belongs to both the meaning and the reading section.
wkItemInfo.forType("radical,kanji").under("meaning,reading").append("Mnemonic Artwork", ({id}) => artworkSection(id));
Keisei userscript
This script inserts its info for radicals and kanji. During lessons, the info should appear in the āReadingsā tab for kanji, and in the āNameā tab for radicals. The info contains the meaning and reading of the current item, so in reviews it should only appear once the item info is fully expanded to avoid spoilers.
wkItemInfo.forType("kanji").under("reading").spoiling("meaning,reading").notifyWhenVisible(this.injectKeiseiSection.bind(this));
wkItemInfo.forType("radical").under("meaning").notifyWhenVisible(this.injectKeiseiSection.bind(this));
Niai userscript
This script inserts its info only for kanji. During lessons, the info should appear in the āExamplesā tab, but on all other pages, it should appear below the reading section. The info contains the meaning and reading of the current item, so in reviews it should only appear once the item info is fully expanded to avoid spoilers.
wkItemInfo.on("itemPage,lessonQuiz,review").forType("kanji").under("reading").spoiling("meaning,reading").notify(this.injectNiaiSection.bind(this));
wkItemInfo.on("lesson").forType("kanji").under("examples").notify(this.injectNiaiSection.bind(this));
Advanced Context Sentence 2 userscript
This script does not insert a new section, but modifies the existing context sentence section of vocabulary items. WK Item Info Injector can notify the script whenever a new context sentence section appears on screen, so that the section can immediately be modified:
wkItemInfo.forType("vocabulary", "kanaVocabulary").under("examples").notifyWhenVisible(() => evolveContextSentence());
KanjiDamage Mnemonics 2 userscript
This script inserts additional meaning and reading mnemonics for some kanji. They are inserted as subsections for the existing meaning and reading sections.
wkItemInfo.forType("kanji").under("meaning").appendSubsection(meaningHeading, appendMeaningMnemonic);
wkItemInfo.forType("kanji").under("reading").appendSubsection(readingHeading, appendReadingMnemonic);
Old Mnemonics userscript
This script inserts meaning and reading mnemonics for some radicals and kanji. In contrast to the KanjiDamage Mnemonics userscript, for some reason I decided to place the two mnemonics not as subsections, but as two main sections after the reading section (so that the reading mnemonic comes right after the meaning mnemonic). I will show two versions how to achieve this: In the first, both the meaning and the reading mnemonic still belong to āmeaningā and āreadingā, respectively, but the meaning mnemonic should not use the default location below the meaning section, but should be inserted below the reading section. This requires more control than offered by the append actions: Instead use the notify()
action and append with the additional settings object {under: "reading"}
(but only if the reading section exists on the current page ā in lessons and/or for radicals, the mnemonic should be inserted below the meaning section).
wkItemInfo.forType("radical,kanji").under("meaning").notify(o => {
let heading = o.type === "radical" ? "Old Name Mnemonic" : "Old Meaning Mnemonic";
let body = oldMnemonicSection(o.id, "meaning");
o.injector.append(header, body, {under: [...o.under, ...o.hiddenSpoiler].includes("reading") ? "reading" : "meaning"});
});
wkItemInfo.forType("kanji").under("reading").append("Old Reading Mnemonic", o => oldMnemonicSection(o.id, "reading"));
Alternative version with the āmeaningā part split up into more cases to simplify the callback function (with added spaces to make the code easier to read):
wkItemInfo .forType("radical").under("meaning") .append("Old Name Mnemonic" , o => oldMnemonicSection(o.id, "meaning"));
wkItemInfo.on("lesson" ).forType("kanji" ).under("meaning") .append("Old Meaning Mnemonic", o => oldMnemonicSection(o.id, "meaning"));
wkItemInfo.on("itemPage,lessonQuiz,review,extraStudy").forType("kanji" ).under("reading").spoiling("meaning").append("Old Meaning Mnemonic", o => oldMnemonicSection(o.id, "meaning"));
wkItemInfo .forType("kanji" ).under("reading") .append("Old Reading Mnemonic", o => oldMnemonicSection(o.id, "reading"));
Security
WK Item Info Injector assumes that everything on the WaniKani page is trustworthy. This might not be the case if the user has installed a malicious userscript, a malicious browser extension, or a userscript with an XSS vulnerability. The script does not sanitize data read from the DOM tree or from jStorage and just passes it on to the callback functions. Furthermore, if a sandboxed script (anything with @grant
other than none
) @require
s WK Item Info Injector, it might use unsafeWindow
to install wkItemInfo
into the global page context. For more details about this security concern, you can read my discussion with @est_fills_cando about this topic.
I am of the opinion that the risk of malicious code being executed on the WaniKani site is low enough to ignore this possibility, but if you have a different opinion let me know.
Used Version
WK Item Info Injector is placed in the global scope of the webpage, so usually all scripts use the same instance. This makes the order in which the additional sections are appended deterministic. An outdated instance in the global scope is however replaced if a newer version of WK Item Info Injector is executed.
Example: Script A
@require
s version 1.0 of WK Item Info Injector, and Script B@require
s version 1.1. If Script A executes before Script B, then Script A will install version 1.0 into the global scope, but Script B will later replace it with version 1.1. If at that point, Script A has already registered a selector in version 1.0, then this selector will continue to be handled by version 1.0. Both version 1.0 and 1.1 will continue to run in any case.
If Script B executes before Script A, then Script B will install version 1.1, and Script A will also use the existing version 1.1, which should not cause a problem because all versions should be backwards compatible.
Consequentially, if the user has installed WK Item Info Injector directly in their script manager, it should automatically be kept up-to-date, and as long as it runs before all @require
-ing scripts, all of them will use the same (the newest) version. WK Item Info Injector will by default run before most other scripts because it uses @run-at document-start
, but if there are @require
-ing scripts that also use @run-at document-start
, WK Item Info Injector has to be placed above those other scripts in the script manager.
Features
- Simplified injection of item info
- Usually deterministic order of injected sections (depending on the order in which the actions were registered)
- Adds links at the top of item pages that point to injected main sections
- Still works on item pages if not logged into WaniKani
- Still works even if the user has Tampermonkey disabled on page load and enables it later
- Tries to minimize browser reflow by inserting sections in batches
- Adds a side info bar to the meaning tab of radical lessons if required
- Easily remove or recompute your inserted section, e.g. after the user changed a setting
Why?
Aside from hopefully being useful for other script authors, the main reason for the creation of this library script are the extensive changes of the WaniKani page structure caused by the ongoing transition to React. Two months ago, a change to the lesson page caused several scripts to break: Two of them were my own, but also popular scripts like Keisei and Niai were affected. Most of the broken scripts were unmaintained, so while I was updating my two scripts, I got the idea that I could instead move the injection functionality into a separate library script, which I could then use to quickly fix all the unmaintained scripts.
Feedback and Requests
If you need additional features, ask here in this thread and I will think about adding them. Iām also interested to hear what I could have done better in the design of the interface (selector chain + action), but I probably wonāt change it because everything should stay backwards compatible. Also let me know if any part of the documentation is unclear, or if the script shows some unexpected behavior. Lastly, Iām not a native English speaker, so my wording might be awkward or grammatically incorrect ā feel free to also correct me on that.