Merge branch 'dev'

This commit is contained in:
Alex Yatskov 2016-09-18 19:50:32 -07:00
commit b44d19b35e
25 changed files with 966 additions and 667 deletions

5
.gitattributes vendored
View File

@ -1,3 +1,4 @@
util/data/*dic* filter=lfs diff=lfs merge=lfs -text ext/bg/data/edict/*.json filter=lfs diff=lfs merge=lfs -text
ext/bg/data/*dic* filter=lfs diff=lfs merge=lfs -text ext/bg/data/enamdict/*.json filter=lfs diff=lfs merge=lfs -text
ext/bg/data/kanjidic/*.json filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text *.ttf filter=lfs diff=lfs merge=lfs -text

View File

@ -2,7 +2,9 @@
<html lang="en"> <html lang="en">
<body> <body>
<script src="../lib/handlebars.min.js"></script> <script src="../lib/handlebars.min.js"></script>
<script src="../lib/dexie.min.js"></script>
<script src="js/templates.js"></script> <script src="js/templates.js"></script>
<script src="js/util.js"></script>
<script src="js/dictionary.js"></script> <script src="js/dictionary.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>

View File

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Yomichan Guide</title>
<link rel="stylesheet" type="text/css" href="../lib/bootstrap-3.3.6-dist/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="../lib/bootstrap-3.3.6-dist/css/bootstrap-theme.min.css">
</head>
<body>
<div class="container">
<div class="page-header">
<h1>Yomichan Guide</h1>
</div>
<p>This is a minimal guide to get you started with Yomichan. For complete documentation, visit the <a href="https://foosoft.net/projects/yomichan-chrome/">official homepage</a>.</p>
<ol>
<li>Left-click on the <img src="../img/icon16.png" alt> icon to enable or disable Yomichan for the current browser instance.</li>
<li>Right-click on the <img src="../img/icon16.png" alt> icon and select <em>Options</em> to open the Yomichan options page.</li>
<li>Hold down <kbd>Shift</kbd> or the middle mouse button as you move your cursor over text to see definitions.</li>
<li>Resize the definition window by dragging the bottom-left corner inwards or outwards.</li>
<li>Click on Kanji in the definition window to view additional information about that character.</li>
</ol>
<p>Enjoy!</p>
</div>
</body>
</html>

62
ext/bg/import.html Normal file
View File

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Yomichan Dictionary Import</title>
<link rel="stylesheet" type="text/css" href="../lib/bootstrap-3.3.6-dist/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="../lib/bootstrap-3.3.6-dist/css/bootstrap-theme.min.css">
<style>
div.alert {
display: none;
}
</style>
</head>
<body>
<div class="container">
<div class="page-header">
<h1>Welcome to Yomichan!</h1>
</div>
<p>Thank you for downloading this extension! I sincerely hope that it will assist you on your language learning journey.</p>
<div>
<h2>Dictionary Import</h2>
<p>
Before it can be used for the first time, Yomichan must import the Japanese dictionary data included with this extension. This process can take a
couple of minutes to finish so please be patient! Please do not completely exit out of your browser until this process completes.
</p>
<div class="progress">
<div class="progress-bar progress-bar-striped" style="width: 0%"></div>
</div>
<div class="alert alert-success">Dictionary import complete!</div>
</div>
<div>
<h2>Quick Guide</h2>
<p>
Please read the steps outlined below to get quickly get up and running with Yomichan. For complete documentation,
visit the <a href="https://foosoft.net/projects/yomichan-chrome/">official homepage</a>.
</p>
<ol>
<li>Left-click on the <img src="../img/icon16.png" alt> icon to enable or disable Yomichan for the current browser instance.</li>
<li>Right-click on the <img src="../img/icon16.png" alt> icon and select <em>Options</em> to open the Yomichan options page.</li>
<li>Hold down <kbd>Shift</kbd> or the middle mouse button as you move your cursor over text to see definitions (or <kbd>Shift</kbd> + <kbd>Ctrl</kbd> for Kanji).</li>
<li>Resize the definitions window by dragging the bottom-left corner inwards or outwards.</li>
<li>Click on Kanji in the definition window to view additional information about that character.</li>
</ol>
</div>
<br>
<p>よろしくね!</p>
</div>
<script src="../lib/jquery-2.2.2.min.js"></script>
<script src="js/import.js"></script>
</body>
</html>

View File

@ -26,32 +26,36 @@ class Deinflection {
} }
validate(validator) { validate(validator) {
for (const tags of validator(this.term)) { return validator(this.term).then(sets => {
if (this.tags.length === 0) { for (const tags of sets) {
return true; if (this.tags.length === 0) {
}
for (const tag of this.tags) {
if (tags.indexOf(tag) !== -1) {
return true; return true;
} }
}
}
return false; for (const tag of this.tags) {
if (tags.includes(tag)) {
return true;
}
}
}
return false;
});
} }
deinflect(validator, rules) { deinflect(validator, rules) {
if (this.validate(validator)) { const promises = [
const child = new Deinflection(this.term, this.tags); this.validate(validator).then(valid => {
this.children.push(child); const child = new Deinflection(this.term, this.tags);
} this.children.push(child);
})
];
for (const rule in rules) { for (const rule in rules) {
for (const variant of rules[rule]) { for (const variant of rules[rule]) {
let allowed = this.tags.length === 0; let allowed = this.tags.length === 0;
for (const tag of this.tags) { for (const tag of this.tags) {
if (variant.ti.indexOf(tag) !== -1) { if (variant.ti.includes(tag)) {
allowed = true; allowed = true;
break; break;
} }
@ -62,14 +66,24 @@ class Deinflection {
} }
const term = this.term.slice(0, -variant.ki.length) + variant.ko; const term = this.term.slice(0, -variant.ki.length) + variant.ko;
const child = new Deinflection(term, variant.to, rule); if (term.length === 0) {
if (child.deinflect(validator, rules)) { continue;
this.children.push(child);
} }
const child = new Deinflection(term, variant.to, rule);
promises.push(
child.deinflect(validator, rules).then(valid => {
if (valid) {
this.children.push(child);
}
}
));
} }
} }
return this.children.length > 0; return Promise.all(promises).then(() => {
return this.children.length > 0;
});
} }
gather() { gather() {
@ -105,10 +119,6 @@ class Deinflector {
deinflect(term, validator) { deinflect(term, validator) {
const node = new Deinflection(term); const node = new Deinflection(term);
if (node.deinflect(validator, this.rules)) { return node.deinflect(validator, this.rules).then(success => success ? node.gather() : []);
return node.gather();
}
return null;
} }
} }

View File

@ -19,63 +19,199 @@
class Dictionary { class Dictionary {
constructor() { constructor() {
this.termDicts = {}; this.db = null;
this.kanjiDicts = {}; this.dbVer = 1;
this.entities = null;
} }
addTermDict(name, dict) { initDb() {
this.termDicts[name] = dict; 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',
});
} }
addKanjiDict(name, dict) { prepareDb() {
this.kanjiDicts[name] = dict; 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) { findTerm(term) {
let results = []; if (this.db === null) {
return Promise.reject('database not initialized');
for (let name in this.termDicts) {
const dict = this.termDicts[name];
if (!(term in dict.i)) {
continue;
}
const indices = dict.i[term].split(' ').map(Number);
results = results.concat(
indices.map(index => {
const [e, r, t, ...g] = dict.d[index];
return {
expression: e,
reading: r,
tags: t.split(' '),
glossary: g,
entities: dict.e,
id: index
};
})
);
} }
return results; const results = [];
return this.db.terms.where('expression').equals(term).or('reading').equals(term).each(row => {
results.push({
expression: row.expression,
reading: row.reading,
tags: row.tags.split(' '),
glossary: row.glossary,
id: row.id
});
}).then(() => {
return this.getEntities();
}).then(entities => {
for (const result of results) {
result.entities = entities;
}
return results;
});
} }
findKanji(kanji) { findKanji(kanji) {
const results = []; if (this.db === null) {
return Promise.reject('database not initialized');
for (let name in this.kanjiDicts) {
const def = this.kanjiDicts[name].c[kanji];
if (def) {
const [k, o, t, ...g] = def;
results.push({
character: kanji,
kunyomi: k.split(' '),
onyomi: o.split(' '),
tags: t.split(' '),
glossary: g
});
}
} }
return results; const results = [];
return this.db.kanji.where('character').equals(kanji).each(row => {
results.push({
character: row.character,
onyomi: row.onyomi.split(' '),
kunyomi: row.kunyomi.split(' '),
tags: row.tags.split(' '),
glossary: row.meanings
});
}).then(() => results);
}
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

@ -17,26 +17,20 @@
*/ */
function bgSendMessage(action, params) { function api_setProgress(progress) {
return new Promise((resolve, reject) => chrome.runtime.sendMessage({action, params}, resolve)); $('.progress-bar').css('width', `${progress}%`);
if (progress === 100.0) {
$('.progress').hide();
$('.alert').show();
}
} }
function bgFindTerm(text) { chrome.runtime.onMessage.addListener(({action, params}, sender, callback) => {
return bgSendMessage('findTerm', {text}); const method = this['api_' + action];
} if (typeof(method) === 'function') {
method.call(this, params);
}
function bgFindKanji(text) { callback();
return bgSendMessage('findKanji', {text}); });
}
function bgRenderText(data, template) {
return bgSendMessage('renderText', {data, template});
}
function bgCanAddDefinitions(definitions, modes) {
return bgSendMessage('canAddDefinitions', {definitions, modes});
}
function bgAddDefinition(definition, mode) {
return bgSendMessage('addDefinition', {definition, mode});
}

View File

@ -32,7 +32,7 @@ function fieldsToDict(selection) {
function modelIdToFieldOptKey(id) { function modelIdToFieldOptKey(id) {
return { return {
'anki-term-model': 'ankiTermFields', 'anki-term-model': 'ankiTermFields',
'anki-kanji-model': 'ankiKanjiFields' 'anki-kanji-model': 'ankiKanjiFields'
}[id]; }[id];
} }
@ -60,15 +60,14 @@ function modelIdToMarkers(id) {
}[id]; }[id];
} }
function formToOptions(section, callback) { function formToOptions(section) {
loadOptions((optsOld) => { return loadOptions().then(optsOld => {
const optsNew = $.extend({}, optsOld); const optsNew = $.extend({}, optsOld);
switch (section) { switch (section) {
case 'general': case 'general':
optsNew.scanLength = parseInt($('#scan-length').val(), 10); optsNew.scanLength = parseInt($('#scan-length').val(), 10);
optsNew.activateOnStartup = $('#activate-on-startup').prop('checked'); optsNew.activateOnStartup = $('#activate-on-startup').prop('checked');
optsNew.loadEnamDict = $('#load-enamdict').prop('checked');
optsNew.selectMatchedText = $('#select-matched-text').prop('checked'); optsNew.selectMatchedText = $('#select-matched-text').prop('checked');
optsNew.showAdvancedOptions = $('#show-advanced-options').prop('checked'); optsNew.showAdvancedOptions = $('#show-advanced-options').prop('checked');
optsNew.enableAudioPlayback = $('#enable-audio-playback').prop('checked'); optsNew.enableAudioPlayback = $('#enable-audio-playback').prop('checked');
@ -86,7 +85,10 @@ function formToOptions(section, callback) {
break; break;
} }
callback(sanitizeOptions(optsNew), sanitizeOptions(optsOld)); return {
optsNew: sanitizeOptions(optsNew),
optsOld: sanitizeOptions(optsOld)
};
}); });
} }
@ -95,9 +97,9 @@ function populateAnkiDeckAndModel(opts) {
const ankiDeck = $('.anki-deck'); const ankiDeck = $('.anki-deck');
ankiDeck.find('option').remove(); ankiDeck.find('option').remove();
yomi.api_getDeckNames({callback: (names) => { yomi.api_getDeckNames({callback: names => {
if (names !== null) { if (names !== null) {
names.forEach((name) => ankiDeck.append($('<option/>', {value: name, text: name}))); names.forEach(name => ankiDeck.append($('<option/>', {value: name, text: name})));
} }
$('#anki-term-deck').val(opts.ankiTermDeck); $('#anki-term-deck').val(opts.ankiTermDeck);
@ -106,9 +108,9 @@ function populateAnkiDeckAndModel(opts) {
const ankiModel = $('.anki-model'); const ankiModel = $('.anki-model');
ankiModel.find('option').remove(); ankiModel.find('option').remove();
yomi.api_getModelNames({callback: (names) => { yomi.api_getModelNames({callback: names => {
if (names !== null) { if (names !== null) {
names.forEach((name) => ankiModel.append($('<option/>', {value: name, text: name}))); names.forEach(name => ankiModel.append($('<option/>', {value: name, text: name})));
} }
populateAnkiFields($('#anki-term-model').val(opts.ankiTermModel), opts); populateAnkiFields($('#anki-term-model').val(opts.ankiTermModel), opts);
@ -119,7 +121,7 @@ function populateAnkiDeckAndModel(opts) {
function updateAnkiStatus() { function updateAnkiStatus() {
$('.error-dlg').hide(); $('.error-dlg').hide();
yomichan().api_getVersion({callback: (version) => { yomichan().api_getVersion({callback: version => {
if (version === null) { if (version === null) {
$('.error-dlg-connection').show(); $('.error-dlg-connection').show();
$('.options-anki-controls').hide(); $('.options-anki-controls').hide();
@ -142,19 +144,19 @@ function populateAnkiFields(element, opts) {
const optKey = modelIdToFieldOptKey(modelId); const optKey = modelIdToFieldOptKey(modelId);
const markers = modelIdToMarkers(modelId); const markers = modelIdToMarkers(modelId);
yomichan().api_getModelFieldNames({modelName, callback: (names) => { yomichan().api_getModelFieldNames({modelName, callback: names => {
const table = element.closest('.tab-pane').find('.anki-fields'); const table = element.closest('.tab-pane').find('.anki-fields');
table.find('tbody').remove(); table.find('tbody').remove();
const tbody = $('<tbody>'); const tbody = $('<tbody>');
names.forEach((name) => { names.forEach(name => {
const button = $('<button>', {type: 'button', class: 'btn btn-default dropdown-toggle'}); const button = $('<button>', {type: 'button', class: 'btn btn-default dropdown-toggle'});
button.attr('data-toggle', 'dropdown').dropdown(); button.attr('data-toggle', 'dropdown').dropdown();
const markerItems = $('<ul>', {class: 'dropdown-menu dropdown-menu-right'}); const markerItems = $('<ul>', {class: 'dropdown-menu dropdown-menu-right'});
for (const marker of markers) { for (const marker of markers) {
const link = $('<a>', {href: '#'}).text(`{${marker}}`); const link = $('<a>', {href: '#'}).text(`{${marker}}`);
link.click((e) => { link.click(e => {
e.preventDefault(); e.preventDefault();
link.closest('.input-group').find('.anki-field-value').val(link.text()).trigger('change'); link.closest('.input-group').find('.anki-field-value').val(link.text()).trigger('change');
}); });
@ -185,8 +187,8 @@ function onOptionsGeneralChanged(e) {
return; return;
} }
formToOptions('general', (optsNew, optsOld) => { formToOptions('general').then(({optsNew, optsOld}) => {
saveOptions(optsNew, () => { saveOptions(optsNew).then(() => {
yomichan().setOptions(optsNew); yomichan().setOptions(optsNew);
if (!optsOld.enableAnkiConnect && optsNew.enableAnkiConnect) { if (!optsOld.enableAnkiConnect && optsNew.enableAnkiConnect) {
updateAnkiStatus(); updateAnkiStatus();
@ -210,30 +212,29 @@ function onOptionsAnkiChanged(e) {
return; return;
} }
formToOptions('anki', (opts) => { formToOptions('anki').then(({optsNew, optsOld}) => {
saveOptions(opts, () => yomichan().setOptions(opts)); saveOptions(optsNew).then(() => yomichan().setOptions(optsNew));
}); });
} }
function onAnkiModelChanged(e) { function onAnkiModelChanged(e) {
if (e.originalEvent) { if (e.originalEvent) {
formToOptions('anki', (opts) => { formToOptions('anki').then(({optsNew, optsOld}) => {
opts[modelIdToFieldOptKey($(this).id)] = {}; optsNew[modelIdToFieldOptKey($(this).id)] = {};
populateAnkiFields($(this), opts); populateAnkiFields($(this), optsNew);
saveOptions(opts, () => yomichan().setOptions(opts)); saveOptions(optsNew).then(() => yomichan().setOptions(optsNew));
}); });
} }
} }
$(document).ready(() => { $(document).ready(() => {
loadOptions((opts) => { loadOptions().then(opts => {
$('#scan-length').val(opts.scanLength);
$('#activate-on-startup').prop('checked', opts.activateOnStartup); $('#activate-on-startup').prop('checked', opts.activateOnStartup);
$('#load-enamdict').prop('checked', opts.loadEnamDict);
$('#select-matched-text').prop('checked', opts.selectMatchedText); $('#select-matched-text').prop('checked', opts.selectMatchedText);
$('#show-advanced-options').prop('checked', opts.showAdvancedOptions);
$('#enable-audio-playback').prop('checked', opts.enableAudioPlayback); $('#enable-audio-playback').prop('checked', opts.enableAudioPlayback);
$('#enable-anki-connect').prop('checked', opts.enableAnkiConnect); $('#enable-anki-connect').prop('checked', opts.enableAnkiConnect);
$('#show-advanced-options').prop('checked', opts.showAdvancedOptions);
$('#scan-length').val(opts.scanLength);
$('#anki-card-tags').val(opts.ankiCardTags.join(' ')); $('#anki-card-tags').val(opts.ankiCardTags.join(' '));
$('#sentence-extent').val(opts.sentenceExtent); $('#sentence-extent').val(opts.sentenceExtent);

View File

@ -19,21 +19,22 @@
function sanitizeOptions(options) { function sanitizeOptions(options) {
const defaults = { const defaults = {
scanLength: 20, activateOnStartup: true,
activateOnStartup: false, selectMatchedText: true,
selectMatchedText: true,
showAdvancedOptions: false,
loadEnamDict: false,
enableAudioPlayback: true, enableAudioPlayback: true,
enableAnkiConnect: false, enableAnkiConnect: false,
ankiCardTags: ['yomichan'], showAdvancedOptions: false,
sentenceExtent: 200, scanLength: 20,
ankiTermDeck: '',
ankiTermModel: '', ankiCardTags: ['yomichan'],
ankiTermFields: {}, sentenceExtent: 200,
ankiKanjiDeck: '',
ankiKanjiModel: '', ankiTermDeck: '',
ankiKanjiFields: {} ankiTermModel: '',
ankiTermFields: {},
ankiKanjiDeck: '',
ankiKanjiModel: '',
ankiKanjiFields: {}
}; };
for (const key in defaults) { for (const key in defaults) {
@ -45,10 +46,16 @@ function sanitizeOptions(options) {
return options; return options;
} }
function loadOptions(callback) { function loadOptions() {
chrome.storage.sync.get(null, (items) => callback(sanitizeOptions(items))); return new Promise((resolve, reject) => {
chrome.storage.sync.get(null, opts => {
resolve(sanitizeOptions(opts));
});
});
} }
function saveOptions(opts, callback) { function saveOptions(opts) {
chrome.storage.sync.set(sanitizeOptions(opts), callback); return new Promise((resolve, reject) => {
chrome.storage.sync.set(sanitizeOptions(opts), resolve);
});
} }

View File

@ -25,171 +25,199 @@ class Translator {
this.deinflector = new Deinflector(); this.deinflector = new Deinflector();
} }
loadData({loadEnamDict=true}, callback) { loadData(callback) {
if (this.loaded) { if (this.loaded) {
callback(); return Promise.resolve();
return;
} }
Translator.loadData('bg/data/rules.json') return loadJson('bg/data/rules.json').then(rules => {
.then((response) => { this.deinflector.setRules(rules);
this.deinflector.setRules(JSON.parse(response)); return loadJson('bg/data/tags.json');
return Translator.loadData('bg/data/tags.json'); }).then(tagMeta => {
}) this.tagMeta = tagMeta;
.then((response) => { return this.dictionary.prepareDb();
this.tagMeta = JSON.parse(response); }).then(exists => {
return Translator.loadData('bg/data/edict.json'); if (exists) {
}) return;
.then((response) => { }
this.dictionary.addTermDict('edict', JSON.parse(response));
return Translator.loadData('bg/data/kanjidic.json'); if (callback) {
}) callback({state: 'begin', progress: 0});
.then((response) => { }
this.dictionary.addKanjiDict('kanjidic', JSON.parse(response));
return loadEnamDict ? Translator.loadData('bg/data/enamdict.json') : Promise.resolve(null); const banks = {};
}) const bankCallback = (loaded, total, indexUrl) => {
.then((response) => { banks[indexUrl] = {loaded, total};
if (response !== null) {
this.dictionary.addTermDict('enamdict', JSON.parse(response)); let percent = 0.0;
for (const url in banks) {
percent += banks[url].loaded / banks[url].total;
} }
this.loaded = true; percent /= 3.0;
callback();
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;
});
}
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) { findTerm(text) {
const groups = {}; return this.findTermGroups(text).then(deinflectGroups => {
for (let i = text.length; i > 0; --i) { let definitions = [];
const term = text.slice(0, i); for (const key in deinflectGroups) {
const dfs = this.deinflector.deinflect(term, t => { definitions.push(deinflectGroups[key]);
const tags = []; }
for (const d of this.dictionary.findTerm(t)) {
tags.push(d.tags); definitions = definitions.sort((v1, v2) => {
const sl1 = v1.source.length;
const sl2 = v2.source.length;
if (sl1 > sl2) {
return -1;
} else if (sl1 < sl2) {
return 1;
} }
return tags; const s1 = v1.score;
const s2 = v2.score;
if (s1 > s2) {
return -1;
} else if (s1 < s2) {
return 1;
}
const rl1 = v1.rules.length;
const rl2 = v2.rules.length;
if (rl1 < rl2) {
return -1;
} else if (rl1 > rl2) {
return 1;
}
return v2.expression.localeCompare(v1.expression);
}); });
if (dfs === null) { let length = 0;
continue; for (const result of definitions) {
length = Math.max(length, result.source.length);
} }
for (const df of dfs) { return {definitions, length};
this.processTerm(groups, df.source, df.tags, df.rules, df.root);
}
}
let definitions = [];
for (const key in groups) {
definitions.push(groups[key]);
}
definitions = definitions.sort((v1, v2) => {
const sl1 = v1.source.length;
const sl2 = v2.source.length;
if (sl1 > sl2) {
return -1;
} else if (sl1 < sl2) {
return 1;
}
const s1 = v1.score;
const s2 = v2.score;
if (s1 > s2) {
return -1;
} else if (s1 < s2) {
return 1;
}
const rl1 = v1.rules.length;
const rl2 = v2.rules.length;
if (rl1 < rl2) {
return -1;
} else if (rl1 > rl2) {
return 1;
}
return v2.expression.localeCompare(v1.expression);
}); });
let length = 0;
for (const result of definitions) {
length = Math.max(length, result.source.length);
}
return {definitions: definitions, length: length};
} }
findKanji(text) { findKanji(text) {
let definitions = [];
const processed = {}; const processed = {};
const promises = [];
for (const c of text) { for (const c of text) {
if (!processed[c]) { if (!processed[c]) {
definitions = definitions.concat(this.dictionary.findKanji(c)); promises.push(this.dictionary.findKanji(c).then((definitions) => definitions));
processed[c] = true; processed[c] = true;
} }
} }
return this.processKanji(definitions); return Promise.all(promises).then(sets => this.processKanji(sets.reduce((a, b) => a.concat(b))));
} }
processTerm(groups, source, tags, rules, root) { processTerm(groups, source, tags, rules, root) {
for (const entry of this.dictionary.findTerm(root)) { return this.dictionary.findTerm(root).then(definitions => {
if (entry.id in groups) { for (const definition of definitions) {
continue; if (definition.id in groups) {
} continue;
let matched = tags.length === 0;
for (const tag of tags) {
if (entry.tags.indexOf(tag) !== -1) {
matched = true;
break;
} }
}
if (!matched) { let matched = tags.length === 0;
continue; for (const tag of tags) {
} if (definition.tags.includes(tag)) {
matched = true;
break;
}
}
const tagItems = []; if (!matched) {
for (const tag of entry.tags) { continue;
const tagItem = { }
name: tag,
class: 'default', const tagItems = [];
order: Number.MAX_SAFE_INTEGER, for (const tag of definition.tags) {
score: 0, const tagItem = {
desc: entry.entities[tag] || '', name: tag,
class: 'default',
order: Number.MAX_SAFE_INTEGER,
score: 0,
desc: definition.entities[tag] || '',
};
this.applyTagMeta(tagItem);
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: Translator.sortTags(tagItems)
}; };
this.applyTagMeta(tagItem);
tagItems.push(tagItem);
} }
});
let score = 0;
for (const tagItem of tagItems) {
score += tagItem.score;
}
groups[entry.id] = {
score,
source,
rules,
expression: entry.expression,
reading: entry.reading,
glossary: entry.glossary,
tags: Translator.sortTags(tagItems)
};
}
} }
processKanji(entries) { processKanji(definitions) {
const results = []; for (const definition of definitions) {
for (const entry of entries) {
const tagItems = []; const tagItems = [];
for (const tag of entry.tags) { for (const tag of definition.tags) {
const tagItem = { const tagItem = {
name: tag, name: tag,
class: 'default', class: 'default',
@ -201,16 +229,10 @@ class Translator {
tagItems.push(tagItem); tagItems.push(tagItem);
} }
results.push({ definition.tags = Translator.sortTags(tagItems);
character: entry.character,
kunyomi: entry.kunyomi,
onyomi: entry.onyomi,
glossary: entry.glossary,
tags: Translator.sortTags(tagItems)
});
} }
return results; return definitions;
} }
applyTagMeta(tag) { applyTagMeta(tag) {
@ -241,18 +263,4 @@ class Translator {
return 0; return 0;
}); });
} }
static isKanji(c) {
const code = c.charCodeAt(0);
return code >= 0x4e00 && code < 0x9fb0 || code >= 0x3400 && code < 0x4dc0;
}
static loadData(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.addEventListener('load', () => resolve(xhr.responseText));
xhr.open('GET', chrome.extension.getURL(url), true);
xhr.send();
});
}
} }

46
ext/bg/js/util.js Normal file
View File

@ -0,0 +1,46 @@
/*
* Copyright (C) 2016 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
function kanjiLinks(options) {
let result = '';
for (const c of options.fn(this)) {
if (isKanji(c)) {
result += Handlebars.templates['kanji-link.html']({kanji: c}).trim();
} else {
result += c;
}
}
return result;
}
function loadJson(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.addEventListener('load', () => resolve(JSON.parse(xhr.responseText)));
xhr.open('GET', chrome.extension.getURL(url), true);
xhr.send();
});
}
function isKanji(c) {
const code = c.charCodeAt(0);
return code >= 0x4e00 && code < 0x9fb0 || code >= 0x3400 && code < 0x4dc0;
}

View File

@ -20,31 +20,20 @@
class Yomichan { class Yomichan {
constructor() { constructor() {
Handlebars.partials = Handlebars.templates; Handlebars.partials = Handlebars.templates;
Handlebars.registerHelper('kanjiLinks', function(options) { Handlebars.registerHelper('kanjiLinks', kanjiLinks);
let result = '';
for (const c of options.fn(this)) {
if (Translator.isKanji(c)) {
result += Handlebars.templates['kanji-link.html']({kanji: c}).trim();
} else {
result += c;
}
}
return result;
});
this.translator = new Translator(); this.translator = new Translator();
this.importTabId = null;
this.asyncPools = {}; this.asyncPools = {};
this.ankiConnectVer = 0; this.ankiConnectVer = 0;
this.setState('disabled'); this.setState('disabled');
chrome.runtime.onInstalled.addListener(this.onInstalled.bind(this));
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.tabs.onCreated.addListener((tab) => this.onTabReady(tab.id)); chrome.tabs.onCreated.addListener(tab => this.onTabReady(tab.id));
chrome.tabs.onUpdated.addListener(this.onTabReady.bind(this)); chrome.tabs.onUpdated.addListener(this.onTabReady.bind(this));
loadOptions((opts) => { loadOptions().then(opts => {
this.setOptions(opts); this.setOptions(opts);
if (this.options.activateOnStartup) { if (this.options.activateOnStartup) {
this.setState('loading'); this.setState('loading');
@ -52,9 +41,17 @@ class Yomichan {
}); });
} }
onInstalled(details) { onImport({state, progress}) {
if (details.reason === 'install') { if (state === 'begin') {
chrome.tabs.create({url: chrome.extension.getURL('bg/guide.html')}); chrome.tabs.create({url: chrome.extension.getURL('bg/import.html')}, tab => this.importTabId = tab.id);
}
if (this.importTabId !== null) {
this.tabInvoke(this.importTabId, 'setProgress', progress);
}
if (state === 'end') {
this.importTabId = null;
} }
} }
@ -101,7 +98,7 @@ class Yomichan {
break; break;
case 'loading': case 'loading':
chrome.browserAction.setBadgeText({text: '...'}); chrome.browserAction.setBadgeText({text: '...'});
this.translator.loadData({loadEnamDict: this.options.loadEnamDict}, () => this.setState('enabled')); this.translator.loadData(this.onImport.bind(this)).then(() => this.setState('enabled'));
break; break;
} }
@ -118,7 +115,7 @@ class Yomichan {
} }
tabInvokeAll(action, params) { tabInvokeAll(action, params) {
chrome.tabs.query({}, (tabs) => { chrome.tabs.query({}, tabs => {
for (const tab of tabs) { for (const tab of tabs) {
this.tabInvoke(tab.id, action, params); this.tabInvoke(tab.id, action, params);
} }
@ -133,7 +130,7 @@ class Yomichan {
if (this.ankiConnectVer === this.getApiVersion()) { if (this.ankiConnectVer === this.getApiVersion()) {
this.ankiInvoke(action, params, pool, callback); this.ankiInvoke(action, params, pool, callback);
} else { } else {
this.api_getVersion({callback: (version) => { this.api_getVersion({callback: version => {
if (version === this.getApiVersion()) { if (version === this.getApiVersion()) {
this.ankiConnectVer = version; this.ankiConnectVer = version;
this.ankiInvoke(action, params, pool, callback); this.ankiInvoke(action, params, pool, callback);
@ -209,7 +206,7 @@ class Yomichan {
break; break;
case 'tags': case 'tags':
if (definition.tags) { if (definition.tags) {
value = definition.tags.map((t) => t.name); value = definition.tags.map(t => t.name);
} }
break; break;
} }
@ -244,7 +241,7 @@ class Yomichan {
}; };
for (const name in fields) { for (const name in fields) {
if (fields[name].indexOf('{audio}') !== -1) { if (fields[name].includes('{audio}')) {
audio.fields.push(name); audio.fields.push(name);
} }
} }
@ -274,7 +271,7 @@ class Yomichan {
} }
} }
this.ankiInvokeSafe('canAddNotes', {notes}, 'notes', (results) => { this.ankiInvokeSafe('canAddNotes', {notes}, 'notes', results => {
const states = []; const states = [];
if (results !== null) { if (results !== null) {
@ -293,11 +290,11 @@ class Yomichan {
} }
api_findKanji({text, callback}) { api_findKanji({text, callback}) {
callback(this.translator.findKanji(text)); this.translator.findKanji(text).then(result => callback(result));
} }
api_findTerm({text, callback}) { api_findTerm({text, callback}) {
callback(this.translator.findTerm(text)); this.translator.findTerm(text).then(result => callback(result));
} }
api_getDeckNames({callback}) { api_getDeckNames({callback}) {

View File

@ -28,11 +28,6 @@
<h3>General Options</h3> <h3>General Options</h3>
<form class="form-horizontal"> <form class="form-horizontal">
<div class="form-group options-advanced">
<label for="scan-length" class="control-label col-sm-2">Scan length</label>
<div class="col-sm-10"><input type="number" min="1" id="scan-length" class="form-control"></div>
</div>
<div class="form-group"> <div class="form-group">
<div class="col-sm-offset-2 col-sm-10"> <div class="col-sm-offset-2 col-sm-10">
<div class="checkbox"> <div class="checkbox">
@ -41,14 +36,6 @@
</div> </div>
</div> </div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="checkbox">
<label class="control-label"><input type="checkbox" id="load-enamdict"> Load <a href="http://www.edrdg.org/enamdict/enamdict_doc.html">ENAMDICT</a> (requires restart)</label>
</div>
</div>
</div>
<div class="form-group"> <div class="form-group">
<div class="col-sm-offset-2 col-sm-10"> <div class="col-sm-offset-2 col-sm-10">
<div class="checkbox"> <div class="checkbox">
@ -73,7 +60,6 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-sm-offset-2 col-sm-10"> <div class="col-sm-offset-2 col-sm-10">
<div class="checkbox"> <div class="checkbox">
@ -81,6 +67,11 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-group options-advanced">
<label for="scan-length" class="control-label col-sm-2">Scan length</label>
<div class="col-sm-10"><input type="number" min="1" id="scan-length" class="form-control"></div>
</div>
</form> </form>
</div> </div>

View File

@ -1,289 +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 Client {
constructor() {
this.popup = new Popup();
this.audio = {};
this.lastMousePos = null;
this.lastTextSource = null;
this.activateKey = 16;
this.activateBtn = 2;
this.enabled = false;
this.options = {};
this.definitions = null;
this.sequence = 0;
this.fgRoot = chrome.extension.getURL('fg');
chrome.runtime.onMessage.addListener(this.onBgMessage.bind(this));
window.addEventListener('message', this.onFrameMessage.bind(this));
window.addEventListener('mousedown', this.onMouseDown.bind(this));
window.addEventListener('mousemove', this.onMouseMove.bind(this));
window.addEventListener('keydown', this.onKeyDown.bind(this));
window.addEventListener('scroll', (e) => this.hidePopup());
window.addEventListener('resize', (e) => this.hidePopup());
}
onKeyDown(e) {
if (this.enabled && this.lastMousePos !== null && (e.keyCode === this.activateKey || e.charCode === this.activateKey)) {
this.searchAt(this.lastMousePos);
}
}
onMouseMove(e) {
this.lastMousePos = {x: e.clientX, y: e.clientY};
if (this.enabled && (e.shiftKey || e.which === this.activateBtn)) {
this.searchAt(this.lastMousePos);
}
}
onMouseDown(e) {
this.lastMousePos = {x: e.clientX, y: e.clientY};
if (this.enabled && (e.shiftKey || e.which === this.activateBtn)) {
this.searchAt(this.lastMousePos);
} else {
this.hidePopup();
}
}
onBgMessage({action, params}, sender, callback) {
const method = this['api_' + action];
if (typeof(method) === 'function') {
method.call(this, params);
}
callback();
}
onFrameMessage(e) {
const {action, params} = e.data, method = this['api_' + action];
if (typeof(method) === 'function') {
method.call(this, params);
}
}
searchAt(point) {
const textSource = Client.textSourceFromPoint(point);
if (textSource === null || !textSource.containsPoint(point)) {
this.hidePopup();
return;
}
if (this.lastTextSource !== null && this.lastTextSource.equals(textSource)) {
return;
}
textSource.setEndOffset(this.options.scanLength);
let defs = [];
let seq = -1;
bgFindTerm(textSource.text())
.then(({definitions, length}) => {
if (length === 0) {
return Promise.reject();
}
textSource.setEndOffset(length);
const sentence = Client.extractSentence(textSource, this.options.sentenceExtent);
definitions.forEach((definition) => {
definition.url = window.location.href;
definition.sentence = sentence;
});
defs = definitions;
seq = ++this.sequence;
return bgRenderText({definitions, root: this.fgRoot, options: this.options, sequence: seq}, 'term-list.html');
})
.then((content) => {
this.definitions = defs;
this.showPopup(textSource, content);
return bgCanAddDefinitions(defs, ['term_kanji', 'term_kana']);
})
.then((states) => {
if (states !== null) {
states.forEach((state, index) => this.popup.sendMessage('setActionState', {index, state, sequence: seq}));
}
}, () => this.hidePopup());
}
showPopup(textSource, content) {
this.popup.showNextTo(textSource.getRect(), content);
if (this.options.selectMatchedText) {
textSource.select();
}
this.lastTextSource = textSource;
}
hidePopup() {
this.popup.hide();
if (this.options.selectMatchedText && this.lastTextSource !== null) {
this.lastTextSource.deselect();
}
this.lastTextSource = null;
this.definitions = null;
}
api_setOptions(opts) {
this.options = opts;
}
api_setEnabled(enabled) {
if (!(this.enabled = enabled)) {
this.hidePopup();
}
}
api_addNote({index, mode}) {
const state = {[mode]: false};
bgAddDefinition(this.definitions[index], mode).then((success) => {
if (success) {
this.popup.sendMessage('setActionState', {index, state, sequence: this.sequence});
} else {
alert('Note could not be added');
}
});
}
api_playAudio(index) {
const definition = this.definitions[index];
let url = `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kanji=${encodeURIComponent(definition.expression)}`;
if (definition.reading) {
url += `&kana=${encodeURIComponent(definition.reading)}`;
}
for (const key in this.audio) {
this.audio[key].pause();
}
const audio = this.audio[url] || new Audio(url);
audio.currentTime = 0;
audio.play();
this.audio[url] = audio;
}
api_displayKanji(kanji) {
let defs = [];
let seq = -1;
bgFindKanji(kanji)
.then((definitions) => {
definitions.forEach((definition) => definition.url = window.location.href);
defs = definitions;
seq = ++this.sequence;
return bgRenderText({definitions, root: this.fgRoot, options: this.options, sequence: seq}, 'kanji-list.html');
})
.then((content) => {
this.definitions = defs;
this.popup.setContent(content, defs);
return bgCanAddDefinitions(defs, ['kanji']);
})
.then((states) => {
if (states !== null) {
states.forEach((state, index) => this.popup.sendMessage('setActionState', {index, state, sequence: seq}));
}
});
}
static textSourceFromPoint(point) {
const element = document.elementFromPoint(point.x, point.y);
if (element !== null) {
const names = ['IMG', 'INPUT', 'BUTTON', 'TEXTAREA'];
if (names.indexOf(element.nodeName) !== -1) {
return new TextSourceElement(element);
}
}
const range = document.caretRangeFromPoint(point.x, point.y);
if (range !== null) {
return new TextSourceRange(range);
}
return null;
}
static extractSentence(source, extent) {
const quotesFwd = {'「': '」', '『': '』', "'": "'", '"': '"'};
const quotesBwd = {'」': '「', '』': '『', "'": "'", '"': '"'};
const terminators = '…。..?!';
const sourceLocal = source.clone();
const position = sourceLocal.setStartOffset(extent);
sourceLocal.setEndOffset(position + extent);
const content = sourceLocal.text();
let quoteStack = [];
let startPos = 0;
for (let i = position; i >= startPos; --i) {
const c = content[i];
if (quoteStack.length === 0 && (terminators.indexOf(c) !== -1 || c in quotesFwd)) {
startPos = i + 1;
break;
}
if (quoteStack.length > 0 && c === quoteStack[0]) {
quoteStack.pop();
} else if (c in quotesBwd) {
quoteStack = [quotesBwd[c]].concat(quoteStack);
}
}
quoteStack = [];
let endPos = content.length;
for (let i = position; i < endPos; ++i) {
const c = content[i];
if (quoteStack.length === 0) {
if (terminators.indexOf(c) !== -1) {
endPos = i + 1;
break;
}
else if (c in quotesBwd) {
endPos = i;
break;
}
}
if (quoteStack.length > 0 && c === quoteStack[0]) {
quoteStack.pop();
} else if (c in quotesFwd) {
quoteStack = [quotesFwd[c]].concat(quoteStack);
}
}
return content.substring(startPos, endPos).trim();
}
}
window.yomiClient = new Client();

244
ext/fg/js/driver.js Normal file
View File

@ -0,0 +1,244 @@
/*
* 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 Driver {
constructor() {
this.popup = new Popup();
this.audio = {};
this.lastMousePos = null;
this.lastTextSource = null;
this.pendingLookup = false;
this.enabled = false;
this.options = {};
this.definitions = null;
this.sequence = 0;
this.fgRoot = chrome.extension.getURL('fg');
chrome.runtime.onMessage.addListener(this.onBgMessage.bind(this));
window.addEventListener('message', this.onFrameMessage.bind(this));
window.addEventListener('mousedown', this.onMouseDown.bind(this));
window.addEventListener('mousemove', this.onMouseMove.bind(this));
window.addEventListener('keydown', this.onKeyDown.bind(this));
window.addEventListener('scroll', e => this.hidePopup());
window.addEventListener('resize', e => this.hidePopup());
}
onKeyDown(e) {
if (this.enabled && this.lastMousePos !== null && (e.keyCode === 16 || e.charCode === 16)) {
this.searchAt(this.lastMousePos, e.ctrlKey ? 'kanji' : 'terms');
} else {
this.hidePopup();
}
}
onMouseMove(e) {
this.lastMousePos = {x: e.clientX, y: e.clientY};
if (this.enabled && (e.shiftKey || e.which === 2)) {
this.searchAt(this.lastMousePos, e.ctrlKey ? 'kanji' : 'terms');
}
}
onMouseDown(e) {
this.lastMousePos = {x: e.clientX, y: e.clientY};
if (this.enabled && (e.shiftKey || e.which === 2)) {
this.searchAt(this.lastMousePos, e.ctrlKey ? 'kanji' : 'terms');
} else {
this.hidePopup();
}
}
onBgMessage({action, params}, sender, callback) {
const method = this['api_' + action];
if (typeof(method) === 'function') {
method.call(this, params);
}
callback();
}
onFrameMessage(e) {
const {action, params} = e.data, method = this['api_' + action];
if (typeof(method) === 'function') {
method.call(this, params);
}
}
searchTerms(textSource) {
textSource.setEndOffset(this.options.scanLength);
this.pendingLookup = true;
findTerm(textSource.text()).then(({definitions, length}) => {
if (definitions.length === 0) {
this.pendingLookup = false;
this.hidePopup();
} else {
textSource.setEndOffset(length);
const sentence = extractSentence(textSource, this.options.sentenceExtent);
definitions.forEach(definition => {
definition.url = window.location.href;
definition.sentence = sentence;
});
const sequence = ++this.sequence;
return renderText({definitions, sequence, root: this.fgRoot, options: this.options}, 'term-list.html').then(content => {
this.definitions = definitions;
this.pendingLookup = false;
this.showPopup(textSource, content);
return canAddDefinitions(definitions, ['term_kanji', 'term_kana']);
}).then(states => {
if (states !== null) {
states.forEach((state, index) => this.popup.invokeApi('setActionState', {index, state, sequence}));
}
});
}
});
}
searchKanji(textSource) {
textSource.setEndOffset(1);
this.pendingLookup = true;
findKanji(textSource.text()).then(definitions => {
if (definitions.length === 0) {
this.pendingLookup = false;
this.hidePopup();
} else {
definitions.forEach(definition => definition.url = window.location.href);
const sequence = ++this.sequence;
return renderText({definitions, sequence, root: this.fgRoot, options: this.options}, 'kanji-list.html').then(content => {
this.definitions = definitions;
this.pendingLookup = false;
this.showPopup(textSource, content);
return canAddDefinitions(definitions, ['kanji']);
}).then(states => {
if (states !== null) {
states.forEach((state, index) => this.popup.invokeApi('setActionState', {index, state, sequence}));
}
});
}
});
}
searchAt(point, mode) {
if (this.pendingLookup) {
return;
}
const textSource = textSourceFromPoint(point);
if (textSource === null || !textSource.containsPoint(point)) {
this.hidePopup();
return;
}
if (this.lastTextSource !== null && this.lastTextSource.equals(textSource)) {
return;
}
switch (mode) {
case 'terms':
this.searchTerms(textSource);
break;
case 'kanji':
this.searchKanji(textSource);
break;
}
}
showPopup(textSource, content) {
this.popup.showNextTo(textSource.getRect(), content);
if (this.options.selectMatchedText) {
textSource.select();
}
this.lastTextSource = textSource;
}
hidePopup() {
this.popup.hide();
if (this.options.selectMatchedText && this.lastTextSource !== null) {
this.lastTextSource.deselect();
}
this.lastTextSource = null;
this.definitions = null;
}
api_setOptions(opts) {
this.options = opts;
}
api_setEnabled(enabled) {
if (!(this.enabled = enabled)) {
this.hidePopup();
}
}
api_addNote({index, mode}) {
const state = {[mode]: false};
addDefinition(this.definitions[index], mode).then(success => {
if (success) {
this.popup.invokeApi('setActionState', {index, state, sequence: this.sequence});
} else {
alert('Note could not be added');
}
});
}
api_playAudio(index) {
const definition = this.definitions[index];
let url = `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kanji=${encodeURIComponent(definition.expression)}`;
if (definition.reading) {
url += `&kana=${encodeURIComponent(definition.reading)}`;
}
for (const key in this.audio) {
this.audio[key].pause();
}
const audio = this.audio[url] || new Audio(url);
audio.currentTime = 0;
audio.play();
this.audio[url] = audio;
}
api_displayKanji(kanji) {
findKanji(kanji).then(definitions => {
definitions.forEach(definition => definition.url = window.location.href);
const sequence = ++this.sequence;
return renderText({definitions, sequence, root: this.fgRoot, options: this.options}, 'kanji-list.html').then(content => {
this.definitions = definitions;
this.popup.setContent(content, definitions);
return canAddDefinitions(definitions, ['kanji']);
}).then(states => {
if (states !== null) {
states.forEach((state, index) => this.popup.invokeApi('setActionState', {index, state, sequence}));
}
});
});
}
}
window.driver = new Driver();

View File

@ -17,48 +17,39 @@
*/ */
function invokeApi(action, params, target) {
target.postMessage({action, params}, '*');
}
function registerKanjiLinks() { function registerKanjiLinks() {
for (const link of Array.from(document.getElementsByClassName('kanji-link'))) { for (const link of Array.from(document.getElementsByClassName('kanji-link'))) {
link.addEventListener('click', (e) => { link.addEventListener('click', e => {
e.preventDefault(); e.preventDefault();
window.parent.postMessage({action: 'displayKanji', params: e.target.innerHTML}, '*'); invokeApi('displayKanji', e.target.innerHTML, window.parent);
}); });
} }
} }
function registerAddNoteLinks() { function registerAddNoteLinks() {
for (const link of Array.from(document.getElementsByClassName('action-add-note'))) { for (const link of Array.from(document.getElementsByClassName('action-add-note'))) {
link.addEventListener('click', (e) => { link.addEventListener('click', e => {
e.preventDefault(); e.preventDefault();
const ds = e.currentTarget.dataset; const ds = e.currentTarget.dataset;
window.parent.postMessage({action: 'addNote', params: {index: ds.index, mode: ds.mode}}, '*'); invokeApi('addNote', {index: ds.index, mode: ds.mode}, window.parent);
}); });
} }
} }
function registerAudioLinks() { function registerAudioLinks() {
for (const link of Array.from(document.getElementsByClassName('action-play-audio'))) { for (const link of Array.from(document.getElementsByClassName('action-play-audio'))) {
link.addEventListener('click', (e) => { link.addEventListener('click', e => {
e.preventDefault(); e.preventDefault();
const ds = e.currentTarget.dataset; const ds = e.currentTarget.dataset;
window.parent.postMessage({action: 'playAudio', params: ds.index}, '*'); invokeApi('playAudio', ds.index, window.parent);
}); });
} }
} }
function onDomContentLoaded() {
registerKanjiLinks();
registerAddNoteLinks();
registerAudioLinks();
}
function onMessage(e) {
const {action, params} = e.data, method = window['api_' + action];
if (typeof(method) === 'function') {
method(params);
}
}
function api_setActionState({index, state, sequence}) { function api_setActionState({index, state, sequence}) {
for (const mode in state) { for (const mode in state) {
const matches = document.querySelectorAll(`.action-bar[data-sequence="${sequence}"] .action-add-note[data-index="${index}"][data-mode="${mode}"]`); const matches = document.querySelectorAll(`.action-bar[data-sequence="${sequence}"] .action-add-note[data-index="${index}"][data-mode="${mode}"]`);
@ -75,5 +66,15 @@ function api_setActionState({index, state, sequence}) {
} }
} }
document.addEventListener('DOMContentLoaded', onDomContentLoaded, false); document.addEventListener('DOMContentLoaded', () => {
window.addEventListener('message', onMessage); registerKanjiLinks();
registerAddNoteLinks();
registerAudioLinks();
});
window.addEventListener('message', e => {
const {action, params} = e.data, method = window['api_' + action];
if (typeof(method) === 'function') {
method(params);
}
});

View File

@ -68,7 +68,7 @@ class Popup {
doc.close(); doc.close();
} }
sendMessage(action, params, callback) { invokeApi(action, params) {
if (this.popup !== null) { if (this.popup !== null) {
this.popup.contentWindow.postMessage({action, params}, '*'); this.popup.contentWindow.postMessage({action, params}, '*');
} }
@ -81,8 +81,8 @@ class Popup {
this.popup = document.createElement('iframe'); this.popup = document.createElement('iframe');
this.popup.id = 'yomichan-popup'; this.popup.id = 'yomichan-popup';
this.popup.addEventListener('mousedown', (e) => e.stopPropagation()); this.popup.addEventListener('mousedown', e => e.stopPropagation());
this.popup.addEventListener('scroll', (e) => e.stopPropagation()); this.popup.addEventListener('scroll', e => e.stopPropagation());
document.body.appendChild(this.popup); document.body.appendChild(this.popup);
} }

114
ext/fg/js/util.js Normal file
View File

@ -0,0 +1,114 @@
/*
* 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 invokeApiBg(action, params) {
return new Promise((resolve, reject) => chrome.runtime.sendMessage({action, params}, resolve));
}
function findTerm(text) {
return invokeApiBg('findTerm', {text});
}
function findKanji(text) {
return invokeApiBg('findKanji', {text});
}
function renderText(data, template) {
return invokeApiBg('renderText', {data, template});
}
function canAddDefinitions(definitions, modes) {
return invokeApiBg('canAddDefinitions', {definitions, modes});
}
function addDefinition(definition, mode) {
return invokeApiBg('addDefinition', {definition, mode});
}
function textSourceFromPoint(point) {
const element = document.elementFromPoint(point.x, point.y);
if (element !== null) {
const names = ['IMG', 'INPUT', 'BUTTON', 'TEXTAREA'];
if (names.includes(element.nodeName)) {
return new TextSourceElement(element);
}
}
const range = document.caretRangeFromPoint(point.x, point.y);
if (range !== null) {
return new TextSourceRange(range);
}
return null;
}
function extractSentence(source, extent) {
const quotesFwd = {'「': '」', '『': '』', "'": "'", '"': '"'};
const quotesBwd = {'」': '「', '』': '『', "'": "'", '"': '"'};
const terminators = '…。..?!';
const sourceLocal = source.clone();
const position = sourceLocal.setStartOffset(extent);
sourceLocal.setEndOffset(position + extent);
const content = sourceLocal.text();
let quoteStack = [];
let startPos = 0;
for (let i = position; i >= startPos; --i) {
const c = content[i];
if (quoteStack.length === 0 && (terminators.includes(c) || c in quotesFwd)) {
startPos = i + 1;
break;
}
if (quoteStack.length > 0 && c === quoteStack[0]) {
quoteStack.pop();
} else if (c in quotesBwd) {
quoteStack = [quotesBwd[c]].concat(quoteStack);
}
}
quoteStack = [];
let endPos = content.length;
for (let i = position; i < endPos; ++i) {
const c = content[i];
if (quoteStack.length === 0) {
if (terminators.includes(c)) {
endPos = i + 1;
break;
}
else if (c in quotesBwd) {
endPos = i;
break;
}
}
if (quoteStack.length > 0 && c === quoteStack[0]) {
quoteStack.pop();
} else if (c in quotesFwd) {
quoteStack = [quotesFwd[c]].concat(quoteStack);
}
}
return content.substring(startPos, endPos).trim();
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 B

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 B

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 B

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 B

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 B

After

Width:  |  Height:  |  Size: 223 B

3
ext/lib/dexie.min.js vendored Normal file

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.97", "version": "0.98",
"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"},
@ -15,8 +15,8 @@
"fg/js/source-range.js", "fg/js/source-range.js",
"fg/js/source-element.js", "fg/js/source-element.js",
"fg/js/popup.js", "fg/js/popup.js",
"fg/js/api.js", "fg/js/util.js",
"fg/js/client.js" "fg/js/driver.js"
], ],
"css": ["fg/css/client.css"] "css": ["fg/css/client.css"]
}], }],