summaryrefslogtreecommitdiff
path: root/ext/bg/js
diff options
context:
space:
mode:
authorAlex Yatskov <alex@foosoft.net>2016-04-04 22:02:26 -0700
committerAlex Yatskov <alex@foosoft.net>2016-04-04 22:02:26 -0700
commit37ffcbf3ee2d9722751e899bdf4bac91ca7c92c1 (patch)
treeee2c320602cbf5bb846d2b2832375e8e4650f11e /ext/bg/js
parente5a3904cf7a90519aaf7d7529179156eb4b28fdf (diff)
File reorg
Diffstat (limited to 'ext/bg/js')
-rw-r--r--ext/bg/js/deinflector.js126
-rw-r--r--ext/bg/js/dictionary.js67
-rw-r--r--ext/bg/js/options-form.js42
-rw-r--r--ext/bg/js/options.js38
-rw-r--r--ext/bg/js/templates.js35
-rw-r--r--ext/bg/js/translator.js178
-rw-r--r--ext/bg/js/yomichan.js83
7 files changed, 569 insertions, 0 deletions
diff --git a/ext/bg/js/deinflector.js b/ext/bg/js/deinflector.js
new file mode 100644
index 00000000..03f9d40a
--- /dev/null
+++ b/ext/bg/js/deinflector.js
@@ -0,0 +1,126 @@
+/*
+ * 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 Deinflection {
+ constructor(term, tags=[], rule='') {
+ this.children = [];
+ this.term = term;
+ this.tags = tags;
+ this.rule = rule;
+ }
+
+ validate(validator) {
+ for (const tags of validator(this.term)) {
+ if (this.tags.length === 0) {
+ return true;
+ }
+
+ for (const tag of this.tags) {
+ if (this.searchTags(tag, tags)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ deinflect(validator, rules) {
+ if (this.validate(validator)) {
+ const child = new Deinflection(this.term);
+ this.children.push(child);
+ }
+
+ for (const rule in rules) {
+ const variants = rules[rule];
+ for (const v of variants) {
+ let allowed = this.tags.length === 0;
+ for (const tag of this.tags) {
+ if (this.searchTags(tag, v.tagsIn)) {
+ allowed = true;
+ break;
+ }
+ }
+
+ if (!allowed || !this.term.endsWith(v.kanaIn)) {
+ continue;
+ }
+
+ const term = this.term.slice(0, -v.kanaIn.length) + v.kanaOut;
+ const child = new Deinflection(term, v.tagsOut, rule);
+ if (child.deinflect(validator, rules)) {
+ this.children.push(child);
+ }
+ }
+ }
+
+ return this.children.length > 0;
+ }
+
+ searchTags(tag, tags) {
+ for (const t of tags) {
+ const re = new RegExp(tag);
+ if (re.test(t)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ gather() {
+ if (this.children.length === 0) {
+ return [{root: this.term, rules: []}];
+ }
+
+ const paths = [];
+ for (const child of this.children) {
+ for (const path of child.gather()) {
+ if (this.rule.length > 0) {
+ path.rules.push(this.rule);
+ }
+
+ path.source = this.term;
+ paths.push(path);
+ }
+ }
+
+ return paths;
+ }
+}
+
+
+class Deinflector {
+ constructor() {
+ this.rules = {};
+ }
+
+ setRules(rules) {
+ this.rules = rules;
+ }
+
+ deinflect(term, validator) {
+ const node = new Deinflection(term);
+ if (node.deinflect(validator, this.rules)) {
+ return node.gather();
+ }
+
+ return null;
+ }
+}
diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js
new file mode 100644
index 00000000..a68c2daf
--- /dev/null
+++ b/ext/bg/js/dictionary.js
@@ -0,0 +1,67 @@
+/*
+ * 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.terms = [];
+ this.termIndices = {};
+
+ this.kanji = [];
+ this.kanjiIndices = {};
+ }
+
+ addTermData(terms) {
+ let index = this.terms.length;
+ for (const [e, r, g, t] of terms) {
+ this.storeIndex(this.termIndices, e, index);
+ this.storeIndex(this.termIndices, r, index++);
+ this.terms.push([e, r, g, t]);
+ }
+ }
+
+ addKanjiData(kanji) {
+ let index = this.kanji.length;
+ for (const [c, k, o, g] of kanji) {
+ this.storeIndex(this.kanjiIndices, c, index++);
+ this.kanji.push([c, k, o, g]);
+ }
+ }
+
+ findTerm(term) {
+ return (this.termIndices[term] || []).map(index => {
+ const [e, r, g, t] = this.terms[index];
+ return {id: index, expression: e, reading: r, glossary: g, tags: t.split(' ')};
+ });
+ }
+
+ findKanji(kanji) {
+ return (this.kanjiIndices[kanji] || []).map(index => {
+ const [c, k, o, g] = def;
+ return {id: index, character: c, kunyomi: k, onyomi: o, glossary: g};
+ });
+ }
+
+ storeIndex(indices, term, index) {
+ if (term.length > 0) {
+ const indices = this.termIndices[term] || [];
+ indices.push(index);
+ this.termIndices[term] = indices;
+ }
+ }
+}
diff --git a/ext/bg/js/options-form.js b/ext/bg/js/options-form.js
new file mode 100644
index 00000000..cde0ea62
--- /dev/null
+++ b/ext/bg/js/options-form.js
@@ -0,0 +1,42 @@
+/*
+ * 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/>.
+ */
+
+
+$('#saveOptions').click(() => {
+ saveOptions(sanitizeOptions(formToOptions()))
+});
+
+$('#resetOptions').click(() => {
+ if (confirm('Reset options to defaults?')) {
+ optionsToForm(sanitizeOptions({}));
+ }
+});
+
+function optionsToForm(opts) {
+ $('#scanLength').val(opts.scanLength);
+}
+
+function formToOptions() {
+ return {
+ scanLength: $('#scanLength').val()
+ };
+}
+
+$(document).ready(() => {
+ loadOptions((opts) => optionsToForm(sanitizeOptions(opts)));
+});
diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js
new file mode 100644
index 00000000..1db3be5e
--- /dev/null
+++ b/ext/bg/js/options.js
@@ -0,0 +1,38 @@
+/*
+ * 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 sanitizeOptions(options) {
+ const defaults = {
+ scanLength: 20
+ };
+
+ for (const key in defaults) {
+ options[key] = options[key] || defaults[key];
+ }
+
+ return options;
+}
+
+function loadOptions(callback) {
+ chrome.storage.sync.get(null, (items) => callback(sanitizeOptions(items)));
+}
+
+function saveOptions(opts, callback) {
+ chrome.storage.sync.set(sanitizeOptions(opts), callback);
+}
diff --git a/ext/bg/js/templates.js b/ext/bg/js/templates.js
new file mode 100644
index 00000000..8ac5f8e5
--- /dev/null
+++ b/ext/bg/js/templates.js
@@ -0,0 +1,35 @@
+(function() {
+ var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
+templates['defs.html'] = template({"1":function(container,depth0,helpers,partials,data) {
+ var stack1;
+
+ return "<div class=\"yomichan-def\">\n"
+ + ((stack1 = container.invokePartial(partials["term.html"],depth0,{"name":"term.html","data":data,"indent":" ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "")
+ + " "
+ + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : {},(data && data.last),{"name":"unless","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + "\n</div>\n";
+},"2":function(container,depth0,helpers,partials,data) {
+ return "<br>";
+},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+ var stack1;
+
+ return ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.defs : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "");
+},"usePartial":true,"useData":true});
+templates['term.html'] = template({"1":function(container,depth0,helpers,partials,data) {
+ var helper;
+
+ return "<div class=\"yomichan-def-reading\">"
+ + container.escapeExpression(((helper = (helper = helpers.reading || (depth0 != null ? depth0.reading : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},{"name":"reading","hash":{},"data":data}) : helper)))
+ + "</div>";
+},"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=\"yomichan-def-expression\">"
+ + alias4(((helper = (helper = helpers.expression || (depth0 != null ? depth0.expression : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"expression","hash":{},"data":data}) : helper)))
+ + "</div>\n"
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.reading : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + "\n<div class=\"yomichan-def-glossary\">"
+ + alias4(((helper = (helper = helpers.glossary || (depth0 != null ? depth0.glossary : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"glossary","hash":{},"data":data}) : helper)))
+ + "</div>\n";
+},"useData":true});
+})(); \ No newline at end of file
diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js
new file mode 100644
index 00000000..e8224320
--- /dev/null
+++ b/ext/bg/js/translator.js
@@ -0,0 +1,178 @@
+/*
+ * 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 Translator {
+ constructor() {
+ this.loaded = false;
+ this.paths = {
+ rules: 'bg/data/rules.json',
+ edict: 'bg/data/edict.csv',
+ enamdict: 'bg/data/enamdict.csv',
+ kanjidic: 'bg/data/kanjidic.csv'
+ };
+
+ this.dictionary = new Dictionary();
+ this.deinflector = new Deinflector();
+ }
+
+ loadData(callback) {
+ if (this.loaded) {
+ callback();
+ return;
+ }
+
+ const pendingLoads = [];
+ for (const key of ['rules', 'edict', 'enamdict', 'kanjidic']) {
+ pendingLoads.push(key);
+ Translator.loadData(this.paths[key], (response) => {
+ switch (key) {
+ case 'rules':
+ this.deinflector.setRules(JSON.parse(response));
+ break;
+ case 'kanjidic':
+ this.dictionary.addKanjiData(Translator.parseCsv(response));
+ break;
+ case 'edict':
+ case 'enamdict':
+ this.dictionary.addTermData(Translator.parseCsv(response));
+ break;
+ }
+
+ pendingLoads.splice(pendingLoads.indexOf(key), 1);
+ if (pendingLoads.length === 0) {
+ this.loaded = true;
+ callback();
+ }
+ });
+ }
+ }
+
+ findTerm(text) {
+ const groups = {};
+ for (let i = text.length; i > 0; --i) {
+ const term = text.slice(0, i);
+
+ const dfs = this.deinflector.deinflect(term, t => {
+ const tags = [];
+ for (const d of this.dictionary.findTerm(t)) {
+ tags.push(d.tags);
+ }
+
+ return tags;
+ });
+
+ if (dfs === null) {
+ this.processTerm(groups, term);
+ } else {
+ for (const df of dfs) {
+ this.processTerm(groups, df.source, df.rules, df.root);
+ }
+ }
+ }
+
+ let results = [];
+ for (const key in groups) {
+ results.push(groups[key]);
+ }
+
+ results = results.sort((v1, v2) => {
+ const sl1 = v1.source.length;
+ const sl2 = v2.source.length;
+ if (sl1 > sl2) {
+ return -1;
+ } else if (sl1 < sl2) {
+ return 1;
+ }
+
+ const p1 = v1.tags.indexOf('P') >= 0;
+ const p2 = v2.tags.indexOf('P') >= 0;
+ if (p1 && !p2) {
+ return -1;
+ } else if (!p1 && p2) {
+ return 1;
+ }
+
+ const rl1 = v1.rules.length;
+ const rl2 = v2.rules.length;
+ if (rl1 < rl2) {
+ return -1;
+ } else if (rl2 > rl1) {
+ return 1;
+ }
+
+ return 0;
+ });
+
+ let length = 0;
+ for (const result of results) {
+ length = Math.max(length, result.source.length);
+ }
+
+ return {results: results, length: length};
+ }
+
+ findKanji(text) {
+ let results = [];
+
+ const processed = {};
+ for (const c of text) {
+ if (!processed.has(c)) {
+ results = results.concat(this.dictionary.findKanji(c));
+ processed[c] = true;
+ }
+ }
+
+ return results;
+ }
+
+ processTerm(groups, source, rules=[], root='') {
+ for (const entry of this.dictionary.findTerm(root || source)) {
+ if (entry.id in groups) {
+ continue;
+ }
+
+ groups[entry.id] = {
+ expression: entry.expression,
+ reading: entry.reading,
+ glossary: entry.glossary,
+ tags: entry.tags,
+ source: source,
+ rules: rules
+ };
+ }
+ }
+
+ static loadData(url, callback) {
+ const xhr = new XMLHttpRequest();
+ xhr.addEventListener('load', () => callback(xhr.responseText));
+ xhr.open('GET', chrome.extension.getURL(url), true);
+ xhr.send();
+ }
+
+ static parseCsv(data) {
+ const result = [];
+ for (const row of data.split('\n')) {
+ if (row.length > 0) {
+ result.push(row.split('\t'));
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/ext/bg/js/yomichan.js b/ext/bg/js/yomichan.js
new file mode 100644
index 00000000..2e4552a1
--- /dev/null
+++ b/ext/bg/js/yomichan.js
@@ -0,0 +1,83 @@
+/*
+ * 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 Yomichan {
+ constructor() {
+ Handlebars.partials = Handlebars.templates;
+
+ this.translator = new Translator();
+ this.updateState('disabled');
+
+ chrome.runtime.onMessage.addListener(this.onMessage.bind(this));
+ chrome.browserAction.onClicked.addListener(this.onBrowserAction.bind(this));
+ }
+
+ onMessage(request, sender, callback) {
+ const {action, data} = request;
+ const handler = {
+ findKanji: ({text}) => this.translator.onFindKanji(text),
+ findTerm: ({text}) => this.translator.findTerm(text),
+ getState: () => this.state,
+ renderTemplate: ({data, template}) => Handlebars.templates[template](data)
+ }[action];
+
+ if (handler !== null) {
+ const result = handler.call(this, data);
+ if (callback !== null) {
+ callback(result);
+ }
+ }
+ }
+
+ onBrowserAction(tab) {
+ switch (this.state) {
+ case 'disabled':
+ this.updateState('loading');
+ break;
+ case 'enabled':
+ this.updateState('disabled');
+ break;
+ }
+ }
+
+ updateState(state) {
+ this.state = state;
+
+ switch (state) {
+ case 'disabled':
+ chrome.browserAction.setBadgeText({text: ''});
+ break;
+ case 'enabled':
+ chrome.browserAction.setBadgeText({text: 'on'});
+ break;
+ case 'loading':
+ chrome.browserAction.setBadgeText({text: '...'});
+ this.translator.loadData(() => this.updateState('enabled'));
+ break;
+ }
+
+ chrome.tabs.query({}, (tabs) => {
+ for (const tab of tabs) {
+ chrome.tabs.sendMessage(tab.id, this.state, () => null);
+ }
+ });
+ }
+}
+
+window.yomichan = new Yomichan();