Wanikani Open Framework: Visually similar kanji filter

Adds a wkof filter for visually similar kanji

// ==UserScript==
// @name         Wanikani Open Framework: Visually similar kanji filter
// @namespace    http://tampermonkey.net/
// @version      2.0.0
// @description  Adds a wkof filter for visually similar kanji
// @author       Kumirei
// @include      https://www.wanikani.com/*
// @include      *preview.wanikani.com*
// @grant        none
// ==/UserScript==
/*jshint esversion: 8 */

(function(wkof) {
    let script_id = "VSKFilter";
    if (!wkof) return;

    var visuallySimilarFilename = 'https://raw.githubusercontent.com/mwil/wanikani-userscripts/master/wanikani-similar-kanji/db/stroke_edit_dist_esc.json';
    var visuallySimilarData;
    async function get_data() {
        let data = await wkof.load_file(visuallySimilarFilename, true);
        visuallySimilarData = JSON.parse(data);
    }

    function ageCache(){
        // periodically ages the cache
        let oneMonth = 1000*60*60*24*30;
        var now = Date.now();
        if (wkof.settings[script_id] === undefined) wkof.settings[script_id] = {};
        if (wkof.settings[script_id].lastTime === undefined){
            wkof.settings[script_id].lastTime = now;
            wkof.Settings.save(script_id);
        }
        let lastTime = wkof.settings[script_id].lastTime;
        if (now > lastTime + oneMonth){
            wkof.file_cache.delete(visuallySimilarFilename);
            wkof.settings[script_id].lastTime = now;
            wkof.Settings.save(script_id);
        }
    }

    // Turns comma separated lists into arrays. Supports latin and Japanese commas. Converts Japanese periods into latin decimal points
    // Adapted from a handy function of rfindley
    function split_list(str) {return str.replace(/./g,'.').replace(/[、,]/g,',').replace(/[\s ]+/g,' ').trim().replace(/ *, */g, ',').toLowerCase().split(',').filter(function(name) {return (name.length > 0);});}

    // Translate Japanese numerals into latin so they can be parsed into numbers by Number()
    function tr(text) {
        return text.replace( /[0123456789]/g, (chr => '0123456789'.charAt('0123456789'.indexOf( chr ))));
    }

    // Create an array of visually similar kanji for all search terms
    function makeVisuallySimilarArrays(str){
        var arr = split_list(str);
        if (arr.length === 0) return [];
        // compute the similarity threshold if one is provided
        var threshold = 0.0; // default if threshold is absent
        var first = Number(tr(arr[0]));
        if (!isNaN(first)){
            // number is present - calculate threshold
            if (arr.length <= 1) return [];
            var second = Number(tr(arr[1]));
            if (isNaN(second)) {
                // decimal separator not a comma - first is threshold
                threshold = first;
                arr = arr.splice(1);
            } else {
                // decimal separator is a comma - calculate threshold
                threshold = first + second/10;
                arr = arr.splice(2);
            }
        }
        return arr.reduce(filterThreshold, []);

        function filterThreshold(running, kanji){
            if (visuallySimilarData[kanji] === undefined) return running; // ignore trash input
            for (var data of visuallySimilarData[kanji]){
                if (data.score >= threshold) running.push(data.kan);
            }
            return running;
        }
    }

    // Adds the filter to wkof
    function register_filter(data) {
        wkof.ready('ItemData.registry').then(()=>{
            wkof.ItemData.registry.sources.wk_items.filters.include_visually_similar_kanji = {
                filter_func: function(filter_value, item){if (item.object === "kanji") item.visually_similar_kanji = visuallySimilarData[item.data.characters]; return true;}
            };
            wkof.ItemData.registry.sources.wk_items.filters.visually_similar_kanji = {
                type: 'text',
                label: 'LY Visually Similar',
                placeholder: '0.5,人、能、力',
                hover_tip: 'Select the kanji whose similar kanji you want to study.\nMultiple search terms are separated by commas.\n\n'+
                           'You may specify a similarity threshold as as a real number\nbetween 0 and 1 at the beginning of the search terms.\n'+
                           '0 means all similar kanji. Higher numbers give fewer kanji.\n\nThis filter uses similarity data by Lars Yencken.',
         		default: '',
		     	filter_func: function (filterValue, item){return (item.object === 'kanji' && filterValue.indexOf(item.data.characters) >= 0);},
                filter_value_map: makeVisuallySimilarArrays,
            };
            wkof.set_state(script_id, 'ready');
        });
    }

    // startup
    get_data();
    register_filter();
    wkof.include('Settings');
    wkof.ready('Settings')
        .then(function(){return wkof.Settings.load(script_id);})
        .then(ageCache);

})(window.wkof);