Kronos / User Glossary for MTL

// ==UserScript==
// @name         User Glossary for MTL
// @version      1.12.8
// @license      MIT
// @namespace    lnmtl_glossary
// @description  User glossary fetched from google docs spreadsheet for lnmtl.com
// @author       Kronos
// @match        https://lnmtl.com/*
// @match        https://*.lnmtl.com/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js#sha512-+BMamP0e7wn39JGL8nKAZ3yAQT2dL5oaXWr4ZYlTGkKOaoXM/Yj7c4oy50Ngz5yoUutAG17flueD4F6QpTlPng==
// @require      https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.1/papaparse.min.js#sha512=EbdJQSugx0nVWrtyK3JdQQ/03mS3Q1UiAhRtErbwl1YL/+e2hZdlIcSURxxh7WXHTzn83sjlh2rysACoJGfb6g==
// @updateURL    https://openuserjs.org/meta/Kronos/User_Glossary_for_MTL.meta.js
// @downloadURL  https://openuserjs.org/install/Kronos/User_Glossary_for_MTL.user.js
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM.deleteValue
// ==/UserScript==

// linter options to prevent some warnings
/* eslint curly: "off"  */
/* global $, Tabletop, Papa, localforage */

if(typeof $ !== "function") //stop execution if site hasn't loaded jquery
    return;

const GM_SuperValue = new function() {
    const Object_Marker = 'json_val: ';
    /* Tampermonkey, Greasemonkey, Userscript support */
    this._set = async function (name, value) { return (typeof GM_setValue === 'function') ? GM_setValue(name, value): GM.setValue(name, value);};
    this._get = async function (name, defValue) { return (typeof GM_getValue === 'function') ? GM_getValue(name, defValue): GM.getValue(name, defValue);};
    this.delete = async function (name) { return (typeof GM_deleteValue === 'function') ? GM_deleteValue(name): (GM && typeof GM.deleteValue === 'function') ? GM.deleteValue(name): undefined;};
    this.set = async function (varName, varValue) { if (!varName) return;
                                                   switch (typeof varValue) {
                                                       case 'function': break;
                                                       case 'boolean':
                                                       case 'string': this._set (varName, varValue); break;
                                                       case 'number': if (varValue === parseInt (varValue) && Math.abs (varValue) < 2147483647) { this._set (varName, varValue); break;}
                                                       case 'object': this._set (varName, Object_Marker + JSON.stringify (varValue)); break;
                                                   }
                                                  }
    this.get = async function (varName, defaultValue) { if (!varName) return;
                                                       let varValue = await this._get (varName);
                                                       if (!varValue) return defaultValue;
                                                       if (typeof varValue == "string") {
                                                           let m = varValue.match (new RegExp ('^' + Object_Marker + '(.+)$'));
                                                           if (m && m.length > 1) return JSON.parse ( m[1] );
                                                       }
                                                       return varValue;
                                                      }
};


/* debugging for iOS etc. */
if (window.location.hash == "#debug" && console){
  console._log = console.log;
  console._error = console.error;
  console.log = function (...text) { alert(text.join("\n")); return console._log(...text)};
  console.error = function (...text) { alert("!! " + text.join("\n")) + "!! "; return console._error(...text) };
  console.log("Loading User Glossary Script");
}



(async function() {
const storageCleanupInterval = 3600000 * 36; /* hours */
const statsCleanupInterval = storageCleanupInterval * 3;
const glossaryChunkSize = 256;
const novelRefreshRate = 6; /* in hours */
const statRefreshRate = 3; /* in days */
const storagePruneSize = 1048576 * 10 /* MB */

let googleAPIKey = await GM_SuperValue.get("gAPIKey", undefined);
let publicSpreadsheetUrl = await GM_SuperValue.get("publicSpreadsheetUrl", "");
let replaceInOriginal = await GM_SuperValue.get("repalceInOriginal", false);
let noWordWrap = await GM_SuperValue.get("noWordWrap", false);
let reduceContrast = await GM_SuperValue.get("reduceContrast", false);
let useLNMTLGlossary = await GM_SuperValue.get("useLNMTLGlossary", "true") === "true";
let useMultiStepReplacer = true; // await GM_SuperValue.get("useMultiStepReplacer", false);
let useMultiSheetAPI = await GM_SuperValue.get("useMultiSheetAPI", false);
let autoSolveConflicts = await GM_SuperValue.get("autoSolveConflicts", true);
let useFullDict = await GM_SuperValue.get("useFullDict", true);
let filterTerms = await GM_SuperValue.get("filterTerms", true);
let nextStorageCleanup = await GM_SuperValue.get("nextStorageCleanup", 0);
let disableGlossaryAnalyzer = await GM_SuperValue.get("disableGlossaryAnalyzer", false);
let glossaryAutoLoader = await GM_SuperValue.get("glossaryAutoLoader", isDev());
let rawsReplaced = false;
let reload = false;
let appliedTerms = [];
let LNMTLGlossaryLock = Promise.resolve();
let LNMTLGlossaryStatsPromise = sleep(700);

const regexGlossaryTerms = /(\d+)\) (?:(\S+) )?=>(?: (.+))?$/;
const tableColumnFilter = ["raw", "meaning", "overwrite"];
const dbName = 'LNMTL Glossary Storage';

localforage.config({
    name: dbName
});

// Create lookuptable
const LRUStack = localforage.createInstance({
    name        : dbName,
    driver      : localforage.LOCALSTORAGE,
    storeName   : 'lrustack',
    description : 'Storage Cleanup'
});

let myTime = new Date().getTime();
if (myTime >= nextStorageCleanup || nextStorageCleanup - myTime >= storageCleanupInterval) {
    setTimeout(function() {
        nextStorageCleanup = myTime;
        Object.keys(localStorage).forEach(key => {
            if (key.startsWith("ndata")) {
                let data = JSON.parse(localStorage[key]);
                if (myTime - data.time >= storageCleanupInterval) {
                    localStorage.removeItem("gdata" + data.novel_id);
                    localStorage.removeItem("gvalid" + data.novel_id);
                    sessionStorage.removeItem("gmdata" + data.novel_id);
                    sessionStorage.removeItem("gmvalid" + data.novel_id);
                }
                else if (nextStorageCleanup > data.time + storageCleanupInterval) { nextStorageCleanup = data.time + storageCleanupInterval; }
                if (myTime - data.time >= Math.max(statsCleanupInterval, storageCleanupInterval)) {
                    localStorage.removeItem(key);

                }
            }});
        GM_SuperValue.set("nextStorageCleanup", nextStorageCleanup += storageCleanupInterval);
    }, 30000);
}


if (myTime >= nextStorageCleanup || nextStorageCleanup - myTime >= storageCleanupInterval) {
    setTimeout(function() {
        nextStorageCleanup = myTime;
        localforage.iterate( (value, key) => {
            if (key.startsWith("ndata")) {
                const data = value;
                if (myTime - data.time >= storageCleanupInterval) {
                    removeStorage("gdata" + data.novel_id);
                    removeStorage("gvalid" + data.novel_id);
                    sessionStorage.removeItem("gmdata" + data.novel_id);
                    sessionStorage.removeItem("gmvalid" + data.novel_id);
                }
                else if (nextStorageCleanup > data.time + storageCleanupInterval) { nextStorageCleanup = data.time + storageCleanupInterval; }
                if (myTime - data.time >= Math.max(statsCleanupInterval, storageCleanupInterval)) {
                    localforage.removeItem(key);

                }
            }});
        GM_SuperValue.set("nextStorageCleanup", nextStorageCleanup += storageCleanupInterval);
        LRUClean("gdata|stack", 64, storagePruneSize);
        LRUClean("ndata|stack", 96);
    }, 30000);
}

async function LRUShift(key, remove){
    let stackname = "stack";

    if (key.startsWith("gdata"))
        stackname = "gdata|stack";
    else if (key.startsWith("ndata"))
        stackname = "ndata|stack";
    else
        return Promise.resolve();

    let sema = new Semaphore();
    await sema.run();

    const stackArray = await LRUStack.getItem(stackname);
    const stack = new Set(stackArray);

    stack.delete(key);
    if (!remove)
        stack.add(key);
    try{
        await LRUStack.setItem(stackname, Array.from(stack));
    }
    catch(e){ console.error("LRUStack storage error: " + stackname + " " + e )}
    finally{
        sema.resolve();
    }

    /*
    const {head, tail} = await LRUStack.getItem("stackpointer") || {head: 0, tail: 0};
    const {newer, older} = await LRUStack.getItem(key) || {newer: key, older: key};
    let shead, stail;

    if (newer != key) {
        shead = (await LRUStack.getItem(newer) || {newer: newer}).newer;
    }

    if (older != key) {
        stail = (await LRUStack.getItem(tail) || {older: older}).older;
    }
    */
}

async function storageUsage(){
    if (!navigator || !navigator.storage || !navigator.storage.estimate)
        return {usage: 0, quota: Number.MAX_VALUE};

    await localforage.ready();
    const estimates = await navigator.storage.estimate();
    let usageKB = estimates.usage;

    if (localforage.driver() == localforage.INDEXEDDB && estimates.usageDetails)
        usageKB = (await navigator.storage.estimate()).usageDetails.indexedDB;
    else if (localforage.driver() == localforage.WEBSQL)
       ;
    else if (localforage.driver() == localforage.LOCALSTORAGE)
       ;

    return {usage: usageKB, quota: estimates.quota};
}


async function LRUClean(stackname, itemLimit, sizeLimit, oldSema){
    let sema = oldSema || new Semaphore();

    if (!stackname)
        return;

    if (!oldSema)
        await sema.run();

    itemLimit = itemLimit || 1;
    const stack = await LRUStack.getItem(stackname);

    if (stack.size == 0 || itemLimit < 0 || sizeLimit < 0)
        return;

    const storageEstimates = await storageUsage();

    if (stack.length < itemLimit && stack.length > 1 && (storageEstimates.usage >= storageEstimates.quota) || (sizeLimit && storageEstimates.usage > sizeLimit))
        itemLimit = Math.floor(stack.length / 2);

    while(stack.length > itemLimit){
        await localforage.removeItem(stack.shift());
    }

    await LRUStack.setItem(stackname, stack);
    await sleep(100);
    const usageNew = (await storageUsage()).usage;

    console.log("Freed up " + (100 - (usageNew/storageEstimates.usage * 100)).toFixed(2) + "% of glossary data");

    if ((usageNew >= storageEstimates.quota || (sizeLimit && usageNew > sizeLimit)) && stack.length > 1){
        await sleep(200);
        return await LRUClean(stackname, Math.floor(stack.length / 2), sizeLimit, sema);
    }

    sema.resolve();
}

async function store(name, value, level){
    const key = compressKey(name);
    level = (level || 0) + 1;

    if (typeof(localforage) !== undefined) {
        try {
            await LRUShift(key);
            return await localforage.setItem(key, value);
        } catch(e) {
            if (level == 1){
                await LRUClean("gdata|stack", 64, storagePruneSize);
                return store(name, value, level);
            }
            else if (level == 2){
                await LRUClean("ndata|stack", 64);
                return store(name, value, level);
            }
            else if (level == 3){
                await localforage.clear();
                await LRUStack.clear();
                console.log("cleared storage!");
                return store(name, value, level);
            }
            else{
                console.error("storage failure: " + e);
                return;
            }
        }
    }
    else if (typeof(Storage) !== "undefined") {
        try {
            return localStorage.setItem(key, JSON.stringify(value));
        } catch(e) {
            // Cleanup on all errors (full: e.code == 22)
            Object.keys(localStorage).forEach(key => { if (key.startsWith("ndata") || key.startsWith("gdata") || key.startsWith("gvalid")){ localStorage.removeItem(key);}});
            Object.keys(sessionStorage).forEach(key => { if (key.startsWith("gmdata") || key.startsWith("gmvalid") ) { sessionStorage.removeItem(key);}});
            console.error(e);
             if (level > 1)
                return;
             return store(name, value, level);
        }
    } else {
        GM_SuperValue.set(key, value);
    }
}

async function restore(name, defaultValue){
    name = compressKey(name);

    if (typeof(localforage) !== undefined) {
        LRUShift(name);
        const item = await localforage.getItem(name);
        if(item === null && defaultValue !== undefined)
            return defaultValue;
        return item;
    }
    else if (typeof(Storage) !== "undefined") {
        const item = localStorage.getItem(name);
        if(item === null && defaultValue !== undefined)
            return defaultValue;
        return JSON.parse(item);
    } else {
        // No Web Storage support..
        GM_SuperValue.get(name, defaultValue);
    }
}

async function removeStorage(name){
    const key = compressKey(name);
    if (typeof(localforage) !== undefined) {
        await LRUShift(key, true);
        return await localforage.removeItem(key);
    }
    else if (typeof(Storage) !== "undefined") {
        return localStorage.removeItem(key);
    }
}

function storeSession(name, value){
    if (typeof(Storage) !== "undefined") {
        try {
            return sessionStorage.setItem(name, JSON.stringify(value));
        } catch(e) {
            // Cleanup on all errors (full: e.code == 22)
            Object.keys(sessionStorage).forEach(key => { if (key.startsWith("gmdata") || key.startsWith("gmvalid")) { sessionStorage.removeItem(key);}});
        }
    }
}

function restoreSession(name, defaultValue){
    if (typeof(Storage) !== "undefined") {
        let item = sessionStorage.getItem(name);
        if(item === null && defaultValue !== undefined)
            return defaultValue;
        return JSON.parse(item);
    }
    return defaultValue;
}

function removeSession(name){
    if (typeof(Storage) !== "undefined") {
        return sessionStorage.removeItem(name);
    }
}

function compressKey(key){
    if (key.startsWith("ndata"))
        return "ndata|" + key.split("/",5).pop();
    return key;
}

async function LNMTLGlossaryLength(novelurl, load){
    load = load || glossaryAutoLoader;

    let novelData = await restore("ndata" + novelurl, {});

    if (new Date().getTime() - novelData.time >= 3600000 * Math.max(statRefreshRate * 24, novelRefreshRate))
        novelData = {};

    if ((Object.keys(novelData).length == 0 || novelData.glossary_length === undefined) && load) {
        const myanswer = async function() {novelData = await restore("ndata" + novelurl, {}); return {length: novelData.glossary_length, estimate: novelData.length_estimated}};
        return new Promise( resolve => {
           LNMTLGlossaryStatsPromise = LNMTLGlossaryStatsPromise.then(() => loadLNMTLGlossary(novelurl, () => Promise.resolve(resolve(myanswer())), false, true)).then(() => sleep(7777));
        });
    }

    return Promise.resolve({length: novelData.glossary_length, estimate: novelData.length_estimated});
}

const Semaphore = (function() {
    // private data shared among all instances
    let sharedPromise = Promise.resolve();

    return class Sempaphore {
        constructor() {
            let priorP = sharedPromise;
            let resolver;

            // create our promise (to be resolved later)
            let newP = new Promise(resolve => {
                resolver = resolve;
            });

            // chain our position onto the sharedPromise to force serialization
            // of semaphores based on when the constructor is called
            sharedPromise = sharedPromise.then(() => {
                return newP;
            });

            // allow caller to wait on prior promise for its turn in the chain
            this.run = function() {
                return priorP;
            }

            // finish our promise to enable next caller in the chain to get notified
            this.resolve = function() {
                resolver();
            }
        }
    }
})();

String.prototype.count = function (regexp) {
    if (typeof regexp === 'string' || regexp instanceof String)
        regexp = new RegExp(regexp.escapeRegExp(), 'g');
    let res = this.match(regexp);
    return res !== null ? res.length: 0;
};

function capitalize(string) {
    return string.charAt(0).toUpperCase() + string.slice(1);
}

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function lock(func) {
    return new Promise(resolve => { func().then(() => resolve()); });
}

function fetchLNMTLNovelData(url, callback) {
    return $.get(url, function(response) { return callback(response); });
}

async function loadLNMTLGlossary(url, callback, withMetaData, onlyStats) {
    if ($("#navbar a:contains('Login')").length > 0){
        return Promise.resolve(); // not logged in
    }
    withMetaData = withMetaData || false;
    onlyStats = onlyStats || false;
    if (!useLNMTLGlossary)
        callback = callback || function() { return Promise.resolve();};
    else
        callback = callback || applyGlossaryAsync;
    let novelData = await restore("ndata" + url);
    let timeNow = new Date().getTime();

    if (novelData === null )
        novelData = { time: 0 , changes: 0 };

    let old_changes = novelData.changes;
    let old_retrans = novelData.retrans;
    let callGetLNMTLGLossary = function (novelData, old_changes, old_retrans, callback, withMetaData, novelurl){
        return getLNMTLGlossary(novelData.novel_id, novelData.changes, ((novelData.changes - old_changes <= 0) && old_retrans === novelData.retrans),
            async function(glossaryData) {
            novelData.glossary_length = glossaryData.length;
            delete novelData.length_estimated;
            await store("ndata" + novelurl, novelData);
            return callback.apply(this, arguments);
        }, withMetaData);
    };
    if (timeNow - novelData.time >= 3600000 * novelRefreshRate){
        console.log("fetch novel data: " + url);
        return new Promise( resolve => {
            fetchLNMTLNovelData(url, async function (data) {
                let novelinfo = $('.panel.panel-default .progress-bar[role="progressbar"]', $(data));
                let glossary_changes = novelinfo.text().trim().split(' / ');
                let retranslation_time = new Date($(".panel.panel-default dt:contains('Latest retranslation at') ~ * span", $(data)).text().replace(/\-/g, '/')).getTime();
                let novel_id = $("a.btn.btn-primary[href^='https://lnmtl.com/termProposition?novel_id=']", $(data)).attr("href").split("https://lnmtl.com/termProposition?novel_id=")[1];
                novelData.time = timeNow;
                let needGlosary = true;
                if (onlyStats && novelData.glossary_length && novelData.retrans == retranslation_time && novelData.changes - glossary_changes[0] >= 0){
                    novelData.glossary_length += novelData.changes - glossary_changes[0];
                    if (novelData.changes != glossary_changes[0])
                        novelData.length_estimated = true; /* estimate, since not every change is adding a term) */
                    needGlosary = false;
                }
                novelData.changes = glossary_changes[0];
                novelData.retrans = retranslation_time;
                novelData.novel_id = novel_id;
                await store("ndata" + url, novelData);
                if (needGlosary)
                    callGetLNMTLGLossary(novelData, old_changes, old_retrans, callback, withMetaData, url).then((value) => resolve(value));
                else
                    sleep(10).then((value) => resolve(value));
            }).fail(e => resolve());
        });
    } else {
        return callGetLNMTLGLossary(novelData, old_changes, old_retrans, callback, withMetaData, url);
    }
}

function analyseGlossary(dict, metaDict, monitor){
    return new Promise(function(resolve, reject) {
        monitor = monitor || function() {};
        metaDict = metaDict || [];
        let reorder = {};
        let badorder = {};
        let goodorder = {};
        let duplicates = {};
        let whitespaces = [];
        let chunk = 100;
        let pcount = 0;
        let i = 0;

        function processTerms() {
            let cnt = chunk;
            for (;i < dict.length && ( cnt-- > 0); i++) {
                let term = dict[i];
                if (term.raw == "") //#skip empty raw
                    continue;

                let j = i + 1;
                let after_term_pos = i;
                while (j < dict.length){
                    if (i === j){
                        break;}
                    let bterm = dict[j];
                    //filter dublicate entries
                    if (term.raw == bterm.raw){
                        if (j > i)
                            duplicates[i] = [j];
                    }
                    else {
                        if (bterm.raw.includes(term.raw) && after_term_pos < j) {
                            if(j > i){
                                reorder[i] = j;
                                after_term_pos = j;
                                (badorder[i] || (badorder[i] = [])).push(j);
                            }
                            else{
                                (goodorder[i] || (goodorder[i] = [])).push(j);
                            }
                        }
                    }
                    if (++j == dict.length && after_term_pos != i){
                        after_term_pos = -1;
                        j = 0;
                    }

                }
                if (term.meaning.trim() != term.meaning)
                    whitespaces.push(term);
            }
            if (pcount % 10 == 0)
                monitor(i, dict.length);
            if (i < dict.length) {
                pcount++;
                setTimeout(processTerms, 1);
            }
            else{
                monitor(100, 100); // progress complete
                let reverseDict = function(map, f) {
                    let id = function(x) {return x;};
                    return Object.keys(map).reduce(function(acc, k) {
                        acc[map[k]] = (acc[map[k]] || []).concat((f || id)(k));
                        return acc;
                    },{});
                };
                duplicates = reverseDict(duplicates);
                let conflicts = reverseDict(reorder);

                for(let key in conflicts){
                    if(!reorder[key]){
                        let cons = conflicts[key];
                        let goodset = new Set(cons.map( x => parseInt(x)));
                        cons.forEach( (con) => {
                            let conflictContext = (goodorder[con] || []).concat(badorder[con] || []);
                            conflictContext.forEach((c) => { if (dict[c].raw.includes(dict[key].raw)) goodset.add(c);});
                        });
                        cons.forEach( x => goodset.delete(parseInt(x)));
                        goodset.delete(parseInt(key));
                        goodorder[key] = Array.from(goodset).sort((a, b) => parseInt(a) - parseInt(b));
                    }
                }

                let metadata = {"terms": dict.length,"conflicts": Object.keys(reorder).length, "duplicates": Object.keys(duplicates).length, "whitespaces": whitespaces.length};
                let realPosition = (pos) => (metaDict[pos]||[parseInt(pos)+1])[0];
                let posWrapper = (term, pos) => { return {"raw":term.raw.replace(/\\\\/g,''), "meaning":term.meaning, "pos":realPosition(pos)}; };
                let infoWrapper = (term, pos) => { return {"raw":term.raw.replace(/\\\\/g,''), "meaning":term.meaning, "pos":realPosition(pos), "goodorder":termInfos(goodorder[pos], posWrapper), "badorder": termInfos(badorder[pos], posWrapper)}; };
                let termInfos = (terms, wrapper) => (terms||[]).reduce((p, i) => p.concat(wrapper(dict[i],i)), []);
                let conflictResult = Object.keys(conflicts).reduce((p, key) => {
                    return function(p, term, key, value) {
                        return p.concat([termInfos(value, infoWrapper).concat([infoWrapper(term, key)])]);
                    }(p, dict[key], key, conflicts[key]);
                }, []);

                let duplicatesResult = Object.keys(duplicates).reduce((p, key) => {
                    return function(p, term, key, value) {
                        return p.concat([termInfos(value, posWrapper).concat([posWrapper(term, key)])]);
                    }(p, dict[key], key, duplicates[key]);
                }, []);

                let result = {"metadata":metadata, "conflicts":conflictResult, "duplicates": duplicatesResult, "whitespaces":whitespaces};
                resolve(result);
            }
        }
        processTerms();

    });
}

function processArrayAsync(arr, fn, maxTimePerChunk, chunk, context) {
    context = context || window;
    maxTimePerChunk = maxTimePerChunk || 200;
    chunk = chunk || 50;
    let i = 0;

    function now() {
        return new Date().getTime();
    }

    function processChunk() {
        let startTime = now();
        let cnt = chunk;
        while (i < arr.length && ( cnt-- > 0 || (now() - startTime) <= maxTimePerChunk)) {
            if (cnt <= 0){
                cnt = chunk;
            }
            // callback called with (value, index, array)
            fn.call(context, arr[i], i++, arr);
        }
        if (i < arr.length) {
            setTimeout(processChunk, 1);
        }
    }
    processChunk();
}

async function getLNMTLGlossary(novelid, changeCount, skipUpdate, callback, withMetaData){
    withMetaData = withMetaData || false;
    let glossaryData = await restore("gdata" + novelid);
    let glossaryMetaData;

    if(withMetaData && await restore("gvalid" + novelid, 0) != restoreSession("gmvalid" + novelid, 1)){
        removeSession("gmdata"+ novelid);
        removeSession("gmvalid"+ novelid);
    }

    if (glossaryData === null || (withMetaData && ((glossaryMetaData = restoreSession("gmdata" + novelid)) === null))){
        glossaryData = [];
        glossaryMetaData = [];
        skipUpdate = false;
    }

    if (!skipUpdate){
        console.log("fetch novel glossary:" + novelid);
        return new Promise( resolve => {
            fetchLNMTLGlossary(novelid, async function (data){
                let o = $('#after_term_id', $(data))[0].options;
                processArrayAsync(o, (v,i) => {
                    let g = (regexGlossaryTerms.exec(v.text) || []);
                    let dict = {};
                    dict.raw = (g[2]||"").trim();//.replace(/[!"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]/g, "\\\\$&"); //TODO better handle this to make special chars really work
                    dict.meaning = (g[3]||"");
                    //dict.id = v.value;
                    glossaryData[i] = dict;
                    if(withMetaData)
                        glossaryMetaData[i] = [parseInt(g[1]),parseInt(v.value)];
                });
                let fetchTime = new Date().getTime();
                //glossaryData = $.map($('#after_term_id option', $(data)).get(), function( a ) { let dict = {}; return dict;});
                await store("gdata" + novelid, glossaryData);
                if(withMetaData){
                    await store("gvalid" + novelid, fetchTime); //indicate if sessionStorage and localStorage are consistent
                    storeSession("gmdata" + novelid, glossaryMetaData);
                    storeSession("gmvalid" + novelid, fetchTime); //check later if sessionStorage and localStorage have same timestamp
                } else if (await restore("gvalid" + novelid, 0) > 0)
                    await removeStorage("gvalid" + novelid); //invalidate sessionStorage on localStorage change

                //console.log("dict length ="+ glossaryData.length);
                callback(glossaryData, changeCount, glossaryMetaData, novelid).then((value) => resolve(value));
            }, resolve);
        });
    }
    else {
        //console.log("store dict length ="+ glossaryData.length);
        return callback(glossaryData, changeCount, glossaryMetaData, novelid);
    }
}


function fetchLNMTLGlossary(novelid, callback, resolve, term_id, trys) {
    if (!term_id)
        term_id = 1;
    if (!trys)
        trys = 10;
    $.get("https://lnmtl.com/termProposition/create?type=move&novel_id=" + novelid + "&term_id="+term_id, function(response) {
        callback(response);
    }).fail(function(e) { // workaround if id 1 is missing.
        if (e.status == 404) {
            if(trys > 0 || term_id < 4196)
                return fetchLNMTLGlossary(novelid, callback, resolve, term_id*2, trys-1);
            else
                resolve();
        } else {
            resolve();
        }
    });
}

String.prototype.escapeRegExp = function () {
    const regFilter = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g;
    return this.replace(regFilter, "\\$&");
};

function escapeRegExp(str) {
    const regFilter = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g;
    if (str.constructor === Array){
        let newStr = [];
        for(let i in str){
            newStr.push(str[i].replace(regFilter, "\\$&"));
        }
        return newStr;
    }
    return str.replace(regFilter, "\\$&");
}

function validateUrl(relativeUrl, baseUrl){
    try { return new URL(relativeUrl, baseUrl); } catch (e) { if (e instanceof TypeError) { return undefined } else return undefined };
}

function getSpreadsheetId() {
    const sheetUrl = validateUrl("./", publicSpreadsheetUrl);

    if (!sheetUrl)
        return undefined;

    const pathSegments = sheetUrl.pathname.slice(0,-1).split('/').pop();

     if (sheetUrl.hostname != "docs.google.com") {
        console.error("not a google spreadsheet: " + publicSpreadsheetUrl)
         return undefined;
     }

    if (pathSegments != undefined && pathSegments.length > 0 )
        return pathSegments;
    return undefined;
}

function fetch_glossary() {
    let googleAPIaccess = false;

    if (getSpreadsheetId() !== undefined && useMultiSheetAPI){
       googleAPIaccess = true;
    }

    if(googleAPIaccess)
    {
       fetchGoogle(googleAPIKey, fetchSpreadSheetData, result => useDataV4(result), {}, () => fetchCSV());
    }
    else
       fetchCSV();
}

function fetchCSV(){
    const sheetUrl = validateUrl("./pub?output=csv", publicSpreadsheetUrl);
    if (sheetUrl){
        Papa.parse(sheetUrl.href, {
            download: true,
            skipEmptyLines: "greedy",
            header: false,
            complete: (result) => { singleSheet(Papa.parse(Papa.unparse(extractColumns(result.data)), { header: true })).then(() => rawsReplace()) }
        });
    }
}

function updateReplace(replace){
    replaceInOriginal = replace;
    GM_SuperValue.set("repalceInOriginal", replaceInOriginal);
}

function updateDocsURL(url){
    if(!(url.startsWith("https://") || url.startsWith("http://") || url === ""))
        // no url no save
        return;
    publicSpreadsheetUrl = url;
    GM_SuperValue.set("publicSpreadsheetUrl", publicSpreadsheetUrl);
}

function updateUseLNMTLGlossary(_useLNMTLGlossary){
    useLNMTLGlossary = _useLNMTLGlossary;
    GM_SuperValue.set("useLNMTLGlossary", useLNMTLGlossary.toString());

    if (window.location.pathname.startsWith("/chapter/") || window.location.pathname.startsWith("chapter/") && useLNMTLGlossary) {
        let novelname = window.location.pathname.split("/").pop();
        novelname = novelname.substr(0,novelname.lastIndexOf("-chapter-"));
        let book_index = novelname.lastIndexOf("-book-");
        if(book_index > novelname.length - 10 && book_index > 0){
            novelname = novelname.substr(0, book_index);
        }

        LNMTLGlossaryLock = lock(() => loadLNMTLGlossary("https://lnmtl.com/novel/" + novelname));
        fetch_glossary(); //google docs glossary
    }
}

function updateDisableGlossaryAnalyzer(_disableGlossaryAnalyzer){
    disableGlossaryAnalyzer = _disableGlossaryAnalyzer;
    GM_SuperValue.set("disableGlossaryAnalyzer", disableGlossaryAnalyzer);

    if (window.location.pathname.startsWith("/novel/") || window.location.pathname.startsWith("novel/") && !disableGlossaryAnalyzer) {
        if (!disableGlossaryAnalyzer)
            if ($('#novel-display-conflicts-button').length == 0){
                addGlossaryAnalyseModal();
                addAnalyseButton();
            }
            else
                $('#novel-display-conflicts-button').show();
        else $('#novel-display-conflicts-button').hide();
    }
}

function getAPIkey(mesText){
    mesText = mesText || "Please enter your google api key";
    googleAPIKey = String(prompt(mesText, "API_KEY"));
    if (googleAPIKey === "null")
        googleAPIKey = undefined;
    return googleAPIKey;
}

function fetchSpreadSheetData(apiKey){
    const spreadsheetId = getSpreadsheetId();

    if (!googleAPIKey || !spreadsheetId)
      return Promise.resolve(false);

    const tURL = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}?includeGridData=false&key=${apiKey}`;
    return fetch(tURL);
}

function fetchSheet(apiKey, {sheetID}){

    const spreadsheetId = getSpreadsheetId();
    let url = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${sheetID}?key=${apiKey}`;
    return fetch(url);
}

function fetchGoogle (apiKey, fetchAPI, complete, fetchParams, failure, cancel, errKey, e400, e404){
    failure = failure || ( () => {} );
    fetchParams = fetchParams || {};
    errKey = errKey || ( (error) => fetchGoogle(getAPIkey(error.message), fetchAPI, complete, fetchParams, failure, cancel, errKey, e400, e404) );
    e400 = e400 || ( error => { console.error(error.message)});
    e404 = e404 || ( error => { console.error(error.message)});

    if (apiKey === undefined) {
        if (typeof cancel === "function")
            cancel();
        return failure();
    }

    if (apiKey == "") {
        if (!cancel)
            cancel = () => { $('#useMultiSheetAPI').prop('checked', false); updateMultiSheetAPI(false); };
        return errKey(apiKey);
    }

    fetchAPI(apiKey, fetchParams).then( response =>
                                       response.json().then(result => {
        if (response.status == "200" && complete)
            return complete(result, apiKey);
        else if(result.error && result.error.code == 400 && result.error.details && result.error.details[0].reason == "API_KEY_INVALID"){
            if (!cancel)
                cancel = () => { $('#useMultiSheetAPI').prop('checked', false); updateMultiSheetAPI(false); };
            return errKey(result.error);
        }
        else if(result.error && result.error.code == 400) {
            e400(result.error);
        }
        else if(result.error && result.error.code == 404) {
            e404(result.error);
        }
        return failure(result.error);
    }));
}

function checkAPIKey(test){
    const fail = () => { $('#useMultiSheetAPI').prop('checked', false); googleAPIKey = undefined; };

    if (test && getSpreadsheetId() !== undefined)
        return fetchGoogle(getAPIkey(), fetchSpreadSheetData, (result, apiKey) => { return GM_SuperValue.set("gAPIKey", apiKey)}, {}, fail);
    else {
        const apiKey = getAPIkey();
        GM_SuperValue.set("gAPIKey", apiKey);
    }

}

async function updateMultiSheetAPI(_useMultiSheetAPI){
    googleAPIKey = undefined;
    useMultiSheetAPI = _useMultiSheetAPI;
    GM_SuperValue.set("useMultiSheetAPI", _useMultiSheetAPI)
    if (_useMultiSheetAPI === true)
        checkAPIKey(true)
    if (!googleAPIKey)
        await GM_SuperValue.delete("gAPIKey");
}

function updateContrast(_reduceContrast){
    reduceContrast = _reduceContrast;
    GM_SuperValue.set("reduceContrast", reduceContrast);
    changeContrast(reduceContrast);
}

function updateWordWrap(_noWordWrap){
    noWordWrap = _noWordWrap;
    GM_SuperValue.set("noWordWrap", noWordWrap);
    changeWordWrap(noWordWrap);
}

function addCloseSettingsListener(){
    $("#chapter-display-options-modal div.modal-footer button").click(function(){$('#chapter-display-options-modal div.modal-body #gdocs-url-input').val(publicSpreadsheetUrl);});
}

function addUserGlossaryLink(){
    if(publicSpreadsheetUrl && publicSpreadsheetUrl !== ""){
        // if the gdocs url links to the web version than move to edit mode
        let editurl = publicSpreadsheetUrl.replace("/pubhtml","/edit");
        let ex_icon = '<sup> <span class="glyphicon glyphicon-new-window"></span></sup>';
        let link = $('<li><a target="_blank" href="'+editurl+'">Google User Glossary'+ex_icon+'</a></li>');
        $('#navbar > ul.nav.navbar-nav.navbar-right > li.dropdown .dropdown-header:contains("Glossary") ~ .divider').before(link);
    }
}

function addGlossarySettings(){
    let title = $("<h3>Advanced Glossary</h3>");

    let row = $('<div class="row"></div>');
    let col = $('<div class="col-xs-6"></div>');

    let label = $('<label class="control-label">User Glossary Sheet Url:</label>');
    let textarea_row =$('<div class="col-xs-12"></div>');
    let textarea =$('<textarea class="form-control" id="gdocs-url-input" rows="2" input type="text" name="gdocs-url" wrap="soft">');
    textarea.val(publicSpreadsheetUrl);
    textarea.on("change keyup paste", function(){updateDocsURL(textarea.val());});
    textarea_row.append(textarea);
    col.append(label);

    let col2 = $('<div class="col-xs-6"></div>');

    let checked = "";
    if(replaceInOriginal)
        checked = "checked";
    let option2 = $('<sub><input id="replaceInOriginal" type="checkbox" '+checked+'></sub> <label for="replaceInOriginal">Replace all raws in source sentence</label>');
    option2.on("change", function(){updateReplace($("#replaceInOriginal")[0].checked);});
    col2.append(option2);

    row.append(col).append(col2).append(textarea_row);

    $("#chapter-display-options-modal .modal-body").append(title).append(row);

    if ($("#navbar a:contains('Login')").length > 0){
        // not logged in
        return;
    }

    let row3 = $('<div class="row"></div>');
    let col3 = $('<div class="col-xs-12"></div>');

    let LNMTLGlossaryChecked = "";
    if (useLNMTLGlossary)
        LNMTLGlossaryChecked = "checked";
    let option3 = $('<sub><input id="useLNMTLGlossary" type="checkbox" '+LNMTLGlossaryChecked+'></sub> <label for="useLNMTLGlossary">Apply approved LNMTL Glossary entries to chapters. (cache refreshes every ' + novelRefreshRate+'h)</label>');
    option3.on("change", function(){updateUseLNMTLGlossary($("#useLNMTLGlossary")[0].checked);});
    row3.append(col3.append(option3));

    let row4 = $('<div class="row"></div>');
    let col4_1 = $('<div class="col-xs-6"></div>');

    let GlossaryAnalyzerChecked = "";
    if (disableGlossaryAnalyzer)
        GlossaryAnalyzerChecked = "checked";
    let option4 = $('<sub><input id="disableGlossaryAnalyzer" type="checkbox" '+GlossaryAnalyzerChecked+'></sub> <label for="disableGlossaryAnalyzer">Disable Glossary Analyser</label>');
    option4.on("change", function(){updateDisableGlossaryAnalyzer($("#disableGlossaryAnalyzer")[0].checked);});

    let col4_2 = $('<div class="col-xs-6"></div>');

    let MultiSheetAPIChecked = "";
    if (useMultiSheetAPI && googleAPIKey){
        MultiSheetAPIChecked = "checked";
    }
    let option5 = $('<sub><input id="useMultiSheetAPI" type="checkbox" '+MultiSheetAPIChecked+'></sub> <label for="useMultiSheetAPI" title="support spreadsheets that contain multiple named sheets (own google api key required!)">Multi-Sheet Spreadsheet</label>');
    option5.on("change", function(){updateMultiSheetAPI($("#useMultiSheetAPI")[0].checked);});
    row4.append(col4_1.append(option4)).append(col4_2.append(option5));

    $("#chapter-display-options-modal .modal-body").append(row3).append(row4);
}

function createConflictReport(data, showPopover, caption){
    showPopover = showPopover || false;
    if (caption)
        caption = '<caption align="bottom">' + caption + '</caption>';
    else
        caption = "";
    let popdiv = $('<div id=popoverConflictTarget> </div>');
    let table = $('<table class="table table-sm table-condensed"> '+ caption + ' <thead> <tr> <th scope="col">raw</th> <th scope="col">meaning</th> <th scope="col">glossary position</th> </tr> </thead> </table>');
    let tdata = '';
    let delimiter = '<tr class="active"> <th scope="row" colspan="3"></th> </tr>';

    let addPopover = function(text, popover) {
        if(!showPopover)
            return '<td>' + text + '</td>'
        return '<td tabindex="0" data-toggle="popover" data-trigger="focus" role="button" data-container="body" title="terms including the <em>raw</em>" data-delay="{ "show": 800, "hide": 400 }" data-placement="auto" data-content="'+popover+'" data-html="true" >' + text + '</td>';
    };

    let locationContext = function (term){
        let result = "<ul class='list-group'>";
        let good = term.goodorder;
        let bad = term.badorder;

        if(good.length == 0 && bad.length == 0)
            return result + "<li class='list-group-item active'>" + termPosition(term) + "</ul>";

        for(let key in good){
            result +="<li class='list-group-item'>"+ termPosition(good[key]) +"</li>";
        }
        result +="<li class='list-group-item active'>"+ termPosition(term) +"</li>";
        for(let key in bad){
            result += "<li class='list-group-item'>"+ termPosition(bad[key]) +"</li>";
        }
        return result + "</ul>";
    };

    let termPosition = function (term){
        return term.pos + " | " + term.raw;
    };

    for (let i=0; i < data.length; i++ ) {
        for (let j=0; j < data[i].length; j++ ) {
            let term = data[i][j];
            tdata += '<tr> <th scope="row">' + term.raw + '</th> ' + addPopover(term.meaning, locationContext(term)) + '<td>'+ term.pos + '</td> </tr>';
        }
        tdata += delimiter;
    }
    let tbody = $('<tbody>' + tdata + '</tbody>');
    table.append(tbody);
    return popdiv.append(table);
}

function createTermGroupReport(data, caption){
    if (caption)
        caption = '<caption align="bottom">' + caption + '</caption>';
    else
        caption = "";
    let table = $('<table class="table" table-sm table-condensed> ' + caption + ' <thead> <tr> <th scope="col">raw</th> <th scope="col">meaning</th> <th scope="col">position</th> </tr> </thead>');
    let tdata = '';
    let delimiter = '<tr class="active"> <th scope="row" colspan="3"></th> </tr>';

    for (let i=0; i < data.length; i++ ) {
        for (let j=0; j < data[i].length; j++ ) {
            let term = data[i][j];
            tdata += '<tr> <th scope="row">' + term.raw + '</th> <td>' + term.meaning + '</td> <td>'+ term.pos + '</td> </tr>';
        }
        tdata += delimiter;
    }

    let tbody = $('<tbody>' + tdata + '</tbody>');
    table.append(tbody);
    return table;
}

function createTermReport(data,caption){
    if (caption)
        caption = '<caption align="bottom">' + caption + '</caption>';
    else
        caption = "";
    let table = $('<table class="table table-sm table-condensed">' + caption + ' <thead> <tr> <th scope="col">raw</th> <th scope="col">meaning</th> </tr> </thead>');
    let tdata = '';

    for (let i=0; i < data.length; i++ ) {
        let term = data[i];
        tdata += '<tr> <th scope="row">' + term.raw + '</th> <td>' + term.meaning + '</td> </tr>';
    }

    let tbody = $('<tbody>' + tdata + '</tbody>');
    table.append(tbody);
    return table;
}

function createGlossaryStatsLabel(novelnode){
    return LNMTLGlossaryLength(novelnode.attr("href")).then(
        (gldata) => (gldata && gldata.length != undefined) ? $(novelnode).parent().next("p").children("span.label.label-default:last").after('<span class="label label-default"> <span class="glyphicon glyphicon-stats"></span> ' + gldata.length + ((gldata.estimate)? '*':'') + ' </span>') : undefined);
}

function addGlossaryStatsInfo(infonode){
    return LNMTLGlossaryLength(window.location.toString()).then(
        (gldata) => (gldata && gldata.length != undefined) ? $(infonode).children(":first").after('<dl> <dt>Glossary entries' + ((gldata.estimate)? '*':'') + '</dt> <dd><span class="label label-default">' + gldata.length + ' terms </span></dd> </dl>') : undefined);
}

function createAnalyzerReport(data){
    let intro = $('<p></p>');
    let metadata = data.metadata;
    let plural = 's';

    let info1 = '<h5><span class="label label-default">'+metadata.conflicts+'</span> of <span class="label label-default">'+metadata.terms+'</span> terms out of order</h5>' ;
    if (metadata.duplicates == 1)
        plural = '';
    let info2 = '<h5><span class="label label-default">'+metadata.duplicates+'</span> duplicate'+plural+' detected</h5>';
    plural = 's';
    if (metadata.whitespaces == 1)
        plural = '';
    let info3 = '<h5><span class="label label-default">'+ metadata.whitespaces+'</span> term'+plural+' with misplaced whitespaces</h5>';

    let head = intro.append($(info1));
    if(metadata.conflicts != 0){
        let conflictReport = createConflictReport(data.conflicts, true, "conflicting groups of terms");
        head.append(conflictReport);
    }
    if(metadata.duplicates != 0){
        if(metadata.conflicts != 0)
            head.append($('<hr>'));
        let duplicateReport = createTermGroupReport(data.duplicates, "duplicate terms");
        head.append(info2).append(duplicateReport);
    }
    if(metadata.whitespaces != 0){
        if(metadata.conflicts != 0 || metadata.duplicates != 0)
            head.append($('<hr>'));
        let whitespaceReport = createTermReport(data.whitespaces, "terms with leading or trailing whitespace");
        head.append(info3).append(whitespaceReport);
    }
    return head;
}

function addAnalyseButton(){
    let firstBtn = $("a.btn.btn-primary[href^='https://lnmtl.com/termProposition?novel_id=']:contains('Check New Term Propositions')" );
    firstBtn.after($('<button type="button" id="novel-display-conflicts-button" class="btn btn-warning" data-toggle="modal" data-target="#novel-display-conflicts-modal"> Analyse Glossary</button>'));
}

function addGlossaryAnalyseModal() {
    //anylyse glossary
    // conflics, duplicates, whitespace terms
    let novelname = $('.novel-name')[0].firstChild.textContent.trim();
    let module = $('<div class="my-module"> <div class="modal fade" id="novel-display-conflicts-modal"> <div class="modal-dialog"> <div class="modal-content"> </div> </div> </div> </div>');
    let modal_header = $('<div class="modal-header"> <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button> <h4 class="modal-title"> Analyse <em>' + novelname + '</em> Glossary</h4> </div>');
    let modal_body = $('<div class="modal-body"></div>');
    let progress_bar = $('<div class="collapse" id="collapseProgressbar"> <div class="progress"> <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" id="progress-bar-scan" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%; min-width: 3em;"> Scan Glossary </div> </div> </div>');
    let modal_footer = $('<div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> </div>');
    modal_body.append(progress_bar);
    $('.modal-content', module).append(modal_header).append(modal_body).append(modal_footer);
    $( "#chapter-display-options-modal" ).after(module);

    let pMonitor = (value, maxvalue) => progressMonitor($('#progress-bar-scan', module), value, maxvalue);
    pMonitor(0,100);

    $('#novel-display-conflicts-modal').on('shown.bs.modal', scan = function(e) {
        $('#novel-display-conflicts-modal').off('shown.bs.modal', scan);
        loadLNMTLGlossary(window.location.toString(), function (dict, changes, metaDict) { return Promise.resolve(analyseGlossary(dict, metaDict, pMonitor));}, true)
            .then((results) => modal_body.append(createAnalyzerReport(results)))
            .then(() => setTimeout(() => $('#collapseProgressbar').collapse('hide'),1000))
            .then(() => $('#novel-display-conflicts-modal [data-toggle="popover"]').popover({container: "#popoverConflictTarget"}));
    });

    $('#novel-display-conflicts-modal').on('hidden.bs.modal', () => $('#novel-display-conflicts-button').focus(function() { this.blur();}));

    $('#chapter-display-conflicts-modal > .modal').modal("show");
    $('#collapseProgressbar').collapse();
    //addAnalyseBotton();
}

function progressMonitor(progressBar, value, maxvalue) {
    let pvalue = ~~(value / maxvalue * 100);
    progressBar.css('width', pvalue+'%').attr('aria-valuenow', pvalue);
}

function addStyleSettings() {
    let reduceContrastChecked = "";
    let noWordWrapChecked = "";
    if(reduceContrast)
        reduceContrastChecked = "checked";
    if(noWordWrap)
        noWordWrapChecked = "checked";

    let title = $("<h3>Styling</h3>");
    let row = $('<div class="row"></div>');
    let col = $('<div class="col-xs-6"></div>');
    let option = $('<sub><input id="reduceContrast" type="checkbox" '+reduceContrastChecked+'></sub> <label for="reduceContrast"> Reduce contrast of comment info box</label>');
    option.on("change", function(){updateContrast($("#reduceContrast")[0].checked);});
    let col2 = $('<div class="col-xs-6"></div>');
    let option2 = $('<sub><input id="noWordWrap" type="checkbox" '+noWordWrapChecked+'></sub> <label for="noWordWrap">Do not wrap words </label>');
    option2.on("change", function(){updateWordWrap($("#noWordWrap")[0].checked);});
    col.append(option);
    col2.append(option2);
    row.append(col2);
    if($('#chapter-display-options-modal div.modal-body select.form-control[data-type=backgroundColor]').val() === "dark")
        row.append(col);
    $("#chapter-display-options-modal .modal-body").append(title).append(row);
}

function addGlobalStyle(css) {
    let head, style;
    head = document.getElementsByTagName('head')[0];
    if (!head) { return; }
    style = document.createElement('style');
    style.type = 'text/css';
    style.innerHTML = css;
    head.appendChild(style);
}

function swapStr(str, first, last){
    return str.substr(0, first)
        + str[last]
    //+ str.substring(first+1, last)
        + str[first]
        + str.substr(last+1);
}

function swapAt(str, at){
    const pos = str.indexOf(at,2); // prevent replace on small strings (length < 4)
    return (pos > 0 && pos < str.length-1) ? str : swapStr(str, pos-1, pos+1);
}

function smartGlossary(dict){
    if(rawsReplaced)
        return;
    let exit = false;
    const ofRegExp = /çš„/g;
    const itRegExp = /之/g;

    let smartDict = [];
    for(let i in dict){
        if (dict[i].raw.length === 0)
            continue;
        let smartReplace = false;
        $('.original:contains("'+dict[i].raw+'")').each(function(){
            let dictRaw = dict[i].raw;
            let raw = this.innerText;
            let original = $(this);
            let translation = original.prev('.translated');
            let entry = new RegExp(escapeRegExp(dictRaw), 'g');
            let rawmatch = raw.match(entry);
            if (!rawmatch)
                return;
            let numOriginal = rawmatch.length;
            let traw = "";
            translation.find('w,t').each(function(){
                traw = traw + $(this).attr( 'data-title' );
            });
            let numTranslation = traw.count(entry);
            let trawClean = traw.replace(entry,"");
            if(numOriginal !== numTranslation){
                let anchorChars = "";
                let prependChars = "";
                let appendChars = "";

                //check if only "的" or "之" were omitted and maybe re-ordered
                const testRaws = [['çš„', ofRegExp, dictRaw.replace(ofRegExp,"")],
                                  ['之', itRegExp, dictRaw.split('之').reverse().join('')],
                                  ['之', itRegExp, dictRaw.replace(itRegExp,"")],
                                  ['之', itRegExp, swapAt(dictRaw, '之')]];

                let smartRestore = false;

                for(let j = 0; j < testRaws.length; j += 1){
                    const testParams = testRaws[j];
                    const testChar = testParams[0], testRegEx = testParams[1], testRaw =testParams[2];

                    if (dictRaw.includes(testChar) && numOriginal === traw.count(testRaw) && raw.count(testRaw) === 0)
                    {
                        // console.log(testRaw + " | " + traw.count(testRegEx) + " + " + numOriginal + " <= " + raw.count(testRegEx));
                        if(traw.count(testRegEx) + numOriginal <= raw.count(testRegEx))
                            anchorChars = testRaw;
                        dictRaw = testRaw;
                        smartRestore = true;
                        break;
                    }
                }

                if (!smartRestore){
                    let xpendMissing = numOriginal - numTranslation;
                    for(let j = 0; j < dictRaw.length; j++)
                    {
                        let char = dictRaw.charAt(j);
                        let charReg = new RegExp(escapeRegExp(anchorChars + char), 'g');
                        let numOrigChar = raw.count(charReg);
                        //console.log( "#O " + numOriginal + " #OC " +numOrigChar);
                        if (numOriginal === numOrigChar){ //use as possible anchor
                            if (numOriginal === trawClean.count(charReg) + numTranslation){ //is likely safe anchor
                                anchorChars += char;
                                continue;
                            }
                        }

                        //check if xpend chars are missing or just relocated.
                        if(traw.count(char) + xpendMissing > raw.count(char)){
                            anchorChars=""; //remove anchor, unsafe xpend!
                            //console.log((traw.count(char) + xpendMissing) + ">" + (raw.count(char))+ " | " + xpendMissing + " ! " + dictRaw +" : "+char+ "\n" + raw);
                            break;
                        }

                        if(anchorChars.length === 0)
                            prependChars += char;
                        else{
                            //if not anchor char it has to be appended
                            appendChars += char;
                            //console.log("_"+anchorChars+"$ "+prependChars +" | "+ char);
                        }

                    }
                }

                let safetyCheck;
                try { //firefox & safari have no lookback
                    safetyCheck = new RegExp("(?<=[^" + escapeRegExp(prependChars) + "]|^)" + escapeRegExp(anchorChars) + "(?=[^" + escapeRegExp(appendChars) + "]|$)", 'g');
                } catch (error) {
                    safetyCheck = new RegExp("(?:[^" + escapeRegExp(prependChars) + "]|^)" + escapeRegExp(anchorChars) + "(?=[^" + escapeRegExp(appendChars) + "]|$)", 'g');
                }

                let safetySize = traw.count(safetyCheck);
                if((prependChars.length > 0 || appendChars.length > 0 || dictRaw !== dict[i].raw) && anchorChars.length > 0 && safetySize === (numOriginal - numTranslation)){
                    smartReplace = true;
                    // console.log("!" + anchorChars + " | " + safetySize + " === " + numOriginal + " - " + numTranslation + " | " + safetyCheck);
                    let type = dict[i].overwrite === "TRUE" ? '':'w';
                    let firstChar = anchorChars.charAt(0).replace(/[!"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]/, "\\$&");
                    translation.find(type+'[data-title^='+firstChar+']').each(function(){
                        let node = $(this);
                        let rawattr = node.attr("data-title");
                        let toCleanup = [];
                        if(dict[i].overwrite === "TRUE"){
                            type = 'w, t';
                        }
                        while(rawattr.length < anchorChars.length && node.next(type).length > 0){
                            let nextNode = node[0].nextSibling; //to check later for textnodes
                            node = node.next(type);
                            rawattr += node.attr("data-title");
                            // skip fill words from translation like "the", "can", whitespaces...
                            while (nextNode && nextNode.nodeType == 3) {
                                toCleanup.push(nextNode);
                                nextNode = node.nextSibling;
                            }
                            toCleanup.push(node);
                        }
                        if(rawattr == anchorChars){
                            let textPlaceholder = "";
                            for(let j in toCleanup){
                                textPlaceholder += $(toCleanup[j]).text();
                                toCleanup[j].remove();
                            }
                            if(textPlaceholder.length >0)
                                $(this).text($(this).text() + textPlaceholder);
                            $(this).attr('data-title', dict[i].raw);
                        }
                    });
                }
                //console.log(numOriginal + " <> " + numTranslation + ": " + dictRaw + " || " + anchorChars + " + " + appendChars);
            }
        });

        for(let j in smartDict)
        {
            if(smartReplace === true)
                break;
            if (dict[i].raw.includes(smartDict[j].raw))
                smartReplace = true;
        }
        if(smartReplace === true)
            smartDict.push(dict[i]);

    }

    //replace terms for "fixed" Raws
    replaceTerms(smartDict, false);
}

function rawsReplace(){
    if(replaceInOriginal && !rawsReplaced) {
        const dict = appliedTerms;

         /* replace raws in original text popover */
        const origReplacer = function(origNode, term, dir){
            let node = $(origNode);
            let raw = node.text();
            let toCleanup = [];

            let findSibling = node => node[0].nextSibling;
            let isBorderSibling = (siblingRaw, termRaw) => siblingRaw.startsWith(termRaw);
            let removeSiblingRaw = (siblingText, termRaw, foundRaw) => siblingText.substring(termRaw.length - foundRaw.length);

            if (dir !== "next")
            {
                findSibling = node => node[0].previousSibling;
                isBorderSibling = (siblingRaw, termRaw) => siblingRaw.endsWith(termRaw);
                removeSiblingRaw = (siblingText, termRaw, foundRaw) => siblingText.slice(0, foundRaw.length - termRaw.length);
            }

            while(raw.length < term.raw.length && findSibling(node) !== null){
                let siblingNode = findSibling(node); //to check later for textnodes
                node = $(siblingNode);
                if (siblingNode.nodeType == 3){
                    let siblingRaw = (dir === "next") ? raw + siblingNode.data : siblingNode.data + raw;
                    if ( siblingRaw.length > term.raw.length && isBorderSibling(siblingRaw, term.raw) ) {
                        siblingNode.data = removeSiblingRaw(siblingNode.data, term.raw, raw);
                        raw = term.raw;
                    }
                    else if ( siblingRaw.length <= term.raw.length && isBorderSibling(term.raw, siblingRaw)) {
                        raw = siblingRaw;
                        toCleanup.push(siblingNode);
                    }
                }
                else {
                    raw = (dir === "next") ? raw + node.text() : node.text() + raw;
                    toCleanup.push(node);
                }
            }


            if(raw == term.raw){
                for(let j in toCleanup){
                    toCleanup[j].remove();
                }
                let original = $("<t></t>").text(term.raw);
                original.attr("data-title", term.meaning);
                $(origNode).after(original);
                $(origNode).remove();
                //fix click
                $(original).popover({trigger: 'click', placement: 'top', html : true});
            }
        }

        for(let i in dict){
            if (dict[i].raw.length === 0)
                continue;

            /* TODO: remove all <t> that are in the middle off the raw
            $('.original:contains("'+dict[i].raw+'")').find('t,w').each(function () {
                if (dict[i].overwrite === "TRUE") && dict[i].raw.includes($(this).text().slice(1,-1) && sibling has remaining raw)
                  remove <t,w>
            }
            */

            //$('.original:contains("'+dict[i].raw+'")').find().andSelf().contents().each ..
            $('.original:contains("'+dict[i].raw+'")').contents().each(function() {
                if (this.nodeType === 3 && this.data.includes(dict[i].raw)) {
                    let text = $(this).text();
                    $(this).replaceWith(text.replace(new RegExp(escapeRegExp(dict[i].raw), "g"), '<w data-title="' + dict[i].raw + '">'+dict[i].meaning+'</w>'));
                }
            });

            if(dict[i].overwrite === "TRUE"){
              $('.original:contains("'+dict[i].raw+'")').find('t').each(function () {
                  if (dict[i].raw.startsWith($(this).text()))
                      origReplacer(this, dict[i], "next");
                  else if (dict[i].raw.endsWith($(this).text()))
                      origReplacer(this, dict[i], "prev")} );
            }
        }

        //add popover in former textnodes
        $('.original w').each( function() { $(this).popover({trigger: 'click', placement: 'top', html : true})} );

        appliedTerms = [];

        $('.original t').each(function() {
            let replacementNode = $("<t></t>").text($(this).attr('data-title'));
            replacementNode.attr("data-title", $(this).text());
            $(this).after(replacementNode);
            $(this).remove();
            //fix click
            $(replacementNode).popover({trigger: 'click', placement: 'top', html : true});
        });

        const regLatinPrev = /[a-zA-Z0-9]$/;
        const regLatinNext = /^[a-zA-Z0-9]/;
        // add whitespace between two english words
        $('.original').find('t,w').filter( function(index){var prev = $(this).get(0).previousSibling; return prev ? prev.nodeName === 'T' || prev.nodeName === 'W' : false;}).each(function(){$(this).text(' ' + $(this).text());});
        $('.original').find('t,w').filter( function(index){var prev = $(this).get(0).previousSibling; return prev ? prev.nodeType === 3 && prev.data != '' && regLatinPrev.test(prev.data) : false;}).each(function(){$(this).text(' ' + $(this).text());});
        $('.original').find('t,w').filter( function(index){var next = $(this).get(0).nextSibling; return next ? next.nodeType === 3 && next.data != '' && regLatinNext.test(next.data) : false;}).each(function(){$(this).text($(this).text() + ' ');});

        rawsReplaced = true;
    }
    window.performance.mark('userjs_UGMTLComplete');
    storeSession("userjs_UGMTLComplete", new Date().getTime());
    document.dispatchEvent(new Event('userjs_UGMTLComplete'));
}

function applyAsyncDict(dict, forceReplace){
    forceReplace = forceReplace || false;

    if(replaceInOriginal && !rawsReplaced)
        appliedTerms = appliedTerms.concat(dict);

    return new Promise( resolve => {
        setTimeout(async function(){
            for(let i = 0; i*glossaryChunkSize < dict.length; i++){
                if ( i != 0 ) {
                    await sleep(200);
                }
                applyGlossary(dict.slice(i*glossaryChunkSize, (i+1)*glossaryChunkSize), forceReplace);
            }
            resolve();
        }, 1);
    });
}

function filteredTerms(dict){
    //const filtering_time = new Date().getTime();
    const rawText = $('.original').text();

    let filteredDict = [];
    for (var i = 0; i < dict.length; i++) {
        if (rawText.includes(dict[i].raw)) {
            filteredDict.push(dict[i]);
        }
    }

    //console.log("filtered " + (dict.length - filteredDict.length) + " of " + dict.length + " terms in " + (new Date().getTime() - filtering_time) + "ms");
    return filteredDict;
}

const OVERWRITE = {overwrite:"TRUE"};
function applyGlossaryAsync(dict, changeCount){
    if (filterTerms)
        dict = filteredTerms(dict);
    if (autoSolveConflicts)
        dict = dict.map( item => { return {...item, ...OVERWRITE}; });
    if (useFullDict)
        changeCount = dict.length;
    if (changeCount == 0)
        dict = [];

    dict = dict.slice(-changeCount);

    return applyAsyncDict(dict).then(() => console.log("Latest LNMTL glossary entries applied (" + dict.length + " terms)"));
}

function waitApplyPersonalGlossary(dict){
    return new Promise( resolve => {
        LNMTLGlossaryLock.then(() => {
            applyAsyncDict(dict, true).then(() => resolve());
        });
    });
}

function applyGlossary(dict, forceReplace){
    forceReplace = forceReplace || false;
    replaceTerms(dict, forceReplace && !useMultiStepReplacer);
    smartGlossary(dict);
    if (forceReplace && useMultiStepReplacer)
        replaceTerms(dict, true);
}

function replaceTerms(dict, forceReplace){
    forceReplace = forceReplace || false
    let simplereplace = false;

    for(let i in dict){
        dict[i].raw = dict[i].raw.trim();

        if(dict[i].raw.length === 0)
            continue;

        let firstChar = dict[i].raw.charAt(0).replace(/[!"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]/, "\\$&");

        let type = 'w';
        if(dict[i].overwrite === "TRUE"){
            type = '';
        }
        const replacer = function(origNode) {
            let node = $(origNode);
            let raw = node.attr("data-title");
            if(simplereplace && raw == dict[i].raw){
                node.text(dict[i].meaning);
                return;
            }
            let toCleanup = [];
            if(dict[i].overwrite === "TRUE"){
                type = 'w, t';
            }

            while(raw.length < dict[i].raw.length && node.next(type).length > 0){
                let nextNode = node[0].nextSibling; //to check later for textnodes
                node = node.next(type);
                // console.log(node.text() + node.attr("data-title"));
                raw += node.attr("data-title");
                // skip fill words from translation like "the", "can", whitespaces...
                while (nextNode && nextNode.nodeType == 3) {
                    //console.log("textnode: " + nextNode.data);
                    toCleanup.push(nextNode);
                    nextNode = nextNode.nextSibling;
                }

                toCleanup.push(node);

            }

            if(raw == dict[i].raw){
                for(let j in toCleanup){
                    toCleanup[j].remove();
                }
                let text = dict[i].meaning;
                if(origNode.previousSibling.nodeValue === ". " || $(origNode).prev().length === 0 && (origNode.previousSibling === null || origNode.previousSibling.nodeValue === '\n') && ($(origNode).parent('sentence').length > 0 || ($(origNode).parent('dq').length > 0 && $(origNode).parent('dq').prev().length === 0 ))){
                    text = capitalize(text);
                }

                let translate = $("<t></t>").text(text);
                translate.attr("data-title", dict[i].raw);
                $(origNode).after(translate);
                $(origNode).remove();
                //fix click
                $(translate).popover({trigger: 'click', placement: 'top', html : true});
                //translate.click(function(){ $('t').popover({trigger: 'click', placement: 'top', html : true, container: 't'});});
            }
        };

        if(forceReplace)
            $('.translated '+type+'[data-title^='+firstChar+']').each(function() {replacer(this)}); //does not prevent wrong replacing due to rearanged/missing characters
        else
            $('.original:contains("'+dict[i].raw+'")').prev('.translated').find(type+'[data-title^='+firstChar+']').each(function() {replacer(this)});
    }
}

function novelSheet(tabletop, novelname){
    let sheetnames = tabletop.modelNames.join('|').toLowerCase().replace(/ /g,"-").split('|');
    let sheetindex = sheetnames.indexOf(novelname);
    if (sheetindex !== -1) {
        let sheetname = tabletop.modelNames[sheetindex];
        if(tabletop.sheets(sheetname) !== undefined){
            let columnNames = tabletop.sheets(sheetname).columnNames;
            if(columnNames.indexOf("raw") !== -1 && columnNames.indexOf("meaning") !== -1)
                return waitApplyPersonalGlossary(tabletop.sheets(sheetname).elements).then(() => console.log("Applied glossary: " + sheetname));
            console.log("Error: column-names are incorrect ("+ columnNames + ")");
        }
    }
    return Promise.resolve();
}

function singleSheet(results, sheetname){
    sheetname = sheetname || "sheet1";
    let columnNames = results.meta.fields;
    if(columnNames.indexOf("raw") !== -1 && columnNames.indexOf("meaning") !== -1){
        const data = results.data.filter(term => term.raw && term.meaning); /* optional check */
        return waitApplyPersonalGlossary(data).then(() => console.log("Applied glossary: " + sheetname + " ("+ results.data.length + " terms)"));
    }
    console.log("Error: column-names are incorrect ("+ columnNames + ")");
    return Promise.resolve();
}

function useDataV4(data) {
    const sheets = data.sheets.map(sheet => ({ title: sheet.properties.title, id: sheet.properties.sheetId}));

    if(sheets.length === 1){
        let sheetname = sheets[0].name;
        return novelSheetV4(sheets, sheetname).then(() => rawsReplace());
    }

    let novelname = window.location.pathname.split("/").pop();
      novelname = novelname.substr(0,novelname.lastIndexOf("-chapter-"));
      let book_index = novelname.lastIndexOf("-book-");
      if(book_index > novelname.length - 10 && book_index > 0){
          novelname = novelname.substr(0, book_index);
      }


    novelSheetV4(sheets, novelname).then(() => novelSheetV4(sheets, "global")).then(() => rawsReplace());

}

function extractColumns(arr) {
    if (!Array.isArray(arr) || arr.length == 0)
        return arr;

    const needColumns = new Set(tableColumnFilter);
    const usedColums = new Array();

    arr[0].forEach( (value, index ) => { if (needColumns.delete(value)) usedColums.push(index); });

    return arr.map(r => usedColums.map(i => r[i]));
}

function novelSheetV4(sheets, novelname){
    const sheet = sheets.find(sheet => sheet.title.trim().toLowerCase().replace(/ /g,"-").replace(/,/g,"") === novelname);

    if (!sheet)
        return Promise.resolve();

    return new Promise( resolve => {
        fetchGoogle(googleAPIKey, fetchSheet, result => {
            singleSheet(Papa.parse(Papa.unparse(extractColumns(result.values)),{ header: true }), sheet.title).then(() => resolve())
        }, { sheetID: sheet.title } );
    });
}


function useData(data, tabletop) {
    let sheets = Object.keys(data);
    if(sheets.length === 1){
        let columnNames = tabletop.sheets(sheets[0]).columnNames;
        if(columnNames.indexOf("raw") !== -1 && columnNames.indexOf("meaning") !== -1)
            return waitApplyPersonalGlossary(tabletop.sheets(sheets[0]).elements).then(() => console.log("Applied glossary: " + sheets[0])).then(() => rawsReplace());
        console.log("Error: column-names are incorrect ("+ columnNames + ")");
    }

    let novelname = window.location.pathname.split("/").pop();
    novelname = novelname.substr(0,novelname.lastIndexOf("-chapter-"));
    let book_index = novelname.lastIndexOf("-book-");
    if(book_index > novelname.length - 10 && book_index > 0){
        novelname = novelname.substr(0, book_index);
    }

    novelSheet(tabletop, novelname).then(() => novelSheet(tabletop, "global")).then(() => rawsReplace());
}

function applySetting(){
    changeWordWrap(noWordWrap);
    changeContrast(reduceContrast);
}

function addCustomStyles(){
    // no word wrapping
    addGlobalStyle('body .chapter-body.noWordWrapping { -webkit-hyphens: none; -ms-hyphens: none; hyphens: none; }');

    // reduced contrast (of comment box)
    addGlobalStyle('.reducedContrast.backgroundColorDark .alert-info , .reducedContrast.backgroundColorBlack .alert-info { background-color: #2d323f; border-color: #145461}');
    // bugfixes
    addGlobalStyle('.backgroundColorBright .tt-input  { color: #464444 !important;}');
    addGlobalStyle('.fontColorGray .chapter-body w.postfixed { color: #95a597 !important; }');
    addGlobalStyle('.fontColorGray .chapter-title {color: #9b9b9b;}');
    addGlobalStyle('.fontColorGray .chapter-title {color: #9b9b9b;}');
    addGlobalStyle('.col-xs-6 label {vertical-align: text-top;}');
    // glossary analyzer
    addGlobalStyle('.progress-bar-animated {animation: progress-bar-stripes 1s linear infinite;}');
    addGlobalStyle('.backgroundColorBlack #novel-display-conflicts-modal .table>tbody>tr.active>td, .backgroundColorBlack #novel-display-conflicts-modal .table>tbody>tr.active>th { background-color: #DDD !important; padding: 1px !important;}');
    addGlobalStyle('.backgroundColorDark #novel-display-conflicts-modal .table>tbody>tr.active>td, .backgroundColorDark #novel-display-conflicts-modal .table>tbody>tr.active>th { background-color: #DDD !important; padding: 1px !important;}');

}

function changeWordWrap(on){
    if(on === true)
        $('body .chapter-body').addClass("noWordWrapping");
    if(on === false)
        $('body .chapter-body').removeClass("noWordWrapping");
}

function changeContrast(on){
    if(on === true)
        $('body').addClass("reducedContrast");
    if(on === false)
        $('body').removeClass("reducedContrast");
}

function isDev(){
    return (typeof GM_getValue === 'function') && $("#navbar").find("ul > li > a > span.hidden-sm").text().endsWith("Kronos");
}

(async function() {
    'use strict';

    addCustomStyles();
    if (!disableGlossaryAnalyzer && $("#navbar a:contains('Login')").length == 0)
        addAnalyseButton();
    applySetting();
    addCloseSettingsListener();
    addUserGlossaryLink();
    addGlossarySettings();
    addStyleSettings();

    const data = ":root";
    if (window.location.pathname == "/" && ($("#navbar a:contains('Login')").length == 0 && (useLNMTLGlossary || !disableGlossaryAnalyzer))) {
        $("main div.panel-heading > .panel-title", $(data)).parent().next().find("div.media div.media-body .media-title [href^='https://lnmtl.com/novel/']").each((index, value) => createGlossaryStatsLabel($(value)));
    }

    if (window.location.pathname == "/novel" && ($("#navbar a:contains('Login')").length == 0 && (useLNMTLGlossary || !disableGlossaryAnalyzer))) {
        $("main div.media-body .media-title [href^='https://lnmtl.com/novel/']", $(data)).each((index, value) => createGlossaryStatsLabel($(value)));
    }

    if ((window.location.pathname.startsWith("/novel/") || window.location.pathname.startsWith("novel/"))
        && ($("#navbar a:contains('Login')").length == 0 && (useLNMTLGlossary || !disableGlossaryAnalyzer))) {
        let url = window.location.toString();
        let novelData = await restore("ndata" + url);
        let timeNow = new Date().getTime();
        if (novelData === null )
            novelData = { changes: 0 };
        console.log("update novel metadata: " + url);
        let old_changes = novelData.changes;
        let old_retrans = novelData.retrans;
        let novelinfo = $('.panel.panel-default .progress-bar[role="progressbar"]', $(data));
        let glossary_changes = novelinfo.text().trim().split(' / ');
        let retranslation_time = new Date($(".panel.panel-default dt:contains('Latest retranslation at') ~ * span", $(data)).text().replace(/\-/g, '/')).getTime();
        let novel_id = $("a.btn.btn-primary[href^='https://lnmtl.com/termProposition?novel_id=']", $(data)).attr("href").split("https://lnmtl.com/termProposition?novel_id=")[1];
        novelData.time = timeNow;
        novelData.changes = glossary_changes[0];
        novelData.retrans = retranslation_time;
        novelData.novel_id = novel_id;
        if (novelData.glossary_length && old_retrans == novelData.retrans && novelData.changes - old_changes > 0){
            novelData.glossary_length += novelData.changes - old_changes;
            novelData.length_estimated = true;
        }
        else if (novelData.glossary_length && old_retrans !== novelData.retrans){
            delete novelData.glossary_length;
            delete novelData.length_estimated;
        }
        await store("ndata" + url, novelData);
        if (novelData.changes - old_changes != 0 || old_retrans !== novelData.retrans)
            removeStorage("gdata" + novelData.novel_id);
        if (!disableGlossaryAnalyzer)
            addGlossaryAnalyseModal();
        addGlossaryStatsInfo(novelinfo.closest("dl").parent());
    }

    if (window.location.pathname.startsWith("/chapter/") || window.location.pathname.startsWith("chapter/")){
        let novelname = window.location.pathname.split("/").pop();
        novelname = novelname.substr(0,novelname.lastIndexOf("-chapter-"));
        let book_index = novelname.lastIndexOf("-book-");
        if(book_index > novelname.length - 10 && book_index > 0){
            novelname = novelname.substr(0, book_index);
        }

        if (useLNMTLGlossary){
            let novelurl = $("#chapter-container .chapter-head .dashhead-titles a[href]").first().attr("href") || "https://lnmtl.com/novel/" + novelname;
            LNMTLGlossaryLock = lock(() => loadLNMTLGlossary(novelurl));
        }

        fetch_glossary(); //google docs glossary
        if (publicSpreadsheetUrl.length === 0) {
          rawsReplace();
        }
    }
})();

})();

if (typeof localforage === 'undefined') /* iOS workaround */ {
    /*! localForage -- Offline Storage, Improved Version 1.10.0 https://localforage.github.io/localForage (c) 2013-2017 Mozilla, Apache License 2.0 */
    !function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;b="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,b.localforage=a()}}(function(){return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c||a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g<d.length;g++)e(d[g]);return e}({1:[function(a,b,c){(function(a){"use strict";function c(){k=!0;for(var a,b,c=l.length;c;){for(b=l,l=[],a=-1;++a<c;)b[a]();c=l.length}k=!1}function d(a){1!==l.push(a)||k||e()}var e,f=a.MutationObserver||a.WebKitMutationObserver;if(f){var g=0,h=new f(c),i=a.document.createTextNode("");h.observe(i,{characterData:!0}),e=function(){i.data=g=++g%2}}else if(a.setImmediate||void 0===a.MessageChannel)e="document"in a&&"onreadystatechange"in a.document.createElement("script")?function(){var b=a.document.createElement("script");b.onreadystatechange=function(){c(),b.onreadystatechange=null,b.parentNode.removeChild(b),b=null},a.document.documentElement.appendChild(b)}:function(){setTimeout(c,0)};else{var j=new a.MessageChannel;j.port1.onmessage=c,e=function(){j.port2.postMessage(0)}}var k,l=[];b.exports=d}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],2:[function(a,b,c){"use strict";function d(){}function e(a){if("function"!=typeof a)throw new TypeError("resolver must be a function");this.state=s,this.queue=[],this.outcome=void 0,a!==d&&i(this,a)}function f(a,b,c){this.promise=a,"function"==typeof b&&(this.onFulfilled=b,this.callFulfilled=this.otherCallFulfilled),"function"==typeof c&&(this.onRejected=c,this.callRejected=this.otherCallRejected)}function g(a,b,c){o(function(){var d;try{d=b(c)}catch(b){return p.reject(a,b)}d===a?p.reject(a,new TypeError("Cannot resolve promise with itself")):p.resolve(a,d)})}function h(a){var b=a&&a.then;if(a&&("object"==typeof a||"function"==typeof a)&&"function"==typeof b)return function(){b.apply(a,arguments)}}function i(a,b){function c(b){f||(f=!0,p.reject(a,b))}function d(b){f||(f=!0,p.resolve(a,b))}function e(){b(d,c)}var f=!1,g=j(e);"error"===g.status&&c(g.value)}function j(a,b){var c={};try{c.value=a(b),c.status="success"}catch(a){c.status="error",c.value=a}return c}function k(a){return a instanceof this?a:p.resolve(new this(d),a)}function l(a){var b=new this(d);return p.reject(b,a)}function m(a){function b(a,b){function d(a){g[b]=a,++h!==e||f||(f=!0,p.resolve(j,g))}c.resolve(a).then(d,function(a){f||(f=!0,p.reject(j,a))})}var c=this;if("[object Array]"!==Object.prototype.toString.call(a))return this.reject(new TypeError("must be an array"));var e=a.length,f=!1;if(!e)return this.resolve([]);for(var g=new Array(e),h=0,i=-1,j=new this(d);++i<e;)b(a[i],i);return j}function n(a){function b(a){c.resolve(a).then(function(a){f||(f=!0,p.resolve(h,a))},function(a){f||(f=!0,p.reject(h,a))})}var c=this;if("[object Array]"!==Object.prototype.toString.call(a))return this.reject(new TypeError("must be an array"));var e=a.length,f=!1;if(!e)return this.resolve([]);for(var g=-1,h=new this(d);++g<e;)b(a[g]);return h}var o=a(1),p={},q=["REJECTED"],r=["FULFILLED"],s=["PENDING"];b.exports=e,e.prototype.catch=function(a){return this.then(null,a)},e.prototype.then=function(a,b){if("function"!=typeof a&&this.state===r||"function"!=typeof b&&this.state===q)return this;var c=new this.constructor(d);if(this.state!==s){g(c,this.state===r?a:b,this.outcome)}else this.queue.push(new f(c,a,b));return c},f.prototype.callFulfilled=function(a){p.resolve(this.promise,a)},f.prototype.otherCallFulfilled=function(a){g(this.promise,this.onFulfilled,a)},f.prototype.callRejected=function(a){p.reject(this.promise,a)},f.prototype.otherCallRejected=function(a){g(this.promise,this.onRejected,a)},p.resolve=function(a,b){var c=j(h,b);if("error"===c.status)return p.reject(a,c.value);var d=c.value;if(d)i(a,d);else{a.state=r,a.outcome=b;for(var e=-1,f=a.queue.length;++e<f;)a.queue[e].callFulfilled(b)}return a},p.reject=function(a,b){a.state=q,a.outcome=b;for(var c=-1,d=a.queue.length;++c<d;)a.queue[c].callRejected(b);return a},e.resolve=k,e.reject=l,e.all=m,e.race=n},{1:1}],3:[function(a,b,c){(function(b){"use strict";"function"!=typeof b.Promise&&(b.Promise=a(2))}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{2:2}],4:[function(a,b,c){"use strict";function d(a,b){if(!(a instanceof b))throw new TypeError("Cannot call a class as a function")}function e(){try{if("undefined"!=typeof indexedDB)return indexedDB;if("undefined"!=typeof webkitIndexedDB)return webkitIndexedDB;if("undefined"!=typeof mozIndexedDB)return mozIndexedDB;if("undefined"!=typeof OIndexedDB)return OIndexedDB;if("undefined"!=typeof msIndexedDB)return msIndexedDB}catch(a){return}}function f(){try{if(!ua||!ua.open)return!1;var a="undefined"!=typeof openDatabase&&/(Safari|iPhone|iPad|iPod)/.test(navigator.userAgent)&&!/Chrome/.test(navigator.userAgent)&&!/BlackBerry/.test(navigator.platform),b="function"==typeof fetch&&-1!==fetch.toString().indexOf("[native code");return(!a||b)&&"undefined"!=typeof indexedDB&&"undefined"!=typeof IDBKeyRange}catch(a){return!1}}function g(a,b){a=a||[],b=b||{};try{return new Blob(a,b)}catch(f){if("TypeError"!==f.name)throw f;for(var c="undefined"!=typeof BlobBuilder?BlobBuilder:"undefined"!=typeof MSBlobBuilder?MSBlobBuilder:"undefined"!=typeof MozBlobBuilder?MozBlobBuilder:WebKitBlobBuilder,d=new c,e=0;e<a.length;e+=1)d.append(a[e]);return d.getBlob(b.type)}}function h(a,b){b&&a.then(function(a){b(null,a)},function(a){b(a)})}function i(a,b,c){"function"==typeof b&&a.then(b),"function"==typeof c&&a.catch(c)}function j(a){return"string"!=typeof a&&(console.warn(a+" used as a key, but it is not a string."),a=String(a)),a}function k(){if(arguments.length&&"function"==typeof arguments[arguments.length-1])return arguments[arguments.length-1]}function l(a){for(var b=a.length,c=new ArrayBuffer(b),d=new Uint8Array(c),e=0;e<b;e++)d[e]=a.charCodeAt(e);return c}function m(a){return new va(function(b){var c=a.transaction(wa,Ba),d=g([""]);c.objectStore(wa).put(d,"key"),c.onabort=function(a){a.preventDefault(),a.stopPropagation(),b(!1)},c.oncomplete=function(){var a=navigator.userAgent.match(/Chrome\/(\d+)/),c=navigator.userAgent.match(/Edge\//);b(c||!a||parseInt(a[1],10)>=43)}}).catch(function(){return!1})}function n(a){return"boolean"==typeof xa?va.resolve(xa):m(a).then(function(a){return xa=a})}function o(a){var b=ya[a.name],c={};c.promise=new va(function(a,b){c.resolve=a,c.reject=b}),b.deferredOperations.push(c),b.dbReady?b.dbReady=b.dbReady.then(function(){return c.promise}):b.dbReady=c.promise}function p(a){var b=ya[a.name],c=b.deferredOperations.pop();if(c)return c.resolve(),c.promise}function q(a,b){var c=ya[a.name],d=c.deferredOperations.pop();if(d)return d.reject(b),d.promise}function r(a,b){return new va(function(c,d){if(ya[a.name]=ya[a.name]||B(),a.db){if(!b)return c(a.db);o(a),a.db.close()}var e=[a.name];b&&e.push(a.version);var f=ua.open.apply(ua,e);b&&(f.onupgradeneeded=function(b){var c=f.result;try{c.createObjectStore(a.storeName),b.oldVersion<=1&&c.createObjectStore(wa)}catch(c){if("ConstraintError"!==c.name)throw c;console.warn('The database "'+a.name+'" has been upgraded from version '+b.oldVersion+" to version "+b.newVersion+', but the storage "'+a.storeName+'" already exists.')}}),f.onerror=function(a){a.preventDefault(),d(f.error)},f.onsuccess=function(){var b=f.result;b.onversionchange=function(a){a.target.close()},c(b),p(a)}})}function s(a){return r(a,!1)}function t(a){return r(a,!0)}function u(a,b){if(!a.db)return!0;var c=!a.db.objectStoreNames.contains(a.storeName),d=a.version<a.db.version,e=a.version>a.db.version;if(d&&(a.version!==b&&console.warn('The database "'+a.name+"\" can't be downgraded from version "+a.db.version+" to version "+a.version+"."),a.version=a.db.version),e||c){if(c){var f=a.db.version+1;f>a.version&&(a.version=f)}return!0}return!1}function v(a){return new va(function(b,c){var d=new FileReader;d.onerror=c,d.onloadend=function(c){var d=btoa(c.target.result||"");b({__local_forage_encoded_blob:!0,data:d,type:a.type})},d.readAsBinaryString(a)})}function w(a){return g([l(atob(a.data))],{type:a.type})}function x(a){return a&&a.__local_forage_encoded_blob}function y(a){var b=this,c=b._initReady().then(function(){var a=ya[b._dbInfo.name];if(a&&a.dbReady)return a.dbReady});return i(c,a,a),c}function z(a){o(a);for(var b=ya[a.name],c=b.forages,d=0;d<c.length;d++){var e=c[d];e._dbInfo.db&&(e._dbInfo.db.close(),e._dbInfo.db=null)}return a.db=null,s(a).then(function(b){return a.db=b,u(a)?t(a):b}).then(function(d){a.db=b.db=d;for(var e=0;e<c.length;e++)c[e]._dbInfo.db=d}).catch(function(b){throw q(a,b),b})}function A(a,b,c,d){void 0===d&&(d=1);try{var e=a.db.transaction(a.storeName,b);c(null,e)}catch(e){if(d>0&&(!a.db||"InvalidStateError"===e.name||"NotFoundError"===e.name))return va.resolve().then(function(){if(!a.db||"NotFoundError"===e.name&&!a.db.objectStoreNames.contains(a.storeName)&&a.version<=a.db.version)return a.db&&(a.version=a.db.version+1),t(a)}).then(function(){return z(a).then(function(){A(a,b,c,d-1)})}).catch(c);c(e)}}function B(){return{forages:[],db:null,dbReady:null,deferredOperations:[]}}function C(a){function b(){return va.resolve()}var c=this,d={db:null};if(a)for(var e in a)d[e]=a[e];var f=ya[d.name];f||(f=B(),ya[d.name]=f),f.forages.push(c),c._initReady||(c._initReady=c.ready,c.ready=y);for(var g=[],h=0;h<f.forages.length;h++){var i=f.forages[h];i!==c&&g.push(i._initReady().catch(b))}var j=f.forages.slice(0);return va.all(g).then(function(){return d.db=f.db,s(d)}).then(function(a){return d.db=a,u(d,c._defaultConfig.version)?t(d):a}).then(function(a){d.db=f.db=a,c._dbInfo=d;for(var b=0;b<j.length;b++){var e=j[b];e!==c&&(e._dbInfo.db=d.db,e._dbInfo.version=d.version)}})}function D(a,b){var c=this;a=j(a);var d=new va(function(b,d){c.ready().then(function(){A(c._dbInfo,Aa,function(e,f){if(e)return d(e);try{var g=f.objectStore(c._dbInfo.storeName),h=g.get(a);h.onsuccess=function(){var a=h.result;void 0===a&&(a=null),x(a)&&(a=w(a)),b(a)},h.onerror=function(){d(h.error)}}catch(a){d(a)}})}).catch(d)});return h(d,b),d}function E(a,b){var c=this,d=new va(function(b,d){c.ready().then(function(){A(c._dbInfo,Aa,function(e,f){if(e)return d(e);try{var g=f.objectStore(c._dbInfo.storeName),h=g.openCursor(),i=1;h.onsuccess=function(){var c=h.result;if(c){var d=c.value;x(d)&&(d=w(d));var e=a(d,c.key,i++);void 0!==e?b(e):c.continue()}else b()},h.onerror=function(){d(h.error)}}catch(a){d(a)}})}).catch(d)});return h(d,b),d}function F(a,b,c){var d=this;a=j(a);var e=new va(function(c,e){var f;d.ready().then(function(){return f=d._dbInfo,"[object Blob]"===za.call(b)?n(f.db).then(function(a){return a?b:v(b)}):b}).then(function(b){A(d._dbInfo,Ba,function(f,g){if(f)return e(f);try{var h=g.objectStore(d._dbInfo.storeName);null===b&&(b=void 0);var i=h.put(b,a);g.oncomplete=function(){void 0===b&&(b=null),c(b)},g.onabort=g.onerror=function(){var a=i.error?i.error:i.transaction.error;e(a)}}catch(a){e(a)}})}).catch(e)});return h(e,c),e}function G(a,b){var c=this;a=j(a);var d=new va(function(b,d){c.ready().then(function(){A(c._dbInfo,Ba,function(e,f){if(e)return d(e);try{var g=f.objectStore(c._dbInfo.storeName),h=g.delete(a);f.oncomplete=function(){b()},f.onerror=function(){d(h.error)},f.onabort=function(){var a=h.error?h.error:h.transaction.error;d(a)}}catch(a){d(a)}})}).catch(d)});return h(d,b),d}function H(a){var b=this,c=new va(function(a,c){b.ready().then(function(){A(b._dbInfo,Ba,function(d,e){if(d)return c(d);try{var f=e.objectStore(b._dbInfo.storeName),g=f.clear();e.oncomplete=function(){a()},e.onabort=e.onerror=function(){var a=g.error?g.error:g.transaction.error;c(a)}}catch(a){c(a)}})}).catch(c)});return h(c,a),c}function I(a){var b=this,c=new va(function(a,c){b.ready().then(function(){A(b._dbInfo,Aa,function(d,e){if(d)return c(d);try{var f=e.objectStore(b._dbInfo.storeName),g=f.count();g.onsuccess=function(){a(g.result)},g.onerror=function(){c(g.error)}}catch(a){c(a)}})}).catch(c)});return h(c,a),c}function J(a,b){var c=this,d=new va(function(b,d){if(a<0)return void b(null);c.ready().then(function(){A(c._dbInfo,Aa,function(e,f){if(e)return d(e);try{var g=f.objectStore(c._dbInfo.storeName),h=!1,i=g.openKeyCursor();i.onsuccess=function(){var c=i.result;if(!c)return void b(null);0===a?b(c.key):h?b(c.key):(h=!0,c.advance(a))},i.onerror=function(){d(i.error)}}catch(a){d(a)}})}).catch(d)});return h(d,b),d}function K(a){var b=this,c=new va(function(a,c){b.ready().then(function(){A(b._dbInfo,Aa,function(d,e){if(d)return c(d);try{var f=e.objectStore(b._dbInfo.storeName),g=f.openKeyCursor(),h=[];g.onsuccess=function(){var b=g.result;if(!b)return void a(h);h.push(b.key),b.continue()},g.onerror=function(){c(g.error)}}catch(a){c(a)}})}).catch(c)});return h(c,a),c}function L(a,b){b=k.apply(this,arguments);var c=this.config();a="function"!=typeof a&&a||{},a.name||(a.name=a.name||c.name,a.storeName=a.storeName||c.storeName);var d,e=this;if(a.name){var f=a.name===c.name&&e._dbInfo.db,g=f?va.resolve(e._dbInfo.db):s(a).then(function(b){var c=ya[a.name],d=c.forages;c.db=b;for(var e=0;e<d.length;e++)d[e]._dbInfo.db=b;return b});d=a.storeName?g.then(function(b){if(b.objectStoreNames.contains(a.storeName)){var c=b.version+1;o(a);var d=ya[a.name],e=d.forages;b.close();for(var f=0;f<e.length;f++){var g=e[f];g._dbInfo.db=null,g._dbInfo.version=c}return new va(function(b,d){var e=ua.open(a.name,c);e.onerror=function(a){e.result.close(),d(a)},e.onupgradeneeded=function(){e.result.deleteObjectStore(a.storeName)},e.onsuccess=function(){var a=e.result;a.close(),b(a)}}).then(function(a){d.db=a;for(var b=0;b<e.length;b++){var c=e[b];c._dbInfo.db=a,p(c._dbInfo)}}).catch(function(b){throw(q(a,b)||va.resolve()).catch(function(){}),b})}}):g.then(function(b){o(a);var c=ya[a.name],d=c.forages;b.close();for(var e=0;e<d.length;e++){d[e]._dbInfo.db=null}return new va(function(b,c){var d=ua.deleteDatabase(a.name);d.onerror=function(){var a=d.result;a&&a.close(),c(d.error)},d.onblocked=function(){console.warn('dropInstance blocked for database "'+a.name+'" until all open connections are closed')},d.onsuccess=function(){var a=d.result;a&&a.close(),b(a)}}).then(function(a){c.db=a;for(var b=0;b<d.length;b++)p(d[b]._dbInfo)}).catch(function(b){throw(q(a,b)||va.resolve()).catch(function(){}),b})})}else d=va.reject("Invalid arguments");return h(d,b),d}function M(){return"function"==typeof openDatabase}function N(a){var b,c,d,e,f,g=.75*a.length,h=a.length,i=0;"="===a[a.length-1]&&(g--,"="===a[a.length-2]&&g--);var j=new ArrayBuffer(g),k=new Uint8Array(j);for(b=0;b<h;b+=4)c=Da.indexOf(a[b]),d=Da.indexOf(a[b+1]),e=Da.indexOf(a[b+2]),f=Da.indexOf(a[b+3]),k[i++]=c<<2|d>>4,k[i++]=(15&d)<<4|e>>2,k[i++]=(3&e)<<6|63&f;return j}function O(a){var b,c=new Uint8Array(a),d="";for(b=0;b<c.length;b+=3)d+=Da[c[b]>>2],d+=Da[(3&c[b])<<4|c[b+1]>>4],d+=Da[(15&c[b+1])<<2|c[b+2]>>6],d+=Da[63&c[b+2]];return c.length%3==2?d=d.substring(0,d.length-1)+"=":c.length%3==1&&(d=d.substring(0,d.length-2)+"=="),d}function P(a,b){var c="";if(a&&(c=Ua.call(a)),a&&("[object ArrayBuffer]"===c||a.buffer&&"[object ArrayBuffer]"===Ua.call(a.buffer))){var d,e=Ga;a instanceof ArrayBuffer?(d=a,e+=Ia):(d=a.buffer,"[object Int8Array]"===c?e+=Ka:"[object Uint8Array]"===c?e+=La:"[object Uint8ClampedArray]"===c?e+=Ma:"[object Int16Array]"===c?e+=Na:"[object Uint16Array]"===c?e+=Pa:"[object Int32Array]"===c?e+=Oa:"[object Uint32Array]"===c?e+=Qa:"[object Float32Array]"===c?e+=Ra:"[object Float64Array]"===c?e+=Sa:b(new Error("Failed to get type for BinaryArray"))),b(e+O(d))}else if("[object Blob]"===c){var f=new FileReader;f.onload=function(){var c=Ea+a.type+"~"+O(this.result);b(Ga+Ja+c)},f.readAsArrayBuffer(a)}else try{b(JSON.stringify(a))}catch(c){console.error("Couldn't convert value into a JSON string: ",a),b(null,c)}}function Q(a){if(a.substring(0,Ha)!==Ga)return JSON.parse(a);var b,c=a.substring(Ta),d=a.substring(Ha,Ta);if(d===Ja&&Fa.test(c)){var e=c.match(Fa);b=e[1],c=c.substring(e[0].length)}var f=N(c);switch(d){case Ia:return f;case Ja:return g([f],{type:b});case Ka:return new Int8Array(f);case La:return new Uint8Array(f);case Ma:return new Uint8ClampedArray(f);case Na:return new Int16Array(f);case Pa:return new Uint16Array(f);case Oa:return new Int32Array(f);case Qa:return new Uint32Array(f);case Ra:return new Float32Array(f);case Sa:return new Float64Array(f);default:throw new Error("Unkown type: "+d)}}function R(a,b,c,d){a.executeSql("CREATE TABLE IF NOT EXISTS "+b.storeName+" (id INTEGER PRIMARY KEY, key unique, value)",[],c,d)}function S(a){var b=this,c={db:null};if(a)for(var d in a)c[d]="string"!=typeof a[d]?a[d].toString():a[d];var e=new va(function(a,d){try{c.db=openDatabase(c.name,String(c.version),c.description,c.size)}catch(a){return d(a)}c.db.transaction(function(e){R(e,c,function(){b._dbInfo=c,a()},function(a,b){d(b)})},d)});return c.serializer=Va,e}function T(a,b,c,d,e,f){a.executeSql(c,d,e,function(a,g){g.code===g.SYNTAX_ERR?a.executeSql("SELECT name FROM sqlite_master WHERE type='table' AND name = ?",[b.storeName],function(a,h){h.rows.length?f(a,g):R(a,b,function(){a.executeSql(c,d,e,f)},f)},f):f(a,g)},f)}function U(a,b){var c=this;a=j(a);var d=new va(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){T(c,e,"SELECT * FROM "+e.storeName+" WHERE key = ? LIMIT 1",[a],function(a,c){var d=c.rows.length?c.rows.item(0).value:null;d&&(d=e.serializer.deserialize(d)),b(d)},function(a,b){d(b)})})}).catch(d)});return h(d,b),d}function V(a,b){var c=this,d=new va(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){T(c,e,"SELECT * FROM "+e.storeName,[],function(c,d){for(var f=d.rows,g=f.length,h=0;h<g;h++){var i=f.item(h),j=i.value;if(j&&(j=e.serializer.deserialize(j)),void 0!==(j=a(j,i.key,h+1)))return void b(j)}b()},function(a,b){d(b)})})}).catch(d)});return h(d,b),d}function W(a,b,c,d){var e=this;a=j(a);var f=new va(function(f,g){e.ready().then(function(){void 0===b&&(b=null);var h=b,i=e._dbInfo;i.serializer.serialize(b,function(b,j){j?g(j):i.db.transaction(function(c){T(c,i,"INSERT OR REPLACE INTO "+i.storeName+" (key, value) VALUES (?, ?)",[a,b],function(){f(h)},function(a,b){g(b)})},function(b){if(b.code===b.QUOTA_ERR){if(d>0)return void f(W.apply(e,[a,h,c,d-1]));g(b)}})})}).catch(g)});return h(f,c),f}function X(a,b,c){return W.apply(this,[a,b,c,1])}function Y(a,b){var c=this;a=j(a);var d=new va(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){T(c,e,"DELETE FROM "+e.storeName+" WHERE key = ?",[a],function(){b()},function(a,b){d(b)})})}).catch(d)});return h(d,b),d}function Z(a){var b=this,c=new va(function(a,c){b.ready().then(function(){var d=b._dbInfo;d.db.transaction(function(b){T(b,d,"DELETE FROM "+d.storeName,[],function(){a()},function(a,b){c(b)})})}).catch(c)});return h(c,a),c}function $(a){var b=this,c=new va(function(a,c){b.ready().then(function(){var d=b._dbInfo;d.db.transaction(function(b){T(b,d,"SELECT COUNT(key) as c FROM "+d.storeName,[],function(b,c){var d=c.rows.item(0).c;a(d)},function(a,b){c(b)})})}).catch(c)});return h(c,a),c}function _(a,b){var c=this,d=new va(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){T(c,e,"SELECT key FROM "+e.storeName+" WHERE id = ? LIMIT 1",[a+1],function(a,c){var d=c.rows.length?c.rows.item(0).key:null;b(d)},function(a,b){d(b)})})}).catch(d)});return h(d,b),d}function aa(a){var b=this,c=new va(function(a,c){b.ready().then(function(){var d=b._dbInfo;d.db.transaction(function(b){T(b,d,"SELECT key FROM "+d.storeName,[],function(b,c){for(var d=[],e=0;e<c.rows.length;e++)d.push(c.rows.item(e).key);a(d)},function(a,b){c(b)})})}).catch(c)});return h(c,a),c}function ba(a){return new va(function(b,c){a.transaction(function(d){d.executeSql("SELECT name FROM sqlite_master WHERE type='table' AND name <> '__WebKitDatabaseInfoTable__'",[],function(c,d){for(var e=[],f=0;f<d.rows.length;f++)e.push(d.rows.item(f).name);b({db:a,storeNames:e})},function(a,b){c(b)})},function(a){c(a)})})}function ca(a,b){b=k.apply(this,arguments);var c=this.config();a="function"!=typeof a&&a||{},a.name||(a.name=a.name||c.name,a.storeName=a.storeName||c.storeName);var d,e=this;return d=a.name?new va(function(b){var d;d=a.name===c.name?e._dbInfo.db:openDatabase(a.name,"","",0),b(a.storeName?{db:d,storeNames:[a.storeName]}:ba(d))}).then(function(a){return new va(function(b,c){a.db.transaction(function(d){function e(a){return new va(function(b,c){d.executeSql("DROP TABLE IF EXISTS "+a,[],function(){b()},function(a,b){c(b)})})}for(var f=[],g=0,h=a.storeNames.length;g<h;g++)f.push(e(a.storeNames[g]));va.all(f).then(function(){b()}).catch(function(a){c(a)})},function(a){c(a)})})}):va.reject("Invalid arguments"),h(d,b),d}function da(){try{return"undefined"!=typeof localStorage&&"setItem"in localStorage&&!!localStorage.setItem}catch(a){return!1}}function ea(a,b){var c=a.name+"/";return a.storeName!==b.storeName&&(c+=a.storeName+"/"),c}function fa(){var a="_localforage_support_test";try{return localStorage.setItem(a,!0),localStorage.removeItem(a),!1}catch(a){return!0}}function ga(){return!fa()||localStorage.length>0}function ha(a){var b=this,c={};if(a)for(var d in a)c[d]=a[d];return c.keyPrefix=ea(a,b._defaultConfig),ga()?(b._dbInfo=c,c.serializer=Va,va.resolve()):va.reject()}function ia(a){var b=this,c=b.ready().then(function(){for(var a=b._dbInfo.keyPrefix,c=localStorage.length-1;c>=0;c--){var d=localStorage.key(c);0===d.indexOf(a)&&localStorage.removeItem(d)}});return h(c,a),c}function ja(a,b){var c=this;a=j(a);var d=c.ready().then(function(){var b=c._dbInfo,d=localStorage.getItem(b.keyPrefix+a);return d&&(d=b.serializer.deserialize(d)),d});return h(d,b),d}function ka(a,b){var c=this,d=c.ready().then(function(){for(var b=c._dbInfo,d=b.keyPrefix,e=d.length,f=localStorage.length,g=1,h=0;h<f;h++){var i=localStorage.key(h);if(0===i.indexOf(d)){var j=localStorage.getItem(i);if(j&&(j=b.serializer.deserialize(j)),void 0!==(j=a(j,i.substring(e),g++)))return j}}});return h(d,b),d}function la(a,b){var c=this,d=c.ready().then(function(){var b,d=c._dbInfo;try{b=localStorage.key(a)}catch(a){b=null}return b&&(b=b.substring(d.keyPrefix.length)),b});return h(d,b),d}function ma(a){var b=this,c=b.ready().then(function(){for(var a=b._dbInfo,c=localStorage.length,d=[],e=0;e<c;e++){var f=localStorage.key(e);0===f.indexOf(a.keyPrefix)&&d.push(f.substring(a.keyPrefix.length))}return d});return h(c,a),c}function na(a){var b=this,c=b.keys().then(function(a){return a.length});return h(c,a),c}function oa(a,b){var c=this;a=j(a);var d=c.ready().then(function(){var b=c._dbInfo;localStorage.removeItem(b.keyPrefix+a)});return h(d,b),d}function pa(a,b,c){var d=this;a=j(a);var e=d.ready().then(function(){void 0===b&&(b=null);var c=b;return new va(function(e,f){var g=d._dbInfo;g.serializer.serialize(b,function(b,d){if(d)f(d);else try{localStorage.setItem(g.keyPrefix+a,b),e(c)}catch(a){"QuotaExceededError"!==a.name&&"NS_ERROR_DOM_QUOTA_REACHED"!==a.name||f(a),f(a)}})})});return h(e,c),e}function qa(a,b){if(b=k.apply(this,arguments),a="function"!=typeof a&&a||{},!a.name){var c=this.config();a.name=a.name||c.name,a.storeName=a.storeName||c.storeName}var d,e=this;return d=a.name?new va(function(b){b(a.storeName?ea(a,e._defaultConfig):a.name+"/")}).then(function(a){for(var b=localStorage.length-1;b>=0;b--){var c=localStorage.key(b);0===c.indexOf(a)&&localStorage.removeItem(c)}}):va.reject("Invalid arguments"),h(d,b),d}function ra(a,b){a[b]=function(){var c=arguments;return a.ready().then(function(){return a[b].apply(a,c)})}}function sa(){for(var a=1;a<arguments.length;a++){var b=arguments[a];if(b)for(var c in b)b.hasOwnProperty(c)&&($a(b[c])?arguments[0][c]=b[c].slice():arguments[0][c]=b[c])}return arguments[0]}var ta="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a},ua=e();"undefined"==typeof Promise&&a(3);var va=Promise,wa="local-forage-detect-blob-support",xa=void 0,ya={},za=Object.prototype.toString,Aa="readonly",Ba="readwrite",Ca={_driver:"asyncStorage",_initStorage:C,_support:f(),iterate:E,getItem:D,setItem:F,removeItem:G,clear:H,length:I,key:J,keys:K,dropInstance:L},Da="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",Ea="~~local_forage_type~",Fa=/^~~local_forage_type~([^~]+)~/,Ga="__lfsc__:",Ha=Ga.length,Ia="arbf",Ja="blob",Ka="si08",La="ui08",Ma="uic8",Na="si16",Oa="si32",Pa="ur16",Qa="ui32",Ra="fl32",Sa="fl64",Ta=Ha+Ia.length,Ua=Object.prototype.toString,Va={serialize:P,deserialize:Q,stringToBuffer:N,bufferToString:O},Wa={_driver:"webSQLStorage",_initStorage:S,_support:M(),iterate:V,getItem:U,setItem:X,removeItem:Y,clear:Z,length:$,key:_,keys:aa,dropInstance:ca},Xa={_driver:"localStorageWrapper",_initStorage:ha,_support:da(),iterate:ka,getItem:ja,setItem:pa,removeItem:oa,clear:ia,length:na,key:la,keys:ma,dropInstance:qa},Ya=function(a,b){return a===b||"number"==typeof a&&"number"==typeof b&&isNaN(a)&&isNaN(b)},Za=function(a,b){for(var c=a.length,d=0;d<c;){if(Ya(a[d],b))return!0;d++}return!1},$a=Array.isArray||function(a){return"[object Array]"===Object.prototype.toString.call(a)},_a={},ab={},bb={INDEXEDDB:Ca,WEBSQL:Wa,LOCALSTORAGE:Xa},cb=[bb.INDEXEDDB._driver,bb.WEBSQL._driver,bb.LOCALSTORAGE._driver],db=["dropInstance"],eb=["clear","getItem","iterate","key","keys","length","removeItem","setItem"].concat(db),fb={description:"",driver:cb.slice(),name:"localforage",size:4980736,storeName:"keyvaluepairs",version:1},gb=function(){function a(b){d(this,a);for(var c in bb)if(bb.hasOwnProperty(c)){var e=bb[c],f=e._driver;this[c]=f,_a[f]||this.defineDriver(e)}this._defaultConfig=sa({},fb),this._config=sa({},this._defaultConfig,b),this._driverSet=null,this._initDriver=null,this._ready=!1,this._dbInfo=null,this._wrapLibraryMethodsWithReady(),this.setDriver(this._config.driver).catch(function(){})}return a.prototype.config=function(a){if("object"===(void 0===a?"undefined":ta(a))){if(this._ready)return new Error("Can't call config() after localforage has been used.");for(var b in a){if("storeName"===b&&(a[b]=a[b].replace(/\W/g,"_")),"version"===b&&"number"!=typeof a[b])return new Error("Database version must be a number.");this._config[b]=a[b]}return!("driver"in a&&a.driver)||this.setDriver(this._config.driver)}return"string"==typeof a?this._config[a]:this._config},a.prototype.defineDriver=function(a,b,c){var d=new va(function(b,c){try{var d=a._driver,e=new Error("Custom driver not compliant; see https://mozilla.github.io/localForage/#definedriver");if(!a._driver)return void c(e);for(var f=eb.concat("_initStorage"),g=0,i=f.length;g<i;g++){var j=f[g];if((!Za(db,j)||a[j])&&"function"!=typeof a[j])return void c(e)}(function(){for(var b=function(a){return function(){var b=new Error("Method "+a+" is not implemented by the current driver"),c=va.reject(b);return h(c,arguments[arguments.length-1]),c}},c=0,d=db.length;c<d;c++){var e=db[c];a[e]||(a[e]=b(e))}})();var k=function(c){_a[d]&&console.info("Redefining LocalForage driver: "+d),_a[d]=a,ab[d]=c,b()};"_support"in a?a._support&&"function"==typeof a._support?a._support().then(k,c):k(!!a._support):k(!0)}catch(a){c(a)}});return i(d,b,c),d},a.prototype.driver=function(){return this._driver||null},a.prototype.getDriver=function(a,b,c){var d=_a[a]?va.resolve(_a[a]):va.reject(new Error("Driver not found."));return i(d,b,c),d},a.prototype.getSerializer=function(a){var b=va.resolve(Va);return i(b,a),b},a.prototype.ready=function(a){var b=this,c=b._driverSet.then(function(){return null===b._ready&&(b._ready=b._initDriver()),b._ready});return i(c,a,a),c},a.prototype.setDriver=function(a,b,c){function d(){g._config.driver=g.driver()}function e(a){return g._extend(a),d(),g._ready=g._initStorage(g._config),g._ready}function f(a){return function(){function b(){for(;c<a.length;){var f=a[c];return c++,g._dbInfo=null,g._ready=null,g.getDriver(f).then(e).catch(b)}d();var h=new Error("No available storage method found.");return g._driverSet=va.reject(h),g._driverSet}var c=0;return b()}}var g=this;$a(a)||(a=[a]);var h=this._getSupportedDrivers(a),j=null!==this._driverSet?this._driverSet.catch(function(){return va.resolve()}):va.resolve();return this._driverSet=j.then(function(){var a=h[0];return g._dbInfo=null,g._ready=null,g.getDriver(a).then(function(a){g._driver=a._driver,d(),g._wrapLibraryMethodsWithReady(),g._initDriver=f(h)})}).catch(function(){d();var a=new Error("No available storage method found.");return g._driverSet=va.reject(a),g._driverSet}),i(this._driverSet,b,c),this._driverSet},a.prototype.supports=function(a){return!!ab[a]},a.prototype._extend=function(a){sa(this,a)},a.prototype._getSupportedDrivers=function(a){for(var b=[],c=0,d=a.length;c<d;c++){var e=a[c];this.supports(e)&&b.push(e)}return b},a.prototype._wrapLibraryMethodsWithReady=function(){for(var a=0,b=eb.length;a<b;a++)ra(this,eb[a])},a.prototype.createInstance=function(b){return new a(b)},a}(),hb=new gb;b.exports=hb},{3:3}]},{},[4])(4)});
}

if (typeof Papa === 'undefined') /* iOS workaround */ {
    /* @license Papa Parse v5.3.1 https://github.com/mholt/PapaParse License: MIT */
    !function(e,t){"function"==typeof define&&define.amd?define([],t):"object"==typeof module&&"undefined"!=typeof exports?module.exports=t():e.Papa=t()}(this,function s(){"use strict";var f="undefined"!=typeof self?self:"undefined"!=typeof window?window:void 0!==f?f:{};var n=!f.document&&!!f.postMessage,o=n&&/blob:/i.test((f.location||{}).protocol),a={},h=0,b={parse:function(e,t){var i=(t=t||{}).dynamicTyping||!1;M(i)&&(t.dynamicTypingFunction=i,i={});if(t.dynamicTyping=i,t.transform=!!M(t.transform)&&t.transform,t.worker&&b.WORKERS_SUPPORTED){var r=function(){if(!b.WORKERS_SUPPORTED)return!1;var e=(i=f.URL||f.webkitURL||null,r=s.toString(),b.BLOB_URL||(b.BLOB_URL=i.createObjectURL(new Blob(["(",r,")();"],{type:"text/javascript"})))),t=new f.Worker(e);var i,r;return t.onmessage=_,t.id=h++,a[t.id]=t}();return r.userStep=t.step,r.userChunk=t.chunk,r.userComplete=t.complete,r.userError=t.error,t.step=M(t.step),t.chunk=M(t.chunk),t.complete=M(t.complete),t.error=M(t.error),delete t.worker,void r.postMessage({input:e,config:t,workerId:r.id})}var n=null;b.NODE_STREAM_INPUT,"string"==typeof e?n=t.download?new l(t):new p(t):!0===e.readable&&M(e.read)&&M(e.on)?n=new g(t):(f.File&&e instanceof File||e instanceof Object)&&(n=new c(t));return n.stream(e)},unparse:function(e,t){var n=!1,_=!0,m=",",y="\r\n",s='"',a=s+s,i=!1,r=null,o=!1;!function(){if("object"!=typeof t)return;"string"!=typeof t.delimiter||b.BAD_DELIMITERS.filter(function(e){return-1!==t.delimiter.indexOf(e)}).length||(m=t.delimiter);("boolean"==typeof t.quotes||"function"==typeof t.quotes||Array.isArray(t.quotes))&&(n=t.quotes);"boolean"!=typeof t.skipEmptyLines&&"string"!=typeof t.skipEmptyLines||(i=t.skipEmptyLines);"string"==typeof t.newline&&(y=t.newline);"string"==typeof t.quoteChar&&(s=t.quoteChar);"boolean"==typeof t.header&&(_=t.header);if(Array.isArray(t.columns)){if(0===t.columns.length)throw new Error("Option columns is empty");r=t.columns}void 0!==t.escapeChar&&(a=t.escapeChar+s);"boolean"==typeof t.escapeFormulae&&(o=t.escapeFormulae)}();var h=new RegExp(j(s),"g");"string"==typeof e&&(e=JSON.parse(e));if(Array.isArray(e)){if(!e.length||Array.isArray(e[0]))return u(null,e,i);if("object"==typeof e[0])return u(r||Object.keys(e[0]),e,i)}else if("object"==typeof e)return"string"==typeof e.data&&(e.data=JSON.parse(e.data)),Array.isArray(e.data)&&(e.fields||(e.fields=e.meta&&e.meta.fields),e.fields||(e.fields=Array.isArray(e.data[0])?e.fields:"object"==typeof e.data[0]?Object.keys(e.data[0]):[]),Array.isArray(e.data[0])||"object"==typeof e.data[0]||(e.data=[e.data])),u(e.fields||[],e.data||[],i);throw new Error("Unable to serialize unrecognized input");function u(e,t,i){var r="";"string"==typeof e&&(e=JSON.parse(e)),"string"==typeof t&&(t=JSON.parse(t));var n=Array.isArray(e)&&0<e.length,s=!Array.isArray(t[0]);if(n&&_){for(var a=0;a<e.length;a++)0<a&&(r+=m),r+=v(e[a],a);0<t.length&&(r+=y)}for(var o=0;o<t.length;o++){var h=n?e.length:t[o].length,u=!1,f=n?0===Object.keys(t[o]).length:0===t[o].length;if(i&&!n&&(u="greedy"===i?""===t[o].join("").trim():1===t[o].length&&0===t[o][0].length),"greedy"===i&&n){for(var d=[],l=0;l<h;l++){var c=s?e[l]:l;d.push(t[o][c])}u=""===d.join("").trim()}if(!u){for(var p=0;p<h;p++){0<p&&!f&&(r+=m);var g=n&&s?e[p]:p;r+=v(t[o][g],p)}o<t.length-1&&(!i||0<h&&!f)&&(r+=y)}}return r}function v(e,t){if(null==e)return"";if(e.constructor===Date)return JSON.stringify(e).slice(1,25);!0===o&&"string"==typeof e&&null!==e.match(/^[=+\-@].*$/)&&(e="'"+e);var i=e.toString().replace(h,a),r="boolean"==typeof n&&n||"function"==typeof n&&n(e,t)||Array.isArray(n)&&n[t]||function(e,t){for(var i=0;i<t.length;i++)if(-1<e.indexOf(t[i]))return!0;return!1}(i,b.BAD_DELIMITERS)||-1<i.indexOf(m)||" "===i.charAt(0)||" "===i.charAt(i.length-1);return r?s+i+s:i}}};if(b.RECORD_SEP=String.fromCharCode(30),b.UNIT_SEP=String.fromCharCode(31),b.BYTE_ORDER_MARK="\ufeff",b.BAD_DELIMITERS=["\r","\n",'"',b.BYTE_ORDER_MARK],b.WORKERS_SUPPORTED=!n&&!!f.Worker,b.NODE_STREAM_INPUT=1,b.LocalChunkSize=10485760,b.RemoteChunkSize=5242880,b.DefaultDelimiter=",",b.Parser=E,b.ParserHandle=i,b.NetworkStreamer=l,b.FileStreamer=c,b.StringStreamer=p,b.ReadableStreamStreamer=g,f.jQuery){var d=f.jQuery;d.fn.parse=function(o){var i=o.config||{},h=[];return this.each(function(e){if(!("INPUT"===d(this).prop("tagName").toUpperCase()&&"file"===d(this).attr("type").toLowerCase()&&f.FileReader)||!this.files||0===this.files.length)return!0;for(var t=0;t<this.files.length;t++)h.push({file:this.files[t],inputElem:this,instanceConfig:d.extend({},i)})}),e(),this;function e(){if(0!==h.length){var e,t,i,r,n=h[0];if(M(o.before)){var s=o.before(n.file,n.inputElem);if("object"==typeof s){if("abort"===s.action)return e="AbortError",t=n.file,i=n.inputElem,r=s.reason,void(M(o.error)&&o.error({name:e},t,i,r));if("skip"===s.action)return void u();"object"==typeof s.config&&(n.instanceConfig=d.extend(n.instanceConfig,s.config))}else if("skip"===s)return void u()}var a=n.instanceConfig.complete;n.instanceConfig.complete=function(e){M(a)&&a(e,n.file,n.inputElem),u()},b.parse(n.file,n.instanceConfig)}else M(o.complete)&&o.complete()}function u(){h.splice(0,1),e()}}}function u(e){this._handle=null,this._finished=!1,this._completed=!1,this._halted=!1,this._input=null,this._baseIndex=0,this._partialLine="",this._rowCount=0,this._start=0,this._nextChunk=null,this.isFirstChunk=!0,this._completeResults={data:[],errors:[],meta:{}},function(e){var t=w(e);t.chunkSize=parseInt(t.chunkSize),e.step||e.chunk||(t.chunkSize=null);this._handle=new i(t),(this._handle.streamer=this)._config=t}.call(this,e),this.parseChunk=function(e,t){if(this.isFirstChunk&&M(this._config.beforeFirstChunk)){var i=this._config.beforeFirstChunk(e);void 0!==i&&(e=i)}this.isFirstChunk=!1,this._halted=!1;var r=this._partialLine+e;this._partialLine="";var n=this._handle.parse(r,this._baseIndex,!this._finished);if(!this._handle.paused()&&!this._handle.aborted()){var s=n.meta.cursor;this._finished||(this._partialLine=r.substring(s-this._baseIndex),this._baseIndex=s),n&&n.data&&(this._rowCount+=n.data.length);var a=this._finished||this._config.preview&&this._rowCount>=this._config.preview;if(o)f.postMessage({results:n,workerId:b.WORKER_ID,finished:a});else if(M(this._config.chunk)&&!t){if(this._config.chunk(n,this._handle),this._handle.paused()||this._handle.aborted())return void(this._halted=!0);n=void 0,this._completeResults=void 0}return this._config.step||this._config.chunk||(this._completeResults.data=this._completeResults.data.concat(n.data),this._completeResults.errors=this._completeResults.errors.concat(n.errors),this._completeResults.meta=n.meta),this._completed||!a||!M(this._config.complete)||n&&n.meta.aborted||(this._config.complete(this._completeResults,this._input),this._completed=!0),a||n&&n.meta.paused||this._nextChunk(),n}this._halted=!0},this._sendError=function(e){M(this._config.error)?this._config.error(e):o&&this._config.error&&f.postMessage({workerId:b.WORKER_ID,error:e,finished:!1})}}function l(e){var r;(e=e||{}).chunkSize||(e.chunkSize=b.RemoteChunkSize),u.call(this,e),this._nextChunk=n?function(){this._readChunk(),this._chunkLoaded()}:function(){this._readChunk()},this.stream=function(e){this._input=e,this._nextChunk()},this._readChunk=function(){if(this._finished)this._chunkLoaded();else{if(r=new XMLHttpRequest,this._config.withCredentials&&(r.withCredentials=this._config.withCredentials),n||(r.onload=v(this._chunkLoaded,this),r.onerror=v(this._chunkError,this)),r.open(this._config.downloadRequestBody?"POST":"GET",this._input,!n),this._config.downloadRequestHeaders){var e=this._config.downloadRequestHeaders;for(var t in e)r.setRequestHeader(t,e[t])}if(this._config.chunkSize){var i=this._start+this._config.chunkSize-1;r.setRequestHeader("Range","bytes="+this._start+"-"+i)}try{r.send(this._config.downloadRequestBody)}catch(e){this._chunkError(e.message)}n&&0===r.status&&this._chunkError()}},this._chunkLoaded=function(){4===r.readyState&&(r.status<200||400<=r.status?this._chunkError():(this._start+=this._config.chunkSize?this._config.chunkSize:r.responseText.length,this._finished=!this._config.chunkSize||this._start>=function(e){var t=e.getResponseHeader("Content-Range");if(null===t)return-1;return parseInt(t.substring(t.lastIndexOf("/")+1))}(r),this.parseChunk(r.responseText)))},this._chunkError=function(e){var t=r.statusText||e;this._sendError(new Error(t))}}function c(e){var r,n;(e=e||{}).chunkSize||(e.chunkSize=b.LocalChunkSize),u.call(this,e);var s="undefined"!=typeof FileReader;this.stream=function(e){this._input=e,n=e.slice||e.webkitSlice||e.mozSlice,s?((r=new FileReader).onload=v(this._chunkLoaded,this),r.onerror=v(this._chunkError,this)):r=new FileReaderSync,this._nextChunk()},this._nextChunk=function(){this._finished||this._config.preview&&!(this._rowCount<this._config.preview)||this._readChunk()},this._readChunk=function(){var e=this._input;if(this._config.chunkSize){var t=Math.min(this._start+this._config.chunkSize,this._input.size);e=n.call(e,this._start,t)}var i=r.readAsText(e,this._config.encoding);s||this._chunkLoaded({target:{result:i}})},this._chunkLoaded=function(e){this._start+=this._config.chunkSize,this._finished=!this._config.chunkSize||this._start>=this._input.size,this.parseChunk(e.target.result)},this._chunkError=function(){this._sendError(r.error)}}function p(e){var i;u.call(this,e=e||{}),this.stream=function(e){return i=e,this._nextChunk()},this._nextChunk=function(){if(!this._finished){var e,t=this._config.chunkSize;return t?(e=i.substring(0,t),i=i.substring(t)):(e=i,i=""),this._finished=!i,this.parseChunk(e)}}}function g(e){u.call(this,e=e||{});var t=[],i=!0,r=!1;this.pause=function(){u.prototype.pause.apply(this,arguments),this._input.pause()},this.resume=function(){u.prototype.resume.apply(this,arguments),this._input.resume()},this.stream=function(e){this._input=e,this._input.on("data",this._streamData),this._input.on("end",this._streamEnd),this._input.on("error",this._streamError)},this._checkIsFinished=function(){r&&1===t.length&&(this._finished=!0)},this._nextChunk=function(){this._checkIsFinished(),t.length?this.parseChunk(t.shift()):i=!0},this._streamData=v(function(e){try{t.push("string"==typeof e?e:e.toString(this._config.encoding)),i&&(i=!1,this._checkIsFinished(),this.parseChunk(t.shift()))}catch(e){this._streamError(e)}},this),this._streamError=v(function(e){this._streamCleanUp(),this._sendError(e)},this),this._streamEnd=v(function(){this._streamCleanUp(),r=!0,this._streamData("")},this),this._streamCleanUp=v(function(){this._input.removeListener("data",this._streamData),this._input.removeListener("end",this._streamEnd),this._input.removeListener("error",this._streamError)},this)}function i(m){var a,o,h,r=Math.pow(2,53),n=-r,s=/^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/,u=/^(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))$/,t=this,i=0,f=0,d=!1,e=!1,l=[],c={data:[],errors:[],meta:{}};if(M(m.step)){var p=m.step;m.step=function(e){if(c=e,_())g();else{if(g(),0===c.data.length)return;i+=e.data.length,m.preview&&i>m.preview?o.abort():(c.data=c.data[0],p(c,t))}}}function y(e){return"greedy"===m.skipEmptyLines?""===e.join("").trim():1===e.length&&0===e[0].length}function g(){if(c&&h&&(k("Delimiter","UndetectableDelimiter","Unable to auto-detect delimiting character; defaulted to '"+b.DefaultDelimiter+"'"),h=!1),m.skipEmptyLines)for(var e=0;e<c.data.length;e++)y(c.data[e])&&c.data.splice(e--,1);return _()&&function(){if(!c)return;function e(e,t){M(m.transformHeader)&&(e=m.transformHeader(e,t)),l.push(e)}if(Array.isArray(c.data[0])){for(var t=0;_()&&t<c.data.length;t++)c.data[t].forEach(e);c.data.splice(0,1)}else c.data.forEach(e)}(),function(){if(!c||!m.header&&!m.dynamicTyping&&!m.transform)return c;function e(e,t){var i,r=m.header?{}:[];for(i=0;i<e.length;i++){var n=i,s=e[i];m.header&&(n=i>=l.length?"__parsed_extra":l[i]),m.transform&&(s=m.transform(s,n)),s=v(n,s),"__parsed_extra"===n?(r[n]=r[n]||[],r[n].push(s)):r[n]=s}return m.header&&(i>l.length?k("FieldMismatch","TooManyFields","Too many fields: expected "+l.length+" fields but parsed "+i,f+t):i<l.length&&k("FieldMismatch","TooFewFields","Too few fields: expected "+l.length+" fields but parsed "+i,f+t)),r}var t=1;!c.data.length||Array.isArray(c.data[0])?(c.data=c.data.map(e),t=c.data.length):c.data=e(c.data,0);m.header&&c.meta&&(c.meta.fields=l);return f+=t,c}()}function _(){return m.header&&0===l.length}function v(e,t){return i=e,m.dynamicTypingFunction&&void 0===m.dynamicTyping[i]&&(m.dynamicTyping[i]=m.dynamicTypingFunction(i)),!0===(m.dynamicTyping[i]||m.dynamicTyping)?"true"===t||"TRUE"===t||"false"!==t&&"FALSE"!==t&&(function(e){if(s.test(e)){var t=parseFloat(e);if(n<t&&t<r)return!0}return!1}(t)?parseFloat(t):u.test(t)?new Date(t):""===t?null:t):t;var i}function k(e,t,i,r){var n={type:e,code:t,message:i};void 0!==r&&(n.row=r),c.errors.push(n)}this.parse=function(e,t,i){var r=m.quoteChar||'"';if(m.newline||(m.newline=function(e,t){e=e.substring(0,1048576);var i=new RegExp(j(t)+"([^]*?)"+j(t),"gm"),r=(e=e.replace(i,"")).split("\r"),n=e.split("\n"),s=1<n.length&&n[0].length<r[0].length;if(1===r.length||s)return"\n";for(var a=0,o=0;o<r.length;o++)"\n"===r[o][0]&&a++;return a>=r.length/2?"\r\n":"\r"}(e,r)),h=!1,m.delimiter)M(m.delimiter)&&(m.delimiter=m.delimiter(e),c.meta.delimiter=m.delimiter);else{var n=function(e,t,i,r,n){var s,a,o,h;n=n||[",","\t","|",";",b.RECORD_SEP,b.UNIT_SEP];for(var u=0;u<n.length;u++){var f=n[u],d=0,l=0,c=0;o=void 0;for(var p=new E({comments:r,delimiter:f,newline:t,preview:10}).parse(e),g=0;g<p.data.length;g++)if(i&&y(p.data[g]))c++;else{var _=p.data[g].length;l+=_,void 0!==o?0<_&&(d+=Math.abs(_-o),o=_):o=_}0<p.data.length&&(l/=p.data.length-c),(void 0===a||d<=a)&&(void 0===h||h<l)&&1.99<l&&(a=d,s=f,h=l)}return{successful:!!(m.delimiter=s),bestDelimiter:s}}(e,m.newline,m.skipEmptyLines,m.comments,m.delimitersToGuess);n.successful?m.delimiter=n.bestDelimiter:(h=!0,m.delimiter=b.DefaultDelimiter),c.meta.delimiter=m.delimiter}var s=w(m);return m.preview&&m.header&&s.preview++,a=e,o=new E(s),c=o.parse(a,t,i),g(),d?{meta:{paused:!0}}:c||{meta:{paused:!1}}},this.paused=function(){return d},this.pause=function(){d=!0,o.abort(),a=M(m.chunk)?"":a.substring(o.getCharIndex())},this.resume=function(){t.streamer._halted?(d=!1,t.streamer.parseChunk(a,!0)):setTimeout(t.resume,3)},this.aborted=function(){return e},this.abort=function(){e=!0,o.abort(),c.meta.aborted=!0,M(m.complete)&&m.complete(c),a=""}}function j(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function E(e){var S,O=(e=e||{}).delimiter,x=e.newline,I=e.comments,T=e.step,D=e.preview,A=e.fastMode,L=S=void 0===e.quoteChar?'"':e.quoteChar;if(void 0!==e.escapeChar&&(L=e.escapeChar),("string"!=typeof O||-1<b.BAD_DELIMITERS.indexOf(O))&&(O=","),I===O)throw new Error("Comment character same as delimiter");!0===I?I="#":("string"!=typeof I||-1<b.BAD_DELIMITERS.indexOf(I))&&(I=!1),"\n"!==x&&"\r"!==x&&"\r\n"!==x&&(x="\n");var F=0,z=!1;this.parse=function(r,t,i){if("string"!=typeof r)throw new Error("Input must be a string");var n=r.length,e=O.length,s=x.length,a=I.length,o=M(T),h=[],u=[],f=[],d=F=0;if(!r)return C();if(A||!1!==A&&-1===r.indexOf(S)){for(var l=r.split(x),c=0;c<l.length;c++){if(f=l[c],F+=f.length,c!==l.length-1)F+=x.length;else if(i)return C();if(!I||f.substring(0,a)!==I){if(o){if(h=[],k(f.split(O)),R(),z)return C()}else k(f.split(O));if(D&&D<=c)return h=h.slice(0,D),C(!0)}}return C()}for(var p=r.indexOf(O,F),g=r.indexOf(x,F),_=new RegExp(j(L)+j(S),"g"),m=r.indexOf(S,F);;)if(r[F]!==S)if(I&&0===f.length&&r.substring(F,F+a)===I){if(-1===g)return C();F=g+s,g=r.indexOf(x,F),p=r.indexOf(O,F)}else if(-1!==p&&(p<g||-1===g))f.push(r.substring(F,p)),F=p+e,p=r.indexOf(O,F);else{if(-1===g)break;if(f.push(r.substring(F,g)),w(g+s),o&&(R(),z))return C();if(D&&h.length>=D)return C(!0)}else for(m=F,F++;;){if(-1===(m=r.indexOf(S,m+1)))return i||u.push({type:"Quotes",code:"MissingQuotes",message:"Quoted field unterminated",row:h.length,index:F}),E();if(m===n-1)return E(r.substring(F,m).replace(_,S));if(S!==L||r[m+1]!==L){if(S===L||0===m||r[m-1]!==L){-1!==p&&p<m+1&&(p=r.indexOf(O,m+1)),-1!==g&&g<m+1&&(g=r.indexOf(x,m+1));var y=b(-1===g?p:Math.min(p,g));if(r[m+1+y]===O){f.push(r.substring(F,m).replace(_,S)),r[F=m+1+y+e]!==S&&(m=r.indexOf(S,F)),p=r.indexOf(O,F),g=r.indexOf(x,F);break}var v=b(g);if(r.substring(m+1+v,m+1+v+s)===x){if(f.push(r.substring(F,m).replace(_,S)),w(m+1+v+s),p=r.indexOf(O,F),m=r.indexOf(S,F),o&&(R(),z))return C();if(D&&h.length>=D)return C(!0);break}u.push({type:"Quotes",code:"InvalidQuotes",message:"Trailing quote on quoted field is malformed",row:h.length,index:F}),m++}}else m++}return E();function k(e){h.push(e),d=F}function b(e){var t=0;if(-1!==e){var i=r.substring(m+1,e);i&&""===i.trim()&&(t=i.length)}return t}function E(e){return i||(void 0===e&&(e=r.substring(F)),f.push(e),F=n,k(f),o&&R()),C()}function w(e){F=e,k(f),f=[],g=r.indexOf(x,F)}function C(e){return{data:h,errors:u,meta:{delimiter:O,linebreak:x,aborted:z,truncated:!!e,cursor:d+(t||0)}}}function R(){T(C()),h=[],u=[]}},this.abort=function(){z=!0},this.getCharIndex=function(){return F}}function _(e){var t=e.data,i=a[t.workerId],r=!1;if(t.error)i.userError(t.error,t.file);else if(t.results&&t.results.data){var n={abort:function(){r=!0,m(t.workerId,{data:[],errors:[],meta:{aborted:!0}})},pause:y,resume:y};if(M(i.userStep)){for(var s=0;s<t.results.data.length&&(i.userStep({data:t.results.data[s],errors:t.results.errors,meta:t.results.meta},n),!r);s++);delete t.results}else M(i.userChunk)&&(i.userChunk(t.results,n,t.file),delete t.results)}t.finished&&!r&&m(t.workerId,t.results)}function m(e,t){var i=a[e];M(i.userComplete)&&i.userComplete(t),i.terminate(),delete a[e]}function y(){throw new Error("Not implemented.")}function w(e){if("object"!=typeof e||null===e)return e;var t=Array.isArray(e)?[]:{};for(var i in e)t[i]=w(e[i]);return t}function v(e,t){return function(){e.apply(t,arguments)}}function M(e){return"function"==typeof e}return o&&(f.onmessage=function(e){var t=e.data;void 0===b.WORKER_ID&&t&&(b.WORKER_ID=t.workerId);if("string"==typeof t.input)f.postMessage({workerId:b.WORKER_ID,results:b.parse(t.input,t.config),finished:!0});else if(f.File&&t.input instanceof File||t.input instanceof Object){var i=b.parse(t.input,t.config);i&&f.postMessage({workerId:b.WORKER_ID,results:i,finished:!0})}}),(l.prototype=Object.create(u.prototype)).constructor=l,(c.prototype=Object.create(u.prototype)).constructor=c,(p.prototype=Object.create(p.prototype)).constructor=p,(g.prototype=Object.create(u.prototype)).constructor=g,b});
}