summaryrefslogtreecommitdiff
path: root/ext/bg
diff options
context:
space:
mode:
Diffstat (limited to 'ext/bg')
-rw-r--r--ext/bg/background.html2
-rw-r--r--ext/bg/guide.html29
-rw-r--r--ext/bg/import.html62
-rw-r--r--ext/bg/js/deinflector.js56
-rw-r--r--ext/bg/js/dictionary.js216
-rw-r--r--ext/bg/js/import.js36
-rw-r--r--ext/bg/js/options-form.js51
-rw-r--r--ext/bg/js/options.js43
-rw-r--r--ext/bg/js/translator.js294
-rw-r--r--ext/bg/js/util.js46
-rw-r--r--ext/bg/js/yomichan.js49
-rw-r--r--ext/bg/options.html19
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>