diff options
Diffstat (limited to 'ext/bg')
-rw-r--r-- | ext/bg/background.html | 2 | ||||
-rw-r--r-- | ext/bg/guide.html | 29 | ||||
-rw-r--r-- | ext/bg/import.html | 62 | ||||
-rw-r--r-- | ext/bg/js/deinflector.js | 56 | ||||
-rw-r--r-- | ext/bg/js/dictionary.js | 216 | ||||
-rw-r--r-- | ext/bg/js/import.js | 36 | ||||
-rw-r--r-- | ext/bg/js/options-form.js | 51 | ||||
-rw-r--r-- | ext/bg/js/options.js | 43 | ||||
-rw-r--r-- | ext/bg/js/translator.js | 294 | ||||
-rw-r--r-- | ext/bg/js/util.js | 46 | ||||
-rw-r--r-- | ext/bg/js/yomichan.js | 49 | ||||
-rw-r--r-- | ext/bg/options.html | 19 |
12 files changed, 585 insertions, 318 deletions
diff --git a/ext/bg/background.html b/ext/bg/background.html index c35e917d..c490df81 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -2,7 +2,9 @@ <html lang="en"> <body> <script src="../lib/handlebars.min.js"></script> + <script src="../lib/dexie.min.js"></script> <script src="js/templates.js"></script> + <script src="js/util.js"></script> <script src="js/dictionary.js"></script> <script src="js/deinflector.js"></script> <script src="js/translator.js"></script> diff --git a/ext/bg/guide.html b/ext/bg/guide.html deleted file mode 100644 index a3fa8221..00000000 --- a/ext/bg/guide.html +++ /dev/null @@ -1,29 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="UTF-8"> - <title>Yomichan Guide</title> - <link rel="stylesheet" type="text/css" href="../lib/bootstrap-3.3.6-dist/css/bootstrap.min.css"> - <link rel="stylesheet" type="text/css" href="../lib/bootstrap-3.3.6-dist/css/bootstrap-theme.min.css"> - </head> - <body> - <div class="container"> - - <div class="page-header"> - <h1>Yomichan Guide</h1> - </div> - - <p>This is a minimal guide to get you started with Yomichan. For complete documentation, visit the <a href="https://foosoft.net/projects/yomichan-chrome/">official homepage</a>.</p> - - <ol> - <li>Left-click on the <img src="../img/icon16.png" alt> icon to enable or disable Yomichan for the current browser instance.</li> - <li>Right-click on the <img src="../img/icon16.png" alt> icon and select <em>Options</em> to open the Yomichan options page.</li> - <li>Hold down <kbd>Shift</kbd> or the middle mouse button as you move your cursor over text to see definitions.</li> - <li>Resize the definition window by dragging the bottom-left corner inwards or outwards.</li> - <li>Click on Kanji in the definition window to view additional information about that character.</li> - </ol> - - <p>Enjoy!</p> - </div> - </body> -</html> diff --git a/ext/bg/import.html b/ext/bg/import.html new file mode 100644 index 00000000..b5d2db68 --- /dev/null +++ b/ext/bg/import.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <title>Yomichan Dictionary Import</title> + <link rel="stylesheet" type="text/css" href="../lib/bootstrap-3.3.6-dist/css/bootstrap.min.css"> + <link rel="stylesheet" type="text/css" href="../lib/bootstrap-3.3.6-dist/css/bootstrap-theme.min.css"> + <style> + div.alert { + display: none; + } + </style> + </head> + <body> + <div class="container"> + <div class="page-header"> + <h1>Welcome to Yomichan!</h1> + </div> + + <p>Thank you for downloading this extension! I sincerely hope that it will assist you on your language learning journey.</p> + + <div> + <h2>Dictionary Import</h2> + + <p> + Before it can be used for the first time, Yomichan must import the Japanese dictionary data included with this extension. This process can take a + couple of minutes to finish so please be patient! Please do not completely exit out of your browser until this process completes. + </p> + + <div class="progress"> + <div class="progress-bar progress-bar-striped" style="width: 0%"></div> + </div> + + <div class="alert alert-success">Dictionary import complete!</div> + </div> + + <div> + <h2>Quick Guide</h2> + + <p> + Please read the steps outlined below to get quickly get up and running with Yomichan. For complete documentation, + visit the <a href="https://foosoft.net/projects/yomichan-chrome/">official homepage</a>. + </p> + + <ol> + <li>Left-click on the <img src="../img/icon16.png" alt> icon to enable or disable Yomichan for the current browser instance.</li> + <li>Right-click on the <img src="../img/icon16.png" alt> icon and select <em>Options</em> to open the Yomichan options page.</li> + <li>Hold down <kbd>Shift</kbd> or the middle mouse button as you move your cursor over text to see definitions (or <kbd>Shift</kbd> + <kbd>Ctrl</kbd> for Kanji).</li> + <li>Resize the definitions window by dragging the bottom-left corner inwards or outwards.</li> + <li>Click on Kanji in the definition window to view additional information about that character.</li> + </ol> + </div> + + <br> + + <p>よろしくね!</p> + </div> + + <script src="../lib/jquery-2.2.2.min.js"></script> + <script src="js/import.js"></script> + </body> +</html> diff --git a/ext/bg/js/deinflector.js b/ext/bg/js/deinflector.js index 0eabd0f3..8b9f88e2 100644 --- a/ext/bg/js/deinflector.js +++ b/ext/bg/js/deinflector.js @@ -26,32 +26,36 @@ class Deinflection { } validate(validator) { - for (const tags of validator(this.term)) { - if (this.tags.length === 0) { - return true; - } - - for (const tag of this.tags) { - if (tags.indexOf(tag) !== -1) { + return validator(this.term).then(sets => { + for (const tags of sets) { + if (this.tags.length === 0) { return true; } + + for (const tag of this.tags) { + if (tags.includes(tag)) { + return true; + } + } } - } - return false; + return false; + }); } deinflect(validator, rules) { - if (this.validate(validator)) { - const child = new Deinflection(this.term, this.tags); - this.children.push(child); - } + const promises = [ + this.validate(validator).then(valid => { + const child = new Deinflection(this.term, this.tags); + this.children.push(child); + }) + ]; for (const rule in rules) { for (const variant of rules[rule]) { let allowed = this.tags.length === 0; for (const tag of this.tags) { - if (variant.ti.indexOf(tag) !== -1) { + if (variant.ti.includes(tag)) { allowed = true; break; } @@ -62,14 +66,24 @@ class Deinflection { } const term = this.term.slice(0, -variant.ki.length) + variant.ko; - const child = new Deinflection(term, variant.to, rule); - if (child.deinflect(validator, rules)) { - this.children.push(child); + if (term.length === 0) { + continue; } + + const child = new Deinflection(term, variant.to, rule); + promises.push( + child.deinflect(validator, rules).then(valid => { + if (valid) { + this.children.push(child); + } + } + )); } } - return this.children.length > 0; + return Promise.all(promises).then(() => { + return this.children.length > 0; + }); } gather() { @@ -105,10 +119,6 @@ class Deinflector { deinflect(term, validator) { const node = new Deinflection(term); - if (node.deinflect(validator, this.rules)) { - return node.gather(); - } - - return null; + return node.deinflect(validator, this.rules).then(success => success ? node.gather() : []); } } diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js index a6438523..4562c821 100644 --- a/ext/bg/js/dictionary.js +++ b/ext/bg/js/dictionary.js @@ -19,63 +19,199 @@ class Dictionary { constructor() { - this.termDicts = {}; - this.kanjiDicts = {}; + this.db = null; + this.dbVer = 1; + this.entities = null; } - addTermDict(name, dict) { - this.termDicts[name] = dict; - } + initDb() { + if (this.db !== null) { + return Promise.reject('database already initialized'); + } - addKanjiDict(name, dict) { - this.kanjiDicts[name] = dict; + this.db = new Dexie('dict'); + this.db.version(1).stores({ + terms: '++id,expression,reading', + entities: '++,name', + kanji: '++,character', + meta: 'name,value', + }); } - findTerm(term) { - let results = []; + prepareDb() { + this.initDb(); - for (let name in this.termDicts) { - const dict = this.termDicts[name]; - if (!(term in dict.i)) { - continue; + return this.db.meta.get('version').then(row => { + return row ? row.value : 0; + }).catch(() => { + return 0; + }).then(version => { + if (this.dbVer === version) { + return true; } - const indices = dict.i[term].split(' ').map(Number); - results = results.concat( - indices.map(index => { - const [e, r, t, ...g] = dict.d[index]; - return { - expression: e, - reading: r, - tags: t.split(' '), - glossary: g, - entities: dict.e, - id: index - }; - }) - ); + const db = this.db; + this.db.close(); + this.db = null; + + return db.delete().then(() => { + this.initDb(); + return false; + }); + }); + } + + sealDb() { + if (this.db === null) { + return Promise.reject('database not initialized'); } - return results; + return this.db.meta.put({name: 'version', value: this.dbVer}); + } + + findTerm(term) { + if (this.db === null) { + return Promise.reject('database not initialized'); + } + + const results = []; + return this.db.terms.where('expression').equals(term).or('reading').equals(term).each(row => { + results.push({ + expression: row.expression, + reading: row.reading, + tags: row.tags.split(' '), + glossary: row.glossary, + id: row.id + }); + }).then(() => { + return this.getEntities(); + }).then(entities => { + for (const result of results) { + result.entities = entities; + } + + return results; + }); } findKanji(kanji) { + if (this.db === null) { + return Promise.reject('database not initialized'); + } + const results = []; + return this.db.kanji.where('character').equals(kanji).each(row => { + results.push({ + character: row.character, + onyomi: row.onyomi.split(' '), + kunyomi: row.kunyomi.split(' '), + tags: row.tags.split(' '), + glossary: row.meanings + }); + }).then(() => results); + } - for (let name in this.kanjiDicts) { - const def = this.kanjiDicts[name].c[kanji]; - if (def) { - const [k, o, t, ...g] = def; - results.push({ - character: kanji, - kunyomi: k.split(' '), - onyomi: o.split(' '), - tags: t.split(' '), - glossary: g - }); + getEntities(tags) { + if (this.db === null) { + return Promise.reject('database not initialized'); + } + + if (this.entities !== null) { + return Promise.resolve(this.entities); + } + + return this.db.entities.toArray(rows => { + this.entities = {}; + for (const row of rows) { + this.entities[row.name] = row.value; + } + + return this.entities; + }); + } + + importTermDict(indexUrl, callback) { + if (this.db === null) { + return Promise.reject('database not initialized'); + } + + const indexDir = indexUrl.slice(0, indexUrl.lastIndexOf('/')); + return loadJson(indexUrl).then(index => { + const entities = []; + for (const [name, value] of index.ents) { + entities.push({name, value}); } + + return this.db.entities.bulkAdd(entities).then(() => { + if (this.entities === null) { + this.entities = {}; + } + + for (const entity of entities) { + this.entities[entity.name] = entity.value; + } + }).then(() => { + const loaders = []; + for (let i = 1; i <= index.banks; ++i) { + const bankUrl = `${indexDir}/bank_${i}.json`; + loaders.push(() => { + return loadJson(bankUrl).then(definitions => { + const rows = []; + for (const [expression, reading, tags, ...glossary] of definitions) { + rows.push({expression, reading, tags, glossary}); + } + + return this.db.terms.bulkAdd(rows).then(() => { + if (callback) { + callback(i, index.banks, indexUrl); + } + }); + }); + }); + } + + let chain = Promise.resolve(); + for (const loader of loaders) { + chain = chain.then(loader); + } + + return chain; + }); + }); + } + + importKanjiDict(indexUrl, callback) { + if (this.db === null) { + return Promise.reject('database not initialized'); } - return results; + const indexDir = indexUrl.slice(0, indexUrl.lastIndexOf('/')); + return loadJson(indexUrl).then(index => { + const loaders = []; + for (let i = 1; i <= index.banks; ++i) { + const bankUrl = `${indexDir}/bank_${i}.json`; + loaders.push(() => { + return loadJson(bankUrl).then(definitions => { + const rows = []; + for (const [character, onyomi, kunyomi, tags, ...meanings] of definitions) { + rows.push({character, onyomi, kunyomi, tags, meanings}); + } + + return this.db.kanji.bulkAdd(rows).then(() => { + if (callback) { + callback(i, index.banks, indexUrl); + } + }); + }); + }); + } + + let chain = Promise.resolve(); + for (const loader of loaders) { + chain = chain.then(loader); + } + + return chain; + }); } } diff --git a/ext/bg/js/import.js b/ext/bg/js/import.js new file mode 100644 index 00000000..0601cb9f --- /dev/null +++ b/ext/bg/js/import.js @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016 Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + + +function api_setProgress(progress) { + $('.progress-bar').css('width', `${progress}%`); + + if (progress === 100.0) { + $('.progress').hide(); + $('.alert').show(); + } +} + +chrome.runtime.onMessage.addListener(({action, params}, sender, callback) => { + const method = this['api_' + action]; + if (typeof(method) === 'function') { + method.call(this, params); + } + + callback(); +}); diff --git a/ext/bg/js/options-form.js b/ext/bg/js/options-form.js index 3dab0a87..1cd050b4 100644 --- a/ext/bg/js/options-form.js +++ b/ext/bg/js/options-form.js @@ -32,7 +32,7 @@ function fieldsToDict(selection) { function modelIdToFieldOptKey(id) { return { - 'anki-term-model': 'ankiTermFields', + 'anki-term-model': 'ankiTermFields', 'anki-kanji-model': 'ankiKanjiFields' }[id]; } @@ -60,15 +60,14 @@ function modelIdToMarkers(id) { }[id]; } -function formToOptions(section, callback) { - loadOptions((optsOld) => { +function formToOptions(section) { + return loadOptions().then(optsOld => { const optsNew = $.extend({}, optsOld); switch (section) { case 'general': optsNew.scanLength = parseInt($('#scan-length').val(), 10); optsNew.activateOnStartup = $('#activate-on-startup').prop('checked'); - optsNew.loadEnamDict = $('#load-enamdict').prop('checked'); optsNew.selectMatchedText = $('#select-matched-text').prop('checked'); optsNew.showAdvancedOptions = $('#show-advanced-options').prop('checked'); optsNew.enableAudioPlayback = $('#enable-audio-playback').prop('checked'); @@ -86,7 +85,10 @@ function formToOptions(section, callback) { break; } - callback(sanitizeOptions(optsNew), sanitizeOptions(optsOld)); + return { + optsNew: sanitizeOptions(optsNew), + optsOld: sanitizeOptions(optsOld) + }; }); } @@ -95,9 +97,9 @@ function populateAnkiDeckAndModel(opts) { const ankiDeck = $('.anki-deck'); ankiDeck.find('option').remove(); - yomi.api_getDeckNames({callback: (names) => { + yomi.api_getDeckNames({callback: names => { if (names !== null) { - names.forEach((name) => ankiDeck.append($('<option/>', {value: name, text: name}))); + names.forEach(name => ankiDeck.append($('<option/>', {value: name, text: name}))); } $('#anki-term-deck').val(opts.ankiTermDeck); @@ -106,9 +108,9 @@ function populateAnkiDeckAndModel(opts) { const ankiModel = $('.anki-model'); ankiModel.find('option').remove(); - yomi.api_getModelNames({callback: (names) => { + yomi.api_getModelNames({callback: names => { if (names !== null) { - names.forEach((name) => ankiModel.append($('<option/>', {value: name, text: name}))); + names.forEach(name => ankiModel.append($('<option/>', {value: name, text: name}))); } populateAnkiFields($('#anki-term-model').val(opts.ankiTermModel), opts); @@ -119,7 +121,7 @@ function populateAnkiDeckAndModel(opts) { function updateAnkiStatus() { $('.error-dlg').hide(); - yomichan().api_getVersion({callback: (version) => { + yomichan().api_getVersion({callback: version => { if (version === null) { $('.error-dlg-connection').show(); $('.options-anki-controls').hide(); @@ -142,19 +144,19 @@ function populateAnkiFields(element, opts) { const optKey = modelIdToFieldOptKey(modelId); const markers = modelIdToMarkers(modelId); - yomichan().api_getModelFieldNames({modelName, callback: (names) => { + yomichan().api_getModelFieldNames({modelName, callback: names => { const table = element.closest('.tab-pane').find('.anki-fields'); table.find('tbody').remove(); const tbody = $('<tbody>'); - names.forEach((name) => { + names.forEach(name => { const button = $('<button>', {type: 'button', class: 'btn btn-default dropdown-toggle'}); button.attr('data-toggle', 'dropdown').dropdown(); const markerItems = $('<ul>', {class: 'dropdown-menu dropdown-menu-right'}); for (const marker of markers) { const link = $('<a>', {href: '#'}).text(`{${marker}}`); - link.click((e) => { + link.click(e => { e.preventDefault(); link.closest('.input-group').find('.anki-field-value').val(link.text()).trigger('change'); }); @@ -185,8 +187,8 @@ function onOptionsGeneralChanged(e) { return; } - formToOptions('general', (optsNew, optsOld) => { - saveOptions(optsNew, () => { + formToOptions('general').then(({optsNew, optsOld}) => { + saveOptions(optsNew).then(() => { yomichan().setOptions(optsNew); if (!optsOld.enableAnkiConnect && optsNew.enableAnkiConnect) { updateAnkiStatus(); @@ -210,30 +212,29 @@ function onOptionsAnkiChanged(e) { return; } - formToOptions('anki', (opts) => { - saveOptions(opts, () => yomichan().setOptions(opts)); + formToOptions('anki').then(({optsNew, optsOld}) => { + saveOptions(optsNew).then(() => yomichan().setOptions(optsNew)); }); } function onAnkiModelChanged(e) { if (e.originalEvent) { - formToOptions('anki', (opts) => { - opts[modelIdToFieldOptKey($(this).id)] = {}; - populateAnkiFields($(this), opts); - saveOptions(opts, () => yomichan().setOptions(opts)); + formToOptions('anki').then(({optsNew, optsOld}) => { + optsNew[modelIdToFieldOptKey($(this).id)] = {}; + populateAnkiFields($(this), optsNew); + saveOptions(optsNew).then(() => yomichan().setOptions(optsNew)); }); } } $(document).ready(() => { - loadOptions((opts) => { - $('#scan-length').val(opts.scanLength); + loadOptions().then(opts => { $('#activate-on-startup').prop('checked', opts.activateOnStartup); - $('#load-enamdict').prop('checked', opts.loadEnamDict); $('#select-matched-text').prop('checked', opts.selectMatchedText); - $('#show-advanced-options').prop('checked', opts.showAdvancedOptions); $('#enable-audio-playback').prop('checked', opts.enableAudioPlayback); $('#enable-anki-connect').prop('checked', opts.enableAnkiConnect); + $('#show-advanced-options').prop('checked', opts.showAdvancedOptions); + $('#scan-length').val(opts.scanLength); $('#anki-card-tags').val(opts.ankiCardTags.join(' ')); $('#sentence-extent').val(opts.sentenceExtent); diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 831bb817..915164c7 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -19,21 +19,22 @@ function sanitizeOptions(options) { const defaults = { - scanLength: 20, - activateOnStartup: false, - selectMatchedText: true, - showAdvancedOptions: false, - loadEnamDict: false, + activateOnStartup: true, + selectMatchedText: true, enableAudioPlayback: true, - enableAnkiConnect: false, - ankiCardTags: ['yomichan'], - sentenceExtent: 200, - ankiTermDeck: '', - ankiTermModel: '', - ankiTermFields: {}, - ankiKanjiDeck: '', - ankiKanjiModel: '', - ankiKanjiFields: {} + enableAnkiConnect: false, + showAdvancedOptions: false, + scanLength: 20, + + ankiCardTags: ['yomichan'], + sentenceExtent: 200, + + ankiTermDeck: '', + ankiTermModel: '', + ankiTermFields: {}, + ankiKanjiDeck: '', + ankiKanjiModel: '', + ankiKanjiFields: {} }; for (const key in defaults) { @@ -45,10 +46,16 @@ function sanitizeOptions(options) { return options; } -function loadOptions(callback) { - chrome.storage.sync.get(null, (items) => callback(sanitizeOptions(items))); +function loadOptions() { + return new Promise((resolve, reject) => { + chrome.storage.sync.get(null, opts => { + resolve(sanitizeOptions(opts)); + }); + }); } -function saveOptions(opts, callback) { - chrome.storage.sync.set(sanitizeOptions(opts), callback); +function saveOptions(opts) { + return new Promise((resolve, reject) => { + chrome.storage.sync.set(sanitizeOptions(opts), resolve); + }); } diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js index fd414847..e534e0cb 100644 --- a/ext/bg/js/translator.js +++ b/ext/bg/js/translator.js @@ -25,171 +25,199 @@ class Translator { this.deinflector = new Deinflector(); } - loadData({loadEnamDict=true}, callback) { + loadData(callback) { if (this.loaded) { - callback(); - return; + return Promise.resolve(); } - Translator.loadData('bg/data/rules.json') - .then((response) => { - this.deinflector.setRules(JSON.parse(response)); - return Translator.loadData('bg/data/tags.json'); - }) - .then((response) => { - this.tagMeta = JSON.parse(response); - return Translator.loadData('bg/data/edict.json'); - }) - .then((response) => { - this.dictionary.addTermDict('edict', JSON.parse(response)); - return Translator.loadData('bg/data/kanjidic.json'); - }) - .then((response) => { - this.dictionary.addKanjiDict('kanjidic', JSON.parse(response)); - return loadEnamDict ? Translator.loadData('bg/data/enamdict.json') : Promise.resolve(null); - }) - .then((response) => { - if (response !== null) { - this.dictionary.addTermDict('enamdict', JSON.parse(response)); + return loadJson('bg/data/rules.json').then(rules => { + this.deinflector.setRules(rules); + return loadJson('bg/data/tags.json'); + }).then(tagMeta => { + this.tagMeta = tagMeta; + return this.dictionary.prepareDb(); + }).then(exists => { + if (exists) { + return; + } + + if (callback) { + callback({state: 'begin', progress: 0}); + } + + const banks = {}; + const bankCallback = (loaded, total, indexUrl) => { + banks[indexUrl] = {loaded, total}; + + let percent = 0.0; + for (const url in banks) { + percent += banks[url].loaded / banks[url].total; } - this.loaded = true; - callback(); - }); - } + percent /= 3.0; - findTerm(text) { - const groups = {}; - for (let i = text.length; i > 0; --i) { - const term = text.slice(0, i); - const dfs = this.deinflector.deinflect(term, t => { - const tags = []; - for (const d of this.dictionary.findTerm(t)) { - tags.push(d.tags); + if (callback) { + callback({state: 'update', progress: Math.ceil(100.0 * percent)}); } + }; - return tags; + return Promise.all([ + this.dictionary.importTermDict('bg/data/edict/index.json', bankCallback), + this.dictionary.importTermDict('bg/data/enamdict/index.json', bankCallback), + this.dictionary.importKanjiDict('bg/data/kanjidic/index.json', bankCallback), + ]).then(() => { + return this.dictionary.sealDb(); + }).then(() => { + if (callback) { + callback({state: 'end', progress: 100.0}); + } }); + }).then(() => { + this.loaded = true; + }); + } - if (dfs === null) { - continue; - } + findTermGroups(text) { + const deinflectGroups = {}; + const deinflectPromises = []; - for (const df of dfs) { - this.processTerm(groups, df.source, df.tags, df.rules, df.root); - } + for (let i = text.length; i > 0; --i) { + deinflectPromises.push( + this.deinflector.deinflect(text.slice(0, i), term => { + return this.dictionary.findTerm(term).then(definitions => definitions.map(definition => definition.tags)); + }).then(deinflects => { + const processPromises = []; + for (const deinflect of deinflects) { + processPromises.push(this.processTerm( + deinflectGroups, + deinflect.source, + deinflect.tags, + deinflect.rules, + deinflect.root + )); + } + + return Promise.all(processPromises); + }) + ); } - let definitions = []; - for (const key in groups) { - definitions.push(groups[key]); - } + return Promise.all(deinflectPromises).then(() => deinflectGroups); + } - definitions = 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; + findTerm(text) { + return this.findTermGroups(text).then(deinflectGroups => { + let definitions = []; + for (const key in deinflectGroups) { + definitions.push(deinflectGroups[key]); } - const s1 = v1.score; - const s2 = v2.score; - if (s1 > s2) { - return -1; - } else if (s1 < s2) { - return 1; - } + definitions = 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; + } - const rl1 = v1.rules.length; - const rl2 = v2.rules.length; - if (rl1 < rl2) { - return -1; - } else if (rl1 > rl2) { - 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); - }); + const rl1 = v1.rules.length; + const rl2 = v2.rules.length; + if (rl1 < rl2) { + return -1; + } else if (rl1 > rl2) { + return 1; + } - let length = 0; - for (const result of definitions) { - length = Math.max(length, result.source.length); - } + return v2.expression.localeCompare(v1.expression); + }); + + let length = 0; + for (const result of definitions) { + length = Math.max(length, result.source.length); + } - return {definitions: definitions, length: length}; + return {definitions, length}; + }); } findKanji(text) { - let definitions = []; const processed = {}; + const promises = []; for (const c of text) { if (!processed[c]) { - definitions = definitions.concat(this.dictionary.findKanji(c)); + promises.push(this.dictionary.findKanji(c).then((definitions) => definitions)); processed[c] = true; } } - return this.processKanji(definitions); + return Promise.all(promises).then(sets => this.processKanji(sets.reduce((a, b) => a.concat(b)))); } processTerm(groups, source, tags, rules, root) { - for (const entry of this.dictionary.findTerm(root)) { - if (entry.id in groups) { - continue; - } + return this.dictionary.findTerm(root).then(definitions => { + for (const definition of definitions) { + if (definition.id in groups) { + continue; + } - let matched = tags.length === 0; - for (const tag of tags) { - if (entry.tags.indexOf(tag) !== -1) { - matched = true; - break; + let matched = tags.length === 0; + for (const tag of tags) { + if (definition.tags.includes(tag)) { + matched = true; + break; + } } - } - if (!matched) { - continue; - } + if (!matched) { + continue; + } - const tagItems = []; - for (const tag of entry.tags) { - const tagItem = { - name: tag, - class: 'default', - order: Number.MAX_SAFE_INTEGER, - score: 0, - desc: entry.entities[tag] || '', - }; + const tagItems = []; + for (const tag of definition.tags) { + const tagItem = { + name: tag, + class: 'default', + order: Number.MAX_SAFE_INTEGER, + score: 0, + desc: definition.entities[tag] || '', + }; + + this.applyTagMeta(tagItem); + tagItems.push(tagItem); + } - this.applyTagMeta(tagItem); - tagItems.push(tagItem); - } + let score = 0; + for (const tagItem of tagItems) { + score += tagItem.score; + } - let score = 0; - for (const tagItem of tagItems) { - score += tagItem.score; + groups[definition.id] = { + score, + source, + rules, + expression: definition.expression, + reading: definition.reading, + glossary: definition.glossary, + tags: Translator.sortTags(tagItems) + }; } - - groups[entry.id] = { - score, - source, - rules, - expression: entry.expression, - reading: entry.reading, - glossary: entry.glossary, - tags: Translator.sortTags(tagItems) - }; - } + }); } - processKanji(entries) { - const results = []; - - for (const entry of entries) { + processKanji(definitions) { + for (const definition of definitions) { const tagItems = []; - for (const tag of entry.tags) { + for (const tag of definition.tags) { const tagItem = { name: tag, class: 'default', @@ -201,16 +229,10 @@ class Translator { tagItems.push(tagItem); } - results.push({ - character: entry.character, - kunyomi: entry.kunyomi, - onyomi: entry.onyomi, - glossary: entry.glossary, - tags: Translator.sortTags(tagItems) - }); + definition.tags = Translator.sortTags(tagItems); } - return results; + return definitions; } applyTagMeta(tag) { @@ -241,18 +263,4 @@ class Translator { return 0; }); } - - static isKanji(c) { - const code = c.charCodeAt(0); - return code >= 0x4e00 && code < 0x9fb0 || code >= 0x3400 && code < 0x4dc0; - } - - static loadData(url) { - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.addEventListener('load', () => resolve(xhr.responseText)); - xhr.open('GET', chrome.extension.getURL(url), true); - xhr.send(); - }); - } } diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js new file mode 100644 index 00000000..5583502d --- /dev/null +++ b/ext/bg/js/util.js @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2016 Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + + +function kanjiLinks(options) { + let result = ''; + for (const c of options.fn(this)) { + if (isKanji(c)) { + result += Handlebars.templates['kanji-link.html']({kanji: c}).trim(); + } else { + result += c; + } + } + + return result; +} + +function loadJson(url) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.addEventListener('load', () => resolve(JSON.parse(xhr.responseText))); + xhr.open('GET', chrome.extension.getURL(url), true); + xhr.send(); + }); +} + +function isKanji(c) { + const code = c.charCodeAt(0); + return code >= 0x4e00 && code < 0x9fb0 || code >= 0x3400 && code < 0x4dc0; +} + diff --git a/ext/bg/js/yomichan.js b/ext/bg/js/yomichan.js index fd9b84d3..f1b3ffc4 100644 --- a/ext/bg/js/yomichan.js +++ b/ext/bg/js/yomichan.js @@ -20,31 +20,20 @@ class Yomichan { constructor() { Handlebars.partials = Handlebars.templates; - Handlebars.registerHelper('kanjiLinks', function(options) { - let result = ''; - for (const c of options.fn(this)) { - if (Translator.isKanji(c)) { - result += Handlebars.templates['kanji-link.html']({kanji: c}).trim(); - } else { - result += c; - } - } - - return result; - }); + Handlebars.registerHelper('kanjiLinks', kanjiLinks); this.translator = new Translator(); + this.importTabId = null; this.asyncPools = {}; this.ankiConnectVer = 0; this.setState('disabled'); - chrome.runtime.onInstalled.addListener(this.onInstalled.bind(this)); chrome.runtime.onMessage.addListener(this.onMessage.bind(this)); chrome.browserAction.onClicked.addListener(this.onBrowserAction.bind(this)); - chrome.tabs.onCreated.addListener((tab) => this.onTabReady(tab.id)); + chrome.tabs.onCreated.addListener(tab => this.onTabReady(tab.id)); chrome.tabs.onUpdated.addListener(this.onTabReady.bind(this)); - loadOptions((opts) => { + loadOptions().then(opts => { this.setOptions(opts); if (this.options.activateOnStartup) { this.setState('loading'); @@ -52,9 +41,17 @@ class Yomichan { }); } - onInstalled(details) { - if (details.reason === 'install') { - chrome.tabs.create({url: chrome.extension.getURL('bg/guide.html')}); + onImport({state, progress}) { + if (state === 'begin') { + chrome.tabs.create({url: chrome.extension.getURL('bg/import.html')}, tab => this.importTabId = tab.id); + } + + if (this.importTabId !== null) { + this.tabInvoke(this.importTabId, 'setProgress', progress); + } + + if (state === 'end') { + this.importTabId = null; } } @@ -101,7 +98,7 @@ class Yomichan { break; case 'loading': chrome.browserAction.setBadgeText({text: '...'}); - this.translator.loadData({loadEnamDict: this.options.loadEnamDict}, () => this.setState('enabled')); + this.translator.loadData(this.onImport.bind(this)).then(() => this.setState('enabled')); break; } @@ -118,7 +115,7 @@ class Yomichan { } tabInvokeAll(action, params) { - chrome.tabs.query({}, (tabs) => { + chrome.tabs.query({}, tabs => { for (const tab of tabs) { this.tabInvoke(tab.id, action, params); } @@ -133,7 +130,7 @@ class Yomichan { if (this.ankiConnectVer === this.getApiVersion()) { this.ankiInvoke(action, params, pool, callback); } else { - this.api_getVersion({callback: (version) => { + this.api_getVersion({callback: version => { if (version === this.getApiVersion()) { this.ankiConnectVer = version; this.ankiInvoke(action, params, pool, callback); @@ -209,7 +206,7 @@ class Yomichan { break; case 'tags': if (definition.tags) { - value = definition.tags.map((t) => t.name); + value = definition.tags.map(t => t.name); } break; } @@ -244,7 +241,7 @@ class Yomichan { }; for (const name in fields) { - if (fields[name].indexOf('{audio}') !== -1) { + if (fields[name].includes('{audio}')) { audio.fields.push(name); } } @@ -274,7 +271,7 @@ class Yomichan { } } - this.ankiInvokeSafe('canAddNotes', {notes}, 'notes', (results) => { + this.ankiInvokeSafe('canAddNotes', {notes}, 'notes', results => { const states = []; if (results !== null) { @@ -293,11 +290,11 @@ class Yomichan { } api_findKanji({text, callback}) { - callback(this.translator.findKanji(text)); + this.translator.findKanji(text).then(result => callback(result)); } api_findTerm({text, callback}) { - callback(this.translator.findTerm(text)); + this.translator.findTerm(text).then(result => callback(result)); } api_getDeckNames({callback}) { diff --git a/ext/bg/options.html b/ext/bg/options.html index 9a2bd0e8..781257a2 100644 --- a/ext/bg/options.html +++ b/ext/bg/options.html @@ -28,11 +28,6 @@ <h3>General Options</h3> <form class="form-horizontal"> - <div class="form-group options-advanced"> - <label for="scan-length" class="control-label col-sm-2">Scan length</label> - <div class="col-sm-10"><input type="number" min="1" id="scan-length" class="form-control"></div> - </div> - <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <div class="checkbox"> @@ -44,14 +39,6 @@ <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <div class="checkbox"> - <label class="control-label"><input type="checkbox" id="load-enamdict"> Load <a href="http://www.edrdg.org/enamdict/enamdict_doc.html">ENAMDICT</a> (requires restart)</label> - </div> - </div> - </div> - - <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10"> - <div class="checkbox"> <label class="control-label"><input type="checkbox" id="select-matched-text"> Select matched text</label> </div> </div> @@ -73,7 +60,6 @@ </div> </div> - <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <div class="checkbox"> @@ -81,6 +67,11 @@ </div> </div> </div> + + <div class="form-group options-advanced"> + <label for="scan-length" class="control-label col-sm-2">Scan length</label> + <div class="col-sm-10"><input type="number" min="1" id="scan-length" class="form-control"></div> + </div> </form> </div> |