diff options
Diffstat (limited to 'ext')
| -rw-r--r-- | ext/bg/js/anki.js | 2 | ||||
| -rw-r--r-- | ext/bg/js/api.js | 2 | ||||
| -rw-r--r-- | ext/bg/js/audio.js | 4 | ||||
| -rw-r--r-- | ext/bg/js/backend.js | 2 | ||||
| -rw-r--r-- | ext/bg/js/database.js | 366 | ||||
| -rw-r--r-- | ext/bg/js/dictionary.js | 34 | ||||
| -rw-r--r-- | ext/bg/js/options.js | 3 | ||||
| -rw-r--r-- | ext/bg/js/request.js | 4 | ||||
| -rw-r--r-- | ext/bg/js/search.js | 2 | ||||
| -rw-r--r-- | ext/bg/js/settings.js | 42 | ||||
| -rw-r--r-- | ext/bg/js/templates.js | 216 | ||||
| -rw-r--r-- | ext/bg/js/translator.js | 108 | ||||
| -rw-r--r-- | ext/bg/js/util.js | 4 | ||||
| -rw-r--r-- | ext/bg/settings.html | 27 | ||||
| -rw-r--r-- | ext/fg/js/document.js | 2 | ||||
| -rw-r--r-- | ext/fg/js/float.js | 2 | ||||
| -rw-r--r-- | ext/fg/js/frontend.js | 38 | ||||
| -rw-r--r-- | ext/fg/js/popup.js | 15 | ||||
| -rw-r--r-- | ext/fg/js/source.js | 4 | ||||
| -rw-r--r-- | ext/manifest.json | 2 | ||||
| -rw-r--r-- | ext/mixed/css/display.css | 16 | ||||
| -rw-r--r-- | ext/mixed/js/display.js | 6 | 
22 files changed, 625 insertions, 276 deletions
| diff --git a/ext/bg/js/anki.js b/ext/bg/js/anki.js index c327969f..183f37bc 100644 --- a/ext/bg/js/anki.js +++ b/ext/bg/js/anki.js @@ -62,7 +62,7 @@ class AnkiConnect {          if (this.remoteVersion < this.localVersion) {              this.remoteVersion = await this.ankiInvoke('version');              if (this.remoteVersion < this.localVersion) { -                throw 'extension and plugin versions incompatible'; +                throw 'Extension and plugin versions incompatible';              }          }      } diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js index 5c1aebb6..9f65bb07 100644 --- a/ext/bg/js/api.js +++ b/ext/bg/js/api.js @@ -31,7 +31,7 @@ async function apiTermsFind(text) {      const searcher = options.general.groupResults ?          translator.findTermsGrouped.bind(translator) : -        translator.findTerms.bind(translator); +        translator.findTermsSplit.bind(translator);      const {definitions, length} = await searcher(          text, diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js index 0952887e..ce47490c 100644 --- a/ext/bg/js/audio.js +++ b/ext/bg/js/audio.js @@ -57,7 +57,7 @@ async function audioBuildUrl(definition, mode, cache={}) {                  const xhr = new XMLHttpRequest();                  xhr.open('POST', 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post');                  xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); -                xhr.addEventListener('error', () => reject('failed to scrape audio data')); +                xhr.addEventListener('error', () => reject('Failed to scrape audio data'));                  xhr.addEventListener('load', () => {                      cache[definition.expression] = xhr.responseText;                      resolve(xhr.responseText); @@ -87,7 +87,7 @@ async function audioBuildUrl(definition, mode, cache={}) {              } else {                  const xhr = new XMLHttpRequest();                  xhr.open('GET', `http://jisho.org/search/${definition.expression}`); -                xhr.addEventListener('error', () => reject('failed to scrape audio data')); +                xhr.addEventListener('error', () => reject('Failed to scrape audio data'));                  xhr.addEventListener('load', () => {                      cache[definition.expression] = xhr.responseText;                      resolve(xhr.responseText); diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 7d68ed84..01340419 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -71,7 +71,7 @@ class Backend {              return promise.then(result => {                  callback({result});              }).catch(error => { -                callback({error}); +                callback({error: error.toString ? error.toString() : error});              });          }; diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js index e00cb7a3..6ceb3ec8 100644 --- a/ext/bg/js/database.js +++ b/ext/bg/js/database.js @@ -20,44 +20,33 @@  class Database {      constructor() {          this.db = null; -        this.version = 2;          this.tagCache = {};      } -    async sanitize() { -        try { -            const db = new Dexie('dict'); -            await db.open(); -            db.close(); -            if (db.verno !== this.version) { -                await db.delete(); -            } -        } catch(e) { -            // NOP -        } -    } -      async prepare() {          if (this.db) { -            throw 'database already initialized'; +            throw 'Database already initialized';          } -        await this.sanitize(); -          this.db = new Dexie('dict'); -        this.db.version(this.version).stores({ -            terms: '++id,dictionary,expression,reading', -            kanji: '++,dictionary,character', -            tagMeta: '++,dictionary', +        this.db.version(2).stores({ +            terms:        '++id,dictionary,expression,reading', +            kanji:        '++,dictionary,character', +            tagMeta:      '++,dictionary',              dictionaries: '++,title,version'          }); +        this.db.version(3).stores({ +            termMeta:  '++,dictionary,expression', +            kanjiMeta: '++,dictionary,character', +            tagMeta:   '++,dictionary,name' +        });          await this.db.open();      }      async purge() {          if (!this.db) { -            throw 'database not initialized'; +            throw 'Database not initialized';          }          this.db.close(); @@ -70,7 +59,7 @@ class Database {      async findTerms(term, titles) {          if (!this.db) { -            throw 'database not initialized'; +            throw 'Database not initialized';          }          const results = []; @@ -89,17 +78,31 @@ class Database {              }          }); -        await this.cacheTagMeta(titles); -        for (const result of results) { -            result.tagMeta = this.tagCache[result.dictionary] || {}; +        return results; +    } + +    async findTermMeta(term, titles) { +        if (!this.db) { +            throw 'Database not initialized';          } +        const results = []; +        await this.db.termMeta.where('expression').equals(term).each(row => { +            if (titles.includes(row.dictionary)) { +                results.push({ +                    mode: row.mode, +                    data: row.data, +                    dictionary: row.dictionary +                }); +            } +        }); +          return results;      }      async findKanji(kanji, titles) {          if (!this.db) { -            return Promise.reject('database not initialized'); +            throw 'Database not initialized';          }          const results = []; @@ -111,165 +114,304 @@ class Database {                      kunyomi: dictFieldSplit(row.kunyomi),                      tags: dictFieldSplit(row.tags),                      glossary: row.meanings, +                    stats: row.stats,                      dictionary: row.dictionary                  });              }          }); -        await this.cacheTagMeta(titles); -        for (const result of results) { -            result.tagMeta = this.tagCache[result.dictionary] || {}; +        return results; +    } + +    async findKanjiMeta(kanji, titles) { +        if (!this.db) { +            throw 'Database not initialized';          } +        const results = []; +        await this.db.kanjiMeta.where('character').equals(kanji).each(row => { +            if (titles.includes(row.dictionary)) { +                results.push({ +                    mode: row.mode, +                    data: row.data, +                    dictionary: row.dictionary +                }); +            } +        }); +          return results;      } -    async cacheTagMeta(titles) { +    async findTagForTitle(name, title) {          if (!this.db) { -            throw 'database not initialized'; +            throw 'Database not initialized';          } -        for (const title of titles) { -            if (!this.tagCache[title]) { -                const tagMeta = {}; -                await this.db.tagMeta.where('dictionary').equals(title).each(row => { -                    tagMeta[row.name] = {category: row.category, notes: row.notes, order: row.order}; -                }); +        this.tagCache[title] = this.tagCache[title] || {}; -                this.tagCache[title] = tagMeta; -            } +        let result = this.tagCache[title][name]; +        if (!result) { +            await this.db.tagMeta.where('name').equals(name).each(row => { +                if (title === row.dictionary) { +                    result = row; +                } +            }); + +            this.tagCache[title][name] = result;          } + +        return result;      } -    async getDictionaries() { +    async getTitles() {          if (this.db) {              return this.db.dictionaries.toArray();          } else { -            throw 'database not initialized'; +            throw 'Database not initialized';          }      }      async importDictionary(archive, callback) {          if (!this.db) { -            return Promise.reject('database not initialized'); +            throw 'Database not initialized';          } -        let summary = null; -        const indexLoaded = async (title, version, revision, tagMeta, hasTerms, hasKanji) => { -            summary = {title, version, revision, hasTerms, hasKanji}; +        const indexDataLoaded = async summary => { +            if (summary.version > 2) { +                throw 'Unsupported dictionary version'; +            } -            const count = await this.db.dictionaries.where('title').equals(title).count(); +            const count = await this.db.dictionaries.where('title').equals(summary.title).count();              if (count > 0) { -                throw `dictionary "${title}" is already imported`; +                throw 'Dictionary is already imported';              } -            await this.db.dictionaries.add({title, version, revision, hasTerms, hasKanji}); +            await this.db.dictionaries.add(summary); +        }; -            const rows = []; -            for (const tag in tagMeta || {}) { -                const meta = tagMeta[tag]; -                const row = dictTagSanitize({ -                    name: tag, -                    category: meta.category, -                    notes: meta.notes, -                    order: meta.order, -                    dictionary: title -                }); +        const termDataLoaded = async (summary, entries, total, current) => { +            if (callback) { +                callback(total, current); +            } -                rows.push(row); +            const rows = []; +            if (summary.version === 1) { +                for (const [expression, reading, tags, rules, score, ...glossary] of entries) { +                    rows.push({ +                        expression, +                        reading, +                        tags, +                        rules, +                        score, +                        glossary, +                        dictionary: summary.title +                    }); +                } +            } else { +                for (const [expression, reading, tags, rules, score, glossary] of entries) { +                    rows.push({ +                        expression, +                        reading, +                        tags, +                        rules, +                        score, +                        glossary, +                        dictionary: summary.title +                    }); +                }              } -            await this.db.tagMeta.bulkAdd(rows); +            await this.db.terms.bulkAdd(rows);          }; -        const termsLoaded = async (title, entries, total, current) => { +        const termMetaDataLoaded = async (summary, entries, total, current) => {              if (callback) {                  callback(total, current);              }              const rows = []; -            for (const [expression, reading, tags, rules, score, ...glossary] of entries) { +            for (const [expression, mode, data] of entries) {                  rows.push({                      expression, -                    reading, -                    tags, -                    rules, -                    score, -                    glossary, -                    dictionary: title +                    mode, +                    data, +                    dictionary: summary.title                  });              } -            await this.db.terms.bulkAdd(rows); +            await this.db.termMeta.bulkAdd(rows); +        }; + +        const kanjiDataLoaded = async (summary, entries, total, current)  => { +            if (callback) { +                callback(total, current); +            } + +            const rows = []; +            if (summary.version === 1) { +                for (const [character, onyomi, kunyomi, tags, ...meanings] of entries) { +                    rows.push({ +                        character, +                        onyomi, +                        kunyomi, +                        tags, +                        meanings, +                        dictionary: summary.title +                    }); +                } +            } else { +                for (const [character, onyomi, kunyomi, tags, meanings, stats] of entries) { +                    rows.push({ +                        character, +                        onyomi, +                        kunyomi, +                        tags, +                        meanings, +                        stats, +                        dictionary: summary.title +                    }); +                } +            } + +            await this.db.kanji.bulkAdd(rows);          }; -        const kanjiLoaded = async (title, entries, total, current)  => { +        const kanjiMetaDataLoaded = async (summary, entries, total, current) => {              if (callback) {                  callback(total, current);              }              const rows = []; -            for (const [character, onyomi, kunyomi, tags, ...meanings] of entries) { +            for (const [character, mode, data] of entries) {                  rows.push({                      character, -                    onyomi, -                    kunyomi, -                    tags, -                    meanings, -                    dictionary: title +                    mode, +                    data, +                    dictionary: summary.title                  });              } -            await this.db.kanji.bulkAdd(rows); +            await this.db.kanjiMeta.bulkAdd(rows);          }; -        await Database.importDictionaryZip(archive, indexLoaded, termsLoaded, kanjiLoaded); -        return summary; -    } +        const tagDataLoaded = async (summary, entries, total, current) => { +            if (callback) { +                callback(total, current); +            } -    static async importDictionaryZip(archive, indexLoaded, termsLoaded, kanjiLoaded) { -        const files = (await JSZip.loadAsync(archive)).files; +            const rows = []; +            for (const [name, category, order, notes] of entries) { +                const row = dictTagSanitize({ +                    name, +                    category, +                    order, +                    notes, +                    dictionary: summary.title +                }); + +                rows.push(row); +            } -        const indexFile = files['index.json']; +            await this.db.tagMeta.bulkAdd(rows); +        }; + +        return await Database.importDictionaryZip( +            archive, +            indexDataLoaded, +            termDataLoaded, +            termMetaDataLoaded, +            kanjiDataLoaded, +            kanjiMetaDataLoaded, +            tagDataLoaded +        ); +    } + +    static async importDictionaryZip( +        archive, +        indexDataLoaded, +        termDataLoaded, +        termMetaDataLoaded, +        kanjiDataLoaded, +        kanjiMetaDataLoaded, +        tagDataLoaded +    ) { +        const zip = await JSZip.loadAsync(archive); + +        const indexFile = zip.files['index.json'];          if (!indexFile) { -            throw 'no dictionary index found in archive'; +            throw 'No dictionary index found in archive';          }          const index = JSON.parse(await indexFile.async('string')); -        if (!index.title || !index.version || !index.revision) { -            throw 'unrecognized dictionary format'; +        if (!index.title || !index.revision) { +            throw 'Unrecognized dictionary format';          } -        await indexLoaded( -            index.title, -            index.version, -            index.revision, -            index.tagMeta || {}, -            index.termBanks > 0, -            index.kanjiBanks > 0 -        ); +        const summary = { +            title: index.title, +            revision: index.revision, +            version: index.format || index.version +        }; + +        if (indexDataLoaded) { +            await indexDataLoaded(summary); +        } -        const banksTotal = index.termBanks + index.kanjiBanks; -        let banksLoaded = 0; +        const buildTermBankName      = index => `term_bank_${index + 1}.json`; +        const buildTermMetaBankName  = index => `term_meta_bank_${index + 1}.json`; +        const buildKanjiBankName     = index => `kanji_bank_${index + 1}.json`; +        const buildKanjiMetaBankName = index => `kanji_meta_bank_${index + 1}.json`; +        const buildTagBankName       = index => `tag_bank_${index + 1}.json`; -        for (let i = 1; i <= index.termBanks; ++i) { -            const bankFile = files[`term_bank_${i}.json`]; -            if (bankFile) { -                const bank = JSON.parse(await bankFile.async('string')); -                await termsLoaded(index.title, bank, banksTotal, banksLoaded++); -            } else { -                throw 'missing term bank file'; +        const countBanks = namer => { +            let count = 0; +            while (zip.files[namer(count)]) { +                ++count;              } -        } -        for (let i = 1; i <= index.kanjiBanks; ++i) { -            const bankFile = files[`kanji_bank_${i}.json`]; -            if (bankFile) { -                const bank = JSON.parse(await bankFile.async('string')); -                await kanjiLoaded(index.title, bank, banksTotal, banksLoaded++); -            } else { -                throw 'missing kanji bank file'; +            return count; +        }; + +        const termBankCount      = countBanks(buildTermBankName); +        const termMetaBankCount  = countBanks(buildTermMetaBankName); +        const kanjiBankCount     = countBanks(buildKanjiBankName); +        const kanjiMetaBankCount = countBanks(buildKanjiMetaBankName); +        const tagBankCount       = countBanks(buildTagBankName); + +        let bankLoadedCount = 0; +        let bankTotalCount = +            termBankCount + +            termMetaBankCount + +            kanjiBankCount + +            kanjiMetaBankCount + +            tagBankCount; + +        if (tagDataLoaded && index.tagMeta) { +            const bank = []; +            for (const name in index.tagMeta) { +                const tag = index.tagMeta[name]; +                bank.push([name, tag.category, tag.order, tag.notes]);              } + +            tagDataLoaded(summary, bank, ++bankTotalCount, bankLoadedCount++);          } + +        const loadBank = async (summary, namer, count, callback) => { +            if (callback) { +                for (let i = 0; i < count; ++i) { +                    const bankFile = zip.files[namer(i)]; +                    const bank = JSON.parse(await bankFile.async('string')); +                    await callback(summary, bank, bankTotalCount, bankLoadedCount++); +                } +            } +        }; + +        await loadBank(summary, buildTermBankName, termBankCount, termDataLoaded); +        await loadBank(summary, buildTermMetaBankName, termMetaBankCount, termMetaDataLoaded); +        await loadBank(summary, buildKanjiBankName, kanjiBankCount, kanjiDataLoaded); +        await loadBank(summary, buildKanjiMetaBankName, kanjiMetaBankCount, kanjiMetaDataLoaded); +        await loadBank(summary, buildTagBankName, tagBankCount, tagDataLoaded); + +        return summary;      }  } diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js index e749390f..57acbe5e 100644 --- a/ext/bg/js/dictionary.js +++ b/ext/bg/js/dictionary.js @@ -55,14 +55,6 @@ function dictRowsSort(rows, options) {  function dictTermsSort(definitions, dictionaries=null) {      return definitions.sort((v1, v2) => { -        const sl1 = v1.source.length; -        const sl2 = v2.source.length; -        if (sl1 > sl2) { -            return -1; -        } else if (sl1 < sl2) { -            return 1; -        } -          if (dictionaries !== null) {              const p1 = (dictionaries[v1.dictionary] || {}).priority || 0;              const p2 = (dictionaries[v2.dictionary] || {}).priority || 0; @@ -73,11 +65,11 @@ function dictTermsSort(definitions, dictionaries=null) {              }          } -        const s1 = v1.score; -        const s2 = v2.score; -        if (s1 > s2) { +        const sl1 = v1.source.length; +        const sl2 = v2.source.length; +        if (sl1 > sl2) {              return -1; -        } else if (s1 < s2) { +        } else if (sl1 < sl2) {              return 1;          } @@ -89,6 +81,14 @@ function dictTermsSort(definitions, dictionaries=null) {              return 1;          } +        const s1 = v1.score; +        const s2 = v2.score; +        if (s1 > s2) { +            return -1; +        } else if (s1 < s2) { +            return 1; +        } +          return v2.expression.localeCompare(v1.expression);      });  } @@ -148,16 +148,6 @@ function dictTagBuildSource(name) {      return dictTagSanitize({name, category: 'dictionary', order: 100});  } -function dictTagBuild(name, meta) { -    const tag = {name}; -    const symbol = name.split(':')[0]; -    for (const prop in meta[symbol] || {}) { -        tag[prop] = meta[symbol][prop]; -    } - -    return dictTagSanitize(tag); -} -  function dictTagSanitize(tag) {      tag.name = tag.name || 'untitled';      tag.category = tag.category || 'default'; diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index dcad97d4..36ab7694 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -146,7 +146,8 @@ function optionsSetDefaults(options) {              middleMouse: true,              selectText: true,              alphanumeric: true, -            delay: 15, +            autoHideResults: false, +            delay: 20,              length: 10,              modifier: 'shift'          }, diff --git a/ext/bg/js/request.js b/ext/bg/js/request.js index 94fd135a..e4359863 100644 --- a/ext/bg/js/request.js +++ b/ext/bg/js/request.js @@ -22,7 +22,7 @@ function requestJson(url, action, params) {          const xhr = new XMLHttpRequest();          xhr.overrideMimeType('application/json');          xhr.addEventListener('load', () => resolve(xhr.responseText)); -        xhr.addEventListener('error', () => reject('failed to execute network request')); +        xhr.addEventListener('error', () => reject('Failed to connect'));          xhr.open(action, url);          if (params) {              xhr.send(JSON.stringify(params)); @@ -34,7 +34,7 @@ function requestJson(url, action, params) {              return JSON.parse(responseText);          }          catch (e) { -            return Promise.reject('invalid JSON response'); +            return Promise.reject('Invalid response');          }      });  } diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index 54cda8ec..40bf2019 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -29,7 +29,7 @@ class DisplaySearch extends Display {      }      onError(error) { -        window.alert(`Error: ${error}`); +        window.alert(`Error: ${error.toString ? error.toString() : error}`);      }      onSearchClear() { diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js index 55b469d0..a2f22371 100644 --- a/ext/bg/js/settings.js +++ b/ext/bg/js/settings.js @@ -35,6 +35,7 @@ async function formRead() {      optionsNew.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked');      optionsNew.scanning.selectText = $('#select-matched-text').prop('checked');      optionsNew.scanning.alphanumeric = $('#search-alphanumeric').prop('checked'); +    optionsNew.scanning.autoHideResults = $('#auto-hide-results').prop('checked');      optionsNew.scanning.delay = parseInt($('#scan-delay').val(), 10);      optionsNew.scanning.length = parseInt($('#scan-length').val(), 10);      optionsNew.scanning.modifier = $('#scan-modifier-key').val(); @@ -82,7 +83,9 @@ function formUpdateVisibility(options) {      const debug = $('#debug');      if (options.general.debugInfo) { -        const text = JSON.stringify(options, null, 4); +        const temp = utilIsolate(options); +        temp.anki.fieldTemplates = '...'; +        const text = JSON.stringify(temp, null, 4);          debug.html(handlebarsEscape(text));          debug.show();      } else { @@ -134,11 +137,12 @@ async function onReady() {      $('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse);      $('#select-matched-text').prop('checked', options.scanning.selectText);      $('#search-alphanumeric').prop('checked', options.scanning.alphanumeric); +    $('#auto-hide-results').prop('checked', options.scanning.autoHideResults);      $('#scan-delay').val(options.scanning.delay);      $('#scan-length').val(options.scanning.length);      $('#scan-modifier-key').val(options.scanning.modifier); -    $('#dict-purge').click(utilAsync(onDictionaryPurge)); +    $('#dict-purge-link').click(utilAsync(onDictionaryPurge));      $('#dict-file').change(utilAsync(onDictionaryImport));      $('#anki-enable').prop('checked', options.anki.enable); @@ -175,7 +179,33 @@ $(document).ready(utilAsync(onReady));  function dictionaryErrorShow(error) {      const dialog = $('#dict-error');      if (error) { -        dialog.show().find('span').text(error); +        const overrides = [ +            [ +                'A mutation operation was attempted on a database that did not allow mutations.', +                'Access to IndexedDB appears to be restricted. Firefox seems to require that the history preference is set to "Remember history" before IndexedDB use of any kind is allowed.' +            ], +            [ +                'The operation failed for reasons unrelated to the database itself and not covered by any other error code.', +                'Unable to access IndexedDB due to a possibly corrupt user profile. Try using the "Refresh Firefox" feature to reset your user profile.' +            ], +            [ +                'BulkError', +                'Unable to finish importing dictionary data into IndexedDB. This may indicate that you do not have sufficient disk space available to complete this operation.' +            ] +        ]; + +        if (error.toString) { +            error = error.toString(); +        } + +        for (const [match, subst] of overrides) { +            if (error.includes(match)) { +                error = subst; +                break; +            } +        } + +        dialog.show().text(error);      } else {          dialog.hide();      } @@ -211,7 +241,7 @@ async function dictionaryGroupsPopulate(options) {      const dictGroups = $('#dict-groups').empty();      const dictWarning = $('#dict-warning').hide(); -    const dictRows = await utilDatabaseGetDictionaries(); +    const dictRows = await utilDatabaseGetTitles();      if (dictRows.length === 0) {          dictWarning.show();      } @@ -241,7 +271,7 @@ async function onDictionaryPurge(e) {      e.preventDefault();      const dictControls = $('#dict-importer, #dict-groups').hide(); -    const dictProgress = $('#dict-purge-progress').show(); +    const dictProgress = $('#dict-purge').show();      try {          dictionaryErrorShow(); @@ -310,7 +340,7 @@ function ankiSpinnerShow(show) {  function ankiErrorShow(error) {      const dialog = $('#anki-error');      if (error) { -        dialog.show().find('span').text(error); +        dialog.show().text(error);      }      else {          dialog.hide(); diff --git a/ext/bg/js/templates.js b/ext/bg/js/templates.js index 1c059365..b5d352fc 100644 --- a/ext/bg/js/templates.js +++ b/ext/bg/js/templates.js @@ -22,38 +22,87 @@ templates['dictionary.html'] = template({"1":function(container,depth0,helpers,p      + "\" class=\"form-control dict-priority\">\n    </div>\n</div>\n";  },"useData":true});  templates['kanji.html'] = template({"1":function(container,depth0,helpers,partials,data) { +    var stack1; + +  return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.data : depth0),{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.program(8, data, 0),"data":data})) != null ? stack1 : ""); +},"2":function(container,depth0,helpers,partials,data) { +    var stack1; + +  return "<table class=\"info-output\">\n" +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.data : depth0),{"name":"each","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "</table>\n"; +},"3":function(container,depth0,helpers,partials,data) { +    var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}); + +  return "    <tr>\n        <th>" +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.notes : depth0),{"name":"if","hash":{},"fn":container.program(4, data, 0),"inverse":container.program(6, data, 0),"data":data})) != null ? stack1 : "") +    + "</th>\n        <td>" +    + container.escapeExpression(((helper = (helper = helpers.value || (depth0 != null ? depth0.value : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(alias1,{"name":"value","hash":{},"data":data}) : helper))) +    + "</td>\n    </tr>\n"; +},"4":function(container,depth0,helpers,partials,data) { +    var helper; + +  return container.escapeExpression(((helper = (helper = helpers.notes || (depth0 != null ? depth0.notes : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"notes","hash":{},"data":data}) : helper))); +},"6":function(container,depth0,helpers,partials,data) { +    var helper; + +  return container.escapeExpression(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"name","hash":{},"data":data}) : helper))); +},"8":function(container,depth0,helpers,partials,data) { +    return "No data found\n"; +},"10":function(container,depth0,helpers,partials,data) {      var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {});    return "<div class=\"entry\" data-type=\"kanji\">\n    <div class=\"actions\">\n" -    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.source : depth0),{"name":"if","hash":{},"fn":container.program(4, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(11, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.source : depth0),{"name":"if","hash":{},"fn":container.program(13, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "        <img src=\"/mixed/img/entry-current.png\" class=\"current\" title=\"Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)\" alt>\n    </div>\n\n    <div class=\"glyph\">"      + container.escapeExpression(((helper = (helper = helpers.character || (depth0 != null ? depth0.character : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(alias1,{"name":"character","hash":{},"data":data}) : helper))) -    + "</div>\n\n    <div class=\"reading\">\n        <table>\n            <tr>\n                <th>Kunyomi:</th>\n                <td>\n" -    + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.kunyomi : depth0),{"name":"each","hash":{},"fn":container.program(6, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "                </td>\n            </tr>\n            <tr>\n                <th>Onyomi:</th>\n                <td>\n" -    + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.onyomi : depth0),{"name":"each","hash":{},"fn":container.program(6, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "                </td>\n            </tr>\n        </table>\n    </div>\n\n    <div>\n" -    + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.tags : depth0),{"name":"each","hash":{},"fn":container.program(9, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "    </div>\n\n    <div class=\"glossary\">\n" -    + ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(11, data, 0),"inverse":container.program(15, data, 0),"data":data})) != null ? stack1 : "") -    + "    </div>\n\n" -    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(18, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "</div>\n\n" +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(15, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "\n" +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.tags : depth0),{"name":"if","hash":{},"fn":container.program(18, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "\n    <table class=\"table table-condensed glyph-data\">\n        <tr>\n            <th>Glossary</th>\n            <th>Readings</th>\n            <th>Statistics</th>\n        </tr>\n        <tr>\n            <td class=\"glossary\">\n" +    + ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(21, data, 0),"inverse":container.program(24, data, 0),"data":data})) != null ? stack1 : "") +    + "            </td>\n            <td class=\"reading\">\n                " +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.onyomi : depth0),{"name":"if","hash":{},"fn":container.program(26, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "\n                " +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.kunyomi : depth0),{"name":"if","hash":{},"fn":container.program(29, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "\n            </td>\n            <td>" +    + ((stack1 = container.invokePartial(partials.table,depth0,{"name":"table","hash":{"data":((stack1 = (depth0 != null ? depth0.stats : depth0)) != null ? stack1.misc : stack1)},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") +    + "</td>\n        </tr>\n        <tr>\n            <th colspan=\"3\">Classifications</th>\n        </tr>\n        <tr>\n            <td colspan=\"3\">" +    + ((stack1 = container.invokePartial(partials.table,depth0,{"name":"table","hash":{"data":((stack1 = (depth0 != null ? depth0.stats : depth0)) != null ? stack1["class"] : stack1)},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") +    + "</td>\n        </tr>\n        <tr>\n            <th colspan=\"3\">Codepoints</th>\n        </tr>\n        <tr>\n            <td colspan=\"3\">" +    + ((stack1 = container.invokePartial(partials.table,depth0,{"name":"table","hash":{"data":((stack1 = (depth0 != null ? depth0.stats : depth0)) != null ? stack1.code : stack1)},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") +    + "</td>\n        </tr>\n        <tr>\n            <th colspan=\"3\">Dictionary Indices</th>\n        </tr>\n        <tr>\n            <td colspan=\"3\">" +    + ((stack1 = container.invokePartial(partials.table,depth0,{"name":"table","hash":{"data":((stack1 = (depth0 != null ? depth0.stats : depth0)) != null ? stack1.index : stack1)},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") +    + "</td>\n        </tr>\n    </table>\n\n" +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(31, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "</div>\n"; -},"2":function(container,depth0,helpers,partials,data) { +},"11":function(container,depth0,helpers,partials,data) {      return "        <a href=\"#\" class=\"action-view-note pending disabled\"><img src=\"/mixed/img/view-note.png\" title=\"View added note (Alt + V)\" alt></a>\n        <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"kanji\"><img src=\"/mixed/img/add-kanji.png\" title=\"Add Kanji (Alt + K)\" alt></a>\n"; -},"4":function(container,depth0,helpers,partials,data) { +},"13":function(container,depth0,helpers,partials,data) {      return "        <a href=\"#\" class=\"source-term\"><img src=\"/mixed/img/source-term.png\" title=\"Source term (Alt + B)\" alt></a>\n"; -},"6":function(container,depth0,helpers,partials,data) { +},"15":function(container,depth0,helpers,partials,data) {      var stack1; -  return "                    " -    + container.escapeExpression(container.lambda(depth0, depth0)) -    + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.last),{"name":"unless","hash":{},"fn":container.program(7, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "\n"; -},"7":function(container,depth0,helpers,partials,data) { -    return ", "; -},"9":function(container,depth0,helpers,partials,data) { +  return "    <div>\n" +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(16, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "    </div>\n"; +},"16":function(container,depth0,helpers,partials,data) { +    var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; + +  return "        <span class=\"label label-default tag-frequency\">" +    + alias4(((helper = (helper = helpers.dictionary || (depth0 != null ? depth0.dictionary : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"dictionary","hash":{},"data":data}) : helper))) +    + ":" +    + alias4(((helper = (helper = helpers.frequency || (depth0 != null ? depth0.frequency : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"frequency","hash":{},"data":data}) : helper))) +    + "</span>\n"; +},"18":function(container,depth0,helpers,partials,data) { +    var stack1; + +  return "    <div>\n" +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.tags : depth0),{"name":"each","hash":{},"fn":container.program(19, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "    </div>\n"; +},"19":function(container,depth0,helpers,partials,data) {      var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;    return "        <span class=\"label label-default tag-" @@ -63,67 +112,74 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia      + "\">"      + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))      + "</span>\n"; -},"11":function(container,depth0,helpers,partials,data) { +},"21":function(container,depth0,helpers,partials,data) {      var stack1; -  return "        <ol>\n" -    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(12, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "        </ol>\n"; -},"12":function(container,depth0,helpers,partials,data) { -    var stack1, helper, options, buffer =  -  "            <li><span class=\"glossary-item\">"; -  stack1 = ((helper = (helper = helpers.multiLine || (depth0 != null ? depth0.multiLine : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"multiLine","hash":{},"fn":container.program(13, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper)); -  if (!helpers.multiLine) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)} -  if (stack1 != null) { buffer += stack1; } -  return buffer + "</span></li>\n"; -},"13":function(container,depth0,helpers,partials,data) { -    return container.escapeExpression(container.lambda(depth0, depth0)); -},"15":function(container,depth0,helpers,partials,data) { -    var stack1, helper, options, buffer =  -  "        <div class=\"glossary-item\">"; -  stack1 = ((helper = (helper = helpers.multiLine || (depth0 != null ? depth0.multiLine : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"multiLine","hash":{},"fn":container.program(16, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper)); -  if (!helpers.multiLine) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)} -  if (stack1 != null) { buffer += stack1; } -  return buffer + "</div>\n"; -},"16":function(container,depth0,helpers,partials,data) { +  return "                <ol>" +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(22, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "</ol>\n"; +},"22":function(container,depth0,helpers,partials,data) { +    return "<li><span class=\"glossary-item\">" +    + container.escapeExpression(container.lambda(depth0, depth0)) +    + "</span></li>"; +},"24":function(container,depth0,helpers,partials,data) {      var stack1; -  return container.escapeExpression(container.lambda(((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["0"] : stack1), depth0)); -},"18":function(container,depth0,helpers,partials,data) { +  return "                <span class=\"glossary-item\">" +    + container.escapeExpression(container.lambda(((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["0"] : stack1), depth0)) +    + "</span>\n"; +},"26":function(container,depth0,helpers,partials,data) { +    var stack1; + +  return "<dl>" +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.onyomi : depth0),{"name":"each","hash":{},"fn":container.program(27, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "</dl>"; +},"27":function(container,depth0,helpers,partials,data) { +    return "<dd>" +    + container.escapeExpression(container.lambda(depth0, depth0)) +    + "</dd>"; +},"29":function(container,depth0,helpers,partials,data) { +    var stack1; + +  return "<dl>" +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.kunyomi : depth0),{"name":"each","hash":{},"fn":container.program(27, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "</dl>"; +},"31":function(container,depth0,helpers,partials,data) {      var stack1, helper, options, buffer =     "    <pre>"; -  stack1 = ((helper = (helper = helpers.dumpObject || (depth0 != null ? depth0.dumpObject : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"dumpObject","hash":{},"fn":container.program(19, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper)); +  stack1 = ((helper = (helper = helpers.dumpObject || (depth0 != null ? depth0.dumpObject : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"dumpObject","hash":{},"fn":container.program(32, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper));    if (!helpers.dumpObject) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}    if (stack1 != null) { buffer += stack1; }    return buffer + "</pre>\n"; -},"19":function(container,depth0,helpers,partials,data) { +},"32":function(container,depth0,helpers,partials,data) {      var stack1;    return ((stack1 = container.lambda(depth0, depth0)) != null ? stack1 : ""); -},"21":function(container,depth0,helpers,partials,data,blockParams,depths) { +},"34":function(container,depth0,helpers,partials,data,blockParams,depths) {      var stack1; -  return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(22, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); -},"22":function(container,depth0,helpers,partials,data,blockParams,depths) { +  return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(35, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"35":function(container,depth0,helpers,partials,data,blockParams,depths) {      var stack1; -  return ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.first),{"name":"unless","hash":{},"fn":container.program(23, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") +  return ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.first),{"name":"unless","hash":{},"fn":container.program(36, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "\n"      + ((stack1 = container.invokePartial(partials.kanji,depth0,{"name":"kanji","hash":{"root":(depths[1] != null ? depths[1].root : depths[1]),"source":(depths[1] != null ? depths[1].source : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1]),"debug":(depths[1] != null ? depths[1].debug : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); -},"23":function(container,depth0,helpers,partials,data) { +},"36":function(container,depth0,helpers,partials,data) {      return "<hr>"; -},"25":function(container,depth0,helpers,partials,data) { -    return "<p class=\"note\">No results found.</p>\n"; +},"38":function(container,depth0,helpers,partials,data) { +    return "<p class=\"note\">No results found</p>\n";  },"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) {      var stack1; -  return "\n" -    + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(21, data, 0, blockParams, depths),"inverse":container.program(25, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); +  return "\n\n" +    + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(34, data, 0, blockParams, depths),"inverse":container.program(38, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "");  },"main_d":  function(fn, props, container, depth0, data, blockParams, depths) {    var decorators = container.decorators; -  fn = decorators.inline(fn,props,container,{"name":"inline","hash":{},"fn":container.program(1, data, 0, blockParams, depths),"inverse":container.noop,"args":["kanji"],"data":data}) || fn; +  fn = decorators.inline(fn,props,container,{"name":"inline","hash":{},"fn":container.program(1, data, 0, blockParams, depths),"inverse":container.noop,"args":["table"],"data":data}) || fn; +  fn = decorators.inline(fn,props,container,{"name":"inline","hash":{},"fn":container.program(10, data, 0, blockParams, depths),"inverse":container.noop,"args":["kanji"],"data":data}) || fn;    return fn;    } @@ -203,10 +259,12 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia    if (stack1 != null) { buffer += stack1; }    return buffer + "</div>\n\n"      + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.reasons : depth0),{"name":"if","hash":{},"fn":container.program(20, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "\n" +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(24, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "\n    <div class=\"glossary\">\n" -    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.grouped : depth0),{"name":"if","hash":{},"fn":container.program(24, data, 0),"inverse":container.program(30, data, 0),"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.grouped : depth0),{"name":"if","hash":{},"fn":container.program(27, data, 0),"inverse":container.program(33, data, 0),"data":data})) != null ? stack1 : "")      + "    </div>\n\n" -    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(32, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(35, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "</div>\n";  },"13":function(container,depth0,helpers,partials,data) {      return "        <a href=\"#\" class=\"action-view-note pending disabled\"><img src=\"/mixed/img/view-note.png\" title=\"View added note (Alt + V)\" alt></a>\n        <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"term-kanji\"><img src=\"/mixed/img/add-term-kanji.png\" title=\"Add expression (Alt + E)\" alt></a>\n        <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"term-kana\"><img src=\"/mixed/img/add-term-kana.png\" title=\"Add reading (Alt + R)\" alt></a>\n"; @@ -242,53 +300,67 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia  },"24":function(container,depth0,helpers,partials,data) {      var stack1; -  return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),((stack1 = (depth0 != null ? depth0.definitions : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(25, data, 0),"inverse":container.program(28, data, 0),"data":data})) != null ? stack1 : ""); +  return "    <div>\n" +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(25, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "    </div>\n";  },"25":function(container,depth0,helpers,partials,data) { +    var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; + +  return "        <span class=\"label label-default tag-frequency\">" +    + alias4(((helper = (helper = helpers.dictionary || (depth0 != null ? depth0.dictionary : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"dictionary","hash":{},"data":data}) : helper))) +    + ":" +    + alias4(((helper = (helper = helpers.frequency || (depth0 != null ? depth0.frequency : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"frequency","hash":{},"data":data}) : helper))) +    + "</span>\n"; +},"27":function(container,depth0,helpers,partials,data) { +    var stack1; + +  return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),((stack1 = (depth0 != null ? depth0.definitions : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(28, data, 0),"inverse":container.program(31, data, 0),"data":data})) != null ? stack1 : ""); +},"28":function(container,depth0,helpers,partials,data) {      var stack1;    return "        <ol>\n" -    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(26, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(29, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "        </ol>\n"; -},"26":function(container,depth0,helpers,partials,data) { +},"29":function(container,depth0,helpers,partials,data) {      var stack1;    return "            <li>"      + ((stack1 = container.invokePartial(partials.definition,depth0,{"name":"definition","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "")      + "</li>\n"; -},"28":function(container,depth0,helpers,partials,data) { +},"31":function(container,depth0,helpers,partials,data) {      var stack1;    return ((stack1 = container.invokePartial(partials.definition,((stack1 = (depth0 != null ? depth0.definitions : depth0)) != null ? stack1["0"] : stack1),{"name":"definition","data":data,"indent":"        ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); -},"30":function(container,depth0,helpers,partials,data) { +},"33":function(container,depth0,helpers,partials,data) {      var stack1;    return ((stack1 = container.invokePartial(partials.definition,depth0,{"name":"definition","data":data,"indent":"        ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); -},"32":function(container,depth0,helpers,partials,data) { +},"35":function(container,depth0,helpers,partials,data) {      var stack1, helper, options, buffer =     "    <pre>";    stack1 = ((helper = (helper = helpers.dumpObject || (depth0 != null ? depth0.dumpObject : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"dumpObject","hash":{},"fn":container.program(18, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper));    if (!helpers.dumpObject) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}    if (stack1 != null) { buffer += stack1; }    return buffer + "</pre>\n"; -},"34":function(container,depth0,helpers,partials,data,blockParams,depths) { +},"37":function(container,depth0,helpers,partials,data,blockParams,depths) {      var stack1; -  return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(35, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); -},"35":function(container,depth0,helpers,partials,data,blockParams,depths) { +  return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(38, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"38":function(container,depth0,helpers,partials,data,blockParams,depths) {      var stack1; -  return ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.first),{"name":"unless","hash":{},"fn":container.program(36, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") +  return ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.first),{"name":"unless","hash":{},"fn":container.program(39, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "\n"      + ((stack1 = container.invokePartial(partials.term,depth0,{"name":"term","hash":{"playback":(depths[1] != null ? depths[1].playback : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1]),"grouped":(depths[1] != null ? depths[1].grouped : depths[1]),"debug":(depths[1] != null ? depths[1].debug : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); -},"36":function(container,depth0,helpers,partials,data) { +},"39":function(container,depth0,helpers,partials,data) {      return "<hr>"; -},"38":function(container,depth0,helpers,partials,data) { +},"41":function(container,depth0,helpers,partials,data) {      return "<p class=\"note\">No results found.</p>\n";  },"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) {      var stack1;    return "\n\n" -    + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(34, data, 0, blockParams, depths),"inverse":container.program(38, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); +    + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(37, data, 0, blockParams, depths),"inverse":container.program(41, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "");  },"main_d":  function(fn, props, container, depth0, data, blockParams, depths) {    var decorators = container.decorators; diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js index 1be485c7..c915dbc0 100644 --- a/ext/bg/js/translator.js +++ b/ext/bg/js/translator.js @@ -37,8 +37,26 @@ class Translator {      }      async findTermsGrouped(text, dictionaries, alphanumeric) { +        const titles = Object.keys(dictionaries);          const {length, definitions} = await this.findTerms(text, dictionaries, alphanumeric); -        return {length, definitions: dictTermsGroup(definitions, dictionaries)}; + +        const definitionsGrouped = dictTermsGroup(definitions, dictionaries); +        for (const definition of definitionsGrouped) { +            await this.buildTermFrequencies(definition, titles); +        } + +        return {length, definitions: definitionsGrouped}; +    } + +    async findTermsSplit(text, dictionaries, alphanumeric) { +        const titles = Object.keys(dictionaries); +        const {length, definitions} = await this.findTerms(text, dictionaries, alphanumeric); + +        for (const definition of definitions) { +            await this.buildTermFrequencies(definition, titles); +        } + +        return {length, definitions};      }      async findTerms(text, dictionaries, alphanumeric) { @@ -51,17 +69,18 @@ class Translator {          const cache = {};          const titles = Object.keys(dictionaries); -        let deinflections = await this.findTermsDeinflected(text, titles, cache); +        let deinflections = await this.findTermDeinflections(text, titles, cache);          const textHiragana = jpKatakanaToHiragana(text);          if (text !== textHiragana) { -            deinflections = deinflections.concat(await this.findTermsDeinflected(textHiragana, titles, cache)); +            deinflections = deinflections.concat(await this.findTermDeinflections(textHiragana, titles, cache));          }          let definitions = [];          for (const deinflection of deinflections) {              for (const definition of deinflection.definitions) { -                const tags = definition.tags.map(tag => dictTagBuild(tag, definition.tagMeta)); +                const tags = await this.expandTags(definition.tags, definition.dictionary);                  tags.push(dictTagBuildSource(definition.dictionary)); +                  definitions.push({                      source: deinflection.source,                      reasons: deinflection.reasons, @@ -87,7 +106,7 @@ class Translator {          return {length, definitions};      } -    async findTermsDeinflected(text, titles, cache) { +    async findTermDeinflections(text, titles, cache) {          const definer = async term => {              if (cache.hasOwnProperty(term)) {                  return cache[term]; @@ -117,11 +136,88 @@ class Translator {          }          for (const definition of definitions) { -            const tags = definition.tags.map(tag => dictTagBuild(tag, definition.tagMeta)); +            const tags = await this.expandTags(definition.tags, definition.dictionary);              tags.push(dictTagBuildSource(definition.dictionary)); +              definition.tags = dictTagsSort(tags); +            definition.stats = await this.expandStats(definition.stats, definition.dictionary); + +            definition.frequencies = []; +            for (const meta of await this.database.findKanjiMeta(definition.character, titles)) { +                if (meta.mode === 'freq') { +                    definition.frequencies.push({ +                        character: meta.character, +                        frequency: meta.data, +                        dictionary: meta.dictionary +                    }); +                } +            }          }          return definitions;      } + +    async buildTermFrequencies(definition, titles) { +        definition.frequencies = []; +        for (const meta of await this.database.findTermMeta(definition.expression, titles)) { +            if (meta.mode === 'freq') { +                definition.frequencies.push({ +                    expression: meta.expression, +                    frequency: meta.data, +                    dictionary: meta.dictionary +                }); +            } +        } +    } + +    async expandTags(names, title) { +        const tags = []; +        for (const name of names) { +            const base = name.split(':')[0]; +            const meta = await this.database.findTagForTitle(base, title); + +            const tag = {name}; +            for (const prop in meta || {}) { +                if (prop !== 'name') { +                    tag[prop] = meta[prop]; +                } +            } + +            tags.push(dictTagSanitize(tag)); +        } + +        return tags; +    } + +    async expandStats(items, title) { +        const stats = {}; +        for (const name in items) { +            const base = name.split(':')[0]; +            const meta = await this.database.findTagForTitle(base, title); +            const group = stats[meta.category] = stats[meta.category] || []; + +            const stat = {name, value: items[name]}; +            for (const prop in meta || {}) { +                if (prop !== 'name') { +                    stat[prop] = meta[prop]; +                } +            } + +            group.push(dictTagSanitize(stat)); +        } + +        for (const category in stats) { +            stats[category].sort((a, b) => { +                if (a.notes < b.notes) { +                    return -1; +                } else if (a.notes > b.notes) { +                    return 1; +                } else { +                    return 0; +                } +            }); +        } + +        return stats; +    }  } diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js index a92fd0bc..f44582eb 100644 --- a/ext/bg/js/util.js +++ b/ext/bg/js/util.js @@ -42,8 +42,8 @@ function utilAnkiGetModelFieldNames(modelName) {      return utilBackend().anki.getModelFieldNames(modelName);  } -function utilDatabaseGetDictionaries() { -    return utilBackend().translator.database.getDictionaries(); +function utilDatabaseGetTitles() { +    return utilBackend().translator.database.getTitles();  }  function utilDatabasePurge() { diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 0a5c205c..4315d74b 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -7,7 +7,7 @@          <link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap/css/bootstrap-theme.min.css">          <style>              #anki-spinner, #anki-general, #anki-error, -            #dict-spinner, #dict-error, #dict-warning, #dict-purge-progress, #dict-import-progress, +            #dict-spinner, #dict-error, #dict-warning, #dict-purge, #dict-import-progress,              #debug, .options-advanced {                  display: none;              } @@ -96,6 +96,10 @@                      <label><input type="checkbox" id="search-alphanumeric"> Search alphanumeric text</label>                  </div> +                <div class="checkbox"> +                    <label><input type="checkbox" id="auto-hide-results"> Automatically hide results</label> +                </div> +                  <div class="form-group options-advanced">                      <label for="scan-delay">Scan delay (in milliseconds)</label>                      <input type="number" min="1" id="scan-delay" class="form-control"> @@ -125,24 +129,16 @@                  <p class="help-block">                      Yomichan can import and use a variety of dictionary formats. Unneeded dictionaries can be disabled, -                    or you can simply <a href="#" id="dict-purge">purge the database</a> to delete everything. +                    or you can simply <a href="#" id="dict-purge-link">purge the database</a> to delete everything.                  </p>                  <p class="help-block">                      Please visit the <a href="https://foosoft.net/projects/yomichan" target="_blank">Yomichan</a> homepage to download free                      dictionaries that you can use with this extension.                  </p> -                <div id="dict-purge-progress" class="text-danger">Dictionary data is being purged, please be patient...</div> - -                <div class="alert alert-warning" id="dict-warning"> -                    <strong>Warning:</strong> -                    <span>no dictionaries found; please use the importer below to install packaged dictionaries</span> -                </div> - -                <div class="alert alert-danger" id="dict-error"> -                    <strong>Error:</strong> -                    <span></span> -                </div> +                <div class="text-danger" id="dict-purge">Dictionary data is being purged, please be patient...</div> +                <div class="alert alert-warning" id="dict-warning">No dictionaries have been installed</div> +                <div class="alert alert-danger" id="dict-error"></div>                  <div id="dict-groups"></div> @@ -170,10 +166,7 @@                      <a href="https://foosoft.net/projects/anki-connect/" target="_blank">AnkiConnect</a> plugin.                  </p> -                <div class="alert alert-danger" id="anki-error"> -                    <strong>Error:</strong> -                    <span></span> -                </div> +                <div class="alert alert-danger" id="anki-error"></div>                  <div class="checkbox">                      <label><input type="checkbox" id="anki-enable"> Enable Anki integration</label> diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js index b1e71777..a1806d77 100644 --- a/ext/fg/js/document.js +++ b/ext/fg/js/document.js @@ -86,7 +86,7 @@ function docRangeFromPoint(point) {      }      const range = document.caretRangeFromPoint(point.x, point.y); -    if (range) { +    if (range && range.startContainer.nodeType === 3 && range.endContainer.nodeType === 3) {          return new TextSourceRange(range);      }  } diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index 22374f8b..ff50483d 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -27,7 +27,7 @@ class DisplayFloat extends Display {          if (window.yomichan_orphaned) {              this.onOrphaned();          } else { -            window.alert(`Error: ${error}`); +            window.alert(`Error: ${error.toString ? error.toString() : error}`);          }      } diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 41c93f00..3a90b3ad 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -80,7 +80,6 @@ class Frontend {          const search = async () => {              try {                  await this.searchAt({x: e.clientX, y: e.clientY}); -                this.pendingLookup = false;              } catch (e) {                  this.onError(e);              } @@ -153,7 +152,7 @@ class Frontend {      }      onError(error) { -        window.alert(`Error: ${error}`); +        window.alert(`Error: ${error.toString ? error.toString() : error}`);      }      popupTimerSet(callback) { @@ -169,27 +168,17 @@ class Frontend {      }      async searchAt(point) { -        let textSource = null; - -        try { -            if (this.pendingLookup) { -                return; -            } - -            textSource = docRangeFromPoint(point); -            if (!textSource || !textSource.containsPoint(point)) { -                docImposterDestroy(); -                return; -            } - -            if (this.textSourceLast && this.textSourceLast.equals(textSource)) { -                return; -            } +        if (this.pendingLookup || this.popup.containsPoint(point)) { +            return; +        } -            this.pendingLookup = true; +        const textSource = docRangeFromPoint(point); +        let hideResults = !textSource || !textSource.containsPoint(point); -            if (!await this.searchTerms(textSource)) { -                await this.searchKanji(textSource); +        try { +            if (!hideResults && (!this.textSourceLast || !this.textSourceLast.equals(textSource))) { +                this.pendingLookup = true; +                hideResults = !await this.searchTerms(textSource) && !await this.searchKanji(textSource);              }          } catch (e) {              if (window.yomichan_orphaned) { @@ -200,7 +189,12 @@ class Frontend {                  this.onError(e);              }          } finally { -            docImposterDestroy(); +            if (hideResults && this.options.scanning.autoHideResults) { +                this.searchClear(); +            } else { +                docImposterDestroy(); +            } +              this.pendingLookup = false;          }      } diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 03958832..d1009fe9 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -100,6 +100,21 @@ class Popup {          return this.injected && this.container.style.visibility !== 'hidden';      } +    containsPoint(point) { +        if (!this.isVisible()) { +            return false; +        } + +        const rect = this.container.getBoundingClientRect(); +        const contained = +            point.x >= rect.left && +            point.y >= rect.top && +            point.x < rect.right && +            point.y < rect.bottom; + +        return contained; +    } +      async termsShow(elementRect, definitions, options, context) {          await this.show(elementRect, options);          this.invokeApi('termsShow', {definitions, options, context}); diff --git a/ext/fg/js/source.js b/ext/fg/js/source.js index 3b6ecb2a..664dbec7 100644 --- a/ext/fg/js/source.js +++ b/ext/fg/js/source.js @@ -82,7 +82,7 @@ class TextSourceRange {      }      equals(other) { -        return other.range && other.range.compareBoundaryPoints(Range.START_TO_START, this.range) === 0; +        return other && other.range && other.range.compareBoundaryPoints(Range.START_TO_START, this.range) === 0;      }      static shouldEnter(node) { @@ -239,6 +239,6 @@ class TextSourceElement {      }      equals(other) { -        return other.element === this.element && other.content === this.content; +        return other && other.element === this.element && other.content === this.content;      }  } diff --git a/ext/manifest.json b/ext/manifest.json index 7ff76437..b6d0642e 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -1,7 +1,7 @@  {      "manifest_version": 2,      "name": "Yomichan", -    "version": "1.3.5", +    "version": "1.4.0",      "description": "Japanese dictionary with Anki integration",      "icons": {"16": "mixed/img/icon16.png", "48": "mixed/img/icon48.png", "128": "mixed/img/icon128.png"}, diff --git a/ext/mixed/css/display.css b/ext/mixed/css/display.css index 8b1819bd..cdc1be8c 100644 --- a/ext/mixed/css/display.css +++ b/ext/mixed/css/display.css @@ -84,6 +84,10 @@ hr {      background-color: #aa66cc;  } +.tag-frequency { +    background-color: #5cb85c; +} +  .actions .disabled {      pointer-events: none;      cursor: default; @@ -144,3 +148,15 @@ hr {      padding: 0.01em;      vertical-align: top;  } + +.glyph-data { +    margin-top: 10px; +} + +.info-output { +    width: 100%; +} + +.info-output td { +    text-align: right; +} diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 47efd195..75ee339a 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -32,11 +32,11 @@ class Display {      }      onError(error) { -        throw 'override me'; +        throw 'Override me';      }      onSearchClear() { -        throw 'override me'; +        throw 'Override me';      }      onSourceTermView(e) { @@ -350,7 +350,7 @@ class Display {                  Display.adderButtonFind(index, mode).addClass('disabled');                  Display.viewerButtonFind(index).removeClass('pending disabled').data('noteId', noteId);              } else { -                throw 'note could note be added'; +                throw 'Note could note be added';              }          } catch (e) {              this.onError(e); |