summaryrefslogtreecommitdiff
path: root/ext/bg/js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/bg/js')
-rw-r--r--ext/bg/js/database.js297
-rw-r--r--ext/bg/js/deinflector.js117
-rw-r--r--ext/bg/js/dictionary.js217
-rw-r--r--ext/bg/js/import.js36
-rw-r--r--ext/bg/js/options-form.js470
-rw-r--r--ext/bg/js/options.js2
-rw-r--r--ext/bg/js/templates.js118
-rw-r--r--ext/bg/js/translator.js199
-rw-r--r--ext/bg/js/util.js100
-rw-r--r--ext/bg/js/yomichan.js42
10 files changed, 952 insertions, 646 deletions
diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js
new file mode 100644
index 00000000..7ad7d410
--- /dev/null
+++ b/ext/bg/js/database.js
@@ -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);
+ }
+}
diff --git a/ext/bg/js/deinflector.js b/ext/bg/js/deinflector.js
index 1474e56d..6e480068 100644
--- a/ext/bg/js/deinflector.js
+++ b/ext/bg/js/deinflector.js
@@ -18,50 +18,47 @@
class Deinflection {
- constructor(term, tags=[], rule='') {
- this.children = [];
+ constructor(term, {rules=[], definitions=[], reason=''} = {}) {
this.term = term;
- this.tags = tags;
- this.rule = rule;
+ this.rules = rules;
+ this.definitions = definitions;
+ this.reason = reason;
+ this.children = [];
}
- validate(validator) {
- 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;
+ deinflect(definer, reasons) {
+ const define = () => {
+ return definer(this.term).then(definitions => {
+ if (this.rules.length === 0) {
+ this.definitions = definitions;
+ } else {
+ for (const rule of this.rules) {
+ for (const definition of definitions) {
+ if (definition.rules.includes(rule)) {
+ this.definitions.push(definition);
+ }
+ }
}
}
- }
-
- return false;
- });
- }
- deinflect(validator, rules) {
- 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.tagsIn.includes(tag)) {
- allowed = true;
- break;
+ return this.definitions.length > 0;
+ });
+ };
+
+ const promises = [];
+ for (const reason in reasons) {
+ for (const variant of reasons[reason]) {
+ let accept = this.rules.length === 0;
+ if (!accept) {
+ for (const rule of this.rules) {
+ if (variant.rulesIn.includes(rule)) {
+ accept = true;
+ break;
+ }
}
}
- if (!allowed || !this.term.endsWith(variant.kanaIn)) {
+ if (!accept || !this.term.endsWith(variant.kanaIn)) {
continue;
}
@@ -70,55 +67,61 @@ class Deinflection {
continue;
}
- const child = new Deinflection(term, variant.tagsOut, rule);
+ const child = new Deinflection(term, {reason, rules: variant.rulesOut});
promises.push(
- child.deinflect(validator, rules).then(valid => {
- if (valid) {
- this.children.push(child);
- }
- }
- ));
+ child.deinflect(definer, reasons).then(valid => valid && this.children.push(child))
+ );
}
}
- return Promise.all(promises).then(() => {
- return this.children.length > 0;
+ return Promise.all(promises).then(define).then(valid => {
+ 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() {
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 path of child.gather()) {
- if (this.rule.length > 0) {
- path.rules.push(this.rule);
+ for (const result of child.gather()) {
+ if (this.reason.length > 0) {
+ result.reasons.push(this.reason);
}
- path.source = this.term;
- paths.push(path);
+ result.source = this.term;
+ results.push(result);
}
}
- return paths;
+ return results;
}
}
class Deinflector {
constructor() {
- this.rules = {};
+ this.reasons = {};
}
- setRules(rules) {
- this.rules = rules;
+ setReasons(reasons) {
+ this.reasons = reasons;
}
- deinflect(term, validator) {
+ deinflect(term, definer) {
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() : []);
}
}
diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js
deleted file mode 100644
index 1d54190e..00000000
--- a/ext/bg/js/dictionary.js
+++ /dev/null
@@ -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;
- });
- }
-}
diff --git a/ext/bg/js/import.js b/ext/bg/js/import.js
deleted file mode 100644
index 0601cb9f..00000000
--- a/ext/bg/js/import.js
+++ /dev/null
@@ -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();
-});
diff --git a/ext/bg/js/options-form.js b/ext/bg/js/options-form.js
index eb562142..fb81e83a 100644
--- a/ext/bg/js/options-form.js
+++ b/ext/bg/js/options-form.js
@@ -16,55 +16,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+//
+// General
+//
function yomichan() {
return chrome.extension.getBackgroundPage().yomichan;
}
-function anki() {
- return yomichan().anki;
-}
-
-function fieldsToDict(selection) {
- const result = {};
- selection.each((index, element) => {
- result[$(element).data('field')] = $(element).val();
- });
-
- return result;
-}
-
-function modelIdToFieldOptKey(id) {
- return {
- 'anki-term-model': 'ankiTermFields',
- 'anki-kanji-model': 'ankiKanjiFields'
- }[id];
-}
-
-function modelIdToMarkers(id) {
- return {
- 'anki-term-model': [
- 'audio',
- 'expression',
- 'expression-furigana',
- 'glossary',
- 'glossary-list',
- 'reading',
- 'sentence',
- 'tags',
- 'url'
- ],
- 'anki-kanji-model': [
- 'character',
- 'glossary',
- 'glossary-list',
- 'kunyomi',
- 'onyomi',
- 'url'
- ],
- }[id];
-}
-
function getFormValues() {
return loadOptions().then(optsOld => {
const optsNew = $.extend({}, optsOld);
@@ -91,6 +50,14 @@ function getFormValues() {
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)
@@ -120,43 +87,294 @@ function updateVisibility(opts) {
}
}
-function populateAnkiDeckAndModel(opts) {
- const ankiSpinner = $('#anki-spinner');
- ankiSpinner.show();
+$(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);
- const ankiFormat = $('#anki-format');
- ankiFormat.hide();
+ populateDictionaries(opts);
+ populateAnkiDeckAndModel(opts);
+ updateVisibility(opts);
+ });
+});
- const ankiDeck = $('.anki-deck');
- ankiDeck.find('option').remove();
+//
+// Dictionary
+//
- const ankiModel = $('.anki-model');
- ankiModel.find('option').remove();
+function database() {
+ return yomichan().translator.database;
+}
- return anki().getDeckNames().then(names => {
- names.forEach(name => ankiDeck.append($('<option/>', {value: name, text: name})));
- $('#anki-term-deck').val(opts.ankiTermDeck);
- $('#anki-kanji-deck').val(opts.ankiKanjiDeck);
+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(() => {
- return anki().getModelNames();
- }).then(names => {
- names.forEach(name => ankiModel.append($('<option/>', {value: name, text: name})));
- return populateAnkiFields($('#anki-term-model').val(opts.ankiTermModel), opts);
+ 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(() => {
- return populateAnkiFields($('#anki-kanji-model').val(opts.ankiKanjiModel), opts);
+ 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() {
+ 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) {
+ const result = {};
+ selection.each((index, element) => {
+ result[$(element).data('field')] = $(element).val();
+ });
+
+ return result;
+}
+
+function modelIdToFieldOptKey(id) {
+ return {
+ 'anki-term-model': 'ankiTermFields',
+ 'anki-kanji-model': 'ankiKanjiFields'
+ }[id];
+}
+
+function modelIdToMarkers(id) {
+ return {
+ 'anki-term-model': [
+ 'audio',
+ 'expression',
+ 'expression-furigana',
+ 'glossary',
+ 'glossary-list',
+ 'reading',
+ 'sentence',
+ 'tags',
+ 'url'
+ ],
+ 'anki-kanji-model': [
+ 'character',
+ 'glossary',
+ 'glossary-list',
+ 'kunyomi',
+ 'onyomi',
+ 'url'
+ ],
+ }[id];
+}
+
+function populateAnkiDeckAndModel(opts) {
+ showAnkiError(null);
+ showAnkiSpinner(true);
+
+ const ankiFormat = $('#anki-format').hide();
+
+ return Promise.all([anki().getDeckNames(), anki().getModelNames()]).then(([deckNames, modelNames]) => {
+ const ankiDeck = $('.anki-deck');
+ 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');
+ ankiModel.find('option').remove();
+ modelNames.forEach(name => ankiModel.append($('<option/>', {value: name, text: name})));
+
+ return Promise.all([
+ populateAnkiFields($('#anki-term-model').val(opts.ankiTermModel), opts),
+ populateAnkiFields($('#anki-kanji-model').val(opts.ankiKanjiModel), opts)
+ ]);
}).then(() => {
- $('#anki-error').hide();
ankiFormat.show();
}).catch(error => {
- $('#anki-error').show().find('span').text(error);
+ showAnkiError(error);
}).then(() => {
- ankiSpinner.hide();
+ showAnkiSpinner(false);
});
}
function populateAnkiFields(element, opts) {
- const table = element.closest('.tab-pane').find('.anki-fields');
- table.find('tbody').remove();
+ const tab = element.closest('.tab-pane');
+ const container = tab.find('tbody').empty();
const modelName = element.val();
if (modelName === null) {
@@ -168,41 +386,37 @@ function populateAnkiFields(element, opts) {
const markers = modelIdToMarkers(modelId);
return anki().getModelFieldNames(modelName).then(names => {
- const tbody = $('<tbody>');
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 => {
- e.preventDefault();
- link.closest('.input-group').find('.anki-field-value').val(link.text()).trigger('change');
- });
- markerItems.append($('<li>').append(link));
- }
+ const html = Handlebars.templates['model.html']({name, markers, value: opts[optKey][name] || ''});
+ container.append($(html));
+ });
- const groupBtn = $('<div>', {class: 'input-group-btn'});
- groupBtn.append(button.append($('<span>', {class: 'caret'})));
- groupBtn.append(markerItems);
+ tab.find('.anki-field-value').change(onOptionsChanged);
+ tab.find('.marker-link').click(e => {
+ e.preventDefault();
+ const link = e.target;
+ $(link).closest('.input-group').find('.anki-field-value').val(`{${link.text}}`).trigger('change');
+ });
+ });
+}
- 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);
+function onAnkiModelChanged(e) {
+ if (!e.originalEvent) {
+ return;
+ }
- const row = $('<tr>');
- row.append($('<td>', {class: 'col-sm-2'}).text(name));
- row.append($('<td>', {class: 'col-sm-10'}).append(group));
+ showAnkiError(null);
+ showAnkiSpinner(true);
- tbody.append(row);
+ getFormValues().then(({optsNew, optsOld}) => {
+ optsNew[modelIdToFieldOptKey($(this).id)] = {};
+ populateAnkiFields($(this), optsNew).then(() => {
+ saveOptions(optsNew).then(() => yomichan().setOptions(optsNew));
+ }).catch(error => {
+ showAnkiError(error);
+ }).then(() => {
+ showAnkiSpinner(false);
});
-
- table.append(tbody);
});
}
@@ -212,7 +426,7 @@ function onOptionsChanged(e) {
}
getFormValues().then(({optsNew, optsOld}) => {
- saveOptions(optsNew).then(() => {
+ return saveOptions(optsNew).then(() => {
yomichan().setOptions(optsNew);
updateVisibility(optsNew);
@@ -221,60 +435,18 @@ function onOptionsChanged(e) {
optsNew.ankiPassword !== optsOld.ankiPassword;
if (loginChanged && optsNew.ankiMethod === 'ankiweb') {
- anki().logout().then(() => populateAnkiDeckAndModel(optsNew)).catch(error => {
- $('#anki-error').show().find('span').text(error);
- });
+ showAnkiError(null);
+ showAnkiSpinner(true);
+ return anki().logout().then(() => populateAnkiDeckAndModel(optsNew));
} else if (loginChanged || optsNew.ankiMethod !== optsOld.ankiMethod) {
- populateAnkiDeckAndModel(optsNew);
+ showAnkiError(null);
+ showAnkiSpinner(true);
+ return populateAnkiDeckAndModel(optsNew);
}
});
+ }).catch(error => {
+ showAnkiError(error);
+ }).then(() => {
+ showAnkiSpinner(false);
});
}
-
-function onAnkiModelChanged(e) {
- if (!e.originalEvent) {
- return;
- }
-
- getFormValues().then(({optsNew, optsOld}) => {
- optsNew[modelIdToFieldOptKey($(this).id)] = {};
-
- const ankiSpinner = $('#anki-spinner');
- ankiSpinner.show();
-
- populateAnkiFields($(this), optsNew).then(() => {
- saveOptions(optsNew).then(() => yomichan().setOptions(optsNew));
- }).catch(error => {
- $('#anki-error').show().find('span').text(error);
- }).then(() => {
- $('#anki-error').hide();
- ankiSpinner.hide();
- });
- });
-}
-
-$(document).ready(() => {
- 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);
-
- populateAnkiDeckAndModel(opts);
- updateVisibility(opts);
- });
-});
diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js
index 15288afc..28448b96 100644
--- a/ext/bg/js/options.js
+++ b/ext/bg/js/options.js
@@ -28,6 +28,8 @@ function sanitizeOptions(options) {
scanDelay: 15,
scanLength: 20,
+ dictionaries: {},
+
ankiMethod: 'disabled',
ankiUsername: '',
ankiPassword: '',
diff --git a/ext/bg/js/templates.js b/ext/bg/js/templates.js
index 70920cec..1480bb17 100644
--- a/ext/bg/js/templates.js
+++ b/ext/bg/js/templates.js
@@ -1,5 +1,36 @@
(function() {
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) {
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;
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=\""
- + 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)))
+ "</span>\n";
},"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>"
+ container.escapeExpression(container.lambda(depth0, depth0))
+ "</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) {
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 : "")
+ " </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 : "")
- + " </div>\n\n <div class=\"kanji-glossary\">\n <ol>\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 : "")
- + " </ol>\n </div>\n</div>\n";
+ + " </div>\n\n <div class=\"kanji-glossary\">\n"
+ + ((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 : "")
+ + " </div>\n</div>\n";
},"useData":true});
templates['kanji-link.html'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
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) {
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) {
var 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 : "");
},"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) {
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) {
var stack1;
- return " <span class=\"rule\">"
+ return " <span class=\"reasons\">"
+ container.escapeExpression(container.lambda(depth0, depth0))
+ "</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 : "")
@@ -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;
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=\""
- + 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)))
+ "</span>\n";
},"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>"
+ container.escapeExpression(container.lambda(depth0, depth0))
+ "</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) {
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 : "")
+ " </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 : "")
- + "\n <div class=\"term-rules\">\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 : "")
+ + "\n <div class=\"term-reasons\">\n"
+ + ((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"
+ ((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"
- + ((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 : "")
- + " </ol>\n </div>\n</div>\n";
+ + " </div>\n\n <div class=\"term-glossary\">\n"
+ + ((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 : "")
+ + " </div>\n</div>\n";
},"useData":true});
templates['term-list.html'] = template({"1":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,"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) {
var 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 : "");
},"usePartial":true,"useData":true,"useDepths":true});
})(); \ No newline at end of file
diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js
index 44f37e31..f29b90c9 100644
--- a/ext/bg/js/translator.js
+++ b/ext/bg/js/translator.js
@@ -20,96 +20,33 @@
class Translator {
constructor() {
this.loaded = false;
- this.tagMeta = null;
- this.dictionary = new Dictionary();
+ this.ruleMeta = null;
+ this.database = new Database();
this.deinflector = new Deinflector();
}
- loadData(callback) {
+ prepare() {
if (this.loaded) {
return Promise.resolve();
}
- 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;
- }
+ const promises = [
+ loadJsonInt('bg/data/deinflect.json'),
+ this.database.prepare()
+ ];
- 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(() => {
+ return Promise.all(promises).then(([reasons]) => {
+ this.deinflector.setReasons(reasons);
this.loaded = true;
});
}
- findTermGroups(text) {
- const deinflectGroups = {};
- const deinflectPromises = [];
-
- 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 => {
+ findTerm(text, dictionaries, enableSoftKatakanaSearch) {
+ const cache = {};
+ return this.findDeinflectionGroups(text, dictionaries, cache).then(groups => {
const textHiragana = wanakana._katakanaToHiragana(text);
if (text !== textHiragana && enableSoftKatakanaSearch) {
- return this.findTermGroups(textHiragana).then(groupsHiragana => {
+ return this.findDeinflectionGroups(textHiragana, dictionaries, cache).then(groupsHiragana => {
for (const key in groupsHiragana) {
groups[key] = groups[key] || groupsHiragana[key];
}
@@ -137,13 +74,11 @@ class Translator {
});
}
- findKanji(text) {
- const processed = {};
- const promises = [];
-
+ findKanji(text, dictionaries) {
+ const processed = {}, promises = [];
for (const c of text) {
if (!processed[c]) {
- promises.push(this.dictionary.findKanji(c).then((definitions) => definitions));
+ promises.push(this.database.findKanji(c, dictionaries));
processed[c] = true;
}
}
@@ -151,73 +86,53 @@ class Translator {
return Promise.all(promises).then(sets => this.processKanji(sets.reduce((a, b) => a.concat(b), [])));
}
- processTerm(groups, source, tags, rules, root) {
- 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 (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] = {
- score,
- source,
- rules,
- expression: definition.expression,
- reading: definition.reading,
- glossary: definition.glossary,
- tags: sortTags(tagItems)
- };
+ findDeinflectionGroups(text, dictionaries, cache) {
+ 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);
}
- processKanji(definitions) {
+ processDeinflection(groups, {source, rules, reasons, definitions}, dictionaries) {
for (const definition of definitions) {
- const tagItems = [];
- for (const tag of definition.tags) {
- const tagItem = {
- name: tag,
- class: 'default',
- order: Number.MAX_SAFE_INTEGER,
- desc: '',
- };
-
- applyTagMeta(tagItem, this.tagMeta);
- tagItems.push(tagItem);
+ if (definition.id in groups) {
+ continue;
}
- definition.tags = sortTags(tagItems);
+ const tags = definition.tags.map(tag => buildTag(tag, definition.tagMeta));
+ groups[definition.id] = {
+ source,
+ reasons,
+ score: definition.score,
+ dictionary: definition.dictionary,
+ expression: definition.expression,
+ reading: definition.reading,
+ glossary: definition.glossary,
+ tags: sortTags(tags)
+ };
+ }
+ }
+
+ processKanji(definitions) {
+ for (const definition of definitions) {
+ const tags = definition.tags.map(tag => buildTag(tag, definition.tagMeta));
+ definition.tags = sortTags(tags);
}
return definitions;
diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js
index 4e0cc671..a0fca270 100644
--- a/ext/bg/js/util.js
+++ b/ext/bg/js/util.js
@@ -39,19 +39,11 @@ function promiseCallback(promise, callback) {
return promise.then(result => {
callback({result});
}).catch(error => {
+ console.log(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) {
return tags.sort((v1, v2) => {
const order1 = v1.order;
@@ -92,8 +84,8 @@ function sortTermDefs(definitions) {
return 1;
}
- const rl1 = v1.rules.length;
- const rl2 = v2.rules.length;
+ const rl1 = v1.reasons.length;
+ const rl2 = v2.reasons.length;
if (rl1 < rl2) {
return -1;
} else if (rl1 > rl2) {
@@ -104,15 +96,97 @@ function sortTermDefs(definitions) {
});
}
-function applyTagMeta(tag, meta) {
- const symbol = tag.name.split(':')[0];
+function buildTag(name, meta) {
+ const tag = {name};
+ const symbol = name.split(':')[0];
for (const prop in meta[symbol] || {}) {
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;
}
function splitField(field) {
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;
+ });
+}
diff --git a/ext/bg/js/yomichan.js b/ext/bg/js/yomichan.js
index 7bca579d..e8956057 100644
--- a/ext/bg/js/yomichan.js
+++ b/ext/bg/js/yomichan.js
@@ -25,11 +25,11 @@ class Yomichan {
this.translator = new Translator();
this.anki = new AnkiNull();
this.options = null;
- this.importTabId = null;
this.setState('disabled');
chrome.runtime.onMessage.addListener(this.onMessage.bind(this));
chrome.browserAction.onClicked.addListener(this.onBrowserAction.bind(this));
+ chrome.runtime.onInstalled.addListener(this.onInstalled.bind(this));
loadOptions().then(opts => {
this.setOptions(opts);
@@ -39,17 +39,9 @@ class Yomichan {
});
}
- 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;
+ onInstalled(details) {
+ if (details.reason === 'install') {
+ chrome.tabs.create({url: chrome.extension.getURL('bg/guide.html')});
}
}
@@ -91,7 +83,7 @@ class Yomichan {
break;
case 'loading':
chrome.browserAction.setBadgeText({text: '...'});
- this.translator.loadData(this.onImport.bind(this)).then(() => this.setState('enabled'));
+ this.translator.prepare().then(this.setState('enabled'));
break;
}
@@ -239,11 +231,31 @@ class Yomichan {
}
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}) {
- 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}) {