Merge branch 'dev'

This commit is contained in:
Alex Yatskov 2016-12-22 18:50:58 -08:00
commit 39fa11f72b
24 changed files with 3936 additions and 712 deletions

View File

@ -9,7 +9,7 @@
<script src="js/ankinull.js"></script> <script src="js/ankinull.js"></script>
<script src="js/templates.js"></script> <script src="js/templates.js"></script>
<script src="js/util.js"></script> <script src="js/util.js"></script>
<script src="js/dictionary.js"></script> <script src="js/database.js"></script>
<script src="js/deinflector.js"></script> <script src="js/deinflector.js"></script>
<script src="js/translator.js"></script> <script src="js/translator.js"></script>
<script src="js/options.js"></script> <script src="js/options.js"></script>

2799
ext/bg/data/deinflect.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,14 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Yomichan Dictionary Import</title> <title>Welcome to Yomichan!</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.min.css">
<link rel="stylesheet" type="text/css" href="../lib/bootstrap-3.3.6-dist/css/bootstrap-theme.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> </head>
<body> <body>
<div class="container"> <div class="container">
@ -19,40 +14,23 @@
<p>Thank you for downloading this extension! I sincerely hope that it will assist you on your language learning journey.</p> <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> <div>
<h2>Quick Guide</h2> <h2>Quick Guide</h2>
<p> <p>
Please read the steps outlined below to get quickly get up and running with Yomichan. For complete documentation, 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>. visit the <a href="https://foosoft.net/projects/yomichan-chrome/" target="_blank">official homepage</a>.
</p> </p>
<ol> <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>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>Right-click on the <img src="../img/icon16.png" alt> icon and select <em>Options</em> to open the Yomichan options page.</li>
<li>Import any dictionaries (bundled or custom) you wish to use for Kanji and term searches.</li>
<li>Hold down <kbd>Shift</kbd> (or the middle mouse button) as you hover over text to see term definitions.</li> <li>Hold down <kbd>Shift</kbd> (or the middle mouse button) as you hover over text to see term definitions.</li>
<li>Resize the definitions window by dragging the bottom-left corner inwards or outwards.</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> <li>Click on Kanji in the definition window to view additional information about that character.</li>
</ol> </ol>
</div> </div>
</div> </div>
<script src="../lib/jquery-2.2.2.min.js"></script>
<script src="js/import.js"></script>
</body> </body>
</html> </html>

BIN
ext/bg/img/paypal.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

297
ext/bg/js/database.js Normal file
View File

@ -0,0 +1,297 @@
/*
* 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/>.
*/
class Database {
constructor() {
this.db = null;
this.tagMetaCache = {};
}
prepare() {
if (this.db !== null) {
return Promise.reject('database already initialized');
}
this.db = new Dexie('dict');
this.db.version(1).stores({
terms: '++id,dictionary,expression,reading',
kanji: '++,dictionary,character',
tagMeta: '++,dictionary',
dictionaries: '++,title,version',
});
return this.db.open();
}
purge() {
if (this.db === null) {
return Promise.reject('database not initialized');
}
this.db.close();
return this.db.delete().then(() => {
this.db = null;
this.tagMetaCache = {};
return this.prepare();
});
}
findTerm(term, dictionaries) {
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 => {
if (dictionaries.includes(row.dictionary)) {
results.push({
expression: row.expression,
reading: row.reading,
tags: splitField(row.tags),
rules: splitField(row.rules),
glossary: row.glossary,
score: row.score,
dictionary: row.dictionary,
id: row.id
});
}
}).then(() => {
return this.cacheTagMeta(dictionaries);
}).then(() => {
for (const result of results) {
result.tagMeta = this.tagMetaCache[result.dictionary] || {};
}
return results;
});
}
findKanji(kanji, dictionaries) {
if (this.db === null) {
return Promise.reject('database not initialized');
}
const results = [];
return this.db.kanji.where('character').equals(kanji).each(row => {
if (dictionaries.includes(row.dictionary)) {
results.push({
character: row.character,
onyomi: splitField(row.onyomi),
kunyomi: splitField(row.kunyomi),
tags: splitField(row.tags),
glossary: row.meanings,
dictionary: row.dictionary
});
}
}).then(() => {
return this.cacheTagMeta(dictionaries);
}).then(() => {
for (const result of results) {
result.tagMeta = this.tagMetaCache[result.dictionary] || {};
}
return results;
});
}
cacheTagMeta(dictionaries) {
if (this.db === null) {
return Promise.reject('database not initialized');
}
const promises = [];
for (const dictionary of dictionaries) {
if (this.tagMetaCache[dictionary]) {
continue;
}
const tagMeta = {};
promises.push(
this.db.tagMeta.where('dictionary').equals(dictionary).each(row => {
tagMeta[row.name] = {category: row.category, notes: row.notes, order: row.order};
}).then(() => {
this.tagMetaCache[dictionary] = tagMeta;
})
);
}
return Promise.all(promises);
}
getDictionaries() {
if (this.db === null) {
return Promise.reject('database not initialized');
}
return this.db.dictionaries.toArray();
}
deleteDictionary(title, callback) {
if (this.db === null) {
return Promise.reject('database not initialized');
}
return this.db.dictionaries.where('title').equals(title).first(info => {
if (!info) {
return;
}
let termCounter = Promise.resolve(0);
if (info.hasTerms) {
termCounter = this.db.terms.where('dictionary').equals(title).count();
}
let kanjiCounter = Promise.resolve(0);
if (info.hasKanji) {
kanjiCounter = this.db.kanji.where('dictionary').equals(title).count();
}
return Promise.all([termCounter, kanjiCounter]).then(([termCount, kanjiCount]) => {
const rowLimit = 500;
const totalCount = termCount + kanjiCount;
let deletedCount = 0;
let termDeleter = Promise.resolve();
if (info.hasTerms) {
const termDeleterFunc = () => {
return this.db.terms.where('dictionary').equals(title).limit(rowLimit).delete().then(count => {
if (count === 0) {
return Promise.resolve();
}
deletedCount += count;
if (callback) {
callback(totalCount, deletedCount);
}
return termDeleterFunc();
});
};
termDeleter = termDeleterFunc();
}
let kanjiDeleter = Promise.resolve();
if (info.hasKanji) {
const kanjiDeleterFunc = () => {
return this.db.kanji.where('dictionary').equals(title).limit(rowLimit).delete().then(count => {
if (count === 0) {
return Promise.resolve();
}
deletedCount += count;
if (callback) {
callback(totalCount, deletedCount);
}
return kanjiDeleterFunc();
});
};
kanjiDeleter = kanjiDeleterFunc();
}
return Promise.all([termDeleter, kanjiDeleter]);
});
}).then(() => {
return this.db.tagMeta.where('dictionary').equals(title).delete();
}).then(() => {
return this.db.dictionaries.where('title').equals(title).delete();
}).then(() => {
delete this.cacheTagMeta[title];
});
}
importDictionary(indexUrl, callback) {
if (this.db === null) {
return Promise.reject('database not initialized');
}
let summary = null;
const indexLoaded = (title, version, tagMeta, hasTerms, hasKanji) => {
summary = {title, hasTerms, hasKanji, version};
return this.db.dictionaries.where('title').equals(title).count().then(count => {
if (count > 0) {
return Promise.reject(`dictionary "${title}" is already imported`);
}
return this.db.dictionaries.add({title, version, hasTerms, hasKanji}).then(() => {
const rows = [];
for (const tag in tagMeta || {}) {
const meta = tagMeta[tag];
const row = sanitizeTag({
name: tag,
category: meta.category,
notes: meta.notes,
order: meta.order,
dictionary: title
});
rows.push(row);
}
return this.db.tagMeta.bulkAdd(rows);
});
});
};
const termsLoaded = (title, entries, total, current) => {
const rows = [];
for (const [expression, reading, tags, rules, score, ...glossary] of entries) {
rows.push({
expression,
reading,
tags,
rules,
score,
glossary,
dictionary: title
});
}
return this.db.terms.bulkAdd(rows).then(() => {
if (callback) {
callback(total, current, indexUrl);
}
});
};
const kanjiLoaded = (title, entries, total, current) => {
const rows = [];
for (const [character, onyomi, kunyomi, tags, ...meanings] of entries) {
rows.push({
character,
onyomi,
kunyomi,
tags,
meanings,
dictionary: title
});
}
return this.db.kanji.bulkAdd(rows).then(() => {
if (callback) {
callback(total, current, indexUrl);
}
});
};
return importJsonDb(indexUrl, indexLoaded, termsLoaded, kanjiLoaded).then(() => summary);
}
}

View File

@ -18,50 +18,47 @@
class Deinflection { class Deinflection {
constructor(term, tags=[], rule='') { constructor(term, {rules=[], definitions=[], reason=''} = {}) {
this.children = [];
this.term = term; this.term = term;
this.tags = tags; this.rules = rules;
this.rule = rule; this.definitions = definitions;
this.reason = reason;
this.children = [];
} }
validate(validator) { deinflect(definer, reasons) {
return validator(this.term).then(sets => { const define = () => {
for (const tags of sets) { return definer(this.term).then(definitions => {
if (this.tags.length === 0) { if (this.rules.length === 0) {
return true; this.definitions = definitions;
} else {
for (const rule of this.rules) {
for (const definition of definitions) {
if (definition.rules.includes(rule)) {
this.definitions.push(definition);
} }
for (const tag of this.tags) {
if (tags.includes(tag)) {
return true;
} }
} }
} }
return false; return this.definitions.length > 0;
}); });
} };
deinflect(validator, rules) { const promises = [];
const promises = [ for (const reason in reasons) {
this.validate(validator).then(valid => { for (const variant of reasons[reason]) {
const child = new Deinflection(this.term, this.tags); let accept = this.rules.length === 0;
this.children.push(child); if (!accept) {
}) for (const rule of this.rules) {
]; if (variant.rulesIn.includes(rule)) {
accept = true;
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.tagsIn.includes(tag)) {
allowed = true;
break; break;
} }
} }
}
if (!allowed || !this.term.endsWith(variant.kanaIn)) { if (!accept || !this.term.endsWith(variant.kanaIn)) {
continue; continue;
} }
@ -70,55 +67,61 @@ class Deinflection {
continue; continue;
} }
const child = new Deinflection(term, variant.tagsOut, rule); const child = new Deinflection(term, {reason, rules: variant.rulesOut});
promises.push( promises.push(
child.deinflect(validator, rules).then(valid => { child.deinflect(definer, reasons).then(valid => valid && this.children.push(child))
if (valid) { );
this.children.push(child);
}
}
));
} }
} }
return Promise.all(promises).then(() => { return Promise.all(promises).then(define).then(valid => {
return this.children.length > 0; if (valid && this.children.length > 0) {
const child = new Deinflection(this.term, {rules: this.rules, definitions: this.definitions});
this.children.push(child);
}
return valid || this.children.length > 0;
}); });
} }
gather() { gather() {
if (this.children.length === 0) { if (this.children.length === 0) {
return [{root: this.term, tags: this.tags, rules: []}]; return [{
source: this.term,
rules: this.rules,
definitions: this.definitions,
reasons: [this.reason]
}];
} }
const paths = []; const results = [];
for (const child of this.children) { for (const child of this.children) {
for (const path of child.gather()) { for (const result of child.gather()) {
if (this.rule.length > 0) { if (this.reason.length > 0) {
path.rules.push(this.rule); result.reasons.push(this.reason);
} }
path.source = this.term; result.source = this.term;
paths.push(path); results.push(result);
} }
} }
return paths; return results;
} }
} }
class Deinflector { class Deinflector {
constructor() { constructor() {
this.rules = {}; this.reasons = {};
} }
setRules(rules) { setReasons(reasons) {
this.rules = rules; this.reasons = reasons;
} }
deinflect(term, validator) { deinflect(term, definer) {
const node = new Deinflection(term); const node = new Deinflection(term);
return node.deinflect(validator, this.rules).then(success => success ? node.gather() : []); return node.deinflect(definer, this.reasons).then(success => success ? node.gather() : []);
} }
} }

View File

@ -1,217 +0,0 @@
/*
* 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/>.
*/
class Dictionary {
constructor() {
this.db = null;
this.dbVer = 2;
this.entities = null;
}
initDb() {
if (this.db !== null) {
return Promise.reject('database already initialized');
}
this.db = new Dexie('dict');
this.db.version(1).stores({
terms: '++id,expression,reading',
entities: '++,name',
kanji: '++,character',
meta: 'name,value',
});
}
prepareDb() {
this.initDb();
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 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 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: splitField(row.tags),
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: splitField(row.onyomi),
kunyomi: splitField(row.kunyomi),
tags: splitField(row.tags),
glossary: row.meanings
});
}).then(() => results);
}
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');
}
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;
});
}
}

View File

@ -1,36 +0,0 @@
/*
* 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();
});

View File

@ -16,15 +16,291 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
//
// General
//
function yomichan() { function yomichan() {
return chrome.extension.getBackgroundPage().yomichan; return chrome.extension.getBackgroundPage().yomichan;
} }
function getFormValues() {
return loadOptions().then(optsOld => {
const optsNew = $.extend({}, optsOld);
optsNew.activateOnStartup = $('#activate-on-startup').prop('checked');
optsNew.enableAudioPlayback = $('#enable-audio-playback').prop('checked');
optsNew.showAdvancedOptions = $('#show-advanced-options').prop('checked');
optsNew.enableSoftKatakanaSearch = $('#enable-soft-katakana-search').prop('checked');
optsNew.holdShiftToScan = $('#hold-shift-to-scan').prop('checked');
optsNew.selectMatchedText = $('#select-matched-text').prop('checked');
optsNew.scanDelay = parseInt($('#scan-delay').val(), 10);
optsNew.scanLength = parseInt($('#scan-length').val(), 10);
optsNew.ankiMethod = $('#anki-method').val();
optsNew.ankiUsername = $('#anki-username').val();
optsNew.ankiPassword = $('#anki-password').val();
optsNew.ankiCardTags = $('#anki-card-tags').val().split(/[,; ]+/);
optsNew.sentenceExtent = parseInt($('#sentence-extent').val(), 10);
optsNew.ankiTermDeck = $('#anki-term-deck').val();
optsNew.ankiTermModel = $('#anki-term-model').val();
optsNew.ankiTermFields = fieldsToDict($('#term .anki-field-value'));
optsNew.ankiKanjiDeck = $('#anki-kanji-deck').val();
optsNew.ankiKanjiModel = $('#anki-kanji-model').val();
optsNew.ankiKanjiFields = fieldsToDict($('#kanji .anki-field-value'));
$('.dict-group').each((index, element) => {
const dictionary = $(element);
const title = dictionary.data('title');
const enableTerms = dictionary.find('.dict-enable-terms').prop('checked');
const enableKanji = dictionary.find('.dict-enable-kanji').prop('checked');
optsNew.dictionaries[title] = {enableTerms, enableKanji};
});
return {
optsNew: sanitizeOptions(optsNew),
optsOld: sanitizeOptions(optsOld)
};
});
}
function updateVisibility(opts) {
switch (opts.ankiMethod) {
case 'ankiweb':
$('#anki-general').show();
$('.anki-login').show();
break;
case 'ankiconnect':
$('#anki-general').show();
$('.anki-login').hide();
break;
default:
$('#anki-general').hide();
break;
}
if (opts.showAdvancedOptions) {
$('.options-advanced').show();
} else {
$('.options-advanced').hide();
}
}
$(document).ready(() => {
Handlebars.partials = Handlebars.templates;
loadOptions().then(opts => {
$('#activate-on-startup').prop('checked', opts.activateOnStartup);
$('#enable-audio-playback').prop('checked', opts.enableAudioPlayback);
$('#enable-soft-katakana-search').prop('checked', opts.enableSoftKatakanaSearch);
$('#show-advanced-options').prop('checked', opts.showAdvancedOptions);
$('#hold-shift-to-scan').prop('checked', opts.holdShiftToScan);
$('#select-matched-text').prop('checked', opts.selectMatchedText);
$('#scan-delay').val(opts.scanDelay);
$('#scan-length').val(opts.scanLength);
$('#anki-method').val(opts.ankiMethod);
$('#anki-username').val(opts.ankiUsername);
$('#anki-password').val(opts.ankiPassword);
$('#anki-card-tags').val(opts.ankiCardTags.join(' '));
$('#sentence-extent').val(opts.sentenceExtent);
$('input, select').not('.anki-model').change(onOptionsChanged);
$('.anki-model').change(onAnkiModelChanged);
$('#dict-purge').click(onDictionaryPurge);
$('#dict-importer a').click(onDictionarySetUrl);
$('#dict-import').click(onDictionaryImport);
$('#dict-url').on('input', onDictionaryUpdateUrl);
populateDictionaries(opts);
populateAnkiDeckAndModel(opts);
updateVisibility(opts);
});
});
//
// Dictionary
//
function database() {
return yomichan().translator.database;
}
function showDictionaryError(error) {
const dialog = $('#dict-error');
if (error) {
dialog.show().find('span').text(error);
} else {
dialog.hide();
}
}
function showDictionarySpinner(show) {
const spinner = $('#dict-spinner');
if (show) {
spinner.show();
} else {
spinner.hide();
}
}
function populateDictionaries(opts) {
showDictionaryError(null);
showDictionarySpinner(true);
const dictGroups = $('#dict-groups').empty();
const dictWarning = $('#dict-warning').hide();
let dictCount = 0;
return database().getDictionaries().then(rows => {
rows.forEach(row => {
const dictOpts = opts.dictionaries[row.title] || {enableTerms: false, enableKanji: false};
const html = Handlebars.templates['dictionary.html']({
title: row.title,
version: row.version,
hasTerms: row.hasTerms,
hasKanji: row.hasKanji,
enableTerms: dictOpts.enableTerms,
enableKanji: dictOpts.enableKanji
});
dictGroups.append($(html));
++dictCount;
});
$('.dict-enable-terms, .dict-enable-kanji').change(onOptionsChanged);
$('.dict-delete').click(onDictionaryDelete);
}).catch(error => {
showDictionaryError(error);
}).then(() => {
showDictionarySpinner(false);
if (dictCount === 0) {
dictWarning.show();
}
});
}
function onDictionaryPurge(e) {
e.preventDefault();
showDictionaryError(null);
showDictionarySpinner(true);
const dictControls = $('#dict-importer, #dict-groups').hide();
const dictProgress = $('#dict-purge-progress').show();
return database().purge().catch(error => {
showDictionaryError(error);
}).then(() => {
showDictionarySpinner(false);
dictControls.show();
dictProgress.hide();
return loadOptions().then(opts => populateDictionaries(opts));
});
}
function onDictionaryDelete() {
showDictionaryError(null);
showDictionarySpinner(true);
const dictGroup = $(this).closest('.dict-group');
const dictProgress = dictGroup.find('.dict-delete-progress').show();
const dictControls = dictGroup.find('.dict-group-controls').hide();
const setProgress = percent => {
dictProgress.find('.progress-bar').css('width', `${percent}%`);
};
setProgress(0.0);
database().deleteDictionary(dictGroup.data('title'), (total, current) => setProgress(current / total * 100.0)).catch(error => {
showDictionaryError(error);
}).then(() => {
showDictionarySpinner(false);
dictProgress.hide();
dictControls.show();
return loadOptions().then(opts => populateDictionaries(opts));
});
}
function onDictionaryImport() {
showDictionaryError(null);
showDictionarySpinner(true);
const dictUrl = $('#dict-url');
const dictImporter = $('#dict-importer').hide();
const dictProgress = $('#dict-import-progress').show();
const setProgress = percent => {
dictProgress.find('.progress-bar').css('width', `${percent}%`);
};
setProgress(0.0);
loadOptions().then(opts => {
database().importDictionary(dictUrl.val(), (total, current) => setProgress(current / total * 100.0)).then(summary => {
opts.dictionaries[summary.title] = {enableTerms: summary.hasTerms, enableKanji: summary.hasKanji};
return saveOptions(opts).then(() => yomichan().setOptions(opts));
}).then(() => {
return populateDictionaries(opts);
}).catch(error => {
showDictionaryError(error);
}).then(() => {
showDictionarySpinner(false);
dictProgress.hide();
dictImporter.show();
dictUrl.val('');
dictUrl.trigger('input');
});
});
}
function onDictionarySetUrl(e) {
e.preventDefault();
const dictUrl = $('#dict-url');
const url = $(this).data('url');
if (url.includes('/')) {
dictUrl.val(url);
} else {
dictUrl.val(chrome.extension.getURL(`bg/data/${url}/index.json`));
}
dictUrl.trigger('input');
}
function onDictionaryUpdateUrl() {
$('#dict-import').prop('disabled', $(this).val().length === 0);
}
//
// Anki
//
function anki() { function anki() {
return yomichan().anki; return yomichan().anki;
} }
function showAnkiSpinner(show) {
const spinner = $('#anki-spinner');
if (show) {
spinner.show();
} else {
spinner.hide();
}
}
function showAnkiError(error) {
const dialog = $('#anki-error');
if (error) {
dialog.show().find('span').text(error);
}
else {
dialog.hide();
}
}
function fieldsToDict(selection) { function fieldsToDict(selection) {
const result = {}; const result = {};
selection.each((index, element) => { selection.each((index, element) => {
@ -65,98 +341,40 @@ function modelIdToMarkers(id) {
}[id]; }[id];
} }
function getFormValues() {
return loadOptions().then(optsOld => {
const optsNew = $.extend({}, optsOld);
optsNew.activateOnStartup = $('#activate-on-startup').prop('checked');
optsNew.enableAudioPlayback = $('#enable-audio-playback').prop('checked');
optsNew.showAdvancedOptions = $('#show-advanced-options').prop('checked');
optsNew.enableSoftKatakanaSearch = $('#enable-soft-katakana-search').prop('checked');
optsNew.holdShiftToScan = $('#hold-shift-to-scan').prop('checked');
optsNew.selectMatchedText = $('#select-matched-text').prop('checked');
optsNew.scanDelay = parseInt($('#scan-delay').val(), 10);
optsNew.scanLength = parseInt($('#scan-length').val(), 10);
optsNew.ankiMethod = $('#anki-method').val();
optsNew.ankiUsername = $('#anki-username').val();
optsNew.ankiPassword = $('#anki-password').val();
optsNew.ankiCardTags = $('#anki-card-tags').val().split(/[,; ]+/);
optsNew.sentenceExtent = parseInt($('#sentence-extent').val(), 10);
optsNew.ankiTermDeck = $('#anki-term-deck').val();
optsNew.ankiTermModel = $('#anki-term-model').val();
optsNew.ankiTermFields = fieldsToDict($('#term .anki-field-value'));
optsNew.ankiKanjiDeck = $('#anki-kanji-deck').val();
optsNew.ankiKanjiModel = $('#anki-kanji-model').val();
optsNew.ankiKanjiFields = fieldsToDict($('#kanji .anki-field-value'));
return {
optsNew: sanitizeOptions(optsNew),
optsOld: sanitizeOptions(optsOld)
};
});
}
function updateVisibility(opts) {
switch (opts.ankiMethod) {
case 'ankiweb':
$('#anki-general').show();
$('.anki-login').show();
break;
case 'ankiconnect':
$('#anki-general').show();
$('.anki-login').hide();
break;
default:
$('#anki-general').hide();
break;
}
if (opts.showAdvancedOptions) {
$('.options-advanced').show();
} else {
$('.options-advanced').hide();
}
}
function populateAnkiDeckAndModel(opts) { function populateAnkiDeckAndModel(opts) {
const ankiSpinner = $('#anki-spinner'); showAnkiError(null);
ankiSpinner.show(); showAnkiSpinner(true);
const ankiFormat = $('#anki-format'); const ankiFormat = $('#anki-format').hide();
ankiFormat.hide();
return Promise.all([anki().getDeckNames(), anki().getModelNames()]).then(([deckNames, modelNames]) => {
const ankiDeck = $('.anki-deck'); const ankiDeck = $('.anki-deck');
ankiDeck.find('option').remove(); ankiDeck.find('option').remove();
deckNames.forEach(name => ankiDeck.append($('<option/>', {value: name, text: name})));
$('#anki-term-deck').val(opts.ankiTermDeck);
$('#anki-kanji-deck').val(opts.ankiKanjiDeck);
const ankiModel = $('.anki-model'); const ankiModel = $('.anki-model');
ankiModel.find('option').remove(); ankiModel.find('option').remove();
modelNames.forEach(name => ankiModel.append($('<option/>', {value: name, text: name})));
return anki().getDeckNames().then(names => { return Promise.all([
names.forEach(name => ankiDeck.append($('<option/>', {value: name, text: name}))); populateAnkiFields($('#anki-term-model').val(opts.ankiTermModel), opts),
$('#anki-term-deck').val(opts.ankiTermDeck); populateAnkiFields($('#anki-kanji-model').val(opts.ankiKanjiModel), opts)
$('#anki-kanji-deck').val(opts.ankiKanjiDeck); ]);
}).then(() => { }).then(() => {
return anki().getModelNames();
}).then(names => {
names.forEach(name => ankiModel.append($('<option/>', {value: name, text: name})));
return populateAnkiFields($('#anki-term-model').val(opts.ankiTermModel), opts);
}).then(() => {
return populateAnkiFields($('#anki-kanji-model').val(opts.ankiKanjiModel), opts);
}).then(() => {
$('#anki-error').hide();
ankiFormat.show(); ankiFormat.show();
}).catch(error => { }).catch(error => {
$('#anki-error').show().find('span').text(error); showAnkiError(error);
}).then(() => { }).then(() => {
ankiSpinner.hide(); showAnkiSpinner(false);
}); });
} }
function populateAnkiFields(element, opts) { function populateAnkiFields(element, opts) {
const table = element.closest('.tab-pane').find('.anki-fields'); const tab = element.closest('.tab-pane');
table.find('tbody').remove(); const container = tab.find('tbody').empty();
const modelName = element.val(); const modelName = element.val();
if (modelName === null) { if (modelName === null) {
@ -168,65 +386,16 @@ function populateAnkiFields(element, opts) {
const markers = modelIdToMarkers(modelId); const markers = modelIdToMarkers(modelId);
return anki().getModelFieldNames(modelName).then(names => { return anki().getModelFieldNames(modelName).then(names => {
const tbody = $('<tbody>');
names.forEach(name => { names.forEach(name => {
const button = $('<button>', {type: 'button', class: 'btn btn-default dropdown-toggle'}); const html = Handlebars.templates['model.html']({name, markers, value: opts[optKey][name] || ''});
button.attr('data-toggle', 'dropdown').dropdown(); container.append($(html));
});
const markerItems = $('<ul>', {class: 'dropdown-menu dropdown-menu-right'}); tab.find('.anki-field-value').change(onOptionsChanged);
for (const marker of markers) { tab.find('.marker-link').click(e => {
const link = $('<a>', {href: '#'}).text(`{${marker}}`);
link.click(e => {
e.preventDefault(); e.preventDefault();
link.closest('.input-group').find('.anki-field-value').val(link.text()).trigger('change'); const link = e.target;
}); $(link).closest('.input-group').find('.anki-field-value').val(`{${link.text}}`).trigger('change');
markerItems.append($('<li>').append(link));
}
const groupBtn = $('<div>', {class: 'input-group-btn'});
groupBtn.append(button.append($('<span>', {class: 'caret'})));
groupBtn.append(markerItems);
const group = $('<div>', {class: 'input-group'});
group.append($('<input>', {
type: 'text',
class: 'anki-field-value form-control',
value: opts[optKey][name] || ''
}).data('field', name).change(onOptionsChanged));
group.append(groupBtn);
const row = $('<tr>');
row.append($('<td>', {class: 'col-sm-2'}).text(name));
row.append($('<td>', {class: 'col-sm-10'}).append(group));
tbody.append(row);
});
table.append(tbody);
});
}
function onOptionsChanged(e) {
if (!e.originalEvent && !e.isTrigger) {
return;
}
getFormValues().then(({optsNew, optsOld}) => {
saveOptions(optsNew).then(() => {
yomichan().setOptions(optsNew);
updateVisibility(optsNew);
const loginChanged =
optsNew.ankiUsername !== optsOld.ankiUsername ||
optsNew.ankiPassword !== optsOld.ankiPassword;
if (loginChanged && optsNew.ankiMethod === 'ankiweb') {
anki().logout().then(() => populateAnkiDeckAndModel(optsNew)).catch(error => {
$('#anki-error').show().find('span').text(error);
});
} else if (loginChanged || optsNew.ankiMethod !== optsOld.ankiMethod) {
populateAnkiDeckAndModel(optsNew);
}
}); });
}); });
} }
@ -236,45 +405,48 @@ function onAnkiModelChanged(e) {
return; return;
} }
showAnkiError(null);
showAnkiSpinner(true);
getFormValues().then(({optsNew, optsOld}) => { getFormValues().then(({optsNew, optsOld}) => {
optsNew[modelIdToFieldOptKey($(this).id)] = {}; optsNew[modelIdToFieldOptKey($(this).id)] = {};
const ankiSpinner = $('#anki-spinner');
ankiSpinner.show();
populateAnkiFields($(this), optsNew).then(() => { populateAnkiFields($(this), optsNew).then(() => {
saveOptions(optsNew).then(() => yomichan().setOptions(optsNew)); saveOptions(optsNew).then(() => yomichan().setOptions(optsNew));
}).catch(error => { }).catch(error => {
$('#anki-error').show().find('span').text(error); showAnkiError(error);
}).then(() => { }).then(() => {
$('#anki-error').hide(); showAnkiSpinner(false);
ankiSpinner.hide();
}); });
}); });
} }
$(document).ready(() => { function onOptionsChanged(e) {
loadOptions().then(opts => { if (!e.originalEvent && !e.isTrigger) {
$('#activate-on-startup').prop('checked', opts.activateOnStartup); return;
$('#enable-audio-playback').prop('checked', opts.enableAudioPlayback); }
$('#enable-soft-katakana-search').prop('checked', opts.enableSoftKatakanaSearch);
$('#show-advanced-options').prop('checked', opts.showAdvancedOptions);
$('#hold-shift-to-scan').prop('checked', opts.holdShiftToScan); getFormValues().then(({optsNew, optsOld}) => {
$('#select-matched-text').prop('checked', opts.selectMatchedText); return saveOptions(optsNew).then(() => {
$('#scan-delay').val(opts.scanDelay); yomichan().setOptions(optsNew);
$('#scan-length').val(opts.scanLength); updateVisibility(optsNew);
$('#anki-method').val(opts.ankiMethod); const loginChanged =
$('#anki-username').val(opts.ankiUsername); optsNew.ankiUsername !== optsOld.ankiUsername ||
$('#anki-password').val(opts.ankiPassword); optsNew.ankiPassword !== optsOld.ankiPassword;
$('#anki-card-tags').val(opts.ankiCardTags.join(' '));
$('#sentence-extent').val(opts.sentenceExtent);
$('input, select').not('.anki-model').change(onOptionsChanged); if (loginChanged && optsNew.ankiMethod === 'ankiweb') {
$('.anki-model').change(onAnkiModelChanged); showAnkiError(null);
showAnkiSpinner(true);
populateAnkiDeckAndModel(opts); return anki().logout().then(() => populateAnkiDeckAndModel(optsNew));
updateVisibility(opts); } else if (loginChanged || optsNew.ankiMethod !== optsOld.ankiMethod) {
showAnkiError(null);
showAnkiSpinner(true);
return populateAnkiDeckAndModel(optsNew);
}
}); });
}); }).catch(error => {
showAnkiError(error);
}).then(() => {
showAnkiSpinner(false);
});
}

View File

@ -28,6 +28,8 @@ function sanitizeOptions(options) {
scanDelay: 15, scanDelay: 15,
scanLength: 20, scanLength: 20,
dictionaries: {},
ankiMethod: 'disabled', ankiMethod: 'disabled',
ankiUsername: '', ankiUsername: '',
ankiPassword: '', ankiPassword: '',

View File

@ -1,5 +1,36 @@
(function() { (function() {
var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
templates['dictionary.html'] = template({"1":function(container,depth0,helpers,partials,data) {
return "disabled";
},"3":function(container,depth0,helpers,partials,data) {
return "checked";
},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
var stack1, helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
return "<div class=\"dict-group well well-sm\" data-title=\""
+ alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper)))
+ "\">\n <h4><span class=\"text-muted glyphicon glyphicon-book\"></span> "
+ alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper)))
+ " <small>v."
+ alias4(((helper = (helper = helpers.version || (depth0 != null ? depth0.version : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"version","hash":{},"data":data}) : helper)))
+ "</small></h4>\n\n <!-- <div class=\"row\"> -->\n <!-- <div class=\"col-xs-8\"> -->\n <!-- <h4><span class=\"text-muted glyphicon glyphicon-book\"></span> "
+ alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper)))
+ " <small>v."
+ alias4(((helper = (helper = helpers.version || (depth0 != null ? depth0.version : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"version","hash":{},"data":data}) : helper)))
+ "</small></h4> -->\n <!-- </div> -->\n <!-- <div class=\"col-xs-4 text-right disabled\"> -->\n <!-- <button type=\"button\" class=\"dict-group-controls dict-delete btn btn-danger\">Delete</button> -->\n <!-- </div> -->\n <!-- </div> -->\n\n <div class=\"dict-delete-progress\">\n Dictionary data is being deleted, please be patient...\n <div class=\"progress\">\n <div class=\"progress-bar progress-bar-striped progress-bar-danger\" style=\"width: 0%\"></div>\n </div>\n </div>\n\n <div class=\"checkbox dict-group-controls "
+ ((stack1 = helpers.unless.call(alias1,(depth0 != null ? depth0.hasTerms : depth0),{"name":"unless","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "\">\n <label><input type=\"checkbox\" class=\"dict-enable-terms\" "
+ ((stack1 = helpers.unless.call(alias1,(depth0 != null ? depth0.hasTerms : depth0),{"name":"unless","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " "
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.enableTerms : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "> Enable term search</label>\n </div>\n <div class=\"checkbox dict-group-controls "
+ ((stack1 = helpers.unless.call(alias1,(depth0 != null ? depth0.hasKanji : depth0),{"name":"unless","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "\">\n <label><input type=\"checkbox\" class=\"dict-enable-kanji\" "
+ ((stack1 = helpers.unless.call(alias1,(depth0 != null ? depth0.hasKanji : depth0),{"name":"unless","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " "
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.enableKanji : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "> Enable Kanji search</label>\n </div>\n</div>\n";
},"useData":true});
templates['footer.html'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { templates['footer.html'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
var helper; var helper;
@ -39,16 +70,28 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia
var helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; var helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
return " <span class=\"tag tag-" return " <span class=\"tag tag-"
+ alias4(((helper = (helper = helpers["class"] || (depth0 != null ? depth0["class"] : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"class","hash":{},"data":data}) : helper))) + alias4(((helper = (helper = helpers.category || (depth0 != null ? depth0.category : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"category","hash":{},"data":data}) : helper)))
+ "\" title=\"" + "\" title=\""
+ alias4(((helper = (helper = helpers.desc || (depth0 != null ? depth0.desc : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"desc","hash":{},"data":data}) : helper))) + alias4(((helper = (helper = helpers.notes || (depth0 != null ? depth0.notes : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"notes","hash":{},"data":data}) : helper)))
+ "\">" + "\">"
+ 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))) + 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"; + "</span>\n";
},"8":function(container,depth0,helpers,partials,data) { },"8":function(container,depth0,helpers,partials,data) {
var stack1;
return " <ol>\n"
+ ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(9, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " </ol>\n";
},"9":function(container,depth0,helpers,partials,data) {
return " <li><span>" return " <li><span>"
+ container.escapeExpression(container.lambda(depth0, depth0)) + container.escapeExpression(container.lambda(depth0, depth0))
+ "</span></li>\n"; + "</span></li>\n";
},"11":function(container,depth0,helpers,partials,data) {
var stack1;
return " <p>\n "
+ container.escapeExpression(container.lambda(((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["0"] : stack1), depth0))
+ "\n </p>\n";
},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { },"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
var stack1, helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; var stack1, helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
@ -64,9 +107,9 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia
+ ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.onyomi : depth0),{"name":"each","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.onyomi : depth0),{"name":"each","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " </td>\n </tr>\n </table>\n </div>\n\n <div class=\"kanji-tags\">\n" + " </td>\n </tr>\n </table>\n </div>\n\n <div class=\"kanji-tags\">\n"
+ ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.tags : depth0),{"name":"each","hash":{},"fn":container.program(6, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.tags : depth0),{"name":"each","hash":{},"fn":container.program(6, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " </div>\n\n <div class=\"kanji-glossary\">\n <ol>\n" + " </div>\n\n <div class=\"kanji-glossary\">\n"
+ ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(8, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(8, data, 0),"inverse":container.program(11, data, 0),"data":data})) != null ? stack1 : "")
+ " </ol>\n </div>\n</div>\n"; + " </div>\n</div>\n";
},"useData":true}); },"useData":true});
templates['kanji-link.html'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { templates['kanji-link.html'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
var helper; var helper;
@ -78,14 +121,37 @@ templates['kanji-link.html'] = template({"compiler":[7,">= 4.0.0"],"main":functi
templates['kanji-list.html'] = template({"1":function(container,depth0,helpers,partials,data,blockParams,depths) { templates['kanji-list.html'] = template({"1":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1; var stack1;
return ((stack1 = container.invokePartial(partials["kanji.html"],depth0,{"name":"kanji.html","hash":{"sequence":(depths[1] != null ? depths[1].sequence : depths[1]),"options":(depths[1] != null ? depths[1].options : depths[1]),"root":(depths[1] != null ? depths[1].root : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); return ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(2, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "");
},"2":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
return ((stack1 = container.invokePartial(partials["kanji.html"],depth0,{"name":"kanji.html","hash":{"sequence":(depths[1] != null ? depths[1].sequence : depths[1]),"options":(depths[1] != null ? depths[1].options : depths[1]),"root":(depths[1] != null ? depths[1].root : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1])},"data":data,"indent":" ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "");
},"4":function(container,depth0,helpers,partials,data) {
return " <p>No results found</p>\n";
},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) { },"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1; var stack1;
return ((stack1 = container.invokePartial(partials["header.html"],depth0,{"name":"header.html","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") return ((stack1 = container.invokePartial(partials["header.html"],depth0,{"name":"header.html","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "")
+ ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0, blockParams, depths),"inverse":container.program(4, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "")
+ ((stack1 = container.invokePartial(partials["footer.html"],depth0,{"name":"footer.html","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); + ((stack1 = container.invokePartial(partials["footer.html"],depth0,{"name":"footer.html","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "");
},"usePartial":true,"useData":true,"useDepths":true}); },"usePartial":true,"useData":true,"useDepths":true});
templates['model.html'] = template({"1":function(container,depth0,helpers,partials,data) {
return " <li><a class=\"marker-link\" href=\"#\">"
+ container.escapeExpression(container.lambda(depth0, depth0))
+ "</a></li>\n";
},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
var stack1, helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
return "<tr>\n <td class=\"col-sm-2\">"
+ 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)))
+ "</td>\n <td class=\"col-sm-10\">\n <div class=\"input-group\">\n <input type=\"text\" class=\"anki-field-value form-control\" data-field=\""
+ 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)))
+ "\" value=\""
+ alias4(((helper = (helper = helpers.value || (depth0 != null ? depth0.value : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"value","hash":{},"data":data}) : helper)))
+ "\">\n <div class=\"input-group-btn\">\n <button type=\"button\" class=\"btn btn-default dropdown-toggle\" data-toggle=\"dropdown\">\n <span class=\"caret\"></span>\n </button>\n <ul class=\"dropdown-menu dropdown-menu-right\">\n"
+ ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.markers : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " </ul>\n </div>\n </div>\n </td>\n</tr>\n";
},"useData":true});
templates['term.html'] = template({"1":function(container,depth0,helpers,partials,data) { templates['term.html'] = template({"1":function(container,depth0,helpers,partials,data) {
var helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; var helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
@ -129,7 +195,7 @@ templates['term.html'] = template({"1":function(container,depth0,helpers,partial
},"10":function(container,depth0,helpers,partials,data) { },"10":function(container,depth0,helpers,partials,data) {
var stack1; var stack1;
return " <span class=\"rule\">" return " <span class=\"reasons\">"
+ container.escapeExpression(container.lambda(depth0, depth0)) + container.escapeExpression(container.lambda(depth0, depth0))
+ "</span> " + "</span> "
+ ((stack1 = helpers.unless.call(depth0 != null ? depth0 : {},(data && data.last),{"name":"unless","hash":{},"fn":container.program(11, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : {},(data && data.last),{"name":"unless","hash":{},"fn":container.program(11, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
@ -140,16 +206,28 @@ templates['term.html'] = template({"1":function(container,depth0,helpers,partial
var helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; var helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
return " <span class=\"tag tag-" return " <span class=\"tag tag-"
+ alias4(((helper = (helper = helpers["class"] || (depth0 != null ? depth0["class"] : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"class","hash":{},"data":data}) : helper))) + alias4(((helper = (helper = helpers.category || (depth0 != null ? depth0.category : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"category","hash":{},"data":data}) : helper)))
+ "\" title=\"" + "\" title=\""
+ alias4(((helper = (helper = helpers.desc || (depth0 != null ? depth0.desc : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"desc","hash":{},"data":data}) : helper))) + alias4(((helper = (helper = helpers.notes || (depth0 != null ? depth0.notes : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"notes","hash":{},"data":data}) : helper)))
+ "\">" + "\">"
+ 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))) + 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"; + "</span>\n";
},"15":function(container,depth0,helpers,partials,data) { },"15":function(container,depth0,helpers,partials,data) {
var stack1;
return " <ol>\n"
+ ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(16, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " </ol>\n";
},"16":function(container,depth0,helpers,partials,data) {
return " <li><span>" return " <li><span>"
+ container.escapeExpression(container.lambda(depth0, depth0)) + container.escapeExpression(container.lambda(depth0, depth0))
+ "</span></li>\n"; + "</span></li>\n";
},"18":function(container,depth0,helpers,partials,data) {
var stack1;
return " <p>"
+ container.escapeExpression(container.lambda(((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["0"] : stack1), depth0))
+ "</p>\n";
},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { },"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
var stack1, helper, alias1=depth0 != null ? depth0 : {}; var stack1, helper, alias1=depth0 != null ? depth0 : {};
@ -160,23 +238,29 @@ templates['term.html'] = template({"1":function(container,depth0,helpers,partial
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(3, 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(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " </div>\n\n" + " </div>\n\n"
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.reading : depth0),{"name":"if","hash":{},"fn":container.program(5, data, 0),"inverse":container.program(8, data, 0),"data":data})) != null ? stack1 : "") + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.reading : depth0),{"name":"if","hash":{},"fn":container.program(5, data, 0),"inverse":container.program(8, data, 0),"data":data})) != null ? stack1 : "")
+ "\n <div class=\"term-rules\">\n" + "\n <div class=\"term-reasons\">\n"
+ ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.rules : depth0),{"name":"each","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.reasons : depth0),{"name":"each","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " </div>\n\n <div class=\"term-tags\">\n" + " </div>\n\n <div class=\"term-tags\">\n"
+ ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.tags : depth0),{"name":"each","hash":{},"fn":container.program(13, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.tags : depth0),{"name":"each","hash":{},"fn":container.program(13, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " </div>\n\n <div class=\"term-glossary\">\n <ol>\n" + " </div>\n\n <div class=\"term-glossary\">\n"
+ ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(15, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(15, data, 0),"inverse":container.program(18, data, 0),"data":data})) != null ? stack1 : "")
+ " </ol>\n </div>\n</div>\n"; + " </div>\n</div>\n";
},"useData":true}); },"useData":true});
templates['term-list.html'] = template({"1":function(container,depth0,helpers,partials,data,blockParams,depths) { templates['term-list.html'] = template({"1":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1; var stack1;
return ((stack1 = container.invokePartial(partials["term.html"],depth0,{"name":"term.html","hash":{"sequence":(depths[1] != null ? depths[1].sequence : depths[1]),"options":(depths[1] != null ? depths[1].options : depths[1]),"root":(depths[1] != null ? depths[1].root : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); return ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(2, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "");
},"2":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
return ((stack1 = container.invokePartial(partials["term.html"],depth0,{"name":"term.html","hash":{"sequence":(depths[1] != null ? depths[1].sequence : depths[1]),"options":(depths[1] != null ? depths[1].options : depths[1]),"root":(depths[1] != null ? depths[1].root : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1])},"data":data,"indent":" ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "");
},"4":function(container,depth0,helpers,partials,data) {
return " <p>No results found</p>\n";
},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) { },"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1; var stack1;
return ((stack1 = container.invokePartial(partials["header.html"],depth0,{"name":"header.html","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") return ((stack1 = container.invokePartial(partials["header.html"],depth0,{"name":"header.html","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "")
+ ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0, blockParams, depths),"inverse":container.program(4, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "")
+ ((stack1 = container.invokePartial(partials["footer.html"],depth0,{"name":"footer.html","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); + ((stack1 = container.invokePartial(partials["footer.html"],depth0,{"name":"footer.html","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "");
},"usePartial":true,"useData":true,"useDepths":true}); },"usePartial":true,"useData":true,"useDepths":true});
})(); })();

View File

@ -20,96 +20,33 @@
class Translator { class Translator {
constructor() { constructor() {
this.loaded = false; this.loaded = false;
this.tagMeta = null; this.ruleMeta = null;
this.dictionary = new Dictionary(); this.database = new Database();
this.deinflector = new Deinflector(); this.deinflector = new Deinflector();
} }
loadData(callback) { prepare() {
if (this.loaded) { if (this.loaded) {
return Promise.resolve(); return Promise.resolve();
} }
return loadJson('bg/data/rules.json').then(rules => { const promises = [
this.deinflector.setRules(rules); loadJsonInt('bg/data/deinflect.json'),
return loadJson('bg/data/tags.json'); this.database.prepare()
}).then(tagMeta => { ];
this.tagMeta = tagMeta;
return this.dictionary.prepareDb();
}).then(exists => {
if (exists) {
return;
}
if (callback) { return Promise.all(promises).then(([reasons]) => {
callback({state: 'begin', progress: 0}); this.deinflector.setReasons(reasons);
}
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;
}
percent /= 3.0;
if (callback) {
callback({state: 'update', progress: Math.ceil(100.0 * percent)});
}
};
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; this.loaded = true;
}); });
} }
findTermGroups(text) { findTerm(text, dictionaries, enableSoftKatakanaSearch) {
const deinflectGroups = {}; const cache = {};
const deinflectPromises = []; return this.findDeinflectionGroups(text, dictionaries, cache).then(groups => {
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);
})
);
}
return Promise.all(deinflectPromises).then(() => deinflectGroups);
}
findTerm(text, enableSoftKatakanaSearch) {
return this.findTermGroups(text).then(groups => {
const textHiragana = wanakana._katakanaToHiragana(text); const textHiragana = wanakana._katakanaToHiragana(text);
if (text !== textHiragana && enableSoftKatakanaSearch) { if (text !== textHiragana && enableSoftKatakanaSearch) {
return this.findTermGroups(textHiragana).then(groupsHiragana => { return this.findDeinflectionGroups(textHiragana, dictionaries, cache).then(groupsHiragana => {
for (const key in groupsHiragana) { for (const key in groupsHiragana) {
groups[key] = groups[key] || groupsHiragana[key]; groups[key] = groups[key] || groupsHiragana[key];
} }
@ -137,13 +74,11 @@ class Translator {
}); });
} }
findKanji(text) { findKanji(text, dictionaries) {
const processed = {}; const processed = {}, promises = [];
const promises = [];
for (const c of text) { for (const c of text) {
if (!processed[c]) { if (!processed[c]) {
promises.push(this.dictionary.findKanji(c).then((definitions) => definitions)); promises.push(this.database.findKanji(c, dictionaries));
processed[c] = true; processed[c] = true;
} }
} }
@ -151,73 +86,53 @@ class Translator {
return Promise.all(promises).then(sets => this.processKanji(sets.reduce((a, b) => a.concat(b), []))); return Promise.all(promises).then(sets => this.processKanji(sets.reduce((a, b) => a.concat(b), [])));
} }
processTerm(groups, source, tags, rules, root) { findDeinflectionGroups(text, dictionaries, cache) {
return this.dictionary.findTerm(root).then(definitions => { const definer = term => {
if (cache.hasOwnProperty(term)) {
return Promise.resolve(cache[term]);
}
return this.database.findTerm(term, dictionaries).then(definitions => cache[term] = definitions);
};
const groups = {}, promises = [];
for (let i = text.length; i > 0; --i) {
promises.push(
this.deinflector.deinflect(text.slice(0, i), definer).then(deinflections => {
for (const deinflection of deinflections) {
this.processDeinflection(groups, deinflection);
}
})
);
}
return Promise.all(promises).then(() => groups);
}
processDeinflection(groups, {source, rules, reasons, definitions}, dictionaries) {
for (const definition of definitions) { for (const definition of definitions) {
if (definition.id in groups) { if (definition.id in groups) {
continue; continue;
} }
let matched = tags.length === 0; const tags = definition.tags.map(tag => buildTag(tag, definition.tagMeta));
for (const tag of tags) {
if (definition.tags.includes(tag)) {
matched = true;
break;
}
}
if (!matched) {
continue;
}
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] || '',
};
applyTagMeta(tagItem, this.tagMeta);
tagItems.push(tagItem);
}
let score = 0;
for (const tagItem of tagItems) {
score += tagItem.score;
}
groups[definition.id] = { groups[definition.id] = {
score,
source, source,
rules, reasons,
score: definition.score,
dictionary: definition.dictionary,
expression: definition.expression, expression: definition.expression,
reading: definition.reading, reading: definition.reading,
glossary: definition.glossary, glossary: definition.glossary,
tags: sortTags(tagItems) tags: sortTags(tags)
}; };
} }
});
} }
processKanji(definitions) { processKanji(definitions) {
for (const definition of definitions) { for (const definition of definitions) {
const tagItems = []; const tags = definition.tags.map(tag => buildTag(tag, definition.tagMeta));
for (const tag of definition.tags) { definition.tags = sortTags(tags);
const tagItem = {
name: tag,
class: 'default',
order: Number.MAX_SAFE_INTEGER,
desc: '',
};
applyTagMeta(tagItem, this.tagMeta);
tagItems.push(tagItem);
}
definition.tags = sortTags(tagItems);
} }
return definitions; return definitions;

View File

@ -39,19 +39,11 @@ function promiseCallback(promise, callback) {
return promise.then(result => { return promise.then(result => {
callback({result}); callback({result});
}).catch(error => { }).catch(error => {
console.log(error);
callback({error}); callback({error});
}); });
} }
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));
xhr.send();
});
}
function sortTags(tags) { function sortTags(tags) {
return tags.sort((v1, v2) => { return tags.sort((v1, v2) => {
const order1 = v1.order; const order1 = v1.order;
@ -92,8 +84,8 @@ function sortTermDefs(definitions) {
return 1; return 1;
} }
const rl1 = v1.rules.length; const rl1 = v1.reasons.length;
const rl2 = v2.rules.length; const rl2 = v2.reasons.length;
if (rl1 < rl2) { if (rl1 < rl2) {
return -1; return -1;
} else if (rl1 > rl2) { } else if (rl1 > rl2) {
@ -104,15 +96,97 @@ function sortTermDefs(definitions) {
}); });
} }
function applyTagMeta(tag, meta) { function buildTag(name, meta) {
const symbol = tag.name.split(':')[0]; const tag = {name};
const symbol = name.split(':')[0];
for (const prop in meta[symbol] || {}) { for (const prop in meta[symbol] || {}) {
tag[prop] = meta[symbol][prop]; tag[prop] = meta[symbol][prop];
} }
return sanitizeTag(tag);
}
function sanitizeTag(tag) {
tag.name = tag.name || 'untitled';
tag.category = tag.category || 'default';
tag.notes = tag.notes || '';
tag.order = tag.order || 0;
return tag; return tag;
} }
function splitField(field) { function splitField(field) {
return field.length === 0 ? [] : field.split(' '); return field.length === 0 ? [] : field.split(' ');
} }
function loadJson(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.addEventListener('load', () => resolve(xhr.responseText));
xhr.addEventListener('error', () => reject('failed to execute network request'));
xhr.open('GET', url);
xhr.send();
}).then(responseText => {
try {
return JSON.parse(responseText);
}
catch (e) {
return Promise.reject('invalid JSON response');
}
});
}
function loadJsonInt(url) {
return loadJson(chrome.extension.getURL(url));
}
function importJsonDb(indexUrl, indexLoaded, termsLoaded, kanjiLoaded) {
const indexDir = indexUrl.slice(0, indexUrl.lastIndexOf('/'));
return loadJson(indexUrl).then(index => {
if (!index.title || !index.version) {
return Promise.reject('unrecognized dictionary format');
}
if (indexLoaded !== null) {
return indexLoaded(
index.title,
index.version,
index.tagMeta,
index.termBanks > 0,
index.kanjiBanks > 0
).then(() => index);
}
return index;
}).then(index => {
const loaders = [];
const banksTotal = index.termBanks + index.kanjiBanks;
let banksLoaded = 0;
for (let i = 1; i <= index.termBanks; ++i) {
const bankUrl = `${indexDir}/term_bank_${i}.json`;
loaders.push(() => loadJson(bankUrl).then(entries => termsLoaded(
index.title,
entries,
banksTotal,
banksLoaded++
)));
}
for (let i = 1; i <= index.kanjiBanks; ++i) {
const bankUrl = `${indexDir}/kanji_bank_${i}.json`;
loaders.push(() => loadJson(bankUrl).then(entries => kanjiLoaded(
index.title,
entries,
banksTotal,
banksLoaded++
)));
}
let chain = Promise.resolve();
for (const loader of loaders) {
chain = chain.then(loader);
}
return chain;
});
}

View File

@ -25,11 +25,11 @@ class Yomichan {
this.translator = new Translator(); this.translator = new Translator();
this.anki = new AnkiNull(); this.anki = new AnkiNull();
this.options = null; this.options = null;
this.importTabId = null;
this.setState('disabled'); this.setState('disabled');
chrome.runtime.onMessage.addListener(this.onMessage.bind(this)); chrome.runtime.onMessage.addListener(this.onMessage.bind(this));
chrome.browserAction.onClicked.addListener(this.onBrowserAction.bind(this)); chrome.browserAction.onClicked.addListener(this.onBrowserAction.bind(this));
chrome.runtime.onInstalled.addListener(this.onInstalled.bind(this));
loadOptions().then(opts => { loadOptions().then(opts => {
this.setOptions(opts); this.setOptions(opts);
@ -39,17 +39,9 @@ class Yomichan {
}); });
} }
onImport({state, progress}) { onInstalled(details) {
if (state === 'begin') { if (details.reason === 'install') {
chrome.tabs.create({url: chrome.extension.getURL('bg/import.html')}, tab => this.importTabId = tab.id); chrome.tabs.create({url: chrome.extension.getURL('bg/guide.html')});
}
if (this.importTabId !== null) {
this.tabInvoke(this.importTabId, 'setProgress', progress);
}
if (state === 'end') {
this.importTabId = null;
} }
} }
@ -91,7 +83,7 @@ class Yomichan {
break; break;
case 'loading': case 'loading':
chrome.browserAction.setBadgeText({text: '...'}); chrome.browserAction.setBadgeText({text: '...'});
this.translator.loadData(this.onImport.bind(this)).then(() => this.setState('enabled')); this.translator.prepare().then(this.setState('enabled'));
break; break;
} }
@ -239,11 +231,31 @@ class Yomichan {
} }
api_findKanji({text, callback}) { api_findKanji({text, callback}) {
promiseCallback(this.translator.findKanji(text), callback); const dictionaries = [];
for (const title in this.options.dictionaries) {
if (this.options.dictionaries[title].enableKanji) {
dictionaries.push(title);
}
}
promiseCallback(
this.translator.findKanji(text, dictionaries),
callback
);
} }
api_findTerm({text, callback}) { api_findTerm({text, callback}) {
promiseCallback(this.translator.findTerm(text, this.options.enableSoftKatakanaSearch), callback); const dictionaries = [];
for (const title in this.options.dictionaries) {
if (this.options.dictionaries[title].enableTerms) {
dictionaries.push(title);
}
}
promiseCallback(
this.translator.findTerm(text, dictionaries, this.options.enableSoftKatakanaSearch),
callback
);
} }
api_renderText({template, data, callback}) { api_renderText({template, data, callback}) {

View File

@ -6,7 +6,9 @@
<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.min.css">
<link rel="stylesheet" type="text/css" href="../lib/bootstrap-3.3.6-dist/css/bootstrap-theme.min.css"> <link rel="stylesheet" type="text/css" href="../lib/bootstrap-3.3.6-dist/css/bootstrap-theme.min.css">
<style> <style>
#anki-spinner, #anki-general, #anki-error, #options-advanced { #anki-spinner, #anki-general, #anki-error,
#dict-spinner, #dict-error, #dict-warning, #dict-purge-progress, #dict-import-progress, .dict-delete-progress,
#options-advanced {
display: none; display: none;
} }
@ -62,12 +64,71 @@
</div> </div>
</div> </div>
<div>
<div>
<img src="img/spinner.gif" class="pull-right" id="dict-spinner" alt>
<h3>Dictionaries</h3>
</div>
<p class="help-block">
Yomichan can utilize both bundled and custom (see the <a href="https://foosoft.net/projects/yomichan-import">Yomichan Import</a>
page for details) dictionaries. You can disable dictionaries that you no longer wish to use, or you can simply
<a href="#" id="dict-purge">purge the database</a> to delete everything. Please make sure to wait for import
and delete operations to complete before closing this page.
</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, use the importer below to install packaged and external dictionaries</span>
</div>
<div class="alert alert-danger" id="dict-error">
<strong>Error:</strong>
<span></span>
</div>
<div id="dict-groups"></div>
<div id="dict-import-progress">
Dictionary data is being imported, please be patient...
<div class="progress">
<div class="progress-bar progress-bar-striped" style="width: 0%"></div>
</div>
</div>
<div class="input-group" id="dict-importer">
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button>
<ul class="dropdown-menu">
<li><a href="#" data-url="edict">JMdict</a></li>
<li><a href="#" data-url="enamdict">JMnedict</a></li>
<li><a href="#" data-url="kanjidic">KANJIDIC2</a></li>
<li role="separator" class="divider"></li>
<li><a href="#" data-url="http://localhost:9876/index.json">Local dictionary</a></li>
</ul>
</div>
<input type="text" id="dict-url" class="form-control" placeholder="Dictionary import URL">
<div class="input-group-btn">
<button type="button" id="dict-import" class="btn btn-primary" disabled>Import</button>
</div>
</div>
</div>
<div> <div>
<div> <div>
<img src="img/spinner.gif" class="pull-right" id="anki-spinner" alt> <img src="img/spinner.gif" class="pull-right" id="anki-spinner" alt>
<h3>Anki Options</h3> <h3>Anki Options</h3>
</div> </div>
<p class="help-block">
Yomichan features automatic flashcard creation for <a href="http://ankisrs.net/">Anki</a>, a free application
designed to help you retain knowledge. While the <a href="https://foosoft.net/projects/anki-connect/">AnkiConnect</a> plugin
offers the best experience, it is also possible to create flashcards through <a href="https://ankiweb.net/">AnkiWeb</a>,
provided you already have an account.
</p>
<div class="alert alert-danger" id="anki-error"> <div class="alert alert-danger" id="anki-error">
<strong>Error:</strong> <strong>Error:</strong>
<span></span> <span></span>
@ -80,12 +141,6 @@
<option value="ankiconnect">AnkiConnect (requires the AnkiConnect plugin)</option> <option value="ankiconnect">AnkiConnect (requires the AnkiConnect plugin)</option>
<option value="ankiweb">AnkiWeb (requires an account on AnkiWeb)</option> <option value="ankiweb">AnkiWeb (requires an account on AnkiWeb)</option>
</select> </select>
<p class="help-block">
Yomichan features automatic flashcard creation for <a href="http://ankisrs.net/">Anki</a>, a free application
designed to help you retain knowledge. While the <a href="https://foosoft.net/projects/anki-connect/">AnkiConnect</a> plugin
offers the best experience, it is also possible to create flashcards through <a href="https://ankiweb.net/">AnkiWeb</a>,
provided you already have an account.
</p>
</div> </div>
<div id="anki-general"> <div id="anki-general">
@ -158,6 +213,19 @@
</div> </div>
</div> </div>
</div> </div>
<div>
<h3>Support Development</h3>
<p class="help-block">
If you find Yomichan useful, please consider making a small donation as a way to show your appreciation for the countless hours
that I have devoted to this extension. Seeing that people care about my work is great motivation for continuing to
improve Yomichan!
</p>
<p>
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4DBTN9A3CUAFN"><img src="img/paypal.gif" alt></a>
</p>
</div>
</div> </div>
<div class="pull-right"> <div class="pull-right">
@ -167,6 +235,8 @@
<script src="../lib/jquery-2.2.2.min.js"></script> <script src="../lib/jquery-2.2.2.min.js"></script>
<script src="../lib/bootstrap-3.3.6-dist/js/bootstrap.min.js"></script> <script src="../lib/bootstrap-3.3.6-dist/js/bootstrap.min.js"></script>
<script src="../lib/handlebars.min.js"></script>
<script src="js/templates.js"></script>
<script src="js/options.js"></script> <script src="js/options.js"></script>
<script src="js/options-form.js"></script> <script src="js/options-form.js"></script>
</body> </body>

View File

@ -100,7 +100,7 @@ body {
text-decoration: none; text-decoration: none;
} }
.term-rules { .term-reasons {
color: #777; color: #777;
display: inline-block; display: inline-block;
} }
@ -117,6 +117,11 @@ body {
color: #000; color: #000;
} }
.term-glossary p {
overflow-x: auto;
white-space: pre;
}
/* kanji styles */ /* kanji styles */
.kanji-glyph { .kanji-glyph {
@ -153,3 +158,8 @@ body {
.kanji-glossary li span { .kanji-glossary li span {
color: #000; color: #000;
} }
.kanji-glossary p {
overflow-x: auto;
white-space: pre;
}

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "Yomichan", "name": "Yomichan",
"version": "0.997", "version": "0.998",
"description": "Japanese dictionary with Anki integration", "description": "Japanese dictionary with Anki integration",
"icons": {"16": "img/icon16.png", "48": "img/icon48.png", "128": "img/icon128.png"}, "icons": {"16": "img/icon16.png", "48": "img/icon48.png", "128": "img/icon128.png"},

26
tmpl/dictionary.html Normal file
View File

@ -0,0 +1,26 @@
<div class="dict-group well well-sm" data-title="{{title}}">
<h4><span class="text-muted glyphicon glyphicon-book"></span> {{title}} <small>v.{{version}}</small></h4>
<!-- <div class="row"> -->
<!-- <div class="col-xs-8"> -->
<!-- <h4><span class="text-muted glyphicon glyphicon-book"></span> {{title}} <small>v.{{version}}</small></h4> -->
<!-- </div> -->
<!-- <div class="col-xs-4 text-right disabled"> -->
<!-- <button type="button" class="dict-group-controls dict-delete btn btn-danger">Delete</button> -->
<!-- </div> -->
<!-- </div> -->
<div class="dict-delete-progress">
Dictionary data is being deleted, please be patient...
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-danger" style="width: 0%"></div>
</div>
</div>
<div class="checkbox dict-group-controls {{#unless hasTerms}}disabled{{/unless}}">
<label><input type="checkbox" class="dict-enable-terms" {{#unless hasTerms}}disabled{{/unless}} {{#if enableTerms}}checked{{/if}}> Enable term search</label>
</div>
<div class="checkbox dict-group-controls {{#unless hasKanji}}disabled{{/unless}}">
<label><input type="checkbox" class="dict-enable-kanji" {{#unless hasKanji}}disabled{{/unless}} {{#if enableKanji}}checked{{/if}}> Enable Kanji search</label>
</div>
</div>

View File

@ -1,5 +1,9 @@
{{> header.html}} {{> header.html}}
{{#each definitions}} {{#if definitions}}
{{> kanji.html addable=../addable root=../root options=../options sequence=../sequence}} {{#each definitions}}
{{/each}} {{> kanji.html addable=../addable root=../root options=../options sequence=../sequence}}
{{/each}}
{{else}}
<p>No results found</p>
{{/if}}
{{> footer.html}} {{> footer.html}}

View File

@ -30,15 +30,21 @@
<div class="kanji-tags"> <div class="kanji-tags">
{{#each tags}} {{#each tags}}
<span class="tag tag-{{class}}" title="{{desc}}">{{name}}</span> <span class="tag tag-{{category}}" title="{{notes}}">{{name}}</span>
{{/each}} {{/each}}
</div> </div>
<div class="kanji-glossary"> <div class="kanji-glossary">
{{#if glossary.[1]}}
<ol> <ol>
{{#each glossary}} {{#each glossary}}
<li><span>{{.}}</span></li> <li><span>{{.}}</span></li>
{{/each}} {{/each}}
</ol> </ol>
{{else}}
<p>
{{glossary.[0]}}
</p>
{{/if}}
</div> </div>
</div> </div>

18
tmpl/model.html Normal file
View File

@ -0,0 +1,18 @@
<tr>
<td class="col-sm-2">{{name}}</td>
<td class="col-sm-10">
<div class="input-group">
<input type="text" class="anki-field-value form-control" data-field="{{name}}" value="{{value}}">
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
{{#each markers}}
<li><a class="marker-link" href="#">{{.}}</a></li>
{{/each}}
</ul>
</div>
</div>
</td>
</tr>

View File

@ -1,5 +1,9 @@
{{> header.html}} {{> header.html}}
{{#each definitions}} {{#if definitions}}
{{> term.html addable=../addable root=../root options=../options sequence=../sequence}} {{#each definitions}}
{{/each}} {{> term.html addable=../addable root=../root options=../options sequence=../sequence}}
{{/each}}
{{else}}
<p>No results found</p>
{{/if}}
{{> footer.html}} {{> footer.html}}

View File

@ -15,23 +15,27 @@
<div class="term-expression">{{#kanjiLinks}}{{expression}}{{/kanjiLinks}}</div> <div class="term-expression">{{#kanjiLinks}}{{expression}}{{/kanjiLinks}}</div>
{{/if}} {{/if}}
<div class="term-rules"> <div class="term-reasons">
{{#each rules}} {{#each reasons}}
<span class="rule">{{.}}</span> {{#unless @last}}&laquo;{{/unless}} <span class="reasons">{{.}}</span> {{#unless @last}}&laquo;{{/unless}}
{{/each}} {{/each}}
</div> </div>
<div class="term-tags"> <div class="term-tags">
{{#each tags}} {{#each tags}}
<span class="tag tag-{{class}}" title="{{desc}}">{{name}}</span> <span class="tag tag-{{category}}" title="{{notes}}">{{name}}</span>
{{/each}} {{/each}}
</div> </div>
<div class="term-glossary"> <div class="term-glossary">
{{#if glossary.[1]}}
<ol> <ol>
{{#each glossary}} {{#each glossary}}
<li><span>{{.}}</span></li> <li><span>{{.}}</span></li>
{{/each}} {{/each}}
</ol> </ol>
{{else}}
<p>{{glossary.[0]}}</p>
{{/if}}
</div> </div>
</div> </div>