Merge branch 'dev'
5
.gitattributes
vendored
@ -1,3 +1,4 @@
|
||||
util/data/*dic* filter=lfs diff=lfs merge=lfs -text
|
||||
ext/bg/data/*dic* filter=lfs diff=lfs merge=lfs -text
|
||||
ext/bg/data/edict/*.json 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
|
||||
|
@ -2,7 +2,9 @@
|
||||
<html lang="en">
|
||||
<body>
|
||||
<script src="../lib/handlebars.min.js"></script>
|
||||
<script src="../lib/dexie.min.js"></script>
|
||||
<script src="js/templates.js"></script>
|
||||
<script src="js/util.js"></script>
|
||||
<script src="js/dictionary.js"></script>
|
||||
<script src="js/deinflector.js"></script>
|
||||
<script src="js/translator.js"></script>
|
||||
|
@ -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
@ -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>
|
@ -26,32 +26,36 @@ class Deinflection {
|
||||
}
|
||||
|
||||
validate(validator) {
|
||||
for (const tags of validator(this.term)) {
|
||||
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.indexOf(tag) !== -1) {
|
||||
if (tags.includes(tag)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
deinflect(validator, rules) {
|
||||
if (this.validate(validator)) {
|
||||
const promises = [
|
||||
this.validate(validator).then(valid => {
|
||||
const child = new Deinflection(this.term, this.tags);
|
||||
this.children.push(child);
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
for (const rule in rules) {
|
||||
for (const variant of rules[rule]) {
|
||||
let allowed = this.tags.length === 0;
|
||||
for (const tag of this.tags) {
|
||||
if (variant.ti.indexOf(tag) !== -1) {
|
||||
if (variant.ti.includes(tag)) {
|
||||
allowed = true;
|
||||
break;
|
||||
}
|
||||
@ -62,14 +66,24 @@ class Deinflection {
|
||||
}
|
||||
|
||||
const term = this.term.slice(0, -variant.ki.length) + variant.ko;
|
||||
if (term.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const child = new Deinflection(term, variant.to, rule);
|
||||
if (child.deinflect(validator, rules)) {
|
||||
promises.push(
|
||||
child.deinflect(validator, rules).then(valid => {
|
||||
if (valid) {
|
||||
this.children.push(child);
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
return this.children.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
gather() {
|
||||
@ -105,10 +119,6 @@ class Deinflector {
|
||||
|
||||
deinflect(term, validator) {
|
||||
const node = new Deinflection(term);
|
||||
if (node.deinflect(validator, this.rules)) {
|
||||
return node.gather();
|
||||
}
|
||||
|
||||
return null;
|
||||
return node.deinflect(validator, this.rules).then(success => success ? node.gather() : []);
|
||||
}
|
||||
}
|
||||
|
@ -19,63 +19,199 @@
|
||||
|
||||
class Dictionary {
|
||||
constructor() {
|
||||
this.termDicts = {};
|
||||
this.kanjiDicts = {};
|
||||
this.db = null;
|
||||
this.dbVer = 1;
|
||||
this.entities = null;
|
||||
}
|
||||
|
||||
addTermDict(name, dict) {
|
||||
this.termDicts[name] = dict;
|
||||
initDb() {
|
||||
if (this.db !== null) {
|
||||
return Promise.reject('database already initialized');
|
||||
}
|
||||
|
||||
addKanjiDict(name, dict) {
|
||||
this.kanjiDicts[name] = dict;
|
||||
this.db = new Dexie('dict');
|
||||
this.db.version(1).stores({
|
||||
terms: '++id,expression,reading',
|
||||
entities: '++,name',
|
||||
kanji: '++,character',
|
||||
meta: 'name,value',
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
let results = [];
|
||||
|
||||
for (let name in this.termDicts) {
|
||||
const dict = this.termDicts[name];
|
||||
if (!(term in dict.i)) {
|
||||
continue;
|
||||
if (this.db === null) {
|
||||
return Promise.reject('database not initialized');
|
||||
}
|
||||
|
||||
const indices = dict.i[term].split(' ').map(Number);
|
||||
results = results.concat(
|
||||
indices.map(index => {
|
||||
const [e, r, t, ...g] = dict.d[index];
|
||||
return {
|
||||
expression: e,
|
||||
reading: r,
|
||||
tags: t.split(' '),
|
||||
glossary: g,
|
||||
entities: dict.e,
|
||||
id: index
|
||||
};
|
||||
})
|
||||
);
|
||||
const 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) {
|
||||
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;
|
||||
const results = [];
|
||||
return this.db.kanji.where('character').equals(kanji).each(row => {
|
||||
results.push({
|
||||
character: kanji,
|
||||
kunyomi: k.split(' '),
|
||||
onyomi: o.split(' '),
|
||||
tags: t.split(' '),
|
||||
glossary: g
|
||||
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');
|
||||
}
|
||||
|
||||
return results;
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -17,26 +17,20 @@
|
||||
*/
|
||||
|
||||
|
||||
function bgSendMessage(action, params) {
|
||||
return new Promise((resolve, reject) => chrome.runtime.sendMessage({action, params}, resolve));
|
||||
function api_setProgress(progress) {
|
||||
$('.progress-bar').css('width', `${progress}%`);
|
||||
|
||||
if (progress === 100.0) {
|
||||
$('.progress').hide();
|
||||
$('.alert').show();
|
||||
}
|
||||
}
|
||||
|
||||
function bgFindTerm(text) {
|
||||
return bgSendMessage('findTerm', {text});
|
||||
}
|
||||
chrome.runtime.onMessage.addListener(({action, params}, sender, callback) => {
|
||||
const method = this['api_' + action];
|
||||
if (typeof(method) === 'function') {
|
||||
method.call(this, params);
|
||||
}
|
||||
|
||||
function bgFindKanji(text) {
|
||||
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});
|
||||
}
|
||||
callback();
|
||||
});
|
@ -60,15 +60,14 @@ function modelIdToMarkers(id) {
|
||||
}[id];
|
||||
}
|
||||
|
||||
function formToOptions(section, callback) {
|
||||
loadOptions((optsOld) => {
|
||||
function formToOptions(section) {
|
||||
return loadOptions().then(optsOld => {
|
||||
const optsNew = $.extend({}, optsOld);
|
||||
|
||||
switch (section) {
|
||||
case 'general':
|
||||
optsNew.scanLength = parseInt($('#scan-length').val(), 10);
|
||||
optsNew.activateOnStartup = $('#activate-on-startup').prop('checked');
|
||||
optsNew.loadEnamDict = $('#load-enamdict').prop('checked');
|
||||
optsNew.selectMatchedText = $('#select-matched-text').prop('checked');
|
||||
optsNew.showAdvancedOptions = $('#show-advanced-options').prop('checked');
|
||||
optsNew.enableAudioPlayback = $('#enable-audio-playback').prop('checked');
|
||||
@ -86,7 +85,10 @@ function formToOptions(section, callback) {
|
||||
break;
|
||||
}
|
||||
|
||||
callback(sanitizeOptions(optsNew), sanitizeOptions(optsOld));
|
||||
return {
|
||||
optsNew: sanitizeOptions(optsNew),
|
||||
optsOld: sanitizeOptions(optsOld)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@ -95,9 +97,9 @@ function populateAnkiDeckAndModel(opts) {
|
||||
|
||||
const ankiDeck = $('.anki-deck');
|
||||
ankiDeck.find('option').remove();
|
||||
yomi.api_getDeckNames({callback: (names) => {
|
||||
yomi.api_getDeckNames({callback: names => {
|
||||
if (names !== null) {
|
||||
names.forEach((name) => ankiDeck.append($('<option/>', {value: name, text: name})));
|
||||
names.forEach(name => ankiDeck.append($('<option/>', {value: name, text: name})));
|
||||
}
|
||||
|
||||
$('#anki-term-deck').val(opts.ankiTermDeck);
|
||||
@ -106,9 +108,9 @@ function populateAnkiDeckAndModel(opts) {
|
||||
|
||||
const ankiModel = $('.anki-model');
|
||||
ankiModel.find('option').remove();
|
||||
yomi.api_getModelNames({callback: (names) => {
|
||||
yomi.api_getModelNames({callback: names => {
|
||||
if (names !== null) {
|
||||
names.forEach((name) => ankiModel.append($('<option/>', {value: name, text: name})));
|
||||
names.forEach(name => ankiModel.append($('<option/>', {value: name, text: name})));
|
||||
}
|
||||
|
||||
populateAnkiFields($('#anki-term-model').val(opts.ankiTermModel), opts);
|
||||
@ -119,7 +121,7 @@ function populateAnkiDeckAndModel(opts) {
|
||||
function updateAnkiStatus() {
|
||||
$('.error-dlg').hide();
|
||||
|
||||
yomichan().api_getVersion({callback: (version) => {
|
||||
yomichan().api_getVersion({callback: version => {
|
||||
if (version === null) {
|
||||
$('.error-dlg-connection').show();
|
||||
$('.options-anki-controls').hide();
|
||||
@ -142,19 +144,19 @@ function populateAnkiFields(element, opts) {
|
||||
const optKey = modelIdToFieldOptKey(modelId);
|
||||
const markers = modelIdToMarkers(modelId);
|
||||
|
||||
yomichan().api_getModelFieldNames({modelName, callback: (names) => {
|
||||
yomichan().api_getModelFieldNames({modelName, callback: names => {
|
||||
const table = element.closest('.tab-pane').find('.anki-fields');
|
||||
table.find('tbody').remove();
|
||||
|
||||
const tbody = $('<tbody>');
|
||||
names.forEach((name) => {
|
||||
names.forEach(name => {
|
||||
const button = $('<button>', {type: 'button', class: 'btn btn-default dropdown-toggle'});
|
||||
button.attr('data-toggle', 'dropdown').dropdown();
|
||||
|
||||
const markerItems = $('<ul>', {class: 'dropdown-menu dropdown-menu-right'});
|
||||
for (const marker of markers) {
|
||||
const link = $('<a>', {href: '#'}).text(`{${marker}}`);
|
||||
link.click((e) => {
|
||||
link.click(e => {
|
||||
e.preventDefault();
|
||||
link.closest('.input-group').find('.anki-field-value').val(link.text()).trigger('change');
|
||||
});
|
||||
@ -185,8 +187,8 @@ function onOptionsGeneralChanged(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
formToOptions('general', (optsNew, optsOld) => {
|
||||
saveOptions(optsNew, () => {
|
||||
formToOptions('general').then(({optsNew, optsOld}) => {
|
||||
saveOptions(optsNew).then(() => {
|
||||
yomichan().setOptions(optsNew);
|
||||
if (!optsOld.enableAnkiConnect && optsNew.enableAnkiConnect) {
|
||||
updateAnkiStatus();
|
||||
@ -210,30 +212,29 @@ function onOptionsAnkiChanged(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
formToOptions('anki', (opts) => {
|
||||
saveOptions(opts, () => yomichan().setOptions(opts));
|
||||
formToOptions('anki').then(({optsNew, optsOld}) => {
|
||||
saveOptions(optsNew).then(() => yomichan().setOptions(optsNew));
|
||||
});
|
||||
}
|
||||
|
||||
function onAnkiModelChanged(e) {
|
||||
if (e.originalEvent) {
|
||||
formToOptions('anki', (opts) => {
|
||||
opts[modelIdToFieldOptKey($(this).id)] = {};
|
||||
populateAnkiFields($(this), opts);
|
||||
saveOptions(opts, () => yomichan().setOptions(opts));
|
||||
formToOptions('anki').then(({optsNew, optsOld}) => {
|
||||
optsNew[modelIdToFieldOptKey($(this).id)] = {};
|
||||
populateAnkiFields($(this), optsNew);
|
||||
saveOptions(optsNew).then(() => yomichan().setOptions(optsNew));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(() => {
|
||||
loadOptions((opts) => {
|
||||
$('#scan-length').val(opts.scanLength);
|
||||
loadOptions().then(opts => {
|
||||
$('#activate-on-startup').prop('checked', opts.activateOnStartup);
|
||||
$('#load-enamdict').prop('checked', opts.loadEnamDict);
|
||||
$('#select-matched-text').prop('checked', opts.selectMatchedText);
|
||||
$('#show-advanced-options').prop('checked', opts.showAdvancedOptions);
|
||||
$('#enable-audio-playback').prop('checked', opts.enableAudioPlayback);
|
||||
$('#enable-anki-connect').prop('checked', opts.enableAnkiConnect);
|
||||
$('#show-advanced-options').prop('checked', opts.showAdvancedOptions);
|
||||
$('#scan-length').val(opts.scanLength);
|
||||
|
||||
$('#anki-card-tags').val(opts.ankiCardTags.join(' '));
|
||||
$('#sentence-extent').val(opts.sentenceExtent);
|
||||
|
@ -19,15 +19,16 @@
|
||||
|
||||
function sanitizeOptions(options) {
|
||||
const defaults = {
|
||||
scanLength: 20,
|
||||
activateOnStartup: false,
|
||||
activateOnStartup: true,
|
||||
selectMatchedText: true,
|
||||
showAdvancedOptions: false,
|
||||
loadEnamDict: false,
|
||||
enableAudioPlayback: true,
|
||||
enableAnkiConnect: false,
|
||||
showAdvancedOptions: false,
|
||||
scanLength: 20,
|
||||
|
||||
ankiCardTags: ['yomichan'],
|
||||
sentenceExtent: 200,
|
||||
|
||||
ankiTermDeck: '',
|
||||
ankiTermModel: '',
|
||||
ankiTermFields: {},
|
||||
@ -45,10 +46,16 @@ function sanitizeOptions(options) {
|
||||
return options;
|
||||
}
|
||||
|
||||
function loadOptions(callback) {
|
||||
chrome.storage.sync.get(null, (items) => callback(sanitizeOptions(items)));
|
||||
function loadOptions() {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.storage.sync.get(null, opts => {
|
||||
resolve(sanitizeOptions(opts));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function saveOptions(opts, callback) {
|
||||
chrome.storage.sync.set(sanitizeOptions(opts), callback);
|
||||
function saveOptions(opts) {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.storage.sync.set(sanitizeOptions(opts), resolve);
|
||||
});
|
||||
}
|
||||
|
@ -25,64 +25,91 @@ class Translator {
|
||||
this.deinflector = new Deinflector();
|
||||
}
|
||||
|
||||
loadData({loadEnamDict=true}, callback) {
|
||||
loadData(callback) {
|
||||
if (this.loaded) {
|
||||
callback();
|
||||
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;
|
||||
}
|
||||
|
||||
Translator.loadData('bg/data/rules.json')
|
||||
.then((response) => {
|
||||
this.deinflector.setRules(JSON.parse(response));
|
||||
return Translator.loadData('bg/data/tags.json');
|
||||
})
|
||||
.then((response) => {
|
||||
this.tagMeta = JSON.parse(response);
|
||||
return Translator.loadData('bg/data/edict.json');
|
||||
})
|
||||
.then((response) => {
|
||||
this.dictionary.addTermDict('edict', JSON.parse(response));
|
||||
return Translator.loadData('bg/data/kanjidic.json');
|
||||
})
|
||||
.then((response) => {
|
||||
this.dictionary.addKanjiDict('kanjidic', JSON.parse(response));
|
||||
return loadEnamDict ? Translator.loadData('bg/data/enamdict.json') : Promise.resolve(null);
|
||||
})
|
||||
.then((response) => {
|
||||
if (response !== null) {
|
||||
this.dictionary.addTermDict('enamdict', JSON.parse(response));
|
||||
if (callback) {
|
||||
callback({state: 'begin', progress: 0});
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
callback();
|
||||
const banks = {};
|
||||
const bankCallback = (loaded, total, indexUrl) => {
|
||||
banks[indexUrl] = {loaded, total};
|
||||
|
||||
let percent = 0.0;
|
||||
for (const url in banks) {
|
||||
percent += banks[url].loaded / banks[url].total;
|
||||
}
|
||||
|
||||
percent /= 3.0;
|
||||
|
||||
if (callback) {
|
||||
callback({state: 'update', progress: Math.ceil(100.0 * percent)});
|
||||
}
|
||||
};
|
||||
|
||||
return Promise.all([
|
||||
this.dictionary.importTermDict('bg/data/edict/index.json', bankCallback),
|
||||
this.dictionary.importTermDict('bg/data/enamdict/index.json', bankCallback),
|
||||
this.dictionary.importKanjiDict('bg/data/kanjidic/index.json', bankCallback),
|
||||
]).then(() => {
|
||||
return this.dictionary.sealDb();
|
||||
}).then(() => {
|
||||
if (callback) {
|
||||
callback({state: 'end', progress: 100.0});
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const df of dfs) {
|
||||
this.processTerm(groups, df.source, df.tags, df.rules, df.root);
|
||||
}
|
||||
}
|
||||
|
||||
return this.findTermGroups(text).then(deinflectGroups => {
|
||||
let definitions = [];
|
||||
for (const key in groups) {
|
||||
definitions.push(groups[key]);
|
||||
for (const key in deinflectGroups) {
|
||||
definitions.push(deinflectGroups[key]);
|
||||
}
|
||||
|
||||
definitions = definitions.sort((v1, v2) => {
|
||||
@ -118,32 +145,34 @@ class Translator {
|
||||
length = Math.max(length, result.source.length);
|
||||
}
|
||||
|
||||
return {definitions: definitions, length: length};
|
||||
return {definitions, length};
|
||||
});
|
||||
}
|
||||
|
||||
findKanji(text) {
|
||||
let definitions = [];
|
||||
const processed = {};
|
||||
const promises = [];
|
||||
|
||||
for (const c of text) {
|
||||
if (!processed[c]) {
|
||||
definitions = definitions.concat(this.dictionary.findKanji(c));
|
||||
promises.push(this.dictionary.findKanji(c).then((definitions) => definitions));
|
||||
processed[c] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return this.processKanji(definitions);
|
||||
return Promise.all(promises).then(sets => this.processKanji(sets.reduce((a, b) => a.concat(b))));
|
||||
}
|
||||
|
||||
processTerm(groups, source, tags, rules, root) {
|
||||
for (const entry of this.dictionary.findTerm(root)) {
|
||||
if (entry.id in groups) {
|
||||
return this.dictionary.findTerm(root).then(definitions => {
|
||||
for (const definition of definitions) {
|
||||
if (definition.id in groups) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let matched = tags.length === 0;
|
||||
for (const tag of tags) {
|
||||
if (entry.tags.indexOf(tag) !== -1) {
|
||||
if (definition.tags.includes(tag)) {
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
@ -154,13 +183,13 @@ class Translator {
|
||||
}
|
||||
|
||||
const tagItems = [];
|
||||
for (const tag of entry.tags) {
|
||||
for (const tag of definition.tags) {
|
||||
const tagItem = {
|
||||
name: tag,
|
||||
class: 'default',
|
||||
order: Number.MAX_SAFE_INTEGER,
|
||||
score: 0,
|
||||
desc: entry.entities[tag] || '',
|
||||
desc: definition.entities[tag] || '',
|
||||
};
|
||||
|
||||
this.applyTagMeta(tagItem);
|
||||
@ -172,24 +201,23 @@ class Translator {
|
||||
score += tagItem.score;
|
||||
}
|
||||
|
||||
groups[entry.id] = {
|
||||
groups[definition.id] = {
|
||||
score,
|
||||
source,
|
||||
rules,
|
||||
expression: entry.expression,
|
||||
reading: entry.reading,
|
||||
glossary: entry.glossary,
|
||||
expression: definition.expression,
|
||||
reading: definition.reading,
|
||||
glossary: definition.glossary,
|
||||
tags: Translator.sortTags(tagItems)
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
processKanji(entries) {
|
||||
const results = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
processKanji(definitions) {
|
||||
for (const definition of definitions) {
|
||||
const tagItems = [];
|
||||
for (const tag of entry.tags) {
|
||||
for (const tag of definition.tags) {
|
||||
const tagItem = {
|
||||
name: tag,
|
||||
class: 'default',
|
||||
@ -201,16 +229,10 @@ class Translator {
|
||||
tagItems.push(tagItem);
|
||||
}
|
||||
|
||||
results.push({
|
||||
character: entry.character,
|
||||
kunyomi: entry.kunyomi,
|
||||
onyomi: entry.onyomi,
|
||||
glossary: entry.glossary,
|
||||
tags: Translator.sortTags(tagItems)
|
||||
});
|
||||
definition.tags = Translator.sortTags(tagItems);
|
||||
}
|
||||
|
||||
return results;
|
||||
return definitions;
|
||||
}
|
||||
|
||||
applyTagMeta(tag) {
|
||||
@ -241,18 +263,4 @@ class Translator {
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
static isKanji(c) {
|
||||
const code = c.charCodeAt(0);
|
||||
return code >= 0x4e00 && code < 0x9fb0 || code >= 0x3400 && code < 0x4dc0;
|
||||
}
|
||||
|
||||
static loadData(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.addEventListener('load', () => resolve(xhr.responseText));
|
||||
xhr.open('GET', chrome.extension.getURL(url), true);
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
46
ext/bg/js/util.js
Normal 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;
|
||||
}
|
||||
|
@ -20,31 +20,20 @@
|
||||
class Yomichan {
|
||||
constructor() {
|
||||
Handlebars.partials = Handlebars.templates;
|
||||
Handlebars.registerHelper('kanjiLinks', function(options) {
|
||||
let result = '';
|
||||
for (const c of options.fn(this)) {
|
||||
if (Translator.isKanji(c)) {
|
||||
result += Handlebars.templates['kanji-link.html']({kanji: c}).trim();
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
Handlebars.registerHelper('kanjiLinks', kanjiLinks);
|
||||
|
||||
this.translator = new Translator();
|
||||
this.importTabId = null;
|
||||
this.asyncPools = {};
|
||||
this.ankiConnectVer = 0;
|
||||
this.setState('disabled');
|
||||
|
||||
chrome.runtime.onInstalled.addListener(this.onInstalled.bind(this));
|
||||
chrome.runtime.onMessage.addListener(this.onMessage.bind(this));
|
||||
chrome.browserAction.onClicked.addListener(this.onBrowserAction.bind(this));
|
||||
chrome.tabs.onCreated.addListener((tab) => this.onTabReady(tab.id));
|
||||
chrome.tabs.onCreated.addListener(tab => this.onTabReady(tab.id));
|
||||
chrome.tabs.onUpdated.addListener(this.onTabReady.bind(this));
|
||||
|
||||
loadOptions((opts) => {
|
||||
loadOptions().then(opts => {
|
||||
this.setOptions(opts);
|
||||
if (this.options.activateOnStartup) {
|
||||
this.setState('loading');
|
||||
@ -52,9 +41,17 @@ class Yomichan {
|
||||
});
|
||||
}
|
||||
|
||||
onInstalled(details) {
|
||||
if (details.reason === 'install') {
|
||||
chrome.tabs.create({url: chrome.extension.getURL('bg/guide.html')});
|
||||
onImport({state, progress}) {
|
||||
if (state === 'begin') {
|
||||
chrome.tabs.create({url: chrome.extension.getURL('bg/import.html')}, tab => this.importTabId = tab.id);
|
||||
}
|
||||
|
||||
if (this.importTabId !== null) {
|
||||
this.tabInvoke(this.importTabId, 'setProgress', progress);
|
||||
}
|
||||
|
||||
if (state === 'end') {
|
||||
this.importTabId = null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,7 +98,7 @@ class Yomichan {
|
||||
break;
|
||||
case 'loading':
|
||||
chrome.browserAction.setBadgeText({text: '...'});
|
||||
this.translator.loadData({loadEnamDict: this.options.loadEnamDict}, () => this.setState('enabled'));
|
||||
this.translator.loadData(this.onImport.bind(this)).then(() => this.setState('enabled'));
|
||||
break;
|
||||
}
|
||||
|
||||
@ -118,7 +115,7 @@ class Yomichan {
|
||||
}
|
||||
|
||||
tabInvokeAll(action, params) {
|
||||
chrome.tabs.query({}, (tabs) => {
|
||||
chrome.tabs.query({}, tabs => {
|
||||
for (const tab of tabs) {
|
||||
this.tabInvoke(tab.id, action, params);
|
||||
}
|
||||
@ -133,7 +130,7 @@ class Yomichan {
|
||||
if (this.ankiConnectVer === this.getApiVersion()) {
|
||||
this.ankiInvoke(action, params, pool, callback);
|
||||
} else {
|
||||
this.api_getVersion({callback: (version) => {
|
||||
this.api_getVersion({callback: version => {
|
||||
if (version === this.getApiVersion()) {
|
||||
this.ankiConnectVer = version;
|
||||
this.ankiInvoke(action, params, pool, callback);
|
||||
@ -209,7 +206,7 @@ class Yomichan {
|
||||
break;
|
||||
case 'tags':
|
||||
if (definition.tags) {
|
||||
value = definition.tags.map((t) => t.name);
|
||||
value = definition.tags.map(t => t.name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -244,7 +241,7 @@ class Yomichan {
|
||||
};
|
||||
|
||||
for (const name in fields) {
|
||||
if (fields[name].indexOf('{audio}') !== -1) {
|
||||
if (fields[name].includes('{audio}')) {
|
||||
audio.fields.push(name);
|
||||
}
|
||||
}
|
||||
@ -274,7 +271,7 @@ class Yomichan {
|
||||
}
|
||||
}
|
||||
|
||||
this.ankiInvokeSafe('canAddNotes', {notes}, 'notes', (results) => {
|
||||
this.ankiInvokeSafe('canAddNotes', {notes}, 'notes', results => {
|
||||
const states = [];
|
||||
|
||||
if (results !== null) {
|
||||
@ -293,11 +290,11 @@ class Yomichan {
|
||||
}
|
||||
|
||||
api_findKanji({text, callback}) {
|
||||
callback(this.translator.findKanji(text));
|
||||
this.translator.findKanji(text).then(result => callback(result));
|
||||
}
|
||||
|
||||
api_findTerm({text, callback}) {
|
||||
callback(this.translator.findTerm(text));
|
||||
this.translator.findTerm(text).then(result => callback(result));
|
||||
}
|
||||
|
||||
api_getDeckNames({callback}) {
|
||||
|
@ -28,11 +28,6 @@
|
||||
<h3>General Options</h3>
|
||||
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group options-advanced">
|
||||
<label for="scan-length" class="control-label col-sm-2">Scan length</label>
|
||||
<div class="col-sm-10"><input type="number" min="1" id="scan-length" class="form-control"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<div class="checkbox">
|
||||
@ -41,14 +36,6 @@
|
||||
</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="col-sm-offset-2 col-sm-10">
|
||||
<div class="checkbox">
|
||||
@ -73,7 +60,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<div class="checkbox">
|
||||
@ -81,6 +67,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group options-advanced">
|
||||
<label for="scan-length" class="control-label col-sm-2">Scan length</label>
|
||||
<div class="col-sm-10"><input type="number" min="1" id="scan-length" class="form-control"></div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
@ -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
@ -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();
|
@ -17,48 +17,39 @@
|
||||
*/
|
||||
|
||||
|
||||
function invokeApi(action, params, target) {
|
||||
target.postMessage({action, params}, '*');
|
||||
}
|
||||
|
||||
function registerKanjiLinks() {
|
||||
for (const link of Array.from(document.getElementsByClassName('kanji-link'))) {
|
||||
link.addEventListener('click', (e) => {
|
||||
link.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
window.parent.postMessage({action: 'displayKanji', params: e.target.innerHTML}, '*');
|
||||
invokeApi('displayKanji', e.target.innerHTML, window.parent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerAddNoteLinks() {
|
||||
for (const link of Array.from(document.getElementsByClassName('action-add-note'))) {
|
||||
link.addEventListener('click', (e) => {
|
||||
link.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
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() {
|
||||
for (const link of Array.from(document.getElementsByClassName('action-play-audio'))) {
|
||||
link.addEventListener('click', (e) => {
|
||||
link.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
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}) {
|
||||
for (const mode in state) {
|
||||
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);
|
||||
window.addEventListener('message', onMessage);
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
registerKanjiLinks();
|
||||
registerAddNoteLinks();
|
||||
registerAudioLinks();
|
||||
});
|
||||
|
||||
window.addEventListener('message', e => {
|
||||
const {action, params} = e.data, method = window['api_' + action];
|
||||
if (typeof(method) === 'function') {
|
||||
method(params);
|
||||
}
|
||||
});
|
||||
|
@ -68,7 +68,7 @@ class Popup {
|
||||
doc.close();
|
||||
}
|
||||
|
||||
sendMessage(action, params, callback) {
|
||||
invokeApi(action, params) {
|
||||
if (this.popup !== null) {
|
||||
this.popup.contentWindow.postMessage({action, params}, '*');
|
||||
}
|
||||
@ -81,8 +81,8 @@ class Popup {
|
||||
|
||||
this.popup = document.createElement('iframe');
|
||||
this.popup.id = 'yomichan-popup';
|
||||
this.popup.addEventListener('mousedown', (e) => e.stopPropagation());
|
||||
this.popup.addEventListener('scroll', (e) => e.stopPropagation());
|
||||
this.popup.addEventListener('mousedown', e => e.stopPropagation());
|
||||
this.popup.addEventListener('scroll', e => e.stopPropagation());
|
||||
|
||||
document.body.appendChild(this.popup);
|
||||
}
|
||||
|
114
ext/fg/js/util.js
Normal 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();
|
||||
}
|
Before Width: | Height: | Size: 274 B After Width: | Height: | Size: 594 B |
Before Width: | Height: | Size: 107 B After Width: | Height: | Size: 129 B |
Before Width: | Height: | Size: 111 B After Width: | Height: | Size: 135 B |
Before Width: | Height: | Size: 137 B After Width: | Height: | Size: 196 B |
Before Width: | Height: | Size: 147 B After Width: | Height: | Size: 223 B |
3
ext/lib/dexie.min.js
vendored
Normal file
@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Yomichan",
|
||||
"version": "0.97",
|
||||
"version": "0.98",
|
||||
|
||||
"description": "Japanese dictionary with Anki integration",
|
||||
"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-element.js",
|
||||
"fg/js/popup.js",
|
||||
"fg/js/api.js",
|
||||
"fg/js/client.js"
|
||||
"fg/js/util.js",
|
||||
"fg/js/driver.js"
|
||||
],
|
||||
"css": ["fg/css/client.css"]
|
||||
}],
|
||||
|